|
from django import template |
|
from django.conf import settings |
|
from django.template import Node, NodeList, TemplateSyntaxError, VariableDoesNotExist |
|
from django.template.base import FilterExpression, Parser, Token, token_kwargs |
|
|
|
|
|
register = template.Library() |
|
|
|
|
|
class DefineNode(Node): |
|
"""Store the component template (nodelist) in context.render_context['_components'].""" |
|
|
|
def __init__(self, name_expr: FilterExpression, nodelist): |
|
self.name_expr = name_expr |
|
self.nodelist = nodelist |
|
|
|
def render(self, context): |
|
# Resolve the name at runtime |
|
try: |
|
component_name = self.name_expr.resolve(context) |
|
except VariableDoesNotExist as exc: |
|
msg = f"The component name variable '{self.name_expr.token}' does not exist in the context." |
|
raise TemplateSyntaxError(msg) from exc |
|
|
|
if not isinstance(component_name, str) or not component_name: |
|
msg = "The component name must resolve to a non-empty string." |
|
raise TemplateSyntaxError(msg) |
|
|
|
with context.push(): |
|
if "_components" not in context.render_context: |
|
context.render_context["_components"] = {} |
|
context.render_context["_components"][component_name] = self.nodelist |
|
|
|
if settings.DEBUG: |
|
return f"<!-- Component <{component_name} /> defined -->" |
|
return "" |
|
|
|
|
|
def prepare_tag(name: str, parser: Parser, token: Token) -> tuple[NodeList, list[str]]: |
|
"""Prepare a tag for parsing by extracting the nodelist and tokens.""" |
|
nodelist = parser.parse((f"end{name}",)) |
|
parser.delete_first_token() |
|
tokens = token.split_contents() |
|
return nodelist, tokens |
|
|
|
|
|
@register.tag(name="define") |
|
def do_define(parser: Parser, token: Token): |
|
""" |
|
Define tag to store a component template in context.render_context['_components']. |
|
|
|
{% define "literal-name" %} |
|
...component body... |
|
{% enddefine %} |
|
""" |
|
nodelist, tokens = prepare_tag("define", parser, token) |
|
|
|
if len(tokens) < 2: # noqa: PLR2004 |
|
msg = "'define' tag requires a component name, either a quoted string or a variable." |
|
raise TemplateSyntaxError(msg) |
|
|
|
if getattr(parser, "define_active", False): |
|
msg = "Nested '{% define %}' blocks are not allowed." |
|
raise TemplateSyntaxError(msg) |
|
|
|
old_define_flag = getattr(parser, "define_active", False) |
|
parser.define_active = True |
|
|
|
try: |
|
name_expr = parser.compile_filter(tokens[1]) |
|
finally: |
|
parser.define_active = old_define_flag |
|
|
|
return DefineNode(name_expr, nodelist) |
|
|
|
|
|
class RenderNode(Node): |
|
"""Render a previously defined component by name.""" |
|
|
|
def __init__(self, name_expr: FilterExpression, props: dict, nodelist: NodeList): |
|
self.name_expr = name_expr |
|
self.props = props |
|
self.nodelist = nodelist |
|
|
|
def render(self, context): |
|
try: |
|
component_name = self.name_expr.resolve(context) |
|
except VariableDoesNotExist as exc: |
|
msg = f"The component name variable '{self.name_expr.token}' does not exist in the context." |
|
raise TemplateSyntaxError(msg) from exc |
|
|
|
if not isinstance(component_name, str) or not component_name: |
|
msg = "The component name must resolve to a non-empty string." |
|
raise TemplateSyntaxError(msg) |
|
|
|
components = context.render_context.get("_components", {}) |
|
component_nodelist = components.get(component_name) |
|
if not component_nodelist: |
|
return "" |
|
|
|
resolved_props = {} |
|
|
|
for key, filter_expr in self.props.items(): |
|
try: |
|
resolved_props[key] = filter_expr.resolve(context) |
|
except VariableDoesNotExist: |
|
resolved_props[key] = "" |
|
|
|
# The body inside {% render ... %} is "children" |
|
children_content = self.nodelist.render(context) |
|
|
|
with context.push(): |
|
context["props"] = resolved_props |
|
context["props"]["children"] = children_content |
|
out = component_nodelist.render(context) |
|
|
|
if settings.DEBUG: |
|
props = " ".join(f"{k}={v!r}" for k, v in resolved_props.items() if k != "children") |
|
return "\n".join( |
|
[ |
|
f"\n<!-- Render <{component_name} {props!s}> -->\n", |
|
out.strip(), |
|
f"\n<!-- </{component_name}> -->\n", |
|
] |
|
) |
|
return out |
|
|
|
|
|
@register.tag(name="render") |
|
def do_render(parser: Parser, token: Token): |
|
""" |
|
Render tag to render a previously defined component by name. |
|
|
|
{% render "literal-name" [with key="val" another=var] %} |
|
... (this becomes props.children) ... |
|
{% endrender %} |
|
""" |
|
nodelist, tokens = prepare_tag("render", parser, token) |
|
|
|
if len(tokens) < 2: # noqa: PLR2004 |
|
msg = "'render' tag requires a component name, either a quoted string or a variable." |
|
raise TemplateSyntaxError(msg) |
|
|
|
# bits[0] == 'render', not used directly |
|
tokens.pop(0) |
|
|
|
# bits[0] == component name, parse the component name as a FilterExpression |
|
name_expr = parser.compile_filter(tokens.pop(0)) |
|
|
|
props = {} |
|
if tokens and tokens[0] == "with": |
|
tokens.pop(0) # remove 'with' |
|
props = token_kwargs(tokens, parser, support_legacy=False) |
|
|
|
return RenderNode(name_expr, props, nodelist) |