mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
cc7d9d08f3
@ -242,12 +242,20 @@ General Program Mode
|
||||
[ elif_expr ] [ 'else' expression_list ] 'fi'
|
||||
condition ::= top_expression
|
||||
elif_expr ::= 'elif' condition 'then' expression_list elif_expr | ''
|
||||
for_expr ::= 'for' identifier 'in' list_expr
|
||||
for_expr ::= for_list | for_range
|
||||
for_list ::= 'for' identifier 'in' list_expr
|
||||
[ 'separator' separator_expr ] ':' expression_list 'rof'
|
||||
for_range ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
|
||||
range_expr ::= 'range' '(' [ start_expr ',' ] stop_expr
|
||||
[ ',' step_expr [ ',' limit_expr ] ] ')'
|
||||
list_expr ::= top_expression
|
||||
break_expr ::= 'break'
|
||||
continue_expr ::= 'continue'
|
||||
separator_expr ::= top_expression
|
||||
start_expr ::= top_expression
|
||||
stop_expr ::= top_expression
|
||||
step_expr ::= top_expression
|
||||
limit_expr ::= top_expression
|
||||
|
||||
Notes:
|
||||
|
||||
@ -324,7 +332,7 @@ As a last example, this program returns the value of the ``series`` column if th
|
||||
|
||||
**For expressions**
|
||||
|
||||
The ``for`` expression iterates over a list of values, processing them one at a time. The ``list_expression`` must evaluate to either a metadata field ``lookup name``, 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. The list is assumed to be separated by commas unless the optional keyword ``separator`` is supplied, in which case the list values must be separated by the result of evaluating the ``separator_expr``. Each value in the list is assigned to the specified variable then the ``expression_list`` is evaluated. You can use ``break`` to jump out of the loop, and ``continue`` to jump to the beginning of the loop for the next iteration.
|
||||
The ``for`` expression iterates over a list of values, processing them one at a time. The ``list_expression`` must evaluate either to a metadata field ``lookup name`` e.g., ``tags`` or ``#genre``, or to a list of values. The :ref:`range() function <range_function>` (see below) generates a list of numbers. 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. The list is assumed to be separated by commas unless the optional keyword ``separator`` is supplied, in which case the list values must be separated by the result of evaluating the ``separator_expr``. A separator cannot be used if the list is generated by ``range()``. Each value in the list is assigned to the specified variable then the ``expression_list`` is evaluated. You can use ``break`` to jump out of the loop, and ``continue`` to jump to the beginning of the loop for the next iteration.
|
||||
|
||||
Example: This template removes the first hierarchical name for each value in Genre (``#genre``), constructing a list with the new names::
|
||||
|
||||
@ -541,7 +549,20 @@ In `GPM` the functions described in `Single Function Mode` all require an additi
|
||||
* ``not(value)`` -- returns the string "1" if the value is empty, otherwise returns the empty string. This function can usually be replaced with the unary not (``!``) operator.
|
||||
* ``ondevice()`` -- return the string ``'Yes'`` if ``ondevice`` is set, otherwise return the empty string.
|
||||
* ``or(value [, value]*)`` -- returns the string ``'1'`` if any value is not empty, otherwise returns the empty string. You can have as many values as you want. This function can usually be replaced by the ``||`` operator. A reason it cannot be replaced is if short-circuiting will change the results because of side effects.
|
||||
* ``print(a [, b]*)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole. The ``print`` function always returns the empty string.
|
||||
* ``print(a [, b]*)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go into a black hole. The ``print`` function always returns its first argument.
|
||||
|
||||
.. _range_function:
|
||||
|
||||
* ``range(start, stop, step, limit)`` -- returns a list of numbers generated by looping over the range specified by the parameters start, stop, and step, with a maximum length of limit. The first value produced is 'start'. Subsequent values ``next_v = current_v + step``. The loop continues while ``next_v < stop`` assuming ``step`` is positive, otherwise while ``next_v > stop``. An empty list is produced if ``start`` fails the test: ``start >= stop`` if ``step`` is positive. The ``limit`` sets the maximum length of the list and has a default of 1000. The parameters ``start``, ``step``, and ``limit`` are optional. Calling ``range()`` with one argument specifies ``stop``. Two arguments specify ``start`` and ``stop``. Three arguments specify ``start``, ``stop``, and ``step``. Four arguments specify ``start``, ``stop``, ``step`` and ``limit``. Examples::
|
||||
|
||||
range(5) -> '0, 1, 2, 3, 4'
|
||||
range(0, 5) -> '0, 1, 2, 3, 4'
|
||||
range(-1, 5) -> '-1, 0, 1, 2, 3, 4'
|
||||
range(1, 5) -> '1, 2, 3, 4'
|
||||
range(1, 5, 2) -> '1, 3'
|
||||
range(1, 5, 2, 5) -> '1, 3'
|
||||
range(1, 5, 2, 1) -> error(limit exceeded)
|
||||
|
||||
* ``raw_field(lookup_name [, optional_default])`` -- returns the metadata field named by ``lookup_name`` without applying any formatting. It evaluates and returns the optional second argument ``optional_default`` if the field's value is undefined (``None``).
|
||||
* ``raw_list(lookup_name, separator)`` -- returns the metadata list named by ``lookup_name`` without applying any formatting or sorting, with the items separated by separator.
|
||||
* ``re_group(value, pattern [, template_for_group]*)`` -- return a string made by applying the regular expression pattern to ``value`` and replacing each matched instance with the the value returned by the corresponding template. In :ref:`Template Program Mode <template_mode>`, like for the ``template`` and the ``eval`` functions, you use ``[[`` for ``{`` and ``]]`` for ``}``.
|
||||
|
@ -47,7 +47,7 @@ class TemplateHighlighter(QSyntaxHighlighter):
|
||||
|
||||
KEYWORDS = ["program", 'if', 'then', 'else', 'elif', 'fi', 'for', 'rof',
|
||||
'separator', 'break', 'continue', 'return', 'in', 'inlist',
|
||||
'def', 'fed']
|
||||
'def', 'fed', 'limit']
|
||||
|
||||
def __init__(self, parent=None, builtin_functions=None):
|
||||
super().__init__(parent)
|
||||
|
@ -50,6 +50,7 @@ class Node:
|
||||
NODE_BINARY_STRINGOP = 27
|
||||
NODE_LOCAL_FUNCTION_DEFINE = 28
|
||||
NODE_LOCAL_FUNCTION_CALL = 29
|
||||
NODE_RANGE = 30
|
||||
|
||||
def __init__(self, line_number, name):
|
||||
self.my_line_number = line_number
|
||||
@ -83,6 +84,18 @@ class ForNode(Node):
|
||||
self.block = block
|
||||
|
||||
|
||||
class RangeNode(Node):
|
||||
def __init__(self, line_number, variable, start_expr, stop_expr, step_expr, limit_expr, block):
|
||||
Node.__init__(self, line_number, 'for ...:')
|
||||
self.node_type = self.NODE_RANGE
|
||||
self.variable = variable
|
||||
self.start_expr = start_expr
|
||||
self.stop_expr = stop_expr
|
||||
self.step_expr = step_expr
|
||||
self.limit_expr = limit_expr
|
||||
self.block = block
|
||||
|
||||
|
||||
class BreakNode(Node):
|
||||
def __init__(self, line_number):
|
||||
Node.__init__(self, line_number, 'break')
|
||||
@ -471,12 +484,38 @@ class _Parser:
|
||||
self.error(_("{0} statement: expected '{1}', "
|
||||
"found '{2}'").format('for', 'in', self.token_text()))
|
||||
self.consume()
|
||||
list_expr = self.top_expr()
|
||||
if self.token_is('separator'):
|
||||
if self.token_text() == 'range':
|
||||
is_list = False
|
||||
self.consume()
|
||||
if not self.token_op_is('('):
|
||||
self.error(_("{0} statement: expected '(', "
|
||||
"found '{1}'").format('for', self.token_text()))
|
||||
self.consume()
|
||||
start_expr = ConstantNode(line_number, '0')
|
||||
step_expr = ConstantNode(line_number, '1')
|
||||
limit_expr = None
|
||||
stop_expr = self.top_expr()
|
||||
if self.token_op_is(','):
|
||||
self.consume()
|
||||
start_expr = stop_expr
|
||||
stop_expr = self.top_expr()
|
||||
if self.token_op_is(','):
|
||||
self.consume()
|
||||
step_expr = self.top_expr()
|
||||
if self.token_op_is(','):
|
||||
self.consume()
|
||||
limit_expr = self.top_expr()
|
||||
if not self.token_op_is(')'):
|
||||
self.error(_("{0} statement: expected ')', "
|
||||
"found '{1}'").format('for', self.token_text()))
|
||||
self.consume()
|
||||
separator = self.expr()
|
||||
else:
|
||||
separator = None
|
||||
is_list = True
|
||||
list_expr = self.top_expr()
|
||||
if self.token_is('separator'):
|
||||
separator = self.expr()
|
||||
else:
|
||||
separator = None
|
||||
if not self.token_op_is(':'):
|
||||
self.error(_("{0} statement: expected '{1}', "
|
||||
"found '{2}'").format('for', ':', self.token_text()))
|
||||
@ -486,7 +525,9 @@ class _Parser:
|
||||
self.error(_("{0} statement: expected '{1}', "
|
||||
"found '{2}'").format('for', 'rof', self.token_text()))
|
||||
self.consume()
|
||||
return ForNode(line_number, variable, list_expr, separator, block)
|
||||
if is_list:
|
||||
return ForNode(line_number, variable, list_expr, separator, block)
|
||||
return RangeNode(line_number, variable, start_expr, stop_expr, step_expr, limit_expr, block)
|
||||
|
||||
def define_function_expression(self):
|
||||
self.consume()
|
||||
@ -866,6 +907,57 @@ class _Interpreter:
|
||||
except Exception as e:
|
||||
self.error(_("Unhandled exception '{0}'").format(e), line_number)
|
||||
|
||||
def do_node_range(self, prog):
|
||||
line_number = prog.line_number
|
||||
try:
|
||||
try:
|
||||
start_val = int(self.float_deal_with_none(self.expr(prog.start_expr)))
|
||||
except ValueError:
|
||||
self.error(_("{0}: {1} must be an integer").format('for', 'start'), line_number)
|
||||
try:
|
||||
stop_val = int(self.float_deal_with_none(self.expr(prog.stop_expr)))
|
||||
except ValueError:
|
||||
self.error(_("{0}: {1} must be an integer").format('for', 'stop'), line_number)
|
||||
try:
|
||||
step_val = int(self.float_deal_with_none(self.expr(prog.step_expr)))
|
||||
except ValueError:
|
||||
self.error(_("{0}: {1} must be an integer").format('for', 'step'), line_number)
|
||||
try:
|
||||
limit_val = (1000 if prog.limit_expr is None else
|
||||
int(self.float_deal_with_none(self.expr(prog.limit_expr))))
|
||||
except ValueError:
|
||||
self.error(_("{0}: {1} must be an integer").format('for', 'limit'), line_number)
|
||||
var = prog.variable
|
||||
if (self.break_reporter):
|
||||
self.break_reporter("'for': start value", str(start_val), line_number)
|
||||
self.break_reporter("'for': stop value", str(stop_val), line_number)
|
||||
self.break_reporter("'for': step value", str(step_val), line_number)
|
||||
self.break_reporter("'for': limit value", str(limit_val), line_number)
|
||||
ret = ''
|
||||
try:
|
||||
range_gen = range(start_val, stop_val, step_val)
|
||||
if len(range_gen) > limit_val:
|
||||
self.error(
|
||||
_("{0}: the range length ({1}) is larger than the limit ({2})").format
|
||||
('for', str(len(range_gen)), str(limit_val)), line_number)
|
||||
for x in (str(x) for x in range_gen):
|
||||
try:
|
||||
if (self.break_reporter):
|
||||
self.break_reporter(f"'for': assign to loop index '{var}'", x, line_number)
|
||||
self.locals[var] = x
|
||||
ret = self.expression_list(prog.block)
|
||||
except ContinueExecuted as e:
|
||||
ret = e.get_value()
|
||||
except BreakExecuted as e:
|
||||
ret = e.get_value()
|
||||
if (self.break_reporter):
|
||||
self.break_reporter("'for' block value", ret, line_number)
|
||||
return ret
|
||||
except (StopException, ValueError) as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
self.error(_("Unhandled exception '{0}'").format(e), line_number)
|
||||
|
||||
def do_node_rvalue(self, prog):
|
||||
try:
|
||||
if (self.break_reporter):
|
||||
@ -1270,6 +1362,7 @@ class _Interpreter:
|
||||
Node.NODE_CALL_STORED_TEMPLATE: do_node_stored_template_call,
|
||||
Node.NODE_FIRST_NON_EMPTY: do_node_first_non_empty,
|
||||
Node.NODE_FOR: do_node_for,
|
||||
Node.NODE_RANGE: do_node_range,
|
||||
Node.NODE_GLOBALS: do_node_globals,
|
||||
Node.NODE_SET_GLOBALS: do_node_set_globals,
|
||||
Node.NODE_CONTAINS: do_node_contains,
|
||||
@ -1378,6 +1471,7 @@ class TemplateFormatter(string.Formatter):
|
||||
(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|separator)\b',lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(separator|limit)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(def|fed|continue)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(return|inlist|break)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||
(r'(\|\||&&|!|{|})', lambda x,t: (_Parser.LEX_OP, t)), # noqa
|
||||
|
@ -1409,7 +1409,7 @@ class BuiltinListJoin(BuiltinFormatterFunction):
|
||||
" a = list_join('#@#', $authors, '&', $tags, ',');\n"
|
||||
" b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&')\n"
|
||||
"You can use expressions to generate a list. For example, "
|
||||
"assume you want items for ``authors`` and ``#genre``, but "
|
||||
"assume you want items for authors and #genre, but "
|
||||
"with the genre changed to the word 'Genre: ' followed by "
|
||||
"the first letter of the genre, i.e. the genre 'Fiction' "
|
||||
"becomes 'Genre: F'. The following will do that\n"
|
||||
@ -1452,6 +1452,54 @@ class BuiltinListUnion(BuiltinFormatterFunction):
|
||||
return separator.join(res.values())
|
||||
|
||||
|
||||
class BuiltinRange(BuiltinFormatterFunction):
|
||||
name = 'range'
|
||||
arg_count = -1
|
||||
category = 'List manipulation'
|
||||
__doc__ = doc = _("range(start, stop, step, limit) -- "
|
||||
"returns a list of numbers generated by looping over the "
|
||||
"range specified by the parameters start, stop, and step, "
|
||||
"with a maximum length of limit. The first value produced "
|
||||
"is 'start'. Subsequent values next_v are "
|
||||
"current_v+step. The loop continues while "
|
||||
"next_v<stop assuming step is positive, otherwise "
|
||||
"while next_v>stop. An empty list is produced if "
|
||||
"start fails the test: start>=stop if step "
|
||||
"is positive. The limit sets the maximum length of "
|
||||
"the list and has a default of 1000. The parameters "
|
||||
"start, step, and limit are optional. "
|
||||
"Calling range() with one argument specifies stop. "
|
||||
"Two arguments specify start and stop. Three arguments "
|
||||
"specify start, stop, and step. Four "
|
||||
"arguments specify start, stop, step and limit. "
|
||||
"Examples: range(5)->'0,1,2,3,4'. range(0,5)->'0,1,2,3,4'. "
|
||||
"range(-1,5)->'-1,0,1,2,3,4'. range(1,5)->'1,2,3,4'. "
|
||||
"range(1,5,2)->'1,3'. range(1,5,2,5)->'1,3'. "
|
||||
"range(1,5,2,1)->error(limit exceeded).")
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
||||
limit_val = 1000
|
||||
start_val = 0
|
||||
step_val = 1
|
||||
if len(args) == 1:
|
||||
stop_val = int(args[0] if args[0] and args[0] != 'None' else 0)
|
||||
elif len(args) == 2:
|
||||
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
|
||||
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
|
||||
elif len(args) >= 3:
|
||||
start_val = int(args[0] if args[0] and args[0] != 'None' else 0)
|
||||
stop_val = int(args[1] if args[1] and args[1] != 'None' else 0)
|
||||
step_val = int(args[2] if args[2] and args[2] != 'None' else 0)
|
||||
if len(args) > 3:
|
||||
limit_val = int(args[3] if args[3] and args[3] != 'None' else 0)
|
||||
r = range(start_val, stop_val, step_val)
|
||||
if len(r) > limit_val:
|
||||
raise ValueError(
|
||||
_("{0}: length ({1}) longer than limit ({2})").format(
|
||||
'range', len(r), str(limit_val)))
|
||||
return ', '.join([str(v) for v in r])
|
||||
|
||||
|
||||
class BuiltinListRemoveDuplicates(BuiltinFormatterFunction):
|
||||
name = 'list_remove_duplicates'
|
||||
arg_count = 2
|
||||
@ -2166,7 +2214,8 @@ _formatter_builtins = [
|
||||
BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(),
|
||||
BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(),
|
||||
BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(),
|
||||
BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRawField(), BuiltinRawList(),
|
||||
BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRange(),
|
||||
BuiltinRawField(), BuiltinRawList(),
|
||||
BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(),
|
||||
BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(),
|
||||
BuiltinStrcmp(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user