Spaces:
Paused
Paused
"""Implements a Jinja / Python combination lexer. The ``Lexer`` class | |
is used to do some preprocessing. It filters out invalid operators like | |
the bitshift operators we don't allow in templates. It separates | |
template code and python code in expressions. | |
""" | |
import re | |
import typing as t | |
from ast import literal_eval | |
from collections import deque | |
from sys import intern | |
from ._identifier import pattern as name_re | |
from .exceptions import TemplateSyntaxError | |
from .utils import LRUCache | |
if t.TYPE_CHECKING: | |
import typing_extensions as te | |
from .environment import Environment | |
# cache for the lexers. Exists in order to be able to have multiple | |
# environments with the same lexer | |
_lexer_cache: t.MutableMapping[t.Tuple, "Lexer"] = LRUCache(50) # type: ignore | |
# static regular expressions | |
whitespace_re = re.compile(r"\s+") | |
newline_re = re.compile(r"(\r\n|\r|\n)") | |
string_re = re.compile( | |
r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S | |
) | |
integer_re = re.compile( | |
r""" | |
( | |
0b(_?[0-1])+ # binary | |
| | |
0o(_?[0-7])+ # octal | |
| | |
0x(_?[\da-f])+ # hex | |
| | |
[1-9](_?\d)* # decimal | |
| | |
0(_?0)* # decimal zero | |
) | |
""", | |
re.IGNORECASE | re.VERBOSE, | |
) | |
float_re = re.compile( | |
r""" | |
(?<!\.) # doesn't start with a . | |
(\d+_)*\d+ # digits, possibly _ separated | |
( | |
(\.(\d+_)*\d+)? # optional fractional part | |
e[+\-]?(\d+_)*\d+ # exponent part | |
| | |
\.(\d+_)*\d+ # required fractional part | |
) | |
""", | |
re.IGNORECASE | re.VERBOSE, | |
) | |
# internal the tokens and keep references to them | |
TOKEN_ADD = intern("add") | |
TOKEN_ASSIGN = intern("assign") | |
TOKEN_COLON = intern("colon") | |
TOKEN_COMMA = intern("comma") | |
TOKEN_DIV = intern("div") | |
TOKEN_DOT = intern("dot") | |
TOKEN_EQ = intern("eq") | |
TOKEN_FLOORDIV = intern("floordiv") | |
TOKEN_GT = intern("gt") | |
TOKEN_GTEQ = intern("gteq") | |
TOKEN_LBRACE = intern("lbrace") | |
TOKEN_LBRACKET = intern("lbracket") | |
TOKEN_LPAREN = intern("lparen") | |
TOKEN_LT = intern("lt") | |
TOKEN_LTEQ = intern("lteq") | |
TOKEN_MOD = intern("mod") | |
TOKEN_MUL = intern("mul") | |
TOKEN_NE = intern("ne") | |
TOKEN_PIPE = intern("pipe") | |
TOKEN_POW = intern("pow") | |
TOKEN_RBRACE = intern("rbrace") | |
TOKEN_RBRACKET = intern("rbracket") | |
TOKEN_RPAREN = intern("rparen") | |
TOKEN_SEMICOLON = intern("semicolon") | |
TOKEN_SUB = intern("sub") | |
TOKEN_TILDE = intern("tilde") | |
TOKEN_WHITESPACE = intern("whitespace") | |
TOKEN_FLOAT = intern("float") | |
TOKEN_INTEGER = intern("integer") | |
TOKEN_NAME = intern("name") | |
TOKEN_STRING = intern("string") | |
TOKEN_OPERATOR = intern("operator") | |
TOKEN_BLOCK_BEGIN = intern("block_begin") | |
TOKEN_BLOCK_END = intern("block_end") | |
TOKEN_VARIABLE_BEGIN = intern("variable_begin") | |
TOKEN_VARIABLE_END = intern("variable_end") | |
TOKEN_RAW_BEGIN = intern("raw_begin") | |
TOKEN_RAW_END = intern("raw_end") | |
TOKEN_COMMENT_BEGIN = intern("comment_begin") | |
TOKEN_COMMENT_END = intern("comment_end") | |
TOKEN_COMMENT = intern("comment") | |
TOKEN_LINESTATEMENT_BEGIN = intern("linestatement_begin") | |
TOKEN_LINESTATEMENT_END = intern("linestatement_end") | |
TOKEN_LINECOMMENT_BEGIN = intern("linecomment_begin") | |
TOKEN_LINECOMMENT_END = intern("linecomment_end") | |
TOKEN_LINECOMMENT = intern("linecomment") | |
TOKEN_DATA = intern("data") | |
TOKEN_INITIAL = intern("initial") | |
TOKEN_EOF = intern("eof") | |
# bind operators to token types | |
operators = { | |
"+": TOKEN_ADD, | |
"-": TOKEN_SUB, | |
"/": TOKEN_DIV, | |
"//": TOKEN_FLOORDIV, | |
"*": TOKEN_MUL, | |
"%": TOKEN_MOD, | |
"**": TOKEN_POW, | |
"~": TOKEN_TILDE, | |
"[": TOKEN_LBRACKET, | |
"]": TOKEN_RBRACKET, | |
"(": TOKEN_LPAREN, | |
")": TOKEN_RPAREN, | |
"{": TOKEN_LBRACE, | |
"}": TOKEN_RBRACE, | |
"==": TOKEN_EQ, | |
"!=": TOKEN_NE, | |
">": TOKEN_GT, | |
">=": TOKEN_GTEQ, | |
"<": TOKEN_LT, | |
"<=": TOKEN_LTEQ, | |
"=": TOKEN_ASSIGN, | |
".": TOKEN_DOT, | |
":": TOKEN_COLON, | |
"|": TOKEN_PIPE, | |
",": TOKEN_COMMA, | |
";": TOKEN_SEMICOLON, | |
} | |
reverse_operators = {v: k for k, v in operators.items()} | |
assert len(operators) == len(reverse_operators), "operators dropped" | |
operator_re = re.compile( | |
f"({'|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))})" | |
) | |
ignored_tokens = frozenset( | |
[ | |
TOKEN_COMMENT_BEGIN, | |
TOKEN_COMMENT, | |
TOKEN_COMMENT_END, | |
TOKEN_WHITESPACE, | |
TOKEN_LINECOMMENT_BEGIN, | |
TOKEN_LINECOMMENT_END, | |
TOKEN_LINECOMMENT, | |
] | |
) | |
ignore_if_empty = frozenset( | |
[TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT] | |
) | |
def _describe_token_type(token_type: str) -> str: | |
if token_type in reverse_operators: | |
return reverse_operators[token_type] | |
return { | |
TOKEN_COMMENT_BEGIN: "begin of comment", | |
TOKEN_COMMENT_END: "end of comment", | |
TOKEN_COMMENT: "comment", | |
TOKEN_LINECOMMENT: "comment", | |
TOKEN_BLOCK_BEGIN: "begin of statement block", | |
TOKEN_BLOCK_END: "end of statement block", | |
TOKEN_VARIABLE_BEGIN: "begin of print statement", | |
TOKEN_VARIABLE_END: "end of print statement", | |
TOKEN_LINESTATEMENT_BEGIN: "begin of line statement", | |
TOKEN_LINESTATEMENT_END: "end of line statement", | |
TOKEN_DATA: "template data / text", | |
TOKEN_EOF: "end of template", | |
}.get(token_type, token_type) | |
def describe_token(token: "Token") -> str: | |
"""Returns a description of the token.""" | |
if token.type == TOKEN_NAME: | |
return token.value | |
return _describe_token_type(token.type) | |
def describe_token_expr(expr: str) -> str: | |
"""Like `describe_token` but for token expressions.""" | |
if ":" in expr: | |
type, value = expr.split(":", 1) | |
if type == TOKEN_NAME: | |
return value | |
else: | |
type = expr | |
return _describe_token_type(type) | |
def count_newlines(value: str) -> int: | |
"""Count the number of newline characters in the string. This is | |
useful for extensions that filter a stream. | |
""" | |
return len(newline_re.findall(value)) | |
def compile_rules(environment: "Environment") -> t.List[t.Tuple[str, str]]: | |
"""Compiles all the rules from the environment into a list of rules.""" | |
e = re.escape | |
rules = [ | |
( | |
len(environment.comment_start_string), | |
TOKEN_COMMENT_BEGIN, | |
e(environment.comment_start_string), | |
), | |
( | |
len(environment.block_start_string), | |
TOKEN_BLOCK_BEGIN, | |
e(environment.block_start_string), | |
), | |
( | |
len(environment.variable_start_string), | |
TOKEN_VARIABLE_BEGIN, | |
e(environment.variable_start_string), | |
), | |
] | |
if environment.line_statement_prefix is not None: | |
rules.append( | |
( | |
len(environment.line_statement_prefix), | |
TOKEN_LINESTATEMENT_BEGIN, | |
r"^[ \t\v]*" + e(environment.line_statement_prefix), | |
) | |
) | |
if environment.line_comment_prefix is not None: | |
rules.append( | |
( | |
len(environment.line_comment_prefix), | |
TOKEN_LINECOMMENT_BEGIN, | |
r"(?:^|(?<=\S))[^\S\r\n]*" + e(environment.line_comment_prefix), | |
) | |
) | |
return [x[1:] for x in sorted(rules, reverse=True)] | |
class Failure: | |
"""Class that raises a `TemplateSyntaxError` if called. | |
Used by the `Lexer` to specify known errors. | |
""" | |
def __init__( | |
self, message: str, cls: t.Type[TemplateSyntaxError] = TemplateSyntaxError | |
) -> None: | |
self.message = message | |
self.error_class = cls | |
def __call__(self, lineno: int, filename: str) -> "te.NoReturn": | |
raise self.error_class(self.message, lineno, filename) | |
class Token(t.NamedTuple): | |
lineno: int | |
type: str | |
value: str | |
def __str__(self) -> str: | |
return describe_token(self) | |
def test(self, expr: str) -> bool: | |
"""Test a token against a token expression. This can either be a | |
token type or ``'token_type:token_value'``. This can only test | |
against string values and types. | |
""" | |
# here we do a regular string equality check as test_any is usually | |
# passed an iterable of not interned strings. | |
if self.type == expr: | |
return True | |
if ":" in expr: | |
return expr.split(":", 1) == [self.type, self.value] | |
return False | |
def test_any(self, *iterable: str) -> bool: | |
"""Test against multiple token expressions.""" | |
return any(self.test(expr) for expr in iterable) | |
class TokenStreamIterator: | |
"""The iterator for tokenstreams. Iterate over the stream | |
until the eof token is reached. | |
""" | |
def __init__(self, stream: "TokenStream") -> None: | |
self.stream = stream | |
def __iter__(self) -> "TokenStreamIterator": | |
return self | |
def __next__(self) -> Token: | |
token = self.stream.current | |
if token.type is TOKEN_EOF: | |
self.stream.close() | |
raise StopIteration | |
next(self.stream) | |
return token | |
class TokenStream: | |
"""A token stream is an iterable that yields :class:`Token`\\s. The | |
parser however does not iterate over it but calls :meth:`next` to go | |
one token ahead. The current active token is stored as :attr:`current`. | |
""" | |
def __init__( | |
self, | |
generator: t.Iterable[Token], | |
name: t.Optional[str], | |
filename: t.Optional[str], | |
): | |
self._iter = iter(generator) | |
self._pushed: "te.Deque[Token]" = deque() | |
self.name = name | |
self.filename = filename | |
self.closed = False | |
self.current = Token(1, TOKEN_INITIAL, "") | |
next(self) | |
def __iter__(self) -> TokenStreamIterator: | |
return TokenStreamIterator(self) | |
def __bool__(self) -> bool: | |
return bool(self._pushed) or self.current.type is not TOKEN_EOF | |
def eos(self) -> bool: | |
"""Are we at the end of the stream?""" | |
return not self | |
def push(self, token: Token) -> None: | |
"""Push a token back to the stream.""" | |
self._pushed.append(token) | |
def look(self) -> Token: | |
"""Look at the next token.""" | |
old_token = next(self) | |
result = self.current | |
self.push(result) | |
self.current = old_token | |
return result | |
def skip(self, n: int = 1) -> None: | |
"""Got n tokens ahead.""" | |
for _ in range(n): | |
next(self) | |
def next_if(self, expr: str) -> t.Optional[Token]: | |
"""Perform the token test and return the token if it matched. | |
Otherwise the return value is `None`. | |
""" | |
if self.current.test(expr): | |
return next(self) | |
return None | |
def skip_if(self, expr: str) -> bool: | |
"""Like :meth:`next_if` but only returns `True` or `False`.""" | |
return self.next_if(expr) is not None | |
def __next__(self) -> Token: | |
"""Go one token ahead and return the old one. | |
Use the built-in :func:`next` instead of calling this directly. | |
""" | |
rv = self.current | |
if self._pushed: | |
self.current = self._pushed.popleft() | |
elif self.current.type is not TOKEN_EOF: | |
try: | |
self.current = next(self._iter) | |
except StopIteration: | |
self.close() | |
return rv | |
def close(self) -> None: | |
"""Close the stream.""" | |
self.current = Token(self.current.lineno, TOKEN_EOF, "") | |
self._iter = iter(()) | |
self.closed = True | |
def expect(self, expr: str) -> Token: | |
"""Expect a given token type and return it. This accepts the same | |
argument as :meth:`jinja2.lexer.Token.test`. | |
""" | |
if not self.current.test(expr): | |
expr = describe_token_expr(expr) | |
if self.current.type is TOKEN_EOF: | |
raise TemplateSyntaxError( | |
f"unexpected end of template, expected {expr!r}.", | |
self.current.lineno, | |
self.name, | |
self.filename, | |
) | |
raise TemplateSyntaxError( | |
f"expected token {expr!r}, got {describe_token(self.current)!r}", | |
self.current.lineno, | |
self.name, | |
self.filename, | |
) | |
return next(self) | |
def get_lexer(environment: "Environment") -> "Lexer": | |
"""Return a lexer which is probably cached.""" | |
key = ( | |
environment.block_start_string, | |
environment.block_end_string, | |
environment.variable_start_string, | |
environment.variable_end_string, | |
environment.comment_start_string, | |
environment.comment_end_string, | |
environment.line_statement_prefix, | |
environment.line_comment_prefix, | |
environment.trim_blocks, | |
environment.lstrip_blocks, | |
environment.newline_sequence, | |
environment.keep_trailing_newline, | |
) | |
lexer = _lexer_cache.get(key) | |
if lexer is None: | |
_lexer_cache[key] = lexer = Lexer(environment) | |
return lexer | |
class OptionalLStrip(tuple): # type: ignore[type-arg] | |
"""A special tuple for marking a point in the state that can have | |
lstrip applied. | |
""" | |
__slots__ = () | |
# Even though it looks like a no-op, creating instances fails | |
# without this. | |
def __new__(cls, *members, **kwargs): # type: ignore | |
return super().__new__(cls, members) | |
class _Rule(t.NamedTuple): | |
pattern: t.Pattern[str] | |
tokens: t.Union[str, t.Tuple[str, ...], t.Tuple[Failure]] | |
command: t.Optional[str] | |
class Lexer: | |
"""Class that implements a lexer for a given environment. Automatically | |
created by the environment class, usually you don't have to do that. | |
Note that the lexer is not automatically bound to an environment. | |
Multiple environments can share the same lexer. | |
""" | |
def __init__(self, environment: "Environment") -> None: | |
# shortcuts | |
e = re.escape | |
def c(x: str) -> t.Pattern[str]: | |
return re.compile(x, re.M | re.S) | |
# lexing rules for tags | |
tag_rules: t.List[_Rule] = [ | |
_Rule(whitespace_re, TOKEN_WHITESPACE, None), | |
_Rule(float_re, TOKEN_FLOAT, None), | |
_Rule(integer_re, TOKEN_INTEGER, None), | |
_Rule(name_re, TOKEN_NAME, None), | |
_Rule(string_re, TOKEN_STRING, None), | |
_Rule(operator_re, TOKEN_OPERATOR, None), | |
] | |
# assemble the root lexing rule. because "|" is ungreedy | |
# we have to sort by length so that the lexer continues working | |
# as expected when we have parsing rules like <% for block and | |
# <%= for variables. (if someone wants asp like syntax) | |
# variables are just part of the rules if variable processing | |
# is required. | |
root_tag_rules = compile_rules(environment) | |
block_start_re = e(environment.block_start_string) | |
block_end_re = e(environment.block_end_string) | |
comment_end_re = e(environment.comment_end_string) | |
variable_end_re = e(environment.variable_end_string) | |
# block suffix if trimming is enabled | |
block_suffix_re = "\\n?" if environment.trim_blocks else "" | |
self.lstrip_blocks = environment.lstrip_blocks | |
self.newline_sequence = environment.newline_sequence | |
self.keep_trailing_newline = environment.keep_trailing_newline | |
root_raw_re = ( | |
rf"(?P<raw_begin>{block_start_re}(\-|\+|)\s*raw\s*" | |
rf"(?:\-{block_end_re}\s*|{block_end_re}))" | |
) | |
root_parts_re = "|".join( | |
[root_raw_re] + [rf"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules] | |
) | |
# global lexing rules | |
self.rules: t.Dict[str, t.List[_Rule]] = { | |
"root": [ | |
# directives | |
_Rule( | |
c(rf"(.*?)(?:{root_parts_re})"), | |
OptionalLStrip(TOKEN_DATA, "#bygroup"), # type: ignore | |
"#bygroup", | |
), | |
# data | |
_Rule(c(".+"), TOKEN_DATA, None), | |
], | |
# comments | |
TOKEN_COMMENT_BEGIN: [ | |
_Rule( | |
c( | |
rf"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*" | |
rf"|{comment_end_re}{block_suffix_re}))" | |
), | |
(TOKEN_COMMENT, TOKEN_COMMENT_END), | |
"#pop", | |
), | |
_Rule(c(r"(.)"), (Failure("Missing end of comment tag"),), None), | |
], | |
# blocks | |
TOKEN_BLOCK_BEGIN: [ | |
_Rule( | |
c( | |
rf"(?:\+{block_end_re}|\-{block_end_re}\s*" | |
rf"|{block_end_re}{block_suffix_re})" | |
), | |
TOKEN_BLOCK_END, | |
"#pop", | |
), | |
] | |
+ tag_rules, | |
# variables | |
TOKEN_VARIABLE_BEGIN: [ | |
_Rule( | |
c(rf"\-{variable_end_re}\s*|{variable_end_re}"), | |
TOKEN_VARIABLE_END, | |
"#pop", | |
) | |
] | |
+ tag_rules, | |
# raw block | |
TOKEN_RAW_BEGIN: [ | |
_Rule( | |
c( | |
rf"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*" | |
rf"(?:\+{block_end_re}|\-{block_end_re}\s*" | |
rf"|{block_end_re}{block_suffix_re}))" | |
), | |
OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END), # type: ignore | |
"#pop", | |
), | |
_Rule(c(r"(.)"), (Failure("Missing end of raw directive"),), None), | |
], | |
# line statements | |
TOKEN_LINESTATEMENT_BEGIN: [ | |
_Rule(c(r"\s*(\n|$)"), TOKEN_LINESTATEMENT_END, "#pop") | |
] | |
+ tag_rules, | |
# line comments | |
TOKEN_LINECOMMENT_BEGIN: [ | |
_Rule( | |
c(r"(.*?)()(?=\n|$)"), | |
(TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END), | |
"#pop", | |
) | |
], | |
} | |
def _normalize_newlines(self, value: str) -> str: | |
"""Replace all newlines with the configured sequence in strings | |
and template data. | |
""" | |
return newline_re.sub(self.newline_sequence, value) | |
def tokenize( | |
self, | |
source: str, | |
name: t.Optional[str] = None, | |
filename: t.Optional[str] = None, | |
state: t.Optional[str] = None, | |
) -> TokenStream: | |
"""Calls tokeniter + tokenize and wraps it in a token stream.""" | |
stream = self.tokeniter(source, name, filename, state) | |
return TokenStream(self.wrap(stream, name, filename), name, filename) | |
def wrap( | |
self, | |
stream: t.Iterable[t.Tuple[int, str, str]], | |
name: t.Optional[str] = None, | |
filename: t.Optional[str] = None, | |
) -> t.Iterator[Token]: | |
"""This is called with the stream as returned by `tokenize` and wraps | |
every token in a :class:`Token` and converts the value. | |
""" | |
for lineno, token, value_str in stream: | |
if token in ignored_tokens: | |
continue | |
value: t.Any = value_str | |
if token == TOKEN_LINESTATEMENT_BEGIN: | |
token = TOKEN_BLOCK_BEGIN | |
elif token == TOKEN_LINESTATEMENT_END: | |
token = TOKEN_BLOCK_END | |
# we are not interested in those tokens in the parser | |
elif token in (TOKEN_RAW_BEGIN, TOKEN_RAW_END): | |
continue | |
elif token == TOKEN_DATA: | |
value = self._normalize_newlines(value_str) | |
elif token == "keyword": | |
token = value_str | |
elif token == TOKEN_NAME: | |
value = value_str | |
if not value.isidentifier(): | |
raise TemplateSyntaxError( | |
"Invalid character in identifier", lineno, name, filename | |
) | |
elif token == TOKEN_STRING: | |
# try to unescape string | |
try: | |
value = ( | |
self._normalize_newlines(value_str[1:-1]) | |
.encode("ascii", "backslashreplace") | |
.decode("unicode-escape") | |
) | |
except Exception as e: | |
msg = str(e).split(":")[-1].strip() | |
raise TemplateSyntaxError(msg, lineno, name, filename) from e | |
elif token == TOKEN_INTEGER: | |
value = int(value_str.replace("_", ""), 0) | |
elif token == TOKEN_FLOAT: | |
# remove all "_" first to support more Python versions | |
value = literal_eval(value_str.replace("_", "")) | |
elif token == TOKEN_OPERATOR: | |
token = operators[value_str] | |
yield Token(lineno, token, value) | |
def tokeniter( | |
self, | |
source: str, | |
name: t.Optional[str], | |
filename: t.Optional[str] = None, | |
state: t.Optional[str] = None, | |
) -> t.Iterator[t.Tuple[int, str, str]]: | |
"""This method tokenizes the text and returns the tokens in a | |
generator. Use this method if you just want to tokenize a template. | |
.. versionchanged:: 3.0 | |
Only ``\\n``, ``\\r\\n`` and ``\\r`` are treated as line | |
breaks. | |
""" | |
lines = newline_re.split(source)[::2] | |
if not self.keep_trailing_newline and lines[-1] == "": | |
del lines[-1] | |
source = "\n".join(lines) | |
pos = 0 | |
lineno = 1 | |
stack = ["root"] | |
if state is not None and state != "root": | |
assert state in ("variable", "block"), "invalid state" | |
stack.append(state + "_begin") | |
statetokens = self.rules[stack[-1]] | |
source_length = len(source) | |
balancing_stack: t.List[str] = [] | |
newlines_stripped = 0 | |
line_starting = True | |
while True: | |
# tokenizer loop | |
for regex, tokens, new_state in statetokens: | |
m = regex.match(source, pos) | |
# if no match we try again with the next rule | |
if m is None: | |
continue | |
# we only match blocks and variables if braces / parentheses | |
# are balanced. continue parsing with the lower rule which | |
# is the operator rule. do this only if the end tags look | |
# like operators | |
if balancing_stack and tokens in ( | |
TOKEN_VARIABLE_END, | |
TOKEN_BLOCK_END, | |
TOKEN_LINESTATEMENT_END, | |
): | |
continue | |
# tuples support more options | |
if isinstance(tokens, tuple): | |
groups: t.Sequence[str] = m.groups() | |
if isinstance(tokens, OptionalLStrip): | |
# Rule supports lstrip. Match will look like | |
# text, block type, whitespace control, type, control, ... | |
text = groups[0] | |
# Skipping the text and first type, every other group is the | |
# whitespace control for each type. One of the groups will be | |
# -, +, or empty string instead of None. | |
strip_sign = next(g for g in groups[2::2] if g is not None) | |
if strip_sign == "-": | |
# Strip all whitespace between the text and the tag. | |
stripped = text.rstrip() | |
newlines_stripped = text[len(stripped) :].count("\n") | |
groups = [stripped, *groups[1:]] | |
elif ( | |
# Not marked for preserving whitespace. | |
strip_sign != "+" | |
# lstrip is enabled. | |
and self.lstrip_blocks | |
# Not a variable expression. | |
and not m.groupdict().get(TOKEN_VARIABLE_BEGIN) | |
): | |
# The start of text between the last newline and the tag. | |
l_pos = text.rfind("\n") + 1 | |
if l_pos > 0 or line_starting: | |
# If there's only whitespace between the newline and the | |
# tag, strip it. | |
if whitespace_re.fullmatch(text, l_pos): | |
groups = [text[:l_pos], *groups[1:]] | |
for idx, token in enumerate(tokens): | |
# failure group | |
if token.__class__ is Failure: | |
raise token(lineno, filename) | |
# bygroup is a bit more complex, in that case we | |
# yield for the current token the first named | |
# group that matched | |
elif token == "#bygroup": | |
for key, value in m.groupdict().items(): | |
if value is not None: | |
yield lineno, key, value | |
lineno += value.count("\n") | |
break | |
else: | |
raise RuntimeError( | |
f"{regex!r} wanted to resolve the token dynamically" | |
" but no group matched" | |
) | |
# normal group | |
else: | |
data = groups[idx] | |
if data or token not in ignore_if_empty: | |
yield lineno, token, data | |
lineno += data.count("\n") + newlines_stripped | |
newlines_stripped = 0 | |
# strings as token just are yielded as it. | |
else: | |
data = m.group() | |
# update brace/parentheses balance | |
if tokens == TOKEN_OPERATOR: | |
if data == "{": | |
balancing_stack.append("}") | |
elif data == "(": | |
balancing_stack.append(")") | |
elif data == "[": | |
balancing_stack.append("]") | |
elif data in ("}", ")", "]"): | |
if not balancing_stack: | |
raise TemplateSyntaxError( | |
f"unexpected '{data}'", lineno, name, filename | |
) | |
expected_op = balancing_stack.pop() | |
if expected_op != data: | |
raise TemplateSyntaxError( | |
f"unexpected '{data}', expected '{expected_op}'", | |
lineno, | |
name, | |
filename, | |
) | |
# yield items | |
if data or tokens not in ignore_if_empty: | |
yield lineno, tokens, data | |
lineno += data.count("\n") | |
line_starting = m.group()[-1:] == "\n" | |
# fetch new position into new variable so that we can check | |
# if there is a internal parsing error which would result | |
# in an infinite loop | |
pos2 = m.end() | |
# handle state changes | |
if new_state is not None: | |
# remove the uppermost state | |
if new_state == "#pop": | |
stack.pop() | |
# resolve the new state by group checking | |
elif new_state == "#bygroup": | |
for key, value in m.groupdict().items(): | |
if value is not None: | |
stack.append(key) | |
break | |
else: | |
raise RuntimeError( | |
f"{regex!r} wanted to resolve the new state dynamically" | |
f" but no group matched" | |
) | |
# direct state name given | |
else: | |
stack.append(new_state) | |
statetokens = self.rules[stack[-1]] | |
# we are still at the same position and no stack change. | |
# this means a loop without break condition, avoid that and | |
# raise error | |
elif pos2 == pos: | |
raise RuntimeError( | |
f"{regex!r} yielded empty string without stack change" | |
) | |
# publish new function and start again | |
pos = pos2 | |
break | |
# if loop terminated without break we haven't found a single match | |
# either we are at the end of the file or we have a problem | |
else: | |
# end of text | |
if pos >= source_length: | |
return | |
# something went wrong | |
raise TemplateSyntaxError( | |
f"unexpected char {source[pos]!r} at {pos}", lineno, name, filename | |
) | |