From ac9c9d6ab5f05ebb2e0d635c03ce4731aceb5d08 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 27 Feb 2021 14:20:10 +0000 Subject: [PATCH] Various improvements: 1) Changes to the template program language discussed in https://www.mobileread.com/forums/showthread.php?t=337668 2) General improvement of the template documentation, including documentation of the above changes. I looked at the changes using a markdown interpreter, but there might be problems exposed by generation of the web page. 3) Focus the program text box when opening the template dialog 4) Small changes to non-built-in template functions to improve performance --- manual/template_lang.rst | 803 +++++++++++--------- src/calibre/gui2/dialogs/template_dialog.py | 1 + src/calibre/utils/formatter.py | 253 +++++- src/calibre/utils/formatter_functions.py | 7 +- 4 files changed, 689 insertions(+), 375 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index fa84dc8caf..e467a94710 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -3,12 +3,13 @@ The calibre template language ======================================================= -The calibre template language is used in various places. It is used to control the folder structure and file name when saving files from -the calibre library to the disk or e-book reader. It is also used to define "virtual" columns that contain data from other columns and so on. +The calibre template language is used in various places. It is used to control the folder structure and file name +when saving files from the calibre library to the disk or e-book reader. It is also used to define "virtual" +columns that contain data from other columns and so on. -The basic template language is simple but has powerful advanced features. A template consists of text and names in curly brackets that are -then replaced by the corresponding metadata from the book being processed. For example, the default template used for saving books to device -in calibre is:: +The basic template language is simple but has powerful advanced features. A template consists of text +and names in curly brackets that are then replaced by the corresponding metadata from the book being +processed. For example, the default template used for saving books to device in calibre is:: {author_sort}/{title}/{title} - {authors} @@ -24,19 +25,20 @@ For the book "The Foundation" by "Isaac Asimov" it will become:: Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov -You can use all the metadata fields available in calibre in a template, including any custom columns you have created, by using its -'lookup name'. To find the lookup name for a column (field) hover your mouse over the column header. Names for custom columns (columns -you have created yourself) always have a # as the first character. For series type custom columns there is always an additional field -named ``#seriesname_index`` that is the series index for that series. So if you have a custom series field named ``#myseries``, there -will also be a field named ``#myseries_index``. +You can use all the metadata fields available in calibre in a template, including any custom columns +you have created, by using its 'lookup name'. To find the lookup name for a column (field) hover your +mouse over the column header. Names for custom columns (columns you have created yourself) always have +a # as the first character. For series type custom columns there is always an additional field +named ``#seriesname_index`` that is the series index for that series. So if you have a custom +series field named ``#myseries``, there will also be a field named ``#myseries_index``. In addition to the column based fields, you also can use:: {formats} - A list of formats available in the calibre library for a book {identifiers:select(isbn)} - The ISBN of the book -If a book does not have a particular piece of metadata, the field in the template is replaced by the empty string for that book. -Consider, for example:: +If a book does not have a particular piece of metadata, the field in the template is replaced by the +empty string (``''``) for that book. Consider, for example:: {author_sort}/{series}/{title} {series_index} @@ -53,22 +55,25 @@ and if a book does not have a series:: Advanced formatting ---------------------- -You can do more than just simple substitution with the templates. You can also conditionally include text and control how the -substituted data is formatted. +You can do more than just simple substitution with the templates. You can also conditionally include +text and control how the substituted data is formatted. -First, conditionally including text. There are cases where you might want to have text appear in the output only if a field -is not empty. A common case is ``series`` and ``series_index``, where you want either nothing or the two values with a hyphen -between them. calibre handles this case using a special field syntax. +First, conditionally including text. There are cases where you might want to have text appear +in the output only if a field is not empty. A common case is ``series`` and ``series_index``, where +you want either nothing or the two values with a hyphen between them. calibre handles this case +using a special field syntax. For example, assume you want to use the template:: {series} - {series_index} - {title} -If the book has no series, the answer will be ``- - title``. Most people would rather the result be simply ``title``, without -the hyphens. To do this, use the extended syntax ``{field:|prefix_text|suffix_text}``. When you use this syntax, if field has -the value SERIES then the result will be ``prefix_textSERIESsuffix_text``. If field has no value, then the result will be the -empty string (nothing); the prefix and suffix are ignored. The prefix and suffix can contain blanks. -**Do not use subtemplates (`{ ... }`) or functions (see below) as the prefix or the suffix.** +If the book has no series, the answer will be ``- - title``. Most people would rather the result +be simply ``title``, without the hyphens. To do this, use the extended syntax ``{field:|prefix_text|suffix_text}``. +When you use this syntax, if field has the value SERIES then the result will be ``prefix_textSERIESsuffix_text``. +If field has no value, then the result will be the empty string (nothing); the prefix and suffix are ignored. +The prefix and suffix can contain blanks. + +**Do not use subtemplates (`{ ... }`) or functions (see below) in the prefix or the suffix.** Using this syntax, we can solve the above series problem with the template:: @@ -76,12 +81,13 @@ Using this syntax, we can solve the above series problem with the template:: The hyphens will be included only if the book has a series index, which it will have only if it has a series. -Notes: you must include the : character if you want to use a prefix or a suffix. You must either use no \| characters or both -of them; using one, as in ``{field:| - }``, is not allowed. It is OK not to provide any text for one side or the other, such -as in ``{series:|| - }``. Using ``{title:||}`` is the same as using ``{title}``. +Notes: you must include the colon if you want to use a prefix or a suffix. You must either use +no \| characters or both of them; using one, as in ``{field:| - }``, is not allowed. It is OK not +to provide any text for one side or the other, such as in ``{series:|| - }``. Using ``{title:||}`` is +the same as using ``{title}``. -Second: formatting. Suppose you wanted to ensure that the series_index is always formatted as three digits with leading zeros. -This would do the trick:: +Second: formatting. Suppose you wanted to ensure that the series_index is always formatted as three +digits with leading zeros. This does the trick:: {series_index:0>3s} - Three digits with leading zeros @@ -93,115 +99,145 @@ For trailing zeros, use:: {series_index:0<3s} - Three digits with trailing zeros -If you use series indices with sub values (e.g., 1.1), you might want to ensure that the decimal points line up. For example, you -might want the indices 1 and 2.5 to appear as 01.00 and 02.50 so that they will sort correctly. To do this, use:: +If you use series indices with sub values (e.g., 1.1), you might want to ensure that the decimal +points line up. For example, you might want the indices 1 and 2.5 to appear as 01.00 and 02.50 so +that they will sort correctly. To do this, use:: - {series_index:0>5.2f} - Five characters, consisting of two digits with leading zeros, a decimal point, then 2 digits after the decimal point + {series_index:0>5.2f} - Five characters, consisting of two digits with leading zeros, a + decimal point, then 2 digits after the decimal point If you want only the first two letters of the data, use:: {author_sort:.2} - Only the first two letter of the author sort name -The calibre template language comes from Python and for more details on the syntax of these advanced formatting operations, -look at the `Python documentation `. +The calibre template language comes from Python. For more details on the syntax of these advanced formatting +operations see the `Python documentation `_. Using templates in custom columns ---------------------------------- -Sometimes you want to display metadata in the book list that calibre does not normally display, or to display data in a way different -from how calibre normally does. For example, you might want to display the ISBN, a field that calibre does not display. You can use -custom columns for this by creating a column with the type 'column built from other columns' (hereafter called composite columns), and -entering a template. Result: calibre will display a column showing the result of evaluating that template. To display the ISBN, create -the column and enter ``{identifiers:select(isbn)}`` into the template box. To display a column containing the values of two series -custom columns separated by a comma, use ``{#series1:||,}{#series2}``. +Sometimes you want to display metadata in the book list that calibre does not normally display, or to +display data in a way different from how calibre normally does. For example, you might want to +display the ISBN, a field that calibre does not display. You can use custom columns for this by creating +a column with the type 'column built from other columns' (hereafter called composite columns), and +entering a template. Result: calibre will display a column showing the result of evaluating that +template. To display the ISBN, create the column and enter ``{identifiers:select(isbn)}`` into the +template box. To display a column containing the values of two series custom columns separated by a comma, +use ``{#series1:||,}{#series2}``. Composite columns can use any template option, including formatting. -You cannot edit the data displayed in a composite column. If you edit a composite column, for example by double-clicking it, you will open -the template for editing, not the underlying data. Editing the template on the GUI is a quick way of testing and changing composite columns. +You cannot edit the data displayed in a composite column. If you edit a composite column, for example +by double-clicking it, you will open the template for editing, not the underlying data. Editing the +template on the GUI is a quick way of testing and changing composite columns. + +.. _single_mode: Using functions in templates - Single Function Mode --------------------------------------------------- -Suppose you want to display the value of a field in upper case, when that field is normally in title case. You can do this -(and many more things) using the functions available for templates. For example, to display the title in upper case, use -``{title:uppercase()}``. To display it in title case, use ``{title:titlecase()}``. +Suppose you want to display the value of a field in upper case, when that field is normally in title case. +You can do this (and many more things) using the functions available for templates. For example, to +display the title in upper case, use ``{title:uppercase()}``. To display it in title case, +use ``{title:titlecase()}``. -Functions appear in the format part, going after the ``:`` and before the first ``|`` or the closing ``}``. If you have both a format and a -function reference, the function comes after another ``:``. Functions must always end with ``()``. Some functions take extra values -(arguments), and these go inside the ``()``. +Functions are put in the format part, going after the ``:`` and before the first ``|`` or the closing ``}``. +If you have both a format and a function reference, the function comes after another ``:``. +Functions must always end with ``()``. Some functions take extra values (arguments), and these go inside the ``()``. -Functions are always applied before format specifications. See further down for an example of using both a format and a function, where this -order is demonstrated. +Functions are always applied before format specifications. See further down for an example of using +both a format and a function, where this order is demonstrated. -The syntax for using functions is ``{field:function(arguments)}``, or ``{field:function(arguments)|prefix|suffix}``. Arguments are separated -by commas. Commas inside arguments must be preceded by a backslash ( ``\`` ). The last (or only) argument cannot contain a closing -parenthesis ( ``)`` ). Functions return the value of the field used in the template, suitably modified. +The syntax for using functions is ``{field:function(arguments)}``, +or ``{field:function(arguments)|prefix|suffix}``. Arguments are separated by commas. Literal commas (commas as +arguments) must be preceded by a backslash ( ``\`` ). The last (or only) argument cannot contain a +closing parenthesis ( ``)`` ). Functions return the value of the field used in the template, +suitably modified. -Important: If you have programming experience, please note that the syntax in this mode (single function) is not what you might expect. -Strings are not quoted. Spaces are significant. All arguments must be constants; there is no sub-evaluation. -**Do not use subtemplates (`{ ... }`) as function arguments.** Instead, use :ref:`Template Program Mode ` and -:ref:`General Program Mode `. +Important: If you have programming experience, please note that the syntax in this mode (single function) +is not what you might expect. Strings are not quoted. Spaces are significant. All arguments must be +constants; there is no sub-evaluation. + +**Do not use subtemplates (`{ ... }`) as function arguments.** Instead, +use :ref:`Template Program Mode ` and :ref:`General Program Mode `. Many functions use regular expressions. In all cases, regular expression matching is case-insensitive. -The functions available are listed below. Note that the definitive documentation for functions is available in the section -:ref:`Function reference `: +The functions available are listed below. Note that the definitive documentation for functions is +available in the section :ref:`Function reference `: * ``lowercase()`` -- return value of the field in lower case. * ``uppercase()`` -- return the value of the field in upper case. * ``titlecase()`` -- return the value of the field in title case. * ``capitalize()`` -- return the value with the first letter upper case and the rest lower case. - * ``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(&)}`. + * ``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(&)}`. 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 - documentation for more examples. Returns the empty string if formatting fails. - * ``human_readable()`` -- expects the value to be a number and returns a string representing that number in KB, MB, GB, etc. + * ``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 documentation for more examples. Returns the empty string if + formatting fails. + * ``human_readable()`` -- expects the value to be a number and returns a string representing that + number in KB, MB, GB, etc. * ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`. - * ``in_list(separator, pattern, found_val, ..., not_found_val)`` -- interpret the field as a list 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 + * ``in_list(separator, pattern, found_val, ..., not_found_val)`` -- interpret the field as a list 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. - * ``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. - * ``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. - * ``list_item(index, separator)`` -- interpret the field 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. - * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. - The value of the appropriate field will be fetched and used. 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 (more later). - * ``rating_to_stars(use_half_stars)`` -- Returns the rating as string of star characters. The source value must be a number between 0 and 5. - Set use_half_stars to 1 if you want half star characters for custom ratings columns that are not integers, for example 2.5. - * ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with - `replacement`. As in all of calibre, these are Python-compatible regular expressions. - * ``select(key)`` -- interpret the field as a comma-separated list of items, with the items being of the form "id:value". Find the pair - with the id equal to key, and return the corresponding value. This function is particularly useful for extracting a value such as an - ISBN from the set of identifiers for a book. - * ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from - the beginning of the field, followed by `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-nhoe`. 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. - * ``str_in_list(separator, string, found_val, ..., not_found_val)`` -- interpret the field as a list of items separated by `separator`, - comparing the `string` against each value in the list. If the `string` matches a value (ignoring case), return `found_val`, otherwise return - `not_found_val`. If the string contains separators, then it is also treated as a list and each value is checked. The `string` and `found_value` - can be repeated as many times as desired, permitting returning different values depending on the search. The strings are checked in order. - The first match is returned. - * ``subitems(start_index, end_index)`` -- This function is used to break apart lists of tag-like hierarchical items such as genres. It - interprets the field as a comma-separated list of tag-like items, where each item is a period-separated list. Returns a new list made - by first finding all the period-separated tag-like items, then for each such item extracting the components from `start_index` to - `end_index`, 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. + * ``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. + * ``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. + * ``list_item(index, separator)`` -- interpret the field 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. + * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments + are field (metadata) names, not text. The value of the appropriate field will be fetched and used. + 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 useful when constructing variable save paths + (more later). + * ``rating_to_stars(use_half_stars)`` -- Returns the rating as string of star characters. The source + value must be a number between 0 and 5. Set use_half_stars to 1 if you want half star characters + for custom ratings columns that are not integers, for example 2.5. + * ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances + of `pattern` are replaced with `replacement`. As in all of calibre, these are Python-compatible + regular expressions. + * ``select(key)`` -- interpret the field as a comma-separated list of items, with the items being + of the form "id:value". It finds the pair with the id equal to key and returns the corresponding value. + This function is particularly useful for extracting a value such as an ISBN from the set of identifiers + for a book. + * ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, + consisting of `left chars` characters from the beginning of the field, followed by `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-nhoe`. 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. + * ``str_in_list(separator, string, found_val, ..., not_found_val)`` -- interpret the field as a + list of items separated by `separator`, comparing the `string` against each value in the list. + If the `string` matches a value (ignoring case), return `found_val`, otherwise return + `not_found_val`. If the string contains separators, then it is also treated as a list and each + value is checked. The `string` and `found_value` can be repeated as many times as desired, permitting + returning different values depending on the search. The strings are checked in order. The first match + is returned. + * ``subitems(start_index, end_index)`` -- This function is used to break apart lists of tag-like + hierarchical items such as genres. It interprets the field as a comma-separated list of tag-like items, + where each item is a period-separated list. Returns a new list made by first finding all the + period-separated tag-like items, then for each such item extracting the components from `start_index` to + `end_index`, 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. Examples:: Assuming a #genre column containing "A.B.C": @@ -212,145 +248,226 @@ The functions available are listed below. Note that the definitive documentation {#genre:subitems(0,1)} returns "A, D" {#genre:subitems(0,2)} returns "A.B, D.E" - * ``sublist(start_index, end_index, separator)`` -- interpret the field 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 + * ``sublist(start_index, end_index, separator)`` -- interpret the field 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":: {tags:sublist(0,1,\,)} returns "A" {tags:sublist(-1,0,\,)} returns "C" {tags:sublist(0,-1,\,)} returns "A, B" - * ``swap_around_articles(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. - * ``swap_around_comma()`` -- given a field with 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. - * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular - expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many - ``pattern, value`` pairs as you want. - * ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`. - * ``transliterate()`` -- Returns 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 ``Фёдор Миха́йлович Достоевский`` the function returns ``Fiodor Mikhailovich Dostoievskii``.' + * ``swap_around_articles(separator)`` -- returns the val with articles moved to the end. The value ca + 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. + * ``swap_around_comma()`` -- given a field with 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. + * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, + checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. + If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs + as you wish. + * ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not + empty, otherwise return `text if empty`. + * ``transliterate()`` -- Returns 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 ``Фёдор Миха́йлович Достоевский`` + the function returns ``Fiodor Mikhailovich Dostoievskii``.' -Now, what about using functions and formatting in the same field? Suppose you have an integer custom column called ``#myint`` that you want to -see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals -zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then -you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be:: +**Using functions and formatting in the same template** + +Suppose you have an integer custom column called ``#myint`` that you want to +see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, +if a number (integer or float) equals zero then the field produces the empty value, so zero values will +produce the empty string, not ``000``. If you really want to see ``000`` values, then you use both the +format string and the ``ifempty`` function to change the empty value back to a zero. +The field reference would be:: {#myint:0>3s:ifempty(0)} -Note that you can use the prefix and suffix as well. If you want the number to appear as ``[003]`` or ``[000]``, then use the field:: +Note that you can use the prefix and suffix as well. If you want the number to appear as ``[003]`` or ``[000]``, +then use the field:: {#myint:0>3s:ifempty(0)|[|]} -.. _template_mode: +.. _general_mode: -More complex functions in templates - Template Program Mode -------------------------------------------------------------- +General Program Mode +----------------------------------- -Template Program Mode differs from Single Function Mode in that it permits writing template expressions that refer to other metadata fields, -use nested functions, modify values, and do arithmetic. It is a reasonably complete programming language. +General Program Mode replaces the template with a program written in the Template Language. The syntax of the language +is shown by the following grammar: -You can use the functions documented above in Template Program Mode. See below for details. - -Beginning with an example, assume you want your template to show the series for a book if it has one, otherwise show the value of a custom -field #genre. You cannot do this in the basic template language because you cannot make reference to another metadata field within a template -expression. In Template Program Mode, you can. The following expression works:: - - {#series:'ifempty($, field('#genre'))'} - -The example shows several things: - - * Template Program Mode is used if the expression begins with ``:'`` and ends with ``'``. Anything else is assumed to be in single function mode. - * the variable ``$`` stands for the field the expression is operating upon, ``#series`` in this case. - * functions must be given all their arguments. There is no default value. For example, the standard built-in functions must be given an additional - initial parameter indicating the source field, which is a significant difference from single-function mode. - * white space is ignored and can be used anywhere within the expression. - * constant strings are enclosed in matching quotes, either ``'`` or ``"``. - -The syntax of the language is shown by the following grammar. For a discussion of 'compare','if_expression', and 'template_call' see -:ref:`General Program Mode `::: - - program ::= expression_list - expression_list ::= expression [ ';' expression ]* - expression ::= identifier | constant | function | assignment | compare | if_expression - function ::= identifier '(' expression [ ',' expression ]* ')' - compare ::= expression compare_op expression - compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | '==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#' - if_expression ::= 'if' expression 'then' expression_list [elif_expression] ['else' expression_list] 'fi' - elif_expression ::= 'elif' expression 'then' expression_list elif_expression | '' - assignment ::= identifier '=' expression - constant ::= " string " | ' string ' | number + program ::= 'program:' expression_list + expression_list ::= top_expression [ ';' top_expression ]* + top_expression ::= or_expression + or_expression ::= and_expression [ '||' and_expression ]* + and_expression ::= not_expression [ '&&' not_expression ]* + not_expression ::= ['!' not_expression]* | compare_exp + compare_expr ::= add_sub_expr [ compare_op add_sub_expr ] + compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | + '==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#' + add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]* + add_sub_op ::= '+' | '-' + times_div_expr ::= unary_op_expr [ times_div_op unary_op_expr ]* + times_div_op ::= '*' | '/' + unary_op_expr ::= [ add_sub_op unary_op_expr ]* | expression + expression ::= identifier | constant | function | assignment | + compare | if_expression | for_expression | '(' top_expression ')' identifier ::= sequence of letters or ``_`` characters + constant ::= " string " | ' string ' | number + function ::= identifier '(' top_expression [ ',' top_expression ]* ')' + assignment ::= identifier '=' top_expression + if_expression ::= 'if' top_expression 'then' expression_list + [elif_expression] ['else' expression_list] 'fi' + elif_expression ::= 'elif' top_expression 'then' expression_list elif_expression | '' + for_expression ::= 'for' identifier 'in' top_expression + [ 'separator' top_expression ] ':' expression_list 'rof' Comments are lines with a '#' character at the beginning of the line. -An ``expression`` without errors always has a value. The value of an ``expression_list`` is the value of the last expression in the list. -As such, the value of the program (expression_list):: +A ``top_expression`` always has a value. The value of an ``expression_list`` is the value +of the last top_expression in the list. For example the value of the program (expression_list):: 1; 2; 'foobar'; 3 is 3. -Another example of a complex but rather silly program might help make things clearer:: +**Operator Precedence** - {series_index:' - substr( - strcat($, '->', - cmp(divide($, 2), 1, - assign(c, 1); substr('lt123', c, 0), - 'eq', 'gt')), - 0, 6) - '| prefix | suffix} +The operator precedence (order of evaluation) specified by the above grammar is: -This program does the following: + * Function calls, constants, parenthesized expressions, statement expressions, + assignment expressions. In the template language, 'if', 'for', and assignment + return a value (see below). + * Unary plus (+) and minus (-). These operators evaluate right to left. + These and the other arithmetic operators return integers if the expression has a fractional part + equal to zero (i.e., 3.0). + * Multiply (*) and divide (/). These operators are associative and evaluate left to right. + Use parentheses if you want to change the order of evaluation. + * Add (+) and subtract (-). These operators are associative and evaluate left to right. + * Numeric and string comparisons. These operators return '1' (the number one) if the comparison is True, + otherwise the empty string (``''``). Comparisons are not associative: ``a < b < c`` is a syntax error. + * Unary logical not (!). This operator returns '1' if the expression is False (evaluates to the + empty string), otherwise ''. + * Logical and (&&). This operator returns '1' if both the left-hand and right-hand + expressions are True or the empty string '' if either is False. It is associative, evaluates left to + right, and does `short-circuiting `_. + * Logical or (||). This operator returns '1' if either the left-hand or right-hand expression is + True or '' if both are False. It is associative, evaluates left to right, and does short-circuiting. The + operator is an inclusive or, returning '1' if both the left- and right-hand expressions are True. - * specify that the field being looked at is series_index. The variable ``$`` is set to its value. - * calls the ``substr`` function, which takes 3 parameters ``(str, start, end)``. It returns a string formed by extracting the start - through end characters from string, zero-based (the first character is character zero). In this case the string will be computed by the - ``strcat`` function, the start is 0, and the end is 6. In this case it will return the first 6 characters of the string returned by - ``strcat``, which must be evaluated before substr can return. - * calls the ``strcat`` (string concatenation) function. Strcat accepts 1 or more arguments, and returns a string formed by concatenating - all the values. In this case there are three arguments. The first parameter is the value in ``$``, which here is the value of ``series_index``. - The second paremeter is the constant string ``'->'``. The third parameter is the value returned by the ``cmp`` function, which must be fully - evaluated before ``strcat`` can return. - * The ``cmp`` function takes 5 arguments ``(x, y, lt, eq, gt)``. It compares ``x`` and ``y`` and returns the third argument ``lt`` if ``x < y``, - the fourth argument ``eq`` if ``x == y``, and the fifth argument ``gt`` if ``x > y``. As with all functions, all of the parameters can be - statements. In this case the first parameter (the value for ``x``) is the result of dividing the ``series_index`` by 2. The second parameter - ``y`` is the constant ``1``. The third parameter ``lt`` is a statement (more later). The fourth parameter ``eq`` is the constant string - ``'eq'``. The fifth parameter is the constant string ``'gt'``. - * The third parameter (the one for ``lt``) is a statement, or a sequence of expressions. Remember that a statement (a sequence of - semicolon-separated expressions) is also an expression, returning the value of the last expression in the list. In this case, the program - first assigns the value ``1`` to a local variable ``c``, then returns a substring made by extracting the ``c``'th character to the end. - Since ``c`` always contains the constant ``1``, the substring will return the second through ``end``'th characters, or ``'t123'``. - * Once the statement providing the value to the third parameter is executed, ``cmp`` can return a value. At that point, ``strcat` can return - a value, then ``substr`` can return a value. The program then terminates. +**If Expressions** -For various values of series_index, the program returns: +If expressions evaluate the first top_expression, called the condition. The condition is True if it evaluates +to anything other than the empty string. If it is True then the expression list in the ``then`` section is +evaluated. If it is False then the expression in the ``elif`` or ``else`` expression is evaluated. The +``elif`` and ``else`` parts are optional. The words ``if``, ``then``, ``elif``, ``else``, and ``fi`` are +reserved; you cannot use them as identifier names. You can put newlines and white space wherever they +make sense. The condition is a top_expression; semicolons are not allowed. The expression_lists are +semicolon-separated sequences of template language top_expressions, including nested ifs. An if expression +returns the last expression in an evaluated expression_list, or '' if no expression list was evaluated. - * series_index == undefined, result = ``prefix ->t123 suffix`` - * series_index == 0.5, result = ``prefix 0.50-> suffix`` - * series_index == 1, result = ``prefix 1->t12 suffix`` - * series_index == 2, result = ``prefix 2->eq suffix`` - * series_index == 3, result = ``prefix 3->gt suffix`` +Examples: -**All the functions listed under single-function mode can be used in program mode**. To do so, you must supply the value that the function is to act -upon as the first parameter in addition to the parameters documented above. For example, in program mode the parameters of the `test` function are -``test(x, text_if_not_empty, text_if_empty)``. The `x` parameter, which is the value to be tested, will almost always be a variable or a function -call, often `field()`. + * ``program: if field('series') then 'yes' else 'no' fi`` + * ``program: if field('series') then a = 'yes'; b = 'no' else a = 'no'; b='yes' fi; strcat(a, '-', b)`` + * Nested ``if`` example:: -The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function -mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all -parameters can be statements (sequences of expressions). Note that the definitive documentation for functions is available in the section + program: + if field('series') + then + if check_yes_no(field('#mybool'), '', '', '1') + then + 'yes' + else + 'no' + fi + else + 'no series' + fi + +As said above, an ``if`` produces a value like any other language expression. This means that all the +following are valid: + + * ``program: if field('series') then 'foo' else 'bar' fi`` + * ``program: if field('series') then a = 'foo' else a = 'bar' fi; a`` + * ``program: a = if field('series') then 'foo' else 'bar' fi; a`` + * ``program: a = field(if field('series') then 'series' else 'title' fi); a`` + +** For Expressions** + +The top_expression in ``for`` must evaluate to either a metadata field lookup key, for example ``tags`` or ``#genre``, +or a list of values. If the result is a valid lookup name then the field's value is fetched and the separator +specified for that field type is used. If the result isn't a valid lookup name then it is assumed to be a +list of values. If the optional keyword ``separator`` is supplied then the list values must be separated +by the result of evaluating the second top_expression. If the separator is not specified then the list +values must be separated by commas. Each resulting value in the list is assigned to the variable ``id`` then +the ``expression_list`` is evaluated. + +Example: This template removes the first hierarchical name for each value in Genre (``#genre``), constructing a +list with the new names:: + + program: + new_tags = ''; + for i in '#genre': + j = re(i, '^.*?\.(.*)$', '\1'); + new_tags = list_union(new_tags, j, ',') + rof; + new_tags + +If the original Genre is ``History.Military, Science Fiction.Alternate History, ReadMe`` then the template returns +``Military, Alternate History, ReadMe``. You could use this template in calibre's +:guilabel:`Edit metadata in bulk -> Search & replace` with :guilabel:`Search for` set to ``template`` to strip +off the first level of the hierarchy and assign the resulting value to Genre. + +Note: the last line in the template, ``new_tags``, isn't strictly necessary in this case because ``for`` returns +the value of the last top_expression in the expression list. + +**Relational Operators** + +Relational operators return '1' if they evaluate to True, otherwise the empty string (''). + +There are two forms of relational operator: string comparisons and numeric comparisons. The supported string +comparison operators are ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, and ``in``. +They do case-insensitive string comparison using lexical order. For the ``in`` operator, the result of +the left hand expression is interpreted as a regular expression pattern. The ``in`` operator is True if the +pattern matches the value of the right hand expression. The match is case-insensitive. + +The numeric comparison operators are ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. The left and right +expressions must evaluate to numeric values. Two exceptions: the string value "None" (undefined field) +and the empty string evaluate to the value zero. + +Examples: + + * ``program: field('series') == 'foo'`` returns '1' if the book's series is 'foo', otherwise ''. + * ``program: 'f.o' in field('series')`` returns '1' if the book's series matches the regular expression ``f.o``, otherwise ''. + * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns 'bar' if the book's series is not 'foo', else 'mumble'. + * ``program: if or(field('series') == 'foo', field('series') == '1632') then 'yes' else 'no' fi`` returns 'yes' if series is either + 'foo' or '1632', otherwise 'no'. + * ``program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi`` returns 'yes' if series is either 'foo' or '1632', + otherwise 'no'. + * ``program: if '11' > '2' then 'yes' else 'no' fi`` returns 'no' because it does a lexical comparison. + * ``program: if '11' ># '2' then 'yes' else 'no' fi`` returns 'yes' because it does a numeric comparison. + +**Additional Available Functions** + +The following functions are available in addition to those described in :ref:`Single Function Mode `. +In General Program Mode the Single Function Mode functions require an additional first parameter +specifying the value to operate on. All parameters are top_expressions (see the grammar above). Note +that the definitive documentation for functions is available in the section :ref:`Function reference `: - * ``and(value, value, ...)`` -- returns the string "1" if all values are not empty, otherwise returns the empty string. This function works + * ``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. * ``add(x, y, ...)`` -- returns the sum of its arguments. Throws an exception if an argument is not a number. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression - * ``approximate_formats()`` -- return a comma-separated list of formats that at one point were associated with the book. There is no - guarantee that the list is correct, although it probably is. This function can be called in Template Program Mode using the template + * ``approximate_formats()`` -- return a comma-separated list of formats that at one point were associated + with the book. There is no + guarantee that the 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. * ``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 @@ -548,122 +665,108 @@ parameters can be statements (sequences of expressions). Note that the definitiv ``{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. -.. _general_mode: -Using General Program Mode ------------------------------------ +.. _template_mode: -For more complicated template programs it is often easier to avoid template syntax (all the `{` and `}` characters), instead writing -a more classic-looking program. You can do this by beginning the template with `program:`. The template program is compiled -and executed. No template processing (e.g., formatting, prefixes, suffixes) is done. The special variable `$` is not set. +More complex programs in templates - Template Program Mode +------------------------------------------------------------- -One advantage of `program:` mode is that braces are no longer special. For example, it is not necessary to use `[[` and `]]` when using the -`template()` function. Another advantage is readability. +Template Program Mode is a blend of :ref:`General Program Mode ` and +:ref:`Single Function Mode `. Template Program Mode differs from +Single Function Mode in that it permits +writing template expressions that refer to other metadata fields, use nested functions, modify variables, +and do arithmetic. It differs from General Program Mode in that the template is contained +between ``{`` and ``}`` characters and doesn't begin with the word ``program:``. The program portion +of the template is a General Program Mode expression list. -General and Template Program Modes both support **``if`` expressions** with the following syntax:: +Example: assume you want a template to show the series for a book if it has one, otherwise show +the value of a custom field #genre. You cannot do this in the :ref:`Single Function Mode ` because +you cannot make reference to another metadata field within a template expression. In Template Program Mode +you can, as the following expression demonstrates:: - if <> then - <> - [elif <> then <>]* - [else <>] - fi + {#series:'ifempty($, field('#genre'))'} -The elif and else parts are optional. The words ``if``, ``then``, ``elif``, ``else``, and ``fi`` are reserved; you cannot use them as -identifier names. You can put newlines and white space wherever they make sense. <> is one template language expression; -semicolons are not allowed. <> is a semicolon-separated sequence of template language expressions, including nested ifs. Examples: +The example shows several things: - * ``program: if field('series') then 'yes' else 'no' fi`` - * ``program: if field('series') then a = 'yes'; b = 'no' else a = 'no'; b='yes' fi; strcat(a, '-', b)`` - * Nested ``if`` example:: + * Template Program Mode is used if the expression begins with ``:'`` and ends with ``'``. Anything else is + assumed to be in :ref:`Single Function Mode `. + * the variable ``$`` stands for the field the expression is operating upon, ``#series`` in this case. + * functions must be given all their arguments. There is no default value. For example, the standard + built-in functions must be given an additional initial parameter indicating the source field, which is + a significant difference from single-function mode. + * white space is ignored and can be used anywhere within the expression. + * constant strings are enclosed in matching quotes, either ``'`` or ``"``. - program: - if field('series') - then - if check_yes_no(field('#mybool'), '', '', '1') - then - 'yes' - else - 'no' - fi - else - 'no series' - fi -An ``if`` produces a value like any other language expression. This means that all the following are valid: +Another example of a complex but rather silly program might help make things clearer:: - * ``program: if field('series') then 'foo' else 'bar' fi`` - * ``program: if field('series') then a = 'foo' else a = 'bar' fi; a`` - * ``program: a = if field('series') then 'foo' else 'bar' fi; a`` - * ``program: a = field(if field('series') then 'series' else 'title' fi); a`` + {series_index:' + substr( + strcat($, '->', + cmp(divide($, 2), 1, + assign(c, 1); substr('lt123', c, 0), + 'eq', 'gt')), + 0, 6) + '| prefix | suffix} -Both modes support classic **relational (comparison) operators**: ``==``, ``!=``, ``<``, -``<=``, ``>``, ``>=``. The operators return '1' if they evaluate to True, otherwise ''. They do case-insensitive -string comparison using lexical order. The binary operator ``in`` is supported. The left hand expression is interpreted -as a regular expression pattern. The ``in`` operator evaluates to '1' if the pattern matches the value of the right hand expression. -The match is case-insensitive. +This program does the following: - Examples: + * specify that the field being looked at is series_index. The variable ``$`` is set to its value. + * calls the ``substr`` function, which takes 3 parameters ``(str, start, end)``. It returns a string formed by extracting the start + through end characters from string, zero-based (the first character is character zero). In this case the string will be computed by the + ``strcat`` function, the start is 0, and the end is 6. In this case it will return the first 6 characters of the string returned by + ``strcat``, which must be evaluated before substr can return. + * calls the ``strcat`` (string concatenation) function. Strcat accepts 1 or more arguments, and returns a string formed by concatenating + all the values. In this case there are three arguments. The first parameter is the value in ``$``, which here is the value of ``series_index``. + The second paremeter is the constant string ``'->'``. The third parameter is the value returned by the ``cmp`` function, which must be fully + evaluated before ``strcat`` can return. + * The ``cmp`` function takes 5 arguments ``(x, y, lt, eq, gt)``. It compares ``x`` and ``y`` and returns the third argument ``lt`` if ``x < y``, + the fourth argument ``eq`` if ``x == y``, and the fifth argument ``gt`` if ``x > y``. As with all functions, all of the parameters can be + statements. In this case the first parameter (the value for ``x``) is the result of dividing the ``series_index`` by 2. The second parameter + ``y`` is the constant ``1``. The third parameter ``lt`` is a statement (more later). The fourth parameter ``eq`` is the constant string + ``'eq'``. The fifth parameter is the constant string ``'gt'``. + * The third parameter (the one for ``lt``) is a statement, or a sequence of expressions. Remember that a statement (a sequence of + semicolon-separated expressions) is also an expression, returning the value of the last expression in the list. In this case, the program + first assigns the value ``1`` to a local variable ``c``, then returns a substring made by extracting the ``c``'th character to the end. + Since ``c`` always contains the constant ``1``, the substring will return the second through ``end``'th characters, or ``'t123'``. + * Once the statement providing the value to the third parameter is executed, ``cmp`` can return a value. At that point, ``strcat` can return + a value, then ``substr`` can return a value. The program then terminates. - * ``program: field('series') == 'foo'`` returns '1' if the book's series is 'foo', otherwise ''. - * ``program: 'f.o' in field('series')`` returns '1' if the book's series matches the regular expression ``f.o``, otherwise ''. - * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns 'bar' if the book's series is not 'foo', else 'mumble'. - * ``program: if or(field('series') == 'foo', field('series') == '1632') then 'yes' else 'no' fi`` returns 'yes' if series is either - 'foo' or '1632', otherwise 'no'. - * ``program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi`` returns 'yes' if series is either 'foo' or '1632', - otherwise 'no'. - * ``program: if '11' > '2' then 'yes' else 'no' fi`` returns 'no' because it is doing a lexical comparison. If you want numeric - comparison instead of lexical comparison, use the operators ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. In this case - the left and right values are set to zero if they are undefined or the empty string. If they are not numbers - then an error is raised. +For various values of series_index, the program returns: -The template language supports **``for`` expressions** with the following syntax:: + * series_index == undefined, result = ``prefix ->t123 suffix`` + * series_index == 0.5, result = ``prefix 0.50-> suffix`` + * series_index == 1, result = ``prefix 1->t12 suffix`` + * series_index == 2, result = ``prefix 2->eq suffix`` + * series_index == 3, result = ``prefix 3->gt suffix`` - for <> in <> [separator <>]: - <> - rof +**All the functions listed under :ref:`Single Function Mode ` +and :ref:`General Program Mode ` can be used in Template Program Mode**. For functions +documented under :ref:`Single Function Mode ` you must supply the value the function is to act +upon as the first parameter in addition to the documented parameters. For example, +in :ref:`General Program Mode ` the parameters of the `test` function +are ``test(x, text_if_not_empty, text_if_empty)``. The `x` parameter, which is +the value to be tested, will almost always be a variable or a function call, often `field()`. -The expression must evaluate to either a metadata field lookup key, for example ``tags`` or ``#genre``, or a list of -values. If the result is a valid lookup name then the field's value is fetched and the separator specified for that field type -is used. If the result isn't a valid lookup name then it is assumed to be a list of values. If the optional keyword ``separator`` -is supplied then the list values must be separated by the result of evaluating the second ``expression``. If the separator is not specified then the list values must be separated by commas. Each resulting value in the list is assigned to the -variable ``id`` then the ``expression_list`` is evaluated. - -Example: This template removes the first hierarchical name for each value in Genre (``#genre``), constructing a list with -the new names:: - - program: - new_tags = ''; - for i in '#genre': - j = re(i, '^.*?\.(.*)$', '\1'); - new_tags = list_union(new_tags, j, ',') - rof; - new_tags - -If the original Genre is ``History.Military, Science Fiction.Alternate History, ReadMe`` then the template returns -``Military, Alternate History, ReadMe``. You could use this template in calibre's -:guilabel:`Edit metadata in bulk -> Search & replace` with :guilabel:`Search for` set to ``template`` to strip -off the first level of the hierarchy and assign the resulting value to Genre. - -Note: the last line in the template, ``new_tags``, isn't necessary in this case because ``for`` returns the value -of the last ``expression`` in the ``expression list``. Stored General Program Mode Templates ---------------------------------------- -General Program Mode supports saving templates and calling those templates from another template. You save -templates using :guilabel:`Preferences->Advanced->Template functions`. More information is provided in that dialog. You call -a template the same way you call a function, passing positional arguments if desired. An argument can be any expression. -Examples of calling a template, assuming the stored template is named ``foo``: +:ref:`General Program Mode ` supports saving templates and calling those templates from another template. +You save templates using :guilabel:`Preferences->Advanced->Template functions`. More information is provided in +that dialog. You call a template the same way you call a function, passing positional arguments if desired. +An argument can be any expression. Examples of calling a template, assuming the stored template is named ``foo``: * ``foo()`` -- call the template passing no arguments. * ``foo(a, b)`` call the template passing the values of the two variables ``a`` and ``b``. * ``foo(if field('series') then field('series_index') else 0 fi)`` -- if the book has a ``series`` then pass the ``series_index``, otherwise pass the value ``0``. -In the stored template you retrieve the arguments passed in the call using the ``arguments`` function. It both declares and -initializes local variables, effectively parameters. The variables are positional; they get the value of the value 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. For example, the following ``arguments`` function declares 2 variables, ``key``, ``alternate``:: +In the stored template you retrieve the arguments passed in the call using the ``arguments`` function. It both +declares and initializes local variables, effectively parameters. The variables are positional; they get +the value of the value 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. For example, the +following ``arguments`` function declares 2 variables, ``key``, ``alternate``:: arguments(key, alternate='series') @@ -687,9 +790,9 @@ and use it during the evaluation. **Developer: how to pass additional information** The additional information is a Python dictionary containing pairs ``variable_name: variable_value`` where the values -should be strings. The template can access the dict, creating template local variables named ``variable_name`` containing the -value ``variable_value``. The user cannot change the name so it is best to use names that won't collide with other -template local variables, for example by prefixing the name with an underscore. +must be strings. The template can access the dict, creating template local variables named ``variable_name`` +containing the value ``variable_value``. The user cannot change the name so it is best to use names that +won't collide with other template local variables, for example by prefixing the name with an underscore. This dict is passed to the template processor (the ``formatter``) using the named parameter ``global_vars=your_dict``. The full method signature is: @@ -704,26 +807,26 @@ The full method signature is: 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 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. +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 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``. +``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 ----------------------------------------- -The three program modes, Single Function Mode (SFM), Template Program Mode (TPM), and -General Program Mode (GPM), work differently. SFM is intended to be 'simple' so it hides a lot -of programming language bits. For example, the value of the column is always passed as an 'invisible' first argument -to a function included in the template. SFM also doesn't support the difference between variables -and strings; all values are strings. +The three program modes, :ref:`Single Function Mode ` (SFM), :ref:`Template Program Mode ` +(TPM), and :ref:`General Program Mode ` (GPM), work differently. SFM is intended to be 'simple' so +it hides a lot of programming language bits. For example, the value of the column is always passed as +an 'invisible' first argument to a function included in the template. SFM also doesn't support the difference +between variables and strings; all values are strings. Example: the following SFM template returns either the series name or the string "no series":: @@ -761,31 +864,34 @@ In GPM it would be:: User-defined Python template functions ------------------------------------------ -You can add your own Python functions to the template processor. Such functions can be used in any of the three template programming modes. -The functions are added by going to :guilabel:`Preferences -> Advanced -> Template functions`. Instructions are shown in that dialog. +You can add your own Python functions to the template processor. Such functions can be used in any of the three +template programming modes. The functions are added by going to +:guilabel:`Preferences -> Advanced -> Template functions`. Instructions are shown in that dialog. Special notes for save/send templates --------------------------------------- -Special processing is applied when a template is used in a `save to disk` or `send to device` template. The values of the fields are cleaned, -replacing characters that are special to file systems with underscores, including slashes. This means that field text cannot be used to create -folders. However, slashes are not changed in prefix or suffix strings, so slashes in these strings will cause folders to be created. +Special processing is applied when a template is used in a `save to disk` or `send to device` template. The +values of the fields are cleaned, replacing characters that are special to file systems with underscores, +including slashes. This means that field text cannot be used to create folders. However, slashes are not +changed in prefix or suffix strings, so slashes in these strings will cause folders to be created. Because of this, you can create variable-depth folder structure. -For example, assume we want the folder structure `series/series_index - title`, with the caveat that if series does not exist, then the -title should be in the top folder. The template to do this is:: +For example, assume we want the folder structure `series/series_index - title`, with the caveat that if +series does not exist, then the title should be in the top folder. The template to do this is:: {series:||/}{series_index:|| - }{title} The slash and the hyphen appear only if series is not empty. -The lookup function lets us do even fancier processing. For example, assume that if a book has a series, then we want the folder structure -`series/series index - title.fmt`. If the book does not have a series, then we want the folder structure `genre/author_sort/title.fmt`. If the -book has no genre, we want to use 'Unknown'. We want two completely different paths, depending on the value of series. +The lookup function lets us do even fancier processing. For example, assume that if a book has a series, +then we want the folder structure `series/series index - title.fmt`. If the book does not have a series +then we want the folder structure `genre/author_sort/title.fmt`. If the book has no genre then we want to +use 'Unknown'. We want two completely different paths, depending on the value of series. To accomplish this, we: - 1. Create a composite field (give it lookup name #AA) containing ``{series}/{series_index} - {title}``. If the series is not empty, - then this template will produce `series/series_index - title`. + 1. Create a composite field (give it lookup name #AA) containing ``{series}/{series_index} - {title}``. If + the series is not empty, then this template will produce `series/series_index - title`. 2. Create a composite field (give it lookup name #BB) containing ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``. This template produces `genre/author_sort/title`, where an empty genre is replaced with `Unknown`. 3. Set the save template to ``{series:lookup(.,#AA,#BB)}``. This template chooses composite field #AA if series is not empty, @@ -794,18 +900,20 @@ To accomplish this, we: Templates and plugboards --------------------------- -Plugboards are used for changing the metadata written into books during send-to-device and save-to-disk operations. A plugboard permits you to -specify a template to provide the data to write into the book's metadata. You can use plugboards to modify the following fields: authors, -author_sort, language, publisher, tags, title, title_sort. This feature helps people who want to use different metadata in books on devices to -solve sorting or display issues. +Plugboards are used for changing the metadata written into books during send-to-device and save-to-disk operations. +A plugboard permits you to specify a template to provide the data to write into the book's metadata. You can use +plugboards to modify the following fields: authors, author_sort, language, publisher, tags, title, title_sort. This +feature helps people who want to use different metadata in books on devices to solve sorting or display issues. -When you create a plugboard, you specify the format and device for which the plugboard is to be used. A special device is provided, ``save_to_disk``, -that is used when saving formats (as opposed to sending them to a device). Once you have chosen the format and device, you choose the metadata -fields to change, providing templates to supply the new values. These templates are `connected` to their destination fields, hence the name +When you create a plugboard, you specify the format and device for which the plugboard is to be used. A +special device is provided, ``save_to_disk``, that is used when saving formats (as opposed to sending them to a +device). Once you have chosen the format and device, you choose the metadata fields to change, providing +templates to supply the new values. These templates are `connected` to their destination fields, hence the name `plugboards`. You can, of course, use composite columns in these templates. -When a plugboard might apply (Content server, save to disk, or send to device), calibre searches the defined plugboards to choose the correct one -for the given format and device. For example, to find the appropriate plugboard for an EPUB book being sent to an ANDROID device, calibre searches +When a plugboard might apply (Content server, save to disk, or send to device), calibre searches the +defined plugboards to choose the correct one for the given format and device. For example, to find the +appropriate plugboard for an EPUB book being sent to an ANDROID device, calibre searches the plugboards using the following search order: * a plugboard with an exact match on format and device, e.g., ``EPUB`` and ``ANDROID`` @@ -813,17 +921,21 @@ the plugboards using the following search order: * a plugboard with the special ``any format`` choice and an exact match on device, e.g., ``any format`` and ``ANDROID`` * a plugboard with ``any format`` and ``any device`` -The tags and authors fields have special treatment, because both of these fields can hold more than one item. A book can have many tags and many -authors. When you specify that one of these two fields is to be changed, the template's result is examined to see if more than one item is there. -For tags, the result is cut apart wherever calibre finds a comma. For example, if the template produces the value ``Thriller, Horror``, then the -result will be two tags, ``Thriller`` and ``Horror``. There is no way to put a comma in the middle of a tag. +The tags and authors fields have special treatment, because both of these fields can hold more than one item. +A book can have many tags and many authors. When you specify that one of these two fields is to be changed, +the template's result is examined to see if more than one item is there. +For tags, the result is cut apart wherever calibre finds a comma. For example, if the template produces +the value ``Thriller, Horror``, then the result will be two tags, ``Thriller`` and ``Horror``. There is no way +to put a comma in the middle of a tag. -The same thing happens for authors, but using a different character for the cut, a `&` (ampersand) instead of a comma. For example, if the template -produces the value ``Blogs, Joe&Posts, Susan``, then the book will end up with two authors, ``Blogs, Joe`` and ``Posts, Susan``. If the template -produces the value ``Blogs, Joe;Posts, Susan``, then the book will have one author with a rather strange name. +The same thing happens for authors, but using a different character for the cut, a `&` (ampersand) instead of +a comma. For example, if the template produces the value ``Blogs, Joe&Posts, Susan``, then the book will +end up with two authors, ``Blogs, Joe`` and ``Posts, Susan``. If the template produces the +value ``Blogs, Joe;Posts, Susan``, then the book will have one author with a rather strange name. -Plugboards affect the metadata written into the book when it is saved to disk or written to the device. Plugboards do not affect the metadata used by -``save to disk`` and ``send to device`` to create the file names. Instead, file names are constructed using the templates entered on the appropriate +Plugboards affect the metadata written into the book when it is saved to disk or written to the device. +Plugboards do not affect the metadata used by ``save to disk`` and ``send to device`` to create the file names. +Instead, file names are constructed using the templates entered on the appropriate preferences window. Tips @@ -831,9 +943,10 @@ Tips You might find the following tips useful. - * Use the Template Tester to test templates. Add the tester to the context menu for books in the library and/or give it a keyboard shortcut. - * Templates can use other templates by referencing composite columns built with the desired template. Alternatively, you could - use Stored Templates. + * Use the Template Tester to test templates. Add the tester to the context menu for books in the + library and/or give it a keyboard shortcut. + * Templates can use other templates by referencing composite columns built with the desired template. + Alternatively, you could use Stored Templates. * In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{}``. This template will always evaluate to an empty string. * The technique described above to show numbers even if they have a zero value works with the standard field series_index. diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 70de34d3b1..37dc097d07 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -372,6 +372,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.font_size_box.setValue(gprefs['gpm_template_editor_font_size']) self.font_size_box.valueChanged.connect(self.font_size_changed) + self.textbox.setFocus() def font_size_changed(self, toWhat): gprefs['gpm_template_editor_font_size'] = toWhat diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 83f2a002b3..19dc2f2f66 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -10,6 +10,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import re, string, traceback, numbers +from math import modf from calibre import prints from calibre.constants import DEBUG @@ -23,8 +24,8 @@ class Node(object): NODE_IF = 2 NODE_ASSIGN = 3 NODE_FUNC = 4 - NODE_STRING_INFIX = 5 - NODE_NUMERIC_INFIX = 6 + NODE_COMPARE_STRING = 5 + NODE_COMPARE_NUMERIC = 6 NODE_CONSTANT = 7 NODE_FIELD = 8 NODE_RAW_FIELD = 9 @@ -35,6 +36,10 @@ class Node(object): NODE_GLOBALS = 14 NODE_SET_GLOBALS = 15 NODE_CONTAINS = 16 + NODE_BINARY_LOGOP = 17 + NODE_UNARY_LOGOP = 18 + NODE_BINARY_ARITHOP = 19 + NODE_UNARY_ARITHOP = 20 class IfNode(Node): @@ -101,24 +106,58 @@ class SetGlobalsNode(Node): self.expression_list = expression_list -class StringInfixNode(Node): +class StringCompareNode(Node): def __init__(self, operator, left, right): Node.__init__(self) - self.node_type = self.NODE_STRING_INFIX + self.node_type = self.NODE_COMPARE_STRING self.operator = operator self.left = left self.right = right -class NumericInfixNode(Node): +class NumericCompareNode(Node): def __init__(self, operator, left, right): Node.__init__(self) - self.node_type = self.NODE_NUMERIC_INFIX + self.node_type = self.NODE_COMPARE_NUMERIC self.operator = operator self.left = left self.right = right +class LogopBinaryNode(Node): + def __init__(self, operator, left, right): + Node.__init__(self) + self.node_type = self.NODE_BINARY_LOGOP + self.operator = operator + self.left = left + self.right = right + + +class LogopUnaryNode(Node): + def __init__(self, operator, expr): + Node.__init__(self) + self.node_type = self.NODE_UNARY_LOGOP + self.operator = operator + self.expr = expr + + +class NumericBinaryNode(Node): + def __init__(self, operator, left, right): + Node.__init__(self) + self.node_type = self.NODE_BINARY_ARITHOP + self.operator = operator + self.left = left + self.right = right + + +class NumericUnaryNode(Node): + def __init__(self, operator, expr): + Node.__init__(self) + self.node_type = self.NODE_UNARY_ARITHOP + self.operator = operator + self.expr = expr + + class ConstantNode(Node): def __init__(self, value): Node.__init__(self) @@ -252,6 +291,55 @@ class _Parser(object): except: return False + def token_op_is_plus(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '+' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_minus(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '-' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_times(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '*' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_divide(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '/' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_and(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '&&' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_or(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '||' and token[0] == self.LEX_OP + except: + return False + + def token_op_is_not(self): + try: + token = self.prog[self.lex_pos] + return token[1] == '!' and token[0] == self.LEX_OP + except: + return False + def token_is_id(self): try: return self.prog[self.lex_pos][0] == self.LEX_ID @@ -357,7 +445,7 @@ class _Parser(object): def expression_list(self): expr_list = [] while not self.token_is_eof(): - expr_list.append(self.infix_expr()) + expr_list.append(self.top_expr()) if not self.token_op_is_semicolon(): break self.consume() @@ -365,7 +453,7 @@ class _Parser(object): def if_expression(self): self.consume() - condition = self.infix_expr() + condition = self.top_expr() if not self.token_is_then(): self.error(_("Missing 'then' in if statement")) self.consume() @@ -390,7 +478,7 @@ class _Parser(object): if not self.token_is_in(): self.error(_("Missing 'in' in for statement")) self.consume() - list_expr = self.infix_expr() + list_expr = self.top_expr() if self.token_is_separator(): self.consume() separator = self.expr() @@ -405,16 +493,66 @@ class _Parser(object): self.consume() return ForNode(variable, list_expr, separator, block) - def infix_expr(self): - left = self.expr() + def top_expr(self): + return self.or_expr() + + def or_expr(self): + left = self.and_expr() + while self.token_op_is_or(): + self.consume() + right = self.and_expr() + left = LogopBinaryNode('or', left, right) + return left + + def and_expr(self): + left = self.not_expr() + while self.token_op_is_and(): + self.consume() + right = self.not_expr() + left = LogopBinaryNode('and', left, right) + return left + + def not_expr(self): + if self.token_op_is_not(): + self.consume() + return LogopUnaryNode('not', self.not_expr()) + return self.compare_expr() + + def compare_expr(self): + left = self.add_subtract_expr() if self.token_op_is_string_infix_compare() or self.token_is_in(): operator = self.token() - return StringInfixNode(operator, left, self.expr()) + return StringCompareNode(operator, left, self.add_subtract_expr()) if self.token_op_is_numeric_infix_compare(): operator = self.token() - return NumericInfixNode(operator, left, self.expr()) + return NumericCompareNode(operator, left, self.add_subtract_expr()) return left + def add_subtract_expr(self): + left = self.times_divide_expr() + while self.token_op_is_plus() or self.token_op_is_minus(): + operator = self.token() + right = self.times_divide_expr() + left = NumericBinaryNode(operator, left, right) + return left + + def times_divide_expr(self): + left = self.unary_plus_minus_expr() + while self.token_op_is_times() or self.token_op_is_divide(): + operator = self.token() + right = self.unary_plus_minus_expr() + left = NumericBinaryNode(operator, left, right) + return left + + def unary_plus_minus_expr(self): + if self.token_op_is_plus(): + self.consume() + return NumericUnaryNode('+', self.unary_plus_minus_expr()) + if self.token_op_is_minus(): + self.consume() + return NumericUnaryNode('-', self.unary_plus_minus_expr()) + return self.expr() + def call_expression(self, name, arguments): subprog = self.funcs[name].cached_parse_tree if subprog is None: @@ -428,6 +566,13 @@ class _Parser(object): return CallNode(subprog, arguments) def expr(self): + if self.token_op_is_lparen(): + self.consume() + rv = self.top_expr() + if not self.token_op_is_rparen(): + self.error(_('Missing )')) + self.consume() + return rv if self.token_is_if(): return self.if_expression() if self.token_is_for(): @@ -439,7 +584,7 @@ class _Parser(object): if self.token_op_is_equals(): # classic assignment statement self.consume() - return AssignNode(id_, self.infix_expr()) + return AssignNode(id_, self.top_expr()) return VariableNode(id_) # We have a function. @@ -453,7 +598,7 @@ class _Parser(object): arguments = list() while not self.token_op_is_rparen(): # evaluate the expression (recursive call) - arguments.append(self.infix_expr()) + arguments.append(self.top_expr()) if not self.token_op_is_comma(): break self.consume() @@ -520,12 +665,12 @@ class _Interpreter(object): val = self.expr(p) return val - INFIX_STRING_OPS = { + INFIX_STRING_COMPARE_OPS = { "==": lambda x, y: strcmp(x, y) == 0, "!=": lambda x, y: strcmp(x, y) != 0, - "<": lambda x, y: strcmp(x, y) < 0, + "<": lambda x, y: strcmp(x, y) < 0, "<=": lambda x, y: strcmp(x, y) <= 0, - ">": lambda x, y: strcmp(x, y) > 0, + ">": lambda x, y: strcmp(x, y) > 0, ">=": lambda x, y: strcmp(x, y) >= 0, "in": lambda x, y: re.search(x, y, flags=re.I), } @@ -534,16 +679,16 @@ class _Interpreter(object): try: left = self.expr(prog.left) right = self.expr(prog.right) - return ('1' if self.INFIX_STRING_OPS[prog.operator](left, right) else '') + return ('1' if self.INFIX_STRING_COMPARE_OPS[prog.operator](left, right) else '') except: self.error(_('Error during string comparison. Operator {0}').format(prog.operator)) - INFIX_NUMERIC_OPS = { + INFIX_NUMERIC_COMPARE_OPS = { "==#": lambda x, y: x == y, "!=#": lambda x, y: x != y, - "<#": lambda x, y: x < y, + "<#": lambda x, y: x < y, "<=#": lambda x, y: x <= y, - ">#": lambda x, y: x > y, + ">#": lambda x, y: x > y, ">=#": lambda x, y: x >= y, } @@ -556,7 +701,7 @@ class _Interpreter(object): try: left = self.float_deal_with_none(self.expr(prog.left)) right = self.float_deal_with_none(self.expr(prog.right)) - return '1' if self.INFIX_NUMERIC_OPS[prog.operator](left, right) else '' + return '1' if self.INFIX_NUMERIC_COMPARE_OPS[prog.operator](left, right) else '' except: self.error(_('Value used in comparison is not a number. Operator {0}').format(prog.operator)) @@ -687,6 +832,55 @@ class _Interpreter(object): return self.expr(prog.match_expression) return self.expr(prog.not_match_expression) + LOGICAL_BINARY_OPS = { + 'and': lambda self, x, y: self.expr(x) and self.expr(y), + 'or': lambda self, x, y: self.expr(x) or self.expr(y), + } + + def do_node_logop(self, prog): + try: + return ('1' if self.LOGICAL_BINARY_OPS[prog.operator](self, prog.left, prog.right) else '') + except: + self.error(_('Error during operator evaluation. Operator {0}').format(prog.operator)) + + LOGICAL_UNARY_OPS = { + 'not': lambda x: not x, + } + + def do_node_logop_unary(self, prog): + try: + expr = self.expr(prog.expr) + return ('1' if self.LOGICAL_UNARY_OPS[prog.operator](expr) else '') + except: + self.error(_('Error during operator evaluation. Operator {0}').format(prog.operator)) + + ARITHMETIC_BINARY_OPS = { + '+': lambda x, y: x + y, + '-': lambda x, y: x - y, + '*': lambda x, y: x * y, + '/': lambda x, y: x / y, + } + + def do_node_binary_arithop(self, prog): + try: + answer = self.ARITHMETIC_BINARY_OPS[prog.operator](float(self.expr(prog.left)), + float(self.expr(prog.right))) + return unicode_type(answer if modf(answer)[0] != 0 else int(answer)) + except: + self.error(_('Error during arithmetic operator evaluation. Operator {0}').format(prog.operator)) + + ARITHMETIC_UNARY_OPS = { + '+': lambda x: x, + '-': lambda x: -x, + } + + def do_node_unary_arithop(self, prog): + try: + expr = self.ARITHMETIC_UNARY_OPS[prog.operator](float(self.expr(prog.expr))) + return unicode_type(expr if modf(expr)[0] != 0 else int(expr)) + except: + self.error(_('Error during arithmetic operator evaluation. Operator {0}').format(prog.operator)) + NODE_OPS = { Node.NODE_IF: do_node_if, Node.NODE_ASSIGN: do_node_assign, @@ -695,8 +889,8 @@ class _Interpreter(object): Node.NODE_FUNC: do_node_func, Node.NODE_FIELD: do_node_field, Node.NODE_RAW_FIELD: do_node_raw_field, - Node.NODE_STRING_INFIX: do_node_string_infix, - Node.NODE_NUMERIC_INFIX: do_node_numeric_infix, + Node.NODE_COMPARE_STRING: do_node_string_infix, + Node.NODE_COMPARE_NUMERIC:do_node_numeric_infix, Node.NODE_ARGUMENTS: do_node_arguments, Node.NODE_CALL: do_node_call, Node.NODE_FIRST_NON_EMPTY:do_node_first_non_empty, @@ -704,6 +898,10 @@ class _Interpreter(object): Node.NODE_GLOBALS: do_node_globals, Node.NODE_SET_GLOBALS: do_node_set_globals, Node.NODE_CONTAINS: do_node_contains, + Node.NODE_BINARY_LOGOP: do_node_logop, + Node.NODE_UNARY_LOGOP: do_node_logop_unary, + Node.NODE_BINARY_ARITHOP: do_node_binary_arithop, + Node.NODE_UNARY_ARITHOP: do_node_unary_arithop, } def expr(self, prog): @@ -781,14 +979,15 @@ class TemplateFormatter(string.Formatter): (r'.*?\)', lambda x,t: t[:-1]), ]) - # ################# 'Functional' template language ###################### + # ################# Template language lexical analyzer ###################### lex_scanner = re.Scanner([ (r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)), (r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), # noqa (r'(if|then|else|elif|fi)\b',lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa (r'(for|in|rof)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa - (r'[(),=;:]', lambda x,t: (_Parser.LEX_OP, t)), # noqa + (r'(\|\||&&|!)', lambda x,t: (_Parser.LEX_OP, t)), # noqa + (r'[(),=;:\+\-*/]', lambda x,t: (_Parser.LEX_OP, t)), # noqa (r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa (r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa (r'\w+', lambda x,t: (_Parser.LEX_ID, t)), # noqa diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index c423bab003..258b552594 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -884,9 +884,10 @@ class BuiltinSelect(BuiltinFormatterFunction): if not val: return '' vals = [v.strip() for v in val.split(',')] + tkey = key+':' for v in vals: - if v.startswith(key+':'): - return v[len(key)+1:] + if v.startswith(tkey): + return v[len(tkey):] return '' @@ -1096,7 +1097,7 @@ class BuiltinSubitems(BuiltinFormatterFunction): si = int(start_index) ei = int(end_index) has_periods = '.' in val - items = [v.strip() for v in val.split(',')] + items = [v.strip() for v in val.split(',') if v.strip()] rv = set() for item in items: if has_periods and '.' in item: