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
src/calibre/manual/.build/
src/calibre/manual/cli/
src/calibre/manual/template_ref.rst
build
dist
docs
@ -31,4 +32,4 @@ nbproject/
.pydevproject
.settings/
*.DS_Store
calibre_plugins/
calibre_plugins/

View File

@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase):
formats = ['EPUB', 'PDF']
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):
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.'
@ -1422,6 +1431,7 @@ plugins += [
StoreEBookShoppeUKStore,
StoreEPubBuyDEStore,
StoreEHarlequinStore,
StoreEpubBudStore,
StoreFeedbooksStore,
StoreFoylesUKStore,
StoreGandalfStore,

View File

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

View File

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

View File

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

View File

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

View File

@ -35,8 +35,9 @@ from calibre import force_unicode
class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None):
def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent)
self.max_width = max_width
def to_doc(self, index):
doc = QTextDocument()
@ -46,8 +47,8 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
def sizeHint(self, option, index):
doc = self.to_doc(index)
ans = doc.size().toSize()
if ans.width() > 150:
ans.setWidth(160)
if ans.width() > self.max_width - 10:
ans.setWidth(self.max_width)
ans.setHeight(ans.height()+10)
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>'
__docformat__ = 'restructuredtext en'
from functools import partial
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.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.utils.localization import (available_translations,
get_language, get_lang)
from calibre.utils.config import prefs
from calibre.utils.icu import sort_key
from calibre.gui2 import NONE
from calibre.gui2.book_details import get_field_list
from calibre.gui2.preferences.coloring import EditRules
class DisplayedFields(QAbstractListModel): # {{{
@ -162,117 +160,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.df_up_button.clicked.connect(self.move_df_up)
self.df_down_button.clicked.connect(self.move_df_down)
self.color_help_text.setText('<p>' +
_('Here you can specify coloring rules for columns shown in the '
'library view. Choose the column you wish to color, then '
'supply a template that specifies the color to use based on '
'the values in the column. There is a '
'<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()
self.edit_rules = EditRules(self.tabWidget)
self.edit_rules.changed.connect(self.changed_signal)
self.tabWidget.addTab(self.edit_rules,
QIcon(I('format-fill-color.png')), _('Column coloring'))
self.tabWidget.setCurrentIndex(0)
def initialize(self):
ConfigWidgetBase.initialize(self)
@ -283,6 +175,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.current_font = self.initial_font = font
self.update_font_display()
self.display_model.initialize()
db = self.gui.current_db
self.edit_rules.initialize(db.field_metadata, db.prefs)
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
@ -292,6 +186,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
self.update_font_display()
self.display_model.restore_defaults()
self.edit_rules.clear()
self.changed_signal.emit()
def build_font_obj(self):
@ -341,12 +236,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
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)
if self.current_font != self.initial_font:
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())
rr = True
self.display_model.commit()
self.edit_rules.commit(self.gui.current_db.prefs)
return rr
def refresh_gui(self, gui):
gui.library_view.model().set_color_templates()
gui.library_view.model().reset()
self.update_font_display()
gui.tags_view.reread_collapse_parameters()
gui.library_view.refresh_book_details()

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>717</width>
<width>820</width>
<height>519</height>
</rect>
</property>
@ -407,161 +407,6 @@ then the tags will be displayed each on their own line.</string>
</item>
</layout>
</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>
</item>
</layout>
@ -572,11 +417,6 @@ then the tags will be displayed each on their own line.</string>
<extends>QLineEdit</extends>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>TemplateLineEditor</class>
<extends>QLineEdit</extends>
<header>calibre/gui2/dialogs/template_line_editor.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>

View File

@ -7,7 +7,9 @@ __docformat__ = 'restructuredtext en'
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.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter
@ -152,10 +154,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
_('Name already used'), show=True)
return
if self.argument_count.value() == 0:
error_dialog(self.gui, _('Template functions'),
_('Argument count must be -1 or greater than zero'),
show=True)
return
box = warning_dialog(self.gui, _('Template functions'),
_('Argument count should be -1 or greater than zero.'
'Setting it to zero means that this function cannot '
'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:
prog = unicode(self.program.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['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = []
self.column_color_count = 5
for i in range(1,self.column_color_count+1):
defs['column_color_name_'+str(i)] = ''
defs['column_color_template_'+str(i)] = ''
defs['column_color_rules'] = []
# Migrate the bool tristate tweak
defs['bools_are_tristate'] = \
@ -222,6 +219,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if self.prefs.get('bools_are_tristate') is None:
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
def migrate_preference(key, default):
oldval = prefs[key]

View File

@ -240,11 +240,21 @@ def cli_docs(app):
raw += '\n'+'\n'.join(lines)
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):
app.add_config_value('epub_cover', None, False)
app.add_builder(EPUBHelpBuilder)
app.connect('doctree-read', substitute)
app.connect('builder-inited', cli_docs)
app.connect('builder-inited', generate_docs)
app.connect('build-finished', finished)
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')
name = 'no name provided'
category = 'Unknown'
arg_count = 0
def evaluate(self, formatter, kwargs, mi, locals, *args):
@ -87,6 +88,7 @@ class BuiltinFormatterFunction(FormatterFunction):
class BuiltinStrcmp(BuiltinFormatterFunction):
name = 'strcmp'
arg_count = 5
category = 'Relational'
__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.')
@ -101,6 +103,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction):
class BuiltinCmp(BuiltinFormatterFunction):
name = 'cmp'
category = 'Relational'
arg_count = 5
__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.')
@ -117,6 +120,7 @@ class BuiltinCmp(BuiltinFormatterFunction):
class BuiltinStrcat(BuiltinFormatterFunction):
name = 'strcat'
arg_count = -1
category = 'String Manipulation'
__doc__ = doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a '
'string formed by concatenating all the arguments')
@ -130,6 +134,7 @@ class BuiltinStrcat(BuiltinFormatterFunction):
class BuiltinAdd(BuiltinFormatterFunction):
name = 'add'
arg_count = 2
category = 'Arithmetic'
__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):
@ -140,6 +145,7 @@ class BuiltinAdd(BuiltinFormatterFunction):
class BuiltinSubtract(BuiltinFormatterFunction):
name = 'subtract'
arg_count = 2
category = 'Arithmetic'
__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):
@ -150,6 +156,7 @@ class BuiltinSubtract(BuiltinFormatterFunction):
class BuiltinMultiply(BuiltinFormatterFunction):
name = 'multiply'
arg_count = 2
category = 'Arithmetic'
__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):
@ -160,6 +167,7 @@ class BuiltinMultiply(BuiltinFormatterFunction):
class BuiltinDivide(BuiltinFormatterFunction):
name = 'divide'
arg_count = 2
category = 'Arithmetic'
__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):
@ -170,6 +178,8 @@ class BuiltinDivide(BuiltinFormatterFunction):
class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template'
arg_count = 1
category = 'Recursion'
__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 } '
@ -185,6 +195,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'
arg_count = 1
category = 'Recursion'
__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 '
@ -198,6 +209,7 @@ class BuiltinEval(BuiltinFormatterFunction):
class BuiltinAssign(BuiltinFormatterFunction):
name = 'assign'
arg_count = 2
category = 'Other'
__doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. '
'id must be an identifier, not an expression')
@ -208,6 +220,7 @@ class BuiltinAssign(BuiltinFormatterFunction):
class BuiltinPrint(BuiltinFormatterFunction):
name = 'print'
arg_count = -1
category = 'Other'
__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,14 +232,16 @@ class BuiltinPrint(BuiltinFormatterFunction):
class BuiltinField(BuiltinFormatterFunction):
name = 'field'
arg_count = 1
category = 'Get values from metadata'
__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)
class BuiltinRaw_field(BuiltinFormatterFunction):
class BuiltinRawField(BuiltinFormatterFunction):
name = 'raw_field'
arg_count = 1
category = 'Get values from metadata'
__doc__ = doc = _('raw_field(name) -- returns the metadata field named by name '
'without applying any formatting.')
@ -236,6 +251,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction):
class BuiltinSubstr(BuiltinFormatterFunction):
name = 'substr'
arg_count = 3
category = 'String Manipulation'
__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 '
@ -249,6 +265,7 @@ class BuiltinSubstr(BuiltinFormatterFunction):
class BuiltinLookup(BuiltinFormatterFunction):
name = 'lookup'
arg_count = -1
category = 'Iterating over values'
__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. '
@ -276,6 +293,7 @@ class BuiltinLookup(BuiltinFormatterFunction):
class BuiltinTest(BuiltinFormatterFunction):
name = 'test'
arg_count = 3
category = 'If-then-else'
__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`')
@ -288,6 +306,7 @@ class BuiltinTest(BuiltinFormatterFunction):
class BuiltinContains(BuiltinFormatterFunction):
name = 'contains'
arg_count = 4
category = 'If-then-else'
__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 '
@ -303,6 +322,7 @@ class BuiltinContains(BuiltinFormatterFunction):
class BuiltinSwitch(BuiltinFormatterFunction):
name = 'switch'
arg_count = -1
category = 'Iterating over values'
__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 '
@ -323,6 +343,7 @@ class BuiltinSwitch(BuiltinFormatterFunction):
class BuiltinInList(BuiltinFormatterFunction):
name = 'in_list'
arg_count = 5
category = 'List Lookup'
__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 '
@ -340,6 +361,8 @@ class BuiltinInList(BuiltinFormatterFunction):
class BuiltinStrInList(BuiltinFormatterFunction):
name = 'str_in_list'
arg_count = 5
category = 'List Lookup'
category = 'Iterating over values'
__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 '
@ -360,6 +383,7 @@ class BuiltinStrInList(BuiltinFormatterFunction):
class BuiltinRe(BuiltinFormatterFunction):
name = 're'
arg_count = 3
category = 'String Manipulation'
__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 '
@ -371,6 +395,7 @@ class BuiltinRe(BuiltinFormatterFunction):
class BuiltinIfempty(BuiltinFormatterFunction):
name = 'ifempty'
arg_count = 2
category = 'If-then-else'
__doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, '
'otherwise return `text if empty`')
@ -383,6 +408,7 @@ class BuiltinIfempty(BuiltinFormatterFunction):
class BuiltinShorten(BuiltinFormatterFunction):
name = 'shorten'
arg_count = 4
category = 'String Manipulation'
__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 '
@ -408,6 +434,7 @@ class BuiltinShorten(BuiltinFormatterFunction):
class BuiltinCount(BuiltinFormatterFunction):
name = 'count'
arg_count = 2
category = 'List Manipulation'
__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 '
@ -419,6 +446,7 @@ class BuiltinCount(BuiltinFormatterFunction):
class BuiltinListitem(BuiltinFormatterFunction):
name = 'list_item'
arg_count = 3
category = 'List Lookup'
__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 '
@ -439,6 +467,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
class BuiltinSelect(BuiltinFormatterFunction):
name = 'select'
arg_count = 2
category = 'List Lookup'
__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,6 +485,7 @@ class BuiltinSelect(BuiltinFormatterFunction):
class BuiltinSublist(BuiltinFormatterFunction):
name = 'sublist'
arg_count = 4
category = 'List Manipulation'
__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` to the `end_index` item. '
@ -486,6 +516,7 @@ class BuiltinSublist(BuiltinFormatterFunction):
class BuiltinSubitems(BuiltinFormatterFunction):
name = 'subitems'
arg_count = 3
category = 'List Manipulation'
__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-'
@ -523,6 +554,7 @@ class BuiltinSubitems(BuiltinFormatterFunction):
class BuiltinFormatDate(BuiltinFormatterFunction):
name = 'format_date'
arg_count = 2
category = 'Get values from metadata'
__doc__ = doc = _('format_date(val, format_string) -- format the value, '
'which must be a date, using the format_string, returning a string. '
'The formatting codes are: '
@ -551,6 +583,7 @@ class BuiltinFormatDate(BuiltinFormatterFunction):
class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase'
arg_count = 1
category = 'String case changes'
__doc__ = doc = _('uppercase(val) -- return value of the field in upper case')
def evaluate(self, formatter, kwargs, mi, locals, val):
@ -559,6 +592,7 @@ class BuiltinUppercase(BuiltinFormatterFunction):
class BuiltinLowercase(BuiltinFormatterFunction):
name = 'lowercase'
arg_count = 1
category = 'String case changes'
__doc__ = doc = _('lowercase(val) -- return value of the field in lower case')
def evaluate(self, formatter, kwargs, mi, locals, val):
@ -567,6 +601,7 @@ class BuiltinLowercase(BuiltinFormatterFunction):
class BuiltinTitlecase(BuiltinFormatterFunction):
name = 'titlecase'
arg_count = 1
category = 'String case changes'
__doc__ = doc = _('titlecase(val) -- return value of the field in title case')
def evaluate(self, formatter, kwargs, mi, locals, val):
@ -575,6 +610,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction):
class BuiltinCapitalize(BuiltinFormatterFunction):
name = 'capitalize'
arg_count = 1
category = 'String case changes'
__doc__ = doc = _('capitalize(val) -- return value of the field capitalized')
def evaluate(self, formatter, kwargs, mi, locals, val):
@ -583,6 +619,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction):
class BuiltinBooksize(BuiltinFormatterFunction):
name = 'booksize'
arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('booksize() -- return value of the size field')
def evaluate(self, formatter, kwargs, mi, locals):
@ -596,6 +633,7 @@ class BuiltinBooksize(BuiltinFormatterFunction):
class BuiltinOndevice(BuiltinFormatterFunction):
name = 'ondevice'
arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return '
'the empty string')
@ -607,6 +645,7 @@ class BuiltinOndevice(BuiltinFormatterFunction):
class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
name = 'first_non_empty'
arg_count = -1
category = 'Iterating over values'
__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.'
@ -623,6 +662,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
class BuiltinAnd(BuiltinFormatterFunction):
name = 'and'
arg_count = -1
category = 'Boolean'
__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 '
@ -639,6 +679,7 @@ class BuiltinAnd(BuiltinFormatterFunction):
class BuiltinOr(BuiltinFormatterFunction):
name = 'or'
arg_count = -1
category = 'Boolean'
__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 '
@ -655,6 +696,7 @@ class BuiltinOr(BuiltinFormatterFunction):
class BuiltinNot(BuiltinFormatterFunction):
name = 'not'
arg_count = 1
category = 'Boolean'
__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 '
@ -671,6 +713,7 @@ class BuiltinNot(BuiltinFormatterFunction):
class BuiltinMergeLists(BuiltinFormatterFunction):
name = 'merge_lists'
arg_count = 3
category = 'List Manipulation'
__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 '
@ -716,7 +759,7 @@ builtin_not = BuiltinNot()
builtin_ondevice = BuiltinOndevice()
builtin_or = BuiltinOr()
builtin_print = BuiltinPrint()
builtin_raw_field = BuiltinRaw_field()
builtin_raw_field = BuiltinRawField()
builtin_re = BuiltinRe()
builtin_select = BuiltinSelect()
builtin_shorten = BuiltinShorten()