From be10ae02fda678a1ec0e2da16ae001d2b667996e Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Sat, 28 May 2011 02:09:41 +0800 Subject: [PATCH 1/8] [Bug] Missing API_KEY when getting book details. --- src/calibre/ebooks/metadata/sources/douban.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 70bf01a00e..8f8f5b80c4 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -46,6 +46,8 @@ cover_url = XPath("descendant::atom:link[@rel='image']/attribute::href") def get_details(browser, url, timeout): # {{{ try: + if Douban.DOUBAN_API_KEY and Douban.DOUBAN_API_KEY != '': + url = url + "?apikey=" + Douban.DOUBAN_API_KEY raw = browser.open_novisit(url, timeout=timeout).read() except Exception as e: gc = getattr(e, 'getcode', lambda : -1) From 32bcac2147ffa344fb58bcc97859b9452ee99ff8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 May 2011 22:49:13 -0600 Subject: [PATCH 2/8] When deleting all formats except ..., do not delete if it leaves a book with no formats --- src/calibre/gui2/actions/delete.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 718f0737b3..619a8a1031 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -161,9 +161,12 @@ class DeleteAction(InterfaceAction): continue bfmts = set([x.lower() for x in bfmts.split(',')]) rfmts = bfmts - set(fmts) - for fmt in rfmts: - self.gui.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) + if bfmts - rfmts: + # Do not delete if it will leave the book with no + # formats + for fmt in rfmts: + self.gui.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) self.gui.library_view.model().refresh_ids(ids) self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) From 775c63bd39497eb7c5933b9ddaf91758e4ce20e8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 30 May 2011 22:53:15 -0600 Subject: [PATCH 3/8] ... --- src/calibre/gui2/actions/delete.py | 3 ++- src/calibre/gui2/dialogs/select_formats.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 619a8a1031..43465512e0 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -152,7 +152,8 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted'), ids) + '

'+_('Choose formats not to be deleted.

Note that ' + 'this will never remove all formats from a book.'), ids) if fmts is None: return for id in ids: diff --git a/src/calibre/gui2/dialogs/select_formats.py b/src/calibre/gui2/dialogs/select_formats.py index 5934c8c0f9..aea56ad196 100644 --- a/src/calibre/gui2/dialogs/select_formats.py +++ b/src/calibre/gui2/dialogs/select_formats.py @@ -44,7 +44,7 @@ class SelectFormats(QDialog): self.setLayout(self._l) self.setWindowTitle(_('Choose formats')) self._m = QLabel(msg) - self._m.setWordWrap = True + self._m.setWordWrap(True) self._l.addWidget(self._m) self.formats = Formats(fmt_list) self.fview = QListView(self) From cdeb8ade3d36fc14bf2f84fbd774c617b4e277ae Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 31 May 2011 12:13:08 +0100 Subject: [PATCH 4/8] Yet another improvement on the color wizard. Make 'None' compare to zero in the formatter function 'cmp'. --- .../gui2/dialogs/template_line_editor.py | 327 ++++++++++-------- src/calibre/utils/formatter_functions.py | 4 +- 2 files changed, 187 insertions(+), 144 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index b24328f0ad..6a0b07200e 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -46,8 +46,8 @@ class TemplateLineEditor(QLineEdit): menu.exec_(event.globalPos()) def clear_field(self): - self.setText('') self.txt = None + self.setText('') self.setReadOnly(False) self.setStyleSheet('TemplateLineEditor { color: black }') @@ -95,6 +95,39 @@ class TemplateLineEditor(QLineEdit): class TagWizard(QDialog): + text_template = " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')" + text_empty_template = " test(field('{f}'), '{fv}', '{tv}')" + text_re_template = " contains(field('{f}'), '{v}', '{tv}', '{fv}')" + + templates = { + 'text.mult' : " str_in_list(field('{f}'), '{mult}', '{v}', '{tv}', '{fv}')", + 'text.mult.re' : " in_list(field('{f}'), '{mult}', '^{v}$', '{tv}', '{fv}')", + 'text.mult.empty' : " test(field('{f}'), '{fv}', '{tv}')", + 'text' : text_template, + 'text.re' : text_re_template, + 'text.empty' : text_empty_template, + 'rating' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'rating.empty' : text_empty_template, + 'int' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'int.empty' : text_empty_template, + 'float' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'float.empty' : text_empty_template, + 'bool' : " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'bool.empty' : text_empty_template, + 'series' : text_template, + 'series.re' : text_re_template, + 'series.empty' : text_empty_template, + 'composite' : text_template, + 'composite.re' : text_re_template, + 'composite.empty' : text_empty_template, + 'enumeration' : text_template, + 'enumeration.re' : text_re_template, + 'enumeration.empty' : text_empty_template, + 'comments' : text_template, + 'comments.re' : text_re_template, + 'comments.empty' : text_empty_template, + } + def __init__(self, parent, db, txt, mi): QDialog.__init__(self, parent) self.setWindowTitle(_('Coloring Wizard')) @@ -106,11 +139,19 @@ class TagWizard(QDialog): self.completion_values = defaultdict(dict) for k in db.all_field_keys(): m = db.metadata_for_field(k) - if m['datatype'] in ('text', 'enumeration', 'series') and \ - m['is_category'] and k not in ('identifiers'): + if k.endswith('_index') or ( + m['kind'] == 'field' and m['name'] and + k not in ('ondevice', 'path', 'size', 'sort') and + m['datatype'] not in ('datetime')): self.columns.append(k) + self.completion_values[k]['dt'] = m['datatype'] if m['is_custom']: - self.completion_values[k]['v'] = db.all_custom(m['label']) + if m['datatype'] in ('int', 'float'): + self.completion_values[k]['v'] = [] + elif m['datatype'] == 'bool': + self.completion_values[k]['v'] = [_('Yes'), _('No')] + else: + self.completion_values[k]['v'] = db.all_custom(m['label']) elif k == 'tags': self.completion_values[k]['v'] = db.all_tags() elif k == 'formats': @@ -127,12 +168,15 @@ class TagWizard(QDialog): replace('|', ',') for v in f()] else: self.completion_values[k]['v'] = [v[1] for v in f()] + else: + self.completion_values[k]['v'] = [] if k in self.completion_values: if k == 'authors': - self.completion_values[k]['m'] = None + mult = '&' else: - self.completion_values[k]['m'] = m['is_multiple'] + mult = ',' if m['is_multiple'] == '|' else m['is_multiple'] + self.completion_values[k]['m'] = mult self.columns.sort(key=sort_key) self.columns.insert(0, '') @@ -140,12 +184,12 @@ class TagWizard(QDialog): l = QGridLayout() self.setLayout(l) l.setColumnStretch(2, 10) - l.setColumnMinimumWidth(3, 300) + l.setColumnMinimumWidth(5, 300) h = QLabel(_('And')) h.setToolTip('

' + _('Set this box to indicate that the two conditions must both ' - 'be true to return the "color if value found". For example, you ' + 'be true to use the color. For example, you ' 'can check if two tags are present, if the book has a tag ' 'and a #read custom column is checked, or if a book has ' 'some tag and has a particular format.')) @@ -155,107 +199,106 @@ class TagWizard(QDialog): h.setAlignment(Qt.AlignCenter) l.addWidget(h, 0, 1, 1, 1) - h = QLabel(_('Not')) - h.setToolTip('

' + - _('Set this box to indicate that the value must not match ' - 'to return the "color if value found". For example, you ' - 'can check if a tag does not exist by entering that tag ' - 'and checking this box. You can check if tags are empty by ' - 'checking this box, entering .* (period asterisk) for the text, ' - 'then checking the RE box. The .* regular expression matches ' - 'anything, so if this box is checked, it matches nothing. ' - 'This box is particularly useful when using the AND box.')) + h = QLabel(_('is')) h.setAlignment(Qt.AlignCenter) l.addWidget(h, 0, 2, 1, 1) - h = QLabel(_('Values (see the popup help for more information)')) + h = QLabel(_('not')) + h.setToolTip('

' + + _('Check this box to indicate that the value must not match ' + 'to use the color. For example, you can check if a tag does ' + 'not exist by entering that tag and checking this box.') + '

') + h.setAlignment(Qt.AlignCenter) + l.addWidget(h, 0, 3, 1, 1) + + c = QLabel(_('empty')) + c.setToolTip('

' + + _('Check this box to check if the column is empty') + '

') + l.addWidget(c, 0, 4, 1, 1) + + h = QLabel(_('Values')) h.setAlignment(Qt.AlignCenter) h.setToolTip('

' + _('You can enter more than one value per box, separated by commas. ' - 'The comparison ignores letter case. Special note: you can ' - 'enter at most one author.
' + 'The comparison ignores letter case. Special note: authors are ' + 'separated by ampersands (&).
' 'A value can be a regular expression. Check the box to turn ' 'them on. When using regular expressions, note that the wizard ' 'puts anchors (^ and $) around the expression, so you ' 'must ensure your expression matches from the beginning ' - 'to the end of the column you are checking.
' + 'to the end of the column/value you are checking.
' 'Regular expression examples:') + '

    ' + - _('
  • .* matches anything in the column. No ' - 'empty values are checked, so you don\'t need to worry about ' - 'empty strings
  • ' + _('
  • .* matches anything in the column.
  • ' '
  • A.* matches anything beginning with A
  • ' '
  • .*mystery.* matches anything containing ' 'the word "mystery"
  • ') + '

') - l.addWidget(h , 0, 3, 1, 1) + l.addWidget(h , 0, 5, 1, 1) c = QLabel(_('is RE')) c.setToolTip('

' + _('Check this box if the values box contains regular expressions') + '

') - l.addWidget(c, 0, 4, 1, 1) - - c = QLabel(_('Color if value found')) - c.setToolTip('

' + - _('At least one of the two color boxes must have a value. Leave ' - 'one color box empty if you want the template to use the next ' - 'line in this wizard. If both boxes are filled in, the rest of ' - 'the lines in this wizard will be ignored.') + '

') - l.addWidget(c, 0, 5, 1, 1) - c = QLabel(_('Color if value not found')) - c.setToolTip('

' + - _('This box is usually filled in only on the last test. If it is ' - 'filled in before the last test, then the color for value found box ' - 'must be empty or all the rest of the tests will be ignored.') + '

') l.addWidget(c, 0, 6, 1, 1) - self.andboxes = [] - self.notboxes = [] - self.tagboxes = [] - self.colorboxes = [] - self.nfcolorboxes = [] - self.reboxes = [] - self.colboxes = [] + c = QLabel(_('color')) + c.setAlignment(Qt.AlignCenter) + c.setToolTip('

' + + _('Use this color if the column matches the tests.') + '

') + l.addWidget(c, 0, 7, 1, 1) + + self.andboxes = [] + self.notboxes = [] + self.tagboxes = [] + self.colorboxes = [] + self.reboxes = [] + self.colboxes = [] + self.emptyboxes = [] + self.colors = [unicode(s) for s in list(QColor.colorNames())] self.colors.insert(0, '') + def create_widget(klass, box, layout, row, col, items, + align=Qt.AlignCenter, rowspan=False): + w = klass(self) + if box is not None: + box.append(w) + if rowspan: + layout.addWidget(w, row, col, 2, 1, alignment=Qt.Alignment(align)) + else: + layout.addWidget(w, row, col, 1, 1, alignment=Qt.Alignment(align)) + if items: + w.addItems(items) + return w + maxlines = 10 for i in range(1, maxlines+1): - ab = QCheckBox(self) - self.andboxes.append(ab) - if i != maxlines: - # let the last box float in space - l.addWidget(ab, i, 0, 2, 1) - ab.stateChanged.connect(partial(self.and_box_changed, line=i-1)) - else: - ab.setVisible(False) + w = create_widget(QCheckBox, self.andboxes, l, i, 0, None, rowspan=True) + w.stateChanged.connect(partial(self.and_box_changed, line=i-1)) + if i == maxlines: + # last box is invisible + w.setVisible(False) - w = QComboBox(self) - w.addItems(self.columns) - l.addWidget(w, i, 1, 1, 1) - self.colboxes.append(w) + w = create_widget(QComboBox, self.colboxes, l, i, 1, self.columns) + w.currentIndexChanged[str].connect(partial(self.column_changed, line=i-1)) - nb = QCheckBox(self) - self.notboxes.append(nb) - l.addWidget(nb, i, 2, 1, 1) + w = QLabel(self) + w.setText(_('is')) + l.addWidget(w, i, 2, 1, 1) - tb = MultiCompleteLineEdit(self) - tb.set_separator(', ') - self.tagboxes.append(tb) - l.addWidget(tb, i, 3, 1, 1) - w.currentIndexChanged[str].connect(partial(self.column_changed, valbox=tb)) + create_widget(QCheckBox, self.notboxes, l, i, 3, None) - w = QCheckBox(self) - self.reboxes.append(w) - l.addWidget(w, i, 4, 1, 1) + w = create_widget(QCheckBox, self.emptyboxes, l, i, 4, None) + w.stateChanged.connect(partial(self.empty_box_changed, line=i-1)) - w = QComboBox(self) - w.addItems(self.colors) - self.colorboxes.append(w) - l.addWidget(w, i, 5, 1, 1) + create_widget(MultiCompleteLineEdit, self.tagboxes, l, i, 5, None, align=0) + create_widget(QCheckBox, self.reboxes, l, i, 6, None) + create_widget(QComboBox, self.colorboxes, l, i, 7, self.colors) - w = QComboBox(self) - w.addItems(self.colors) - self.nfcolorboxes.append(w) - l.addWidget(w, i, 6, 1, 1) + w = create_widget(QLabel, None, l, maxlines+1, 5, None) + w.setText(_('If none of the tests match, set the color to')) + self.elsebox = create_widget(QComboBox, None, l, maxlines+1, 7, self.colors) + self.elsebox.setToolTip('

' + + _('If this box contains a color, it will be used if none ' + 'of the above rules match.') + '

') if txt: lines = txt.split('\n')[3:] @@ -263,25 +306,27 @@ class TagWizard(QDialog): for line in lines: if line.startswith('#'): vals = line[1:].split(':|:') + if len(vals) == 1 and line.startswith('#else:'): + try: + self.elsebox.setCurrentIndex(self.elsebox.findText(line[6:])) + except: + pass + continue if len(vals) == 2: t, c = vals - nc = '' - re = False f = 'tags' - a = False - n = False + a = n = e = re = False else: - t,c,f,nc,re,a,n = vals + t,c,f,re,a,n,e = vals try: self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f)) self.colorboxes[i].setCurrentIndex( self.colorboxes[i].findText(c)) - self.nfcolorboxes[i].setCurrentIndex( - self.nfcolorboxes[i].findText(nc)) self.tagboxes[i].setText(t) self.reboxes[i].setChecked(re == '2') self.andboxes[i].setChecked(a == '2') self.notboxes[i].setChecked(n == '2') + self.emptyboxes[i].setChecked(e == '2') i += 1 except: pass @@ -290,13 +335,17 @@ class TagWizard(QDialog): l.addWidget(w, 99, 1, 1, 1) w = self.test_box = QLineEdit(self) w.setReadOnly(True) - l.addWidget(w, 99, 3, 1, 1) + l.addWidget(w, 99, 2, 1, 5) w = QPushButton(_('Test')) - l.addWidget(w, 99, 5, 1, 1) + w.setToolTip('

' + + _('Press this button to see what color this template will ' + 'produce for the book that was selected when you ' + 'entered the preferences dialog.')) + l.addWidget(w, 99, 7, 1, 1) w.clicked.connect(self.preview) bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, parent=self) - l.addWidget(bb, 100, 3, 1, 2) + l.addWidget(bb, 100, 5, 1, 3) bb.accepted.connect(self.accepted) bb.rejected.connect(self.reject) self.template = '' @@ -308,14 +357,22 @@ class TagWizard(QDialog): _('EXCEPTION'), self.mi) self.test_box.setText(t) - def column_changed(self, s, valbox=None): + def column_changed(self, s, line=None): k = unicode(s) if k in self.completion_values: + valbox = self.tagboxes[line] valbox.update_items_cache(self.completion_values[k]['v']) if self.completion_values[k]['m']: valbox.set_separator(', ') else: valbox.set_separator(None) + + dt = self.completion_values[k]['dt'] + if dt in ('int', 'float', 'rating', 'bool'): + self.reboxes[line].setChecked(0) + self.reboxes[line].setEnabled(False) + else: + self.reboxes[line].setEnabled(True) else: valbox.update_items_cache([]) valbox.set_separator(None) @@ -324,59 +381,44 @@ class TagWizard(QDialog): res = ("program:\n#tag wizard -- do not directly edit\n" " first_non_empty(\n") lines = [] - was_and = False - had_line = False + was_and = had_line = False line = 0 - for tb, cb, fb, nfcb, reb, ab, nb in zip( - self.tagboxes, self.colorboxes, self.colboxes, - self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): + for tb, cb, fb, reb, ab, nb, eb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.reboxes, self.andboxes, self.notboxes, self.emptyboxes): f = unicode(fb.currentText()) if not f: continue m = self.completion_values[f]['m'] + dt = self.completion_values[f]['dt'] c = unicode(cb.currentText()).strip() - nfc = unicode(nfcb.currentText()).strip() re = reb.checkState() a = ab.checkState() n = nb.checkState() + e = eb.checkState() line += 1 - if n == 2: - tval = '' - fval = '1' - else: - tval = '1' - fval = '' + tval = '' if n == 2 else '1' + fval = '1' if n == 2 else '' if m: - tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + tags = [t.strip() for t in unicode(tb.text()).split(m) if t.strip()] if re == 2: tags = '$|^'.join(tags) else: - tags = ','.join(tags) + tags = m.join(tags) + if m == '&': + tags = tags.replace(',', '|') else: tags = unicode(tb.text()).strip() - if f == 'authors': - tags.replace(',', '|') - if (tags or f) and not (tags and f and (a == 2 or c)): + if (tags or f) and not ((tags or e) and f and (a == 2 or c)): error_dialog(self, _('Invalid line'), _('Line number {0} is not valid').format(line), show=True, show_copy_button=False) return False - if c not in self.colors: - error_dialog(self, _('Invalid color'), - _('The color {0} is not valid').format(c), - show=True, show_copy_button=False) - return False - if nfc not in self.colors: - error_dialog(self, _('Invalid color'), - _('The color {0} is not valid').format(nfc), - show=True, show_copy_button=False) - return False - if not was_and: if had_line: lines[-1] += ',' @@ -385,57 +427,58 @@ class TagWizard(QDialog): else: lines[-1] += ',' - if re == 2: - if m: - lines.append(" in_list(field('{1}'), ',', '^{0}$', '{2}', '{3}')".\ - format(tags, f, tval, fval)) - else: - lines.append(" contains(field('{1}'), '{0}', '{2}', '{3}')".\ - format(tags, f, tval, fval)) - else: - if m: - lines.append(" str_in_list(field('{1}'), ',', '{0}', '{2}', '{3}')".\ - format(tags, f, tval, fval)) - else: - lines.append(" strcmp(field('{1}'), '{0}', '{3}', '{2}', '{3}')".\ - format(tags, f, tval, fval)) + key = dt + ('.mult' if m else '') + ('.empty' if e else '') + ('.re' if re else '') + template = self.templates[key] + lines.append(template.format(v=tags, f=f, tv=tval, fv=fval, mult=m)) + if a == 2: was_and = True else: was_and = False - lines.append(" ), '{0}', '{1}')".format(c, nfc)) + lines.append(" ), '{0}', '')".format(c)) res += '\n'.join(lines) + else_txt = unicode(self.elsebox.currentText()) + if else_txt: + res += ",\n '" + else_txt + "'" res += ')\n' self.template = res res = '' - for tb, cb, fb, nfcb, reb, ab, nb in zip( - self.tagboxes, self.colorboxes, self.colboxes, - self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): + for tb, cb, fb, reb, ab, nb, eb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.reboxes, self.andboxes, self.notboxes, self.emptyboxes): t = unicode(tb.text()).strip() if t.endswith(','): t = t[:-1] c = unicode(cb.currentText()).strip() f = unicode(fb.currentText()) - nfc = unicode(nfcb.currentText()).strip() re = unicode(reb.checkState()) a = unicode(ab.checkState()) n = unicode(nb.checkState()) - if f and t and (a == '2' or c): - res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + \ - nfc + ':|:' + re + ':|:' + a + ':|:' + n + '\n' + e = unicode(eb.checkState()) + if f and (t or e) and (a == '2' or c): + res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + re + ':|:' + \ + a + ':|:' + n + ':|:' + e + '\n' + res += '#else:' + else_txt + '\n' self.template += res return True + def empty_box_changed(self, state, line=None): + if state == 2: + self.tagboxes[line].setText('') + self.tagboxes[line].setEnabled(False) + self.reboxes[line].setChecked(0) + self.reboxes[line].setEnabled(False) + else: + self.reboxes[line].setEnabled(True) + self.tagboxes[line].setEnabled(True) + def and_box_changed(self, state, line=None): if state == 2: self.colorboxes[line].setCurrentIndex(0) self.colorboxes[line].setEnabled(False) - self.nfcolorboxes[line].setCurrentIndex(0) - self.nfcolorboxes[line].setEnabled(False) else: self.colorboxes[line].setEnabled(True) - self.nfcolorboxes[line].setEnabled(True) def accepted(self): if self.generate_program(): diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index b66aec2cb9..7b9a2ac51c 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -106,8 +106,8 @@ class BuiltinCmp(BuiltinFormatterFunction): 'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt.') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): - x = float(x if x else 0) - y = float(y if y else 0) + x = float(x if x and x != 'None' else 0) + y = float(y if y and y != 'None' else 0) if x < y: return lt if x == y: From f4affb57e3479d5ba811442a4c0654591966e2a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 May 2011 09:12:40 -0600 Subject: [PATCH 5/8] Driver for Motorola Defy --- src/calibre/devices/android/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 1cdf394c24..3c6ea243e2 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -109,7 +109,8 @@ class ANDROID(USBMS): 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2', - 'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK'] + 'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK', + 'MB525'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD'] From 37ddce40741d4249efcc047ab28089cacee77ec3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 May 2011 10:40:27 -0600 Subject: [PATCH 6/8] Improve documentation of template functions --- src/calibre/manual/template_lang.rst | 37 +--- src/calibre/manual/template_ref.rst | 266 +++++++++++++++++++++++ src/calibre/utils/formatter_functions.py | 87 ++++---- 3 files changed, 320 insertions(+), 70 deletions(-) create mode 100644 src/calibre/manual/template_ref.rst diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index ef44b0a5c9..079af59286 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -268,20 +268,14 @@ The following functions are available in addition to those described in single-f * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value. -Function classification summary: +Function classification +--------------------------- + +.. toctree:: + :maxdepth: 3 + + template_ref - * Get values from metadata: ``field``. ``raw_field``. In some situations, ``lookup`` can be used in place of ``field``. - * Arithmetic: ``add``, ``subtract``, ``multiply``, ``divide`` - * Boolean: ``and``, ``or``, ``not``. The function ``if_empty`` is similar to ``and`` called with one argument. - * If-then-else: ``contains``, ``test`` - * Iterating over values: ``first_non_empty``, ``lookup``, ``switch`` - * List lookup: ``in_list``, ``list_item``, ``select``, ``str_in_list`` - * List manipulation: ``count``, ``merge_lists``, ``sublist``, ``subitems`` - * Recursion: ``eval``, ``template`` - * Relational: ``cmp`` (for numbers), ``strcmp`` (for strings) - * String case changes: ``lowercase``, ``uppercase``, ``titlecase``, ``capitalize`` - * String manipulation: ``re``, ``shorten``, ``substr`` - * Other: ``assign``, ``booksize``, ``format_date``, ``ondevice`` ``print`` .. _general_mode: @@ -425,20 +419,9 @@ You might find the following tips useful. * Templates can use other templates by referencing a composite custom column. * In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{null}``. This template will always evaluate to an empty string. * The technique described above to show numbers even if they have a zero value works with the standard field series_index. - -API of the Metadata objects ----------------------------- -.. module:: calibre.ebooks.metadata.book.base +.. toctree:: + :hidden: -.. autoclass:: Metadata - :members: - :member-order: bysource - -.. data:: STANDARD_METADATA_FIELDS - - The set of standard metadata fields. - -.. literalinclude:: ../ebooks/metadata/book/__init__.py - :lines: 7- + template_ref diff --git a/src/calibre/manual/template_ref.rst b/src/calibre/manual/template_ref.rst new file mode 100644 index 0000000000..670a7ba791 --- /dev/null +++ b/src/calibre/manual/template_ref.rst @@ -0,0 +1,266 @@ +.. include:: global.rst + +.. _templaterefcalibre: + +Reference for all builtin template language functions +======================================================== + +Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type. + +.. contents:: + :depth: 2 + :local: + +.. module:: calibre.utils.formatter_functions + +Get values from metadata +-------------------------- + +field(name) +^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinField + +raw_field(name) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinRaw_field + +booksize() +^^^^^^^^^^^^ + +.. autoclass:: BuiltinBooksize + +format_date(val, format_string) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinFormat_date + +ondevice() +^^^^^^^^^^^ + +.. autoclass:: BuiltinOndevice + +Arithmetic +------------- + +add(x, y) +^^^^^^^^^^^^^ +.. autoclass:: BuiltinAdd + +subtract(x, y) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSubtract + +multiply(x, y) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinMultiply + +divide(x, y) +^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinDivide + +Boolean +------------ + +and(value1, value2, ...) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinAnd + +or(value1, value2, ...) +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinOr + +not(value) +^^^^^^^^^^^^^ + +.. autoclass:: BuiltinNot + +If-then-else +----------------- + +contains(val, pattern, text if match, text if not match) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinContains + +test(val, text if not empty, text if empty) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinTest + +ifempty(val, text if empty) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinIfempty + +Iterating over values +------------------------ + +first_non_empty(value, value, ...) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinFirstNonEmpty + +lookup(val, pattern, field, pattern, field, ..., else_field) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinLookup + +switch(val, pattern, value, pattern, value, ..., else_value) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSwitch + +List Lookup +--------------- + +in_list(val, separator, pattern, found_val, not_found_val) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinInList + +str_in_list(val, separator, string, found_val, not_found_val) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinStrInList + +list_item(val, index, separator) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinListitem + +select(val, key) +^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSelect + + +List Manipulation +------------------- + +count(val, separator) +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinCount + +merge_lists(list1, list2, separator) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinMergeLists + +sublist(val, start_index, end_index, separator) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSublist + +subitems(val, start_index, end_index) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSubitems + +Recursion +------------- + +eval(template) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinEval + +template(x) +^^^^^^^^^^^^ + +.. autoclass:: BuiltinTemplate + +Relational +----------- + +cmp(x, y, lt, eq, gt) +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinCmp + +strcmp(x, y, lt, eq, gt) +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinStrcmp + +String case changes +--------------------- + +lowercase(val) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinLowercase + +uppercase(val) +^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinUppercase + +titlecase(val) +^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinTitlecase + +capitalize(val) +^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinCapitalize + +String Manipulation +--------------------- + +re(val, pattern, replacement) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinRe + +shorten(val, left chars, middle text, right chars) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinShorten + +substr(str, start, end) +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinSubstr + + +Other +-------- + +assign(id, val) +^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinAssign + +print(a, b, ...) +^^^^^^^^^^^^^^^^^ + +.. autoclass:: BuiltinPrint + + +API of the Metadata objects +---------------------------- + +The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions. + +.. module:: calibre.ebooks.metadata.book.base + +.. autoclass:: Metadata + :members: + :member-order: bysource + +.. data:: STANDARD_METADATA_FIELDS + + The set of standard metadata fields. + +.. literalinclude:: ../ebooks/metadata/book/__init__.py + :lines: 7- + diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7b9a2ac51c..62764510e9 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -87,7 +87,7 @@ class BuiltinFormatterFunction(FormatterFunction): class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 - doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' + __doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt.') @@ -102,7 +102,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction): class BuiltinCmp(BuiltinFormatterFunction): name = 'cmp' arg_count = 5 - doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to ' + __doc__ = doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to ' 'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt.') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): @@ -117,7 +117,7 @@ class BuiltinCmp(BuiltinFormatterFunction): class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 - doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a ' + __doc__ = doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a ' 'string formed by concatenating all the arguments') def evaluate(self, formatter, kwargs, mi, locals, *args): @@ -130,7 +130,7 @@ class BuiltinStrcat(BuiltinFormatterFunction): class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = 2 - doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.') + __doc__ = doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x else 0) @@ -140,7 +140,7 @@ class BuiltinAdd(BuiltinFormatterFunction): class BuiltinSubtract(BuiltinFormatterFunction): name = 'subtract' arg_count = 2 - doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.') + __doc__ = doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x else 0) @@ -150,7 +150,7 @@ class BuiltinSubtract(BuiltinFormatterFunction): class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = 2 - doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.') + __doc__ = doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x else 0) @@ -160,7 +160,7 @@ class BuiltinMultiply(BuiltinFormatterFunction): class BuiltinDivide(BuiltinFormatterFunction): name = 'divide' arg_count = 2 - doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.') + __doc__ = doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x else 0) @@ -170,7 +170,7 @@ class BuiltinDivide(BuiltinFormatterFunction): class BuiltinTemplate(BuiltinFormatterFunction): name = 'template' arg_count = 1 - doc = _('template(x) -- evaluates x as a template. The evaluation is done ' + __doc__ = doc = _('template(x) -- evaluates x as a template. The evaluation is done ' 'in its own context, meaning that variables are not shared between ' 'the caller and the template evaluation. Because the { and } ' 'characters are special, you must use [[ for the { character and ' @@ -185,7 +185,7 @@ class BuiltinTemplate(BuiltinFormatterFunction): class BuiltinEval(BuiltinFormatterFunction): name = 'eval' arg_count = 1 - doc = _('eval(template) -- evaluates the template, passing the local ' + __doc__ = doc = _('eval(template) -- evaluates the template, passing the local ' 'variables (those \'assign\'ed to) instead of the book metadata. ' ' This permits using the template processor to construct complex ' 'results from local variables.') @@ -198,7 +198,7 @@ class BuiltinEval(BuiltinFormatterFunction): class BuiltinAssign(BuiltinFormatterFunction): name = 'assign' arg_count = 2 - doc = _('assign(id, val) -- assigns val to id, then returns val. ' + __doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. ' 'id must be an identifier, not an expression') def evaluate(self, formatter, kwargs, mi, locals, target, value): @@ -208,7 +208,7 @@ class BuiltinAssign(BuiltinFormatterFunction): class BuiltinPrint(BuiltinFormatterFunction): name = 'print' arg_count = -1 - doc = _('print(a, b, ...) -- prints the arguments to standard output. ' + __doc__ = doc = _('print(a, b, ...) -- prints the arguments to standard output. ' 'Unless you start calibre from the command line (calibre-debug -g), ' 'the output will go to a black hole.') @@ -219,7 +219,7 @@ class BuiltinPrint(BuiltinFormatterFunction): class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 - doc = _('field(name) -- returns the metadata field named by name') + __doc__ = doc = _('field(name) -- returns the metadata field named by name') def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs) @@ -227,7 +227,7 @@ class BuiltinField(BuiltinFormatterFunction): class BuiltinRaw_field(BuiltinFormatterFunction): name = 'raw_field' arg_count = 1 - doc = _('raw_field(name) -- returns the metadata field named by name ' + __doc__ = doc = _('raw_field(name) -- returns the metadata field named by name ' 'without applying any formatting.') def evaluate(self, formatter, kwargs, mi, locals, name): @@ -236,7 +236,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction): class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 - doc = _('substr(str, start, end) -- returns the start\'th through the end\'th ' + __doc__ = doc = _('substr(str, start, end) -- returns the start\'th through the end\'th ' 'characters of str. The first character in str is the zero\'th ' 'character. If end is negative, then it indicates that many ' 'characters counting from the right. If end is zero, then it ' @@ -249,7 +249,7 @@ class BuiltinSubstr(BuiltinFormatterFunction): class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 - doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- ' + __doc__ = doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- ' 'like switch, except the arguments are field (metadata) names, not ' 'text. The value of the appropriate field will be fetched and used. ' 'Note that because composite columns are fields, you can use this ' @@ -276,7 +276,7 @@ class BuiltinLookup(BuiltinFormatterFunction): class BuiltinTest(BuiltinFormatterFunction): name = 'test' arg_count = 3 - doc = _('test(val, text if not empty, text if empty) -- return `text if not ' + __doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not ' 'empty` if the field is not empty, otherwise return `text if empty`') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set): @@ -288,7 +288,7 @@ class BuiltinTest(BuiltinFormatterFunction): class BuiltinContains(BuiltinFormatterFunction): name = 'contains' arg_count = 4 - doc = _('contains(val, pattern, text if match, text if not match) -- checks ' + __doc__ = doc = _('contains(val, pattern, text if match, text if not match) -- checks ' 'if field contains matches for the regular expression `pattern`. ' 'Returns `text if match` if matches are found, otherwise it returns ' '`text if no match`') @@ -303,7 +303,7 @@ class BuiltinContains(BuiltinFormatterFunction): class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 - doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- ' + __doc__ = doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- ' 'for each `pattern, value` pair, checks if the field matches ' 'the regular expression `pattern` and if so, returns that ' '`value`. If no pattern matches, then else_value is returned. ' @@ -323,7 +323,7 @@ class BuiltinSwitch(BuiltinFormatterFunction): class BuiltinInList(BuiltinFormatterFunction): name = 'in_list' arg_count = 5 - doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- ' + __doc__ = doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- ' 'treat val as a list of items separated by separator, ' 'comparing the pattern against each value in the list. If the ' 'pattern matches a value, return found_val, otherwise return ' @@ -340,7 +340,7 @@ class BuiltinInList(BuiltinFormatterFunction): class BuiltinStrInList(BuiltinFormatterFunction): name = 'str_in_list' arg_count = 5 - doc = _('str_in_list(val, separator, string, found_val, not_found_val) -- ' + __doc__ = doc = _('str_in_list(val, separator, string, found_val, not_found_val) -- ' 'treat val as a list of items separated by separator, ' 'comparing the string against each value in the list. If the ' 'string matches a value, return found_val, otherwise return ' @@ -360,7 +360,7 @@ class BuiltinStrInList(BuiltinFormatterFunction): class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 - doc = _('re(val, pattern, replacement) -- return the field after applying ' + __doc__ = doc = _('re(val, pattern, replacement) -- return the field after applying ' 'the regular expression. All instances of `pattern` are replaced ' 'with `replacement`. As in all of calibre, these are ' 'python-compatible regular expressions') @@ -371,7 +371,7 @@ class BuiltinRe(BuiltinFormatterFunction): class BuiltinIfempty(BuiltinFormatterFunction): name = 'ifempty' arg_count = 2 - doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' + __doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' 'otherwise return `text if empty`') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty): @@ -383,7 +383,7 @@ class BuiltinIfempty(BuiltinFormatterFunction): class BuiltinShorten(BuiltinFormatterFunction): name = 'shorten' arg_count = 4 - doc = _('shorten(val, left chars, middle text, right chars) -- Return a ' + __doc__ = doc = _('shorten(val, left chars, middle text, right chars) -- Return a ' 'shortened version of the field, consisting of `left chars` ' 'characters from the beginning of the field, followed by ' '`middle text`, followed by `right chars` characters from ' @@ -408,7 +408,7 @@ class BuiltinShorten(BuiltinFormatterFunction): class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 - doc = _('count(val, separator) -- interprets the value as a list of items ' + __doc__ = doc = _('count(val, separator) -- interprets the value as a list of items ' 'separated by `separator`, returning the number of items in the ' 'list. Most lists use a comma as the separator, but authors ' 'uses an ampersand. Examples: {tags:count(,)}, {authors:count(&)}') @@ -419,7 +419,7 @@ class BuiltinCount(BuiltinFormatterFunction): class BuiltinListitem(BuiltinFormatterFunction): name = 'list_item' arg_count = 3 - doc = _('list_item(val, index, separator) -- interpret the value as a list of ' + __doc__ = doc = _('list_item(val, index, separator) -- interpret the value as a list of ' 'items separated by `separator`, returning the `index`th item. ' 'The first item is number zero. The last item can be returned ' 'using `list_item(-1,separator)`. If the item is not in the list, ' @@ -439,7 +439,7 @@ class BuiltinListitem(BuiltinFormatterFunction): class BuiltinSelect(BuiltinFormatterFunction): name = 'select' arg_count = 2 - doc = _('select(val, key) -- interpret the value as a comma-separated list ' + __doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list ' 'of items, with the items being "id:value". Find the pair with the' 'id equal to key, and return the corresponding value.' ) @@ -456,9 +456,9 @@ class BuiltinSelect(BuiltinFormatterFunction): class BuiltinSublist(BuiltinFormatterFunction): name = 'sublist' arg_count = 4 - doc = _('sublist(val, start_index, end_index, separator) -- interpret the ' + __doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the ' 'value as a list of items separated by `separator`, returning a ' - 'new list made from the `start_index`th to the `end_index`th item. ' + 'new list made from the `start_index` to the `end_index` item. ' 'The first item is number zero. If an index is negative, then it ' 'counts from the end of the list. As a special case, an end_index ' 'of zero is assumed to be the length of the list. Examples using ' @@ -466,7 +466,8 @@ class BuiltinSublist(BuiltinFormatterFunction): 'comma-separated) contains "A, B, C": ' '{tags:sublist(0,1,\,)} returns "A". ' '{tags:sublist(-1,0,\,)} returns "C". ' - '{tags:sublist(0,-1,\,)} returns "A, B".') + '{tags:sublist(0,-1,\,)} returns "A, B".' + ) def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep): if not val: @@ -485,12 +486,12 @@ class BuiltinSublist(BuiltinFormatterFunction): class BuiltinSubitems(BuiltinFormatterFunction): name = 'subitems' arg_count = 3 - doc = _('subitems(val, start_index, end_index) -- This function is used to ' + __doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to ' 'break apart lists of items such as genres. It interprets the value ' 'as a comma-separated list of items, where each item is a period-' 'separated list. Returns a new list made by first finding all the ' 'period-separated items, then for each such item extracting the ' - 'start_index`th to the `end_index`th components, then combining ' + 'start_index` to the `end_index` components, then combining ' 'the results back together. The first component in a period-' 'separated list has an index of zero. If an index is negative, ' 'then it counts from the end of the list. As a special case, an ' @@ -522,7 +523,7 @@ class BuiltinSubitems(BuiltinFormatterFunction): class BuiltinFormat_date(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 - doc = _('format_date(val, format_string) -- format the value, which must ' + __doc__ = doc = _('format_date(val, format_string) -- format the value, which must ' 'be a date field, using the format_string, returning a string. ' 'The formatting codes are: ' 'd : the day as number without a leading zero (1 to 31) ' @@ -550,7 +551,7 @@ class BuiltinFormat_date(BuiltinFormatterFunction): class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 - doc = _('uppercase(val) -- return value of the field in upper case') + __doc__ = doc = _('uppercase(val) -- return value of the field in upper case') def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper() @@ -558,7 +559,7 @@ class BuiltinUppercase(BuiltinFormatterFunction): class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 - doc = _('lowercase(val) -- return value of the field in lower case') + __doc__ = doc = _('lowercase(val) -- return value of the field in lower case') def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower() @@ -566,7 +567,7 @@ class BuiltinLowercase(BuiltinFormatterFunction): class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 - doc = _('titlecase(val) -- return value of the field in title case') + __doc__ = doc = _('titlecase(val) -- return value of the field in title case') def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val) @@ -574,7 +575,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction): class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 - doc = _('capitalize(val) -- return value of the field capitalized') + __doc__ = doc = _('capitalize(val) -- return value of the field capitalized') def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) @@ -582,7 +583,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction): class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 - doc = _('booksize() -- return value of the size field') + __doc__ = doc = _('booksize() -- return value of the size field') def evaluate(self, formatter, kwargs, mi, locals): if mi.book_size is not None: @@ -595,7 +596,7 @@ class BuiltinBooksize(BuiltinFormatterFunction): class BuiltinOndevice(BuiltinFormatterFunction): name = 'ondevice' arg_count = 0 - doc = _('ondevice() -- return Yes if ondevice is set, otherwise return ' + __doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return ' 'the empty string') def evaluate(self, formatter, kwargs, mi, locals): @@ -606,7 +607,7 @@ class BuiltinOndevice(BuiltinFormatterFunction): class BuiltinFirstNonEmpty(BuiltinFormatterFunction): name = 'first_non_empty' arg_count = -1 - doc = _('first_non_empty(value, value, ...) -- ' + __doc__ = doc = _('first_non_empty(value, value, ...) -- ' 'returns the first value that is not empty. If all values are ' 'empty, then the empty value is returned.' 'You can have as many values as you want.') @@ -622,7 +623,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction): class BuiltinAnd(BuiltinFormatterFunction): name = 'and' arg_count = -1 - doc = _('and(value, value, ...) -- ' + __doc__ = doc = _('and(value, value, ...) -- ' 'returns the string "1" if all values are not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want.') @@ -638,7 +639,7 @@ class BuiltinAnd(BuiltinFormatterFunction): class BuiltinOr(BuiltinFormatterFunction): name = 'or' arg_count = -1 - doc = _('or(value, value, ...) -- ' + __doc__ = doc = _('or(value, value, ...) -- ' 'returns the string "1" if any value is not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want.') @@ -654,7 +655,7 @@ class BuiltinOr(BuiltinFormatterFunction): class BuiltinNot(BuiltinFormatterFunction): name = 'not' arg_count = 1 - doc = _('not(value) -- ' + __doc__ = doc = _('not(value) -- ' 'returns the string "1" if the value is empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want.') @@ -670,7 +671,7 @@ class BuiltinNot(BuiltinFormatterFunction): class BuiltinMergeLists(BuiltinFormatterFunction): name = 'merge_lists' arg_count = 3 - doc = _('merge_lists(list1, list2, separator) -- ' + __doc__ = doc = _('merge_lists(list1, list2, separator) -- ' 'return a list made by merging the items in list1 and list2, ' 'removing duplicate items using a case-insensitive compare. If ' 'items differ in case, the one in list1 is used. ' From 5c219da8d3bf393d9095975eb5592c10b58ff868 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 May 2011 11:28:37 -0600 Subject: [PATCH 7/8] EPUB Output: Fix crash caused by ids with non-ascii characters in them --- src/calibre/ebooks/oeb/transforms/split.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/transforms/split.py b/src/calibre/ebooks/oeb/transforms/split.py index 69de740ddc..0c9b492855 100644 --- a/src/calibre/ebooks/oeb/transforms/split.py +++ b/src/calibre/ebooks/oeb/transforms/split.py @@ -120,7 +120,19 @@ class Split(object): for i, x in enumerate(page_breaks): x.set('id', x.get('id', 'calibre_pb_%d'%i)) id = x.get('id') - page_breaks_.append((XPath('//*[@id=%r]'%id), + try: + xp = XPath('//*[@id="%s"]'%id) + except: + try: + xp = XPath("//*[@id='%s']"%id) + except: + # The id has both a quote and an apostrophe or some other + # Just replace it since I doubt its going to work anywhere else + # either + id = 'calibre_pb_%d'%i + x.set('id', id) + xp = XPath('//*[@id=%r]'%id) + page_breaks_.append((xp, x.get('pb_before', False))) page_break_ids.append(id) From db0c24624001e7c2a417566e6cf7538483a9cf64 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 May 2011 13:43:38 -0600 Subject: [PATCH 8/8] ... --- src/calibre/ebooks/epub/fix/container.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/epub/fix/container.py b/src/calibre/ebooks/epub/fix/container.py index 1669290a7b..691bf7132a 100644 --- a/src/calibre/ebooks/epub/fix/container.py +++ b/src/calibre/ebooks/epub/fix/container.py @@ -101,7 +101,7 @@ class Container(object): return None return existing[0] - def add_name_to_manifest(self, name): + def add_name_to_manifest(self, name, mt=None): item = self.manifest_item_for_name(name) if item is not None: return @@ -109,11 +109,27 @@ class Container(object): item = manifest.makeelement('{%s}item'%OPF_NS, nsmap={'opf':OPF_NS}, href=self.name_to_href(name, posixpath.dirname(self.opf_name)), id=self.generate_manifest_id()) - mt = guess_type(posixpath.basename(name))[0] + if not mt: + mt = guess_type(posixpath.basename(name))[0] if not mt: mt = 'application/octest-stream' item.set('media-type', mt) manifest.append(item) + self.fix_tail(item) + + def fix_tail(self, item): + ''' + Designed only to work with self closing elements after item has + just been inserted/appended + ''' + parent = item.getparent() + idx = parent.index(item) + if idx == 0: + item.tail = parent.text + else: + item.tail = parent[idx-1].tail + if idx == len(parent)-1: + parent[idx-1].tail = parent.text def generate_manifest_id(self): items = self.opf.xpath('//opf:manifest/opf:item[@id]',