summaryrefslogtreecommitdiff
path: root/django/template
diff options
context:
space:
mode:
Diffstat (limited to 'django/template')
-rw-r--r--django/template/__init__.py1
-rw-r--r--django/template/base.py56
-rw-r--r--django/template/defaulttags.py96
-rw-r--r--django/template/engine.py22
4 files changed, 174 insertions, 1 deletions
diff --git a/django/template/__init__.py b/django/template/__init__.py
index adb431c00d..92568da793 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -61,6 +61,7 @@ from .base import ( # NOQA isort:skip
Node,
NodeList,
Origin,
+ PartialTemplate,
Template,
Variable,
)
diff --git a/django/template/base.py b/django/template/base.py
index 3e8a59fbe7..74b3987410 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -88,6 +88,11 @@ UNKNOWN_SOURCE = "<unknown source>"
# than instantiating SimpleLazyObject with _lazy_re_compile().
tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")
+combined_partial_re = re.compile(
+ r"{%\s*partialdef\s+(?P<name>[\w-]+)(?:\s+inline)?\s*%}"
+ r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}"
+)
+
logger = logging.getLogger("django.template")
@@ -288,6 +293,57 @@ class Template:
}
+class PartialTemplate:
+ """
+ A lightweight Template lookalike used for template partials.
+
+ Wraps nodelist as a partial, in order to be able to bind context.
+ """
+
+ def __init__(self, nodelist, origin, name):
+ self.nodelist = nodelist
+ self.origin = origin
+ self.name = name
+
+ def get_exception_info(self, exception, token):
+ template = self.origin.loader.get_template(self.origin.template_name)
+ return template.get_exception_info(exception, token)
+
+ def find_partial_source(self, full_source, partial_name):
+ start_match = None
+ nesting = 0
+
+ for match in combined_partial_re.finditer(full_source):
+ if name := match["name"]: # Opening tag.
+ if start_match is None and name == partial_name:
+ start_match = match
+ if start_match is not None:
+ nesting += 1
+ elif start_match is not None:
+ nesting -= 1
+ if nesting == 0:
+ return full_source[start_match.start() : match.end()]
+
+ return ""
+
+ @property
+ def source(self):
+ template = self.origin.loader.get_template(self.origin.template_name)
+ return self.find_partial_source(template.source, self.name)
+
+ def _render(self, context):
+ return self.nodelist.render(context)
+
+ def render(self, context):
+ with context.render_context.push_state(self):
+ if context.template is None:
+ with context.bind_template(self):
+ context.template_name = self.name
+ return self._render(context)
+ else:
+ return self._render(context)
+
+
def linebreak_iter(template_source):
yield 0
p = template_source.find("\n")
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index a20598152c..4cbaf852e1 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -12,6 +12,7 @@ from itertools import groupby
from django.conf import settings
from django.http import QueryDict
from django.utils import timezone
+from django.utils.datastructures import DeferredSubDict
from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words
from django.utils.safestring import mark_safe
@@ -29,6 +30,7 @@ from .base import (
VARIABLE_TAG_START,
Node,
NodeList,
+ PartialTemplate,
TemplateSyntaxError,
VariableDoesNotExist,
kwarg_re,
@@ -408,6 +410,31 @@ class NowNode(Node):
return formatted
+class PartialDefNode(Node):
+ def __init__(self, partial_name, inline, nodelist):
+ self.partial_name = partial_name
+ self.inline = inline
+ self.nodelist = nodelist
+
+ def render(self, context):
+ return self.nodelist.render(context) if self.inline else ""
+
+
+class PartialNode(Node):
+ def __init__(self, partial_name, partial_mapping):
+ # Defer lookup in `partial_mapping` and nodelist to runtime.
+ self.partial_name = partial_name
+ self.partial_mapping = partial_mapping
+
+ def render(self, context):
+ try:
+ return self.partial_mapping[self.partial_name].render(context)
+ except KeyError:
+ raise TemplateSyntaxError(
+ f"Partial '{self.partial_name}' is not defined in the current template."
+ )
+
+
class ResetCycleNode(Node):
def __init__(self, node):
self.node = node
@@ -1174,6 +1201,75 @@ def now(parser, token):
return NowNode(format_string, asvar)
+@register.tag(name="partialdef")
+def partialdef_func(parser, token):
+ """
+ Declare a partial that can be used in the template.
+
+ Usage::
+
+ {% partialdef partial_name %}
+ Content goes here.
+ {% endpartialdef %}
+
+ Store the nodelist in the context under the key "partials". It can be
+ retrieved using the ``{% partial %}`` tag.
+
+ The optional ``inline`` argument renders the partial's contents
+ immediately, at the point where it is defined.
+ """
+ match token.split_contents():
+ case "partialdef", partial_name, "inline":
+ inline = True
+ case "partialdef", partial_name, _:
+ raise TemplateSyntaxError(
+ "The 'inline' argument does not have any parameters; either use "
+ "'inline' or remove it completely."
+ )
+ case "partialdef", partial_name:
+ inline = False
+ case ["partialdef"]:
+ raise TemplateSyntaxError("'partialdef' tag requires a name")
+ case _:
+ raise TemplateSyntaxError("'partialdef' tag takes at most 2 arguments")
+
+ # Parse the content until the end tag.
+ valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")
+ nodelist = parser.parse(valid_endpartials)
+ endpartial = parser.next_token()
+ if endpartial.contents not in valid_endpartials:
+ parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)
+
+ # Store the partial nodelist in the parser.extra_data attribute.
+ partials = parser.extra_data.setdefault("partials", {})
+ if partial_name in partials:
+ raise TemplateSyntaxError(
+ f"Partial '{partial_name}' is already defined in the "
+ f"'{parser.origin.name}' template."
+ )
+ partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
+
+ return PartialDefNode(partial_name, inline, nodelist)
+
+
+@register.tag(name="partial")
+def partial_func(parser, token):
+ """
+ Render a partial previously declared with the ``{% partialdef %}`` tag.
+
+ Usage::
+
+ {% partial partial_name %}
+ """
+ match token.split_contents():
+ case "partial", partial_name:
+ extra_data = parser.extra_data
+ partial_mapping = DeferredSubDict(extra_data, "partials")
+ return PartialNode(partial_name, partial_mapping=partial_mapping)
+ case _:
+ raise TemplateSyntaxError("'partial' tag requires a single argument")
+
+
@register.simple_tag(name="querystring", takes_context=True)
def querystring(context, *args, **kwargs):
"""
diff --git a/django/template/engine.py b/django/template/engine.py
index 9882d3a16d..df5c8316b9 100644
--- a/django/template/engine.py
+++ b/django/template/engine.py
@@ -174,11 +174,31 @@ class Engine:
Return a compiled Template object for the given template name,
handling template inheritance recursively.
"""
+ original_name = template_name
+ try:
+ template_name, _, partial_name = template_name.partition("#")
+ except AttributeError:
+ raise TemplateDoesNotExist(original_name)
+
+ if not template_name:
+ raise TemplateDoesNotExist(original_name)
+
template, origin = self.find_template(template_name)
if not hasattr(template, "render"):
# template needs to be compiled
template = Template(template, origin, template_name, engine=self)
- return template
+
+ if not partial_name:
+ return template
+
+ extra_data = getattr(template, "extra_data", {})
+ try:
+ partial = extra_data["partials"][partial_name]
+ except (KeyError, TypeError):
+ raise TemplateDoesNotExist(partial_name, tried=[template_name])
+ partial.engine = self
+
+ return partial
def render_to_string(self, template_name, context=None):
"""