Make it easier to declare tags and filters limited within the context of another tag
- Dominant language
- No language data
- Stars
- 188
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Description
### Code of Conduct
- [x] 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](https://github.com/django/django/blob/main/django/template/defaulttags.py#L994-L1018).
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)
''
"""
-
+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
````
[I opened a thread on the forum to discuss this](https://forum.djangoproject.com/t/make-it-easier-to-declare-tags-and-filters-available-only-within-the-context-of-another-tag/43360/2).
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.