diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index ee7e1f572b..f34355de61 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -61,6 +61,7 @@ class Node: NODE_SWITCH_IF = 32 NODE_LIST_COUNT_FIELD = 33 NODE_WITH = 34 + NODE_FSTRING = 35 def __init__(self, line_number, name): self.my_line_number = line_number @@ -77,7 +78,7 @@ class Node: class WithNode(Node): def __init__(self, line_number, book_id, block): - Node.__init__(self, line_number, 'if ...') + Node.__init__(self, line_number, 'with ...') self.node_type = self.NODE_WITH self.book_id = book_id self.block = block @@ -348,6 +349,13 @@ class ListCountFieldNode(Node): self.expression = expression +class FStringNode(Node): + def __init__(self, line_number, string): + Node.__init__(self, line_number, 'f_string') + self.node_type = self.NODE_FSTRING + self.string = string + + class _Parser: LEX_OP = 1 LEX_ID = 2 @@ -743,7 +751,9 @@ class _Parser: 'strcat': (lambda _: True, lambda ln, args: StrcatNode(ln, args)), 'list_count_field': (lambda args: len(args) == 1, - lambda ln, args: ListCountFieldNode(ln, args[0])) + lambda ln, args: ListCountFieldNode(ln, args[0])), + 'f_string': (lambda args: len(args) == 1, + lambda ln, args: FStringNode(ln, args[0])), } def expr(self): @@ -1392,6 +1402,14 @@ class _Interpreter: self.break_reporter(prog.node_name, res, prog.line_number) return res + def do_node_f_string(self, prog): + def repl(mo): + print(mo.group()[1:-1]) + p = self.parent.gpm_parser.program(self.parent, self.funcs, + self.parent.lex_scanner.scan(mo.group()[1:-1])) + return self.expr(p) + return str(re.sub(r'\{.*?\}', repl, self.expr(prog.string))) + def do_node_list_count_field(self, prog): name = field_metadata.search_term_to_field_key(self.expr(prog.expression)) res = getattr(self.parent_book, name, None) @@ -1654,6 +1672,7 @@ class _Interpreter: Node.NODE_LOCAL_FUNCTION_CALL: do_node_local_function_call, Node.NODE_LIST_COUNT_FIELD: do_node_list_count_field, Node.NODE_WITH: do_node_with, + Node.NODE_FSTRING: do_node_f_string, } def expr(self, prog): diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 8c3f2b05f0..fbfb55d17f 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -3761,6 +3761,49 @@ This function can be used only in the GUI. return '1' if d.exec() == QDialog.DialogCode.Accepted else '' +class BuiltinFString(BuiltinFormatterFunction): + name = 'f_string' + arg_count = 1 + category = FORMATTING_VALUES + def __doc__getter__(self): return translate_ffml( +r''' +``f_string(string)`` -- interpret ``string`` similar to how python interprets ``f`` strings. +The indended use is to simplify long sequences of ``str & str`` or strcat(a,b,c) expressions. + +Text between braces (``{`` and ``}``) must be General Program Mode template +expressions. The expressions, which can be expression lists, are evaluated in +the current context (current book and local variables). Text not between +braces is passed through unchanged. + +Examples: +[LIST] +[*]``f_string('Here is the title: {$title}')`` - returns the string with ``{$title}`` +replaced with the title of the current book. For example, if the book's title is +`20,000 Leagues Under the Sea` then the ``f_string()`` returns +`Here is the title: 20,000 Leagues Under the Sea`. +[*]Assuming the current date is 18 Sept 2025, this ``f_string()`` +[CODE] +f_string("Today's date: the {d = today(); format_date(d, 'd')} of {format_date(d, 'MMMM')}, {format_date(d, 'yyyy')}") +[/CODE] +returns the string `Today's date: the 18 of September, 2025`. +Note the expression list (an assignment then an ``if`` statement) used in the first ``{ ... }`` group to assign today's date to a local variable. +[*]If the book is book #3 in a series named `Foo` that has 5 books then this template +[CODE] +program: + if $series then + series_count = book_count('series:"""=' & $series & '"""', 0); + return f_string("{$series}, book {$series_index} of {series_count}") + fi; + return 'This book is not in a series' +[/CODE] +returns `Foo, book 3 of 5` +[/LIST] +''') + + def evaluate(self, formatter, kwargs, mi, locals, fstring): + raise ValueError(_('This function cannot be called directly. It is built into the formatter')) + + _formatter_builtins = [ BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(), BuiltinAssign(), @@ -3776,7 +3819,7 @@ _formatter_builtins = [ BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(), BuiltinFormatDate(), BuiltinFormatDateField(), BuiltinFormatDuration(), BuiltinFormatNumber(), BuiltinFormatsModtimes(),BuiltinFormatsPaths(), BuiltinFormatsPathSegments(), - BuiltinFormatsSizes(), BuiltinFractionalPart(),BuiltinGetLink(), + BuiltinFormatsSizes(), BuiltinFractionalPart(),BuiltinFString(), BuiltinGetLink(), BuiltinGetNote(), BuiltinGlobals(), BuiltinHasCover(), BuiltinHasExtraFiles(), BuiltinHasNote(), BuiltinHumanReadable(), BuiltinIdentifierInList(), BuiltinIfempty(), BuiltinIsDarkMode(), BuiltinLanguageCodes(), BuiltinLanguageStrings(),