-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Code of Conduct
- I agree to follow Django's Code of Conduct
Feature Description
Several django builtin tags are only available within a parent tag, lik {% plural %}:
{% blocktranslate count %}
…
{% plural %}
…
{% endblocktranslate %}
while this raises a TemplateSyntaxError:
{% for item in items %}
…
{% plural %}
…
{% endfor %}
I want to modify django.template.Parser to make it easier to declare such local tags.
Problem
Django template engine provides several ways to declare new tags extremely easily. Most notoriously django.template.Library.inclusion_tag, django.template.Library.simple_tag and django.template.Library.simple_block_tag or any subclass of django.template.Node if you want a more advanced usage.
However, in some cases, you may want to declare tags or filters that are available only within the context of a particular tag. For instance {% elif %} and {% else %}are only available within{% if %}, {% empty %}makes sense only within{% for %}and{% plural %}within{% blocktranslate %}`.
In such cases, the current code doesn't offer a way to reuse code facilities. Any variant of django.template.Library.tag will register tags globally and leak them outside the parent tag and into children tags. If you want to implement local tags, you're back to manually parsing the template, token by token.
While this may look like fringe need, I hit this problem regularly while working with design systems where simple components, or highly customizable components cannot be implemented with a single block tag and require implementing optionnal sub-block tags that only make sens withing a parent tag.
Request or proposal
proposal
Additional Details
No response
Implementation Suggestions
This is my proposed API, using {% if %} as an example:
register = template.Library()
def do_elif(parser: Parser, token: Token):
....
def do_else(parser: Parser, token: Token):
…
@register.tag(name="if")
def do_if(parser: Parser, token: Token):
local_library = template.Library()
local_library.tag("elif", do_elif)
local_library.tag("else", do_else)
with parser.local_library(local_library) as local_parser:
nodelist = local_parser.parse(("endif",))
# process nodelist
This diff showcases how I think it can be implemented
diff --git a/django/template/base.py b/django/template/base.py
index 5e541c3960..46db18eb1e 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -49,7 +49,7 @@ times with multiple contexts)
>>> t.render(c)
'<html></html>'
"""
-
+import contextlib
import inspect
import logging
import re
@@ -498,7 +498,7 @@ class DebugLexer(Lexer):
class Parser:
- def __init__(self, tokens, libraries=None, builtins=None, origin=None):
+ def __init__(self, tokens, libraries=None, builtins=None, origin=None, parent=None):
# Reverse the tokens so delete_first_token(), prepend_token(), and
# next_token() can operate at the end of the list in constant time.
self.tokens = list(reversed(tokens))
@@ -522,6 +522,8 @@ class Parser:
self.add_library(builtin)
self.origin = origin
+ self.parent = parent
+
def __repr__(self):
return "<%s tokens=%r>" % (self.__class__.__qualname__, self.tokens)
@@ -579,7 +581,9 @@ class Parser:
# Compile the callback into a node object and add it to
# the node list.
try:
- compiled_result = compile_func(self, token)
+ # Passing self.parent prevents from local libraries to leak into
+ # children tags and filters
+ compiled_result = compile_func(self.parent or self, token)
except Exception as e:
raise self.error(token, e)
self.extend_nodelist(nodelist, compiled_result, token)
@@ -680,6 +684,21 @@ class Parser:
else:
raise TemplateSyntaxError("Invalid filter: '%s'" % filter_name)
+ @contextlib.contextmanager
+ def local_library(self, library):
+ # These fields are copied to prevent messing up with parent's
+ local_parser = Parser([], libraries=self.libraries.copy(), origin=self.origin, parent=self.parent)
+ local_parser.extra_data = self.extra_data.copy()
+
+ # This allows to directly manipulate parent's tokens when calling
+ # parser.next_token() or parser.parse()
+ local_parser.tokens = self.tokens
+ local_parser.command_stack = self.command_stack
+
+ local_parser.add_library(library)
+ yield local_parser
+
+
# This only matches constant *strings* (things in quotes or marked for
# translation). Numbers are treated as variables for implementation reasons
Metadata
Metadata
Assignees
Labels
Type
Projects
Status