Formatter changes:

1) Fix a regression that broke specifying a separator for a 'for' statement
2) When parsing, use a table lookup instead of a series of ifs
3) Add a function 'newline()' that returns a newline. Intended to be used with strcat().
4) Better comments
This commit is contained in:
Charles Haley 2021-04-21 14:11:07 +01:00
parent 37a13064eb
commit cc022c6eb9
2 changed files with 64 additions and 45 deletions

View File

@ -665,9 +665,10 @@ 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) w.setText(v.replace('\n', '\\n'))
w.setCursorPosition(0) w.setCursorPosition(0)
def text_cursor_changed(self): def text_cursor_changed(self):
@ -758,7 +759,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
class BreakReporterItem(QTableWidgetItem): class BreakReporterItem(QTableWidgetItem):
def __init__(self, txt): def __init__(self, txt):
super().__init__(txt) super().__init__(txt.replace('\n', '\\n') 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

@ -332,14 +332,6 @@ class _Parser(object):
except: except:
return False return False
def token_is_separator(self):
self.check_eol()
try:
token = self.prog[self.lex_pos]
return token[1] == 'separator' and token[0] == self.LEX_ID
except:
return False
def token_is_constant(self): def token_is_constant(self):
self.check_eol() self.check_eol()
try: try:
@ -366,7 +358,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())) self.func_names = frozenset(set(self.funcs.keys()) | set(('newline',)))
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] != '':
@ -424,7 +416,7 @@ class _Parser(object):
"found '{2}'").format('for', 'in', self.token_text())) "found '{2}'").format('for', 'in', self.token_text()))
self.consume() self.consume()
list_expr = self.top_expr() list_expr = self.top_expr()
if self.token_is_separator(): if self.token_is('separator'):
self.consume() self.consume()
separator = self.expr() separator = self.expr()
else: else:
@ -513,6 +505,34 @@ class _Parser(object):
self.funcs[name].cached_parse_tree = subprog self.funcs[name].cached_parse_tree = subprog
return CallNode(self.line_number, name, subprog, arguments) return CallNode(self.line_number, name, subprog, arguments)
# {keyword: tuple(preprocessor, node builder) }
keyword_nodes = {
'if': (lambda self:None, if_expression),
'for': (lambda self:None, for_expression),
'break': (lambda self: self.consume(), lambda self: BreakNode(self.line_number)),
'continue': (lambda self: self.consume(), lambda self: ContinueNode(self.line_number)),
'return': (lambda self: self.consume(), lambda self: ReturnNode(self.line_number, self.expr())),
}
# {inlined_function_name: tuple(constraint on number of length, node builder) }
inlined_function_nodes = {
'newline': (lambda args: len(args) == 0,
lambda ln, _: ConstantNode(ln, '\n')),
'field': (lambda args: len(args) == 1,
lambda ln, args: FieldNode(ln, args[0])),
'raw_field': (lambda args: len(args) == 1,
lambda ln, args: RawFieldNode(ln, *args)),
'test': (lambda args: len(args) == 1,
lambda ln, args: IfNode(ln, args[0], (args[1],), (args[2],))),
'first_non_empty': (lambda args: len(args) == 1,
lambda ln, args: FirstNonEmptyNode(ln, args)),
'assign': (lambda args: len(args) == 2 and args[0].node_type == Node.NODE_RVALUE,
lambda ln, args: AssignNode(ln, args[0].name, args[1])),
'contains': (lambda args: len(args) == 4,
lambda ln, args: ContainsNode(ln, args)),
'print': (lambda _: True,
lambda ln, args: PrintNode(ln, args)),
}
def expr(self): def expr(self):
if self.token_op_is('('): if self.token_op_is('('):
self.consume() self.consume()
@ -521,29 +541,29 @@ class _Parser(object):
self.error(_("Expected '{0}', found '{1}'").format(')', self.token_text())) self.error(_("Expected '{0}', found '{1}'").format(')', self.token_text()))
self.consume() self.consume()
return rv return rv
if self.token_is('if'):
return self.if_expression() # Check if we have a keyword-type expression
if self.token_is('for'): t = self.token_text()
return self.for_expression() kw_tuple = self.keyword_nodes.get(t, None)
if self.token_is('break'): if kw_tuple:
self.consume() # These are keywords, so there can't be ambiguity between these, ids,
return BreakNode(self.line_number) # and functions.
if self.token_is('continue'): kw_tuple[0](self)
self.consume() return kw_tuple[1](self)
return ContinueNode(self.line_number)
if self.token_is('return'): # Not a keyword. Check if we have an id reference or a function call
self.consume()
return ReturnNode(self.line_number, self.expr())
if self.token_is_id(): if self.token_is_id():
# We have an identifier. Check if it is a shorthand field reference
line_number = self.line_number line_number = self.line_number
id_ = self.token() id_ = self.token()
# We have an identifier. Check if it is a field reference
if len(id_) > 1 and id_[0] == '$': if len(id_) > 1 and id_[0] == '$':
if id_[1] == '$': if id_[1] == '$':
return RawFieldNode(line_number, ConstantNode(self.line_number, id_[2:])) return RawFieldNode(line_number, ConstantNode(self.line_number, id_[2:]))
return FieldNode(line_number, ConstantNode(self.line_number, id_[1:])) return FieldNode(line_number, ConstantNode(self.line_number, id_[1:]))
# Determine if it is a function
# Do we have a function call?
if not self.token_op_is('('): if not self.token_op_is('('):
# Nope. We must have an lvalue (identifier) or an assignment
if self.token_op_is('='): if self.token_op_is('='):
# classic assignment statement # classic assignment statement
self.consume() self.consume()
@ -556,28 +576,26 @@ class _Parser(object):
id_ = id_.strip() id_ = id_.strip()
if id_ not in self.func_names: if id_ not in self.func_names:
self.error(_('Unknown function {0}').format(id_)) self.error(_('Unknown function {0}').format(id_))
# Eat the paren
# Eat the opening paren, parse the argument list, then eat the closing paren
self.consume() self.consume()
arguments = list() arguments = list()
while not self.token_op_is(')'): while not self.token_op_is(')'):
# evaluate the expression (recursive call) # parse an argument expression (recursive call)
arguments.append(self.expression_list()) arguments.append(self.expression_list())
if not self.token_op_is(','): if not self.token_op_is(','):
break break
self.consume() self.consume()
if self.token() != ')': t = self.token()
if t != ')':
self.error(_("Expected a '{0}' for function call, " self.error(_("Expected a '{0}' for function call, "
"found '{1}'").format(')', self.token_text())) "found '{1}'").format(')', t))
if id_ == 'field' and len(arguments) == 1:
return FieldNode(line_number, arguments[0]) # Check for an inlined function
if id_ == 'raw_field' and (len(arguments) in (1, 2)): function_tuple = self.inlined_function_nodes.get(id_, None)
return RawFieldNode(line_number, *arguments) if function_tuple and function_tuple[0](arguments):
if id_ == 'test' and len(arguments) == 3: return function_tuple[1](line_number, arguments)
return IfNode(line_number, arguments[0], (arguments[1],), (arguments[2],)) # More complicated special cases
if id_ == 'first_non_empty' and len(arguments) > 0:
return FirstNonEmptyNode(line_number, arguments)
if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE):
return AssignNode(line_number, arguments[0].name, arguments[1])
if id_ == 'arguments' or id_ == 'globals' or id_ == 'set_globals': if id_ == 'arguments' or id_ == 'globals' or id_ == 'set_globals':
new_args = [] new_args = []
for arg_list in arguments: for arg_list in arguments:
@ -593,12 +611,11 @@ class _Parser(object):
if id_ == 'set_globals': if id_ == 'set_globals':
return SetGlobalsNode(line_number, new_args) return SetGlobalsNode(line_number, new_args)
return GlobalsNode(line_number, new_args) return GlobalsNode(line_number, new_args)
if id_ == 'contains' and len(arguments) == 4: # Check for calling a stored template
return ContainsNode(line_number, arguments)
if id_ == 'print':
return PrintNode(line_number, arguments)
if id_ in self.func_names and not self.funcs[id_].is_python: if id_ in self.func_names and not self.funcs[id_].is_python:
return self.call_expression(id_, arguments) return self.call_expression(id_, arguments)
# We must have a reference to a formatter function. Check if
# the right number of arguments were supplied
cls = self.funcs[id_] cls = self.funcs[id_]
if cls.arg_count != -1 and len(arguments) != cls.arg_count: if cls.arg_count != -1 and len(arguments) != cls.arg_count:
self.error(_('Incorrect number of arguments for function {0}').format(id_)) self.error(_('Incorrect number of arguments for function {0}').format(id_))
@ -607,6 +624,7 @@ class _Parser(object):
# String or number # String or number
return ConstantNode(self.line_number, self.token()) return ConstantNode(self.line_number, self.token())
else: else:
# Who knows what?
self.error(_("Expected an expression, found '{0}'").format(self.token_text())) self.error(_("Expected an expression, found '{0}'").format(self.token_text()))