Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
lexer.py869 linesDownload Raw Back to jinja2
1"""Implements a Jinja / Python combination lexer. The ``Lexer`` class2is used to do some preprocessing. It filters out invalid operators like3the bitshift operators we don't allow in templates. It separates4template code and python code in expressions.5"""6 7import re8import typing as t9from ast import literal_eval10from collections import deque11from sys import intern12 13from ._identifier import pattern as name_re14from .exceptions import TemplateSyntaxError15from .utils import LRUCache16 17if t.TYPE_CHECKING:18    import typing_extensions as te19 20    from .environment import Environment21 22# cache for the lexers. Exists in order to be able to have multiple23# environments with the same lexer24_lexer_cache: t.MutableMapping[t.Tuple, "Lexer"] = LRUCache(50)  # type: ignore25 26# static regular expressions27whitespace_re = re.compile(r"\s+")28newline_re = re.compile(r"(\r\n|\r|\n)")29string_re = re.compile(30    r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S31)32integer_re = re.compile(33    r"""34    (35        0b(_?[0-1])+ # binary36    |37        0o(_?[0-7])+ # octal38    |39        0x(_?[\da-f])+ # hex40    |41        [1-9](_?\d)* # decimal42    |43        0(_?0)* # decimal zero44    )45    """,46    re.IGNORECASE | re.VERBOSE,47)48float_re = re.compile(49    r"""50    (?<!\.)  # doesn't start with a .51    (\d+_)*\d+  # digits, possibly _ separated52    (53        (\.(\d+_)*\d+)?  # optional fractional part54        e[+\-]?(\d+_)*\d+  # exponent part55    |56        \.(\d+_)*\d+  # required fractional part57    )58    """,59    re.IGNORECASE | re.VERBOSE,60)61 62# internal the tokens and keep references to them63TOKEN_ADD = intern("add")64TOKEN_ASSIGN = intern("assign")65TOKEN_COLON = intern("colon")66TOKEN_COMMA = intern("comma")67TOKEN_DIV = intern("div")68TOKEN_DOT = intern("dot")69TOKEN_EQ = intern("eq")70TOKEN_FLOORDIV = intern("floordiv")71TOKEN_GT = intern("gt")72TOKEN_GTEQ = intern("gteq")73TOKEN_LBRACE = intern("lbrace")74TOKEN_LBRACKET = intern("lbracket")75TOKEN_LPAREN = intern("lparen")76TOKEN_LT = intern("lt")77TOKEN_LTEQ = intern("lteq")78TOKEN_MOD = intern("mod")79TOKEN_MUL = intern("mul")80TOKEN_NE = intern("ne")81TOKEN_PIPE = intern("pipe")82TOKEN_POW = intern("pow")83TOKEN_RBRACE = intern("rbrace")84TOKEN_RBRACKET = intern("rbracket")85TOKEN_RPAREN = intern("rparen")86TOKEN_SEMICOLON = intern("semicolon")87TOKEN_SUB = intern("sub")88TOKEN_TILDE = intern("tilde")89TOKEN_WHITESPACE = intern("whitespace")90TOKEN_FLOAT = intern("float")91TOKEN_INTEGER = intern("integer")92TOKEN_NAME = intern("name")93TOKEN_STRING = intern("string")94TOKEN_OPERATOR = intern("operator")95TOKEN_BLOCK_BEGIN = intern("block_begin")96TOKEN_BLOCK_END = intern("block_end")97TOKEN_VARIABLE_BEGIN = intern("variable_begin")98TOKEN_VARIABLE_END = intern("variable_end")99TOKEN_RAW_BEGIN = intern("raw_begin")100TOKEN_RAW_END = intern("raw_end")101TOKEN_COMMENT_BEGIN = intern("comment_begin")102TOKEN_COMMENT_END = intern("comment_end")103TOKEN_COMMENT = intern("comment")104TOKEN_LINESTATEMENT_BEGIN = intern("linestatement_begin")105TOKEN_LINESTATEMENT_END = intern("linestatement_end")106TOKEN_LINECOMMENT_BEGIN = intern("linecomment_begin")107TOKEN_LINECOMMENT_END = intern("linecomment_end")108TOKEN_LINECOMMENT = intern("linecomment")109TOKEN_DATA = intern("data")110TOKEN_INITIAL = intern("initial")111TOKEN_EOF = intern("eof")112 113# bind operators to token types114operators = {115    "+": TOKEN_ADD,116    "-": TOKEN_SUB,117    "/": TOKEN_DIV,118    "//": TOKEN_FLOORDIV,119    "*": TOKEN_MUL,120    "%": TOKEN_MOD,121    "**": TOKEN_POW,122    "~": TOKEN_TILDE,123    "[": TOKEN_LBRACKET,124    "]": TOKEN_RBRACKET,125    "(": TOKEN_LPAREN,126    ")": TOKEN_RPAREN,127    "{": TOKEN_LBRACE,128    "}": TOKEN_RBRACE,129    "==": TOKEN_EQ,130    "!=": TOKEN_NE,131    ">": TOKEN_GT,132    ">=": TOKEN_GTEQ,133    "<": TOKEN_LT,134    "<=": TOKEN_LTEQ,135    "=": TOKEN_ASSIGN,136    ".": TOKEN_DOT,137    ":": TOKEN_COLON,138    "|": TOKEN_PIPE,139    ",": TOKEN_COMMA,140    ";": TOKEN_SEMICOLON,141}142 143reverse_operators = {v: k for k, v in operators.items()}144assert len(operators) == len(reverse_operators), "operators dropped"145operator_re = re.compile(146    f"({'|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))})"147)148 149ignored_tokens = frozenset(150    [151        TOKEN_COMMENT_BEGIN,152        TOKEN_COMMENT,153        TOKEN_COMMENT_END,154        TOKEN_WHITESPACE,155        TOKEN_LINECOMMENT_BEGIN,156        TOKEN_LINECOMMENT_END,157        TOKEN_LINECOMMENT,158    ]159)160ignore_if_empty = frozenset(161    [TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT]162)163 164 165def _describe_token_type(token_type: str) -> str:166    if token_type in reverse_operators:167        return reverse_operators[token_type]168 169    return {170        TOKEN_COMMENT_BEGIN: "begin of comment",171        TOKEN_COMMENT_END: "end of comment",172        TOKEN_COMMENT: "comment",173        TOKEN_LINECOMMENT: "comment",174        TOKEN_BLOCK_BEGIN: "begin of statement block",175        TOKEN_BLOCK_END: "end of statement block",176        TOKEN_VARIABLE_BEGIN: "begin of print statement",177        TOKEN_VARIABLE_END: "end of print statement",178        TOKEN_LINESTATEMENT_BEGIN: "begin of line statement",179        TOKEN_LINESTATEMENT_END: "end of line statement",180        TOKEN_DATA: "template data / text",181        TOKEN_EOF: "end of template",182    }.get(token_type, token_type)183 184 185def describe_token(token: "Token") -> str:186    """Returns a description of the token."""187    if token.type == TOKEN_NAME:188        return token.value189 190    return _describe_token_type(token.type)191 192 193def describe_token_expr(expr: str) -> str:194    """Like `describe_token` but for token expressions."""195    if ":" in expr:196        type, value = expr.split(":", 1)197 198        if type == TOKEN_NAME:199            return value200    else:201        type = expr202 203    return _describe_token_type(type)204 205 206def count_newlines(value: str) -> int:207    """Count the number of newline characters in the string.  This is208    useful for extensions that filter a stream.209    """210    return len(newline_re.findall(value))211 212 213def compile_rules(environment: "Environment") -> t.List[t.Tuple[str, str]]:214    """Compiles all the rules from the environment into a list of rules."""215    e = re.escape216    rules = [217        (218            len(environment.comment_start_string),219            TOKEN_COMMENT_BEGIN,220            e(environment.comment_start_string),221        ),222        (223            len(environment.block_start_string),224            TOKEN_BLOCK_BEGIN,225            e(environment.block_start_string),226        ),227        (228            len(environment.variable_start_string),229            TOKEN_VARIABLE_BEGIN,230            e(environment.variable_start_string),231        ),232    ]233 234    if environment.line_statement_prefix is not None:235        rules.append(236            (237                len(environment.line_statement_prefix),238                TOKEN_LINESTATEMENT_BEGIN,239                r"^[ \t\v]*" + e(environment.line_statement_prefix),240            )241        )242    if environment.line_comment_prefix is not None:243        rules.append(244            (245                len(environment.line_comment_prefix),246                TOKEN_LINECOMMENT_BEGIN,247                r"(?:^|(?<=\S))[^\S\r\n]*" + e(environment.line_comment_prefix),248            )249        )250 251    return [x[1:] for x in sorted(rules, reverse=True)]252 253 254class Failure:255    """Class that raises a `TemplateSyntaxError` if called.256    Used by the `Lexer` to specify known errors.257    """258 259    def __init__(260        self, message: str, cls: t.Type[TemplateSyntaxError] = TemplateSyntaxError261    ) -> None:262        self.message = message263        self.error_class = cls264 265    def __call__(self, lineno: int, filename: str) -> "te.NoReturn":266        raise self.error_class(self.message, lineno, filename)267 268 269class Token(t.NamedTuple):270    lineno: int271    type: str272    value: str273 274    def __str__(self) -> str:275        return describe_token(self)276 277    def test(self, expr: str) -> bool:278        """Test a token against a token expression.  This can either be a279        token type or ``'token_type:token_value'``.  This can only test280        against string values and types.281        """282        # here we do a regular string equality check as test_any is usually283        # passed an iterable of not interned strings.284        if self.type == expr:285            return True286 287        if ":" in expr:288            return expr.split(":", 1) == [self.type, self.value]289 290        return False291 292    def test_any(self, *iterable: str) -> bool:293        """Test against multiple token expressions."""294        return any(self.test(expr) for expr in iterable)295 296 297class TokenStreamIterator:298    """The iterator for tokenstreams.  Iterate over the stream299    until the eof token is reached.300    """301 302    def __init__(self, stream: "TokenStream") -> None:303        self.stream = stream304 305    def __iter__(self) -> "TokenStreamIterator":306        return self307 308    def __next__(self) -> Token:309        token = self.stream.current310 311        if token.type is TOKEN_EOF:312            self.stream.close()313            raise StopIteration314 315        next(self.stream)316        return token317 318 319class TokenStream:320    """A token stream is an iterable that yields :class:`Token`\\s.  The321    parser however does not iterate over it but calls :meth:`next` to go322    one token ahead.  The current active token is stored as :attr:`current`.323    """324 325    def __init__(326        self,327        generator: t.Iterable[Token],328        name: t.Optional[str],329        filename: t.Optional[str],330    ):331        self._iter = iter(generator)332        self._pushed: "te.Deque[Token]" = deque()333        self.name = name334        self.filename = filename335        self.closed = False336        self.current = Token(1, TOKEN_INITIAL, "")337        next(self)338 339    def __iter__(self) -> TokenStreamIterator:340        return TokenStreamIterator(self)341 342    def __bool__(self) -> bool:343        return bool(self._pushed) or self.current.type is not TOKEN_EOF344 345    @property346    def eos(self) -> bool:347        """Are we at the end of the stream?"""348        return not self349 350    def push(self, token: Token) -> None:351        """Push a token back to the stream."""352        self._pushed.append(token)353 354    def look(self) -> Token:355        """Look at the next token."""356        old_token = next(self)357        result = self.current358        self.push(result)359        self.current = old_token360        return result361 362    def skip(self, n: int = 1) -> None:363        """Got n tokens ahead."""364        for _ in range(n):365            next(self)366 367    def next_if(self, expr: str) -> t.Optional[Token]:368        """Perform the token test and return the token if it matched.369        Otherwise the return value is `None`.370        """371        if self.current.test(expr):372            return next(self)373 374        return None375 376    def skip_if(self, expr: str) -> bool:377        """Like :meth:`next_if` but only returns `True` or `False`."""378        return self.next_if(expr) is not None379 380    def __next__(self) -> Token:381        """Go one token ahead and return the old one.382 383        Use the built-in :func:`next` instead of calling this directly.384        """385        rv = self.current386 387        if self._pushed:388            self.current = self._pushed.popleft()389        elif self.current.type is not TOKEN_EOF:390            try:391                self.current = next(self._iter)392            except StopIteration:393                self.close()394 395        return rv396 397    def close(self) -> None:398        """Close the stream."""399        self.current = Token(self.current.lineno, TOKEN_EOF, "")400        self._iter = iter(())401        self.closed = True402 403    def expect(self, expr: str) -> Token:404        """Expect a given token type and return it.  This accepts the same405        argument as :meth:`jinja2.lexer.Token.test`.406        """407        if not self.current.test(expr):408            expr = describe_token_expr(expr)409 410            if self.current.type is TOKEN_EOF:411                raise TemplateSyntaxError(412                    f"unexpected end of template, expected {expr!r}.",413                    self.current.lineno,414                    self.name,415                    self.filename,416                )417 418            raise TemplateSyntaxError(419                f"expected token {expr!r}, got {describe_token(self.current)!r}",420                self.current.lineno,421                self.name,422                self.filename,423            )424 425        return next(self)426 427 428def get_lexer(environment: "Environment") -> "Lexer":429    """Return a lexer which is probably cached."""430    key = (431        environment.block_start_string,432        environment.block_end_string,433        environment.variable_start_string,434        environment.variable_end_string,435        environment.comment_start_string,436        environment.comment_end_string,437        environment.line_statement_prefix,438        environment.line_comment_prefix,439        environment.trim_blocks,440        environment.lstrip_blocks,441        environment.newline_sequence,442        environment.keep_trailing_newline,443    )444    lexer = _lexer_cache.get(key)445 446    if lexer is None:447        _lexer_cache[key] = lexer = Lexer(environment)448 449    return lexer450 451 452class OptionalLStrip(tuple):  # type: ignore[type-arg]453    """A special tuple for marking a point in the state that can have454    lstrip applied.455    """456 457    __slots__ = ()458 459    # Even though it looks like a no-op, creating instances fails460    # without this.461    def __new__(cls, *members, **kwargs):  # type: ignore462        return super().__new__(cls, members)463 464 465class _Rule(t.NamedTuple):466    pattern: t.Pattern[str]467    tokens: t.Union[str, t.Tuple[str, ...], t.Tuple[Failure]]468    command: t.Optional[str]469 470 471class Lexer:472    """Class that implements a lexer for a given environment. Automatically473    created by the environment class, usually you don't have to do that.474 475    Note that the lexer is not automatically bound to an environment.476    Multiple environments can share the same lexer.477    """478 479    def __init__(self, environment: "Environment") -> None:480        # shortcuts481        e = re.escape482 483        def c(x: str) -> t.Pattern[str]:484            return re.compile(x, re.M | re.S)485 486        # lexing rules for tags487        tag_rules: t.List[_Rule] = [488            _Rule(whitespace_re, TOKEN_WHITESPACE, None),489            _Rule(float_re, TOKEN_FLOAT, None),490            _Rule(integer_re, TOKEN_INTEGER, None),491            _Rule(name_re, TOKEN_NAME, None),492            _Rule(string_re, TOKEN_STRING, None),493            _Rule(operator_re, TOKEN_OPERATOR, None),494        ]495 496        # assemble the root lexing rule. because "|" is ungreedy497        # we have to sort by length so that the lexer continues working498        # as expected when we have parsing rules like <% for block and499        # <%= for variables. (if someone wants asp like syntax)500        # variables are just part of the rules if variable processing501        # is required.502        root_tag_rules = compile_rules(environment)503 504        block_start_re = e(environment.block_start_string)505        block_end_re = e(environment.block_end_string)506        comment_end_re = e(environment.comment_end_string)507        variable_end_re = e(environment.variable_end_string)508 509        # block suffix if trimming is enabled510        block_suffix_re = "\\n?" if environment.trim_blocks else ""511 512        self.lstrip_blocks = environment.lstrip_blocks513 514        self.newline_sequence = environment.newline_sequence515        self.keep_trailing_newline = environment.keep_trailing_newline516 517        root_raw_re = (518            rf"(?P<raw_begin>{block_start_re}(\-|\+|)\s*raw\s*"519            rf"(?:\-{block_end_re}\s*|{block_end_re}))"520        )521        root_parts_re = "|".join(522            [root_raw_re] + [rf"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules]523        )524 525        # global lexing rules526        self.rules: t.Dict[str, t.List[_Rule]] = {527            "root": [528                # directives529                _Rule(530                    c(rf"(.*?)(?:{root_parts_re})"),531                    OptionalLStrip(TOKEN_DATA, "#bygroup"),  # type: ignore532                    "#bygroup",533                ),534                # data535                _Rule(c(".+"), TOKEN_DATA, None),536            ],537            # comments538            TOKEN_COMMENT_BEGIN: [539                _Rule(540                    c(541                        rf"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*"542                        rf"|{comment_end_re}{block_suffix_re}))"543                    ),544                    (TOKEN_COMMENT, TOKEN_COMMENT_END),545                    "#pop",546                ),547                _Rule(c(r"(.)"), (Failure("Missing end of comment tag"),), None),548            ],549            # blocks550            TOKEN_BLOCK_BEGIN: [551                _Rule(552                    c(553                        rf"(?:\+{block_end_re}|\-{block_end_re}\s*"554                        rf"|{block_end_re}{block_suffix_re})"555                    ),556                    TOKEN_BLOCK_END,557                    "#pop",558                ),559            ]560            + tag_rules,561            # variables562            TOKEN_VARIABLE_BEGIN: [563                _Rule(564                    c(rf"\-{variable_end_re}\s*|{variable_end_re}"),565                    TOKEN_VARIABLE_END,566                    "#pop",567                )568            ]569            + tag_rules,570            # raw block571            TOKEN_RAW_BEGIN: [572                _Rule(573                    c(574                        rf"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*"575                        rf"(?:\+{block_end_re}|\-{block_end_re}\s*"576                        rf"|{block_end_re}{block_suffix_re}))"577                    ),578                    OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END),  # type: ignore579                    "#pop",580                ),581                _Rule(c(r"(.)"), (Failure("Missing end of raw directive"),), None),582            ],583            # line statements584            TOKEN_LINESTATEMENT_BEGIN: [585                _Rule(c(r"\s*(\n|$)"), TOKEN_LINESTATEMENT_END, "#pop")586            ]587            + tag_rules,588            # line comments589            TOKEN_LINECOMMENT_BEGIN: [590                _Rule(591                    c(r"(.*?)()(?=\n|$)"),592                    (TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END),593                    "#pop",594                )595            ],596        }597 598    def _normalize_newlines(self, value: str) -> str:599        """Replace all newlines with the configured sequence in strings600        and template data.601        """602        return newline_re.sub(self.newline_sequence, value)603 604    def tokenize(605        self,606        source: str,607        name: t.Optional[str] = None,608        filename: t.Optional[str] = None,609        state: t.Optional[str] = None,610    ) -> TokenStream:611        """Calls tokeniter + tokenize and wraps it in a token stream."""612        stream = self.tokeniter(source, name, filename, state)613        return TokenStream(self.wrap(stream, name, filename), name, filename)614 615    def wrap(616        self,617        stream: t.Iterable[t.Tuple[int, str, str]],618        name: t.Optional[str] = None,619        filename: t.Optional[str] = None,620    ) -> t.Iterator[Token]:621        """This is called with the stream as returned by `tokenize` and wraps622        every token in a :class:`Token` and converts the value.623        """624        for lineno, token, value_str in stream:625            if token in ignored_tokens:626                continue627 628            value: t.Any = value_str629 630            if token == TOKEN_LINESTATEMENT_BEGIN:631                token = TOKEN_BLOCK_BEGIN632            elif token == TOKEN_LINESTATEMENT_END:633                token = TOKEN_BLOCK_END634            # we are not interested in those tokens in the parser635            elif token in (TOKEN_RAW_BEGIN, TOKEN_RAW_END):636                continue637            elif token == TOKEN_DATA:638                value = self._normalize_newlines(value_str)639            elif token == "keyword":640                token = value_str641            elif token == TOKEN_NAME:642                value = value_str643 644                if not value.isidentifier():645                    raise TemplateSyntaxError(646                        "Invalid character in identifier", lineno, name, filename647                    )648            elif token == TOKEN_STRING:649                # try to unescape string650                try:651                    value = (652                        self._normalize_newlines(value_str[1:-1])653                        .encode("ascii", "backslashreplace")654                        .decode("unicode-escape")655                    )656                except Exception as e:657                    msg = str(e).split(":")[-1].strip()658                    raise TemplateSyntaxError(msg, lineno, name, filename) from e659            elif token == TOKEN_INTEGER:660                value = int(value_str.replace("_", ""), 0)661            elif token == TOKEN_FLOAT:662                # remove all "_" first to support more Python versions663                value = literal_eval(value_str.replace("_", ""))664            elif token == TOKEN_OPERATOR:665                token = operators[value_str]666 667            yield Token(lineno, token, value)668 669    def tokeniter(670        self,671        source: str,672        name: t.Optional[str],673        filename: t.Optional[str] = None,674        state: t.Optional[str] = None,675    ) -> t.Iterator[t.Tuple[int, str, str]]:676        """This method tokenizes the text and returns the tokens in a677        generator. Use this method if you just want to tokenize a template.678 679        .. versionchanged:: 3.0680            Only ``\\n``, ``\\r\\n`` and ``\\r`` are treated as line681            breaks.682        """683        lines = newline_re.split(source)[::2]684 685        if not self.keep_trailing_newline and lines[-1] == "":686            del lines[-1]687 688        source = "\n".join(lines)689        pos = 0690        lineno = 1691        stack = ["root"]692 693        if state is not None and state != "root":694            assert state in ("variable", "block"), "invalid state"695            stack.append(state + "_begin")696 697        statetokens = self.rules[stack[-1]]698        source_length = len(source)699        balancing_stack: t.List[str] = []700        newlines_stripped = 0701        line_starting = True702 703        while True:704            # tokenizer loop705            for regex, tokens, new_state in statetokens:706                m = regex.match(source, pos)707 708                # if no match we try again with the next rule709                if m is None:710                    continue711 712                # we only match blocks and variables if braces / parentheses713                # are balanced. continue parsing with the lower rule which714                # is the operator rule. do this only if the end tags look715                # like operators716                if balancing_stack and tokens in (717                    TOKEN_VARIABLE_END,718                    TOKEN_BLOCK_END,719                    TOKEN_LINESTATEMENT_END,720                ):721                    continue722 723                # tuples support more options724                if isinstance(tokens, tuple):725                    groups: t.Sequence[str] = m.groups()726 727                    if isinstance(tokens, OptionalLStrip):728                        # Rule supports lstrip. Match will look like729                        # text, block type, whitespace control, type, control, ...730                        text = groups[0]731                        # Skipping the text and first type, every other group is the732                        # whitespace control for each type. One of the groups will be733                        # -, +, or empty string instead of None.734                        strip_sign = next(g for g in groups[2::2] if g is not None)735 736                        if strip_sign == "-":737                            # Strip all whitespace between the text and the tag.738                            stripped = text.rstrip()739                            newlines_stripped = text[len(stripped) :].count("\n")740                            groups = [stripped, *groups[1:]]741                        elif (742                            # Not marked for preserving whitespace.743                            strip_sign != "+"744                            # lstrip is enabled.745                            and self.lstrip_blocks746                            # Not a variable expression.747                            and not m.groupdict().get(TOKEN_VARIABLE_BEGIN)748                        ):749                            # The start of text between the last newline and the tag.750                            l_pos = text.rfind("\n") + 1751 752                            if l_pos > 0 or line_starting:753                                # If there's only whitespace between the newline and the754                                # tag, strip it.755                                if whitespace_re.fullmatch(text, l_pos):756                                    groups = [text[:l_pos], *groups[1:]]757 758                    for idx, token in enumerate(tokens):759                        # failure group760                        if token.__class__ is Failure:761                            raise token(lineno, filename)762                        # bygroup is a bit more complex, in that case we763                        # yield for the current token the first named764                        # group that matched765                        elif token == "#bygroup":766                            for key, value in m.groupdict().items():767                                if value is not None:768                                    yield lineno, key, value769                                    lineno += value.count("\n")770                                    break771                            else:772                                raise RuntimeError(773                                    f"{regex!r} wanted to resolve the token dynamically"774                                    " but no group matched"775                                )776                        # normal group777                        else:778                            data = groups[idx]779 780                            if data or token not in ignore_if_empty:781                                yield lineno, token, data782 783                            lineno += data.count("\n") + newlines_stripped784                            newlines_stripped = 0785 786                # strings as token just are yielded as it.787                else:788                    data = m.group()789 790                    # update brace/parentheses balance791                    if tokens == TOKEN_OPERATOR:792                        if data == "{":793                            balancing_stack.append("}")794                        elif data == "(":795                            balancing_stack.append(")")796                        elif data == "[":797                            balancing_stack.append("]")798                        elif data in ("}", ")", "]"):799                            if not balancing_stack:800                                raise TemplateSyntaxError(801                                    f"unexpected '{data}'", lineno, name, filename802                                )803 804                            expected_op = balancing_stack.pop()805 806                            if expected_op != data:807                                raise TemplateSyntaxError(808                                    f"unexpected '{data}', expected '{expected_op}'",809                                    lineno,810                                    name,811                                    filename,812                                )813 814                    # yield items815                    if data or tokens not in ignore_if_empty:816                        yield lineno, tokens, data817 818                    lineno += data.count("\n")819 820                line_starting = m.group()[-1:] == "\n"821                # fetch new position into new variable so that we can check822                # if there is a internal parsing error which would result823                # in an infinite loop824                pos2 = m.end()825 826                # handle state changes827                if new_state is not None:828                    # remove the uppermost state829                    if new_state == "#pop":830                        stack.pop()831                    # resolve the new state by group checking832                    elif new_state == "#bygroup":833                        for key, value in m.groupdict().items():834                            if value is not None:835                                stack.append(key)836                                break837                        else:838                            raise RuntimeError(839                                f"{regex!r} wanted to resolve the new state dynamically"840                                f" but no group matched"841                            )842                    # direct state name given843                    else:844                        stack.append(new_state)845 846                    statetokens = self.rules[stack[-1]]847                # we are still at the same position and no stack change.848                # this means a loop without break condition, avoid that and849                # raise error850                elif pos2 == pos:851                    raise RuntimeError(852                        f"{regex!r} yielded empty string without stack change"853                    )854 855                # publish new function and start again856                pos = pos2857                break858            # if loop terminated without break we haven't found a single match859            # either we are at the end of the file or we have a problem860            else:861                # end of text862                if pos >= source_length:863                    return864 865                # something went wrong866                raise TemplateSyntaxError(867                    f"unexpected char {source[pos]!r} at {pos}", lineno, name, filename868                )869 
codekingpro/portable-devtools · Team Ai