diff --git a/manual/template_lang.rst b/manual/template_lang.rst index b24d49fbb8..29913130ba 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -156,7 +156,8 @@ The functions available are listed below. Note that the definitive documentation * ``contains(pattern, text if match, text if not match)`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`. * ``count(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(&)}` + Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}`. + Aliases: ``count()``, ``list_count()`` * ``format_number(template)`` -- interprets the field as a number and format that number 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). You can leave off the leading "{0:" and trailing "}" if the template contains only a format. See the template language and Python @@ -467,6 +468,7 @@ parameters can be statements (sequences of expressions). Note that the definitiv of items separated by `separator`, evaluating the `pattern` against each value in the list. If the `pattern` matches a value, return `found_val`, otherwise return `not_found_val`. The `pattern` and `found_value` can be repeated as many times as desired, permitting returning different values depending on the search. The patterns are checked in order. The first match is returned. + Aliases: ``in_list()``, ``list_contains()`` * ``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. * ``list_equals(list1, sep1, list2, sep2, yes_val, no_val)`` -- return `yes_val` if `list1` and `list2` contain the same items, @@ -481,9 +483,10 @@ parameters can be statements (sequences of expressions). Note that the definitiv replacements are not optional. It uses re_group(item, search_re, template ...) when doing the replacements. * ``list_sort(list, direction, separator)`` -- return list sorted using a case-insensitive sort. If ``direction`` is zero, ``list`` is sorted ascending, otherwise descending. The list items are separated by separator, as are the items in the returned list. - * ``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. + * ``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()`` * ``mod(x)`` -- returns the remainder of ``x / y``, where ``x``, ``y``, and the result are integers. Throws an exception if either ``x`` or ``y`` is not a number. * ``multiply(x, y, ...)`` -- returns the product of its arguments. Throws an exception if any argument is not a number. @@ -681,13 +684,19 @@ The full method signature is: **Template writer: how to access the additional information** -You access the additional information in a template using the template function ``globals(id[=expression] [, id[=expression]]*)`` +You access the additional information (the `globals dict`) in a template using the template function +``globals(id[=expression] [, id[=expression]]*)`` where ``id`` is any legal variable name. This function checks whether the additional information provided by the developer -contains the name. If it does then the function assigns the provided value to a template local variable with the given name. +contains the name. If it does then the function assigns the provided value to a template local variable with that name. If the name is not in the additional information and if an ``expression`` is provided, the ``expression`` is evaluated and the result is assigned to the local variable. If neither a value nor an expression is provided, the function assigns the empty string (``''``) to the local variable. +A template can set a value in the globals dict using the template function +``set_globals(id[=expression] [, id[=expression]]*)``. This function sets the globals dict key:value pair ``id:value`` where +``value`` is the value of the template local variable ``id``. If that local variable doesn't exist then ``value`` is +set to the result of evaluating ``expression``. + Notes on the difference between modes ----------------------------------------- diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index da19bc2327..70de34d3b1 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -215,7 +215,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): def __init__(self, parent, text, mi=None, fm=None, color_field=None, icon_field_key=None, icon_rule_kind=None, doing_emblem=False, text_is_placeholder=False, dialog_is_st_editor=False, - global_vars={}, all_functions=None, builtin_functions=None): + global_vars=None, all_functions=None, builtin_functions=None): QDialog.__init__(self, parent) Ui_TemplateDialog.__init__(self) self.setupUi(self) @@ -224,7 +224,10 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.iconing = icon_field_key is not None self.embleming = doing_emblem self.dialog_is_st_editor = dialog_is_st_editor - self.global_vars = global_vars + if global_vars is None: + self.global_vars = {} + else: + self.global_vars = global_vars cols = [] if fm is not None: @@ -316,7 +319,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.setWindowIcon(icon) self.all_functions = all_functions if all_functions else formatter_functions().get_functions() - self.builtins = builtin_functions if builtin_functions else formatter_functions().get_builtins() + self.builtins = (builtin_functions if builtin_functions else + formatter_functions().get_builtins_and_aliases()) self.last_text = '' self.highlighter = TemplateHighlighter(self.textbox.document(), builtin_functions=self.builtins) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index b9d3715af6..3f35684b35 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -33,7 +33,8 @@ class Node(object): NODE_FIRST_NON_EMPTY = 12 NODE_FOR = 13 NODE_GLOBALS = 14 - NODE_CONTAINS = 15 + NODE_SET_GLOBALS = 15 + NODE_CONTAINS = 16 class IfNode(Node): @@ -92,6 +93,13 @@ class GlobalsNode(Node): self.expression_list = expression_list +class SetGlobalsNode(Node): + def __init__(self, expression_list): + Node.__init__(self) + self.node_type = self.NODE_SET_GLOBALS + self.expression_list = expression_list + + class StringInfixNode(Node): def __init__(self, operator, left, right): Node.__init__(self) @@ -447,7 +455,7 @@ class _Parser(object): return FirstNonEmptyNode(arguments) if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE): return AssignNode(arguments[0].name, arguments[1]) - if id_ == 'arguments' or id_ == 'globals': + if id_ == 'arguments' or id_ == 'globals' or id_ == 'set_globals': new_args = [] for arg in arguments: if arg.node_type not in (Node.NODE_ASSIGN, Node.NODE_RVALUE): @@ -458,6 +466,8 @@ class _Parser(object): new_args.append(arg) if id_ == 'arguments': return ArgumentsNode(new_args) + if id_ == 'set_globals': + return SetGlobalsNode(new_args) return GlobalsNode(new_args) if id_ == 'contains' and len(arguments) == 4: return ContainsNode(arguments) @@ -585,6 +595,12 @@ class _Interpreter(object): res = self.locals[arg.left] = self.global_vars.get(arg.left, self.expr(arg.right)) return res + def do_node_set_globals(self, prog): + res = '' + for arg in prog.expression_list: + res = self.global_vars[arg.left] = self.locals.get(arg.left, self.expr(arg.right)) + return res + def do_node_constant(self, prog): return prog.value @@ -668,6 +684,7 @@ class _Interpreter(object): Node.NODE_FIRST_NON_EMPTY:do_node_first_non_empty, Node.NODE_FOR: do_node_for, Node.NODE_GLOBALS: do_node_globals, + Node.NODE_SET_GLOBALS: do_node_set_globals, Node.NODE_CONTAINS: do_node_contains, } diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 71ba94e50a..1c4e8b543d 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -628,7 +628,7 @@ class BuiltinInList(BuiltinFormatterFunction): 'not_found_val. The pattern and found_value can be repeated as ' 'many times as desired, permitting returning different values ' 'depending on the search. The patterns are checked in order. The ' - 'first match is returned.') + 'first match is returned. Aliases: in_list(), list_contains()') aliases = ['list_contains'] def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args): @@ -814,10 +814,13 @@ class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 category = 'List manipulation' + aliases = ['list_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(&)}') + 'uses an ampersand. Examples: {tags:count(,)}, {authors:count(&)}. ' + 'Aliases: count(), list_count()') def evaluate(self, formatter, kwargs, mi, locals, val, sep): return unicode_type(len([v for v in val.split(sep) if v])) @@ -1350,7 +1353,7 @@ class BuiltinListUnion(BuiltinFormatterFunction): '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.') + 'the items in the returned list. Aliases: list_union(), merge_lists()') aliases = ['merge_lists'] def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): @@ -1917,6 +1920,24 @@ class BuiltinGlobals(BuiltinFormatterFunction): raise NotImplementedError() +class BuiltinSetGlobals(BuiltinFormatterFunction): + name = 'set_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.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + # The globals function is implemented in-line in the formatter + raise NotImplementedError() + + class BuiltinFieldExists(BuiltinFormatterFunction): name = 'field_exists' arg_count = 1 @@ -1953,7 +1974,7 @@ _formatter_builtins = [ BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRawField(), BuiltinRawList(), BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(), - BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(), + BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(), BuiltinStrcmp(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(), BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundArticles(), BuiltinSwapAroundComma(), BuiltinSwitch(),