mirror of
https://github.com/django-components/django-components.git
synced 2025-08-18 13:10:13 +00:00
refactor: use the tag parser to streamline the tag handlers (#827)
This commit is contained in:
parent
db4ca8b74f
commit
894dee3cad
10 changed files with 351 additions and 429 deletions
|
@ -169,10 +169,6 @@ def resolve_string(
|
||||||
return parser.compile_filter(s).resolve(context)
|
return parser.compile_filter(s).resolve(context)
|
||||||
|
|
||||||
|
|
||||||
def is_kwarg(key: str) -> bool:
|
|
||||||
return "=" in key
|
|
||||||
|
|
||||||
|
|
||||||
def is_aggregate_key(key: str) -> bool:
|
def is_aggregate_key(key: str) -> bool:
|
||||||
# NOTE: If we get a key that starts with `:`, like `:class`, we do not split it.
|
# NOTE: If we get a key that starts with `:`, like `:class`, we do not split it.
|
||||||
# This syntax is used by Vue and AlpineJS.
|
# This syntax is used by Vue and AlpineJS.
|
||||||
|
@ -194,7 +190,7 @@ DYNAMIC_EXPR_RE = re.compile(
|
||||||
|
|
||||||
def is_dynamic_expression(value: Any) -> bool:
|
def is_dynamic_expression(value: Any) -> bool:
|
||||||
# NOTE: Currently dynamic expression need at least 6 characters
|
# NOTE: Currently dynamic expression need at least 6 characters
|
||||||
# for the opening and closing tags, and quotes
|
# for the opening and closing tags, and quotes, e.g. `"`, `{%`, `%}` in `" some text {% ... %}"`
|
||||||
MIN_EXPR_LEN = 6
|
MIN_EXPR_LEN = 6
|
||||||
|
|
||||||
if not isinstance(value, str) or not value or len(value) < MIN_EXPR_LEN:
|
if not isinstance(value, str) or not value or len(value) < MIN_EXPR_LEN:
|
||||||
|
@ -214,24 +210,6 @@ def is_spread_operator(value: Any) -> bool:
|
||||||
return value.startswith("...")
|
return value.startswith("...")
|
||||||
|
|
||||||
|
|
||||||
# A string that starts with `...1=`, `...29=`, etc.
|
|
||||||
# We convert the spread syntax to this, so Django parses
|
|
||||||
# it as a kwarg, so it remains in the original position.
|
|
||||||
#
|
|
||||||
# So from `...dict`, we make `...1=dict`
|
|
||||||
#
|
|
||||||
# That way it's trivial to merge the kwargs after the spread
|
|
||||||
# operator is replaced with actual values.
|
|
||||||
INTERNAL_SPREAD_OPERATOR_RE = re.compile(r"^\.\.\.\d+=")
|
|
||||||
|
|
||||||
|
|
||||||
def is_internal_spread_operator(value: Any) -> bool:
|
|
||||||
if not isinstance(value, str) or not value:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return bool(INTERNAL_SPREAD_OPERATOR_RE.match(value))
|
|
||||||
|
|
||||||
|
|
||||||
def process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
def process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs
|
This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs
|
||||||
|
|
|
@ -6,16 +6,14 @@ from django.template import TemplateSyntaxError
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from django_components.expression import resolve_string
|
from django_components.expression import resolve_string
|
||||||
from django_components.template_parser import VAR_CHARS
|
|
||||||
from django_components.util.misc import is_str_wrapped_in_quotes
|
from django_components.util.misc import is_str_wrapped_in_quotes
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
|
|
||||||
|
|
||||||
# Forward slash is added so it's possible to define components like
|
# Require the start / end tags to contain NO spaces and only these characters
|
||||||
# `{% MyComp %}..{% /MyComp %}`
|
TAG_CHARS = r"\w\-\:\@\.\#/"
|
||||||
TAG_CHARS = VAR_CHARS + r"/"
|
|
||||||
TAG_RE = re.compile(r"^[{chars}]+$".format(chars=TAG_CHARS))
|
TAG_RE = re.compile(r"^[{chars}]+$".format(chars=TAG_CHARS))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
"""
|
|
||||||
Overrides for the Django Template system to allow finer control over template parsing.
|
|
||||||
|
|
||||||
Based on Django Slippers v0.6.2 - https://github.com/mixxorz/slippers/blob/main/slippers/template.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import Any, Dict, List, Tuple
|
|
||||||
|
|
||||||
from django.template.base import (
|
|
||||||
FILTER_ARGUMENT_SEPARATOR,
|
|
||||||
FILTER_SEPARATOR,
|
|
||||||
FilterExpression,
|
|
||||||
Parser,
|
|
||||||
Variable,
|
|
||||||
VariableDoesNotExist,
|
|
||||||
constant_string,
|
|
||||||
)
|
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
|
||||||
|
|
||||||
######################################################################################################################
|
|
||||||
# Custom FilterExpression
|
|
||||||
#
|
|
||||||
# This is a copy of the original FilterExpression. The only difference is to allow variable names to have extra special
|
|
||||||
# characters: - : . @ #
|
|
||||||
######################################################################################################################
|
|
||||||
|
|
||||||
VAR_CHARS = r"\w\-\:\@\.\#"
|
|
||||||
|
|
||||||
filter_raw_string = r"""
|
|
||||||
^(?P<constant>{constant})|
|
|
||||||
^(?P<var>[{var_chars}]+|{num})|
|
|
||||||
(?:\s*{filter_sep}\s*
|
|
||||||
(?P<filter_name>\w+)
|
|
||||||
(?:{arg_sep}
|
|
||||||
(?:
|
|
||||||
(?P<constant_arg>{constant})|
|
|
||||||
(?P<var_arg>[{var_chars}]+|{num})
|
|
||||||
)
|
|
||||||
)?
|
|
||||||
)""".format(
|
|
||||||
constant=constant_string,
|
|
||||||
num=r"[-+\.]?\d[\d\.e]*",
|
|
||||||
# The following is the only difference from the original FilterExpression. We allow variable names to have extra
|
|
||||||
# special characters: - : . @ #
|
|
||||||
var_chars=VAR_CHARS,
|
|
||||||
filter_sep=re.escape(FILTER_SEPARATOR),
|
|
||||||
arg_sep=re.escape(FILTER_ARGUMENT_SEPARATOR),
|
|
||||||
)
|
|
||||||
|
|
||||||
filter_re = _lazy_re_compile(filter_raw_string, re.VERBOSE)
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentsFilterExpression(FilterExpression):
|
|
||||||
def __init__(self, token: str, parser: Parser) -> None:
|
|
||||||
# This method is exactly the same as the original FilterExpression.__init__ method, the only difference being
|
|
||||||
# the value of `filter_re`.
|
|
||||||
self.token = token
|
|
||||||
matches = filter_re.finditer(token)
|
|
||||||
var_obj = None
|
|
||||||
filters: List[Any] = []
|
|
||||||
upto = 0
|
|
||||||
for match in matches:
|
|
||||||
start = match.start()
|
|
||||||
if upto != start:
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"Could not parse some characters: " "%s|%s|%s" % (token[:upto], token[upto:start], token[start:])
|
|
||||||
)
|
|
||||||
if var_obj is None:
|
|
||||||
var, constant = match["var"], match["constant"]
|
|
||||||
if constant:
|
|
||||||
try:
|
|
||||||
var_obj = Variable(constant).resolve({})
|
|
||||||
except VariableDoesNotExist:
|
|
||||||
var_obj = None
|
|
||||||
elif var is None:
|
|
||||||
raise TemplateSyntaxError("Could not find variable at " "start of %s." % token)
|
|
||||||
else:
|
|
||||||
var_obj = Variable(var)
|
|
||||||
else:
|
|
||||||
filter_name = match["filter_name"]
|
|
||||||
args = []
|
|
||||||
constant_arg, var_arg = match["constant_arg"], match["var_arg"]
|
|
||||||
if constant_arg:
|
|
||||||
args.append((False, Variable(constant_arg).resolve({})))
|
|
||||||
elif var_arg:
|
|
||||||
args.append((True, Variable(var_arg)))
|
|
||||||
filter_func = parser.find_filter(filter_name)
|
|
||||||
self.args_check(filter_name, filter_func, args)
|
|
||||||
filters.append((filter_func, args))
|
|
||||||
upto = match.end()
|
|
||||||
if upto != len(token):
|
|
||||||
raise TemplateSyntaxError("Could not parse the remainder: '%s' " "from '%s'" % (token[upto:], token))
|
|
||||||
|
|
||||||
self.filters = filters
|
|
||||||
self.var = var_obj
|
|
||||||
self.is_var = isinstance(var_obj, Variable)
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################################################################
|
|
||||||
# Custom token_kwargs
|
|
||||||
#
|
|
||||||
# Same as the original token_kwargs, but uses the ComponentsFilterExpression instead of the original FilterExpression.
|
|
||||||
######################################################################################################################
|
|
||||||
|
|
||||||
# Regex for token keyword arguments
|
|
||||||
kwarg_re = _lazy_re_compile(r"(?:([{var_chars}]+)=)?(.+)".format(var_chars=VAR_CHARS))
|
|
||||||
|
|
||||||
|
|
||||||
def token_kwargs(bits: List[str], parser: Parser) -> Dict[str, FilterExpression]:
|
|
||||||
"""
|
|
||||||
Parse token keyword arguments and return a dictionary of the arguments
|
|
||||||
retrieved from the ``bits`` token list.
|
|
||||||
|
|
||||||
`bits` is a list containing the remainder of the token (split by spaces)
|
|
||||||
that is to be checked for arguments. Valid arguments are removed from this
|
|
||||||
list.
|
|
||||||
|
|
||||||
There is no requirement for all remaining token ``bits`` to be keyword
|
|
||||||
arguments, so return the dictionary as soon as an invalid argument format
|
|
||||||
is reached.
|
|
||||||
"""
|
|
||||||
if not bits:
|
|
||||||
return {}
|
|
||||||
match = kwarg_re.match(bits[0])
|
|
||||||
kwarg_format = match and match[1]
|
|
||||||
if not kwarg_format:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
kwargs: Dict[str, FilterExpression] = {}
|
|
||||||
while bits:
|
|
||||||
if kwarg_format:
|
|
||||||
match = kwarg_re.match(bits[0])
|
|
||||||
if not match or not match[1]:
|
|
||||||
return kwargs
|
|
||||||
key, value = match.groups()
|
|
||||||
del bits[:1]
|
|
||||||
else:
|
|
||||||
if len(bits) < 3 or bits[1] != "as":
|
|
||||||
return kwargs
|
|
||||||
key, value = bits[2], bits[0]
|
|
||||||
del bits[:3]
|
|
||||||
|
|
||||||
# This is the only difference from the original token_kwargs. We use
|
|
||||||
# the ComponentsFilterExpression instead of the original FilterExpression.
|
|
||||||
kwargs[key] = ComponentsFilterExpression(value, parser)
|
|
||||||
if bits and not kwarg_format:
|
|
||||||
if bits[0] != "and":
|
|
||||||
return kwargs
|
|
||||||
del bits[:1]
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bits(
|
|
||||||
parser: Parser,
|
|
||||||
bits: List[str],
|
|
||||||
params: List[str],
|
|
||||||
name: str,
|
|
||||||
) -> Tuple[List[FilterExpression], List[Tuple[str, FilterExpression]]]:
|
|
||||||
"""
|
|
||||||
Parse bits for template tag helpers simple_tag and inclusion_tag, in
|
|
||||||
particular by detecting syntax errors and by extracting positional and
|
|
||||||
keyword arguments.
|
|
||||||
|
|
||||||
This is a simplified version of `django.template.library.parse_bits`
|
|
||||||
where we use custom regex to handle special characters in keyword names.
|
|
||||||
|
|
||||||
Furthermore, our version allows duplicate keys, and instead of return kwargs
|
|
||||||
as a dict, we return it as a list of key-value pairs. So it is up to the
|
|
||||||
user of this function to decide whether they support duplicate keys or not.
|
|
||||||
"""
|
|
||||||
args: List[FilterExpression] = []
|
|
||||||
kwargs: List[Tuple[str, FilterExpression]] = []
|
|
||||||
unhandled_params = list(params)
|
|
||||||
for bit in bits:
|
|
||||||
# First we try to extract a potential kwarg from the bit
|
|
||||||
kwarg = token_kwargs([bit], parser)
|
|
||||||
if kwarg:
|
|
||||||
# The kwarg was successfully extracted
|
|
||||||
param, value = kwarg.popitem()
|
|
||||||
# All good, record the keyword argument
|
|
||||||
kwargs.append((str(param), value))
|
|
||||||
if param in unhandled_params:
|
|
||||||
# If using the keyword syntax for a positional arg, then
|
|
||||||
# consume it.
|
|
||||||
unhandled_params.remove(param)
|
|
||||||
else:
|
|
||||||
if kwargs:
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' received some positional argument(s) after some " "keyword argument(s)" % name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Record the positional argument
|
|
||||||
args.append(parser.compile_filter(bit))
|
|
||||||
try:
|
|
||||||
# Consume from the list of expected positional arguments
|
|
||||||
unhandled_params.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
if unhandled_params:
|
|
||||||
# Some positional arguments were not supplied
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
"'%s' did not receive value(s) for the argument(s): %s"
|
|
||||||
% (name, ", ".join("'%s'" % p for p in unhandled_params))
|
|
||||||
)
|
|
||||||
return args, kwargs
|
|
|
@ -12,10 +12,10 @@
|
||||||
# During documentation generation, we access the `fn._tag_spec`.
|
# During documentation generation, we access the `fn._tag_spec`.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from typing import Any, Callable, Dict, List, Literal, NamedTuple, Optional, Set, Union
|
from typing import Any, Callable, Dict, List, Literal, NamedTuple, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
import django.template
|
import django.template
|
||||||
from django.template.base import NodeList, Parser, TextNode, Token, TokenType
|
from django.template.base import FilterExpression, NodeList, Parser, TextNode, Token, TokenType
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
from django.utils.safestring import SafeString, mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
|
@ -34,8 +34,6 @@ from django_components.expression import (
|
||||||
SpreadOperator,
|
SpreadOperator,
|
||||||
is_aggregate_key,
|
is_aggregate_key,
|
||||||
is_dynamic_expression,
|
is_dynamic_expression,
|
||||||
is_internal_spread_operator,
|
|
||||||
is_kwarg,
|
|
||||||
is_spread_operator,
|
is_spread_operator,
|
||||||
)
|
)
|
||||||
from django_components.provide import PROVIDE_NAME_KWARG, ProvideNode
|
from django_components.provide import PROVIDE_NAME_KWARG, ProvideNode
|
||||||
|
@ -49,10 +47,9 @@ from django_components.slots import (
|
||||||
SlotNode,
|
SlotNode,
|
||||||
)
|
)
|
||||||
from django_components.tag_formatter import get_tag_formatter
|
from django_components.tag_formatter import get_tag_formatter
|
||||||
from django_components.template_parser import parse_bits
|
|
||||||
from django_components.util.logger import trace_msg
|
from django_components.util.logger import trace_msg
|
||||||
from django_components.util.misc import gen_id
|
from django_components.util.misc import gen_id
|
||||||
from django_components.util.tag_parser import parse_tag_attrs
|
from django_components.util.tag_parser import TagAttr, parse_tag_attrs
|
||||||
|
|
||||||
# NOTE: Variable name `register` is required by Django to recognize this as a template tag library
|
# NOTE: Variable name `register` is required by Django to recognize this as a template tag library
|
||||||
# See https://docs.djangoproject.com/en/dev/howto/custom-template-tags
|
# See https://docs.djangoproject.com/en/dev/howto/custom-template-tags
|
||||||
|
@ -159,11 +156,12 @@ def component_css_dependencies(parser: Parser, token: Token, tag_spec: TagSpec)
|
||||||
If you insert this tag multiple times, ALL CSS links will be duplicately inserted into ALL these places.
|
If you insert this tag multiple times, ALL CSS links will be duplicately inserted into ALL these places.
|
||||||
"""
|
"""
|
||||||
# Parse to check that the syntax is valid
|
# Parse to check that the syntax is valid
|
||||||
_parse_tag(parser, token, tag_spec)
|
tag_id = gen_id()
|
||||||
|
_parse_tag(parser, token, tag_spec, tag_id)
|
||||||
return _component_dependencies("css")
|
return _component_dependencies("css")
|
||||||
|
|
||||||
|
|
||||||
@register.tag(name="component_js_dependencies")
|
@register.tag("component_js_dependencies")
|
||||||
@with_tag_spec(
|
@with_tag_spec(
|
||||||
TagSpec(
|
TagSpec(
|
||||||
tag="component_js_dependencies",
|
tag="component_js_dependencies",
|
||||||
|
@ -184,7 +182,8 @@ def component_js_dependencies(parser: Parser, token: Token, tag_spec: TagSpec) -
|
||||||
If you insert this tag multiple times, ALL JS scripts will be duplicately inserted into ALL these places.
|
If you insert this tag multiple times, ALL JS scripts will be duplicately inserted into ALL these places.
|
||||||
"""
|
"""
|
||||||
# Parse to check that the syntax is valid
|
# Parse to check that the syntax is valid
|
||||||
_parse_tag(parser, token, tag_spec)
|
tag_id = gen_id()
|
||||||
|
_parse_tag(parser, token, tag_spec, tag_id)
|
||||||
return _component_dependencies("js")
|
return _component_dependencies("js")
|
||||||
|
|
||||||
|
|
||||||
|
@ -323,7 +322,8 @@ def slot(parser: Parser, token: Token, tag_spec: TagSpec) -> SlotNode:
|
||||||
\"\"\"
|
\"\"\"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
tag = _parse_tag(parser, token, tag_spec)
|
tag_id = gen_id()
|
||||||
|
tag = _parse_tag(parser, token, tag_spec, tag_id=tag_id)
|
||||||
|
|
||||||
slot_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
slot_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
||||||
trace_id = f"slot-id-{tag.id} ({slot_name_kwarg})" if slot_name_kwarg else f"slot-id-{tag.id}"
|
trace_id = f"slot-id-{tag.id} ({slot_name_kwarg})" if slot_name_kwarg else f"slot-id-{tag.id}"
|
||||||
|
@ -444,7 +444,8 @@ def fill(parser: Parser, token: Token, tag_spec: TagSpec) -> FillNode:
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
tag = _parse_tag(parser, token, tag_spec)
|
tag_id = gen_id()
|
||||||
|
tag = _parse_tag(parser, token, tag_spec, tag_id=tag_id)
|
||||||
|
|
||||||
fill_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
fill_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
||||||
trace_id = f"fill-id-{tag.id} ({fill_name_kwarg})" if fill_name_kwarg else f"fill-id-{tag.id}"
|
trace_id = f"fill-id-{tag.id} ({fill_name_kwarg})" if fill_name_kwarg else f"fill-id-{tag.id}"
|
||||||
|
@ -577,6 +578,8 @@ def component(
|
||||||
{% component "name" positional_arg keyword_arg=value ... only %}
|
{% component "name" positional_arg keyword_arg=value ... only %}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
tag_id = gen_id()
|
||||||
|
|
||||||
_fix_nested_tags(parser, token)
|
_fix_nested_tags(parser, token)
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
|
|
||||||
|
@ -599,6 +602,7 @@ def component(
|
||||||
"end_tag": end_tag,
|
"end_tag": end_tag,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
tag_id=tag_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for isolated context keyword
|
# Check for isolated context keyword
|
||||||
|
@ -703,8 +707,10 @@ def provide(parser: Parser, token: Token, tag_spec: TagSpec) -> ProvideNode:
|
||||||
user = self.inject("user_data")["user"]
|
user = self.inject("user_data")["user"]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
tag_id = gen_id()
|
||||||
|
|
||||||
# e.g. {% provide <name> key=val key2=val2 %}
|
# e.g. {% provide <name> key=val key2=val2 %}
|
||||||
tag = _parse_tag(parser, token, tag_spec)
|
tag = _parse_tag(parser, token, tag_spec, tag_id)
|
||||||
|
|
||||||
name_kwarg = tag.kwargs.kwargs.get(PROVIDE_NAME_KWARG, None)
|
name_kwarg = tag.kwargs.kwargs.get(PROVIDE_NAME_KWARG, None)
|
||||||
trace_id = f"provide-id-{tag.id} ({name_kwarg})" if name_kwarg else f"fill-id-{tag.id}"
|
trace_id = f"provide-id-{tag.id} ({name_kwarg})" if name_kwarg else f"fill-id-{tag.id}"
|
||||||
|
@ -788,7 +794,8 @@ def html_attrs(parser: Parser, token: Token, tag_spec: TagSpec) -> HtmlAttrsNode
|
||||||
**See more usage examples in
|
**See more usage examples in
|
||||||
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).**
|
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).**
|
||||||
"""
|
"""
|
||||||
tag = _parse_tag(parser, token, tag_spec)
|
tag_id = gen_id()
|
||||||
|
tag = _parse_tag(parser, token, tag_spec, tag_id)
|
||||||
|
|
||||||
return HtmlAttrsNode(
|
return HtmlAttrsNode(
|
||||||
kwargs=tag.kwargs,
|
kwargs=tag.kwargs,
|
||||||
|
@ -799,7 +806,6 @@ def html_attrs(parser: Parser, token: Token, tag_spec: TagSpec) -> HtmlAttrsNode
|
||||||
class ParsedTag(NamedTuple):
|
class ParsedTag(NamedTuple):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
bits: List[str]
|
|
||||||
flags: Dict[str, bool]
|
flags: Dict[str, bool]
|
||||||
args: List[Expression]
|
args: List[Expression]
|
||||||
named_args: Dict[str, Expression]
|
named_args: Dict[str, Expression]
|
||||||
|
@ -809,164 +815,279 @@ class ParsedTag(NamedTuple):
|
||||||
parse_body: Callable[[], NodeList]
|
parse_body: Callable[[], NodeList]
|
||||||
|
|
||||||
|
|
||||||
|
class TagArg(NamedTuple):
|
||||||
|
name: str
|
||||||
|
positional_only: bool
|
||||||
|
|
||||||
|
|
||||||
def _parse_tag(
|
def _parse_tag(
|
||||||
parser: Parser,
|
parser: Parser,
|
||||||
token: Token,
|
token: Token,
|
||||||
tag_spec: TagSpec,
|
tag_spec: TagSpec,
|
||||||
|
tag_id: str,
|
||||||
) -> ParsedTag:
|
) -> ParsedTag:
|
||||||
# Use a unique ID to be able to tie the fill nodes with components and slots
|
tag_name, raw_args, raw_kwargs, raw_flags, is_inline = _parse_tag_preprocess(parser, token, tag_spec)
|
||||||
# NOTE: MUST be called BEFORE `parse_body()` to ensure predictable numbering
|
|
||||||
tag_id = gen_id()
|
|
||||||
|
|
||||||
params = [*(tag_spec.positional_only_args or []), *(tag_spec.pos_or_keyword_args or [])]
|
parsed_tag = _parse_tag_process(
|
||||||
|
parser=parser,
|
||||||
|
tag_id=tag_id,
|
||||||
|
tag_name=tag_name,
|
||||||
|
tag_spec=tag_spec,
|
||||||
|
raw_args=raw_args,
|
||||||
|
raw_kwargs=raw_kwargs,
|
||||||
|
raw_flags=raw_flags,
|
||||||
|
is_inline=is_inline,
|
||||||
|
)
|
||||||
|
return parsed_tag
|
||||||
|
|
||||||
|
|
||||||
|
class TagKwarg(NamedTuple):
|
||||||
|
type: Literal["kwarg", "spread"]
|
||||||
|
key: str
|
||||||
|
# E.g. `class` in `attrs:class="my-class"`
|
||||||
|
inner_key: Optional[str]
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tag_preprocess(
|
||||||
|
parser: Parser,
|
||||||
|
token: Token,
|
||||||
|
tag_spec: TagSpec,
|
||||||
|
) -> Tuple[str, List[str], List[TagKwarg], Set[str], bool]:
|
||||||
_fix_nested_tags(parser, token)
|
_fix_nested_tags(parser, token)
|
||||||
|
|
||||||
# e.g. {% slot <name> ... %}
|
_, attrs = parse_tag_attrs(token.contents)
|
||||||
tag_name, *bits = token.split_contents()
|
|
||||||
|
# First token is tag name, e.g. `slot` in `{% slot <name> ... %}`
|
||||||
|
tag_name_attr = attrs.pop(0)
|
||||||
|
tag_name = tag_name_attr.value
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
if tag_name != tag_spec.tag:
|
if tag_name != tag_spec.tag:
|
||||||
raise TemplateSyntaxError(f"Start tag parser received tag '{tag_name}', expected '{tag_spec.tag}'")
|
raise TemplateSyntaxError(f"Start tag parser received tag '{tag_name}', expected '{tag_spec.tag}'")
|
||||||
|
|
||||||
# Decide if the template tag is inline or block and strip the trailing slash
|
# There's 3 ways how we tell when a tag ends:
|
||||||
last_token = bits[-1] if len(bits) else None
|
# 1. If the tag contains `/` at the end, it's a self-closing tag (like `<div />`),
|
||||||
|
# and it doesn't have an end tag. In this case we strip the trailing slash.
|
||||||
|
# Otherwise, depending on the tag spec, the tag may be:
|
||||||
|
# 2. Block tag - With corresponding end tag, e.g. `{% endslot %}`
|
||||||
|
# 3. Inlined tag - Without the end tag.
|
||||||
|
last_token = attrs[-1].value if len(attrs) else None
|
||||||
if last_token == "/":
|
if last_token == "/":
|
||||||
bits.pop()
|
attrs.pop()
|
||||||
is_inline = True
|
is_inline = True
|
||||||
else:
|
else:
|
||||||
# If no end tag was given, we assume that the tag is inline-only
|
|
||||||
is_inline = not tag_spec.end_tag
|
is_inline = not tag_spec.end_tag
|
||||||
|
|
||||||
parsed_flags = {flag: False for flag in (tag_spec.flags or [])}
|
raw_args, raw_kwargs, raw_flags = _parse_tag_input(tag_name, attrs)
|
||||||
bits_without_flags: List[str] = []
|
|
||||||
seen_kwargs: Set[str] = set()
|
|
||||||
seen_agg_keys: Set[str] = set()
|
|
||||||
|
|
||||||
def mark_kwarg_key(key: str, is_agg_key: bool) -> None:
|
return tag_name, raw_args, raw_kwargs, raw_flags, is_inline
|
||||||
if (is_agg_key and key in seen_kwargs) or (not is_agg_key and key in seen_agg_keys):
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
f"Received argument '{key}' both as a regular input ({key}=...)"
|
|
||||||
f" and as an aggregate dict ('{key}:key=...'). Must be only one of the two"
|
|
||||||
)
|
|
||||||
if is_agg_key:
|
|
||||||
seen_agg_keys.add(key)
|
|
||||||
else:
|
|
||||||
seen_kwargs.add(key)
|
|
||||||
|
|
||||||
spread_count = 0
|
|
||||||
for bit in bits:
|
|
||||||
value = bit
|
|
||||||
bit_is_kwarg = is_kwarg(bit)
|
|
||||||
|
|
||||||
# Record which kwargs we've seen, to detect if kwargs were passed in
|
def _parse_tag_input(tag_name: str, attrs: List[TagAttr]) -> Tuple[List[str], List[TagKwarg], Set[str]]:
|
||||||
# as both aggregate and regular kwargs
|
# Given a list of attributes passed to a tag, categorise them into args, kwargs, and flags.
|
||||||
if bit_is_kwarg:
|
# The result of this will be passed to plugins to allow them to modify the tag inputs.
|
||||||
key, value = bit.split("=", 1)
|
# And only once we get back the modified inputs, we will parse the data into
|
||||||
|
# internal structures like `DynamicFilterExpression`, or `SpreadOperator`.
|
||||||
|
#
|
||||||
|
# NOTES:
|
||||||
|
# - When args end, kwargs start. Positional args cannot follow kwargs
|
||||||
|
# - There can be multiple kwargs with same keys
|
||||||
|
# - Flags can be anywhere
|
||||||
|
# - Each flag can be present only once
|
||||||
|
is_args = True
|
||||||
|
args_or_flags: List[str] = []
|
||||||
|
kwarg_pairs: List[TagKwarg] = []
|
||||||
|
flags = set()
|
||||||
|
seen_spreads = 0
|
||||||
|
for attr in attrs:
|
||||||
|
value = attr.formatted_value()
|
||||||
|
|
||||||
# Also pick up on aggregate keys like `attr:key=val`
|
# Spread
|
||||||
if is_aggregate_key(key):
|
if is_spread_operator(value):
|
||||||
key = key.split(":")[0]
|
|
||||||
mark_kwarg_key(key, True)
|
|
||||||
else:
|
|
||||||
mark_kwarg_key(key, False)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Extract flags, which are like keywords but without the value part
|
|
||||||
if value in parsed_flags:
|
|
||||||
parsed_flags[value] = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract spread operator (...dict)
|
|
||||||
elif is_spread_operator(value):
|
|
||||||
if value == "...":
|
if value == "...":
|
||||||
raise TemplateSyntaxError("Syntax operator is missing a value")
|
raise TemplateSyntaxError("Syntax operator is missing a value")
|
||||||
|
|
||||||
# Replace the leading `...` with `...=`, so the parser
|
kwarg = TagKwarg(type="spread", key=f"...{seen_spreads}", inner_key=None, value=value[3:])
|
||||||
# interprets it as a kwargs, and keeps it in the correct
|
kwarg_pairs.append(kwarg)
|
||||||
# position.
|
is_args = False
|
||||||
# Since there can be multiple spread operators, we suffix
|
seen_spreads += 1
|
||||||
# them with an index, e.g. `...0=`
|
|
||||||
internal_spread_bit = f"...{spread_count}={value[3:]}"
|
|
||||||
bits_without_flags.append(internal_spread_bit)
|
|
||||||
spread_count += 1
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bits_without_flags.append(bit)
|
# Positional or flag
|
||||||
|
elif is_args and not attr.key:
|
||||||
bits = bits_without_flags
|
args_or_flags.append(value)
|
||||||
|
|
||||||
# To support optional args, we need to convert these to kwargs, so `parse_bits`
|
|
||||||
# can handle them. So we assign the keys to matched positional args,
|
|
||||||
# and then move the kwarg AFTER the pos args.
|
|
||||||
#
|
|
||||||
# TODO: This following section should live in `parse_bits`, but I don't want to
|
|
||||||
# modify it much to maintain some sort of compatibility with Django's version of
|
|
||||||
# `parse_bits`.
|
|
||||||
# Ideally, Django's parser would be expanded to support our use cases.
|
|
||||||
params_to_sort = [param for param in params if param not in seen_kwargs]
|
|
||||||
new_args = []
|
|
||||||
new_params = []
|
|
||||||
new_kwargs = []
|
|
||||||
for index, bit in enumerate(bits):
|
|
||||||
if is_kwarg(bit) or not len(params_to_sort):
|
|
||||||
# Pass all remaining bits (including current one) as kwargs
|
|
||||||
new_kwargs.extend(bits[index:])
|
|
||||||
break
|
|
||||||
|
|
||||||
param = params_to_sort.pop(0)
|
|
||||||
if tag_spec.pos_or_keyword_args and param in tag_spec.pos_or_keyword_args:
|
|
||||||
mark_kwarg_key(param, False)
|
|
||||||
new_kwargs.append(f"{param}={bit}")
|
|
||||||
continue
|
continue
|
||||||
new_args.append(bit)
|
|
||||||
new_params.append(param)
|
|
||||||
|
|
||||||
bits = [*new_args, *new_kwargs]
|
# Keyword
|
||||||
params = [*new_params, *params_to_sort]
|
elif attr.key:
|
||||||
|
if is_aggregate_key(attr.key):
|
||||||
|
key, inner_key = attr.key.split(":", 1)
|
||||||
|
else:
|
||||||
|
key, inner_key = attr.key, None
|
||||||
|
|
||||||
# Remove any remaining optional positional args if they were not given
|
kwarg = TagKwarg(type="kwarg", key=key, inner_key=inner_key, value=value)
|
||||||
if tag_spec.pos_or_keyword_args:
|
kwarg_pairs.append(kwarg)
|
||||||
params = [param for param in params_to_sort if param not in tag_spec.pos_or_keyword_args]
|
is_args = False
|
||||||
|
continue
|
||||||
|
|
||||||
# Parse args/kwargs that will be passed to the fill
|
# Either flag or a misplaced positional arg
|
||||||
raw_args, raw_kwarg_pairs = parse_bits(
|
elif not is_args and not attr.key:
|
||||||
parser=parser,
|
# NOTE: By definition, dynamic expressions CANNOT be identifiers, because
|
||||||
bits=bits,
|
# they contain quotes. So we can catch those early.
|
||||||
params=[] if tag_spec.positional_args_allow_extra else params,
|
if not value.isidentifier():
|
||||||
name=tag_name,
|
raise TemplateSyntaxError(
|
||||||
|
f"'{tag_name}' received positional argument '{value}' after keyword argument(s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Post-process args/kwargs - Mark special cases like aggregate dicts
|
# Otherwise, we assume that the token is a flag. It is up to the tag logic
|
||||||
# or dynamic expressions
|
# to decide whether this is a recognized flag or a misplaced positional arg.
|
||||||
|
if value in flags:
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' received flag '{value}' multiple times")
|
||||||
|
|
||||||
|
flags.add(value)
|
||||||
|
continue
|
||||||
|
return args_or_flags, kwarg_pairs, flags
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tag_process(
|
||||||
|
parser: Parser,
|
||||||
|
tag_id: str,
|
||||||
|
tag_name: str,
|
||||||
|
tag_spec: TagSpec,
|
||||||
|
raw_args: List[str],
|
||||||
|
raw_kwargs: List[TagKwarg],
|
||||||
|
raw_flags: Set[str],
|
||||||
|
is_inline: bool,
|
||||||
|
) -> ParsedTag:
|
||||||
|
seen_kwargs = set([kwarg.key for kwarg in raw_kwargs if kwarg.key and kwarg.type == "kwarg"])
|
||||||
|
|
||||||
|
seen_regular_kwargs = set()
|
||||||
|
seen_agg_kwargs = set()
|
||||||
|
|
||||||
|
def check_kwarg_for_agg_conflict(kwarg: TagKwarg) -> None:
|
||||||
|
# Skip spread operators
|
||||||
|
if kwarg.type == "spread":
|
||||||
|
return
|
||||||
|
|
||||||
|
is_agg_kwarg = kwarg.inner_key
|
||||||
|
if (
|
||||||
|
(is_agg_kwarg and (kwarg.key in seen_regular_kwargs))
|
||||||
|
or (not is_agg_kwarg and (kwarg.key in seen_agg_kwargs))
|
||||||
|
): # fmt: skip
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Received argument '{kwarg.key}' both as a regular input ({kwarg.key}=...)"
|
||||||
|
f" and as an aggregate dict ('{kwarg.key}:key=...'). Must be only one of the two"
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_agg_kwarg:
|
||||||
|
seen_agg_kwargs.add(kwarg.key)
|
||||||
|
else:
|
||||||
|
seen_regular_kwargs.add(kwarg.key)
|
||||||
|
|
||||||
|
for raw_kwarg in raw_kwargs:
|
||||||
|
check_kwarg_for_agg_conflict(raw_kwarg)
|
||||||
|
|
||||||
|
# Params that may be passed as positional args
|
||||||
|
pos_params: List[TagArg] = [
|
||||||
|
*[TagArg(name=name, positional_only=True) for name in (tag_spec.positional_only_args or [])],
|
||||||
|
*[TagArg(name=name, positional_only=False) for name in (tag_spec.pos_or_keyword_args or [])],
|
||||||
|
]
|
||||||
|
|
||||||
args: List[Expression] = []
|
args: List[Expression] = []
|
||||||
for val in raw_args:
|
|
||||||
if is_dynamic_expression(val.token):
|
|
||||||
args.append(DynamicFilterExpression(parser, val.token))
|
|
||||||
else:
|
|
||||||
args.append(val)
|
|
||||||
|
|
||||||
kwarg_pairs: RuntimeKwargPairsInput = []
|
|
||||||
for key, val in raw_kwarg_pairs:
|
|
||||||
is_spread_op = is_internal_spread_operator(key + "=")
|
|
||||||
|
|
||||||
if is_spread_op:
|
|
||||||
# Allow to use dynamic expressions with spread operator, e.g.
|
|
||||||
# `..."{{ }}"`
|
|
||||||
if is_dynamic_expression(val.token):
|
|
||||||
expr = DynamicFilterExpression(parser, val.token)
|
|
||||||
else:
|
|
||||||
expr = parser.compile_filter(val.token)
|
|
||||||
kwarg_pairs.append((key, SpreadOperator(expr)))
|
|
||||||
elif is_dynamic_expression(val.token) and not is_spread_op:
|
|
||||||
kwarg_pairs.append((key, DynamicFilterExpression(parser, val.token)))
|
|
||||||
else:
|
|
||||||
kwarg_pairs.append((key, val))
|
|
||||||
|
|
||||||
# Allow only as many positional args as given
|
|
||||||
if not tag_spec.positional_args_allow_extra and len(args) > len(params): # noqa F712
|
|
||||||
raise TemplateSyntaxError(f"Tag '{tag_name}' received unexpected positional arguments: {args[len(params):]}")
|
|
||||||
|
|
||||||
# For convenience, allow to access named args by their name instead of index
|
# For convenience, allow to access named args by their name instead of index
|
||||||
named_args = {param: args[index] for index, param in enumerate(params)}
|
named_args: Dict[str, Expression] = {}
|
||||||
|
kwarg_pairs: RuntimeKwargPairsInput = []
|
||||||
|
flags = set()
|
||||||
|
# When we come across a flag within positional args, we need to remember
|
||||||
|
# the offset, so we can correctly assign the args to the correct params
|
||||||
|
flag_offset = 0
|
||||||
|
|
||||||
|
for index, arg_input in enumerate(raw_args):
|
||||||
|
# Flags may be anywhere, so we need to check if the arg is a flag
|
||||||
|
if tag_spec.flags and arg_input in tag_spec.flags:
|
||||||
|
if arg_input in raw_flags or arg_input in flags:
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' received flag '{arg_input}' multiple times")
|
||||||
|
flags.add(arg_input)
|
||||||
|
flag_offset += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Allow to use dynamic expressions as args, e.g. `"{{ }}"`
|
||||||
|
if is_dynamic_expression(arg_input):
|
||||||
|
arg = DynamicFilterExpression(parser, arg_input)
|
||||||
|
else:
|
||||||
|
arg = FilterExpression(arg_input, parser)
|
||||||
|
|
||||||
|
if (index - flag_offset) >= len(pos_params):
|
||||||
|
if tag_spec.positional_args_allow_extra:
|
||||||
|
args.append(arg)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Allow only as many positional args as given
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Tag '{tag_name}' received too many positional arguments: {raw_args[index:]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
param = pos_params[index - flag_offset]
|
||||||
|
if param.positional_only:
|
||||||
|
args.append(arg)
|
||||||
|
named_args[param.name] = arg
|
||||||
|
else:
|
||||||
|
kwarg = TagKwarg(type="kwarg", key=param.name, inner_key=None, value=arg_input)
|
||||||
|
check_kwarg_for_agg_conflict(kwarg)
|
||||||
|
if param.name in seen_kwargs:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"'{tag_name}' received argument '{param.name}' both as positional and keyword argument"
|
||||||
|
)
|
||||||
|
|
||||||
|
kwarg_pairs.append((param.name, arg))
|
||||||
|
|
||||||
|
if len(raw_args) - flag_offset < len(tag_spec.positional_only_args or []):
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Tag '{tag_name}' received too few positional arguments. "
|
||||||
|
f"Expected {len(tag_spec.positional_only_args or [])}, got {len(raw_args) - flag_offset}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for kwarg_input in raw_kwargs:
|
||||||
|
# Allow to use dynamic expressions with spread operator, e.g.
|
||||||
|
# `..."{{ }}"` or as kwargs values `key="{{ }}"`
|
||||||
|
if is_dynamic_expression(kwarg_input.value):
|
||||||
|
expr: Union[Expression, Operator] = DynamicFilterExpression(parser, kwarg_input.value)
|
||||||
|
else:
|
||||||
|
expr = FilterExpression(kwarg_input.value, parser)
|
||||||
|
|
||||||
|
if kwarg_input.type == "spread":
|
||||||
|
expr = SpreadOperator(expr)
|
||||||
|
|
||||||
|
if kwarg_input.inner_key:
|
||||||
|
full_key = f"{kwarg_input.key}:{kwarg_input.inner_key}"
|
||||||
|
else:
|
||||||
|
full_key = kwarg_input.key
|
||||||
|
|
||||||
|
kwarg_pairs.append((full_key, expr))
|
||||||
|
|
||||||
|
# Flags
|
||||||
|
flags_dict: Dict[str, bool] = {
|
||||||
|
# Base state, as defined in the tag spec
|
||||||
|
**{flag: False for flag in (tag_spec.flags or [])},
|
||||||
|
# Flags found among positional args
|
||||||
|
**{flag: True for flag in flags},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Flags found among kwargs
|
||||||
|
for flag in raw_flags:
|
||||||
|
if flag in flags:
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' received flag '{flag}' multiple times")
|
||||||
|
if flag not in (tag_spec.flags or []):
|
||||||
|
raise TemplateSyntaxError(f"'{tag_name}' received unknown flag '{flag}'")
|
||||||
|
flags.add(flag)
|
||||||
|
flags_dict[flag] = True
|
||||||
|
|
||||||
|
# Validate that there are no name conflicts between kwargs and flags
|
||||||
|
if flags.intersection(seen_kwargs):
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"'{tag_name}' received flags that conflict with keyword arguments: {flags.intersection(seen_kwargs)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate kwargs
|
# Validate kwargs
|
||||||
kwargs: RuntimeKwargsInput = {}
|
kwargs: RuntimeKwargsInput = {}
|
||||||
|
@ -1010,8 +1131,7 @@ def _parse_tag(
|
||||||
return ParsedTag(
|
return ParsedTag(
|
||||||
id=tag_id,
|
id=tag_id,
|
||||||
name=tag_name,
|
name=tag_name,
|
||||||
bits=bits,
|
flags=flags_dict,
|
||||||
flags=parsed_flags,
|
|
||||||
args=args,
|
args=args,
|
||||||
named_args=named_args,
|
named_args=named_args,
|
||||||
kwargs=RuntimeKwargs(kwargs),
|
kwargs=RuntimeKwargs(kwargs),
|
||||||
|
|
|
@ -13,8 +13,26 @@ class TagAttr:
|
||||||
Start index of the attribute (include both key and value),
|
Start index of the attribute (include both key and value),
|
||||||
relative to the start of the owner Tag.
|
relative to the start of the owner Tag.
|
||||||
"""
|
"""
|
||||||
quoted: bool
|
quoted: Optional[str]
|
||||||
"""Whether the value is quoted (either with single or double quotes)"""
|
"""Whether the value is quoted, and the character that's used for the quotation"""
|
||||||
|
spread: bool
|
||||||
|
"""Whether the value is a spread syntax, e.g. `...my_var`"""
|
||||||
|
translation: bool
|
||||||
|
"""Whether the value is a translation string, e.g. `_("my string")`"""
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.translation and not self.quoted:
|
||||||
|
raise ValueError("Translation value must be quoted")
|
||||||
|
if self.translation and self.spread:
|
||||||
|
raise ValueError("Cannot combine translation and spread syntax")
|
||||||
|
|
||||||
|
def formatted_value(self) -> str:
|
||||||
|
value = f"{self.quoted}{self.value}{self.quoted}" if self.quoted else self.value
|
||||||
|
if self.translation:
|
||||||
|
value = f"_({value})"
|
||||||
|
elif self.spread:
|
||||||
|
value = f"...{value}"
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Parse the content of a Django template tag like this:
|
# Parse the content of a Django template tag like this:
|
||||||
|
@ -125,7 +143,7 @@ def parse_tag_attrs(text: str) -> Tuple[str, List[TagAttr]]:
|
||||||
# If token starts with a quote, we assume it's a value without key part.
|
# If token starts with a quote, we assume it's a value without key part.
|
||||||
# e.g. `component 'my_comp'`
|
# e.g. `component 'my_comp'`
|
||||||
# Otherwise, parse the key.
|
# Otherwise, parse the key.
|
||||||
if is_next_token("'", '"', '_("', "_('"):
|
if is_next_token("'", '"', '_("', "_('", "..."):
|
||||||
key = None
|
key = None
|
||||||
else:
|
else:
|
||||||
key = take_until(["=", *TAG_WHITESPACE])
|
key = take_until(["=", *TAG_WHITESPACE])
|
||||||
|
@ -144,11 +162,18 @@ def parse_tag_attrs(text: str) -> Tuple[str, List[TagAttr]]:
|
||||||
key=None,
|
key=None,
|
||||||
value=key,
|
value=key,
|
||||||
start_index=start_index,
|
start_index=start_index,
|
||||||
quoted=False,
|
quoted=None,
|
||||||
|
translation=False,
|
||||||
|
spread=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Move the spread synxtax out of the way, so that we properly handle what's next.
|
||||||
|
is_spread = is_next_token("...")
|
||||||
|
if is_spread:
|
||||||
|
taken_n(3) # ...
|
||||||
|
|
||||||
# Parse the value
|
# Parse the value
|
||||||
#
|
#
|
||||||
# E.g. `height="20"`
|
# E.g. `height="20"`
|
||||||
|
@ -174,17 +199,15 @@ def parse_tag_attrs(text: str) -> Tuple[str, List[TagAttr]]:
|
||||||
if is_next_token(quote_char):
|
if is_next_token(quote_char):
|
||||||
add_token(quote_char)
|
add_token(quote_char)
|
||||||
if is_translation:
|
if is_translation:
|
||||||
value += taken_n(1) # )
|
taken_n(1) # )
|
||||||
quoted = True
|
quoted = quote_char
|
||||||
else:
|
else:
|
||||||
quoted = False
|
quoted = None
|
||||||
value = quote_char + value
|
value = quote_char + value
|
||||||
if is_translation:
|
|
||||||
value = "_(" + value
|
|
||||||
# E.g. `height=20`
|
# E.g. `height=20`
|
||||||
else:
|
else:
|
||||||
value = take_until(TAG_WHITESPACE)
|
value = take_until(TAG_WHITESPACE)
|
||||||
quoted = False
|
quoted = None
|
||||||
|
|
||||||
attrs.append(
|
attrs.append(
|
||||||
TagAttr(
|
TagAttr(
|
||||||
|
@ -192,6 +215,8 @@ def parse_tag_attrs(text: str) -> Tuple[str, List[TagAttr]]:
|
||||||
value=value,
|
value=value,
|
||||||
start_index=start_index,
|
start_index=start_index,
|
||||||
quoted=quoted,
|
quoted=quoted,
|
||||||
|
spread=is_spread,
|
||||||
|
translation=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@ class HtmlAttrsTests(BaseTestCase):
|
||||||
template = Template(self.template_str)
|
template = Template(self.template_str)
|
||||||
|
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
TemplateSyntaxError, "'html_attrs' received some positional argument(s) after some keyword"
|
TemplateSyntaxError, "'html_attrs' received too many positional arguments: ['class']"
|
||||||
):
|
):
|
||||||
template.render(Context({"class_var": "padding-top-8"}))
|
template.render(Context({"class_var": "padding-top-8"}))
|
||||||
|
|
||||||
|
|
|
@ -775,9 +775,7 @@ class SpreadOperatorTests(BaseTestCase):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(TemplateSyntaxError, "'component' received unknown flag 'var_a'"):
|
||||||
TemplateSyntaxError, "'component' received some positional argument(s) after some keyword argument(s)"
|
|
||||||
):
|
|
||||||
Template(template_str)
|
Template(template_str)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
|
@ -12,10 +12,10 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value="val2 two", start_index=28, quoted=True),
|
TagAttr(key="key2", value="val2 two", start_index=28, quoted="'", spread=False, translation=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,11 +24,13 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted=True),
|
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="text", value="organisation's", start_index=46, quoted=True),
|
TagAttr(
|
||||||
|
key="text", value="organisation's", start_index=46, quoted='"', spread=False, translation=False
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,12 +40,14 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted=True),
|
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="text", value="organisation's", start_index=46, quoted=True),
|
TagAttr(
|
||||||
TagAttr(key=None, value="'abc", start_index=68, quoted=False),
|
key="text", value="organisation's", start_index=46, quoted='"', spread=False, translation=False
|
||||||
|
),
|
||||||
|
TagAttr(key=None, value="'abc", start_index=68, quoted=None, spread=False, translation=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,12 +56,14 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted='"', spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value="val2 'two'", start_index=28, quoted=True),
|
TagAttr(key="key2", value="val2 'two'", start_index=28, quoted='"', spread=False, translation=False),
|
||||||
TagAttr(key="text", value='organisation"s', start_index=46, quoted=True),
|
TagAttr(
|
||||||
TagAttr(key=None, value='"abc', start_index=68, quoted=False),
|
key="text", value='organisation"s', start_index=46, quoted="'", spread=False, translation=False
|
||||||
|
), # noqa: E501
|
||||||
|
TagAttr(key=None, value='"abc', start_index=68, quoted=None, spread=False, translation=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -68,12 +74,14 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted=True),
|
TagAttr(key="key2", value='val2 "two"', start_index=28, quoted="'", spread=False, translation=False),
|
||||||
TagAttr(key="text", value="organisation's", start_index=46, quoted=True),
|
TagAttr(
|
||||||
TagAttr(key="value", value="'abc", start_index=68, quoted=False),
|
key="text", value="organisation's", start_index=46, quoted='"', spread=False, translation=False
|
||||||
|
),
|
||||||
|
TagAttr(key="value", value="'abc", start_index=68, quoted=None, spread=False, translation=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,11 +92,13 @@ class TagParserTests(BaseTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attrs,
|
attrs,
|
||||||
[
|
[
|
||||||
TagAttr(key=None, value="component", start_index=0, quoted=False),
|
TagAttr(key=None, value="component", start_index=0, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key=None, value="my_comp", start_index=10, quoted=True),
|
TagAttr(key=None, value="my_comp", start_index=10, quoted='"', spread=False, translation=False),
|
||||||
TagAttr(key="key", value="val", start_index=20, quoted=False),
|
TagAttr(key="key", value="val", start_index=20, quoted=None, spread=False, translation=False),
|
||||||
TagAttr(key="key2", value="val2 'two'", start_index=28, quoted=True),
|
TagAttr(key="key2", value="val2 'two'", start_index=28, quoted='"', spread=False, translation=False),
|
||||||
TagAttr(key="text", value='organisation"s', start_index=46, quoted=True),
|
TagAttr(
|
||||||
TagAttr(key="value", value='"abc', start_index=68, quoted=False),
|
key="text", value='organisation"s', start_index=46, quoted="'", spread=False, translation=False
|
||||||
|
),
|
||||||
|
TagAttr(key="value", value='"abc', start_index=68, quoted=None, spread=False, translation=False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,7 @@ class ParserTest(BaseTestCase):
|
||||||
pos_or_keyword_args=["num", "var"],
|
pos_or_keyword_args=["num", "var"],
|
||||||
keywordonly_args=True,
|
keywordonly_args=True,
|
||||||
)
|
)
|
||||||
tag = _parse_tag(parser, parser.tokens[0], tag_spec=spec)
|
tag = _parse_tag(parser, parser.tokens[0], tag_spec=spec, tag_id="my-id")
|
||||||
|
|
||||||
ctx = {"myvar": {"a": "b"}, "val2": 1}
|
ctx = {"myvar": {"a": "b"}, "val2": 1}
|
||||||
args = safe_resolve_list(ctx, tag.args)
|
args = safe_resolve_list(ctx, tag.args)
|
||||||
|
@ -42,7 +42,7 @@ class ParserTest(BaseTestCase):
|
||||||
tokens = Lexer(template_str).tokenize()
|
tokens = Lexer(template_str).tokenize()
|
||||||
parser = Parser(tokens=tokens)
|
parser = Parser(tokens=tokens)
|
||||||
spec = TagSpec(tag="component", keywordonly_args=True)
|
spec = TagSpec(tag="component", keywordonly_args=True)
|
||||||
tag = _parse_tag(parser, parser.tokens[0], tag_spec=spec)
|
tag = _parse_tag(parser, parser.tokens[0], tag_spec=spec, tag_id="my-id")
|
||||||
|
|
||||||
ctx = Context({"date": 2024, "bzz": "fzz"})
|
ctx = Context({"date": 2024, "bzz": "fzz"})
|
||||||
args = safe_resolve_list(ctx, tag.args)
|
args = safe_resolve_list(ctx, tag.args)
|
||||||
|
|
|
@ -551,7 +551,7 @@ class NestedTagsTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
rendered = Template(template).render(Context())
|
rendered = Template(template).render(Context())
|
||||||
expected = """
|
expected = """
|
||||||
Variable: <strong>organisation's</strong>
|
Variable: <strong>organisation's</strong>
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@ -565,7 +565,7 @@ class NestedTagsTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
rendered = Template(template).render(Context())
|
rendered = Template(template).render(Context())
|
||||||
expected = """
|
expected = """
|
||||||
Variable: <strong>organisation's</strong>
|
Variable: <strong>organisation's</strong>
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue