Skip to content

SOURCE CODE liquid.exts.ext DOCS

"""Provides a base extension class"""
import re
from base64 import b64encode
from typing import TYPE_CHECKING

from jinja2 import nodes
from jinja2.ext import Extension

if TYPE_CHECKING:
    from jinja2.parser import Parser


re_e = re.escape
re_c = lambda rex: re.compile(rex, re.DOTALL | re.MULTILINE)

# A unique id to encode the start strings
ENCODING_ID = id(Extension)


class LiquidExtension(Extension):DOCS
    """A base extension class for extensions in this package to extend"""

    def __init_subclass__(cls) -> None:DOCS
        """Initalize the tags and raw_tags using tag manager"""
        cls.tags = cls.tag_manager.names
        cls.raw_tags = cls.tag_manager.names_raw

    def preprocess(  # type: ignoreDOCS
        self,
        source: str,
        name: str,
        filename: str,
    ) -> str:
        """Try to keep the tag body raw by encode the variable/comment/block
        start strings ('{{', '{#', '{%') so that the body won't be tokenized
        by jinjia.
        """
        # Turn
        # "  {{* ... }}" to
        # "  {{* ... | indent(2) }}"
        # to keep the indent for multiline variables
        variable_start_re = re_e(self.environment.variable_start_string)
        variable_end_re = re_e(self.environment.variable_end_string)
        indent_re = re_c(
            fr"^([ \t]+){variable_start_re}\*"
            "(.*?)"
            fr"(\-{variable_end_re}|\+{variable_end_re}|{variable_end_re})"
        )
        source = indent_re.sub(
            lambda m: (
                f"{m.group(1)}{self.environment.variable_start_string}"
                f"{m.group(2)} | indent({m.group(1)!r}){m.group(3)}"
            ),
            source,
        )

        if not self.__class__.raw_tags:  # pragma: no cover
            return super().preprocess(source, name, filename=filename)

        block_start_re = re_e(self.environment.block_start_string)
        block_end_re = re_e(self.environment.block_end_string)
        comment_start_re = re_e(self.environment.comment_start_string)
        to_encode = re_c(
            f"({block_start_re}|{variable_start_re}|{comment_start_re})"
        )

        def encode_raw(matched):
            content = to_encode.sub(
                lambda m: (
                    f"$${ENCODING_ID}$"
                    f"{b64encode(m.group(1).encode()).decode()}$$"
                ),
                matched.group(2),
            )
            return f"{matched.group(1)}{content}{matched.group(3)}"

        for raw_tag in self.__class__.raw_tags:
            tag_re = re_c(
                # {% comment "//"
                fr"({block_start_re}(?:\-|\+|)\s*{raw_tag}\s*.*?"
                # %}
                fr"(?:\-{block_end_re}|\+{block_end_re}|{block_end_re}))"
                # ...
                fr"(.*?)"
                # {% endcomment
                fr"({block_start_re}(?:\-|\+|)\s*end{raw_tag}\s*"
                fr"(?:\-{block_end_re}|\+{block_end_re}|{block_end_re}))"
            )
            source = tag_re.sub(encode_raw, source)
        return source

    def parse(self, parser: "Parser") -> nodes.Node:DOCS
        """Let tag manager to parse the tags that are being listened to"""
        token = next(parser.stream)
        return self.__class__.tag_manager.parse(
            self.environment, token, parser
        )