1042 lines
39 KiB
Python
1042 lines
39 KiB
Python
"""Parse tokens from the lexer into nodes for the compiler."""
|
|
|
|
import typing
|
|
import typing as t
|
|
|
|
from . import nodes
|
|
from .exceptions import TemplateAssertionError
|
|
from .exceptions import TemplateSyntaxError
|
|
from .lexer import describe_token
|
|
from .lexer import describe_token_expr
|
|
|
|
if t.TYPE_CHECKING:
|
|
import typing_extensions as te
|
|
|
|
from .environment import Environment
|
|
|
|
_ImportInclude = t.TypeVar("_ImportInclude", nodes.Import, nodes.Include)
|
|
_MacroCall = t.TypeVar("_MacroCall", nodes.Macro, nodes.CallBlock)
|
|
|
|
_statement_keywords = frozenset(
|
|
[
|
|
"for",
|
|
"if",
|
|
"block",
|
|
"extends",
|
|
"print",
|
|
"macro",
|
|
"include",
|
|
"from",
|
|
"import",
|
|
"set",
|
|
"with",
|
|
"autoescape",
|
|
]
|
|
)
|
|
_compare_operators = frozenset(["eq", "ne", "lt", "lteq", "gt", "gteq"])
|
|
|
|
_math_nodes: t.Dict[str, t.Type[nodes.Expr]] = {
|
|
"add": nodes.Add,
|
|
"sub": nodes.Sub,
|
|
"mul": nodes.Mul,
|
|
"div": nodes.Div,
|
|
"floordiv": nodes.FloorDiv,
|
|
"mod": nodes.Mod,
|
|
}
|
|
|
|
|
|
class Parser:
|
|
"""This is the central parsing class Jinja uses. It's passed to
|
|
extensions and can be used to parse expressions or statements.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
environment: "Environment",
|
|
source: str,
|
|
name: t.Optional[str] = None,
|
|
filename: t.Optional[str] = None,
|
|
state: t.Optional[str] = None,
|
|
) -> None:
|
|
self.environment = environment
|
|
self.stream = environment._tokenize(source, name, filename, state)
|
|
self.name = name
|
|
self.filename = filename
|
|
self.closed = False
|
|
self.extensions: t.Dict[
|
|
str, t.Callable[["Parser"], t.Union[nodes.Node, t.List[nodes.Node]]]
|
|
] = {}
|
|
for extension in environment.iter_extensions():
|
|
for tag in extension.tags:
|
|
self.extensions[tag] = extension.parse
|
|
self._last_identifier = 0
|
|
self._tag_stack: t.List[str] = []
|
|
self._end_token_stack: t.List[t.Tuple[str, ...]] = []
|
|
|
|
def fail(
|
|
self,
|
|
msg: str,
|
|
lineno: t.Optional[int] = None,
|
|
exc: t.Type[TemplateSyntaxError] = TemplateSyntaxError,
|
|
) -> "te.NoReturn":
|
|
"""Convenience method that raises `exc` with the message, passed
|
|
line number or last line number as well as the current name and
|
|
filename.
|
|
"""
|
|
if lineno is None:
|
|
lineno = self.stream.current.lineno
|
|
raise exc(msg, lineno, self.name, self.filename)
|
|
|
|
def _fail_ut_eof(
|
|
self,
|
|
name: t.Optional[str],
|
|
end_token_stack: t.List[t.Tuple[str, ...]],
|
|
lineno: t.Optional[int],
|
|
) -> "te.NoReturn":
|
|
expected: t.Set[str] = set()
|
|
for exprs in end_token_stack:
|
|
expected.update(map(describe_token_expr, exprs))
|
|
if end_token_stack:
|
|
currently_looking: t.Optional[str] = " or ".join(
|
|
map(repr, map(describe_token_expr, end_token_stack[-1]))
|
|
)
|
|
else:
|
|
currently_looking = None
|
|
|
|
if name is None:
|
|
message = ["Unexpected end of template."]
|
|
else:
|
|
message = [f"Encountered unknown tag {name!r}."]
|
|
|
|
if currently_looking:
|
|
if name is not None and name in expected:
|
|
message.append(
|
|
"You probably made a nesting mistake. Jinja is expecting this tag,"
|
|
f" but currently looking for {currently_looking}."
|
|
)
|
|
else:
|
|
message.append(
|
|
f"Jinja was looking for the following tags: {currently_looking}."
|
|
)
|
|
|
|
if self._tag_stack:
|
|
message.append(
|
|
"The innermost block that needs to be closed is"
|
|
f" {self._tag_stack[-1]!r}."
|
|
)
|
|
|
|
self.fail(" ".join(message), lineno)
|
|
|
|
def fail_unknown_tag(
|
|
self, name: str, lineno: t.Optional[int] = None
|
|
) -> "te.NoReturn":
|
|
"""Called if the parser encounters an unknown tag. Tries to fail
|
|
with a human readable error message that could help to identify
|
|
the problem.
|
|
"""
|
|
self._fail_ut_eof(name, self._end_token_stack, lineno)
|
|
|
|
def fail_eof(
|
|
self,
|
|
end_tokens: t.Optional[t.Tuple[str, ...]] = None,
|
|
lineno: t.Optional[int] = None,
|
|
) -> "te.NoReturn":
|
|
"""Like fail_unknown_tag but for end of template situations."""
|
|
stack = list(self._end_token_stack)
|
|
if end_tokens is not None:
|
|
stack.append(end_tokens)
|
|
self._fail_ut_eof(None, stack, lineno)
|
|
|
|
def is_tuple_end(
|
|
self, extra_end_rules: t.Optional[t.Tuple[str, ...]] = None
|
|
) -> bool:
|
|
"""Are we at the end of a tuple?"""
|
|
if self.stream.current.type in ("variable_end", "block_end", "rparen"):
|
|
return True
|
|
elif extra_end_rules is not None:
|
|
return self.stream.current.test_any(extra_end_rules) # type: ignore
|
|
return False
|
|
|
|
def free_identifier(self, lineno: t.Optional[int] = None) -> nodes.InternalName:
|
|
"""Return a new free identifier as :class:`~jinja2.nodes.InternalName`."""
|
|
self._last_identifier += 1
|
|
rv = object.__new__(nodes.InternalName)
|
|
nodes.Node.__init__(rv, f"fi{self._last_identifier}", lineno=lineno)
|
|
return rv
|
|
|
|
def parse_statement(self) -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
|
"""Parse a single statement."""
|
|
token = self.stream.current
|
|
if token.type != "name":
|
|
self.fail("tag name expected", token.lineno)
|
|
self._tag_stack.append(token.value)
|
|
pop_tag = True
|
|
try:
|
|
if token.value in _statement_keywords:
|
|
f = getattr(self, f"parse_{self.stream.current.value}")
|
|
return f() # type: ignore
|
|
if token.value == "call":
|
|
return self.parse_call_block()
|
|
if token.value == "filter":
|
|
return self.parse_filter_block()
|
|
ext = self.extensions.get(token.value)
|
|
if ext is not None:
|
|
return ext(self)
|
|
|
|
# did not work out, remove the token we pushed by accident
|
|
# from the stack so that the unknown tag fail function can
|
|
# produce a proper error message.
|
|
self._tag_stack.pop()
|
|
pop_tag = False
|
|
self.fail_unknown_tag(token.value, token.lineno)
|
|
finally:
|
|
if pop_tag:
|
|
self._tag_stack.pop()
|
|
|
|
def parse_statements(
|
|
self, end_tokens: t.Tuple[str, ...], drop_needle: bool = False
|
|
) -> t.List[nodes.Node]:
|
|
"""Parse multiple statements into a list until one of the end tokens
|
|
is reached. This is used to parse the body of statements as it also
|
|
parses template data if appropriate. The parser checks first if the
|
|
current token is a colon and skips it if there is one. Then it checks
|
|
for the block end and parses until if one of the `end_tokens` is
|
|
reached. Per default the active token in the stream at the end of
|
|
the call is the matched end token. If this is not wanted `drop_needle`
|
|
can be set to `True` and the end token is removed.
|
|
"""
|
|
# the first token may be a colon for python compatibility
|
|
self.stream.skip_if("colon")
|
|
|
|
# in the future it would be possible to add whole code sections
|
|
# by adding some sort of end of statement token and parsing those here.
|
|
self.stream.expect("block_end")
|
|
result = self.subparse(end_tokens)
|
|
|
|
# we reached the end of the template too early, the subparser
|
|
# does not check for this, so we do that now
|
|
if self.stream.current.type == "eof":
|
|
self.fail_eof(end_tokens)
|
|
|
|
if drop_needle:
|
|
next(self.stream)
|
|
return result
|
|
|
|
def parse_set(self) -> t.Union[nodes.Assign, nodes.AssignBlock]:
|
|
"""Parse an assign statement."""
|
|
lineno = next(self.stream).lineno
|
|
target = self.parse_assign_target(with_namespace=True)
|
|
if self.stream.skip_if("assign"):
|
|
expr = self.parse_tuple()
|
|
return nodes.Assign(target, expr, lineno=lineno)
|
|
filter_node = self.parse_filter(None)
|
|
body = self.parse_statements(("name:endset",), drop_needle=True)
|
|
return nodes.AssignBlock(target, filter_node, body, lineno=lineno)
|
|
|
|
def parse_for(self) -> nodes.For:
|
|
"""Parse a for loop."""
|
|
lineno = self.stream.expect("name:for").lineno
|
|
target = self.parse_assign_target(extra_end_rules=("name:in",))
|
|
self.stream.expect("name:in")
|
|
iter = self.parse_tuple(
|
|
with_condexpr=False, extra_end_rules=("name:recursive",)
|
|
)
|
|
test = None
|
|
if self.stream.skip_if("name:if"):
|
|
test = self.parse_expression()
|
|
recursive = self.stream.skip_if("name:recursive")
|
|
body = self.parse_statements(("name:endfor", "name:else"))
|
|
if next(self.stream).value == "endfor":
|
|
else_ = []
|
|
else:
|
|
else_ = self.parse_statements(("name:endfor",), drop_needle=True)
|
|
return nodes.For(target, iter, body, else_, test, recursive, lineno=lineno)
|
|
|
|
def parse_if(self) -> nodes.If:
|
|
"""Parse an if construct."""
|
|
node = result = nodes.If(lineno=self.stream.expect("name:if").lineno)
|
|
while True:
|
|
node.test = self.parse_tuple(with_condexpr=False)
|
|
node.body = self.parse_statements(("name:elif", "name:else", "name:endif"))
|
|
node.elif_ = []
|
|
node.else_ = []
|
|
token = next(self.stream)
|
|
if token.test("name:elif"):
|
|
node = nodes.If(lineno=self.stream.current.lineno)
|
|
result.elif_.append(node)
|
|
continue
|
|
elif token.test("name:else"):
|
|
result.else_ = self.parse_statements(("name:endif",), drop_needle=True)
|
|
break
|
|
return result
|
|
|
|
def parse_with(self) -> nodes.With:
|
|
node = nodes.With(lineno=next(self.stream).lineno)
|
|
targets: t.List[nodes.Expr] = []
|
|
values: t.List[nodes.Expr] = []
|
|
while self.stream.current.type != "block_end":
|
|
if targets:
|
|
self.stream.expect("comma")
|
|
target = self.parse_assign_target()
|
|
target.set_ctx("param")
|
|
targets.append(target)
|
|
self.stream.expect("assign")
|
|
values.append(self.parse_expression())
|
|
node.targets = targets
|
|
node.values = values
|
|
node.body = self.parse_statements(("name:endwith",), drop_needle=True)
|
|
return node
|
|
|
|
def parse_autoescape(self) -> nodes.Scope:
|
|
node = nodes.ScopedEvalContextModifier(lineno=next(self.stream).lineno)
|
|
node.options = [nodes.Keyword("autoescape", self.parse_expression())]
|
|
node.body = self.parse_statements(("name:endautoescape",), drop_needle=True)
|
|
return nodes.Scope([node])
|
|
|
|
def parse_block(self) -> nodes.Block:
|
|
node = nodes.Block(lineno=next(self.stream).lineno)
|
|
node.name = self.stream.expect("name").value
|
|
node.scoped = self.stream.skip_if("name:scoped")
|
|
node.required = self.stream.skip_if("name:required")
|
|
|
|
# common problem people encounter when switching from django
|
|
# to jinja. we do not support hyphens in block names, so let's
|
|
# raise a nicer error message in that case.
|
|
if self.stream.current.type == "sub":
|
|
self.fail(
|
|
"Block names in Jinja have to be valid Python identifiers and may not"
|
|
" contain hyphens, use an underscore instead."
|
|
)
|
|
|
|
node.body = self.parse_statements(("name:endblock",), drop_needle=True)
|
|
|
|
# enforce that required blocks only contain whitespace or comments
|
|
# by asserting that the body, if not empty, is just TemplateData nodes
|
|
# with whitespace data
|
|
if node.required:
|
|
for body_node in node.body:
|
|
if not isinstance(body_node, nodes.Output) or any(
|
|
not isinstance(output_node, nodes.TemplateData)
|
|
or not output_node.data.isspace()
|
|
for output_node in body_node.nodes
|
|
):
|
|
self.fail("Required blocks can only contain comments or whitespace")
|
|
|
|
self.stream.skip_if("name:" + node.name)
|
|
return node
|
|
|
|
def parse_extends(self) -> nodes.Extends:
|
|
node = nodes.Extends(lineno=next(self.stream).lineno)
|
|
node.template = self.parse_expression()
|
|
return node
|
|
|
|
def parse_import_context(
|
|
self, node: _ImportInclude, default: bool
|
|
) -> _ImportInclude:
|
|
if self.stream.current.test_any(
|
|
"name:with", "name:without"
|
|
) and self.stream.look().test("name:context"):
|
|
node.with_context = next(self.stream).value == "with"
|
|
self.stream.skip()
|
|
else:
|
|
node.with_context = default
|
|
return node
|
|
|
|
def parse_include(self) -> nodes.Include:
|
|
node = nodes.Include(lineno=next(self.stream).lineno)
|
|
node.template = self.parse_expression()
|
|
if self.stream.current.test("name:ignore") and self.stream.look().test(
|
|
"name:missing"
|
|
):
|
|
node.ignore_missing = True
|
|
self.stream.skip(2)
|
|
else:
|
|
node.ignore_missing = False
|
|
return self.parse_import_context(node, True)
|
|
|
|
def parse_import(self) -> nodes.Import:
|
|
node = nodes.Import(lineno=next(self.stream).lineno)
|
|
node.template = self.parse_expression()
|
|
self.stream.expect("name:as")
|
|
node.target = self.parse_assign_target(name_only=True).name
|
|
return self.parse_import_context(node, False)
|
|
|
|
def parse_from(self) -> nodes.FromImport:
|
|
node = nodes.FromImport(lineno=next(self.stream).lineno)
|
|
node.template = self.parse_expression()
|
|
self.stream.expect("name:import")
|
|
node.names = []
|
|
|
|
def parse_context() -> bool:
|
|
if self.stream.current.value in {
|
|
"with",
|
|
"without",
|
|
} and self.stream.look().test("name:context"):
|
|
node.with_context = next(self.stream).value == "with"
|
|
self.stream.skip()
|
|
return True
|
|
return False
|
|
|
|
while True:
|
|
if node.names:
|
|
self.stream.expect("comma")
|
|
if self.stream.current.type == "name":
|
|
if parse_context():
|
|
break
|
|
target = self.parse_assign_target(name_only=True)
|
|
if target.name.startswith("_"):
|
|
self.fail(
|
|
"names starting with an underline can not be imported",
|
|
target.lineno,
|
|
exc=TemplateAssertionError,
|
|
)
|
|
if self.stream.skip_if("name:as"):
|
|
alias = self.parse_assign_target(name_only=True)
|
|
node.names.append((target.name, alias.name))
|
|
else:
|
|
node.names.append(target.name)
|
|
if parse_context() or self.stream.current.type != "comma":
|
|
break
|
|
else:
|
|
self.stream.expect("name")
|
|
if not hasattr(node, "with_context"):
|
|
node.with_context = False
|
|
return node
|
|
|
|
def parse_signature(self, node: _MacroCall) -> None:
|
|
args = node.args = []
|
|
defaults = node.defaults = []
|
|
self.stream.expect("lparen")
|
|
while self.stream.current.type != "rparen":
|
|
if args:
|
|
self.stream.expect("comma")
|
|
arg = self.parse_assign_target(name_only=True)
|
|
arg.set_ctx("param")
|
|
if self.stream.skip_if("assign"):
|
|
defaults.append(self.parse_expression())
|
|
elif defaults:
|
|
self.fail("non-default argument follows default argument")
|
|
args.append(arg)
|
|
self.stream.expect("rparen")
|
|
|
|
def parse_call_block(self) -> nodes.CallBlock:
|
|
node = nodes.CallBlock(lineno=next(self.stream).lineno)
|
|
if self.stream.current.type == "lparen":
|
|
self.parse_signature(node)
|
|
else:
|
|
node.args = []
|
|
node.defaults = []
|
|
|
|
call_node = self.parse_expression()
|
|
if not isinstance(call_node, nodes.Call):
|
|
self.fail("expected call", node.lineno)
|
|
node.call = call_node
|
|
node.body = self.parse_statements(("name:endcall",), drop_needle=True)
|
|
return node
|
|
|
|
def parse_filter_block(self) -> nodes.FilterBlock:
|
|
node = nodes.FilterBlock(lineno=next(self.stream).lineno)
|
|
node.filter = self.parse_filter(None, start_inline=True) # type: ignore
|
|
node.body = self.parse_statements(("name:endfilter",), drop_needle=True)
|
|
return node
|
|
|
|
def parse_macro(self) -> nodes.Macro:
|
|
node = nodes.Macro(lineno=next(self.stream).lineno)
|
|
node.name = self.parse_assign_target(name_only=True).name
|
|
self.parse_signature(node)
|
|
node.body = self.parse_statements(("name:endmacro",), drop_needle=True)
|
|
return node
|
|
|
|
def parse_print(self) -> nodes.Output:
|
|
node = nodes.Output(lineno=next(self.stream).lineno)
|
|
node.nodes = []
|
|
while self.stream.current.type != "block_end":
|
|
if node.nodes:
|
|
self.stream.expect("comma")
|
|
node.nodes.append(self.parse_expression())
|
|
return node
|
|
|
|
@typing.overload
|
|
def parse_assign_target(
|
|
self, with_tuple: bool = ..., name_only: "te.Literal[True]" = ...
|
|
) -> nodes.Name: ...
|
|
|
|
@typing.overload
|
|
def parse_assign_target(
|
|
self,
|
|
with_tuple: bool = True,
|
|
name_only: bool = False,
|
|
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
|
with_namespace: bool = False,
|
|
) -> t.Union[nodes.NSRef, nodes.Name, nodes.Tuple]: ...
|
|
|
|
def parse_assign_target(
|
|
self,
|
|
with_tuple: bool = True,
|
|
name_only: bool = False,
|
|
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
|
with_namespace: bool = False,
|
|
) -> t.Union[nodes.NSRef, nodes.Name, nodes.Tuple]:
|
|
"""Parse an assignment target. As Jinja allows assignments to
|
|
tuples, this function can parse all allowed assignment targets. Per
|
|
default assignments to tuples are parsed, that can be disable however
|
|
by setting `with_tuple` to `False`. If only assignments to names are
|
|
wanted `name_only` can be set to `True`. The `extra_end_rules`
|
|
parameter is forwarded to the tuple parsing function. If
|
|
`with_namespace` is enabled, a namespace assignment may be parsed.
|
|
"""
|
|
target: nodes.Expr
|
|
|
|
if with_namespace and self.stream.look().type == "dot":
|
|
token = self.stream.expect("name")
|
|
next(self.stream) # dot
|
|
attr = self.stream.expect("name")
|
|
target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
|
|
elif name_only:
|
|
token = self.stream.expect("name")
|
|
target = nodes.Name(token.value, "store", lineno=token.lineno)
|
|
else:
|
|
if with_tuple:
|
|
target = self.parse_tuple(
|
|
simplified=True, extra_end_rules=extra_end_rules
|
|
)
|
|
else:
|
|
target = self.parse_primary()
|
|
|
|
target.set_ctx("store")
|
|
|
|
if not target.can_assign():
|
|
self.fail(
|
|
f"can't assign to {type(target).__name__.lower()!r}", target.lineno
|
|
)
|
|
|
|
return target # type: ignore
|
|
|
|
def parse_expression(self, with_condexpr: bool = True) -> nodes.Expr:
|
|
"""Parse an expression. Per default all expressions are parsed, if
|
|
the optional `with_condexpr` parameter is set to `False` conditional
|
|
expressions are not parsed.
|
|
"""
|
|
if with_condexpr:
|
|
return self.parse_condexpr()
|
|
return self.parse_or()
|
|
|
|
def parse_condexpr(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
expr1 = self.parse_or()
|
|
expr3: t.Optional[nodes.Expr]
|
|
|
|
while self.stream.skip_if("name:if"):
|
|
expr2 = self.parse_or()
|
|
if self.stream.skip_if("name:else"):
|
|
expr3 = self.parse_condexpr()
|
|
else:
|
|
expr3 = None
|
|
expr1 = nodes.CondExpr(expr2, expr1, expr3, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return expr1
|
|
|
|
def parse_or(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
left = self.parse_and()
|
|
while self.stream.skip_if("name:or"):
|
|
right = self.parse_and()
|
|
left = nodes.Or(left, right, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return left
|
|
|
|
def parse_and(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
left = self.parse_not()
|
|
while self.stream.skip_if("name:and"):
|
|
right = self.parse_not()
|
|
left = nodes.And(left, right, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return left
|
|
|
|
def parse_not(self) -> nodes.Expr:
|
|
if self.stream.current.test("name:not"):
|
|
lineno = next(self.stream).lineno
|
|
return nodes.Not(self.parse_not(), lineno=lineno)
|
|
return self.parse_compare()
|
|
|
|
def parse_compare(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
expr = self.parse_math1()
|
|
ops = []
|
|
while True:
|
|
token_type = self.stream.current.type
|
|
if token_type in _compare_operators:
|
|
next(self.stream)
|
|
ops.append(nodes.Operand(token_type, self.parse_math1()))
|
|
elif self.stream.skip_if("name:in"):
|
|
ops.append(nodes.Operand("in", self.parse_math1()))
|
|
elif self.stream.current.test("name:not") and self.stream.look().test(
|
|
"name:in"
|
|
):
|
|
self.stream.skip(2)
|
|
ops.append(nodes.Operand("notin", self.parse_math1()))
|
|
else:
|
|
break
|
|
lineno = self.stream.current.lineno
|
|
if not ops:
|
|
return expr
|
|
return nodes.Compare(expr, ops, lineno=lineno)
|
|
|
|
def parse_math1(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
left = self.parse_concat()
|
|
while self.stream.current.type in ("add", "sub"):
|
|
cls = _math_nodes[self.stream.current.type]
|
|
next(self.stream)
|
|
right = self.parse_concat()
|
|
left = cls(left, right, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return left
|
|
|
|
def parse_concat(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
args = [self.parse_math2()]
|
|
while self.stream.current.type == "tilde":
|
|
next(self.stream)
|
|
args.append(self.parse_math2())
|
|
if len(args) == 1:
|
|
return args[0]
|
|
return nodes.Concat(args, lineno=lineno)
|
|
|
|
def parse_math2(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
left = self.parse_pow()
|
|
while self.stream.current.type in ("mul", "div", "floordiv", "mod"):
|
|
cls = _math_nodes[self.stream.current.type]
|
|
next(self.stream)
|
|
right = self.parse_pow()
|
|
left = cls(left, right, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return left
|
|
|
|
def parse_pow(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
left = self.parse_unary()
|
|
while self.stream.current.type == "pow":
|
|
next(self.stream)
|
|
right = self.parse_unary()
|
|
left = nodes.Pow(left, right, lineno=lineno)
|
|
lineno = self.stream.current.lineno
|
|
return left
|
|
|
|
def parse_unary(self, with_filter: bool = True) -> nodes.Expr:
|
|
token_type = self.stream.current.type
|
|
lineno = self.stream.current.lineno
|
|
node: nodes.Expr
|
|
|
|
if token_type == "sub":
|
|
next(self.stream)
|
|
node = nodes.Neg(self.parse_unary(False), lineno=lineno)
|
|
elif token_type == "add":
|
|
next(self.stream)
|
|
node = nodes.Pos(self.parse_unary(False), lineno=lineno)
|
|
else:
|
|
node = self.parse_primary()
|
|
node = self.parse_postfix(node)
|
|
if with_filter:
|
|
node = self.parse_filter_expr(node)
|
|
return node
|
|
|
|
def parse_primary(self) -> nodes.Expr:
|
|
token = self.stream.current
|
|
node: nodes.Expr
|
|
if token.type == "name":
|
|
if token.value in ("true", "false", "True", "False"):
|
|
node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
|
|
elif token.value in ("none", "None"):
|
|
node = nodes.Const(None, lineno=token.lineno)
|
|
else:
|
|
node = nodes.Name(token.value, "load", lineno=token.lineno)
|
|
next(self.stream)
|
|
elif token.type == "string":
|
|
next(self.stream)
|
|
buf = [token.value]
|
|
lineno = token.lineno
|
|
while self.stream.current.type == "string":
|
|
buf.append(self.stream.current.value)
|
|
next(self.stream)
|
|
node = nodes.Const("".join(buf), lineno=lineno)
|
|
elif token.type in ("integer", "float"):
|
|
next(self.stream)
|
|
node = nodes.Const(token.value, lineno=token.lineno)
|
|
elif token.type == "lparen":
|
|
next(self.stream)
|
|
node = self.parse_tuple(explicit_parentheses=True)
|
|
self.stream.expect("rparen")
|
|
elif token.type == "lbracket":
|
|
node = self.parse_list()
|
|
elif token.type == "lbrace":
|
|
node = self.parse_dict()
|
|
else:
|
|
self.fail(f"unexpected {describe_token(token)!r}", token.lineno)
|
|
return node
|
|
|
|
def parse_tuple(
|
|
self,
|
|
simplified: bool = False,
|
|
with_condexpr: bool = True,
|
|
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
|
explicit_parentheses: bool = False,
|
|
) -> t.Union[nodes.Tuple, nodes.Expr]:
|
|
"""Works like `parse_expression` but if multiple expressions are
|
|
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
|
|
This method could also return a regular expression instead of a tuple
|
|
if no commas where found.
|
|
|
|
The default parsing mode is a full tuple. If `simplified` is `True`
|
|
only names and literals are parsed. The `no_condexpr` parameter is
|
|
forwarded to :meth:`parse_expression`.
|
|
|
|
Because tuples do not require delimiters and may end in a bogus comma
|
|
an extra hint is needed that marks the end of a tuple. For example
|
|
for loops support tuples between `for` and `in`. In that case the
|
|
`extra_end_rules` is set to ``['name:in']``.
|
|
|
|
`explicit_parentheses` is true if the parsing was triggered by an
|
|
expression in parentheses. This is used to figure out if an empty
|
|
tuple is a valid expression or not.
|
|
"""
|
|
lineno = self.stream.current.lineno
|
|
if simplified:
|
|
parse = self.parse_primary
|
|
elif with_condexpr:
|
|
parse = self.parse_expression
|
|
else:
|
|
|
|
def parse() -> nodes.Expr:
|
|
return self.parse_expression(with_condexpr=False)
|
|
|
|
args: t.List[nodes.Expr] = []
|
|
is_tuple = False
|
|
|
|
while True:
|
|
if args:
|
|
self.stream.expect("comma")
|
|
if self.is_tuple_end(extra_end_rules):
|
|
break
|
|
args.append(parse())
|
|
if self.stream.current.type == "comma":
|
|
is_tuple = True
|
|
else:
|
|
break
|
|
lineno = self.stream.current.lineno
|
|
|
|
if not is_tuple:
|
|
if args:
|
|
return args[0]
|
|
|
|
# if we don't have explicit parentheses, an empty tuple is
|
|
# not a valid expression. This would mean nothing (literally
|
|
# nothing) in the spot of an expression would be an empty
|
|
# tuple.
|
|
if not explicit_parentheses:
|
|
self.fail(
|
|
"Expected an expression,"
|
|
f" got {describe_token(self.stream.current)!r}"
|
|
)
|
|
|
|
return nodes.Tuple(args, "load", lineno=lineno)
|
|
|
|
def parse_list(self) -> nodes.List:
|
|
token = self.stream.expect("lbracket")
|
|
items: t.List[nodes.Expr] = []
|
|
while self.stream.current.type != "rbracket":
|
|
if items:
|
|
self.stream.expect("comma")
|
|
if self.stream.current.type == "rbracket":
|
|
break
|
|
items.append(self.parse_expression())
|
|
self.stream.expect("rbracket")
|
|
return nodes.List(items, lineno=token.lineno)
|
|
|
|
def parse_dict(self) -> nodes.Dict:
|
|
token = self.stream.expect("lbrace")
|
|
items: t.List[nodes.Pair] = []
|
|
while self.stream.current.type != "rbrace":
|
|
if items:
|
|
self.stream.expect("comma")
|
|
if self.stream.current.type == "rbrace":
|
|
break
|
|
key = self.parse_expression()
|
|
self.stream.expect("colon")
|
|
value = self.parse_expression()
|
|
items.append(nodes.Pair(key, value, lineno=key.lineno))
|
|
self.stream.expect("rbrace")
|
|
return nodes.Dict(items, lineno=token.lineno)
|
|
|
|
def parse_postfix(self, node: nodes.Expr) -> nodes.Expr:
|
|
while True:
|
|
token_type = self.stream.current.type
|
|
if token_type == "dot" or token_type == "lbracket":
|
|
node = self.parse_subscript(node)
|
|
# calls are valid both after postfix expressions (getattr
|
|
# and getitem) as well as filters and tests
|
|
elif token_type == "lparen":
|
|
node = self.parse_call(node)
|
|
else:
|
|
break
|
|
return node
|
|
|
|
def parse_filter_expr(self, node: nodes.Expr) -> nodes.Expr:
|
|
while True:
|
|
token_type = self.stream.current.type
|
|
if token_type == "pipe":
|
|
node = self.parse_filter(node) # type: ignore
|
|
elif token_type == "name" and self.stream.current.value == "is":
|
|
node = self.parse_test(node)
|
|
# calls are valid both after postfix expressions (getattr
|
|
# and getitem) as well as filters and tests
|
|
elif token_type == "lparen":
|
|
node = self.parse_call(node)
|
|
else:
|
|
break
|
|
return node
|
|
|
|
def parse_subscript(
|
|
self, node: nodes.Expr
|
|
) -> t.Union[nodes.Getattr, nodes.Getitem]:
|
|
token = next(self.stream)
|
|
arg: nodes.Expr
|
|
|
|
if token.type == "dot":
|
|
attr_token = self.stream.current
|
|
next(self.stream)
|
|
if attr_token.type == "name":
|
|
return nodes.Getattr(
|
|
node, attr_token.value, "load", lineno=token.lineno
|
|
)
|
|
elif attr_token.type != "integer":
|
|
self.fail("expected name or number", attr_token.lineno)
|
|
arg = nodes.Const(attr_token.value, lineno=attr_token.lineno)
|
|
return nodes.Getitem(node, arg, "load", lineno=token.lineno)
|
|
if token.type == "lbracket":
|
|
args: t.List[nodes.Expr] = []
|
|
while self.stream.current.type != "rbracket":
|
|
if args:
|
|
self.stream.expect("comma")
|
|
args.append(self.parse_subscribed())
|
|
self.stream.expect("rbracket")
|
|
if len(args) == 1:
|
|
arg = args[0]
|
|
else:
|
|
arg = nodes.Tuple(args, "load", lineno=token.lineno)
|
|
return nodes.Getitem(node, arg, "load", lineno=token.lineno)
|
|
self.fail("expected subscript expression", token.lineno)
|
|
|
|
def parse_subscribed(self) -> nodes.Expr:
|
|
lineno = self.stream.current.lineno
|
|
args: t.List[t.Optional[nodes.Expr]]
|
|
|
|
if self.stream.current.type == "colon":
|
|
next(self.stream)
|
|
args = [None]
|
|
else:
|
|
node = self.parse_expression()
|
|
if self.stream.current.type != "colon":
|
|
return node
|
|
next(self.stream)
|
|
args = [node]
|
|
|
|
if self.stream.current.type == "colon":
|
|
args.append(None)
|
|
elif self.stream.current.type not in ("rbracket", "comma"):
|
|
args.append(self.parse_expression())
|
|
else:
|
|
args.append(None)
|
|
|
|
if self.stream.current.type == "colon":
|
|
next(self.stream)
|
|
if self.stream.current.type not in ("rbracket", "comma"):
|
|
args.append(self.parse_expression())
|
|
else:
|
|
args.append(None)
|
|
else:
|
|
args.append(None)
|
|
|
|
return nodes.Slice(lineno=lineno, *args) # noqa: B026
|
|
|
|
def parse_call_args(
|
|
self,
|
|
) -> t.Tuple[
|
|
t.List[nodes.Expr],
|
|
t.List[nodes.Keyword],
|
|
t.Optional[nodes.Expr],
|
|
t.Optional[nodes.Expr],
|
|
]:
|
|
token = self.stream.expect("lparen")
|
|
args = []
|
|
kwargs = []
|
|
dyn_args = None
|
|
dyn_kwargs = None
|
|
require_comma = False
|
|
|
|
def ensure(expr: bool) -> None:
|
|
if not expr:
|
|
self.fail("invalid syntax for function call expression", token.lineno)
|
|
|
|
while self.stream.current.type != "rparen":
|
|
if require_comma:
|
|
self.stream.expect("comma")
|
|
|
|
# support for trailing comma
|
|
if self.stream.current.type == "rparen":
|
|
break
|
|
|
|
if self.stream.current.type == "mul":
|
|
ensure(dyn_args is None and dyn_kwargs is None)
|
|
next(self.stream)
|
|
dyn_args = self.parse_expression()
|
|
elif self.stream.current.type == "pow":
|
|
ensure(dyn_kwargs is None)
|
|
next(self.stream)
|
|
dyn_kwargs = self.parse_expression()
|
|
else:
|
|
if (
|
|
self.stream.current.type == "name"
|
|
and self.stream.look().type == "assign"
|
|
):
|
|
# Parsing a kwarg
|
|
ensure(dyn_kwargs is None)
|
|
key = self.stream.current.value
|
|
self.stream.skip(2)
|
|
value = self.parse_expression()
|
|
kwargs.append(nodes.Keyword(key, value, lineno=value.lineno))
|
|
else:
|
|
# Parsing an arg
|
|
ensure(dyn_args is None and dyn_kwargs is None and not kwargs)
|
|
args.append(self.parse_expression())
|
|
|
|
require_comma = True
|
|
|
|
self.stream.expect("rparen")
|
|
return args, kwargs, dyn_args, dyn_kwargs
|
|
|
|
def parse_call(self, node: nodes.Expr) -> nodes.Call:
|
|
# The lparen will be expected in parse_call_args, but the lineno
|
|
# needs to be recorded before the stream is advanced.
|
|
token = self.stream.current
|
|
args, kwargs, dyn_args, dyn_kwargs = self.parse_call_args()
|
|
return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno)
|
|
|
|
def parse_filter(
|
|
self, node: t.Optional[nodes.Expr], start_inline: bool = False
|
|
) -> t.Optional[nodes.Expr]:
|
|
while self.stream.current.type == "pipe" or start_inline:
|
|
if not start_inline:
|
|
next(self.stream)
|
|
token = self.stream.expect("name")
|
|
name = token.value
|
|
while self.stream.current.type == "dot":
|
|
next(self.stream)
|
|
name += "." + self.stream.expect("name").value
|
|
if self.stream.current.type == "lparen":
|
|
args, kwargs, dyn_args, dyn_kwargs = self.parse_call_args()
|
|
else:
|
|
args = []
|
|
kwargs = []
|
|
dyn_args = dyn_kwargs = None
|
|
node = nodes.Filter(
|
|
node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno
|
|
)
|
|
start_inline = False
|
|
return node
|
|
|
|
def parse_test(self, node: nodes.Expr) -> nodes.Expr:
|
|
token = next(self.stream)
|
|
if self.stream.current.test("name:not"):
|
|
next(self.stream)
|
|
negated = True
|
|
else:
|
|
negated = False
|
|
name = self.stream.expect("name").value
|
|
while self.stream.current.type == "dot":
|
|
next(self.stream)
|
|
name += "." + self.stream.expect("name").value
|
|
dyn_args = dyn_kwargs = None
|
|
kwargs: t.List[nodes.Keyword] = []
|
|
if self.stream.current.type == "lparen":
|
|
args, kwargs, dyn_args, dyn_kwargs = self.parse_call_args()
|
|
elif self.stream.current.type in {
|
|
"name",
|
|
"string",
|
|
"integer",
|
|
"float",
|
|
"lparen",
|
|
"lbracket",
|
|
"lbrace",
|
|
} and not self.stream.current.test_any("name:else", "name:or", "name:and"):
|
|
if self.stream.current.test("name:is"):
|
|
self.fail("You cannot chain multiple tests with is")
|
|
arg_node = self.parse_primary()
|
|
arg_node = self.parse_postfix(arg_node)
|
|
args = [arg_node]
|
|
else:
|
|
args = []
|
|
node = nodes.Test(
|
|
node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno
|
|
)
|
|
if negated:
|
|
node = nodes.Not(node, lineno=token.lineno)
|
|
return node
|
|
|
|
def subparse(
|
|
self, end_tokens: t.Optional[t.Tuple[str, ...]] = None
|
|
) -> t.List[nodes.Node]:
|
|
body: t.List[nodes.Node] = []
|
|
data_buffer: t.List[nodes.Node] = []
|
|
add_data = data_buffer.append
|
|
|
|
if end_tokens is not None:
|
|
self._end_token_stack.append(end_tokens)
|
|
|
|
def flush_data() -> None:
|
|
if data_buffer:
|
|
lineno = data_buffer[0].lineno
|
|
body.append(nodes.Output(data_buffer[:], lineno=lineno))
|
|
del data_buffer[:]
|
|
|
|
try:
|
|
while self.stream:
|
|
token = self.stream.current
|
|
if token.type == "data":
|
|
if token.value:
|
|
add_data(nodes.TemplateData(token.value, lineno=token.lineno))
|
|
next(self.stream)
|
|
elif token.type == "variable_begin":
|
|
next(self.stream)
|
|
add_data(self.parse_tuple(with_condexpr=True))
|
|
self.stream.expect("variable_end")
|
|
elif token.type == "block_begin":
|
|
flush_data()
|
|
next(self.stream)
|
|
if end_tokens is not None and self.stream.current.test_any(
|
|
*end_tokens
|
|
):
|
|
return body
|
|
rv = self.parse_statement()
|
|
if isinstance(rv, list):
|
|
body.extend(rv)
|
|
else:
|
|
body.append(rv)
|
|
self.stream.expect("block_end")
|
|
else:
|
|
raise AssertionError("internal parsing error")
|
|
|
|
flush_data()
|
|
finally:
|
|
if end_tokens is not None:
|
|
self._end_token_stack.pop()
|
|
return body
|
|
|
|
def parse(self) -> nodes.Template:
|
|
"""Parse the whole template into a `Template` node."""
|
|
result = nodes.Template(self.subparse(), lineno=1)
|
|
result.set_environment(self.environment)
|
|
return result
|