353 lines
13 KiB
Python
353 lines
13 KiB
Python
import posixpath
|
|
from collections import defaultdict
|
|
|
|
from django.utils.safestring import mark_safe
|
|
|
|
from .base import Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs
|
|
from .library import Library
|
|
|
|
register = Library()
|
|
|
|
BLOCK_CONTEXT_KEY = "block_context"
|
|
|
|
|
|
class BlockContext:
|
|
def __init__(self):
|
|
# Dictionary of FIFO queues.
|
|
self.blocks = defaultdict(list)
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__qualname__}: blocks={self.blocks!r}>"
|
|
|
|
def add_blocks(self, blocks):
|
|
for name, block in blocks.items():
|
|
self.blocks[name].insert(0, block)
|
|
|
|
def pop(self, name):
|
|
try:
|
|
return self.blocks[name].pop()
|
|
except IndexError:
|
|
return None
|
|
|
|
def push(self, name, block):
|
|
self.blocks[name].append(block)
|
|
|
|
def get_block(self, name):
|
|
try:
|
|
return self.blocks[name][-1]
|
|
except IndexError:
|
|
return None
|
|
|
|
|
|
class BlockNode(Node):
|
|
def __init__(self, name, nodelist, parent=None):
|
|
self.name, self.nodelist, self.parent = name, nodelist, parent
|
|
|
|
def __repr__(self):
|
|
return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
|
|
|
|
def render(self, context):
|
|
block_context = context.render_context.get(BLOCK_CONTEXT_KEY)
|
|
with context.push():
|
|
if block_context is None:
|
|
context["block"] = self
|
|
result = self.nodelist.render(context)
|
|
else:
|
|
push = block = block_context.pop(self.name)
|
|
if block is None:
|
|
block = self
|
|
# Create new block so we can store context without thread-safety issues.
|
|
block = type(self)(block.name, block.nodelist)
|
|
block.context = context
|
|
context["block"] = block
|
|
result = block.nodelist.render(context)
|
|
if push is not None:
|
|
block_context.push(self.name, push)
|
|
return result
|
|
|
|
def super(self):
|
|
if not hasattr(self, "context"):
|
|
raise TemplateSyntaxError(
|
|
"'%s' object has no attribute 'context'. Did you use "
|
|
"{{ block.super }} in a base template?" % self.__class__.__name__
|
|
)
|
|
render_context = self.context.render_context
|
|
if (
|
|
BLOCK_CONTEXT_KEY in render_context
|
|
and render_context[BLOCK_CONTEXT_KEY].get_block(self.name) is not None
|
|
):
|
|
return mark_safe(self.render(self.context))
|
|
return ""
|
|
|
|
|
|
class ExtendsNode(Node):
|
|
must_be_first = True
|
|
context_key = "extends_context"
|
|
|
|
def __init__(self, nodelist, parent_name, template_dirs=None):
|
|
self.nodelist = nodelist
|
|
self.parent_name = parent_name
|
|
self.template_dirs = template_dirs
|
|
self.blocks = {n.name: n for n in nodelist.get_nodes_by_type(BlockNode)}
|
|
|
|
def __repr__(self):
|
|
return "<%s: extends %s>" % (self.__class__.__name__, self.parent_name.token)
|
|
|
|
def find_template(self, template_name, context):
|
|
"""
|
|
This is a wrapper around engine.find_template(). A history is kept in
|
|
the render_context attribute between successive extends calls and
|
|
passed as the skip argument. This enables extends to work recursively
|
|
without extending the same template twice.
|
|
"""
|
|
history = context.render_context.setdefault(
|
|
self.context_key,
|
|
[self.origin],
|
|
)
|
|
template, origin = context.template.engine.find_template(
|
|
template_name,
|
|
skip=history,
|
|
)
|
|
history.append(origin)
|
|
return template
|
|
|
|
def get_parent(self, context):
|
|
parent = self.parent_name.resolve(context)
|
|
if not parent:
|
|
error_msg = "Invalid template name in 'extends' tag: %r." % parent
|
|
if self.parent_name.filters or isinstance(self.parent_name.var, Variable):
|
|
error_msg += (
|
|
" Got this from the '%s' variable." % self.parent_name.token
|
|
)
|
|
raise TemplateSyntaxError(error_msg)
|
|
if isinstance(parent, Template):
|
|
# parent is a django.template.Template
|
|
return parent
|
|
if isinstance(getattr(parent, "template", None), Template):
|
|
# parent is a django.template.backends.django.Template
|
|
return parent.template
|
|
return self.find_template(parent, context)
|
|
|
|
def render(self, context):
|
|
compiled_parent = self.get_parent(context)
|
|
|
|
if BLOCK_CONTEXT_KEY not in context.render_context:
|
|
context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
|
|
block_context = context.render_context[BLOCK_CONTEXT_KEY]
|
|
|
|
# Add the block nodes from this node to the block context
|
|
block_context.add_blocks(self.blocks)
|
|
|
|
# If this block's parent doesn't have an extends node it is the root,
|
|
# and its block nodes also need to be added to the block context.
|
|
for node in compiled_parent.nodelist:
|
|
# The ExtendsNode has to be the first non-text node.
|
|
if not isinstance(node, TextNode):
|
|
if not isinstance(node, ExtendsNode):
|
|
blocks = {
|
|
n.name: n
|
|
for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)
|
|
}
|
|
block_context.add_blocks(blocks)
|
|
break
|
|
|
|
# Call Template._render explicitly so the parser context stays
|
|
# the same.
|
|
with context.render_context.push_state(compiled_parent, isolated_context=False):
|
|
return compiled_parent._render(context)
|
|
|
|
|
|
class IncludeNode(Node):
|
|
context_key = "__include_context"
|
|
|
|
def __init__(
|
|
self, template, *args, extra_context=None, isolated_context=False, **kwargs
|
|
):
|
|
self.template = template
|
|
self.extra_context = extra_context or {}
|
|
self.isolated_context = isolated_context
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__qualname__}: template={self.template!r}>"
|
|
|
|
def render(self, context):
|
|
"""
|
|
Render the specified template and context. Cache the template object
|
|
in render_context to avoid reparsing and loading when used in a for
|
|
loop.
|
|
"""
|
|
template = self.template.resolve(context)
|
|
# Does this quack like a Template?
|
|
if not callable(getattr(template, "render", None)):
|
|
# If not, try the cache and select_template().
|
|
template_name = template or ()
|
|
if isinstance(template_name, str):
|
|
template_name = (
|
|
construct_relative_path(
|
|
self.origin.template_name,
|
|
template_name,
|
|
),
|
|
)
|
|
else:
|
|
template_name = tuple(template_name)
|
|
cache = context.render_context.dicts[0].setdefault(self, {})
|
|
template = cache.get(template_name)
|
|
if template is None:
|
|
template = context.template.engine.select_template(template_name)
|
|
cache[template_name] = template
|
|
# Use the base.Template of a backends.django.Template.
|
|
elif hasattr(template, "template"):
|
|
template = template.template
|
|
values = {
|
|
name: var.resolve(context) for name, var in self.extra_context.items()
|
|
}
|
|
if self.isolated_context:
|
|
return template.render(context.new(values))
|
|
with context.push(**values):
|
|
return template.render(context)
|
|
|
|
|
|
@register.tag("block")
|
|
def do_block(parser, token):
|
|
"""
|
|
Define a block that can be overridden by child templates.
|
|
"""
|
|
# token.split_contents() isn't useful here because this tag doesn't accept
|
|
# variable as arguments.
|
|
bits = token.contents.split()
|
|
if len(bits) != 2:
|
|
raise TemplateSyntaxError("'%s' tag takes only one argument" % bits[0])
|
|
block_name = bits[1]
|
|
# Keep track of the names of BlockNodes found in this template, so we can
|
|
# check for duplication.
|
|
try:
|
|
if block_name in parser.__loaded_blocks:
|
|
raise TemplateSyntaxError(
|
|
"'%s' tag with name '%s' appears more than once" % (bits[0], block_name)
|
|
)
|
|
parser.__loaded_blocks.append(block_name)
|
|
except AttributeError: # parser.__loaded_blocks isn't a list yet
|
|
parser.__loaded_blocks = [block_name]
|
|
nodelist = parser.parse(("endblock",))
|
|
|
|
# This check is kept for backwards-compatibility. See #3100.
|
|
endblock = parser.next_token()
|
|
acceptable_endblocks = ("endblock", "endblock %s" % block_name)
|
|
if endblock.contents not in acceptable_endblocks:
|
|
parser.invalid_block_tag(endblock, "endblock", acceptable_endblocks)
|
|
|
|
return BlockNode(block_name, nodelist)
|
|
|
|
|
|
def construct_relative_path(current_template_name, relative_name):
|
|
"""
|
|
Convert a relative path (starting with './' or '../') to the full template
|
|
name based on the current_template_name.
|
|
"""
|
|
new_name = relative_name.strip("'\"")
|
|
if not new_name.startswith(("./", "../")):
|
|
# relative_name is a variable or a literal that doesn't contain a
|
|
# relative path.
|
|
return relative_name
|
|
|
|
new_name = posixpath.normpath(
|
|
posixpath.join(
|
|
posixpath.dirname(current_template_name.lstrip("/")),
|
|
new_name,
|
|
)
|
|
)
|
|
if new_name.startswith("../"):
|
|
raise TemplateSyntaxError(
|
|
"The relative path '%s' points outside the file hierarchy that "
|
|
"template '%s' is in." % (relative_name, current_template_name)
|
|
)
|
|
if current_template_name.lstrip("/") == new_name:
|
|
raise TemplateSyntaxError(
|
|
"The relative path '%s' was translated to template name '%s', the "
|
|
"same template in which the tag appears."
|
|
% (relative_name, current_template_name)
|
|
)
|
|
has_quotes = (
|
|
relative_name.startswith(('"', "'")) and relative_name[0] == relative_name[-1]
|
|
)
|
|
return f'"{new_name}"' if has_quotes else new_name
|
|
|
|
|
|
@register.tag("extends")
|
|
def do_extends(parser, token):
|
|
"""
|
|
Signal that this template extends a parent template.
|
|
|
|
This tag may be used in two ways: ``{% extends "base" %}`` (with quotes)
|
|
uses the literal value "base" as the name of the parent template to extend,
|
|
or ``{% extends variable %}`` uses the value of ``variable`` as either the
|
|
name of the parent template to extend (if it evaluates to a string) or as
|
|
the parent template itself (if it evaluates to a Template object).
|
|
"""
|
|
bits = token.split_contents()
|
|
if len(bits) != 2:
|
|
raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
|
|
bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
|
|
parent_name = parser.compile_filter(bits[1])
|
|
nodelist = parser.parse()
|
|
if nodelist.get_nodes_by_type(ExtendsNode):
|
|
raise TemplateSyntaxError(
|
|
"'%s' cannot appear more than once in the same template" % bits[0]
|
|
)
|
|
return ExtendsNode(nodelist, parent_name)
|
|
|
|
|
|
@register.tag("include")
|
|
def do_include(parser, token):
|
|
"""
|
|
Load a template and render it with the current context. You can pass
|
|
additional context using keyword arguments.
|
|
|
|
Example::
|
|
|
|
{% include "foo/some_include" %}
|
|
{% include "foo/some_include" with bar="BAZZ!" baz="BING!" %}
|
|
|
|
Use the ``only`` argument to exclude the current context when rendering
|
|
the included template::
|
|
|
|
{% include "foo/some_include" only %}
|
|
{% include "foo/some_include" with bar="1" only %}
|
|
"""
|
|
bits = token.split_contents()
|
|
if len(bits) < 2:
|
|
raise TemplateSyntaxError(
|
|
"%r tag takes at least one argument: the name of the template to "
|
|
"be included." % bits[0]
|
|
)
|
|
options = {}
|
|
remaining_bits = bits[2:]
|
|
while remaining_bits:
|
|
option = remaining_bits.pop(0)
|
|
if option in options:
|
|
raise TemplateSyntaxError(
|
|
"The %r option was specified more than once." % option
|
|
)
|
|
if option == "with":
|
|
value = token_kwargs(remaining_bits, parser, support_legacy=False)
|
|
if not value:
|
|
raise TemplateSyntaxError(
|
|
'"with" in %r tag needs at least one keyword argument.' % bits[0]
|
|
)
|
|
elif option == "only":
|
|
value = True
|
|
else:
|
|
raise TemplateSyntaxError(
|
|
"Unknown argument for %r tag: %r." % (bits[0], option)
|
|
)
|
|
options[option] = value
|
|
isolated_context = options.get("only", False)
|
|
namemap = options.get("with", {})
|
|
bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
|
|
return IncludeNode(
|
|
parser.compile_filter(bits[1]),
|
|
extra_context=namemap,
|
|
isolated_context=isolated_context,
|
|
)
|