about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/mako/lexer.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/mako/lexer.py')
-rw-r--r--.venv/lib/python3.12/site-packages/mako/lexer.py481
1 files changed, 481 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/mako/lexer.py b/.venv/lib/python3.12/site-packages/mako/lexer.py
new file mode 100644
index 00000000..37afb4de
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/mako/lexer.py
@@ -0,0 +1,481 @@
+# mako/lexer.py
+# Copyright 2006-2025 the Mako authors and contributors <see AUTHORS file>
+#
+# This module is part of Mako and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""provides the Lexer class for parsing template strings into parse trees."""
+
+import codecs
+import re
+
+from mako import exceptions
+from mako import parsetree
+from mako.pygen import adjust_whitespace
+
+_regexp_cache = {}
+
+
+class Lexer:
+    def __init__(
+        self, text, filename=None, input_encoding=None, preprocessor=None
+    ):
+        self.text = text
+        self.filename = filename
+        self.template = parsetree.TemplateNode(self.filename)
+        self.matched_lineno = 1
+        self.matched_charpos = 0
+        self.lineno = 1
+        self.match_position = 0
+        self.tag = []
+        self.control_line = []
+        self.ternary_stack = []
+        self.encoding = input_encoding
+
+        if preprocessor is None:
+            self.preprocessor = []
+        elif not hasattr(preprocessor, "__iter__"):
+            self.preprocessor = [preprocessor]
+        else:
+            self.preprocessor = preprocessor
+
+    @property
+    def exception_kwargs(self):
+        return {
+            "source": self.text,
+            "lineno": self.matched_lineno,
+            "pos": self.matched_charpos,
+            "filename": self.filename,
+        }
+
+    def match(self, regexp, flags=None):
+        """compile the given regexp, cache the reg, and call match_reg()."""
+
+        try:
+            reg = _regexp_cache[(regexp, flags)]
+        except KeyError:
+            reg = re.compile(regexp, flags) if flags else re.compile(regexp)
+            _regexp_cache[(regexp, flags)] = reg
+
+        return self.match_reg(reg)
+
+    def match_reg(self, reg):
+        """match the given regular expression object to the current text
+        position.
+
+        if a match occurs, update the current text and line position.
+
+        """
+
+        mp = self.match_position
+
+        match = reg.match(self.text, self.match_position)
+        if match:
+            (start, end) = match.span()
+            self.match_position = end + 1 if end == start else end
+            self.matched_lineno = self.lineno
+            cp = mp - 1
+            if cp >= 0 and cp < self.textlength:
+                cp = self.text[: cp + 1].rfind("\n")
+            self.matched_charpos = mp - cp
+            self.lineno += self.text[mp : self.match_position].count("\n")
+        return match
+
+    def parse_until_text(self, watch_nesting, *text):
+        startpos = self.match_position
+        text_re = r"|".join(text)
+        brace_level = 0
+        paren_level = 0
+        bracket_level = 0
+        while True:
+            match = self.match(r"#.*\n")
+            if match:
+                continue
+            match = self.match(
+                r"(\"\"\"|\'\'\'|\"|\')[^\\]*?(\\.[^\\]*?)*\1", re.S
+            )
+            if match:
+                continue
+            match = self.match(r"(%s)" % text_re)
+            if match and not (
+                watch_nesting
+                and (brace_level > 0 or paren_level > 0 or bracket_level > 0)
+            ):
+                return (
+                    self.text[
+                        startpos : self.match_position - len(match.group(1))
+                    ],
+                    match.group(1),
+                )
+            elif not match:
+                match = self.match(r"(.*?)(?=\"|\'|#|%s)" % text_re, re.S)
+            if match:
+                brace_level += match.group(1).count("{")
+                brace_level -= match.group(1).count("}")
+                paren_level += match.group(1).count("(")
+                paren_level -= match.group(1).count(")")
+                bracket_level += match.group(1).count("[")
+                bracket_level -= match.group(1).count("]")
+                continue
+            raise exceptions.SyntaxException(
+                "Expected: %s" % ",".join(text), **self.exception_kwargs
+            )
+
+    def append_node(self, nodecls, *args, **kwargs):
+        kwargs.setdefault("source", self.text)
+        kwargs.setdefault("lineno", self.matched_lineno)
+        kwargs.setdefault("pos", self.matched_charpos)
+        kwargs["filename"] = self.filename
+        node = nodecls(*args, **kwargs)
+        if len(self.tag):
+            self.tag[-1].nodes.append(node)
+        else:
+            self.template.nodes.append(node)
+        # build a set of child nodes for the control line
+        # (used for loop variable detection)
+        # also build a set of child nodes on ternary control lines
+        # (used for determining if a pass needs to be auto-inserted
+        if self.control_line:
+            control_frame = self.control_line[-1]
+            control_frame.nodes.append(node)
+            if (
+                not (
+                    isinstance(node, parsetree.ControlLine)
+                    and control_frame.is_ternary(node.keyword)
+                )
+                and self.ternary_stack
+                and self.ternary_stack[-1]
+            ):
+                self.ternary_stack[-1][-1].nodes.append(node)
+        if isinstance(node, parsetree.Tag):
+            if len(self.tag):
+                node.parent = self.tag[-1]
+            self.tag.append(node)
+        elif isinstance(node, parsetree.ControlLine):
+            if node.isend:
+                self.control_line.pop()
+                self.ternary_stack.pop()
+            elif node.is_primary:
+                self.control_line.append(node)
+                self.ternary_stack.append([])
+            elif self.control_line and self.control_line[-1].is_ternary(
+                node.keyword
+            ):
+                self.ternary_stack[-1].append(node)
+            elif self.control_line and not self.control_line[-1].is_ternary(
+                node.keyword
+            ):
+                raise exceptions.SyntaxException(
+                    "Keyword '%s' not a legal ternary for keyword '%s'"
+                    % (node.keyword, self.control_line[-1].keyword),
+                    **self.exception_kwargs,
+                )
+
+    _coding_re = re.compile(r"#.*coding[:=]\s*([-\w.]+).*\r?\n")
+
+    def decode_raw_stream(self, text, decode_raw, known_encoding, filename):
+        """given string/unicode or bytes/string, determine encoding
+        from magic encoding comment, return body as unicode
+        or raw if decode_raw=False
+
+        """
+        if isinstance(text, str):
+            m = self._coding_re.match(text)
+            encoding = m and m.group(1) or known_encoding or "utf-8"
+            return encoding, text
+
+        if text.startswith(codecs.BOM_UTF8):
+            text = text[len(codecs.BOM_UTF8) :]
+            parsed_encoding = "utf-8"
+            m = self._coding_re.match(text.decode("utf-8", "ignore"))
+            if m is not None and m.group(1) != "utf-8":
+                raise exceptions.CompileException(
+                    "Found utf-8 BOM in file, with conflicting "
+                    "magic encoding comment of '%s'" % m.group(1),
+                    text.decode("utf-8", "ignore"),
+                    0,
+                    0,
+                    filename,
+                )
+        else:
+            m = self._coding_re.match(text.decode("utf-8", "ignore"))
+            parsed_encoding = m.group(1) if m else known_encoding or "utf-8"
+        if decode_raw:
+            try:
+                text = text.decode(parsed_encoding)
+            except UnicodeDecodeError:
+                raise exceptions.CompileException(
+                    "Unicode decode operation of encoding '%s' failed"
+                    % parsed_encoding,
+                    text.decode("utf-8", "ignore"),
+                    0,
+                    0,
+                    filename,
+                )
+
+        return parsed_encoding, text
+
+    def parse(self):
+        self.encoding, self.text = self.decode_raw_stream(
+            self.text, True, self.encoding, self.filename
+        )
+
+        for preproc in self.preprocessor:
+            self.text = preproc(self.text)
+
+        # push the match marker past the
+        # encoding comment.
+        self.match_reg(self._coding_re)
+
+        self.textlength = len(self.text)
+
+        while True:
+            if self.match_position > self.textlength:
+                break
+
+            if self.match_end():
+                break
+            if self.match_expression():
+                continue
+            if self.match_control_line():
+                continue
+            if self.match_comment():
+                continue
+            if self.match_tag_start():
+                continue
+            if self.match_tag_end():
+                continue
+            if self.match_python_block():
+                continue
+            if self.match_percent():
+                continue
+            if self.match_text():
+                continue
+
+            if self.match_position > self.textlength:
+                break
+            # TODO: no coverage here
+            raise exceptions.MakoException("assertion failed")
+
+        if len(self.tag):
+            raise exceptions.SyntaxException(
+                "Unclosed tag: <%%%s>" % self.tag[-1].keyword,
+                **self.exception_kwargs,
+            )
+        if len(self.control_line):
+            raise exceptions.SyntaxException(
+                "Unterminated control keyword: '%s'"
+                % self.control_line[-1].keyword,
+                self.text,
+                self.control_line[-1].lineno,
+                self.control_line[-1].pos,
+                self.filename,
+            )
+        return self.template
+
+    def match_tag_start(self):
+        reg = r"""
+            \<%     # opening tag
+
+            ([\w\.\:]+)   # keyword
+
+            ((?:\s+\w+|\s*=\s*|"[^"]*?"|'[^']*?'|\s*,\s*)*)  # attrname, = \
+                                               #        sign, string expression
+                                               # comma is for backwards compat
+                                               # identified in #366
+
+            \s*     # more whitespace
+
+            (/)?>   # closing
+
+        """
+
+        match = self.match(
+            reg,
+            re.I | re.S | re.X,
+        )
+
+        if not match:
+            return False
+
+        keyword, attr, isend = match.groups()
+        self.keyword = keyword
+        attributes = {}
+        if attr:
+            for att in re.findall(
+                r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr
+            ):
+                key, val1, val2 = att
+                text = val1 or val2
+                text = text.replace("\r\n", "\n")
+                attributes[key] = text
+        self.append_node(parsetree.Tag, keyword, attributes)
+        if isend:
+            self.tag.pop()
+        elif keyword == "text":
+            match = self.match(r"(.*?)(?=\</%text>)", re.S)
+            if not match:
+                raise exceptions.SyntaxException(
+                    "Unclosed tag: <%%%s>" % self.tag[-1].keyword,
+                    **self.exception_kwargs,
+                )
+            self.append_node(parsetree.Text, match.group(1))
+            return self.match_tag_end()
+        return True
+
+    def match_tag_end(self):
+        match = self.match(r"\</%[\t ]*([^\t ]+?)[\t ]*>")
+        if match:
+            if not len(self.tag):
+                raise exceptions.SyntaxException(
+                    "Closing tag without opening tag: </%%%s>"
+                    % match.group(1),
+                    **self.exception_kwargs,
+                )
+            elif self.tag[-1].keyword != match.group(1):
+                raise exceptions.SyntaxException(
+                    "Closing tag </%%%s> does not match tag: <%%%s>"
+                    % (match.group(1), self.tag[-1].keyword),
+                    **self.exception_kwargs,
+                )
+            self.tag.pop()
+            return True
+        else:
+            return False
+
+    def match_end(self):
+        match = self.match(r"\Z", re.S)
+        if not match:
+            return False
+
+        string = match.group()
+        if string:
+            return string
+        else:
+            return True
+
+    def match_percent(self):
+        match = self.match(r"(?<=^)(\s*)%%(%*)", re.M)
+        if match:
+            self.append_node(
+                parsetree.Text, match.group(1) + "%" + match.group(2)
+            )
+            return True
+        else:
+            return False
+
+    def match_text(self):
+        match = self.match(
+            r"""
+                (.*?)         # anything, followed by:
+                (
+                 (?<=\n)(?=[ \t]*(?=%|\#\#))  # an eval or line-based
+                                            # comment, preceded by a
+                                            # consumed newline and whitespace
+                 |
+                 (?=\${)      # an expression
+                 |
+                 (?=</?%)  # a substitution or block or call start or end
+                              # - don't consume
+                 |
+                 (\\\r?\n)    # an escaped newline  - throw away
+                 |
+                 \Z           # end of string
+                )""",
+            re.X | re.S,
+        )
+
+        if match:
+            text = match.group(1)
+            if text:
+                self.append_node(parsetree.Text, text)
+            return True
+        else:
+            return False
+
+    def match_python_block(self):
+        match = self.match(r"<%(!)?")
+        if match:
+            line, pos = self.matched_lineno, self.matched_charpos
+            text, end = self.parse_until_text(False, r"%>")
+            # the trailing newline helps
+            # compiler.parse() not complain about indentation
+            text = adjust_whitespace(text) + "\n"
+            self.append_node(
+                parsetree.Code,
+                text,
+                match.group(1) == "!",
+                lineno=line,
+                pos=pos,
+            )
+            return True
+        else:
+            return False
+
+    def match_expression(self):
+        match = self.match(r"\${")
+        if not match:
+            return False
+
+        line, pos = self.matched_lineno, self.matched_charpos
+        text, end = self.parse_until_text(True, r"\|", r"}")
+        if end == "|":
+            escapes, end = self.parse_until_text(True, r"}")
+        else:
+            escapes = ""
+        text = text.replace("\r\n", "\n")
+        self.append_node(
+            parsetree.Expression,
+            text,
+            escapes.strip(),
+            lineno=line,
+            pos=pos,
+        )
+        return True
+
+    def match_control_line(self):
+        match = self.match(
+            r"(?<=^)[\t ]*(%(?!%)|##)[\t ]*((?:(?:\\\r?\n)|[^\r\n])*)"
+            r"(?:\r?\n|\Z)",
+            re.M,
+        )
+        if not match:
+            return False
+
+        operator = match.group(1)
+        text = match.group(2)
+        if operator == "%":
+            m2 = re.match(r"(end)?(\w+)\s*(.*)", text)
+            if not m2:
+                raise exceptions.SyntaxException(
+                    "Invalid control line: '%s'" % text,
+                    **self.exception_kwargs,
+                )
+            isend, keyword = m2.group(1, 2)
+            isend = isend is not None
+
+            if isend:
+                if not len(self.control_line):
+                    raise exceptions.SyntaxException(
+                        "No starting keyword '%s' for '%s'" % (keyword, text),
+                        **self.exception_kwargs,
+                    )
+                elif self.control_line[-1].keyword != keyword:
+                    raise exceptions.SyntaxException(
+                        "Keyword '%s' doesn't match keyword '%s'"
+                        % (text, self.control_line[-1].keyword),
+                        **self.exception_kwargs,
+                    )
+            self.append_node(parsetree.ControlLine, keyword, isend, text)
+        else:
+            self.append_node(parsetree.Comment, text)
+        return True
+
+    def match_comment(self):
+        """matches the multiline version of a comment"""
+        match = self.match(r"<%doc>(.*?)</%doc>", re.S)
+        if match:
+            self.append_node(parsetree.Comment, match.group(1))
+            return True
+        else:
+            return False