More template stuff:

- Replace the newline() function with the character('name') function
- Document the new function
- In the template editor/debugger, display special characters like newline as escaped characters
This commit is contained in:
Charles Haley 2021-04-21 16:44:16 +01:00
parent dc157a4e6b
commit 7d3c0b5b05
4 changed files with 70 additions and 15 deletions

View File

@ -383,6 +383,7 @@ In `GPM` the functions described in `Single Function Mode` all require an additi
More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1. More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1.
* ``ceiling(x)`` -- returns the smallest integer greater than or equal to ``x``. Throws an exception if ``x`` is not a number. * ``ceiling(x)`` -- returns the smallest integer greater than or equal to ``x``. Throws an exception if ``x`` is not a number.
* ``character(character_name)`` -- returns the character named by character_name. For example, ``character('newline')`` returns a newline character (``'\n'``). The supported character names are ``newline``, ``return``, ``tab``, and ``backslash``.
* ``cmp(x, y, lt, eq, gt)`` -- compares ``x`` and ``y`` after converting both to numbers. Returns ``lt`` if ``x <# y``, ``eq`` if ``x ==# y``, otherwise ``gt``. This function can usually be replaced with one of the numeric compare operators (``==#``, ``<#``, ``>#``, etc). * ``cmp(x, y, lt, eq, gt)`` -- compares ``x`` and ``y`` after converting both to numbers. Returns ``lt`` if ``x <# y``, ``eq`` if ``x ==# y``, otherwise ``gt``. This function can usually be replaced with one of the numeric compare operators (``==#``, ``<#``, ``>#``, etc).
* ``connected_device_name(storage_location_key)`` -- if a device is connected then return the device name, otherwise return the empty string. Each storage location on a device has its own device name. The ``storage_location_key`` names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI. * ``connected_device_name(storage_location_key)`` -- if a device is connected then return the device name, otherwise return the empty string. Each storage location on a device has its own device name. The ``storage_location_key`` names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.
* ``connected_device_uuid(storage_location_key)`` -- if a device is connected then return the device uuid (unique id), otherwise return the empty string. Each storage location on a device has a different uuid. The ``storage_location_key`` location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI. * ``connected_device_uuid(storage_location_key)`` -- if a device is connected then return the device uuid (unique id), otherwise return the empty string. Each storage location on a device has a different uuid. The ``storage_location_key`` location names are ``'main'``, ``'carda'`` and ``'cardb'``. This function works only in the GUI.

View File

@ -228,6 +228,13 @@ class TemplateHighlighter(QSyntaxHighlighter):
self.generate_paren_positions = False self.generate_paren_positions = False
translate_table = str.maketrans({
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\\': '\\\\',
})
class TemplateDialog(QDialog, Ui_TemplateDialog): class TemplateDialog(QDialog, Ui_TemplateDialog):
def __init__(self, parent, text, mi=None, fm=None, color_field=None, def __init__(self, parent, text, mi=None, fm=None, color_field=None,
@ -665,10 +672,9 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
v = SafeFormat().safe_format(txt, mi, _('EXCEPTION: '), v = SafeFormat().safe_format(txt, mi, _('EXCEPTION: '),
mi, global_vars=self.global_vars, mi, global_vars=self.global_vars,
template_functions=self.all_functions, template_functions=self.all_functions,
strip_results=False,
break_reporter=self.break_reporter if r == break_on_mi else None) break_reporter=self.break_reporter if r == break_on_mi else None)
w = tv.cellWidget(r, 1) w = tv.cellWidget(r, 1)
w.setText(v.replace('\n', '\\n')) w.setText(v.translate(translate_table))
w.setCursorPosition(0) w.setCursorPosition(0)
def text_cursor_changed(self): def text_cursor_changed(self):
@ -759,7 +765,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
class BreakReporterItem(QTableWidgetItem): class BreakReporterItem(QTableWidgetItem):
def __init__(self, txt): def __init__(self, txt):
super().__init__(txt.replace('\n', '\\n') if txt else txt) super().__init__(txt.translate(translate_table) if txt else txt)
self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsEditable|Qt.ItemFlag.ItemIsSelectable)) self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsEditable|Qt.ItemFlag.ItemIsSelectable))

View File

@ -45,6 +45,7 @@ class Node(object):
NODE_BREAK = 22 NODE_BREAK = 22
NODE_CONTINUE = 23 NODE_CONTINUE = 23
NODE_RETURN = 24 NODE_RETURN = 24
NODE_CHARACTER = 25
def __init__(self, line_number, name): def __init__(self, line_number, name):
self.my_line_number = line_number self.my_line_number = line_number
@ -247,6 +248,13 @@ class PrintNode(Node):
self.arguments = arguments self.arguments = arguments
class CharacterNode(Node):
def __init__(self, line_number, expression):
Node.__init__(self, line_number, 'character')
self.node_type = self.NODE_CHARACTER
self.expression = expression
class _Parser(object): class _Parser(object):
LEX_OP = 1 LEX_OP = 1
LEX_ID = 2 LEX_ID = 2
@ -332,6 +340,13 @@ class _Parser(object):
except: except:
return False return False
def token_is_keyword(self):
self.check_eol()
try:
return self.prog[self.lex_pos][0] == self.LEX_KEYWORD
except:
return False
def token_is_constant(self): def token_is_constant(self):
self.check_eol() self.check_eol()
try: try:
@ -358,7 +373,7 @@ class _Parser(object):
self.lex_pos = 0 self.lex_pos = 0
self.parent = parent self.parent = parent
self.funcs = funcs self.funcs = funcs
self.func_names = frozenset(set(self.funcs.keys()) | set(('newline',))) self.func_names = frozenset(set(self.funcs.keys()))
self.prog = prog[0] self.prog = prog[0]
self.prog_len = len(self.prog) self.prog_len = len(self.prog)
if prog[1] != '': if prog[1] != '':
@ -516,8 +531,6 @@ class _Parser(object):
# {inlined_function_name: tuple(constraint on number of length, node builder) } # {inlined_function_name: tuple(constraint on number of length, node builder) }
inlined_function_nodes = { inlined_function_nodes = {
'newline': (lambda args: len(args) == 0,
lambda ln, _: ConstantNode(ln, '\n')),
'field': (lambda args: len(args) == 1, 'field': (lambda args: len(args) == 1,
lambda ln, args: FieldNode(ln, args[0])), lambda ln, args: FieldNode(ln, args[0])),
'raw_field': (lambda args: len(args) == 1, 'raw_field': (lambda args: len(args) == 1,
@ -530,6 +543,8 @@ class _Parser(object):
lambda ln, args: AssignNode(ln, args[0].name, args[1])), lambda ln, args: AssignNode(ln, args[0].name, args[1])),
'contains': (lambda args: len(args) == 4, 'contains': (lambda args: len(args) == 4,
lambda ln, args: ContainsNode(ln, args)), lambda ln, args: ContainsNode(ln, args)),
'character': (lambda args: len(args) == 1,
lambda ln, args: CharacterNode(ln, args[0])),
'print': (lambda _: True, 'print': (lambda _: True,
lambda ln, args: PrintNode(ln, args)), lambda ln, args: PrintNode(ln, args)),
} }
@ -544,13 +559,14 @@ class _Parser(object):
return rv return rv
# Check if we have a keyword-type expression # Check if we have a keyword-type expression
t = self.token_text() if self.token_is_keyword():
kw_tuple = self.keyword_nodes.get(t, None) t = self.token_text()
if kw_tuple: kw_tuple = self.keyword_nodes.get(t, None)
# These are keywords, so there can't be ambiguity between these, ids, if kw_tuple:
# and functions. # These are keywords, so there can't be ambiguity between these,
kw_tuple[0](self) # ids, and functions.
return kw_tuple[1](self) kw_tuple[0](self)
return kw_tuple[1](self)
# Not a keyword. Check if we have an id reference or a function call # Not a keyword. Check if we have an id reference or a function call
if self.token_is_id(): if self.token_is_id():
@ -1053,6 +1069,20 @@ class _Interpreter(object):
self.error(_("Error during operator evaluation: " self.error(_("Error during operator evaluation: "
"operator '{0}'").format(prog.operator), prog.line_number) "operator '{0}'").format(prog.operator), prog.line_number)
characters = {
'return': '\r',
'newline': '\n',
'tab': '\t',
'backslash': '\\',
}
def do_node_character(self, prog):
key = self.expr(prog.expression)
ret = self.characters.get(key, None)
if ret is None:
self.error(_("Function {0}: invalid character name '{1}")
.format('character', key), prog.line_number)
return ret
def do_node_print(self, prog): def do_node_print(self, prog):
res = [] res = []
for arg in prog.arguments: for arg in prog.arguments:
@ -1085,6 +1115,7 @@ class _Interpreter(object):
Node.NODE_BREAK: do_node_break, Node.NODE_BREAK: do_node_break,
Node.NODE_CONTINUE: do_node_continue, Node.NODE_CONTINUE: do_node_continue,
Node.NODE_RETURN: do_node_return, Node.NODE_RETURN: do_node_return,
Node.NODE_CHARACTER: do_node_character,
} }
def expr(self, prog): def expr(self, prog):
@ -1290,8 +1321,10 @@ class TemplateFormatter(string.Formatter):
self.column_name, global_vars, break_reporter) self.column_name, global_vars, break_reporter)
else: else:
ans = self.vformat(fmt, args, kwargs) ans = self.vformat(fmt, args, kwargs)
if self.strip_results:
ans = self.compress_spaces.sub(' ', ans)
if self.strip_results: if self.strip_results:
return self.compress_spaces.sub(' ', ans).strip() ans = ans.strip(' ')
return ans return ans
# ######### a formatter that throws exceptions ############ # ######### a formatter that throws exceptions ############

View File

@ -2022,11 +2022,26 @@ class BuiltinFieldExists(BuiltinFormatterFunction):
return '' return ''
class BuiltinCharacter(BuiltinFormatterFunction):
name = 'character'
arg_count = 1
category = 'String manipulation'
__doc__ = doc = _('character(character_name) -- returns the '
'character named by character_name. For example, '
"character('newline') returns a newline character ('\n'). "
"The supported character names are 'newline', 'return', "
"'tab', and 'backslash'.")
def evaluate(self, formatter, kwargs, mi, locals, character_name):
# The globals function is implemented in-line in the formatter
raise NotImplementedError()
_formatter_builtins = [ _formatter_builtins = [
BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(), BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(),
BuiltinAssign(), BuiltinAssign(),
BuiltinAuthorLinks(), BuiltinAuthorSorts(), BuiltinBooksize(), BuiltinAuthorLinks(), BuiltinAuthorSorts(), BuiltinBooksize(),
BuiltinCapitalize(), BuiltinCheckYesNo(), BuiltinCeiling(), BuiltinCapitalize(), BuiltinCharacter(), BuiltinCheckYesNo(), BuiltinCeiling(),
BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(), BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(),
BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(),
BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(),