Skip to content

SOURCE CODE liquid.tags.wild DOCS

"""Provides tags for wild mode"""
import textwrap
from contextlib import redirect_stdout
from io import StringIO
from typing import TYPE_CHECKING, List, Union

from jinja2 import nodes
from jinja2.exceptions import TemplateSyntaxError
from jinja2.lexer import TOKEN_BLOCK_END

try:
    from jinja2 import pass_environment
except ImportError:
    from jinja2 import environmentfilter as pass_environment

from .manager import TagManager, decode_raw
from .standard import assign, capture, case, comment, cycle

if TYPE_CHECKING:
    from jinja2.lexer import Token
    from jinja2.parser import Parser
    from jinja2.environment import Environment


wild_tags = TagManager()

wild_tags.register(comment, raw=True)
wild_tags.register(case)
wild_tags.register(capture)
wild_tags.register(assign)
wild_tags.register(cycle)


@wild_tags.register(raw=True, env=True)DOCS
def python(env: "Environment", token: "Token", parser: "Parser") -> nodes.Node:
    """The python tag

    {% python %} ... {% endpython %} or
    {% python ... %}

    The globals from the enviornment will be used to evaluate the code
    It also affect the globals from the environment

    Args:
        env: The environment
        token: The token matches the tag name
        parser: The parser

    Returns:
        The parsed node
    """
    if parser.stream.current.type is TOKEN_BLOCK_END:
        # expect {% endpython %}
        body = parser.parse_statements(("name:endpython",), drop_needle=True)
        body = decode_raw(body[0].nodes[0].data)
        body_parts = body.split("\n", 1)
        if not body_parts[0]:
            body = "" if len(body_parts) < 2 else body_parts[1]
        body = textwrap.dedent(body)
    else:
        pieces: List[str] = []
        pieces_append = pieces.append
        while True:
            token = next(parser.stream)
            pieces_append(str(token.value))
            if parser.stream.current.type is TOKEN_BLOCK_END:
                break

        body = " ".join(pieces)

    code = compile(body, "<liquid-python-tag>", mode="exec")
    out = StringIO()
    with redirect_stdout(out):
        exec(code, env.globals)
    return nodes.Output([nodes.Const(out.getvalue())], lineno=token.lineno)


@wild_tags.register(env=True)DOCS
def import_(
    env: "Environment", token: "Token", parser: "Parser"
) -> nodes.Node:
    """The import_ tag {% import_ ... %}

    Name it 'import_' so the 'import' tag from jinja can still work

    Args:
        env: The environment
        token: The token matches tag name
        parser: The parser

    Returns:
        The parsed node
    """
    pieces = ["import"]
    pieces_append = pieces.append
    while True:
        token = next(parser.stream)
        pieces_append(str(token.value))
        if parser.stream.current.type is TOKEN_BLOCK_END:
            break
    body = " ".join(pieces)
    code = compile(body, "<liquid-import_-tag>", mode="exec")
    exec(code, env.globals)
    return nodes.Output([], lineno=token.lineno)


@wild_tags.register(env=True)DOCS
def from_(env: "Environment", token: "Token", parser: "Parser") -> nodes.Node:
    """The from_ tag {% from_ ... %}

    Name it 'from_' so the 'from_' tag from jinja can still work

    Args:
        env: The environment
        token: The token matches tag name
        parser: The parser

    Returns:
        The parsed node
    """
    pieces = ["from"]
    pieces_append = pieces.append
    while True:
        token = next(parser.stream)
        pieces_append(str(token.value))
        if parser.stream.current.type is TOKEN_BLOCK_END:
            break
    body = " ".join(pieces)
    code = compile(body, "<liquid-from_-tag>", mode="exec")
    exec(code, env.globals)
    return nodes.Output([], lineno=token.lineno)


@wild_tags.register(env=True, raw=True)DOCS
def addfilter(
    env: "Environment", token: "Token", parser: "Parser"
) -> nodes.Node:
    """The addfilter tag {% addfilter name ... %} ... {% endaddfilter %}

    This allows one to use the python code inside the body to add a filter or
    replace an existing filter

    Args:
        env: The environment
        token: The token matches tag name
        parser: The parser

    Returns:
        The parsed node
    """
    token = parser.stream.expect("name")
    filtername = token.value

    pass_env: Union[bool, Token]
    if parser.stream.current.type is TOKEN_BLOCK_END:
        # no pass_environment
        pass_env = False
    else:
        pass_env = parser.stream.expect("name:pass_env")

    body = parser.parse_statements(("name:endaddfilter",), drop_needle=True)
    body = decode_raw(body[0].nodes[0].data)
    body_parts = body.split("\n", 1)
    if not body_parts[0]:
        body = "" if len(body_parts) < 2 else body_parts[1]
    body = textwrap.dedent(body)

    globs = env.globals.copy()
    code = compile(body, "<liquid-addfilter-tag>", mode="exec")
    exec(code, globs)
    try:
        filterfunc = globs[filtername]
    except KeyError:
        raise TemplateSyntaxError(
            f"No such filter defined in 'addfilter': {filtername}",
            token.lineno,
        ) from None

    if pass_env:
        filterfunc = pass_environment(filterfunc)  # type: ignore
    env.filters[filtername] = filterfunc

    return nodes.Output([], lineno=token.lineno)