diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 259c7fe17d..1dca596933 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -218,7 +218,7 @@ General Program Mode 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' | + compare_op ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | 'inlist' | '==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#' add_sub_expr ::= times_div_expr [ add_sub_op times_div_expr ]* add_sub_op ::= '+' | '-' @@ -343,8 +343,8 @@ Relational operators return ``'1'`` if the comparison is true, otherwise the emp There are two forms of relational operators: string comparisons and numeric comparisons. -String comparisons do case-insensitive string comparison using lexical order. The supported string comparison operators are ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, and ``in``. -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 value of left-hand expression interpreted as a regular expression matches the value of the right hand expression. The match is case-insensitive. +String comparisons do case-insensitive string comparison using lexical order. The supported string comparison operators are ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``in``, and ``inlist``. +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 value of left-hand regular expression matches the value of the right hand expression. The ``inlist`` operator is true if the left hand regular expression matches any one of the items in the right hand list where the items in the list are separated by commas. The matches are case-insensitive. The numeric comparison operators are ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. The left and right expressions must evaluate to numeric values with two exceptions: both the string value "None" (undefined field) and the empty string evaluate to the value zero. @@ -352,6 +352,8 @@ 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`` (e.g., `foo`, `Off Onyx`, etc.), otherwise ``''``. + * ``program: 'science' inlist field('#genre')`` returns ``'1'`` if any of the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc.), otherwise ``''``. + * ``program: '^science$' inlist field('#genre')`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`. The genres `History of Science` and `Science Fiction` don't match. If there isn't a match then returns ``''``. * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not ``foo``. Otherwise it returns ``'mumble'``. * ``program: if 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'``. diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index d30a65911b..5f8a046680 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -46,8 +46,8 @@ class TemplateHighlighter(QSyntaxHighlighter): Formats = {} BN_FACTOR = 1000 - KEYWORDS = ["program", 'if', 'then', 'else', 'elif', 'fi', 'for', 'in', - 'separator', 'rof', 'break', 'continue', 'return'] + KEYWORDS = ["program", 'if', 'then', 'else', 'elif', 'fi', 'for', 'rof', + 'separator', 'break', 'continue', 'return', 'in', 'inlist'] def __init__(self, parent=None, builtin_functions=None): super(TemplateHighlighter, self).__init__(parent) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 5b753bca0b..0c5950d09a 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 functools import partial from math import modf from calibre import prints @@ -466,7 +467,8 @@ class _Parser(object): def compare_expr(self): left = self.add_subtract_expr() - if self.token_op_is_string_infix_compare() or self.token_is('in'): + if (self.token_op_is_string_infix_compare() or + self.token_is('in') or self.token_is('inlist')): operator = self.token() return StringCompareNode(self.line_number, operator, left, self.add_subtract_expr()) if self.token_op_is_numeric_infix_compare(): @@ -692,6 +694,8 @@ class _Interpreter(object): ">": 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), + "inlist": lambda x, y: list(filter(partial(re.search, x, flags=re.I), + [v.strip() for v in y.split(',') if v.strip()])) } def do_node_string_infix(self, prog): @@ -1146,8 +1150,9 @@ class TemplateFormatter(string.Formatter): (r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)), # noqa (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|return)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa + (r'(for|in|rof|separator)\b',lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa (r'(break|continue)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa + (r'(return|inlist)\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'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa