| """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 |
| |
| @property |
| 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): |
| """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 |
| ) |