|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- 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 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 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 get_template().
- template_name = template
- cache = context.render_context.dicts[0].setdefault(self, {})
- template = cache.get(template_name)
- if template is None:
- template = context.template.engine.get_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.
- """
- if not relative_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('/')),
- relative_name.strip('\'"')
- )
- )
- 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)
- )
- return '"%s"' % 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)
|