KG updates

This commit is contained in:
GRiker 2011-06-02 03:41:40 -06:00
commit 5987a0eeae
18 changed files with 1083 additions and 584 deletions

View File

@ -4,6 +4,7 @@ src/calibre/plugins
resources/images.qrc resources/images.qrc
src/calibre/manual/.build/ src/calibre/manual/.build/
src/calibre/manual/cli/ src/calibre/manual/cli/
src/calibre/manual/template_ref.rst
build build
dist dist
docs docs
@ -31,4 +32,4 @@ nbproject/
.pydevproject .pydevproject
.settings/ .settings/
*.DS_Store *.DS_Store
calibre_plugins/ calibre_plugins/

View File

@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase):
formats = ['EPUB', 'PDF'] formats = ['EPUB', 'PDF']
affiliate = True affiliate = True
class StoreEpubBudStore(StoreBase):
name = 'ePub Bud'
description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore'
drm_free_only = True
headquarters = 'US'
formats = ['EPUB']
class StoreFeedbooksStore(StoreBase): class StoreFeedbooksStore(StoreBase):
name = 'Feedbooks' name = 'Feedbooks'
description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.' description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.'
@ -1422,6 +1431,7 @@ plugins += [
StoreEBookShoppeUKStore, StoreEBookShoppeUKStore,
StoreEPubBuyDEStore, StoreEPubBuyDEStore,
StoreEHarlequinStore, StoreEHarlequinStore,
StoreEpubBudStore,
StoreFeedbooksStore, StoreFeedbooksStore,
StoreFoylesUKStore, StoreFoylesUKStore,
StoreGandalfStore, StoreGandalfStore,

View File

@ -408,6 +408,8 @@ def identify(log, abort, # {{{
for f in plugin.prefs['ignore_fields']: for f in plugin.prefs['ignore_fields']:
if ':' not in f: if ':' not in f:
setattr(result, f, getattr(dummy, f)) setattr(result, f, getattr(dummy, f))
if f == 'series':
result.series_index = dummy.series_index
result.relevance_in_source = i result.relevance_in_source = i
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
and plugin.get_cached_cover_url(result.identifiers) is not and plugin.get_cached_cover_url(result.identifiers) is not

View File

@ -171,10 +171,9 @@ class ConvertAction(InterfaceAction):
raise Exception(_('Empty output file, ' raise Exception(_('Empty output file, '
'probably the conversion process crashed')) 'probably the conversion process crashed'))
data = open(temp_files[-1].name, 'rb') with open(temp_files[-1].name, 'rb') as data:
self.gui.library_view.model().db.add_format(book_id, \ self.gui.library_view.model().db.add_format(book_id, \
fmt, data, index_is_id=True) fmt, data, index_is_id=True)
data.close()
self.gui.status_bar.show_message(job.description + \ self.gui.status_bar.show_message(job.description + \
(' completed'), 2000) (' completed'), 2000)
finally: finally:

View File

@ -482,6 +482,8 @@ class EditMetadataAction(InterfaceAction):
if mi.identifiers: if mi.identifiers:
idents.update(mi.identifiers) idents.update(mi.identifiers)
mi.identifiers = idents mi.identifiers = idents
if mi.is_null('series'):
mi.series_index = None
db.set_metadata(i, mi, commit=False, set_title=set_title, db.set_metadata(i, mi, commit=False, set_title=set_title,
set_authors=set_authors, notify=False) set_authors=set_authors, notify=False)
self.applied_ids.append(i) self.applied_ids.append(i)

View File

@ -99,8 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.ids_to_highlight_set = set() self.ids_to_highlight_set = set()
self.current_highlighted_idx = None self.current_highlighted_idx = None
self.highlight_only = False self.highlight_only = False
self.column_color_list = [] self.colors = frozenset([unicode(c) for c in QColor.colorNames()])
self.colors = [unicode(c) for c in QColor.colorNames()]
self.read_config() self.read_config()
def change_alignment(self, colname, alignment): def change_alignment(self, colname, alignment):
@ -156,7 +155,6 @@ class BooksModel(QAbstractTableModel): # {{{
self.headers[col] = self.custom_columns[col]['name'] self.headers[col] = self.custom_columns[col]['name']
self.build_data_convertors() self.build_data_convertors()
self.set_color_templates(reset=False)
self.reset() self.reset()
self.database_changed.emit(db) self.database_changed.emit(db)
self.stop_metadata_backup() self.stop_metadata_backup()
@ -545,16 +543,6 @@ class BooksModel(QAbstractTableModel): # {{{
img = self.default_image img = self.default_image
return img return img
def set_color_templates(self, reset=True):
self.column_color_list = []
for i in range(1,self.db.column_color_count+1):
name = self.db.prefs.get('column_color_name_'+str(i))
if name:
self.column_color_list.append((name,
self.db.prefs.get('column_color_template_'+str(i))))
if reset:
self.reset()
def build_data_convertors(self): def build_data_convertors(self):
def authors(r, idx=-1): def authors(r, idx=-1):
au = self.db.data[r][idx] au = self.db.data[r][idx]
@ -726,14 +714,16 @@ class BooksModel(QAbstractTableModel): # {{{
return QVariant(QColor('lightgreen')) return QVariant(QColor('lightgreen'))
elif role == Qt.ForegroundRole: elif role == Qt.ForegroundRole:
key = self.column_map[col] key = self.column_map[col]
for k,fmt in self.column_color_list: mi = None
for k, fmt in self.db.prefs['column_color_rules']:
if k != key: if k != key:
continue continue
id_ = self.id(index) id_ = self.id(index)
if id_ in self.color_cache: if id_ in self.color_cache:
if key in self.color_cache[id_]: if key in self.color_cache[id_]:
return self.color_cache[id_][key] return self.color_cache[id_][key]
mi = self.db.get_metadata(self.id(index), index_is_id=True) if mi is None:
mi = self.db.get_metadata(id_, index_is_id=True)
try: try:
color = composite_formatter.safe_format(fmt, mi, '', mi) color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors: if color in self.colors:
@ -743,7 +733,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.color_cache[id_][key] = color self.color_cache[id_][key] = color
return color return color
except: except:
return NONE continue
if self.is_custom_column(key) and \ if self.is_custom_column(key) and \
self.custom_columns[key]['datatype'] == 'enumeration': self.custom_columns[key]['datatype'] == 'enumeration':
cc = self.custom_columns[self.column_map[col]]['display'] cc = self.custom_columns[self.column_map[col]]['display']

View File

@ -35,8 +35,9 @@ from calibre import force_unicode
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None): def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
self.max_width = max_width
def to_doc(self, index): def to_doc(self, index):
doc = QTextDocument() doc = QTextDocument()
@ -46,8 +47,8 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
def sizeHint(self, option, index): def sizeHint(self, option, index):
doc = self.to_doc(index) doc = self.to_doc(index)
ans = doc.size().toSize() ans = doc.size().toSize()
if ans.width() > 150: if ans.width() > self.max_width - 10:
ans.setWidth(160) ans.setWidth(self.max_width)
ans.setHeight(ans.height()+10) ans.setHeight(ans.height()+10)
return ans return ans

View File

@ -0,0 +1,596 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton,
QListView, QAbstractListModel, pyqtSignal)
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.library.coloring import (Rule, conditionable_columns,
displayable_columns, rule_from_template)
class ConditionEditor(QWidget): # {{{
def __init__(self, fm, parent=None):
QWidget.__init__(self, parent)
self.fm = fm
self.action_map = {
'bool' : (
(_('is true'), 'is true',),
(_('is false'), 'is false'),
(_('is undefined'), 'is undefined')
),
'int' : (
(_('is equal to'), 'eq'),
(_('is less than'), 'lt'),
(_('is greater than'), 'gt')
),
'multiple' : (
(_('has'), 'has'),
(_('does not have'), 'does not have'),
(_('has pattern'), 'has pattern'),
(_('does not have pattern'), 'does not have pattern'),
(_('is set'), 'is set'),
(_('is not set'), 'is not set'),
),
'single' : (
(_('is'), 'is'),
(_('is not'), 'is not'),
(_('matches pattern'), 'matches pattern'),
(_('does not match pattern'), 'does not match pattern'),
(_('is set'), 'is set'),
(_('is not set'), 'is not set'),
),
}
for x in ('float', 'rating', 'datetime'):
self.action_map[x] = self.action_map['int']
self.l = l = QGridLayout(self)
self.setLayout(l)
self.l1 = l1 = QLabel(_('If the '))
l.addWidget(l1, 0, 0)
self.column_box = QComboBox(self)
l.addWidget(self.column_box, 0, 1)
self.l2 = l2 = QLabel(_(' column '))
l.addWidget(l2, 0, 2)
self.action_box = QComboBox(self)
l.addWidget(self.action_box, 0, 3)
self.l3 = l3 = QLabel(_(' the value '))
l.addWidget(l3, 0, 4)
self.value_box = QLineEdit(self)
l.addWidget(self.value_box, 0, 5)
self.column_box.addItem('', '')
for key in sorted(
conditionable_columns(fm),
key=lambda x:sort_key(fm[x]['name'])):
self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.column_box.currentIndexChanged.connect(self.init_action_box)
self.action_box.currentIndexChanged.connect(self.init_value_box)
for b in (self.column_box, self.action_box):
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
b.setMinimumContentsLength(15)
@dynamic_property
def current_col(self):
def fget(self):
idx = self.column_box.currentIndex()
return unicode(self.column_box.itemData(idx).toString())
def fset(self, val):
for idx in range(self.column_box.count()):
c = unicode(self.column_box.itemData(idx).toString())
if c == val:
self.column_box.setCurrentIndex(idx)
return
raise ValueError('Column %r not found'%val)
return property(fget=fget, fset=fset)
@dynamic_property
def current_action(self):
def fget(self):
idx = self.action_box.currentIndex()
return unicode(self.action_box.itemData(idx).toString())
def fset(self, val):
for idx in range(self.action_box.count()):
c = unicode(self.action_box.itemData(idx).toString())
if c == val:
self.action_box.setCurrentIndex(idx)
return
raise ValueError('Action %r not valid for current column'%val)
return property(fget=fget, fset=fset)
@property
def current_val(self):
return unicode(self.value_box.text()).strip()
@dynamic_property
def condition(self):
def fget(self):
c, a, v = (self.current_col, self.current_action,
self.current_val)
if not c or not a:
return None
return (c, a, v)
def fset(self, condition):
c, a, v = condition
if not v:
v = ''
v = v.strip()
self.current_col = c
self.current_action = a
self.value_box.setText(v)
return property(fget=fget, fset=fset)
def init_action_box(self):
self.action_box.blockSignals(True)
self.action_box.clear()
self.action_box.addItem('', '')
col = self.current_col
m = self.fm[col]
dt = m['datatype']
if dt in self.action_map:
actions = self.action_map[dt]
else:
k = 'multiple' if m['is_multiple'] else 'single'
actions = self.action_map[k]
for text, key in actions:
self.action_box.addItem(text, key)
self.action_box.setCurrentIndex(0)
self.action_box.blockSignals(False)
self.init_value_box()
def init_value_box(self):
self.value_box.setEnabled(True)
self.value_box.setText('')
self.value_box.setInputMask('')
self.value_box.setValidator(None)
col = self.current_col
m = self.fm[col]
dt = m['datatype']
action = self.current_action
if not col or not action:
return
tt = ''
if dt in ('int', 'float', 'rating'):
tt = _('Enter a number')
v = QIntValidator if dt == 'int' else QDoubleValidator
self.value_box.setValidator(v(self.value_box))
elif dt == 'datetime':
self.value_box.setInputMask('9999-99-99')
tt = _('Enter a date in the format YYYY-MM-DD')
else:
tt = _('Enter a string')
if 'pattern' in action:
tt = _('Enter a regular expression')
self.value_box.setToolTip(tt)
if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'):
self.value_box.setEnabled(False)
# }}}
class RuleEditor(QDialog): # {{{
def __init__(self, fm, parent=None):
QDialog.__init__(self, parent)
self.fm = fm
self.setWindowIcon(QIcon(I('format-fill-color.png')))
self.setWindowTitle(_('Create/edit a column coloring rule'))
self.l = l = QGridLayout(self)
self.setLayout(l)
self.l1 = l1 = QLabel(_('Create a coloring rule by'
' filling in the boxes below'))
l.addWidget(l1, 0, 0, 1, 4)
self.f1 = QFrame(self)
self.f1.setFrameShape(QFrame.HLine)
l.addWidget(self.f1, 1, 0, 1, 4)
self.l2 = l2 = QLabel(_('Set the color of the column:'))
l.addWidget(l2, 2, 0)
self.column_box = QComboBox(self)
l.addWidget(self.column_box, 2, 1)
self.l3 = l3 = QLabel(_('to'))
l3.setAlignment(Qt.AlignHCenter)
l.addWidget(l3, 2, 2)
self.color_box = QComboBox(self)
l.addWidget(self.color_box, 2, 3)
self.l4 = l4 = QLabel(
_('Only if the following conditions are all satisfied:'))
l4.setAlignment(Qt.AlignHCenter)
l.addWidget(l4, 3, 0, 1, 4)
self.scroll_area = sa = QScrollArea(self)
sa.setMinimumHeight(300)
sa.setMinimumWidth(950)
sa.setWidgetResizable(True)
l.addWidget(sa, 4, 0, 1, 4)
self.add_button = b = QPushButton(QIcon(I('plus.png')),
_('Add another condition'))
l.addWidget(b, 5, 0, 1, 4)
b.clicked.connect(self.add_blank_condition)
self.l5 = l5 = QLabel(_('You can disable a condition by'
' blanking all of its boxes'))
l.addWidget(l5, 6, 0, 1, 4)
self.bb = bb = QDialogButtonBox(
QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
l.addWidget(bb, 7, 0, 1, 4)
self.conditions_widget = QWidget(self)
sa.setWidget(self.conditions_widget)
self.conditions_widget.setLayout(QVBoxLayout())
self.conditions_widget.layout().setAlignment(Qt.AlignTop)
self.conditions = []
for b in (self.column_box, self.color_box):
b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon)
b.setMinimumContentsLength(15)
for key in sorted(
displayable_columns(fm),
key=lambda x:sort_key(fm[x]['name'])):
name = fm[key]['name']
if name:
self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.color_box.addItems(QColor.colorNames())
self.color_box.setCurrentIndex(0)
self.resize(self.sizeHint())
def add_blank_condition(self):
c = ConditionEditor(self.fm, parent=self.conditions_widget)
self.conditions.append(c)
self.conditions_widget.layout().addWidget(c)
def apply_rule(self, col, rule):
for i in range(self.column_box.count()):
c = unicode(self.column_box.itemData(i).toString())
if col == c:
self.column_box.setCurrentIndex(i)
break
if rule.color:
idx = self.color_box.findText(rule.color)
if idx >= 0:
self.color_box.setCurrentIndex(idx)
for c in rule.conditions:
ce = ConditionEditor(self.fm, parent=self.conditions_widget)
self.conditions.append(ce)
self.conditions_widget.layout().addWidget(ce)
try:
ce.condition = c
except:
import traceback
traceback.print_exc()
def accept(self):
if self.validate():
QDialog.accept(self)
def validate(self):
r = Rule(self.fm)
for c in self.conditions:
condition = c.condition
if condition is not None:
try:
r.add_condition(*condition)
except Exception as e:
import traceback
error_dialog(self, _('Invalid condition'),
_('One of the conditions for this rule is'
' invalid: <b>%s</b>')%e,
det_msg=traceback.format_exc(), show=True)
return False
if len(r.conditions) < 1:
error_dialog(self, _('No conditions'),
_('You must specify at least one non-empty condition'
' for this rule'), show=True)
return False
return True
@property
def rule(self):
r = Rule(self.fm)
r.color = unicode(self.color_box.currentText())
idx = self.column_box.currentIndex()
col = unicode(self.column_box.itemData(idx).toString())
for c in self.conditions:
condition = c.condition
if condition is not None:
r.add_condition(*condition)
return col, r
# }}}
class RulesModel(QAbstractListModel): # {{{
def __init__(self, prefs, fm, parent=None):
QAbstractListModel.__init__(self, parent)
self.fm = fm
rules = list(prefs['column_color_rules'])
self.rules = []
for col, template in rules:
try:
rule = rule_from_template(self.fm, template)
except:
rule = template
self.rules.append((col, rule))
def rowCount(self, *args):
return len(self.rules)
def data(self, index, role):
row = index.row()
try:
col, rule = self.rules[row]
except:
return None
if role == Qt.DisplayRole:
return self.rule_to_html(col, rule)
if role == Qt.UserRole:
return (col, rule)
def add_rule(self, col, rule):
self.rules.append((col, rule))
self.reset()
return self.index(len(self.rules)-1)
def replace_rule(self, index, col, r):
self.rules[index.row()] = (col, r)
self.dataChanged.emit(index, index)
def remove_rule(self, index):
self.rules.remove(self.rules[index.row()])
self.reset()
def commit(self, prefs):
rules = []
for col, r in self.rules:
if isinstance(r, Rule):
r = r.template
if r is not None:
rules.append((col, r))
prefs['column_color_rules'] = rules
def move(self, idx, delta):
row = idx.row() + delta
if row >= 0 and row < len(self.rules):
t = self.rules[row]
self.rules[row] = self.rules[row-delta]
self.rules[row-delta] = t
self.dataChanged.emit(idx, idx)
idx = self.index(row)
self.dataChanged.emit(idx, idx)
return idx
def clear(self):
self.rules = []
self.reset()
def rule_to_html(self, col, rule):
if isinstance(rule, basestring):
return _('''
<p>Advanced Rule for column: %s
<pre>%s</pre>
''')%(col, rule)
conditions = [self.condition_to_html(c) for c in rule.conditions]
return _('''\
<p>Set the color of <b>%s</b> to <b>%s</b> if the following
conditions are met:</p>
<ul>%s</ul>
''') % (col, rule.color, ''.join(conditions))
def condition_to_html(self, condition):
return (
_('<li>If the <b>%s</b> column <b>%s</b> the value: <b>%s</b>') %
tuple(condition))
# }}}
class EditRules(QWidget): # {{{
changed = pyqtSignal()
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QGridLayout(self)
self.setLayout(l)
self.l1 = l1 = QLabel(_(
'You can control the color of columns in the'
' book list by creating "rules" that tell calibre'
' what color to use. Click the Add Rule button below'
' to get started. You can change an existing rule by double'
' clicking it.'))
l1.setWordWrap(True)
l.addWidget(l1, 0, 0, 1, 2)
self.add_button = QPushButton(QIcon(I('plus.png')), _('Add Rule'),
self)
self.remove_button = QPushButton(QIcon(I('minus.png')),
_('Remove Rule'), self)
self.add_button.clicked.connect(self.add_rule)
self.remove_button.clicked.connect(self.remove_rule)
l.addWidget(self.add_button, 1, 0)
l.addWidget(self.remove_button, 1, 1)
self.g = g = QGridLayout()
self.rules_view = QListView(self)
self.rules_view.doubleClicked.connect(self.edit_rule)
self.rules_view.setSelectionMode(self.rules_view.SingleSelection)
self.rules_view.setAlternatingRowColors(True)
self.rtfd = RichTextDelegate(parent=self.rules_view, max_width=400)
self.rules_view.setItemDelegate(self.rtfd)
g.addWidget(self.rules_view, 0, 0, 2, 1)
self.up_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-up.png')))
b.setToolTip(_('Move the selected rule up'))
b.clicked.connect(self.move_up)
g.addWidget(b, 0, 1, 1, 1, Qt.AlignTop)
self.down_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-down.png')))
b.setToolTip(_('Move the selected rule down'))
b.clicked.connect(self.move_down)
g.addWidget(b, 1, 1, 1, 1, Qt.AlignBottom)
l.addLayout(g, 2, 0, 1, 2)
l.setRowStretch(2, 10)
self.add_advanced_button = b = QPushButton(QIcon(I('plus.png')),
_('Add Advanced Rule'), self)
b.clicked.connect(self.add_advanced)
l.addWidget(b, 3, 0, 1, 2)
def initialize(self, fm, prefs):
self.model = RulesModel(prefs, fm)
self.rules_view.setModel(self.model)
def add_rule(self):
d = RuleEditor(self.model.fm)
d.add_blank_condition()
if d.exec_() == d.Accepted:
col, r = d.rule
if r is not None and col:
idx = self.model.add_rule(col, r)
self.rules_view.scrollTo(idx)
self.changed.emit()
def add_advanced(self):
td = TemplateDialog(self, '', None)
if td.exec_() == td.Accepted:
self.changed.emit()
pass # TODO
def edit_rule(self, index):
try:
col, rule = self.model.data(index, Qt.UserRole)
except:
return
if isinstance(rule, Rule):
d = RuleEditor(self.model.fm)
d.apply_rule(col, rule)
if d.exec_() == d.Accepted:
col, r = d.rule
if r is not None and col:
self.model.replace_rule(index, col, r)
self.rules_view.scrollTo(index)
self.changed.emit()
else:
td = TemplateDialog(self, rule, None)
if td.exec_() == td.Accepted:
self.changed.emit()
pass # TODO
def get_selected_row(self, txt):
sm = self.rules_view.selectionModel()
rows = list(sm.selectedRows())
if not rows:
error_dialog(self, _('No rule selected'),
_('No rule selected for %s.')%txt, show=True)
return None
return rows[0]
def remove_rule(self):
row = self.get_selected_row(_('removal'))
if row is not None:
self.model.remove_rule(row)
self.changed.emit()
def move_up(self):
idx = self.rules_view.currentIndex()
if idx.isValid():
idx = self.model.move(idx, -1)
if idx is not None:
sm = self.rules_view.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.rules_view.setCurrentIndex(idx)
self.changed.emit()
def move_down(self):
idx = self.rules_view.currentIndex()
if idx.isValid():
idx = self.model.move(idx, 1)
if idx is not None:
sm = self.rules_view.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.rules_view.setCurrentIndex(idx)
self.changed.emit()
def clear(self):
self.model.clear()
self.changed.emit()
def commit(self, prefs):
self.model.commit(prefs)
# }}}
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
from calibre.library import db
db = db()
if False:
d = RuleEditor(db.field_metadata)
d.add_blank_condition()
d.exec_()
col, r = d.rule
print ('Column to be colored:', col)
print ('Template:')
print (r.template)
else:
d = EditRules()
d.resize(QSize(800, 600))
d.initialize(db.field_metadata, db.prefs)
d.show()
app.exec_()
d.commit(db.prefs)

View File

@ -5,21 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
QAbstractListModel, Qt, QColor, QIcon, QToolButton, QComboBox) QAbstractListModel, Qt, QIcon)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app from calibre.gui2 import config, gprefs, qt_app
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.utils.localization import (available_translations, from calibre.utils.localization import (available_translations,
get_language, get_lang) get_language, get_lang)
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2 import NONE from calibre.gui2 import NONE
from calibre.gui2.book_details import get_field_list from calibre.gui2.book_details import get_field_list
from calibre.gui2.preferences.coloring import EditRules
class DisplayedFields(QAbstractListModel): # {{{ class DisplayedFields(QAbstractListModel): # {{{
@ -162,117 +160,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.df_up_button.clicked.connect(self.move_df_up) self.df_up_button.clicked.connect(self.move_df_up)
self.df_down_button.clicked.connect(self.move_df_down) self.df_down_button.clicked.connect(self.move_df_down)
self.color_help_text.setText('<p>' + self.edit_rules = EditRules(self.tabWidget)
_('Here you can specify coloring rules for columns shown in the ' self.edit_rules.changed.connect(self.changed_signal)
'library view. Choose the column you wish to color, then ' self.tabWidget.addTab(self.edit_rules,
'supply a template that specifies the color to use based on ' QIcon(I('format-fill-color.png')), _('Column coloring'))
'the values in the column. There is a ' self.tabWidget.setCurrentIndex(0)
'<a href="http://manual.calibre-ebook.com/template_lang.html">'
'tutorial</a> on using templates.') +
'</p><p>' +
_('If you want to color a field based on contents of columns, '
'then click the button next to an empty line to open the wizard. '
'It will build a template for you. You can later edit that '
'template with the same wizard. This is by far the easiest '
'way to specify a template.') +
'</p><p>' +
_('If you manually construct a template, then the template must '
'evaluate to a valid color name shown in the color names box.'
'You can use any legal template expression. '
'For example, you can set the title to always display in '
'green using the template "green" (without the quotes). '
'To show the title in the color named in the custom column '
'#column, use "{#column}". To show the title in blue if the '
'custom column #column contains the value "foo", in red if the '
'column contains the value "bar", otherwise in black, use '
'<pre>{#column:switch(foo,blue,bar,red,black)}</pre>'
'To show the title in blue if the book has the exact tag '
'"Science Fiction", red if the book has the exact tag '
'"Mystery", or black if the book has neither tag, use'
"<pre>program: \n"
" t = field('tags'); \n"
" first_non_empty(\n"
" in_list(t, ',', '^Science Fiction$', 'blue', ''), \n"
" in_list(t, ',', '^Mystery$', 'red', 'black'))</pre>"
'To show the title in green if it has one format, blue if it '
'two formats, and red if more, use'
"<pre>program:cmp(count(field('formats'),','), 2, 'green', 'blue', 'red')</pre>") +
'</p><p>' +
_('You can access a multi-line template editor from the '
'context menu (right-click).') + '</p><p>' +
_('<b>Note:</b> if you want to color a "custom column with a fixed set '
'of values", it is often easier to specify the '
'colors in the column definition dialog. There you can '
'provide a color for each value without using a template.')+ '</p>')
self.color_help_scrollArea.setVisible(False)
self.color_help_button.clicked.connect(self.change_help_text)
self.colors_scrollArea.setVisible(False)
self.colors_label.setVisible(False)
self.colors_button.clicked.connect(self.change_colors_text)
choices = db.field_metadata.displayable_field_keys()
choices.sort(key=sort_key)
choices.insert(0, '')
self.column_color_count = db.column_color_count+1
mi=None
try:
idx = gui.library_view.currentIndex().row()
mi = db.get_metadata(idx, index_is_id=False)
except:
pass
l = self.column_color_layout
for i in range(1, self.column_color_count):
ccn = QComboBox(parent=self)
setattr(self, 'opt_column_color_name_'+str(i), ccn)
l.addWidget(ccn, i, 0, 1, 1)
wtb = QToolButton(parent=self)
setattr(self, 'opt_column_color_wizard_'+str(i), wtb)
wtb.setIcon(QIcon(I('wizard.png')))
l.addWidget(wtb, i, 1, 1, 1)
ttb = QToolButton(parent=self)
setattr(self, 'opt_column_color_tpledit_'+str(i), ttb)
ttb.setIcon(QIcon(I('edit_input.png')))
l.addWidget(ttb, i, 2, 1, 1)
tpl = TemplateLineEditor(parent=self)
setattr(self, 'opt_column_color_template_'+str(i), tpl)
tpl.textChanged.connect(partial(self.tpl_edit_text_changed, ctrl=i))
tpl.set_db(db)
tpl.set_mi(mi)
l.addWidget(tpl, i, 3, 1, 1)
wtb.clicked.connect(tpl.tag_wizard)
ttb.clicked.connect(tpl.open_editor)
r('column_color_name_'+str(i), db.prefs, choices=choices)
r('column_color_template_'+str(i), db.prefs)
txt = db.prefs.get('column_color_template_'+str(i), None)
wtb.setEnabled(tpl.enable_wizard_button(txt))
ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt)
all_colors = [unicode(s) for s in list(QColor.colorNames())]
self.colors_box.setText(', '.join(all_colors))
def change_help_text(self):
self.color_help_scrollArea.setVisible(not self.color_help_scrollArea.isVisible())
def change_colors_text(self):
self.colors_scrollArea.setVisible(not self.colors_scrollArea.isVisible())
self.colors_label.setVisible(not self.colors_label.isVisible())
def tpl_edit_text_changed(self, ign, ctrl=None):
tpl = getattr(self, 'opt_column_color_template_'+str(ctrl))
txt = unicode(tpl.text())
wtb = getattr(self, 'opt_column_color_wizard_'+str(ctrl))
ttb = getattr(self, 'opt_column_color_tpledit_'+str(ctrl))
wtb.setEnabled(tpl.enable_wizard_button(txt))
ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt)
tpl.setFocus()
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
@ -283,6 +175,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.current_font = self.initial_font = font self.current_font = self.initial_font = font
self.update_font_display() self.update_font_display()
self.display_model.initialize() self.display_model.initialize()
db = self.gui.current_db
self.edit_rules.initialize(db.field_metadata, db.prefs)
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) ConfigWidgetBase.restore_defaults(self)
@ -292,6 +186,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit() self.changed_signal.emit()
self.update_font_display() self.update_font_display()
self.display_model.restore_defaults() self.display_model.restore_defaults()
self.edit_rules.clear()
self.changed_signal.emit() self.changed_signal.emit()
def build_font_obj(self): def build_font_obj(self):
@ -341,12 +236,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit() self.changed_signal.emit()
def commit(self, *args): def commit(self, *args):
for i in range(1, self.column_color_count):
col = getattr(self, 'opt_column_color_name_'+str(i))
tpl = getattr(self, 'opt_column_color_template_'+str(i))
if not col.currentIndex() or not unicode(tpl.text()).strip():
col.setCurrentIndex(0)
tpl.setText('')
rr = ConfigWidgetBase.commit(self, *args) rr = ConfigWidgetBase.commit(self, *args)
if self.current_font != self.initial_font: if self.current_font != self.initial_font:
gprefs['font'] = (self.current_font[:4] if self.current_font else gprefs['font'] = (self.current_font[:4] if self.current_font else
@ -356,10 +245,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
QApplication.setFont(self.font_display.font()) QApplication.setFont(self.font_display.font())
rr = True rr = True
self.display_model.commit() self.display_model.commit()
self.edit_rules.commit(self.gui.current_db.prefs)
return rr return rr
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.library_view.model().set_color_templates() gui.library_view.model().reset()
self.update_font_display() self.update_font_display()
gui.tags_view.reread_collapse_parameters() gui.tags_view.reread_collapse_parameters()
gui.library_view.refresh_book_details() gui.library_view.refresh_book_details()

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>717</width> <width>820</width>
<height>519</height> <height>519</height>
</rect> </rect>
</property> </property>
@ -407,161 +407,6 @@ then the tags will be displayed each on their own line.</string>
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab_5">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/format-fill-color.png</normaloff>:/images/format-fill-color.png</iconset>
</attribute>
<attribute name="title">
<string>Column Coloring</string>
</attribute>
<layout class="QGridLayout" name="column_color_layout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column to color</string>
</property>
</widget>
</item>
<item row="0" column="3">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Color selection template</string>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>The template wizard is easiest to use</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="color_help_button">
<property name="text">
<string>Show/hide help text</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="colors_button">
<property name="text">
<string>Show/hide colors</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="20" column="0">
<widget class="QLabel" name="colors_label">
<property name="text">
<string>Color names</string>
</property>
</widget>
</item>
<item row="21" column="0" colspan="8">
<widget class="QScrollArea" name="colors_scrollArea">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>300</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>61</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="colors_box">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="30" column="0" colspan="8">
<widget class="QScrollArea" name="color_help_scrollArea">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>194</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="color_help_text">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="40" column="0">
<spacer>
<property name="sizePolicy">
<sizepolicy vsizetype="Expanding" hsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
</layout> </layout>
@ -572,11 +417,6 @@ then the tags will be displayed each on their own line.</string>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>calibre/gui2/complete.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget>
<class>TemplateLineEditor</class>
<extends>QLineEdit</extends>
<header>calibre/gui2/dialogs/template_line_editor.h</header>
</customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -7,7 +7,9 @@ __docformat__ = 'restructuredtext en'
import json, traceback import json, traceback
from calibre.gui2 import error_dialog from PyQt4.Qt import QDialogButtonBox
from calibre.gui2 import error_dialog, warning_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.template_functions_ui import Ui_Form from calibre.gui2.preferences.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter from calibre.gui2.widgets import PythonHighlighter
@ -152,10 +154,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
_('Name already used'), show=True) _('Name already used'), show=True)
return return
if self.argument_count.value() == 0: if self.argument_count.value() == 0:
error_dialog(self.gui, _('Template functions'), box = warning_dialog(self.gui, _('Template functions'),
_('Argument count must be -1 or greater than zero'), _('Argument count should be -1 or greater than zero.'
show=True) 'Setting it to zero means that this function cannot '
return 'be used in single function mode.'), det_msg = '',
show=False)
box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.Cancel)
box.det_msg_toggle.setVisible(False)
if not box.exec_():
return
try: try:
prog = unicode(self.program.toPlainText()) prog = unicode(self.program.toPlainText())
cls = compile_user_function(name, unicode(self.documentation.toPlainText()), cls = compile_user_function(name, unicode(self.documentation.toPlainText()),

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import urllib
from contextlib import closing
from lxml import html
from PyQt4.Qt import QUrl
from calibre import browser, url_slash_cleaner
from calibre.gui2 import open_url
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EpubBudStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url = 'http://epubbud.com/'
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):
'''
OPDS based search.
We really should get the catelog from http://pragprog.com/catalog.opds
and look for the application/opensearchdescription+xml entry.
Then get the opensearch description to get the search url and
format. However, we are going to be lazy and hard code it.
'''
url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query)
br = browser()
counter = max_results
with closing(br.open(url, timeout=timeout)) as f:
# Use html instead of etree as html allows us
# to ignore the namespace easily.
doc = html.fromstring(f.read())
for data in doc.xpath('//entry'):
if counter <= 0:
break
id = ''.join(data.xpath('.//id/text()'))
if not id:
continue
cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href'))
title = u''.join(data.xpath('.//title/text()'))
author = u''.join(data.xpath('.//author/name/text()'))
counter -= 1
s = SearchResult()
s.cover_url = cover_url
s.title = title.strip()
s.author = author.strip()
s.price = '$0.00'
s.detail_item = id.strip()
s.drm = SearchResult.DRM_UNLOCKED
s.formats = 'EPUB'
yield s

View File

@ -0,0 +1,189 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import binascii, re, json
from textwrap import dedent
class Rule(object): # {{{
SIGNATURE = '# BasicColorRule():'
def __init__(self, fm, color=None):
self.color = color
self.fm = fm
self.conditions = []
def add_condition(self, col, action, val):
if col not in self.fm:
raise ValueError('%r is not a valid column name'%col)
v = self.validate_condition(col, action, val)
if v:
raise ValueError(v)
self.conditions.append((col, action, val))
def validate_condition(self, col, action, val):
m = self.fm[col]
dt = m['datatype']
if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
try:
int(val) if dt == 'int' else float(val)
except:
return '%r is not a valid numerical value'%val
if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
in action):
try:
re.compile(val)
except:
return '%r is not a valid regular expression'%val
@property
def signature(self):
args = (self.color, self.conditions)
sig = json.dumps(args, ensure_ascii=False)
return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
@property
def template(self):
if not self.color or not self.conditions:
return None
conditions = map(self.apply_condition, self.conditions)
conditions = (',\n' + ' '*9).join(conditions)
return dedent('''\
program:
{sig}
test(and(
{conditions}
), '{color}', '');
''').format(sig=self.signature, conditions=conditions,
color=self.color)
def apply_condition(self, condition):
col, action, val = condition
m = self.fm[col]
dt = m['datatype']
if dt == 'bool':
return self.bool_condition(col, action, val)
if dt in ('int', 'float', 'rating'):
return self.number_condition(col, action, val)
if dt == 'datetime':
return self.date_condition(col, action, val)
if dt in ('comments', 'series', 'text', 'enumeration'):
ism = m.get('is_multiple', False)
if ism:
return self.multiple_condition(col, action, val, ism)
return self.text_condition(col, action, val)
def bool_condition(self, col, action, val):
test = {'is true': 'True',
'is false': 'False',
'is undefined': 'None'}[action]
return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
def number_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
lt, eq, gt = '', '1', ''
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
def date_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
val, lt, eq, gt)
def multiple_condition(self, col, action, val, sep):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'has':
return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have':
return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
if action == 'has pattern':
return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have pattern':
return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
def text_condition(self, col, action, val):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'is':
return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
if action == 'is not':
return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
if action == 'matches pattern':
return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
if action == 'does not match pattern':
return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
# }}}
def rule_from_template(fm, template):
ok_lines = []
for line in template.splitlines():
if line.startswith(Rule.SIGNATURE):
raw = line[len(Rule.SIGNATURE):].strip()
try:
color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
except:
continue
r = Rule(fm)
r.color = color
for c in conditions:
try:
r.add_condition(*c)
except:
continue
if r.color and r.conditions:
return r
else:
ok_lines.append(line)
return '\n'.join(ok_lines)
def conditionable_columns(fm):
for key in fm:
m = fm[key]
dt = m['datatype']
if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
'comments', 'text', 'enumeration', 'datetime'):
yield key
def displayable_columns(fm):
for key in fm.displayable_field_keys():
if key not in ('sort', 'author_sort', 'comments', 'formats',
'identifiers', 'path'):
yield key
def migrate_old_rule(fm, template):
if template.startswith('program:\n#tag wizard'):
rules = []
for line in template.splitlines():
if line.startswith('#') and ':|:' in line:
value, color = line[1:].split(':|:')
r = Rule(fm, color=color)
r.add_condition('tags', 'has', value)
rules.append(r.template)
return rules
return [template]

View File

@ -211,10 +211,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
defs = self.prefs.defaults defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = '' defs['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = [] defs['categories_using_hierarchy'] = []
self.column_color_count = 5 defs['column_color_rules'] = []
for i in range(1,self.column_color_count+1):
defs['column_color_name_'+str(i)] = ''
defs['column_color_template_'+str(i)] = ''
# Migrate the bool tristate tweak # Migrate the bool tristate tweak
defs['bools_are_tristate'] = \ defs['bools_are_tristate'] = \
@ -222,6 +219,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if self.prefs.get('bools_are_tristate') is None: if self.prefs.get('bools_are_tristate') is None:
self.prefs.set('bools_are_tristate', defs['bools_are_tristate']) self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
# Migrate column coloring rules
if self.prefs.get('column_color_name_1', None) is not None:
from calibre.library.coloring import migrate_old_rule
old_rules = []
for i in range(1, 5):
col = self.prefs.get('column_color_name_'+str(i), None)
templ = self.prefs.get('column_color_template_'+str(i), None)
if col and templ:
try:
del self.prefs['column_color_name_'+str(i)]
rules = migrate_old_rule(self.field_metadata, templ)
for templ in rules:
old_rules.append((col, templ))
except:
pass
if old_rules:
self.prefs['column_color_rules'] += old_rules
# Migrate saved search and user categories to db preference scheme # Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default): def migrate_preference(key, default):
oldval = prefs[key] oldval = prefs[key]

View File

@ -240,11 +240,21 @@ def cli_docs(app):
raw += '\n'+'\n'.join(lines) raw += '\n'+'\n'.join(lines)
update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info) update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info)
def generate_docs(app):
cli_docs(app)
template_docs(app)
def template_docs(app):
from template_ref_generate import generate_template_language_help
info = app.builder.info
raw = generate_template_language_help()
update_cli_doc('template_ref.rst', raw, info)
def setup(app): def setup(app):
app.add_config_value('epub_cover', None, False) app.add_config_value('epub_cover', None, False)
app.add_builder(EPUBHelpBuilder) app.add_builder(EPUBHelpBuilder)
app.connect('doctree-read', substitute) app.connect('doctree-read', substitute)
app.connect('builder-inited', cli_docs) app.connect('builder-inited', generate_docs)
app.connect('build-finished', finished) app.connect('build-finished', finished)
def finished(app, exception): def finished(app, exception):

View File

@ -1,266 +0,0 @@
.. 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:: BuiltinFormatDate
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-

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from collections import defaultdict
PREAMBLE = '''\
.. 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
'''
CATEGORY_TEMPLATE = '''\
{category}
{dashes}
'''
FUNCTION_TEMPLATE = '''\
{fs}
{hats}
.. autoclass:: {cn}
'''
POSTAMBLE = '''\
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-
'''
def generate_template_language_help():
from calibre.utils.formatter_functions import all_builtin_functions
funcs = defaultdict(dict)
for func in all_builtin_functions:
class_name = func.__class__.__name__
func_sig = getattr(func, 'doc')
x = func_sig.find(' -- ')
if x < 0:
print 'No sig for ', class_name
continue
func_sig = func_sig[:x]
func_cat = getattr(func, 'category')
funcs[func_cat][func_sig] = class_name
output = PREAMBLE
cats = sorted(funcs.keys())
for cat in cats:
output += CATEGORY_TEMPLATE.format(category=cat, dashes='-'*len(cat))
entries = [k for k in sorted(funcs[cat].keys())]
for entry in entries:
output += FUNCTION_TEMPLATE.format(fs = entry, cn=funcs[cat][entry],
hats='^'*len(entry))
output += POSTAMBLE
return output
if __name__ == '__main__':
generate_template_language_help()

View File

@ -57,6 +57,7 @@ class FormatterFunction(object):
doc = _('No documentation provided') doc = _('No documentation provided')
name = 'no name provided' name = 'no name provided'
category = 'Unknown'
arg_count = 0 arg_count = 0
def evaluate(self, formatter, kwargs, mi, locals, *args): def evaluate(self, formatter, kwargs, mi, locals, *args):
@ -87,6 +88,7 @@ class BuiltinFormatterFunction(FormatterFunction):
class BuiltinStrcmp(BuiltinFormatterFunction): class BuiltinStrcmp(BuiltinFormatterFunction):
name = 'strcmp' name = 'strcmp'
arg_count = 5 arg_count = 5
category = 'Relational'
__doc__ = 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. ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. '
'Otherwise returns gt.') 'Otherwise returns gt.')
@ -101,6 +103,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction):
class BuiltinCmp(BuiltinFormatterFunction): class BuiltinCmp(BuiltinFormatterFunction):
name = 'cmp' name = 'cmp'
category = 'Relational'
arg_count = 5 arg_count = 5
__doc__ = 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.') 'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt.')
@ -117,6 +120,7 @@ class BuiltinCmp(BuiltinFormatterFunction):
class BuiltinStrcat(BuiltinFormatterFunction): class BuiltinStrcat(BuiltinFormatterFunction):
name = 'strcat' name = 'strcat'
arg_count = -1 arg_count = -1
category = 'String Manipulation'
__doc__ = 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') 'string formed by concatenating all the arguments')
@ -130,6 +134,7 @@ class BuiltinStrcat(BuiltinFormatterFunction):
class BuiltinAdd(BuiltinFormatterFunction): class BuiltinAdd(BuiltinFormatterFunction):
name = 'add' name = 'add'
arg_count = 2 arg_count = 2
category = 'Arithmetic'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, x, y):
@ -140,6 +145,7 @@ class BuiltinAdd(BuiltinFormatterFunction):
class BuiltinSubtract(BuiltinFormatterFunction): class BuiltinSubtract(BuiltinFormatterFunction):
name = 'subtract' name = 'subtract'
arg_count = 2 arg_count = 2
category = 'Arithmetic'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, x, y):
@ -150,6 +156,7 @@ class BuiltinSubtract(BuiltinFormatterFunction):
class BuiltinMultiply(BuiltinFormatterFunction): class BuiltinMultiply(BuiltinFormatterFunction):
name = 'multiply' name = 'multiply'
arg_count = 2 arg_count = 2
category = 'Arithmetic'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, x, y):
@ -160,6 +167,7 @@ class BuiltinMultiply(BuiltinFormatterFunction):
class BuiltinDivide(BuiltinFormatterFunction): class BuiltinDivide(BuiltinFormatterFunction):
name = 'divide' name = 'divide'
arg_count = 2 arg_count = 2
category = 'Arithmetic'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, x, y):
@ -170,6 +178,8 @@ class BuiltinDivide(BuiltinFormatterFunction):
class BuiltinTemplate(BuiltinFormatterFunction): class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template' name = 'template'
arg_count = 1 arg_count = 1
category = 'Recursion'
__doc__ = 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 ' 'in its own context, meaning that variables are not shared between '
'the caller and the template evaluation. Because the { and } ' 'the caller and the template evaluation. Because the { and } '
@ -185,6 +195,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
class BuiltinEval(BuiltinFormatterFunction): class BuiltinEval(BuiltinFormatterFunction):
name = 'eval' name = 'eval'
arg_count = 1 arg_count = 1
category = 'Recursion'
__doc__ = 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. ' 'variables (those \'assign\'ed to) instead of the book metadata. '
' This permits using the template processor to construct complex ' ' This permits using the template processor to construct complex '
@ -198,6 +209,7 @@ class BuiltinEval(BuiltinFormatterFunction):
class BuiltinAssign(BuiltinFormatterFunction): class BuiltinAssign(BuiltinFormatterFunction):
name = 'assign' name = 'assign'
arg_count = 2 arg_count = 2
category = 'Other'
__doc__ = 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') 'id must be an identifier, not an expression')
@ -208,6 +220,7 @@ class BuiltinAssign(BuiltinFormatterFunction):
class BuiltinPrint(BuiltinFormatterFunction): class BuiltinPrint(BuiltinFormatterFunction):
name = 'print' name = 'print'
arg_count = -1 arg_count = -1
category = 'Other'
__doc__ = 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), ' 'Unless you start calibre from the command line (calibre-debug -g), '
'the output will go to a black hole.') 'the output will go to a black hole.')
@ -219,14 +232,16 @@ class BuiltinPrint(BuiltinFormatterFunction):
class BuiltinField(BuiltinFormatterFunction): class BuiltinField(BuiltinFormatterFunction):
name = 'field' name = 'field'
arg_count = 1 arg_count = 1
category = 'Get values from metadata'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, name):
return formatter.get_value(name, [], kwargs) return formatter.get_value(name, [], kwargs)
class BuiltinRaw_field(BuiltinFormatterFunction): class BuiltinRawField(BuiltinFormatterFunction):
name = 'raw_field' name = 'raw_field'
arg_count = 1 arg_count = 1
category = 'Get values from metadata'
__doc__ = 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.') 'without applying any formatting.')
@ -236,6 +251,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction):
class BuiltinSubstr(BuiltinFormatterFunction): class BuiltinSubstr(BuiltinFormatterFunction):
name = 'substr' name = 'substr'
arg_count = 3 arg_count = 3
category = 'String Manipulation'
__doc__ = 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 ' 'characters of str. The first character in str is the zero\'th '
'character. If end is negative, then it indicates that many ' 'character. If end is negative, then it indicates that many '
@ -249,6 +265,7 @@ class BuiltinSubstr(BuiltinFormatterFunction):
class BuiltinLookup(BuiltinFormatterFunction): class BuiltinLookup(BuiltinFormatterFunction):
name = 'lookup' name = 'lookup'
arg_count = -1 arg_count = -1
category = 'Iterating over values'
__doc__ = 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 ' 'like switch, except the arguments are field (metadata) names, not '
'text. The value of the appropriate field will be fetched and used. ' 'text. The value of the appropriate field will be fetched and used. '
@ -276,6 +293,7 @@ class BuiltinLookup(BuiltinFormatterFunction):
class BuiltinTest(BuiltinFormatterFunction): class BuiltinTest(BuiltinFormatterFunction):
name = 'test' name = 'test'
arg_count = 3 arg_count = 3
category = 'If-then-else'
__doc__ = 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`') 'empty` if the field is not empty, otherwise return `text if empty`')
@ -288,6 +306,7 @@ class BuiltinTest(BuiltinFormatterFunction):
class BuiltinContains(BuiltinFormatterFunction): class BuiltinContains(BuiltinFormatterFunction):
name = 'contains' name = 'contains'
arg_count = 4 arg_count = 4
category = 'If-then-else'
__doc__ = 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`. ' 'if field contains matches for the regular expression `pattern`. '
'Returns `text if match` if matches are found, otherwise it returns ' 'Returns `text if match` if matches are found, otherwise it returns '
@ -303,6 +322,7 @@ class BuiltinContains(BuiltinFormatterFunction):
class BuiltinSwitch(BuiltinFormatterFunction): class BuiltinSwitch(BuiltinFormatterFunction):
name = 'switch' name = 'switch'
arg_count = -1 arg_count = -1
category = 'Iterating over values'
__doc__ = 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 ' 'for each `pattern, value` pair, checks if the field matches '
'the regular expression `pattern` and if so, returns that ' 'the regular expression `pattern` and if so, returns that '
@ -323,6 +343,7 @@ class BuiltinSwitch(BuiltinFormatterFunction):
class BuiltinInList(BuiltinFormatterFunction): class BuiltinInList(BuiltinFormatterFunction):
name = 'in_list' name = 'in_list'
arg_count = 5 arg_count = 5
category = 'List Lookup'
__doc__ = 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, ' 'treat val as a list of items separated by separator, '
'comparing the pattern against each value in the list. If the ' 'comparing the pattern against each value in the list. If the '
@ -340,6 +361,8 @@ class BuiltinInList(BuiltinFormatterFunction):
class BuiltinStrInList(BuiltinFormatterFunction): class BuiltinStrInList(BuiltinFormatterFunction):
name = 'str_in_list' name = 'str_in_list'
arg_count = 5 arg_count = 5
category = 'List Lookup'
category = 'Iterating over values'
__doc__ = 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, ' 'treat val as a list of items separated by separator, '
'comparing the string against each value in the list. If the ' 'comparing the string against each value in the list. If the '
@ -360,6 +383,7 @@ class BuiltinStrInList(BuiltinFormatterFunction):
class BuiltinRe(BuiltinFormatterFunction): class BuiltinRe(BuiltinFormatterFunction):
name = 're' name = 're'
arg_count = 3 arg_count = 3
category = 'String Manipulation'
__doc__ = 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 ' 'the regular expression. All instances of `pattern` are replaced '
'with `replacement`. As in all of calibre, these are ' 'with `replacement`. As in all of calibre, these are '
@ -371,6 +395,7 @@ class BuiltinRe(BuiltinFormatterFunction):
class BuiltinIfempty(BuiltinFormatterFunction): class BuiltinIfempty(BuiltinFormatterFunction):
name = 'ifempty' name = 'ifempty'
arg_count = 2 arg_count = 2
category = 'If-then-else'
__doc__ = 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`') 'otherwise return `text if empty`')
@ -383,6 +408,7 @@ class BuiltinIfempty(BuiltinFormatterFunction):
class BuiltinShorten(BuiltinFormatterFunction): class BuiltinShorten(BuiltinFormatterFunction):
name = 'shorten' name = 'shorten'
arg_count = 4 arg_count = 4
category = 'String Manipulation'
__doc__ = 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` ' 'shortened version of the field, consisting of `left chars` '
'characters from the beginning of the field, followed by ' 'characters from the beginning of the field, followed by '
@ -408,6 +434,7 @@ class BuiltinShorten(BuiltinFormatterFunction):
class BuiltinCount(BuiltinFormatterFunction): class BuiltinCount(BuiltinFormatterFunction):
name = 'count' name = 'count'
arg_count = 2 arg_count = 2
category = 'List Manipulation'
__doc__ = 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 ' 'separated by `separator`, returning the number of items in the '
'list. Most lists use a comma as the separator, but authors ' 'list. Most lists use a comma as the separator, but authors '
@ -419,6 +446,7 @@ class BuiltinCount(BuiltinFormatterFunction):
class BuiltinListitem(BuiltinFormatterFunction): class BuiltinListitem(BuiltinFormatterFunction):
name = 'list_item' name = 'list_item'
arg_count = 3 arg_count = 3
category = 'List Lookup'
__doc__ = 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. ' 'items separated by `separator`, returning the `index`th item. '
'The first item is number zero. The last item can be returned ' 'The first item is number zero. The last item can be returned '
@ -439,6 +467,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
class BuiltinSelect(BuiltinFormatterFunction): class BuiltinSelect(BuiltinFormatterFunction):
name = 'select' name = 'select'
arg_count = 2 arg_count = 2
category = 'List Lookup'
__doc__ = 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' 'of items, with the items being "id:value". Find the pair with the'
'id equal to key, and return the corresponding value.' 'id equal to key, and return the corresponding value.'
@ -456,6 +485,7 @@ class BuiltinSelect(BuiltinFormatterFunction):
class BuiltinSublist(BuiltinFormatterFunction): class BuiltinSublist(BuiltinFormatterFunction):
name = 'sublist' name = 'sublist'
arg_count = 4 arg_count = 4
category = 'List Manipulation'
__doc__ = 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 ' 'value as a list of items separated by `separator`, returning a '
'new list made from the `start_index` to the `end_index` item. ' 'new list made from the `start_index` to the `end_index` item. '
@ -486,6 +516,7 @@ class BuiltinSublist(BuiltinFormatterFunction):
class BuiltinSubitems(BuiltinFormatterFunction): class BuiltinSubitems(BuiltinFormatterFunction):
name = 'subitems' name = 'subitems'
arg_count = 3 arg_count = 3
category = 'List Manipulation'
__doc__ = 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 ' '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-' 'as a comma-separated list of items, where each item is a period-'
@ -523,6 +554,7 @@ class BuiltinSubitems(BuiltinFormatterFunction):
class BuiltinFormatDate(BuiltinFormatterFunction): class BuiltinFormatDate(BuiltinFormatterFunction):
name = 'format_date' name = 'format_date'
arg_count = 2 arg_count = 2
category = 'Get values from metadata'
__doc__ = doc = _('format_date(val, format_string) -- format the value, ' __doc__ = doc = _('format_date(val, format_string) -- format the value, '
'which must be a date, using the format_string, returning a string. ' 'which must be a date, using the format_string, returning a string. '
'The formatting codes are: ' 'The formatting codes are: '
@ -551,6 +583,7 @@ class BuiltinFormatDate(BuiltinFormatterFunction):
class BuiltinUppercase(BuiltinFormatterFunction): class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase' name = 'uppercase'
arg_count = 1 arg_count = 1
category = 'String case changes'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, val):
@ -559,6 +592,7 @@ class BuiltinUppercase(BuiltinFormatterFunction):
class BuiltinLowercase(BuiltinFormatterFunction): class BuiltinLowercase(BuiltinFormatterFunction):
name = 'lowercase' name = 'lowercase'
arg_count = 1 arg_count = 1
category = 'String case changes'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, val):
@ -567,6 +601,7 @@ class BuiltinLowercase(BuiltinFormatterFunction):
class BuiltinTitlecase(BuiltinFormatterFunction): class BuiltinTitlecase(BuiltinFormatterFunction):
name = 'titlecase' name = 'titlecase'
arg_count = 1 arg_count = 1
category = 'String case changes'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, val):
@ -575,6 +610,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction):
class BuiltinCapitalize(BuiltinFormatterFunction): class BuiltinCapitalize(BuiltinFormatterFunction):
name = 'capitalize' name = 'capitalize'
arg_count = 1 arg_count = 1
category = 'String case changes'
__doc__ = 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): def evaluate(self, formatter, kwargs, mi, locals, val):
@ -583,6 +619,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction):
class BuiltinBooksize(BuiltinFormatterFunction): class BuiltinBooksize(BuiltinFormatterFunction):
name = 'booksize' name = 'booksize'
arg_count = 0 arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('booksize() -- return value of the size field') __doc__ = doc = _('booksize() -- return value of the size field')
def evaluate(self, formatter, kwargs, mi, locals): def evaluate(self, formatter, kwargs, mi, locals):
@ -596,6 +633,7 @@ class BuiltinBooksize(BuiltinFormatterFunction):
class BuiltinOndevice(BuiltinFormatterFunction): class BuiltinOndevice(BuiltinFormatterFunction):
name = 'ondevice' name = 'ondevice'
arg_count = 0 arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return ' __doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return '
'the empty string') 'the empty string')
@ -607,6 +645,7 @@ class BuiltinOndevice(BuiltinFormatterFunction):
class BuiltinFirstNonEmpty(BuiltinFormatterFunction): class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
name = 'first_non_empty' name = 'first_non_empty'
arg_count = -1 arg_count = -1
category = 'Iterating over values'
__doc__ = 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 ' 'returns the first value that is not empty. If all values are '
'empty, then the empty value is returned.' 'empty, then the empty value is returned.'
@ -623,6 +662,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
class BuiltinAnd(BuiltinFormatterFunction): class BuiltinAnd(BuiltinFormatterFunction):
name = 'and' name = 'and'
arg_count = -1 arg_count = -1
category = 'Boolean'
__doc__ = doc = _('and(value, value, ...) -- ' __doc__ = doc = _('and(value, value, ...) -- '
'returns the string "1" if all values are not empty, otherwise ' 'returns the string "1" if all values are not empty, otherwise '
'returns the empty string. This function works well with test or ' 'returns the empty string. This function works well with test or '
@ -639,6 +679,7 @@ class BuiltinAnd(BuiltinFormatterFunction):
class BuiltinOr(BuiltinFormatterFunction): class BuiltinOr(BuiltinFormatterFunction):
name = 'or' name = 'or'
arg_count = -1 arg_count = -1
category = 'Boolean'
__doc__ = doc = _('or(value, value, ...) -- ' __doc__ = doc = _('or(value, value, ...) -- '
'returns the string "1" if any value is not empty, otherwise ' 'returns the string "1" if any value is not empty, otherwise '
'returns the empty string. This function works well with test or ' 'returns the empty string. This function works well with test or '
@ -655,6 +696,7 @@ class BuiltinOr(BuiltinFormatterFunction):
class BuiltinNot(BuiltinFormatterFunction): class BuiltinNot(BuiltinFormatterFunction):
name = 'not' name = 'not'
arg_count = 1 arg_count = 1
category = 'Boolean'
__doc__ = doc = _('not(value) -- ' __doc__ = doc = _('not(value) -- '
'returns the string "1" if the value is empty, otherwise ' 'returns the string "1" if the value is empty, otherwise '
'returns the empty string. This function works well with test or ' 'returns the empty string. This function works well with test or '
@ -671,6 +713,7 @@ class BuiltinNot(BuiltinFormatterFunction):
class BuiltinMergeLists(BuiltinFormatterFunction): class BuiltinMergeLists(BuiltinFormatterFunction):
name = 'merge_lists' name = 'merge_lists'
arg_count = 3 arg_count = 3
category = 'List Manipulation'
__doc__ = 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, ' 'return a list made by merging the items in list1 and list2, '
'removing duplicate items using a case-insensitive compare. If ' 'removing duplicate items using a case-insensitive compare. If '
@ -716,7 +759,7 @@ builtin_not = BuiltinNot()
builtin_ondevice = BuiltinOndevice() builtin_ondevice = BuiltinOndevice()
builtin_or = BuiltinOr() builtin_or = BuiltinOr()
builtin_print = BuiltinPrint() builtin_print = BuiltinPrint()
builtin_raw_field = BuiltinRaw_field() builtin_raw_field = BuiltinRawField()
builtin_re = BuiltinRe() builtin_re = BuiltinRe()
builtin_select = BuiltinSelect() builtin_select = BuiltinSelect()
builtin_shorten = BuiltinShorten() builtin_shorten = BuiltinShorten()