Merge from trunk

This commit is contained in:
Charles Haley 2011-06-02 07:19:21 +01:00
commit 536ff7a33f
14 changed files with 616 additions and 764 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

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

@ -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

@ -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

@ -2,188 +2,24 @@
# 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 json, binascii, re
from textwrap import dedent
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox,
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox)
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 Rule(object): # {{{
SIGNATURE = '# BasicColorRule():'
def __init__(self, fm):
self.color = None
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('1',
{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('%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
class ConditionEditor(QWidget):
class ConditionEditor(QWidget): # {{{
def __init__(self, fm, parent=None):
QWidget.__init__(self, parent)
@ -246,7 +82,7 @@ class ConditionEditor(QWidget):
for key in sorted(
conditionable_columns(fm),
key=lambda x:sort_key(fm[x]['name'])):
self.column_box.addItem(fm[key]['name'], key)
self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.column_box.currentIndexChanged.connect(self.init_action_box)
@ -352,11 +188,12 @@ class ConditionEditor(QWidget):
if 'pattern' in action:
tt = _('Enter a regular expression')
self.value_box.setToolTip(tt)
if action in ('is set', 'is not set'):
if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'):
self.value_box.setEnabled(False)
# }}}
class RuleEditor(QDialog):
class RuleEditor(QDialog): # {{{
def __init__(self, fm, parent=None):
QDialog.__init__(self, parent)
@ -418,6 +255,7 @@ class RuleEditor(QDialog):
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):
@ -429,7 +267,7 @@ class RuleEditor(QDialog):
key=lambda x:sort_key(fm[x]['name'])):
name = fm[key]['name']
if name:
self.column_box.addItem(name, key)
self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.color_box.addItems(QColor.colorNames())
@ -442,6 +280,27 @@ class RuleEditor(QDialog):
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)
@ -479,8 +338,234 @@ class RuleEditor(QDialog):
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
@ -488,13 +573,24 @@ if __name__ == '__main__':
from calibre.library import db
d = RuleEditor(db().field_metadata)
d.add_blank_condition()
d.exec_()
db = db()
col, r = d.rule
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)
print ('Column to be colored:', col)
print ('Template:')
print (r.template)

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

@ -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

@ -86,8 +86,7 @@ def generate_template_language_help():
hats='^'*len(entry))
output += POSTAMBLE
print output
return output # and hope that something good happens to it
return output
if __name__ == '__main__':
generate_template_language_help()