From 11fc40dd0deebbe29ad353ab6e0c48f151dcc333 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 10 Nov 2024 16:04:49 +0000 Subject: [PATCH] Formatter Function Markup Language first pass. Contains: - The FFML processor in the new file utils.ffml_processor.py. - converted documentation in formatter_functions.py. - changes to template_processor.py to use FFML. Includes a new "show documentation" dialog. What remains: - Deal with translations somehow. Proposal in email. - Changes to the manual to use FFML output when generating function documentation. - Changes to template_lang.rst. --- src/calibre/gui2/dialogs/template_dialog.py | 68 +- src/calibre/gui2/dialogs/template_dialog.ui | 108 +- src/calibre/utils/ffml_processor.py | 516 ++++++ src/calibre/utils/formatter_functions.py | 1769 ++++++++++++------- 4 files changed, 1754 insertions(+), 707 deletions(-) create mode 100644 src/calibre/utils/ffml_processor.py diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index f7d2f7ae26..7febcb68af 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -12,6 +12,8 @@ import sys import traceback from functools import partial +from qt.webengine import QWebEngineView + from qt.core import ( QAbstractItemView, QApplication, @@ -24,14 +26,17 @@ from qt.core import ( QFontDatabase, QFontInfo, QFontMetrics, + QHBoxLayout, QIcon, QLineEdit, QPalette, + QPushButton, QSize, QSyntaxHighlighter, Qt, QTableWidget, QTableWidgetItem, + QTextBrowser, QTextCharFormat, QTextOption, QToolButton, @@ -48,6 +53,7 @@ from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog from calibre.library.coloring import color_row_key, displayable_columns from calibre.utils.config_base import tweaks from calibre.utils.date import DEFAULT_DATE +from calibre.utils.ffml_processor import FFMLProcessor from calibre.utils.formatter import PythonTemplateContext, StopException from calibre.utils.formatter_functions import StoredObjectType, formatter_functions from calibre.utils.icu import lower as icu_lower @@ -364,6 +370,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.setupUi(self) self.setWindowIcon(self.windowIcon()) + self.docs_dsl = FFMLProcessor() self.dialog_number = dialog_number self.coloring = color_field is not None self.iconing = icon_field_key is not None @@ -459,8 +466,11 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.textbox.textChanged.connect(self.textbox_changed) self.set_editor_font() + self.doc_viewer = None + self.current_function_name = None self.documentation.setReadOnly(True) self.source_code.setReadOnly(True) + self.doc_button.clicked.connect(self.open_documentation_viewer) if text is not None: if text_is_placeholder: @@ -501,7 +511,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): '{}'.format( localize_user_manual_link('https://manual.calibre-ebook.com/template_lang.html'), tt)) tt = _('Template function reference') - self.template_func_reference.setText( + self.tf_ref.setText( '{}'.format( localize_user_manual_link('https://manual.calibre-ebook.com/generated/en/template_ref.html'), tt)) @@ -520,6 +530,51 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): # Now geometry self.restore_geometry(gprefs, self.geometry_string('template_editor_dialog_geometry')) + def open_documentation_viewer(self): + if self.doc_viewer is None: + dv = self.doc_viewer = QDialog(self) + l = QVBoxLayout() + dv.setLayout(l) + e = self.doc_viewer_widget = QWebEngineView() #QTextBrowser() + # e.setOpenExternalLinks(True) + # e.setReadOnly(True) + l.addWidget(e) + b = QHBoxLayout() + b.addStretch(10) + pb = QPushButton(_('Show all functions')) + pb.setToolTip((_('Shows a list of all built-in functions in alphabetic order'))) + pb.clicked.connect(self.doc_viewer_show_all) + b.addWidget(pb) + + pb = QPushButton(_('Close')) + pb.clicked.connect(dv.close) + b.addWidget(pb) + l.addLayout(b) + e.setHtml('') + dv.restore_geometry(gprefs, 'template_editor_doc_viewer') + dv.finished.connect(self.doc_viewer_finished) + dv.show() + if self.current_function_name is not None: + self.doc_viewer_widget.setHtml( + self.docs_dsl.document_to_html(self.all_functions[self.current_function_name].doc, + self.current_function_name)) + + def doc_viewer_show_all(self): + funcs = formatter_functions().get_builtins() + result = '' + for name in sorted(funcs): + result += f'\n

{name}

\n' + try: + result += self.docs_dsl.document_to_html(funcs[name].doc.strip(), name) + except Exception: + print('Exception in', name) + raise + self.doc_viewer_widget.setHtml(result) + + def doc_viewer_finished(self): + self.doc_viewer.save_geometry(gprefs, 'template_editor_doc_viewer') + self.doc_viewer = None + def geometry_string(self, txt): if self.dialog_number is None or self.dialog_number == 0: return txt @@ -947,12 +1002,15 @@ def evaluate(book, context): return (_('Stored user defined GPM template') if longform else _('Stored template')) def function_changed(self, toWhat): - name = str(self.function.itemData(toWhat)) + self.current_function_name = name = str(self.function.itemData(toWhat)) self.source_code.clear() self.documentation.clear() self.func_type.clear() if name in self.all_functions: - self.documentation.setPlainText(self.all_functions[name].doc) + doc = self.all_functions[name].doc.strip() + self.documentation.setHtml(self.docs_dsl.document_to_html(doc, name)) + if self.doc_viewer is not None: + self.doc_viewer_widget.setHtml(self.docs_dsl.document_to_html(self.all_functions[name].doc, name)) if name in self.builtins and name in self.builtin_source_dict: self.source_code.setPlainText(self.builtin_source_dict[name]) else: @@ -1009,6 +1067,8 @@ def evaluate(book, context): QDialog.accept(self) if self.dialog_number is not None: self.tester_closed.emit(txt, self.dialog_number) + if self.doc_viewer is not None: + self.doc_viewer.close() def reject(self): self.save_geometry() @@ -1024,6 +1084,8 @@ def evaluate(book, context): break if self.dialog_number is not None: self.tester_closed.emit(None, self.dialog_number) + if self.doc_viewer is not None: + self.doc_viewer.close() class BreakReporterItem(QTableWidgetItem): diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index 6e1d5731c2..2d4e1bb6df 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -660,11 +660,51 @@ you the value as well as all the local variables</p> - - - Template Function Reference - - + + + + + Qt::Horizontal + + + + 10 + 0 + + + + + + + + Template Function Reference + + + true + + + + + + + true + + + + + + + Qt::Horizontal + + + + 10 + 0 + + + + + @@ -700,20 +740,40 @@ you the value as well as all the local variables</p> - - - &Documentation: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - documentation - - + + + + + &Documentation: + + + Click this button to open the documentation is a separate dialog + + + + + + + See tooltip for general information + + + true + + + <p>When using functions in a Single Function Mode template, +for example {title:uppercase()}, the first parameter 'value' is omitted. It is automatically replaced +by the value of the specified field.</p> In all the other modes the value parameter +must be supplied. + + + + - + + + true + 16777215 @@ -747,20 +807,6 @@ you the value as well as all the local variables</p> - - - - true - - - - - - - true - - - diff --git a/src/calibre/utils/ffml_processor.py b/src/calibre/utils/ffml_processor.py new file mode 100644 index 0000000000..abc16f3cc8 --- /dev/null +++ b/src/calibre/utils/ffml_processor.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python + + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from enum import IntEnum +from calibre import prepare_string_for_xml + +class NodeKinds(IntEnum): + DOCUMENT = -1 + CODE_TEXT = -2 + CODE_BLOCK = -3 + URL = -4 + BLANK_LINE = -5 + TEXT = -6 + LIST = -7 + END_LIST = -8 + LIST_ITEM = -9 + GUI_LABEL = -10 + ITALIC_TEXT = -11 + + +class Node: + + def __init__(self, node_kind: NodeKinds): + self._node_kind = node_kind + self._children = [] + + def node_kind(self) -> NodeKinds: + return self._node_kind + + def add_child(self, node): + self._children.append(node) + + def children(self): + return self._children + + def text(self): + return self._text + + def escaped_text(self): + return prepare_string_for_xml(self._text) #.replace('<', 'LESS_THAN') #.replace('>', '>') + + +class DocumentNode(Node): + + def __init__(self): + super().__init__(NodeKinds.DOCUMENT) + self._children = [] + + +class TextNode(Node): + + def __init__(self, text): + super().__init__(NodeKinds.TEXT) + self._text = text + + +class CodeBlock(Node): + + def __init__(self, code_text): + super().__init__(NodeKinds.CODE_BLOCK) + self._text = code_text + + +class CodeText(Node): + + def __init__(self, code_text): + super().__init__(NodeKinds.CODE_TEXT) + self._text = code_text + + +class BlankLineNode(Node): + + def __init__(self): + super().__init__(NodeKinds.BLANK_LINE) + + +class UrlNode(Node): + + def __init__(self, label, url): + super().__init__(NodeKinds.URL) + self._label = label + self._url = url + + def label(self): + return self._label + + def url(self): + return self._url + + +class ListNode(Node): + + def __init__(self): + super().__init__(NodeKinds.LIST) + + +class ListItemNode(Node): + + def __init__(self): + super().__init__(NodeKinds.LIST_ITEM) + + +class ItalicTextNode(Node): + + def __init__(self, text): + super().__init__(NodeKinds.ITALIC_TEXT) + self._text = text + + +class GuiLabelNode(Node): + + def __init__(self, text): + super().__init__(NodeKinds.GUI_LABEL) + self._text = text + + + +class FFMLProcessor: + """ + This class is parser for the Formatter Function Markup Language (FFML). It + provides output methods for RST and HTML. + + FFML is a basic markup language used to document formatter functions. It is + based on a combination of RST used by sphinx and BBCODE used by many + bulletin board systems such as MobileRead. It provides a way to specify: + + - inline program code text: surround this text with `` as in ``foo``. + + - italic text: surround this text with `, as in `foo`. + + - text intended to reference a calibre GUI action. This uses RST syntax. + Example: :guilabel:`Preferences->Advanced->Template functions` + + - empty lines, indicated by two newlines in a row. A visible empty line + in the FFMC will become an empty line in the output. + + - URLs. The syntax is similar to BBCODE: [URL href="http..."]Link text[/URL]. + Example: [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL] + + - example program code text blocks. Surround the code block with [CODE] + and [/CODE] tags. These tags must be first on a line. Example: + [CODE] + program: + get_note('authors', 'Isaac Asimov', 1) + [/CODE] + + - bulleted lists, using BBCODE tags. Surround the list with [LIST] and + [/LIST]. List items are indicated with [*]. All of the tags must be + first on a line. Bulleted lists can be nested and can contain other FFML + elements. Example: a two bullet list containing CODE blocks + [LIST] + [*]Return the HTML of the note attached to the tag `Fiction`: + [CODE] + program: + get_note('tags', 'Fiction', '') + [/CODE] + [*]Return the plain text of the note attached to the author `Isaac Asimov`: + [CODE] + program: + get_note('authors', 'Isaac Asimov', 1) + [/CODE] + [/LIST] + + HTML output contains no CSS and does not start with a tag such as
or

. + + RST output is not indented. + + API example: generate documents for all builtin formatter functions + -------------------- + from calibre.utils.ffml_processor import FFMLProcessor + from calibre.utils.formatter_functions import formatter_functions + from calibre.db.legacy import LibraryDatabase + + # We need this to load the formatter functions + db = LibraryDatabase('') + + ffml = FFMLProcessor() + funcs = formatter_functions().get_builtins() + + with open('all_docs.html', 'w') as w: + for name in sorted(funcs): + w.write(f'\n

{name}

\n') + w.write(ffml.document_to_html(funcs[name].doc, name)) + + with open('all_docs.rst', 'w') as w: + for name in sorted(funcs): + w.write(f"\n\n{name}\n{'^'*len(name)}\n\n") + w.write(ffml.document_to_rst(funcs[name].doc, name)) + -------------------- + """ + +# ====== API ====== + + def print_node_tree(self, node, indent=0): + """ + Pretty print a Formatter Function Markup Language (FFML) parse tree. + + :param node: The root of the tree you want printed. + :param indent: The indent level of the tree. The outermost root should + have an indent of zero. + """ + if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT, + NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT, + NodeKinds.GUI_LABEL): + print(f'{" " * indent}{node.node_kind().name}:{node.text()}') + elif node.node_kind() == NodeKinds.URL: + print(f'{" " * indent}URL: label={node.label()}, URL={node.url()}') + else: + print(f'{" " * indent}{node.node_kind().name}') + for n in node.children(): + self.print_node_tree(n, indent+1) + + def parse_document(self, doc, name): + """ + Given a Formatter Function Markup Language (FFML) document, return + a parse tree for that document. + + :param doc: the document in FFML. + :param name: the name of the document, used for generating errors. This + is usually the name of the function. + + :return: a parse tree for the document + """ + self.input = doc + self.input_pos = 0 + self.document_name = name + + node = DocumentNode() + return self._parse_document(node) + + def tree_to_html(self, tree, depth=0): + """ + Given a Formatter Function Markup Language (FFML) parse tree, return + a string containing the HTML for that tree. + + :param tree: the parsed FFML. + :param depth: the recursion level. This is used for debugging. + + :return: a string containing the HTML text + """ + result = '' + if tree.node_kind() == NodeKinds.TEXT: + result += tree.escaped_text() + elif tree.node_kind() == NodeKinds.CODE_TEXT: + result += f'{tree.escaped_text()}' + elif tree.node_kind() == NodeKinds.CODE_BLOCK: + result += f'
{tree.escaped_text()}
' + elif tree.node_kind() == NodeKinds.ITALIC_TEXT: + result += f'{tree.escaped_text()}' + elif tree.node_kind() == NodeKinds.GUI_LABEL: + result += f'{tree.escaped_text()}' + elif tree.node_kind() == NodeKinds.BLANK_LINE: + result += '\n
\n
\n' + elif tree.node_kind() == NodeKinds.URL: + result += f'{tree.label()}' + elif tree.node_kind() == NodeKinds.LIST: + result += '\n
    \n' + for child in tree.children(): + result += '
  • \n' + result += self.tree_to_html(child, depth+1) + result += '
  • \n' + result += '
\n' + elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM): + for child in tree.children(): + result += self.tree_to_html(child, depth+1) + return result + + def document_to_html(self, document, name): + """ + Given a document in the Formatter Function Markup Language (FFML), return + that document in HTML format. + + :param document: the text in FFML. + :param name: the name of the document, used during error + processing. It is usually the name of the function. + + :return: a string containing the HTML + + """ + tree = self.parse_document(document, name) + return self.tree_to_html(tree, 0) + + def tree_to_rst(self, tree, indent, result=None): + """ + Given a Formatter Function Markup Language (FFML) parse tree, return + a string containing the RST (sphinx reStructuredText) for that tree. + + :param tree: the parsed FFML. + :param indent: the indenting level of the items in the tree. This is + usually zero, but can be greater than zero if you want + the RST output indented. + + :return: a string containing the RST text + """ + if result is None: + result = ' ' * indent + if tree.node_kind() == NodeKinds.TEXT: + txt = tree.text() + if not result: + txt = txt.lstrip() + elif result.endswith('\n'): + txt = txt.lstrip() + result += ' ' * indent + result += txt + elif tree.node_kind() == NodeKinds.CODE_TEXT: + result += f'``{tree.text()}``' + elif tree.node_kind() == NodeKinds.GUI_LABEL: + result += f':guilabel:`{tree.text()}`' + elif tree.node_kind() == NodeKinds.CODE_BLOCK: + result += f"\n\n{' ' * indent}::\n\n" + for line in tree.text().strip().split('\n'): + result += f"{' ' * (indent+1)}{line}\n" + result += '\n' + elif tree.node_kind() == NodeKinds.BLANK_LINE: + result += '\n\n' + elif tree.node_kind() == NodeKinds.ITALIC_TEXT: + result += f'`{tree.text()}`' + elif tree.node_kind() == NodeKinds.URL: + result += f'`{tree.label()} <{tree.url()}>`_' + elif tree.node_kind() == NodeKinds.LIST: + result += '\n\n' + for child in tree.children(): + result += f"{' ' * (indent)}* " + result = self.tree_to_rst(child, indent+1, result) + result += '\n' + result += '\n' + elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM): + for child in tree.children(): + result = self.tree_to_rst(child, indent, result) + return result + + def document_to_rst(self, document, name): + """ + Given a document in the Formatter Function Markup Language (FFML), return + that document in RST (sphinx reStructuredText) format. + + :param document: the text in FFML. + :param name: the name of the document, used during error + processing. It is usually the name of the function. + + :return: a string containing the RST text + + """ + return self.tree_to_rst(self.parse_document(document, name), 0) + +# ============== Internal methods ================= + + keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`' + '`': NodeKinds.ITALIC_TEXT, + ':guilabel:': NodeKinds.GUI_LABEL, + '[CODE]': NodeKinds.CODE_BLOCK, + '[URL': NodeKinds.URL, + '[LIST]': NodeKinds.LIST, + '[/LIST]': NodeKinds.END_LIST, + '[*]': NodeKinds.LIST_ITEM, + '\n\n': NodeKinds.BLANK_LINE + } + + def __init__(self): + self.document = DocumentNode() + self.input = None + self.input_pos = 0 + self.input_line = 1 + + def error(self, message): + raise ValueError(f'{message} on line {self.input_line} in "{self.document_name}"') + + def find(self, for_what): + p = self.input.find(for_what, self.input_pos) + return -1 if p < 0 else p - self.input_pos + + def move_pos(self, to_where): + for c in self.input[self.input_pos:self.input_pos+to_where]: + if c == '\n': + self.input_line += 1 + self.input_pos += to_where + + def at_end(self): + return self.input_pos >= len(self.input) + + def text_to(self, end): + return self.input[self.input_pos:self.input_pos+end] + + def text_contains_newline(self, txt): + return '\n' in txt + + def text_to_no_newline(self, end, block_name): + txt = self.input[self.input_pos:self.input_pos+end] + if self.text_contains_newline(txt): + self.error(f'Newline unexpected in {block_name}') + return txt + + def startswith(self, txt): + return self.input.startswith(txt, self.input_pos) + + def find_one_of(self): + positions = [] + for s in self.keywords: + p = self.find(s) + if p == 0: + return self.keywords[s] + positions.append(self.find(s)) + positions = list(filter(lambda x: x >= 0, positions)) + if positions: + return min(positions) + return len(self.input) + + def get_code_text(self): + self.move_pos(len('``')) + end = self.find('``') + if end < 0: + self.error(f'Missing closing "``" for CODE_TEXT') + node = CodeText(self.text_to(end)) + self.move_pos(end + len('``')) + return node + + def get_italic_text(self): + self.move_pos(1) + end = self.find('`') + if end < 0: + self.error(f'Missing closing "`" for italics') + node = ItalicTextNode(self.text_to(end)) + self.move_pos(end + 1) + return node + + def get_gui_label(self): + self.move_pos(len(':guilabel:`')) + end = self.find('`') + if end < 0: + self.error(f'Missing ` (backquote) for :guilabel:') + node = GuiLabelNode(self.text_to_no_newline(end, 'GUI_LABEL (:guilabel:`)')) + self.move_pos(end + len('`')) + return node + + def get_code_block(self): + self.move_pos(len('[CODE]\n')) + end = self.find('[/CODE]') + if end < 0: + self.error(f'Missing [/CODE] for block') + node = CodeBlock(self.text_to(end)) + self.move_pos(end + len('[/CODE]')) + if self.text_to(1) == '\n': + self.move_pos(1) + return node + + def get_list(self): + self.move_pos(len('[LIST]\n')) + list_node = ListNode() + while True: + if self.startswith('[/LIST]'): + break + if not self.startswith('[*]'): + self.error(f'Missing [*] in list near text:"{self.text_to(10)}"') + self.move_pos(len('[*]')) + node = self._parse_document(ListItemNode()) + list_node.add_child(node) + self.move_pos(len('[/LIST]')) + if self.text_to(1) == '\n': + self.move_pos(1) + return list_node + + def get_url(self): + self.move_pos(len('[URL')) + hp = self.find('href="') + if hp < 0: + self.error(f'Missing href=" near text {self.text_to(10)}') + self.move_pos(hp + len('href="')) + close_quote = self.find('"]') + if close_quote < 0: + self.error(f'Missing closing "> for URL near text:"{self.text_to(10)}"') + href = self.text_to_no_newline(close_quote, 'URL href') + self.move_pos(close_quote + len('"]')) + lp = self.find('[/URL]') + if lp < 0: + self.error(f'Missing closing [/URL] near text {self.text_to(10)}') + label = self.text_to(lp).strip() + label = label.replace('\n', ' ') + node = UrlNode(label, href) + self.move_pos(lp + len('[/URL]')) + return node + + def _parse_document(self, parent): + while True: + p = self.find_one_of() + if p > 0: + txt = self.text_to(p).replace('\n', ' ') + parent.add_child(TextNode(txt)) + self.move_pos(p) + elif p == NodeKinds.CODE_TEXT: + parent.add_child(self.get_code_text()) + elif p == NodeKinds.CODE_BLOCK: + parent.add_child(self.get_code_block()) + elif p == NodeKinds.LIST: + parent.add_child(self.get_list()) + elif p == NodeKinds.LIST_ITEM: + return parent + elif p == NodeKinds.END_LIST: + return parent + elif p == NodeKinds.BLANK_LINE: + parent.add_child(BlankLineNode()) + self.move_pos(2) + elif p == NodeKinds.ITALIC_TEXT: + parent.add_child(self.get_italic_text()) + elif p == NodeKinds.GUI_LABEL: + parent.add_child(self.get_gui_label()) + elif p == NodeKinds.URL: + parent.add_child(self.get_url()) + else: + self.move_pos(p+1) + if self.at_end(): + break + return parent diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 561c6ba41a..ff238346f4 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -218,10 +218,13 @@ 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. In many cases the lexical comparison operators ' - '(>, <, == etc) can replace this function.') + __doc__ = doc = _( +''' +``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive lexical comparison of +``x`` and ``y``. Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise +``gt``. This function can often be replaced by one of the lexical comparison +operators (``==``, ``>``, ``<``, etc.) +''') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): v = strcmp(x, y) @@ -236,12 +239,16 @@ class BuiltinStrcmpcase(BuiltinFormatterFunction): name = 'strcmpcase' arg_count = 5 category = 'Relational' - __doc__ = doc = _('strcmpcase(x, y, lt, eq, gt) -- does a case-sensitive comparison of x ' - 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' - 'Otherwise returns gt.\n' - 'Note: This is NOT the default behavior used by calibre, for example, in the ' - 'lexical comparison operators (==, >, <, etc.). This function could ' - 'cause unexpected results, preferably use strcmp() whenever possible.') + __doc__ = doc = _( +''' +``strcmpcase(x, y, lt, eq, gt)`` -- does a case-sensitive lexical comparison of +``x`` and ``y``. Returns ``lt`` if ``x < y``, ``eq`` if ``x == y``, otherwise +``gt``. + +Note: This is NOT the default behavior used by calibre, for example, in the +lexical comparison operators (``==``, ``>``, ``<``, etc.). This function could +cause unexpected results, preferably use ``strcmp()`` whenever possible. +''') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): from calibre.utils.icu import case_sensitive_strcmp as case_strcmp @@ -257,10 +264,13 @@ 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. ' - 'In many cases the numeric comparison operators ' - '(>#, <#, ==# etc) can replace this function.') + __doc__ = doc = _( +''' +``cmp(x, y, lt, eq, gt)`` -- compares ``x`` and ``y`` after converting both to +numbers. Returns ``lt`` if ``x <# y``, ``eq`` if ``x ==# y``, otherwise ``gt``. +This function can usually be replaced with one of the numeric compare operators +(``==#``, ``<#``, ``>#``, etc). +''') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): x = float(x if x and x != 'None' else 0) @@ -276,12 +286,19 @@ class BuiltinFirstMatchingCmp(BuiltinFormatterFunction): name = 'first_matching_cmp' category = 'Relational' arg_count = -1 - __doc__ = doc = _('first_matching_cmp(val, [cmp1, result1,]+, else_result) -- ' - 'compares "val < cmpN" in sequence, returning resultN for ' - 'the first comparison that succeeds. Returns else_result ' - 'if no comparison succeeds. Example: ' - 'first_matching_cmp(10,5,"small",10,"middle",15,"large","giant") ' - 'returns "large". The same example with a first value of 16 returns "giant".') + __doc__ = doc = _( +''' +``first_matching_cmp(val, [ cmp, result, ]* else_result)`` -- compares ``val < cmp`` +in sequence, returning the associated result for the first comparison that +succeeds. Returns else_result if no comparison succeeds. + +Example: +[CODE] +i = 10; +first_matching_cmp(i,5,"small",10,"middle",15,"large","giant") +[/CODE] +returns ``"large"``. The same example with a first value of 16 returns ``"giant"``. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): if (len(args) % 2) != 0: @@ -298,8 +315,11 @@ class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 category = 'String manipulation' - __doc__ = doc = _('strcat(a [, b]*) -- can take any number of arguments. Returns the ' - 'string formed by concatenating all the arguments') + __doc__ = doc = _( +''' +``strcat(a [, b]*)`` -- can take any number of arguments. Returns a string +formed by concatenating all the arguments. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 @@ -313,8 +333,10 @@ class BuiltinStrlen(BuiltinFormatterFunction): name = 'strlen' arg_count = 1 category = 'String manipulation' - __doc__ = doc = _('strlen(a) -- Returns the length of the string passed as ' - 'the argument') + __doc__ = doc = _( +''' +``strlen(value)`` -- Returns the length of the string ``value``. +''') def evaluate(self, formatter, kwargs, mi, locals, a): try: @@ -327,10 +349,12 @@ class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = -1 category = 'Arithmetic' - __doc__ = doc = _('add(x [, y]*) -- returns the sum of its arguments. ' - 'Throws an exception if an argument is not a number. ' - 'This function can often be ' - 'replaced with the + operator.') + __doc__ = doc = _( +''' +``add(x [, y]*)`` -- returns the sum of its arguments. Throws an exception if an +argument is not a number. In most cases you can use the ``+`` operator instead +of this function. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): res = 0 @@ -344,9 +368,12 @@ 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. This function can often be ' - 'replaced with the - operator.') + __doc__ = doc = _( +''' +``subtract(x, y)`` -- returns ``x - y``. Throws an exception if either ``x`` or +``y`` are not numbers. This function can usually be replaced by the ``-`` +operator. +''') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) @@ -358,9 +385,12 @@ class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = -1 category = 'Arithmetic' - __doc__ = doc = _('multiply(x [, y]*) -- returns the product of its arguments. ' - 'Throws an exception if any argument is not a number. ' - 'This function can often be replaced with the * operator.') + __doc__ = doc = _( +''' +``multiply(x [, y]*)`` -- returns the product of its arguments. Throws an +exception if any argument is not a number. This function can usually be replaced +by the ``*`` operator. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): res = 1 @@ -374,9 +404,12 @@ 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.' - ' This function can often be replaced with the / operator.') + __doc__ = doc = _( +''' +``divide(x, y)`` -- returns ``x / y``. Throws an exception if either ``x`` or +``y`` are not numbers. This function can usually be replaced by the ``/`` +operator. +''') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) @@ -388,9 +421,11 @@ class BuiltinCeiling(BuiltinFormatterFunction): name = 'ceiling' arg_count = 1 category = 'Arithmetic' - __doc__ = doc = _('ceiling(x) -- returns the smallest integer greater ' - 'than or equal to x. Throws an exception if x is ' - 'not a number.') + __doc__ = doc = _( +''' +``ceiling(x)`` -- returns the smallest integer greater than or equal to ``x``. +Throws an exception if ``x`` is not a number. +''') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) @@ -401,9 +436,11 @@ class BuiltinFloor(BuiltinFormatterFunction): name = 'floor' arg_count = 1 category = 'Arithmetic' - __doc__ = doc = _('floor(x) -- returns the largest integer less ' - 'than or equal to x. Throws an exception if x is ' - 'not a number.') + __doc__ = doc = _( +''' +``floor(x)`` -- returns the largest integer less than or equal to ``x``. Throws +an exception if ``x`` is not a number. +''') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) @@ -414,8 +451,11 @@ class BuiltinRound(BuiltinFormatterFunction): name = 'round' arg_count = 1 category = 'Arithmetic' - __doc__ = doc = _('round(x) -- returns the nearest integer to x. ' - 'Throws an exception if x is not a number.') + __doc__ = doc = _( +''' +``round(x)`` -- returns the nearest integer to ``x``. Throws an exception if +``x`` is not a number. +''') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) @@ -426,8 +466,11 @@ class BuiltinMod(BuiltinFormatterFunction): name = 'mod' arg_count = 2 category = 'Arithmetic' - __doc__ = doc = _('mod(x) -- returns floor(remainder of x / y). ' - 'Throws an exception if either x or y is not a number.') + __doc__ = doc = _( +''' +``mod(x, y)`` -- returns the ``floor`` of the remainder of ``x / y``. Throws an +exception if either ``x`` or ``y`` is not a number. +''') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) @@ -439,9 +482,12 @@ class BuiltinFractionalPart(BuiltinFormatterFunction): name = 'fractional_part' arg_count = 1 category = 'Arithmetic' - __doc__ = doc = _('fractional_part(x) -- returns the value after the decimal ' - 'point. For example, fractional_part(3.14) returns 0.14. ' - 'Throws an exception if x is not a number.') + __doc__ = doc = _( +''' +``fractional_part(value)`` -- returns the part of the value after the decimal +point. For example, ``fractional_part(3.14)`` returns ``0.14``. Throws an +exception if ``value`` is not a number. +''') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) @@ -453,15 +499,18 @@ class BuiltinTemplate(BuiltinFormatterFunction): 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 } ' - 'characters are special, you must use [[ for the { character and ' - ']] for the } character; they are converted automatically. ' - 'For example, template(\'[[title_sort]]\') will evaluate the ' - 'template {title_sort} and return its value. Note also that ' - 'prefixes and suffixes (the `|prefix|suffix` syntax) cannot be ' - 'used in the argument to this function when using template program mode.') + __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. If not using General Program Mode, because the ``{`` and +``}`` characters are special, you must use ``[[`` for the ``{`` character and +``]]`` for the } character; they are converted automatically. For example, +``template(\'[[title_sort]]\')`` will evaluate the template ``{title_sort}`` and return +its value. Note also that prefixes and suffixes (the ``|prefix|suffix`` syntax) +cannot be used in the argument to this function when using template program +mode. +''') def evaluate(self, formatter, kwargs, mi, locals, template): template = template.replace('[[', '{').replace(']]', '}') @@ -472,15 +521,19 @@ 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 ' - 'results from local variables. Because the { and } ' - 'characters are special, you must use [[ for the { character and ' - ']] for the } character; they are converted automatically. ' - 'Note also that prefixes and suffixes (the `|prefix|suffix` syntax) ' - 'cannot be used in the argument to this function when using ' - 'template program mode.') + __doc__ = doc = _( +''' +``eval(string)`` -- evaluates the string as a program, passing the local +variables. This permits using the template processor to construct complex +results from local variables. In +[URL href="https://manual.calibre-ebook.com/template_lang.html#more-complex-programs-in-template-expressions-template-program-mode"] +Template Program Mode[/URL], +because the ``{`` and ``}`` characters are interpreted before the template is +evaluated you must use ``[[`` for the ``{`` character and ``]]`` for the ``}`` +character. They are converted automatically. Note also that prefixes and +suffixes (the ``|prefix|suffix`` syntax) cannot be used in the argument to this +function when using Template Program Mode. +''') def evaluate(self, formatter, kwargs, mi, locals, template): from calibre.utils.formatter import EvalFormatter @@ -492,9 +545,12 @@ 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. ' - 'This function can often be replaced with the = operator.') + __doc__ = doc = _( +''' +``assign(id, value)`` -- assigns ``value`` to ``id``, then returns ``value``. ``id`` +must be an identifier, not an expression. In most cases you can use the ``=`` +operator instead of this function. +''') def evaluate(self, formatter, kwargs, mi, locals, target, value): locals[target] = value @@ -505,13 +561,24 @@ class BuiltinListSplit(BuiltinFormatterFunction): name = 'list_split' arg_count = 3 category = 'List manipulation' - __doc__ = doc = _('list_split(list_val, sep, id_prefix) -- splits the list_val ' - "into separate values using 'sep', then assigns the values " - "to variables named 'id_prefix_N' where N is the position " - "of the value in the list. The first item has position 0 (zero). " - "The function returns the last element in the list. " - "Example: split('one:two:foo', ':', 'var') is equivalent " - "to var_0 = 'one'; var_1 = 'two'; var_2 = 'foo'.") + __doc__ = doc = _( +''' +``list_split(list_val, sep, id_prefix)`` -- splits ``list_val`` into separate +values using ``sep``, then assigns the values to local variables named +``id_prefix_N`` where N is the position of the value in the list. The first item +has position 0 (zero). The function returns the last element in the list. + +Example: +[CODE] + list_split('one:two:foo', ':', 'var') +[/CODE] +is equivalent to: +[CODE] + var_0 = 'one'; + var_1 = 'two'; + var_2 = 'foo +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, list_val, sep, id_prefix): l = [v.strip() for v in list_val.split(sep)] @@ -525,9 +592,12 @@ 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.') + __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 into a +black hole. The ``print`` function always returns its first argument. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): print(args) @@ -538,7 +608,10 @@ class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _('field(lookup_name) -- returns the metadata field named by lookup_name') + __doc__ = doc = _( +''' +``field(lookup_name)`` -- returns the value of the metadata field with lookup name ``lookup_name``. +''') def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs) @@ -548,10 +621,14 @@ class BuiltinRawField(BuiltinFormatterFunction): name = 'raw_field' arg_count = -1 category = 'Get values from metadata' - __doc__ = doc = _('raw_field(lookup_name [, optional_default]) -- returns the ' - 'metadata field named by lookup_name without applying any formatting. ' - 'It evaluates and returns the optional second argument ' - "'default' if the field is undefined ('None').") + __doc__ = doc = _( +''' +``raw_field(lookup_name [, optional_default])`` -- returns the metadata field +named by ``lookup_name`` without applying any formatting. It evaluates and +returns the optional second argument ``optional_default`` if the field's value +is undefined (``None``). The ``$$`` prefix can be used instead of the function, +as in ``$$pubdate``. +''') def evaluate(self, formatter, kwargs, mi, locals, name, default=None): res = getattr(mi, name, None) @@ -569,9 +646,12 @@ class BuiltinRawList(BuiltinFormatterFunction): name = 'raw_list' arg_count = 2 category = 'Get values from metadata' - __doc__ = doc = _('raw_list(lookup_name, separator) -- returns the metadata list ' - 'named by lookup_name without applying any formatting or sorting and ' - 'with items separated by separator.') + __doc__ = doc = _( +''' +``raw_list(lookup_name, separator)`` -- returns the metadata list named by +``lookup_name`` without applying any formatting or sorting, with the items +separated by separator. +''') def evaluate(self, formatter, kwargs, mi, locals, name, separator): res = getattr(mi, name, None) @@ -584,12 +664,15 @@ 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 ' - 'characters counting from the right. If end is zero, then it ' - 'indicates the last character. For example, substr(\'12345\', 1, 0) ' - 'returns \'2345\', and substr(\'12345\', 1, -1) returns \'234\'.') + __doc__ = doc = _( +''' +``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th +characters of ``str``. The first character in ``str`` is the zero'th character. +If ``end`` is negative then it indicates that many characters counting from the +right. If ``end`` is zero, then it indicates the last character. For example, +``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` +returns ``'234'``. +''') def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_): return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)] @@ -599,13 +682,13 @@ class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 category = 'Iterating over values' - __doc__ = doc = _('lookup(val, [pattern, field,]+ else_field) -- ' - 'like switch, except the arguments are field (metadata) names, not ' - 'text. The value of the appropriate field will be fetched and used. ' - 'Note that because composite columns are fields, you can use this ' - 'function in one composite field to use the value of some other ' - 'composite field. This is extremely useful when constructing ' - 'variable save paths') + __doc__ = doc = _( +''' +``lookup(value, [ pattern, key, ]* else_key)`` -- The patterns will be checked against +the value in order. If a pattern matches then the value of the field named by +``key`` is returned. If no pattern matches then the value of the field named by +``else_key`` is returned. See also the ``switch()`` function. +''') def evaluate(self, formatter, kwargs, mi, locals, val, *args): if len(args) == 2: # here for backwards compatibility @@ -628,8 +711,11 @@ 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 val is not empty, otherwise return `text if empty`') + __doc__ = doc = _( +''' +``test(value, text if not empty, text if empty)`` -- return ``text if not empty`` if +the value is not empty, otherwise return ``text if empty``. +''') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set): if val: @@ -642,10 +728,12 @@ 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 val contains matches for the regular expression `pattern`. ' - 'Returns `text if match` if matches are found, otherwise it returns ' - '`text if no match`') + __doc__ = doc = _( +''' +``contains(value, pattern, text if match, text if not match)`` -- checks if the value +is matched by the regular expression ``pattern``. Returns ``text if match`` if +the pattern matches the value, otherwise returns ``text if no match``. +''') def evaluate(self, formatter, kwargs, mi, locals, val, test, value_if_present, value_if_not): @@ -659,11 +747,14 @@ class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 category = 'Iterating over values' - __doc__ = doc = _('switch(val, [pattern, value,]+ else_value) -- ' - 'for each `pattern, value` pair, checks if `val` matches ' - 'the regular expression `pattern` and if so, returns that ' - '`value`. If no pattern matches, then `else_value` is returned. ' - 'You can have as many `pattern, value` pairs as you want') + __doc__ = doc = _( +''' +``switch(value, [pattern, value,]+ else_value)`` -- for each ``pattern, value`` pair, +checks if the value matches the regular expression ``pattern`` and if so returns +the associated ``value``. If no ``pattern`` matches, then ``else_value`` is +returned. You can have as many ``pattern, value`` pairs as you wish. The first +match is returned. +''') def evaluate(self, formatter, kwargs, mi, locals, val, *args): if (len(args) % 2) != 1: @@ -681,11 +772,14 @@ class BuiltinSwitchIf(BuiltinFormatterFunction): name = 'switch_if' arg_count = -1 category = 'Iterating over values' - __doc__ = doc = _('switch_if([test_expression, value_expression,]+ else_expression) -- ' - 'for each "test_expression, value_expression" pair, checks if test_expression ' - 'is True (non-empty) and if so returns the result of value_expression. ' - 'If no test_expression is True then the result of else_expression is returned. ' - 'You can have as many "test_expression, value_expression" pairs as you want.') + __doc__ = doc = _( +''' +``switch_if([test_expression, value_expression,]+ else_expression)`` -- for each +``test_expression, value_expression`` pair, checks if ``test_expression`` is +True (non-empty) and if so returns the result of ``value_expression``. If no +``test_expression`` is True then the result of ``else_expression`` is returned. +You can have as many ``test_expression, value_expression`` pairs as you want. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): if (len(args) % 2) != 1: @@ -705,13 +799,15 @@ class BuiltinStrcatMax(BuiltinFormatterFunction): name = 'strcat_max' arg_count = -1 category = 'String manipulation' - __doc__ = doc = _('strcat_max(max, string1 [, prefix2, string2]*) -- ' - 'Returns a string formed by concatenating the arguments. The ' - 'returned value is initialized to string1. `Prefix, string` ' - 'pairs are added to the end of the value as long as the ' - 'resulting string length is less than `max`. String1 is returned ' - 'even if string1 is longer than max. You can pass as many ' - '`prefix, string` pairs as you wish.') + __doc__ = doc = _( +''' +``strcat_max(max, string1 [, prefix2, string2]*)`` -- Returns a string formed by +concatenating the arguments. The returned value is initialized to ``string1``. +Strings made from ``prefix, string`` pairs are added to the end of the value as +long as the resulting string length is less than ``max``. Prefixes can be empty. +Returns ``string1`` even if ``string1`` is longer than ``max``. You can pass as +many ``prefix, string`` pairs as you wish. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): if len(args) < 2: @@ -737,18 +833,23 @@ class BuiltinStrcatMax(BuiltinFormatterFunction): class BuiltinInList(BuiltinFormatterFunction): - name = 'in_list' + name = 'list_contains' arg_count = -1 category = 'List lookup' - __doc__ = doc = _('in_list(val, separator, [ pattern, found_val, ]+ not_found_val) -- ' - 'treating val as a list of items separated by separator, ' - 'if the pattern matches any of the list values then return found_val.' - 'If the pattern matches no list value then return ' - 'not_found_val. The pattern and found_value pairs can be repeated as ' - 'many times as desired. The patterns are checked in order. The ' - 'found_val for the first match is returned. ' - 'Aliases: in_list(), list_contains()') - aliases = ['list_contains'] + __doc__ = doc = _( +''' +``list_contains(value, separator, [ pattern, found_val, ]* not_found_val)`` -- interpret the +value as a list of items separated by ``separator``, checking the ``pattern`` +against each item in the list. If the ``pattern`` matches an item then return +``found_val``, otherwise return ``not_found_val``. The pair ``pattern`` and +``found_value`` can be repeated as many times as desired, permitting returning +different values depending on the item's value. The patterns are checked in +order, and the first match is returned. + +Aliases: in_list(), list_contains() +''') + + aliases = ['in_list'] def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args): if (len(args) % 2) != 1: @@ -771,14 +872,19 @@ class BuiltinStrInList(BuiltinFormatterFunction): name = 'str_in_list' arg_count = -1 category = 'List lookup' - __doc__ = doc = _('str_in_list(val, separator, [string, found_val, ]+ not_found_val) -- ' - 'treating val as a list of items separated by separator, if the ' - 'string matches any of the list values then return found_val.' - 'If the string matches no list value then return ' - 'not_found_val. The comparison is exact match (not contains) and is ' - 'case insensitive. The string and found_value pairs can be repeated as ' - 'many times as desired. The patterns are checked in order. The ' - 'found_val for the first match is returned.') + __doc__ = doc = _( +''' +``str_in_list(value, separator, [ string, found_val, ]+ not_found_val)`` -- interpret +the value as a list of items separated by ``separator`` then compare ``string`` +against each value in the list. The ``string`` is not a regular expression. If +``string`` is equal to any item (ignoring case) then return the corresponding +``found_val``. If ``string`` contains ``separators`` then it is also treated as +a list and each subvalue is checked. The ``string`` and ``found_value`` pairs +can be repeated as many times as desired, permitting returning different values +depending on string's value. If none of the strings match then +``not_found_value`` is returned. The strings are checked in order. The first +match is returned. +''') def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args): if (len(args) % 2) != 1: @@ -803,17 +909,19 @@ class BuiltinIdentifierInList(BuiltinFormatterFunction): name = 'identifier_in_list' arg_count = -1 category = 'List lookup' - __doc__ = doc = _('identifier_in_list(val, id_name [, found_val, not_found_val]) -- ' - 'treat val as a list of identifiers separated by commas. An identifier ' - 'has the format "id_name:value". The id_name parameter is the id_name ' - 'text to search for, either "id_name" or "id_name:regexp". The first case ' - 'matches if there is any identifier matching that id_name. The second ' - 'case matches if id_name matches an identifier and the regexp ' - 'matches the identifier\'s value. If found_val and not_found_val ' - 'are provided then if there is a match then return found_val, otherwise ' - 'return not_found_val. If found_val and not_found_val are not ' - 'provided then if there is a match then return the identifier:value ' - 'pair, otherwise the empty string.') + __doc__ = doc = _( +''' +``identifier_in_list(val, id_name [, found_val, not_found_val])`` -- treat +``val`` as a list of identifiers separated by commas. An identifier has the +format ``id_name:value``. The ``id_name`` parameter is the id_name text to +search for, either ``id_name`` or ``id_name:regexp``. The first case matches if +there is any identifier matching that id_name. The second case matches if +id_name matches an identifier and the regexp matches the identifier's value. If +``found_val`` and ``not_found_val`` are provided then if there is a match then +return ``found_val``, otherwise return ``not_found_val``. If ``found_val`` and +``not_found_val`` are not provided then if there is a match then return the +``identifier:value`` pair, otherwise the empty string (``''``). +''') def evaluate(self, formatter, kwargs, mi, locals, val, ident, *args): if len(args) == 0: @@ -842,10 +950,14 @@ class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 category = 'String manipulation' - __doc__ = doc = _('re(val, pattern, replacement) -- return val after applying ' - 'the regular expression. All instances of `pattern` are replaced ' - 'with `replacement`. As in all of calibre, these are ' - 'Python-compatible regular expressions') + __doc__ = doc = _( +''' +``re(value, pattern, replacement)`` -- return the value after applying the regular +expression. All instances of ``pattern`` in the value are replaced with +``replacement``. The template language uses case insensitive +[URL href="https://docs.python.org/3/library/re.html"]Python regular +expressions[/URL]. +''') def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement): return re.sub(pattern, replacement, val, flags=re.I) @@ -855,16 +967,19 @@ class BuiltinReGroup(BuiltinFormatterFunction): name = 're_group' arg_count = -1 category = 'String manipulation' - __doc__ = doc = _('re_group(val, pattern [, template_for_group]*) -- ' - 'return a string made by applying the regular expression pattern ' - 'to the val and replacing each matched instance with the string ' - 'computed by replacing each matched group by the value returned ' - 'by the corresponding template. The original matched value for the ' - 'group is available as $. In template program mode, like for ' - 'the template and the eval functions, you use [[ for { and ]] for }.' - ' The following example in template program mode looks for series ' - 'with more than one word and uppercases the first word: ' - "{series:'re_group($, \"(\\S* )(.*)\", \"[[$:uppercase()]]\", \"[[$]]\")'}") + __doc__ = doc = _( +''' +``re_group(value, pattern [, template_for_group]*)`` -- return a string made by +applying the regular expression pattern to ``value`` and replacing each matched +instance with the value returned by the corresponding template. In +`Template Program Mode`, like for the ``template`` and the +``eval`` functions, you use ``[[`` for ``{`` and ``]]`` for ``}``. + +The following example looks for a series with more than one word and uppercases the first word: +[CODE] +program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'} +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, val, pattern, *args): from calibre.utils.formatter import EvalFormatter @@ -890,10 +1005,12 @@ class BuiltinSwapAroundComma(BuiltinFormatterFunction): name = 'swap_around_comma' arg_count = 1 category = 'String manipulation' - __doc__ = doc = _('swap_around_comma(val) -- given a value of the form ' - '"B, A", return "A B". This is most useful for converting names ' - 'in LN, FN format to FN LN. If there is no comma, the function ' - 'returns val unchanged') + __doc__ = doc = _( +''' +``swap_around_comma(value)`` -- given a value of the form ``B, A``, return ``A B``. +This is most useful for converting names in LN, FN format to FN LN. If there is +no comma in the value then the function returns the value unchanged. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip() @@ -903,8 +1020,11 @@ 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`') + __doc__ = doc = _( +''' +``ifempty(value, text if empty)`` -- if the value is not empty then return that value, +otherwise return ``text if empty``. +''') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty): if val: @@ -917,18 +1037,23 @@ 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 val, consisting of `left chars` ' - 'characters from the beginning of val, followed by ' - '`middle text`, followed by `right chars` characters from ' - 'the end of the string. `Left chars` and `right chars` must be ' - 'integers. For example, assume the title of the book is ' - '`Ancient English Laws in the Times of Ivanhoe`, and you want ' - 'it to fit in a space of at most 15 characters. If you use ' - '{title:shorten(9,-,5)}, the result will be `Ancient E-anhoe`. ' - 'If the field\'s length is less than left chars + right chars + ' - 'the length of `middle text`, then the field will be used ' - 'intact. For example, the title `The Dome` would not be changed.') + __doc__ = doc = _( +''' +``shorten(value, left chars, middle text, right chars)`` -- Return a shortened version +of the value, consisting of ``left chars`` characters from the beginning of the +value, followed by ``middle text``, followed by ``right chars`` characters from +the end of the value. ``Left chars`` and ``right chars`` must be non-negative +integers. + +Example: assume you want to display the title with a length of at most +15 characters in length. One template that does this is +``{title:shorten(9,-,5)}``. For a book with the title `Ancient English Laws in +the Times of Ivanhoe` the result will be `Ancient E-anhoe`: the first 9 +characters of the title, a ``-``, then the last 5 characters. If the value's +length is less than ``left chars`` + ``right chars`` + the length of ``middle text`` +then the value will be returned unchanged. For example, the title `The +Dome` would not be changed. +''') def evaluate(self, formatter, kwargs, mi, locals, val, leading, center_string, trailing): @@ -941,16 +1066,21 @@ class BuiltinShorten(BuiltinFormatterFunction): class BuiltinCount(BuiltinFormatterFunction): - name = 'count' + name = 'list_count' arg_count = 2 category = 'List manipulation' - aliases = ['list_count'] + aliases = ['count'] - __doc__ = doc = _('count(val, separator) -- interprets the value as a list of items ' - 'separated by `separator`, returning the number of items in the ' - 'list. Most lists use a comma as the separator, but authors ' - 'uses an ampersand. Examples: {tags:count(,)}, {authors:count(&)}. ' - 'Aliases: count(), list_count()') + __doc__ = doc = _( +''' +``list_count(value, separator)`` -- interprets the value as a list of items separated by +``separator`` and returns the number of items in the list. Most lists use +a comma as the separator, but ``authors`` uses an ampersand (&). + +Examples: ``{tags:list_count(,)}``, ``{authors:list_count(&)}``. + +Aliases: ``count()``, ``list_count()`` +''') def evaluate(self, formatter, kwargs, mi, locals, val, sep): return str(len([v for v in val.split(sep) if v])) @@ -962,10 +1092,14 @@ class BuiltinListCountMatching(BuiltinFormatterFunction): category = 'List manipulation' aliases = ['count_matching'] - __doc__ = doc = _('list_count_matching(list, pattern, separator) -- ' - "interprets 'list' as a list of items separated by 'separator', " - 'returning the number of items in the list that match the regular ' - "expression 'pattern'. Aliases: list_count_matching(), count_matching()") + __doc__ = doc = _( +''' +``list_count_matching(list, pattern, separator)`` -- interprets ``list`` as a +list of items separated by ``separator``, returning the number of items in the +list that match the regular expression ``pattern``. + +Aliases: ``list_count_matching()``, ``count_matching()`` +''') def evaluate(self, formatter, kwargs, mi, locals, list_, pattern, sep): res = 0 @@ -979,12 +1113,15 @@ 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 ' - 'using `list_item(-1,separator)`. If the item is not in the list, ' - 'then the empty value is returned. The separator has the same ' - 'meaning as in the count function.') + __doc__ = doc = _( +''' +``list_item(value, 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 has the index ``-1`` as in +``list_item(-1,separator)``. If the item is not in the list, then the empty +string is returned. The separator has the same meaning as in the count function, +usually comma but is ampersand for author-like lists. +''') def evaluate(self, formatter, kwargs, mi, locals, val, index, sep): if not val: @@ -1001,11 +1138,14 @@ 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. Returns the ' - 'empty string if no match is found.' - ) + __doc__ = doc = _( +''' +``select(value, key)`` -- interpret the value as a comma-separated list of items with +each item having the form ``id:value`` (the calibre ``identifier`` format). The +function finds the first pair with the id equal to key and returns the +corresponding value. If no id matches then the function returns the empty +string. +''') def evaluate(self, formatter, kwargs, mi, locals, val, key): if not val: @@ -1022,19 +1162,20 @@ class BuiltinApproximateFormats(BuiltinFormatterFunction): name = 'approximate_formats' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('approximate_formats() -- return a comma-separated ' - 'list of formats that at one point were associated with the ' - 'book. There is no guarantee that this list is correct, ' - 'although it probably is. ' - 'This function can be called in template program mode using ' - 'the template "{:\'approximate_formats()\'}". ' - 'Note that format names are always uppercase, as in EPUB. ' - 'This function works only in the GUI. If you want to use these values ' - 'in save-to-disk or send-to-device templates then you ' - 'must make a custom "Column built from other columns", use ' - 'the function in that column\'s template, and use that ' - 'column\'s value in your save/send templates' - ) + __doc__ = doc = _( +''' +``approximate_formats()`` -- return a comma-separated list of formats associated +with the book. Because the list comes from calibre's database instead of the +file system, there is no guarantee that the list is correct, although it +probably is. Note that resulting format names are always uppercase, as in EPUB. +The ``approximate_formats()`` function is much faster than the ``formats_...`` +functions. + +This function works only in the GUI. If you want to use these values in save-to- +disk or send-to-device templates then you must make a custom "Column built from +other columns", use the function in that column's template, and use that +column's value in your save/send templates. +''') def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): @@ -1050,15 +1191,16 @@ class BuiltinFormatsModtimes(BuiltinFormatterFunction): name = 'formats_modtimes' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _('formats_modtimes(date_format) -- return a comma-separated ' - 'list of colon-separated items representing modification times ' - 'for the formats of a book. The date_format parameter ' - 'specifies how the date is to be formatted. See the ' - 'format_date function for details. You can use the select ' - 'function to get the mod time for a specific ' - 'format. Note that format names are always uppercase, ' - 'as in EPUB.' - ) + __doc__ = doc = _( +''' +``formats_modtimes(date_format_string)`` -- return a comma-separated list of +colon-separated items ``FMT:DATE`` representing modification times for the +formats of a book. The ``date_format_string`` parameter specifies how the date +is to be formatted. See the ``format_date()`` function for details. You can use +the ``select()`` function to get the modification time for a specific format. Note +that format names are always uppercase, as in EPUB. +''') + def evaluate(self, formatter, kwargs, mi, locals, fmt): fmt_data = mi.get('format_metadata', {}) @@ -1074,13 +1216,13 @@ class BuiltinFormatsSizes(BuiltinFormatterFunction): name = 'formats_sizes' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('formats_sizes() -- return a comma-separated list of ' - 'colon-separated items representing sizes in bytes ' - 'of the formats of a book. You can use the select ' - 'function to get the size for a specific ' - 'format. Note that format names are always uppercase, ' - 'as in EPUB.' - ) + __doc__ = doc = _( +''' +``formats_sizes()`` -- return a comma-separated list of colon-separated +``FMT:SIZE`` items giving the sizes of the formats of a book in bytes. You can +use the ``select()`` function to get the size for a specific format. Note that +format names are always uppercase, as in EPUB. +''') def evaluate(self, formatter, kwargs, mi, locals): fmt_data = mi.get('format_metadata', {}) @@ -1094,12 +1236,13 @@ class BuiltinFormatsPaths(BuiltinFormatterFunction): name = 'formats_paths' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('formats_paths() -- return a comma-separated list of ' - 'colon-separated items representing full path to ' - 'the formats of a book. You can use the select ' - 'function to get the path for a specific ' - 'format. Note that format names are always uppercase, ' - 'as in EPUB.') + __doc__ = doc = _( +''' +``formats_paths()`` -- return a comma-separated list of colon-separated items +``FMT:PATH`` giving the full path to the formats of a book. You can use the +``select()`` function to get the path for a specific format. Note that format names +are always uppercase, as in EPUB. +''') def evaluate(self, formatter, kwargs, mi, locals): fmt_data = mi.get('format_metadata', {}) @@ -1113,9 +1256,11 @@ class BuiltinHumanReadable(BuiltinFormatterFunction): name = 'human_readable' arg_count = 1 category = 'Formatting values' - __doc__ = doc = _('human_readable(v) -- return a string ' - 'representing the number v in KB, MB, GB, etc.' - ) + __doc__ = doc = _( +''' +``human_readable(value)`` -- expects the value to be a number and returns a string +representing that number in KB, MB, GB, etc. +''') def evaluate(self, formatter, kwargs, mi, locals, val): try: @@ -1128,15 +1273,17 @@ class BuiltinFormatNumber(BuiltinFormatterFunction): name = 'format_number' arg_count = 2 category = 'Formatting values' - __doc__ = doc = _('format_number(v, template) -- format the number v using ' - 'a Python formatting template such as "{0:5.2f}" or ' - '"{0:,d}" or "${0:5,.2f}". The field_name part of the ' - 'template must be a 0 (zero) (the "{0:" in the above examples). ' - 'See the template language and Python documentation for more ' - 'examples. You can leave off the leading "{0:" and trailing ' - '"}" if the template contains only a format. Returns the empty ' - 'string if formatting fails.' - ) + __doc__ = doc = _( +''' +``format_number(value, template)`` -- interprets the value as a number and formats that +number using a Python formatting template such as ``{0:5.2f}`` or ``{0:,d}`` or +``${0:5,.2f}``. The formatting template must begin with ``{0:`` and end with +``}`` as in the above examples. Exception: you can leave off the leading "{0:" +and trailing "}" if the format template contains only a format. See the template +language and the [URL href="https://docs.python.org/3/library/string.html#formatstrings"] +Python documentation[/URL] +for more examples. Returns the empty string if formatting fails. +''') def evaluate(self, formatter, kwargs, mi, locals, val, template): if val == '' or val == 'None': @@ -1164,18 +1311,21 @@ 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. ' - 'The first item is number zero. If an index is negative, then it ' - 'counts from the end of the list. As a special case, an end_index ' - 'of zero is assumed to be the length of the list. Examples using ' - 'basic template mode and assuming that the tags column (which is ' - 'comma-separated) contains "A, B, C": ' - '{tags:sublist(0,1,\\\\,)} returns "A". ' - '{tags:sublist(-1,0,\\\\,)} returns "C". ' - '{tags:sublist(0,-1,\\\\,)} returns "A, B".' - ) + __doc__ = doc = _( +''' +``sublist(value, start_index, end_index, separator)`` -- interpret the value as a +list of items separated by ``separator``, returning a new list made from the +items from ``start_index`` to ``end_index``. The first item is number zero. If +an index is negative, then it counts from the end of the list. As a special +case, an end_index of zero is assumed to be the length of the list. + +Examples assuming that the tags column (which is comma-separated) contains "A, B ,C": +[LIST] +[*]``{tags:sublist(0,1,\,)}`` returns "A" +[*]``{tags:sublist(-1,0,\,)}`` returns "C" +[*]``{tags:sublist(0,-1,\,)}`` returns "A, B" +[/LIST] +''') def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep): if not val: @@ -1200,21 +1350,32 @@ 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-' - 'separated list. Returns a new list made by first finding all the ' - 'period-separated items, then for each such item extracting the ' - '`start_index` to the `end_index` components, then combining ' - 'the results back together. The first component in a period-' - 'separated list has an index of zero. If an index is negative, ' - 'then it counts from the end of the list. As a special case, an ' - 'end_index of zero is assumed to be the length of the list. ' - 'Example using basic template mode and assuming a #genre value of ' - '"A.B.C": {#genre:subitems(0,1)} returns "A". {#genre:subitems(0,2)} ' - 'returns "A.B". {#genre:subitems(1,0)} returns "B.C". Assuming a #genre ' - 'value of "A.B.C, D.E.F", {#genre:subitems(0,1)} returns "A, D". ' - '{#genre:subitems(0,2)} returns "A.B, D.E"') + __doc__ = doc = _( +''' +``subitems(value, start_index, end_index)`` -- This function breaks apart lists of tag- +like hierarchical items such as genres. It interprets the value as a comma- +separated list of tag-like items, where each item is a period-separated list. It +returns a new list made by extracting from each item the components from +``start_index`` to ``end_index``, then merging the results back together. +Duplicates are removed. The first subitem in a period-separated list has an +index of zero. If an index is negative then it counts from the end of the list. +As a special case, an end_index of zero is assumed to be the length of the list. + +Examples: +[LIST] +[*]Assuming a #genre column containing "A.B.C": +[LIST] +[*]``{#genre:subitems(0,1)}`` returns "A" +[*]``{#genre:subitems(0,2)}`` returns "A.B" +[*]``{#genre:subitems(1,0)}`` returns "B.C" +[/LIST] +[*]Assuming a #genre column containing "A.B.C, D.E": +[LIST] +[*]``{#genre:subitems(0,1)}`` returns "A, D" +[*]``{#genre:subitems(0,2)}`` returns "A.B, D.E" +[/LIST] +[/LIST] +''') period_pattern = re.compile(r'(?<=[^\.\s])\.(?=[^\.\s])', re.U) @@ -1247,34 +1408,47 @@ class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 category = 'Formatting values' - __doc__ = doc = _('format_date(val, format_string) -- format the value, ' - 'which must be a date, using the format_string, returning a string. ' - 'It is best if the date is in ISO format because using other date ' - 'formats often causes errors because the actual date value cannot be ' - 'unambiguously determined. Note that the format_date_field() function ' - 'is both faster and more reliable. ' - 'The formatting codes are: ' - 'd : the day as number without a leading zero (1 to 31) ' - 'dd : the day as number with a leading zero (01 to 31) ' - 'ddd : the abbreviated localized day name (e.g. "Mon" to "Sun"). ' - 'dddd : the long localized day name (e.g. "Monday" to "Sunday"). ' - 'M : the month as number without a leading zero (1 to 12). ' - 'MM : the month as number with a leading zero (01 to 12) ' - 'MMM : the abbreviated localized month name (e.g. "Jan" to "Dec"). ' - 'MMMM : the long localized month name (e.g. "January" to "December"). ' - 'yy : the year as two digit number (00 to 99). ' - 'yyyy : the year as four digit number. ' - 'h : the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm) ' - 'hh : the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm) ' - 'm : the minutes without a leading 0 (0 to 59) ' - 'mm : the minutes with a leading 0 (00 to 59) ' - 's : the seconds without a leading 0 (0 to 59) ' - 'ss : the seconds with a leading 0 (00 to 59) ' - 'ap : use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the localized string for am or pm ' - 'AP : use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the localized string for AM or PM ' - 'iso : the date with time and timezone. Must be the only format present ' - 'to_number: the date as a floating point number ' - 'from_number[:fmt]: format the timestamp using fmt if present otherwise iso') + __doc__ = doc = _( +''' +``format_date(value, format_string)`` -- format the value, which must be a date +string, using the format_string, returning a string. It is best if the date is +in ISO format as using other date formats often causes errors because the actual +date value cannot be unambiguously determined. Note that the +``format_date_field()`` function is both faster and more reliable. + +The formatting codes are: +[LIST] +[*]``d :`` the day as number without a leading zero (1 to 31) +[*]``dd :`` the day as number with a leading zero (01 to 31) +[*]``ddd :`` the abbreviated localized day name (e.g. "Mon" to "Sun"). +[*]``dddd :`` the long localized day name (e.g. "Monday" to "Sunday"). +[*]``M :`` the month as number without a leading zero (1 to 12). +[*]``MM :`` the month as number with a leading zero (01 to 12) +[*]``MMM :`` the abbreviated localized month name (e.g. "Jan" to "Dec"). +[*]``MMMM :`` the long localized month name (e.g. "January" to "December"). +[*]``yy :`` the year as two digit number (00 to 99). +[*]``yyyy :`` the year as four digit number. +[*]``h :`` the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm) +[*]``hh :`` the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm) +[*]``m :`` the minutes without a leading 0 (0 to 59) +[*]``mm :`` the minutes with a leading 0 (00 to 59) +[*]``s :`` the seconds without a leading 0 (0 to 59) +[*]``ss :`` the seconds with a leading 0 (00 to 59) +[*]``ap :`` use a 12-hour clock instead of a 24-hour clock, with 'ap' replaced by the localized string for am or pm. +[*]``AP :`` use a 12-hour clock instead of a 24-hour clock, with 'AP' replaced by the localized string for AM or PM. +[*]``iso :`` the date with time and timezone. Must be the only format present. +[*]``to_number :`` convert the date & time into a floating point number (a `timestamp`) +[*]``from_number :`` convert a floating point number (a `timestamp`) into an +ISO-formatted date. If you want a different date format then add the +desired formatting string after ``from_number`` and a colon (``:``). Example: +[CODE] +format_date(val, 'from_number:MMM dd yyyy') +[/CODE] +[/LIST] +You might get unexpected results if the date you are formatting contains +localized month names, which can happen if you changed the date format to +contain ``MMMM``. Using ``format_date_field()`` avoids this problem. +''') def evaluate(self, formatter, kwargs, mi, locals, val, format_string): if not val or val == 'None': @@ -1298,13 +1472,20 @@ class BuiltinFormatDateField(BuiltinFormatterFunction): name = 'format_date_field' arg_count = 2 category = 'Formatting values' - __doc__ = doc = _("format_date_field(field_name, format_string) -- format " - "the value in the field 'field_name', which must be the lookup name " - "of date field, either standard or custom. See 'format_date' for " - "the formatting codes. This function is much faster than format_date " - "and should be used when you are formatting the value in a field " - "(column). It can't be used for computed dates or dates in string " - "variables. Example: format_date_field('pubdate', 'yyyy.MM.dd')") + __doc__ = doc = _( +''' + ``format_date_field(field_name, format_string)`` -- format the value in the + field ``field_name``, which must be the lookup name of a date field, either + standard or custom. See ``format_date()`` for the formatting codes. This + function is much faster than format_date() and should be used when you are + formatting the value in a field (column). It is also more reliable because it + works directly on the underlying date. It can't be used for computed dates or + dates in string variables. Examples: +[CODE] +format_date_field('pubdate', 'yyyy.MM.dd') +format_date_field('#date_read', 'MMM dd, yyyy') +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, field, format_string): try: @@ -1337,7 +1518,10 @@ class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 category = 'String case changes' - __doc__ = doc = _('uppercase(val) -- return val in upper case') + __doc__ = doc = _( +''' +``uppercase(value)`` -- returns the value in upper case. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper() @@ -1347,7 +1531,10 @@ class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 category = 'String case changes' - __doc__ = doc = _('lowercase(val) -- return val in lower case') + __doc__ = doc = _( +''' +``lowercase(value)`` -- returns the value in lower case. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower() @@ -1357,7 +1544,10 @@ class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 category = 'String case changes' - __doc__ = doc = _('titlecase(val) -- return val in title case') + __doc__ = doc = _( +''' +``titlecase(value)`` -- returns the value in title case. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val) @@ -1367,7 +1557,10 @@ class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 category = 'String case changes' - __doc__ = doc = _('capitalize(val) -- return val capitalized') + __doc__ = doc = _( +''' +``capitalize(value)`` -- returns the value with the first letter in upper case and the rest lower case. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) @@ -1377,12 +1570,15 @@ class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('booksize() -- return value of the size field. ' - 'This function works only in the GUI. If you want to use this value ' - 'in save-to-disk or send-to-device templates then you ' - 'must make a custom "Column built from other columns", use ' - 'the function in that column\'s template, and use that ' - 'column\'s value in your save/send templates') + __doc__ = doc = _( +''' +``booksize()`` -- returns the value of the calibre ``size`` field. Returns '' if the book has no formats. + +This function works only in the GUI. If you want to use this value in save-to-disk +or send-to-device templates then you must make a custom "Column built from +other columns", use the function in that column's template, and use that +column's value in your save/send templates +''') def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): @@ -1401,12 +1597,14 @@ 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. This function works only in the GUI. If you want to ' - 'use this value in save-to-disk or send-to-device templates then you ' - 'must make a custom "Column built from other columns", use ' - 'the function in that column\'s template, and use that ' - 'column\'s value in your save/send templates') + __doc__ = doc = _( +''' +``ondevice()`` -- return the string ``'Yes'`` if ``ondevice`` is set, otherwise +return the empty string. This function works only in the GUI. If you want to use +this value in save-to-disk or send-to-device templates then you must make a +custom "Column built from other columns", use the function in that column\'s +template, and use that column\'s value in your save/send templates. +''') def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): @@ -1420,9 +1618,11 @@ class BuiltinAnnotationCount(BuiltinFormatterFunction): name = 'annotation_count' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('annotation_count() -- return the total number of annotations ' - 'of all types attached to the current book. ' - 'This function works only in the GUI.') + __doc__ = doc = _( +''' +``annotation_count()`` -- return the total number of annotations of all types +attached to the current book. This function works only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).new_api.annotation_count_for_book(mi.id) @@ -1433,10 +1633,13 @@ class BuiltinIsMarked(BuiltinFormatterFunction): name = 'is_marked' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _("is_marked() -- check whether the book is 'marked' in " - "calibre. If it is then return the value of the mark, " - "either 'true' or the comma-separated list of named " - "marks. Returns '' if the book is not marked.") + __doc__ = doc = _( +''' +``is_marked()`` -- check whether the book is `marked` in calibre. If it is then +return the value of the mark, either ``'true'`` (lower case) or a comma- +separated list of named marks. Returns ``''`` (the empty string) if the book is +not marked. This function works only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).data.get_marked(mi.id) @@ -1447,7 +1650,10 @@ class BuiltinSeriesSort(BuiltinFormatterFunction): name = 'series_sort' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('series_sort() -- return the series sort value') + __doc__ = doc = _( +''' +``series_sort()`` -- returns the series sort value. +''') def evaluate(self, formatter, kwargs, mi, locals): if mi.series: @@ -1461,8 +1667,10 @@ class BuiltinHasCover(BuiltinFormatterFunction): name = 'has_cover' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('has_cover() -- return Yes if the book has a cover, ' - 'otherwise return the empty string') + __doc__ = doc = _( +''' +``has_cover()`` -- return ``'Yes'`` if the book has a cover, otherwise the empty string. +''') def evaluate(self, formatter, kwargs, mi, locals): if mi.has_cover: @@ -1474,10 +1682,12 @@ 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 string is returned. ' - 'You can have as many values as you want.') + __doc__ = doc = _( +''' +``first_non_empty(value [, value]*)`` -- returns the first ``value`` that is not +empty. If all values are empty, then the empty string is returned. You can have +as many values as you want. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 @@ -1492,11 +1702,15 @@ 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 ' - 'first_non_empty. You can have as many values as you want. In many ' - 'cases the && operator can replace this function.') + __doc__ = doc = _( +''' +``and(value [, value]*)`` -- returns the string "1" if all values are not empty, +otherwise returns the empty string. You can have as many values as you want. In +most cases you can use the ``&&`` operator instead of this function. One reason +not to replace ``and()`` with ``&&`` is when short-circuiting can change the results +because of side effects. For example, ``and(a='',b=5)`` will always do both +assignments, where the ``&&`` operator won't do the second. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 @@ -1511,11 +1725,14 @@ 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 ' - 'first_non_empty. You can have as many values as you want. In many ' - 'cases the || operator can replace this function.') + __doc__ = doc = _( +''' +``or(value [, value]*)`` -- returns the string ``'1'`` if any value is not +empty, otherwise returns the empty string. You can have as many values as you +want. This function can usually be replaced by the ``||`` operator. A reason it +cannot be replaced is if short-circuiting will change the results because of +side effects. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 @@ -1530,11 +1747,12 @@ 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 ' - 'first_non_empty. In many cases the ! operator can replace this ' - 'function.') + __doc__ = doc = _( +''' +``not(value)`` -- returns the string "1" if the value is empty, otherwise +returns the empty string. This function can usually be replaced with the unary +not (``!``) operator. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return '' if val else '1' @@ -1544,32 +1762,38 @@ class BuiltinListJoin(BuiltinFormatterFunction): name = 'list_join' arg_count = -1 category = 'List manipulation' - __doc__ = doc = _("list_join(with_separator, list1, separator1 [, list2, separator2]*) -- " - "return a list made by joining the items in the source lists " - "(list1, etc) using with_separator between the items in the " - "result list. Items in each source list[123...] are separated " - "by the associated separator[123...]. A list can contain " - "zero values. It can be a field like publisher that is " - "single-valued, effectively a one-item list. Duplicates " - "are removed using a case-insensitive comparison. Items are " - "returned in the order they appear in the source lists. " - "If items on lists differ only in letter case then the last " - "is used. All separators can be more than one character.\n" - "Example:") + "\n\n" + ( - " program:\n" - " list_join('#@#', $authors, '&', $tags, ',')\n\n") + _( - "You can use list_join on the results of previous " - "calls to list_join as follows:") + "\n" + ( - " program:\n\n" - " a = list_join('#@#', $authors, '&', $tags, ',');\n" - " b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&')\n\n") + _( - "You can use expressions to generate a list. For example, " - "assume you want items for authors and #genre, but " - "with the genre changed to the word 'Genre: ' followed by " - "the first letter of the genre, i.e. the genre 'Fiction' " - "becomes 'Genre: F'. The following will do that:") + "\n" + ( - " program:\n" - " list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \\1'), ',')") + __doc__ = doc = _( +''' +``list_join(with_separator, list1, separator1 [, list2, separator2]*)`` -- +return a list made by joining the items in the source lists (``list1`` etc) +using ``with_separator`` between the items in the result list. Items in each +source ``list[123...]`` are separated by the associated ``separator[123...]``. A +list can contain zero values. It can be a field like ``publisher`` that is +single-valued, effectively a one-item list. Duplicates are removed using a case- +insensitive comparison. Items are returned in the order they appear in the +source lists. If items on lists differ only in letter case then the last is +used. All separators can be more than one character. + +Example: +[CODE] +program: + list_join('#@#', $authors, '&', $tags, ',') +[/CODE] +You can use ``list_join`` on the results of previous calls to ``list_join`` as follows: +[CODE] +program: + a = list_join('#@#', $authors, '&', $tags, ','); + b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',') +[/CODE] +You can use expressions to generate a list. For example, assume you want items +for ``authors`` and ``#genre``, but with the genre changed to the word "Genre: " +followed by the first letter of the genre, i.e. the genre "Fiction" becomes +"Genre: F". The following will do that: +[CODE] +program: + list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',') +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, with_separator, *args): if len(args) % 2 != 0: @@ -1591,12 +1815,14 @@ class BuiltinListUnion(BuiltinFormatterFunction): name = 'list_union' arg_count = 3 category = 'List manipulation' - __doc__ = doc = _('list_union(list1, list2, separator) -- ' - 'return a list made by merging the items in list1 and list2, ' - 'removing duplicate items using a case-insensitive comparison. If ' - 'items differ in case, the one in list1 is used. ' - 'The items in list1 and list2 are separated by separator, as are ' - 'the items in the returned list. Aliases: list_union(), merge_lists()') + __doc__ = doc = _( +''' +``list_union(list1, list2, separator)`` -- return a list made by merging the +items in ``list1`` and ``list2``, removing duplicate items using a case- +insensitive comparison. If items differ in case, the one in ``list1`` is used. +The items in ``list1`` and ``list2`` are separated by ``separator``, as are the +items in the returned list. Aliases: ``merge_lists()``, ``list_union()`` +''') aliases = ['merge_lists'] def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): @@ -1611,26 +1837,31 @@ class BuiltinRange(BuiltinFormatterFunction): name = 'range' arg_count = -1 category = 'List manipulation' - __doc__ = doc = _("range(start, stop, step, limit) -- " - "returns a list of numbers generated by looping over the " - "range specified by the parameters start, stop, and step, " - "with a maximum length of limit. The first value produced " - "is 'start'. Subsequent values next_v are " - "current_v+step. The loop continues while " - "next_v < stop assuming step is positive, otherwise " - "while next_v > stop. An empty list is produced if " - "start fails the test: start>=stop if step " - "is positive. The limit sets the maximum length of " - "the list and has a default of 1000. The parameters " - "start, step, and limit are optional. " - "Calling range() with one argument specifies stop. " - "Two arguments specify start and stop. Three arguments " - "specify start, stop, and step. Four " - "arguments specify start, stop, step and limit. " - "Examples: range(5) -> '0,1,2,3,4'. range(0,5) -> '0,1,2,3,4'. " - "range(-1,5) -> '-1,0,1,2,3,4'. range(1,5) -> '1,2,3,4'. " - "range(1,5,2) -> '1,3'. range(1,5,2,5) -> '1,3'. " - "range(1,5,2,1) -> error(limit exceeded).") + __doc__ = doc = _( +''' +``range(start, stop, step, limit)`` -- returns a list of numbers generated by +looping over the range specified by the parameters start, stop, and step, with a +maximum length of limit. The first value produced is 'start'. Subsequent values +``next_v = current_v + step``. The loop continues while ``next_v < stop`` +assuming ``step`` is positive, otherwise while ``next_v > stop``. An empty list +is produced if ``start`` fails the test: ``start >= stop`` if ``step`` is +positive. The ``limit`` sets the maximum length of the list and has a default of +1000. The parameters ``start``, ``step``, and ``limit`` are optional. Calling +``range()`` with one argument specifies ``stop``. Two arguments specify +``start`` and ``stop``. Three arguments specify ``start``, ``stop``, and +``step``. Four arguments specify ``start``, ``stop``, ``step`` and ``limit``. + +Examples: +[CODE] +range(5) -> '0, 1, 2, 3, 4' +range(0, 5) -> '0, 1, 2, 3, 4' +range(-1, 5) -> '-1, 0, 1, 2, 3, 4' +range(1, 5) -> '1, 2, 3, 4' +range(1, 5, 2) -> '1, 3' +range(1, 5, 2, 5) -> '1, 3' +range(1, 5, 2, 1) -> error(limit exceeded) +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, *args): limit_val = 1000 @@ -1659,11 +1890,13 @@ class BuiltinListRemoveDuplicates(BuiltinFormatterFunction): name = 'list_remove_duplicates' arg_count = 2 category = 'List manipulation' - __doc__ = doc = _('list_remove_duplicates(list, separator) -- ' - 'return a list made by removing duplicate items in the source list. ' - 'If items differ only in case, the last of them is returned. ' - 'The items in source list are separated by separator, as are ' - 'the items in the returned list.') + __doc__ = doc = _( +''' +``list_remove_duplicates(list, separator)`` -- return a list made by removing +duplicate items in ``list``. If items differ only in case then the last is +returned. The items in ``list`` are separated by ``separator``, as are the items +in the returned list. +''') def evaluate(self, formatter, kwargs, mi, locals, list_, separator): res = {icu_lower(l.strip()): l.strip() for l in list_.split(separator) if l.strip()} @@ -1676,10 +1909,13 @@ class BuiltinListDifference(BuiltinFormatterFunction): name = 'list_difference' arg_count = 3 category = 'List manipulation' - __doc__ = doc = _('list_difference(list1, list2, separator) -- ' - 'return a list made by removing from list1 any item found in list2, ' - 'using a case-insensitive comparison. The items in list1 and list2 ' - 'are separated by separator, as are the items in the returned list.') + __doc__ = doc = _( +''' +``list_difference(list1, list2, separator)`` -- return a list made by removing +from ``list1`` any item found in ``list2`` using a case-insensitive comparison. +The items in ``list1`` and ``list2`` are separated by separator, as are the +items in the returned list. +''') def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): l1 = [l.strip() for l in list1.split(separator) if l.strip()] @@ -1698,10 +1934,13 @@ class BuiltinListIntersection(BuiltinFormatterFunction): name = 'list_intersection' arg_count = 3 category = 'List manipulation' - __doc__ = doc = _('list_intersection(list1, list2, separator) -- ' - 'return a list made by removing from list1 any item not found in list2, ' - 'using a case-insensitive comparison. The items in list1 and list2 ' - 'are separated by separator, as are the items in the returned list.') + __doc__ = doc = _( +''' +``list_intersection(list1, list2, separator)`` -- return a list made by removing +from ``list1`` any item not found in ``list2``, using a case-insensitive +comparison. The items in ``list1`` and ``list2`` are separated by separator, as +are the items in the returned list. +''') def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): l1 = [l.strip() for l in list1.split(separator) if l.strip()] @@ -1720,10 +1959,13 @@ class BuiltinListSort(BuiltinFormatterFunction): name = 'list_sort' arg_count = 3 category = 'List manipulation' - __doc__ = doc = _('list_sort(list, direction, separator) -- ' - 'return list sorted using a case-insensitive sort. If direction is ' - 'zero, the list is sorted ascending, otherwise descending. The list items ' - 'are separated by separator, as are the items in the returned list.') + __doc__ = doc = _( +''' +``list_sort(list, direction, separator)`` -- return ``list`` sorted using a +case-insensitive lexical sort. If ``direction`` is zero (number or character), +``list`` is sorted ascending, otherwise descending. The list items are separated +by ``separator``, as are the items in the returned list. +''') def evaluate(self, formatter, kwargs, mi, locals, list1, direction, separator): res = [l.strip() for l in list1.split(separator) if l.strip()] @@ -1736,12 +1978,14 @@ class BuiltinListEquals(BuiltinFormatterFunction): name = 'list_equals' arg_count = 6 category = 'List manipulation' - __doc__ = doc = _('list_equals(list1, sep1, list2, sep2, yes_val, no_val) -- ' - 'return yes_val if list1 and list2 contain the same items, ' - 'otherwise return no_val. The items are determined by splitting ' - 'each list using the appropriate separator character (sep1 or ' - 'sep2). The order of items in the lists is not relevant. ' - 'The comparison is case insensitive.') + __doc__ = doc = _( +''' +``list_equals(list1, sep1, list2, sep2, yes_val, no_val)`` -- return ``yes_val`` +if ``list1`` and ``list2`` contain the same items, otherwise return ``no_val``. +The items are determined by splitting each list using the appropriate separator +character (``sep1`` or ``sep2``). The order of items in the lists is not +relevant. The comparison is case-insensitive. +''') def evaluate(self, formatter, kwargs, mi, locals, list1, sep1, list2, sep2, yes_val, no_val): s1 = {icu_lower(l.strip()) for l in list1.split(sep1) if l.strip()} @@ -1755,12 +1999,14 @@ class BuiltinListRe(BuiltinFormatterFunction): name = 'list_re' arg_count = 4 category = 'List manipulation' - __doc__ = doc = _('list_re(src_list, separator, include_re, opt_replace) -- ' - 'Construct a list by first separating src_list into items using ' - 'the separator character. For each item in the list, check if it ' - 'matches include_re. If it does, then add it to the list to be ' - 'returned. If opt_replace is not the empty string, then apply the ' - 'replacement before adding the item to the returned list.') + __doc__ = doc = _( +''' +``list_re(src_list, separator, include_re, opt_replace)`` -- Construct a list by +first separating ``src_list`` into items using the ``separator`` character. For +each item in the list, check if it matches ``include_re``. If it does then add +it to the list to be returned. If ``opt_replace`` is not the empty string then +apply the replacement before adding the item to the returned list. +''') def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, opt_replace): l = [l.strip() for l in src_list.split(separator) if l.strip()] @@ -1781,10 +2027,12 @@ class BuiltinListReGroup(BuiltinFormatterFunction): name = 'list_re_group' arg_count = -1 category = 'List manipulation' - __doc__ = doc = _('list_re_group(src_list, separator, include_re, search_re [, group_template]+) -- ' - 'Like list_re except replacements are not optional. It ' - 'uses re_group(list_item, search_re, group_template, ...) when ' - 'doing the replacements on the resulting list.') + __doc__ = doc = _( +''' +``list_re_group(src_list, separator, include_re, search_re [,template_for_group]*)`` +-- Like list_re except replacements are not optional. It +uses ``re_group(item, search_re, template ...)`` when doing the replacements. +''') def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, search_re, *args): @@ -1821,10 +2069,13 @@ class BuiltinToday(BuiltinFormatterFunction): name = 'today' arg_count = 0 category = 'Date functions' - __doc__ = doc = _('today() -- ' - 'return a date string for today. This value is designed for use in ' - 'format_date or days_between, but can be manipulated like any ' - 'other string. The date is in ISO format.') + __doc__ = doc = _( +''' +``today()`` -- return a date+time string for today (now). This value is designed +for use in ``format_date`` or ``days_between``, but can be manipulated like any +other string. The date is in [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL] +date/time format. +''') def evaluate(self, formatter, kwargs, mi, locals): return format_date(now(), 'iso') @@ -1834,11 +2085,13 @@ class BuiltinDaysBetween(BuiltinFormatterFunction): name = 'days_between' arg_count = 2 category = 'Date functions' - __doc__ = doc = _('days_between(date1, date2) -- ' - 'return the number of days between date1 and date2. The number is ' - 'positive if date1 is greater than date2, otherwise negative. If ' - 'either date1 or date2 are not dates, the function returns the ' - 'empty string.') + __doc__ = doc = _( +''' +``days_between(date1, date2)`` -- return the number of days between ``date1`` +and ``date2``. The number is positive if ``date1`` is greater than ``date2``, +otherwise negative. If either ``date1`` or ``date2`` are not dates, the function +returns the empty string. +''') def evaluate(self, formatter, kwargs, mi, locals, date1, date2): try: @@ -1858,20 +2111,23 @@ class BuiltinDateArithmetic(BuiltinFormatterFunction): name = 'date_arithmetic' arg_count = -1 category = 'Date functions' - __doc__ = doc = _('date_arithmetic(date, calc_spec, fmt) -- ' - "Calculate a new date from 'date' using 'calc_spec'. Return the " - "new date formatted according to optional 'fmt': if not supplied " - "then the result will be in iso format. The calc_spec is a string " - "formed by concatenating pairs of 'vW' (valueWhat) where 'v' is a " - "possibly-negative number and W is one of the following letters: " - "s: add 'v' seconds to 'date' " - "m: add 'v' minutes to 'date' " - "h: add 'v' hours to 'date' " - "d: add 'v' days to 'date' " - "w: add 'v' weeks to 'date' " - "y: add 'v' years to 'date', where a year is 365 days. " - "Example: '1s3d-1m' will add 1 second, add 3 days, and subtract 1 " - "minute from 'date'.") + __doc__ = doc = _( +''' +``date_arithmetic(date, calc_spec, fmt)`` -- Calculate a new date from ``date`` +using ``calc_spec``. Return the new date formatted according to optional +``fmt``: if not supplied then the result will be in ISO format. The calc_spec is +a string formed by concatenating pairs of ``vW`` (``valueWhat``) where ``v`` is +a possibly-negative number and W is one of the following letters: +[LIST] +[*]``s``: add ``v`` seconds to ``date`` +[*]``m``: add ``v`` minutes to ``date`` +[*]``h``: add ``v`` hours to ``date`` +[*]``d``: add ``v`` days to ``date`` +[*]``w``: add ``v`` weeks to ``date`` +[*]``y``: add ``v`` years to ``date``, where a year is 365 days. +[/LIST] +Example: ``'1s3d-1m'`` will add 1 second, add 3 days, and subtract 1 minute from ``date``. + ''') calc_ops = { 's': lambda v: timedelta(seconds=v), @@ -1907,11 +2163,16 @@ class BuiltinLanguageStrings(BuiltinFormatterFunction): name = 'language_strings' arg_count = 2 category = 'Get values from metadata' - __doc__ = doc = _('language_strings(lang_codes, localize) -- ' - 'return the strings for the language codes passed in lang_codes. ' - 'If localize is zero, return the strings in English. If ' - 'localize is not zero, return the strings in the language of ' - 'the current locale. Lang_codes is a comma-separated list.') + __doc__ = doc = _( +""" +``language_strings(localize)`` -- return the +language names for the language codes +([URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"] +see here for names and codes[/URL]) +passed in as the value. Example: ``{languages:language_strings()}``. +If ``localize`` is zero, return the strings in English. If ``localize`` is not zero, +return the strings in the language of the current locale. ``Lang_codes`` is a comma-separated list. +""") def evaluate(self, formatter, kwargs, mi, locals, lang_codes, localize): retval = [] @@ -1929,10 +2190,13 @@ class BuiltinLanguageCodes(BuiltinFormatterFunction): name = 'language_codes' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _('language_codes(lang_strings) -- ' - 'return the language codes for the strings passed in lang_strings. ' - 'The strings must be in the language of the current locale. ' - 'Lang_strings is a comma-separated list.') + __doc__ = doc = _( +''' +``language_codes(lang_strings)`` -- return the +[URL href="https://www.loc.gov/standards/iso639-2/php/code_list.php"]language codes[/URL] for the language +names passed in ``lang_strings``. The strings must be in the language of the +current locale. ``Lang_strings`` is a comma-separated list. +''') def evaluate(self, formatter, kwargs, mi, locals, lang_strings): retval = [] @@ -1950,10 +2214,10 @@ class BuiltinCurrentLibraryName(BuiltinFormatterFunction): name = 'current_library_name' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('current_library_name() -- ' - 'return the last name on the path to the current calibre library. ' - 'This function can be called in template program mode using the ' - 'template "{:\'current_library_name()\'}".') + __doc__ = doc = _( +''' +``current_library_name()`` -- return the last name on the path to the current calibre library. +''') def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_name @@ -1964,10 +2228,11 @@ class BuiltinCurrentLibraryPath(BuiltinFormatterFunction): name = 'current_library_path' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('current_library_path() -- ' - 'return the path to the current calibre library. This function can ' - 'be called in template program mode using the template ' - '"{:\'current_library_path()\'}".') + __doc__ = doc = _( +''' +``current_library_path()`` -- return the full path to the current calibre +library. +''') def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_path @@ -1978,12 +2243,31 @@ class BuiltinFinishFormatting(BuiltinFormatterFunction): name = 'finish_formatting' arg_count = 4 category = 'Formatting values' - __doc__ = doc = _('finish_formatting(val, fmt, prefix, suffix) -- apply the ' - 'format, prefix, and suffix to a value in the same way as ' - 'done in a template like `{series_index:05.2f| - |- }`. For ' - 'example, the following program produces the same output ' - 'as the above template: ' - 'program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")') + __doc__ = doc = _( +''' +``finish_formatting(val, format, prefix, suffix)`` -- apply the ``format``, ``prefix``, and +``suffix`` to a value in the same way as done in a template like +``{series_index:05.2f| - |- }``. This function is provided to ease conversion of +complex single-function- or template-program-mode templates to `GPM` Templates. +For example, the following program produces the same output as the above +template: +[CODE] +program: finish_formatting(field("series_index"), "05.2f", " - ", " - ") +[/CODE] +Another example: for the template: +[CODE] +{series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} +[/CODE] +use: +[CODE] +program: + strcat( + re(field('series'), '([^\s])[^\s]+(\s|$)', '\\1'), + finish_formatting(field('series_index'), '0>2s', ' - ', ' - '), + field('title') + ) +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals_, val, fmt, prefix, suffix): if not val: @@ -1995,13 +2279,14 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction): name = 'virtual_libraries' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('virtual_libraries() -- return a comma-separated list of ' - 'Virtual libraries that contain this book. This function ' - 'works only in the GUI. If you want to use these values ' - 'in save-to-disk or send-to-device templates then you ' - 'must make a custom "Column built from other columns", use ' - 'the function in that column\'s template, and use that ' - 'column\'s value in your save/send templates') + __doc__ = doc = _( +''' +``virtual_libraries()`` -- return a comma-separated list of Virtual libraries that +contain this book. This function works only in the GUI. If you want to use these +values in save-to-disk or send-to-device templates then you must make a custom +"Column built from other columns", use the function in that column's template, +and use that column's value in your save/send templates. +''') def evaluate(self, formatter, kwargs, mi, locals_): db = self.get_database(mi) @@ -2016,10 +2301,16 @@ class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction): name = 'current_virtual_library_name' arg_count = 0 category = 'Get values from metadata' - __doc__ = doc = _('current_virtual_library_name() -- ' - 'return the name of the current virtual library if there is one, ' - 'otherwise the empty string. Library name case is preserved. ' - 'Example: "program: current_virtual_library_name()".') + __doc__ = doc = _( +''' +``current_virtual_library_name()`` -- return the name of the current +virtual library if there is one, otherwise the empty string. Library name case +is preserved. Example: +[CODE] +program: current_virtual_library_name() +[/CODE] +This function works only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals): return self.get_database(mi).data.get_base_restriction_name() @@ -2049,11 +2340,13 @@ class BuiltinTransliterate(BuiltinFormatterFunction): name = 'transliterate' arg_count = 1 category = 'String manipulation' - __doc__ = doc = _('transliterate(a) -- Returns a string in a latin alphabet ' - 'formed by approximating the sound of the words in the ' - 'source string. For example, if the source is "{0}"' - ' the function returns "{1}".').format( - "Фёдор Миха́йлович Достоевский", 'Fiodor Mikhailovich Dostoievskii') + __doc__ = doc = _( +''' +``transliterate(value)`` -- Return a string in a latin alphabet formed by +approximating the sound of the words in the source field. For example, if the +source field is ``Фёдор Миха́йлович Достоевский`` this function returns ``Fiodor +Mikhailovich Dostoievskii``. +''') def evaluate(self, formatter, kwargs, mi, locals, source): from calibre.utils.filenames import ascii_text @@ -2064,11 +2357,31 @@ class BuiltinGetLink(BuiltinFormatterFunction): name = 'get_link' arg_count = 2 category = 'Template database functions' - __doc__ = doc = _("get_link(field_name, field_value) -- fetch the link for " - "field 'field_name' with value 'field_value'. If there is " - "no attached link, return ''. Example: " - "get_link('tags', 'Fiction') returns the link attached to " - "the tag 'Fiction'.") + __doc__ = doc = _( +''' +``get_link(field_name, field_value)`` -- fetch the link for field ``field_name`` +with value ``field_value``. If there is no attached link, return the empty +string. Examples: +[LIST] +[*]The following returns the link attached to the tag ``Fiction``: +[CODE] +get_link('tags', 'Fiction') +[/CODE] +[*]This template makes a list of the links for all the tags associated with a +book in the form ``value:link, ...``: +[CODE] +program: + ans = ''; + for t in $tags: + l = get_link('tags', t); + if l then + ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',') + fi + rof; +ans +[/CODE] +[/LIST] +''') def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api @@ -2087,17 +2400,19 @@ class BuiltinAuthorLinks(BuiltinFormatterFunction): name = 'author_links' arg_count = 2 category = 'Get values from metadata' - __doc__ = doc = _('author_links(val_separator, pair_separator) -- returns ' - 'a string containing a list of authors and that author\'s ' - 'link values in the ' - 'form author1 val_separator author1link pair_separator ' - 'author2 val_separator author2link etc. An author is ' - 'separated from its link value by the val_separator string ' - 'with no added spaces. author:linkvalue pairs are separated ' - 'by the pair_separator string argument with no added spaces. ' - 'It is up to you to choose separator strings that do ' - 'not occur in author names or links. An author is ' - 'included even if the author link is empty.') + __doc__ = doc = _( +''' +``author_links(val_separator, pair_separator)`` -- returns a string containing a +list of authors and those authors' link values in the form: +``author1 val_separator author1_link pair_separator author2 val_separator author2_link`` etc. + +An author is separated from its link value by the ``val_separator`` string +with no added spaces. Assuming the ``val_separator`` is a colon, +``author:link value`` pairs are separated by the +``pair_separator`` string argument with no added spaces. It is up to you to +choose separators that do not occur in author names or links. An author +is included even if the author link is empty. +''') def evaluate(self, formatter, kwargs, mi, locals, val_sep, pair_sep): if hasattr(mi, '_proxy_metadata'): @@ -2116,15 +2431,16 @@ class BuiltinAuthorSorts(BuiltinFormatterFunction): name = 'author_sorts' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _('author_sorts(val_separator) -- returns a string ' - 'containing a list of author\'s sort values for the ' - 'authors of the book. The sort is the one in the author ' - 'metadata (different from the author_sort in books). The ' - 'returned list has the form author sort 1 val_separator ' - 'author sort 2 etc. The author sort values in this list ' - 'are in the same order as the authors of the book. If ' - 'you want spaces around val_separator then include them ' - 'in the separator string') + __doc__ = doc = _( +''' +``author_sorts(val_separator)`` -- returns a string containing a list of +author's sort values for the authors of the book. The sort is the one in the +author metadata information, which can be different from the author_sort in books. The +returned list has the form ``author sort 1`` ``val_separator`` ``author sort 2`` +etc. with no added spaces. The author sort values in this list are in the same +order as the authors of the book. If you want spaces around ``val_separator`` +then include them in the ``val_separator`` string. +''') def evaluate(self, formatter, kwargs, mi, locals, val_sep): sort_data = mi.author_sort_map @@ -2138,11 +2454,14 @@ class BuiltinConnectedDeviceName(BuiltinFormatterFunction): name = 'connected_device_name' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _("connected_device_name(storage_location) -- if a device is " - "connected then return the device name, otherwise return " - "the empty string. Each storage location on a device can " - "have a different name. The location names are 'main', " - "'carda' and 'cardb'. This function works only in the GUI.") + __doc__ = doc = _( +''' +``connected_device_name(storage_location_key)`` -- if a device is connected then +return the device name, otherwise return the empty string. Each storage location +on a device has its own device name. The ``storage_location_key`` names are +``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI. +''') + def evaluate(self, formatter, kwargs, mi, locals, storage_location): # We can't use get_database() here because we need the device manager. @@ -2173,12 +2492,14 @@ class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): name = 'connected_device_uuid' arg_count = 1 category = 'Get values from metadata' - __doc__ = doc = _("connected_device_uuid(storage_location) -- if a device is " - "connected then return the device uuid (unique id), " - "otherwise return the empty string. Each storage location " - "on a device has a different uuid. The location names are " - "'main', 'carda' and 'cardb'. This function works only in " - "the GUI.") + __doc__ = doc = _( +''' +``connected_device_uuid(storage_location_key)`` -- if a device is connected then +return the device uuid (unique id), otherwise return the empty string. Each +storage location on a device has a different uuid. The ``storage_location_key`` +location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works +only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, storage_location): # We can't use get_database() here because we need the device manager. @@ -2209,18 +2530,20 @@ class BuiltinCheckYesNo(BuiltinFormatterFunction): name = 'check_yes_no' arg_count = 4 category = 'If-then-else' - __doc__ = doc = _('check_yes_no(field_name, is_undefined, is_false, is_true) ' - '-- checks the value of the yes/no field named by the ' - 'lookup key field_name for a value specified by the ' - 'parameters, returning "yes" if a match is found, otherwise ' - 'returning an empty string. Set the parameter is_undefined, ' - 'is_false, or is_true to 1 (the number) to check that ' - 'condition, otherwise set it to 0. Example: ' - 'check_yes_no("#bool", 1, 0, 1) returns "yes" if the ' - 'yes/no field "#bool" is either undefined (neither True ' - 'nor False) or True. More than one of is_undefined, ' - 'is_false, or is_true can be set to 1. This function ' - 'is usually used by the test() or is_empty() functions.') + __doc__ = doc = _( +''' +``check_yes_no(field_name, is_undefined, is_false, is_true)`` -- checks if the +value of the yes/no field named by the lookup name ``field_name`` is one of the +values specified by the parameters, returning ``'yes'`` if a match is found +otherwise returning the empty string. Set the parameter ``is_undefined``, +``is_false``, or ``is_true`` to 1 (the number) to check that condition, +otherwise set it to 0. + +Example: ``check_yes_no("#bool", 1, 0, 1)`` returns ``'Yes'`` if the yes/no field +``#bool`` is either True or undefined (neither True nor False). + +More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1. +''') def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true): # 'field' is a lookup name, not a value @@ -2244,11 +2567,13 @@ class BuiltinRatingToStars(BuiltinFormatterFunction): name = 'rating_to_stars' arg_count = 2 category = 'Formatting values' - __doc__ = doc = _('rating_to_stars(value, use_half_stars) ' - '-- Returns the rating as string of star characters. ' - 'The value is a number between 0 and 5. Set use_half_stars ' - 'to 1 if you want half star characters for custom ratings ' - 'columns that support non-integer ratings, for example 2.5.') + __doc__ = doc = _( +''' +``rating_to_stars(value, use_half_stars)`` -- Returns the value as string of star +(``★``) characters. The value must be a number between 0 and 5. Set +use_half_stars to 1 if you want half star characters for fractional numbers +available with custom ratings columns. +''') def evaluate(self, formatter, kwargs, mi, locals, value, use_half_stars): if not value: @@ -2268,13 +2593,14 @@ class BuiltinSwapAroundArticles(BuiltinFormatterFunction): name = 'swap_around_articles' arg_count = 2 category = 'String manipulation' - __doc__ = doc = _('swap_around_articles(val, separator) ' - '-- returns the val with articles moved to the end. ' - 'The value can be a list, in which case each member ' - 'of the list is processed. If the value is a list then ' - 'you must provide the list value separator. If no ' - 'separator is provided then the value is treated as ' - 'being a single value, not a list.') + __doc__ = doc = _( +''' +``swap_around_articles(value, separator)`` -- returns the value with articles moved to +the end. The value can be a list, in which case each item in the list is +processed. If the value is a list then you must provide the ``separator``. If no +``separator`` is provided then the value is treated as being a single value, not +a list. The `articles` are those used by calibre to generate the ``title_sort``. +''') def evaluate(self, formatter, kwargs, mi, locals, val, separator): if not val: @@ -2294,16 +2620,17 @@ class BuiltinArguments(BuiltinFormatterFunction): name = 'arguments' arg_count = -1 category = 'Other' - __doc__ = doc = _('arguments(id[=expression] [, id[=expression]]*) ' - '-- Used in a stored template to retrieve the arguments ' - 'passed in the call. It both declares and initializes ' - 'local variables, effectively parameters. The variables ' - 'are positional; they get the value of the parameter given ' - 'in the call in the same position. If the corresponding ' - 'parameter is not provided in the call then arguments ' - 'assigns that variable the provided default value. If ' - 'there is no default value then the variable is set to ' - 'the empty string.') + __doc__ = doc = _( +''' +``arguments(id[=expression] [, id[=expression]]*)`` -- Used in a stored +template to retrieve the arguments passed in the call. It both declares and +initializes local variables with the supplied names, the ``id``s, making them +effectively parameters. The variables are positional; they get the value of +the argument given in the call in the same position. If the corresponding +argument is not provided in the call then ``arguments()`` assigns that variable +the provided default value. If there is no default value then the variable +is set to the empty string. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): # The arguments function is implemented in-line in the formatter @@ -2314,14 +2641,16 @@ class BuiltinGlobals(BuiltinFormatterFunction): name = 'globals' arg_count = -1 category = 'Other' - __doc__ = doc = _('globals(id[=expression] [, id[=expression]]*) ' - '-- Retrieves "global variables" that can be passed into ' - 'the formatter. It both declares and initializes local ' - 'variables with the names of the global variables passed ' - 'in. If the corresponding variable is not provided in ' - 'the passed-in globals then it assigns that variable the ' - 'provided default value. If there is no default value ' - 'then the variable is set to the empty string.') + __doc__ = doc = _( +''' +``globals(id[=expression] [, id[=expression]]*)`` -- Retrieves "global variables" +that can be passed into the formatter. The name ``id`` is the name of the global +variable. It both declares and initializes local variables with the names of the +global variables passed in (the ``id``s. If the corresponding variable is not +provided in the globals then it assigns that variable the provided default +value. If there is no default value then the variable is set to the empty +string.) +''') def evaluate(self, formatter, kwargs, mi, locals, *args): # The globals function is implemented in-line in the formatter @@ -2347,9 +2676,11 @@ class BuiltinFieldExists(BuiltinFormatterFunction): name = 'field_exists' arg_count = 1 category = 'If-then-else' - __doc__ = doc = _('field_exists(field_name) -- checks if a field ' - '(column) named field_name exists, returning ' - "'1' if so and '' if not.") + __doc__ = doc = _( +''' +``field_exists(lookup_name)`` -- checks if a field (column) with the lookup name +``lookup_name`` exists, returning ``'1'`` if so and the empty string if not. +''') def evaluate(self, formatter, kwargs, mi, locals, field_name): if field_name.lower() in mi.all_field_keys(): @@ -2361,11 +2692,14 @@ class BuiltinCharacter(BuiltinFormatterFunction): name = 'character' arg_count = 1 category = 'String manipulation' - __doc__ = doc = _('character(character_name) -- returns the ' - 'character named by character_name. For example, ' - r"character('newline') returns a newline character ('\n'). " - "The supported character names are 'newline', 'return', " - "'tab', and 'backslash'.") + __doc__ = doc = _( +''' +``character(character_name)`` -- returns the character named by character_name. +For example, ``character('newline')`` returns a newline character (``'\\n'``). +The supported character names are ``newline``, ``return``, ``tab``, and +``backslash``. This function is used to put these characters into the output +of templates. +''') def evaluate(self, formatter, kwargs, mi, locals, character_name): # The globals function is implemented in-line in the formatter @@ -2376,8 +2710,11 @@ class BuiltinToHex(BuiltinFormatterFunction): name = 'to_hex' arg_count = 1 category = 'String manipulation' - __doc__ = doc = _('to_hex(val) -- returns the string encoded in hex. ' - 'This is useful when constructing calibre URLs.') + __doc__ = doc = _( +''' +``to_hex(val)`` -- returns the string ``val`` encoded in hex. This is useful +when constructing calibre URLs. +''') def evaluate(self, formatter, kwargs, mi, locals, val): return val.encode().hex() @@ -2387,14 +2724,16 @@ class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction): name = 'urls_from_identifiers' arg_count = 2 category = 'Formatting values' - __doc__ = doc = _('urls_from_identifiers(identifiers, sort_results) -- given ' - 'a comma-separated list of identifiers, where an identifier ' - 'is a colon-separated pair of values (name:id_value), returns a ' - 'comma-separated list of HTML URLs generated from the ' - 'identifiers. The list not sorted if sort_results is 0 ' - '(character or number), otherwise it is sorted alphabetically ' - 'by the identifier name. The URLs are generated in the same way ' - 'as the built-in identifiers column when shown in Book details.') + __doc__ = doc = _( +''' +``urls_from_identifiers(identifiers, sort_results)`` -- given a comma-separated +list of ``identifiers``, where an ``identifier`` is a colon-separated pair of +values (``id_name:id_value``), returns a comma-separated list of HTML URLs +generated from the identifiers. The list not sorted if sort_results is ``0`` +(character or number), otherwise it is sorted alphabetically by the identifier +name. The URLs are generated in the same way as the built-in identifiers column +when shown in Book Details. +''') def evaluate(self, formatter, kwargs, mi, locals, identifiers, sort_results): from calibre.ebooks.metadata.sources.identify import urls_from_identifiers @@ -2422,10 +2761,47 @@ class BuiltinBookCount(BuiltinFormatterFunction): name = 'book_count' arg_count = 2 category = 'Template database functions' - __doc__ = doc = _('book_count(query, use_vl) -- returns the count of ' - 'books found by searching for query. If use_vl is ' - '0 (zero) then virtual libraries are ignored. This ' - 'function can be used only in the GUI.') + __doc__ = doc = _( +''' +``book_count(query, use_vl)`` -- returns the count of books found by searching +for ``query``. If ``use_vl`` is ``0`` (zero) then virtual libraries are ignored. +This function and its companion ``book_values()`` are particularly useful in +template searches, supporting searches that combine information from many books +such as looking for series with only one book. It cannot be used in composite +columns unless the tweak ``allow_template_database_functions_in_composites`` is +set to True. It can be used only in the GUI. + +For example this template search uses this function and its companion to find all series with only one book: +[LIST] +[*]Define a stored template (using :guilabel:`Preferences->Advanced->Template functions`) +named ``series_only_one_book`` (the name is arbitrary). The template +is: +[CODE] +program: + vals = globals(vals=''); + if !vals then + all_series = book_values('series', 'series:true', ',', 0); + for series in all_series: + if book_count('series:="' & series & '"', 0) == 1 then + vals = list_join(',', vals, ',', series, ',') + fi + rof; + set_globals(vals) + fi; + str_in_list(vals, ',', $series, 1, '') +[/CODE] +The first time the template runs (the first book checked) it stores the results +of the database lookups in a ``global`` template variable named ``vals``. These +results are used to check subsequent books without redoing the lookups. +[*] Use the stored template in a template search: +[CODE] +template:"program: series_only_one_book()#@#:n:1" +[/CODE] +Using a stored template instead of putting the template into the search +eliminates problems caused by the requirement to escape quotes in search +expressions. +[/LIST] +''') def evaluate(self, formatter, kwargs, mi, locals, query, use_vl): from calibre.db.fields import rendering_composite_name @@ -2444,11 +2820,18 @@ class BuiltinBookValues(BuiltinFormatterFunction): name = 'book_values' arg_count = 4 category = 'Template database functions' - __doc__ = doc = _('book_values(column, query, sep, use_vl) -- returns a list ' - 'of the values contained in the column "column", separated ' - 'by "sep", in the books found by searching for "query". ' - 'If use_vl is 0 (zero) then virtual libraries are ignored. ' - 'This function can be used only in the GUI.') + __doc__ = doc = _( +''' +``book_values(column, query, sep, use_vl)`` -- returns a list of the unique +values contained in the column ``column`` (a lookup name), separated by ``sep``, +in the books found by searching for ``query``. If ``use_vl`` is ``0`` (zero) +then virtual libraries are ignored. This function and its companion +``book_count()`` are particularly useful in template searches, supporting +searches that combine information from many books such as looking for series +with only one book. It cannot be used in composite columns unless the tweak +``allow_template_database_functions_in_composites`` is set to True. It can be +used only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, column, query, sep, use_vl): from calibre.db.fields import rendering_composite_name @@ -2476,13 +2859,15 @@ class BuiltinHasExtraFiles(BuiltinFormatterFunction): name = 'has_extra_files' arg_count = -1 category = 'Template database functions' - __doc__ = doc = _("has_extra_files([pattern]) -- returns the count of extra " - "files, otherwise '' (the empty string). " - "If the optional parameter 'pattern' (a regular expression) " - "is supplied then the list is filtered to files that match " - "pattern before the files are counted. The pattern match is " - "case insensitive. " - 'This function can be used only in the GUI.') + __doc__ = doc = _( +''' +``has_extra_files([pattern])`` -- returns the count of extra files, otherwise '' +(the empty string). If the optional parameter ``pattern`` (a regular expression) +is supplied then the list is filtered to files that match ``pattern`` before the +files are counted. The pattern match is case insensitive. See also the functions +``extra_file_names()``, ``extra_file_size()`` and ``extra_file_modtime()``. This +function can be used only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, *args): if len(args) > 1: @@ -2505,12 +2890,15 @@ class BuiltinExtraFileNames(BuiltinFormatterFunction): name = 'extra_file_names' arg_count = -1 category = 'Template database functions' - __doc__ = doc = _("extra_file_names(sep [, pattern]) -- returns a sep-separated " - "list of extra files in the book's '{}/' folder. If the " - "optional parameter 'pattern', a regular expression, is " - "supplied then the list is filtered to files that match pattern. " - "The pattern match is case insensitive. " - 'This function can be used only in the GUI.').format(DATA_DIR_NAME) + __doc__ = doc = _( +''' +``extra_file_names(sep [, pattern])`` -- returns a ``sep``-separated list of +extra files in the book's ``data/`` folder. If the optional parameter +``pattern``, a regular expression, is supplied then the list is filtered to +files that match ``pattern``. The pattern match is case insensitive. See also +the functions ``has_extra_files()``, ``extra_file_modtime()`` and +``extra_file_size()``. This function can be used only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, sep, *args): if len(args) > 1: @@ -2533,10 +2921,13 @@ class BuiltinExtraFileSize(BuiltinFormatterFunction): name = 'extra_file_size' arg_count = 1 category = 'Template database functions' - __doc__ = doc = _("extra_file_size(file_name) -- returns the size in bytes of " - "the extra file 'file_name' in the book's '{}/' folder if " - "it exists, otherwise -1." - 'This function can be used only in the GUI.').format(DATA_DIR_NAME) + __doc__ = doc = _( +''' +``extra_file_size(file_name)`` -- returns the size in bytes of the extra file +``file_name`` in the book's ``data/`` folder if it exists, otherwise ``-1``. See +also the functions ``has_extra_files()``, ``extra_file_names()`` and +``extra_file_modtime()``. This function can be used only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, file_name): db = self.get_database(mi).new_api @@ -2555,14 +2946,17 @@ class BuiltinExtraFileModtime(BuiltinFormatterFunction): name = 'extra_file_modtime' arg_count = 2 category = 'Template database functions' - __doc__ = doc = _("extra_file_modtime(file_name, format_string) -- returns the " - "modification time of the extra file 'file_name' in the " - "book's '{}/' folder if it exists, otherwise -1.0. The " - "modtime is formatted according to 'format_string' " - "(see format_date()). If 'format_string' is empty, returns " - "the modtime as the floating point number of seconds since " - "the epoch. The epoch is OS dependent. " - "This function can be used only in the GUI.").format(DATA_DIR_NAME) + __doc__ = doc = _( +''' +``extra_file_modtime(file_name, format_string)`` -- returns the modification +time of the extra file ``file_name`` in the book's ``data/`` folder if it +exists, otherwise ``-1``. The modtime is formatted according to +``format_string`` (see ``format_date()`` for details). If ``format_string`` is +the empty string, returns the modtime as the floating point number of seconds +since the epoch. See also the functions ``has_extra_files()``, +``extra_file_names()`` and ``extra_file_size()``. The epoch is OS dependent. +This function can be used only in the GUI. +''') def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string): db = self.get_database(mi).new_api @@ -2584,14 +2978,26 @@ class BuiltinGetNote(BuiltinFormatterFunction): name = 'get_note' arg_count = 3 category = 'Template database functions' - __doc__ = doc = _("get_note(field_name, field_value, plain_text) -- fetch the " - "note for field 'field_name' with value 'field_value'. If " - "'plain_text' is empty, return the note's HTML including " - "images. If 'plain_text' is 1 (or '1'), return the " - "note's plain text. If the note doesn't exist, return the " - "empty string in both cases. Example: " - "get_note('tags', 'Fiction', '') returns the HTML of the " - "note attached to the tag 'Fiction'.") + __doc__ = doc = _( +''' +``get_note(field_name, field_value, plain_text)`` -- fetch the note for field +'field_name' with value 'field_value'. If ``plain_text`` is empty, return the +note's HTML including images. If ``plain_text`` is ``1`` (or ``'1'``), return the +note's plain text. If the note doesn't exist, return the empty string in both +cases. Example: +[LIST] +[*]Return the HTML of the note attached to the tag `Fiction`: +[CODE] +program: + get_note('tags', 'Fiction', '') +[/CODE] +[*]Return the plain text of the note attached to the author `Isaac Asimov`: +[CODE] +program: + get_note('authors', 'Isaac Asimov', 1) +[/CODE] +[/LIST] +''') def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value, plain_text): db = self.get_database(mi).new_api @@ -2634,20 +3040,31 @@ class BuiltinHasNote(BuiltinFormatterFunction): name = 'has_note' arg_count = 2 category = 'Template database functions' - __doc__ = doc = _("has_note(field_name, field_value) -- if field_value is not " - "'' (the empty string) , return '1' if the value 'field_value' " - "in the field 'field_name' has an attached note, otherwise ''. " - "Example: has_note('tags', 'Fiction') returns '1' if the tag " - "'fiction' has a note, otherwise ''. If field_value " - "is '' then return a list of values in field_name that have " - "a note. If no item in the field has a note, return ''. " - "Example: has_note('authors', '') returns a list of authors " - "that have notes, or '' if no author has a note. The second " - "variant is useful for showing column icons icons if any value " - "in the field has a note, rather than a specific value. " - "You can also test if all the values have a note by comparing " - "the list length of this function's return value against " - "the list length of the values in field_name.") + __doc__ = doc = _( +''' +``has_note(field_name, field_value)``. This function has two variants: +[LIST] +[*]if ``field_value`` is not ``''`` (the empty string) return ``'1'`` if the +value ``field_value`` in the field ``field_name`` has a note, otherwise ``''``. + +Example: ``has_note('tags', 'Fiction')`` returns ``'1'`` if the tag ``fiction`` has an attached note, otherwise ``''``. + +[*]If ``field_value`` is ``''`` then return a list of values in ``field_name`` +that have a note. If no item in the field has a note, return ``''``. This +variant is useful for showing column icons if any value in the field has a note, +rather than a specific value. + +Example: ``has_note('authors', '')`` returns a list of authors that have notes, or +``''`` if no author has a note. +[/LIST] + +You can test if all the values in ``field_name`` have a note by comparing the +list length of this function's return value against the list length of the +values in ``field_name``. Example: +[CODE] + list_count(has_note('authors', ''), '&') ==# list_count_field('authors') +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api @@ -2674,11 +3091,15 @@ class BuiltinIsDarkMode(BuiltinFormatterFunction): name = 'is_dark_mode' arg_count = 0 category = 'other' - __doc__ = doc = _("is_dark_mode() -- Returns '1' if calibre is running " - "in dark mode, '' (the empty string) otherwise. This " - "function can be used in advanced color and icon rules " - "to choose different colors/icons according to the mode. " - "Example: {} ").format("if is_dark_mode() then 'dark.png' else 'light.png' fi") + __doc__ = doc = _( +''' +``is_dark_mode()`` -- returns ``'1'`` if calibre is running in dark mode, ``''`` +(the empty string) otherwise. This function can be used in advanced color and +icon rules to choose different colors/icons according to the mode. Example: +[CODE] + if is_dark_mode() then 'dark.png' else 'light.png' fi +[/CODE] +''') def evaluate(self, formatter, kwargs, mi, locals): try: @@ -2693,13 +3114,15 @@ class BuiltinFieldListCount(BuiltinFormatterFunction): name = 'list_count_field' arg_count = 0 category = 'List manipulation' - __doc__ = doc = _("list_count_field(field_name) -- returns the count of items " - "in the field with the lookup name 'field_name'. The field " - "must be multi-valued such as authors or tags, otherwise " - "the function raises an error. This function is much faster " - "than list_count() because it operates directly on calibre " - "data without converting it to a string first. " - "Example: {}").format("list_count_field('tags')") + __doc__ = doc = _( +''' +``list_count_field(lookup_name)``-- returns the count of items in the field with +the lookup name ``lookup_name``. The field must be multi-valued such as +``authors`` or ``tags``, otherwise the function raises an error. This function +is much faster than ``list_count()`` because it operates directly on calibre +data without converting it to a string first. Example: +``list_count_field('tags')`` +''') def evaluate(self, formatter, kwargs, mi, locals, *args): # The globals function is implemented in-line in the formatter