python-liquid 2.3.6__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- liquid/__about__.py +1 -0
- liquid/__init__.py +161 -187
- liquid/_base_tag.py +66 -0
- liquid/{utils/chain_map.py → _chain_map.py} +18 -19
- liquid/_context.py +465 -0
- liquid/_drop.py +108 -0
- liquid/_filter.py +174 -0
- liquid/_io.py +18 -0
- liquid/_lax_parser_mixin.py +100 -0
- liquid/_lexer.py +93 -0
- liquid/_lexer_extended.py +623 -0
- liquid/_lexer_legacy.py +536 -0
- liquid/_loader.py +84 -0
- liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
- liquid/_markup.py +253 -0
- liquid/_messages.py +292 -0
- liquid/_missing.py +3 -0
- liquid/_nothing.py +28 -0
- liquid/_parser.py +460 -0
- liquid/_parser_extended.py +293 -0
- liquid/_parser_legacy.py +819 -0
- liquid/_resource_limits.py +50 -0
- liquid/_slice.py +41 -0
- liquid/_static_analysis.py +503 -0
- liquid/_strftime.py +186 -0
- liquid/_template.py +536 -0
- liquid/drops/__init__.py +20 -0
- liquid/drops/forloop.py +78 -0
- liquid/drops/iterable.py +25 -0
- liquid/drops/range.py +76 -0
- liquid/drops/self_.py +45 -0
- liquid/drops/tablerowloop.py +142 -0
- liquid/drops/undefined.py +228 -0
- liquid/environment.py +717 -551
- liquid/exceptions.py +67 -214
- liquid/expressions.py +1184 -0
- liquid/filters/__init__.py +134 -0
- liquid/filters/abs.py +7 -0
- liquid/filters/append.py +5 -0
- liquid/filters/at_least.py +9 -0
- liquid/filters/at_most.py +9 -0
- liquid/filters/babel.py +560 -0
- liquid/filters/base64.py +26 -0
- liquid/filters/capitalize.py +5 -0
- liquid/filters/ceil.py +8 -0
- liquid/filters/compact.py +25 -0
- liquid/filters/concat.py +11 -0
- liquid/filters/date.py +27 -0
- liquid/filters/default.py +25 -0
- liquid/filters/divided_by.py +21 -0
- liquid/filters/downcase.py +5 -0
- liquid/filters/escape.py +56 -0
- liquid/filters/find.py +56 -0
- liquid/filters/first.py +19 -0
- liquid/filters/floor.py +8 -0
- liquid/filters/html.py +23 -0
- liquid/filters/index.py +12 -0
- liquid/filters/join.py +13 -0
- liquid/filters/json_.py +30 -0
- liquid/filters/last.py +17 -0
- liquid/filters/map.py +5 -0
- liquid/filters/minus.py +13 -0
- liquid/filters/modulo.py +18 -0
- liquid/filters/newline_to_br.py +16 -0
- liquid/filters/plus.py +13 -0
- liquid/filters/prepend.py +5 -0
- liquid/filters/reject.py +19 -0
- liquid/filters/remove.py +36 -0
- liquid/filters/replace.py +32 -0
- liquid/filters/reverse.py +5 -0
- liquid/filters/round.py +35 -0
- liquid/filters/size.py +25 -0
- liquid/filters/slice.py +39 -0
- liquid/filters/sort.py +148 -0
- liquid/filters/split.py +34 -0
- liquid/filters/squish.py +9 -0
- liquid/filters/strip.py +13 -0
- liquid/filters/strip_html.py +19 -0
- liquid/filters/strip_newlines.py +17 -0
- liquid/filters/sum.py +21 -0
- liquid/filters/times.py +13 -0
- liquid/filters/translate.py +380 -0
- liquid/filters/truncate.py +39 -0
- liquid/filters/uniq.py +11 -0
- liquid/filters/upcase.py +5 -0
- liquid/filters/url.py +18 -0
- liquid/filters/where.py +18 -0
- liquid/loaders/__init__.py +17 -0
- liquid/loaders/caching_file_system_loader.py +64 -0
- liquid/loaders/choice_loader.py +93 -0
- liquid/loaders/dict_loader.py +56 -0
- liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
- liquid/{builtin/loaders → loaders}/mixins.py +39 -51
- liquid/loaders/package_loader.py +110 -0
- liquid/tags/__init__.py +61 -0
- liquid/tags/assign.py +68 -0
- liquid/tags/capture.py +83 -0
- liquid/tags/case.py +199 -0
- liquid/tags/comment.py +56 -0
- liquid/tags/cycle.py +131 -0
- liquid/tags/decrement.py +55 -0
- liquid/tags/doc.py +56 -0
- liquid/tags/echo.py +65 -0
- liquid/tags/else_.py +53 -0
- liquid/tags/error.py +51 -0
- liquid/tags/extends.py +448 -0
- liquid/tags/for_.py +421 -0
- liquid/tags/if_.py +156 -0
- liquid/tags/ifchanged.py +69 -0
- liquid/tags/include.py +223 -0
- liquid/tags/increment.py +55 -0
- liquid/tags/inline_comment.py +70 -0
- liquid/tags/liquid.py +56 -0
- liquid/tags/macro.py +281 -0
- liquid/tags/output.py +58 -0
- liquid/tags/raw.py +60 -0
- liquid/tags/render.py +245 -0
- liquid/tags/tablerow.py +254 -0
- liquid/tags/template_comment.py +56 -0
- liquid/tags/translate.py +316 -0
- liquid/tags/unless.py +121 -0
- liquid/tags/with_.py +83 -0
- liquid/tokens.py +370 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
- python_liquid-3.0.0.dist-info/RECORD +128 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
- liquid/analyze_tags.py +0 -214
- liquid/ast.py +0 -252
- liquid/builtin/__init__.py +0 -203
- liquid/builtin/content.py +0 -41
- liquid/builtin/drops/__init__.py +0 -3
- liquid/builtin/drops/drops.py +0 -13
- liquid/builtin/expressions/__init__.py +0 -47
- liquid/builtin/expressions/_tokenize.py +0 -186
- liquid/builtin/expressions/arguments.py +0 -207
- liquid/builtin/expressions/filtered.py +0 -387
- liquid/builtin/expressions/logical.py +0 -672
- liquid/builtin/expressions/loop.py +0 -275
- liquid/builtin/expressions/path.py +0 -163
- liquid/builtin/expressions/primitive.py +0 -417
- liquid/builtin/filters/__init__.py +0 -0
- liquid/builtin/filters/array.py +0 -329
- liquid/builtin/filters/extra.py +0 -83
- liquid/builtin/filters/math.py +0 -129
- liquid/builtin/filters/misc.py +0 -137
- liquid/builtin/filters/string.py +0 -416
- liquid/builtin/illegal.py +0 -28
- liquid/builtin/loaders/__init__.py +0 -0
- liquid/builtin/loaders/caching_file_system_loader.py +0 -61
- liquid/builtin/loaders/choice_loader.py +0 -94
- liquid/builtin/loaders/dict_loader.py +0 -64
- liquid/builtin/loaders/package_loader.py +0 -132
- liquid/builtin/output.py +0 -74
- liquid/builtin/tags/__init__.py +0 -0
- liquid/builtin/tags/assign_tag.py +0 -75
- liquid/builtin/tags/capture_tag.py +0 -98
- liquid/builtin/tags/case_tag.py +0 -287
- liquid/builtin/tags/comment_tag.py +0 -77
- liquid/builtin/tags/cycle_tag.py +0 -132
- liquid/builtin/tags/decrement_tag.py +0 -59
- liquid/builtin/tags/doc_tag.py +0 -68
- liquid/builtin/tags/echo_tag.py +0 -45
- liquid/builtin/tags/for_tag.py +0 -337
- liquid/builtin/tags/if_tag.py +0 -209
- liquid/builtin/tags/ifchanged_tag.py +0 -94
- liquid/builtin/tags/include_tag.py +0 -251
- liquid/builtin/tags/increment_tag.py +0 -60
- liquid/builtin/tags/inline_comment_tag.py +0 -47
- liquid/builtin/tags/liquid_tag.py +0 -183
- liquid/builtin/tags/render_tag.py +0 -371
- liquid/builtin/tags/tablerow_tag.py +0 -320
- liquid/builtin/tags/unless_tag.py +0 -218
- liquid/context.py +0 -615
- liquid/expression.py +0 -41
- liquid/extra/__init__.py +0 -98
- liquid/extra/filters/__init__.py +0 -33
- liquid/extra/filters/_json.py +0 -33
- liquid/extra/filters/array.py +0 -60
- liquid/extra/filters/babel.py +0 -552
- liquid/extra/filters/html.py +0 -32
- liquid/extra/filters/translate.py +0 -448
- liquid/extra/tags/__init__.py +0 -17
- liquid/extra/tags/_with.py +0 -80
- liquid/extra/tags/extends_tag.py +0 -576
- liquid/extra/tags/macro_tag.py +0 -270
- liquid/extra/tags/snippet_tag.py +0 -107
- liquid/extra/tags/translate_tag.py +0 -388
- liquid/filter.py +0 -230
- liquid/future/__init__.py +0 -3
- liquid/future/environment.py +0 -30
- liquid/future/filters/__init__.py +0 -1
- liquid/future/tags/__init__.py +0 -1
- liquid/lex.py +0 -260
- liquid/limits.py +0 -66
- liquid/loader.py +0 -141
- liquid/messages.py +0 -357
- liquid/mode.py +0 -12
- liquid/output.py +0 -35
- liquid/parser.py +0 -119
- liquid/span.py +0 -35
- liquid/static_analysis.py +0 -443
- liquid/stream.py +0 -148
- liquid/stringify.py +0 -31
- liquid/tag.py +0 -47
- liquid/template.py +0 -658
- liquid/token.py +0 -145
- liquid/undefined.py +0 -218
- liquid/utils/__init__.py +0 -15
- liquid/utils/html.py +0 -57
- liquid/utils/text.py +0 -28
- python_liquid-2.3.6.dist-info/RECORD +0 -96
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(slots=True, kw_only=True)
|
|
5
|
+
class ResourceLimits:
|
|
6
|
+
"""
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
max_assign_score
|
|
10
|
+
The maximum assign score allowed per template before a
|
|
11
|
+
:class:`liquid.ResourceLimitError` is raised.
|
|
12
|
+
max_assign_score_cumulative
|
|
13
|
+
The maximum combined assign score allowed for a template and all its descendant
|
|
14
|
+
partial templates before a :class:`liquid.ResourceLimitError` is raised.
|
|
15
|
+
max_render_score
|
|
16
|
+
The maximum render score allowed per template before a
|
|
17
|
+
:class:`liquid.ResourceLimitError` is raised.
|
|
18
|
+
max_render_score_cumulative
|
|
19
|
+
The maximum combined render score allowed for a template and all its descendant
|
|
20
|
+
partial templates before a :class:`liquid.ResourceLimitError` is raised.
|
|
21
|
+
max_render_size
|
|
22
|
+
The maximum allowed length (in code points) of the output buffer per template
|
|
23
|
+
before a :class:`liquid.ResourceLimitError` is raised.
|
|
24
|
+
max_context_depth
|
|
25
|
+
The maximum number of times a render context can be extended or copied before
|
|
26
|
+
a :class:`liquid.ResourceLimitError` is raised. This helps us guard against
|
|
27
|
+
circular ``{% render %}`` and ``{% include %}`` tags.
|
|
28
|
+
max_loop_iterations
|
|
29
|
+
The maximum number of loop iterations allowed before a
|
|
30
|
+
:class:`liquid.ResourceLimitError` is raised, taking into account nested loops
|
|
31
|
+
that could span multiple partial templates.
|
|
32
|
+
max_block_depth
|
|
33
|
+
The maximum block nesting depth allowed before a :class:`liquid.ResourceLimitError`
|
|
34
|
+
is raised. This guards against specially crafted templates that could hit
|
|
35
|
+
Python's recursion limit during the parsing phase.
|
|
36
|
+
max_expression_depth
|
|
37
|
+
The maximum expression nesting depth allowed before a
|
|
38
|
+
:class:`liquid.ResourceLimitError` is raised. This guards against specially crafted
|
|
39
|
+
expressions that could cause our recursive parser to hit Python's recursion limit.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
max_assign_score: int | None = None
|
|
43
|
+
max_assign_score_cumulative: int | None = None
|
|
44
|
+
max_render_score: int | None = None
|
|
45
|
+
max_render_score_cumulative: int | None = None
|
|
46
|
+
max_render_size: int | None = None
|
|
47
|
+
max_context_depth: int = 30
|
|
48
|
+
max_loop_iterations: int | None = None
|
|
49
|
+
max_block_depth: int = 30
|
|
50
|
+
max_expression_depth: int = 100
|
liquid/_slice.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from collections.abc import Iterator, Sequence
|
|
2
|
+
|
|
3
|
+
from .exceptions import LiquidValueError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def slice_sequence[T](
|
|
7
|
+
seq: Sequence[T], offset: int, limit: int | None, reversed_: bool = False
|
|
8
|
+
) -> tuple[Iterator[T], int]:
|
|
9
|
+
if limit is not None and limit <= 0:
|
|
10
|
+
return iter(()), 0
|
|
11
|
+
|
|
12
|
+
start = max(0, offset)
|
|
13
|
+
end = len(seq) if limit is None else max(0, offset + limit)
|
|
14
|
+
indices = range(start, min(len(seq), end))
|
|
15
|
+
length = len(indices)
|
|
16
|
+
|
|
17
|
+
def it_() -> Iterator[T]:
|
|
18
|
+
for i in reversed(indices) if reversed_ else indices:
|
|
19
|
+
yield seq[i]
|
|
20
|
+
|
|
21
|
+
return it_(), length
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def slice_range(
|
|
25
|
+
it: range, offset: int, limit: int | None, reversed_: bool = False
|
|
26
|
+
) -> range:
|
|
27
|
+
if limit is not None and limit <= 0:
|
|
28
|
+
return range(0)
|
|
29
|
+
|
|
30
|
+
stop_inclusive = it.stop + (1 if it.step > 0 else -1)
|
|
31
|
+
full_range = range(it.start, stop_inclusive, it.step)
|
|
32
|
+
|
|
33
|
+
start = max(0, offset)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
end = len(full_range) if limit is None else max(0, offset + limit)
|
|
37
|
+
except OverflowError:
|
|
38
|
+
raise LiquidValueError("range literal length limit reached")
|
|
39
|
+
|
|
40
|
+
sliced = full_range[start:end]
|
|
41
|
+
return sliced[::-1] if reversed_ else sliced
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from itertools import chain
|
|
7
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
8
|
+
|
|
9
|
+
from . import expressions
|
|
10
|
+
from ._context import RenderContext
|
|
11
|
+
from ._markup import SCOPE_ISOLATED
|
|
12
|
+
from .exceptions import LiquidError
|
|
13
|
+
from .tokens import span, token_value
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
|
|
18
|
+
from ._markup import Expression, Markup
|
|
19
|
+
from ._template import Template
|
|
20
|
+
from .tokens import Token
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True, kw_only=True)
|
|
24
|
+
class _Location:
|
|
25
|
+
template: Template
|
|
26
|
+
token: Token
|
|
27
|
+
|
|
28
|
+
def line_col(self, index: int) -> tuple[int, int]:
|
|
29
|
+
cumulative_length = 0
|
|
30
|
+
target_line_index = -1
|
|
31
|
+
|
|
32
|
+
for i, line in enumerate(self.template.lines):
|
|
33
|
+
cumulative_length += len(line)
|
|
34
|
+
if index < cumulative_length:
|
|
35
|
+
target_line_index = i
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
if target_line_index == -1:
|
|
39
|
+
raise LiquidError("index out of bounds")
|
|
40
|
+
|
|
41
|
+
line_number = target_line_index + 1
|
|
42
|
+
line = self.template.lines[target_line_index]
|
|
43
|
+
column_number = index - (cumulative_length - len(line))
|
|
44
|
+
return line_number, column_number
|
|
45
|
+
|
|
46
|
+
def span(self) -> tuple[tuple[int, int], tuple[int, int]]:
|
|
47
|
+
return self.line_col(self.token[1]), self.line_col(self.token[2])
|
|
48
|
+
|
|
49
|
+
def value(self) -> str:
|
|
50
|
+
return token_value(self.token, self.template.source)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Loc(TypedDict):
|
|
54
|
+
start_index: int
|
|
55
|
+
end_index: int
|
|
56
|
+
start_line: int
|
|
57
|
+
start_column: int
|
|
58
|
+
end_line: int
|
|
59
|
+
end_column: int
|
|
60
|
+
value: str
|
|
61
|
+
template_name: str
|
|
62
|
+
template_path: str | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
type Segments = tuple[int | str | Segments, ...]
|
|
66
|
+
"""Names and indexes that make up a path to a variable."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Var(Loc):
|
|
70
|
+
segments: Segments
|
|
71
|
+
path: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
type Vars = dict[str, list[Var]]
|
|
75
|
+
|
|
76
|
+
type Locations = dict[str, list[Loc]]
|
|
77
|
+
|
|
78
|
+
RE_PROPERTY = re.compile(r"[a-zA-Z_][a-zA-Z0-9_-]*\??")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True, kw_only=True)
|
|
82
|
+
class _StaticVariable:
|
|
83
|
+
segments: Segments
|
|
84
|
+
location: _Location
|
|
85
|
+
|
|
86
|
+
def __eq__(self, other: object) -> bool:
|
|
87
|
+
return isinstance(other, _StaticVariable) and self.segments == other.segments
|
|
88
|
+
|
|
89
|
+
def root(self) -> str:
|
|
90
|
+
segment = self.segments[0] if self.segments else ""
|
|
91
|
+
if isinstance(segment, tuple):
|
|
92
|
+
return self._to_str(segment)
|
|
93
|
+
return str(segment)
|
|
94
|
+
|
|
95
|
+
def __str__(self) -> str:
|
|
96
|
+
return self._to_str(self.segments)
|
|
97
|
+
|
|
98
|
+
def _to_str(self, segments: Segments) -> str:
|
|
99
|
+
def segment_str(segment: int | str | Segments) -> str:
|
|
100
|
+
if isinstance(segment, tuple):
|
|
101
|
+
return f"[{self._to_str(segment)}]"
|
|
102
|
+
if isinstance(segment, str) and RE_PROPERTY.match(segment):
|
|
103
|
+
return f".{segment}"
|
|
104
|
+
return f"[{segment}]"
|
|
105
|
+
|
|
106
|
+
it = iter(segments)
|
|
107
|
+
head = next(it)
|
|
108
|
+
|
|
109
|
+
return "".join(chain(str(head), (segment_str(s) for s in it)))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _StaticScope:
|
|
113
|
+
def __init__(self, globals: set[str]) -> None:
|
|
114
|
+
self.stack: list[set[str]] = [globals]
|
|
115
|
+
|
|
116
|
+
def add(self, name: str) -> None:
|
|
117
|
+
self.stack[0].add(name)
|
|
118
|
+
|
|
119
|
+
def __contains__(self, key: str) -> bool:
|
|
120
|
+
return any(key in scope for scope in self.stack)
|
|
121
|
+
|
|
122
|
+
def pop(self) -> set[str] | None:
|
|
123
|
+
return self.stack.pop() if self.stack else None
|
|
124
|
+
|
|
125
|
+
def push(self, scope: set[str]) -> _StaticScope:
|
|
126
|
+
self.stack.append(scope)
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _VariableMap:
|
|
131
|
+
def __init__(self) -> None:
|
|
132
|
+
self.data: dict[str, list[_StaticVariable]] = {}
|
|
133
|
+
|
|
134
|
+
def add(self, key: _StaticVariable) -> None:
|
|
135
|
+
v = self.get(key)
|
|
136
|
+
if v is not None:
|
|
137
|
+
v.append(key)
|
|
138
|
+
|
|
139
|
+
def get(self, key: _StaticVariable) -> list[_StaticVariable] | None:
|
|
140
|
+
k = key.root()
|
|
141
|
+
if k not in self.data:
|
|
142
|
+
self.data[k] = []
|
|
143
|
+
return self.data[k]
|
|
144
|
+
|
|
145
|
+
def asdict(self) -> Vars:
|
|
146
|
+
obj: Vars = {}
|
|
147
|
+
|
|
148
|
+
for k, v in self.data.items():
|
|
149
|
+
a: list[Var] = []
|
|
150
|
+
|
|
151
|
+
for sv in v:
|
|
152
|
+
((start_line, start_column), (end_line, end_column)) = (
|
|
153
|
+
sv.location.span()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
a.append(
|
|
157
|
+
{
|
|
158
|
+
"segments": sv.segments,
|
|
159
|
+
"path": str(sv),
|
|
160
|
+
"start_index": sv.location.token[1],
|
|
161
|
+
"end_index": sv.location.token[2],
|
|
162
|
+
"start_line": start_line,
|
|
163
|
+
"start_column": start_column,
|
|
164
|
+
"end_line": end_line,
|
|
165
|
+
"end_column": end_column,
|
|
166
|
+
"value": sv.location.value(),
|
|
167
|
+
"template_name": sv.location.template.name,
|
|
168
|
+
"template_path": sv.location.template.path,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
obj[k] = a
|
|
173
|
+
|
|
174
|
+
return obj
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(slots=True, kw_only=True)
|
|
178
|
+
class TemplateAnalysis:
|
|
179
|
+
variables: Vars
|
|
180
|
+
locals: Vars
|
|
181
|
+
globals: Vars
|
|
182
|
+
filters: Locations
|
|
183
|
+
tags: Locations
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def analyze(template: Template, *, include_partials: bool) -> TemplateAnalysis:
|
|
187
|
+
variables = _VariableMap()
|
|
188
|
+
globals = _VariableMap()
|
|
189
|
+
locals = _VariableMap()
|
|
190
|
+
|
|
191
|
+
filters: defaultdict[str, list[_Location]] = defaultdict(list)
|
|
192
|
+
tags: defaultdict[str, list[_Location]] = defaultdict(list)
|
|
193
|
+
|
|
194
|
+
template_scope: set[str] = set()
|
|
195
|
+
root_scope = _StaticScope(template_scope)
|
|
196
|
+
static_context = RenderContext(template)
|
|
197
|
+
|
|
198
|
+
# Names of partial templates that have already been analyzed.
|
|
199
|
+
# Keys are hashes of partial template name and its arguments. If we've
|
|
200
|
+
# visited a template before but with different arguments, later visits
|
|
201
|
+
# only record global variables so as not to double count locals, filters
|
|
202
|
+
# and tags.
|
|
203
|
+
seen: defaultdict[str, set[int | None]] = defaultdict(set)
|
|
204
|
+
|
|
205
|
+
def visit(
|
|
206
|
+
node: Markup,
|
|
207
|
+
template: Template,
|
|
208
|
+
scope: _StaticScope,
|
|
209
|
+
*,
|
|
210
|
+
just_globals: bool = False,
|
|
211
|
+
) -> None:
|
|
212
|
+
if template.name and not just_globals:
|
|
213
|
+
seen[template.name].add(None)
|
|
214
|
+
|
|
215
|
+
# Update tags
|
|
216
|
+
# Markup with empty `node.tag` is silenced.
|
|
217
|
+
if not just_globals and node.tag:
|
|
218
|
+
tags[node.tag].append(_Location(template=template, token=node.token))
|
|
219
|
+
|
|
220
|
+
# Update variables from `node.expressions()`
|
|
221
|
+
for expr in node.expressions():
|
|
222
|
+
analyze_variables(
|
|
223
|
+
expr,
|
|
224
|
+
template,
|
|
225
|
+
scope,
|
|
226
|
+
globals,
|
|
227
|
+
variables if not just_globals else _VariableMap(),
|
|
228
|
+
static_context,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if not just_globals:
|
|
232
|
+
# Update filters from expr
|
|
233
|
+
for filter_name, span in extract_filters(
|
|
234
|
+
expr, template, static_context
|
|
235
|
+
):
|
|
236
|
+
filters[filter_name].append(span)
|
|
237
|
+
|
|
238
|
+
# Update template scope from `node.template_scope()`
|
|
239
|
+
for name in node.template_scope():
|
|
240
|
+
scope.add(name.value)
|
|
241
|
+
locals.add(
|
|
242
|
+
_StaticVariable(
|
|
243
|
+
segments=(name.value,),
|
|
244
|
+
location=_Location(template=template, token=name.span),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Set block scope before descending into child nodes.
|
|
249
|
+
block_scope = node.block_scope()
|
|
250
|
+
if block_scope:
|
|
251
|
+
scope.push({n.value for n in block_scope})
|
|
252
|
+
|
|
253
|
+
for child in node.children():
|
|
254
|
+
visit(child, template, scope, just_globals=just_globals)
|
|
255
|
+
|
|
256
|
+
if block_scope:
|
|
257
|
+
scope.pop()
|
|
258
|
+
|
|
259
|
+
# Descend into partial templates
|
|
260
|
+
if include_partials:
|
|
261
|
+
for partial in node.partials(static_context):
|
|
262
|
+
template_name = partial.template.name
|
|
263
|
+
|
|
264
|
+
# If we've seen this partial before but with different arguments,
|
|
265
|
+
# we might want to visit it again but only capture globals.
|
|
266
|
+
just_globals_ = template_name in seen
|
|
267
|
+
|
|
268
|
+
if partial.key not in seen[template_name]:
|
|
269
|
+
seen[template_name].add(partial.key)
|
|
270
|
+
partial_globals = {n.value for n in partial.in_scope}
|
|
271
|
+
|
|
272
|
+
partial_scope = (
|
|
273
|
+
_StaticScope(partial_globals)
|
|
274
|
+
if partial.scope == SCOPE_ISOLATED
|
|
275
|
+
else root_scope.push(partial_globals)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
for partial_node in partial.template.nodes:
|
|
279
|
+
if not isinstance(partial_node, str):
|
|
280
|
+
visit(
|
|
281
|
+
partial_node,
|
|
282
|
+
partial.template,
|
|
283
|
+
partial_scope,
|
|
284
|
+
just_globals=just_globals_,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if partial.scope != SCOPE_ISOLATED:
|
|
288
|
+
partial_scope.pop()
|
|
289
|
+
|
|
290
|
+
for node in template.nodes:
|
|
291
|
+
if not isinstance(node, str):
|
|
292
|
+
visit(node, template, root_scope)
|
|
293
|
+
|
|
294
|
+
return TemplateAnalysis(
|
|
295
|
+
variables=variables.asdict(),
|
|
296
|
+
locals=locals.asdict(),
|
|
297
|
+
globals=globals.asdict(),
|
|
298
|
+
filters=to_locations(filters),
|
|
299
|
+
tags=to_locations(tags),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def analyze_async(
|
|
304
|
+
template: Template, *, include_partials: bool
|
|
305
|
+
) -> TemplateAnalysis:
|
|
306
|
+
variables = _VariableMap()
|
|
307
|
+
globals = _VariableMap()
|
|
308
|
+
locals = _VariableMap()
|
|
309
|
+
|
|
310
|
+
filters: defaultdict[str, list[_Location]] = defaultdict(list)
|
|
311
|
+
tags: defaultdict[str, list[_Location]] = defaultdict(list)
|
|
312
|
+
|
|
313
|
+
template_scope: set[str] = set()
|
|
314
|
+
root_scope = _StaticScope(template_scope)
|
|
315
|
+
static_context = RenderContext(template)
|
|
316
|
+
|
|
317
|
+
# Names of partial templates that have already been analyzed.
|
|
318
|
+
# Keys are hashes of partial template name and its arguments. If we've
|
|
319
|
+
# visited a template before but with different arguments, later visits
|
|
320
|
+
# only record global variables so as not to double count locals, filters
|
|
321
|
+
# and tags.
|
|
322
|
+
seen: defaultdict[str, set[int | None]] = defaultdict(set)
|
|
323
|
+
|
|
324
|
+
async def visit(
|
|
325
|
+
node: Markup,
|
|
326
|
+
template: Template,
|
|
327
|
+
scope: _StaticScope,
|
|
328
|
+
*,
|
|
329
|
+
just_globals: bool = False,
|
|
330
|
+
) -> None:
|
|
331
|
+
if template.name and not just_globals:
|
|
332
|
+
seen[template.name].add(None)
|
|
333
|
+
|
|
334
|
+
# Update tags
|
|
335
|
+
# Markup with empty `node.tag` is silenced.
|
|
336
|
+
if not just_globals and node.tag:
|
|
337
|
+
tags[node.tag].append(_Location(template=template, token=node.token))
|
|
338
|
+
|
|
339
|
+
# Update variables from `node.expressions()`
|
|
340
|
+
for expr in node.expressions():
|
|
341
|
+
analyze_variables(
|
|
342
|
+
expr,
|
|
343
|
+
template,
|
|
344
|
+
scope,
|
|
345
|
+
globals,
|
|
346
|
+
variables if not just_globals else _VariableMap(),
|
|
347
|
+
static_context,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if not just_globals:
|
|
351
|
+
# Update filters from expr
|
|
352
|
+
for filter_name, span in extract_filters(
|
|
353
|
+
expr, template, static_context
|
|
354
|
+
):
|
|
355
|
+
filters[filter_name].append(span)
|
|
356
|
+
|
|
357
|
+
# Update template scope from `node.template_scope()`
|
|
358
|
+
for name in node.template_scope():
|
|
359
|
+
scope.add(name.value)
|
|
360
|
+
locals.add(
|
|
361
|
+
_StaticVariable(
|
|
362
|
+
segments=(name.value,),
|
|
363
|
+
location=_Location(template=template, token=name.span),
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Set block scope before descending into child nodes.
|
|
368
|
+
block_scope = node.block_scope()
|
|
369
|
+
if block_scope:
|
|
370
|
+
scope.push({n.value for n in block_scope})
|
|
371
|
+
|
|
372
|
+
for child in node.children():
|
|
373
|
+
await visit(child, template, scope, just_globals=just_globals)
|
|
374
|
+
|
|
375
|
+
if block_scope:
|
|
376
|
+
scope.pop()
|
|
377
|
+
|
|
378
|
+
# Descend into partial templates
|
|
379
|
+
if include_partials:
|
|
380
|
+
for partial in await node.partials_async(static_context):
|
|
381
|
+
temlate_name = partial.template.name
|
|
382
|
+
|
|
383
|
+
# If we've seen this partial before but with different arguments,
|
|
384
|
+
# we might want to visit it again but only capture globals.
|
|
385
|
+
just_globals_ = temlate_name in seen
|
|
386
|
+
|
|
387
|
+
if partial.key not in seen[temlate_name]:
|
|
388
|
+
seen[temlate_name].add(partial.key)
|
|
389
|
+
partial_globals = {n.value for n in partial.in_scope}
|
|
390
|
+
|
|
391
|
+
partial_scope = (
|
|
392
|
+
_StaticScope(partial_globals)
|
|
393
|
+
if partial.scope == SCOPE_ISOLATED
|
|
394
|
+
else root_scope.push(partial_globals)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
for partial_node in partial.template.nodes:
|
|
398
|
+
if not isinstance(partial_node, str):
|
|
399
|
+
await visit(
|
|
400
|
+
partial_node,
|
|
401
|
+
partial.template,
|
|
402
|
+
partial_scope,
|
|
403
|
+
just_globals=just_globals_,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if partial.scope != SCOPE_ISOLATED:
|
|
407
|
+
partial_scope.pop()
|
|
408
|
+
|
|
409
|
+
for node in template.nodes:
|
|
410
|
+
if not isinstance(node, str):
|
|
411
|
+
await visit(node, template, root_scope)
|
|
412
|
+
|
|
413
|
+
return TemplateAnalysis(
|
|
414
|
+
variables=variables.asdict(),
|
|
415
|
+
locals=locals.asdict(),
|
|
416
|
+
globals=globals.asdict(),
|
|
417
|
+
filters=to_locations(filters),
|
|
418
|
+
tags=to_locations(tags),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def analyze_variables(
|
|
423
|
+
expression: Expression,
|
|
424
|
+
template: Template,
|
|
425
|
+
scope: _StaticScope,
|
|
426
|
+
globals: _VariableMap,
|
|
427
|
+
variables: _VariableMap,
|
|
428
|
+
static_context: RenderContext,
|
|
429
|
+
) -> None:
|
|
430
|
+
if isinstance(expression, expressions.Variable):
|
|
431
|
+
v = _StaticVariable(
|
|
432
|
+
segments=segments(expression, template),
|
|
433
|
+
location=_Location(template=template, token=expression.span),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
variables.add(v)
|
|
437
|
+
|
|
438
|
+
if str(expression.root) not in scope:
|
|
439
|
+
globals.add(v)
|
|
440
|
+
|
|
441
|
+
for expr in expression.children():
|
|
442
|
+
analyze_variables(expr, template, scope, globals, variables, static_context)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def segments(variable: expressions.Variable, template: Template) -> Segments:
|
|
446
|
+
|
|
447
|
+
def generate_segments() -> Iterable[int | str | Segments]:
|
|
448
|
+
if isinstance(variable.root, expressions.VariableSegment):
|
|
449
|
+
yield segments(variable.root.var, template)
|
|
450
|
+
else:
|
|
451
|
+
yield variable.root.value
|
|
452
|
+
|
|
453
|
+
if variable.selectors:
|
|
454
|
+
for s in variable.selectors:
|
|
455
|
+
if isinstance(s, expressions.VariableSegment):
|
|
456
|
+
yield segments(s.var, template)
|
|
457
|
+
else:
|
|
458
|
+
yield s.value
|
|
459
|
+
|
|
460
|
+
return tuple(generate_segments())
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def extract_filters(
|
|
464
|
+
expression: Expression, template: Template, static_context: RenderContext
|
|
465
|
+
) -> Iterable[tuple[str, _Location]]:
|
|
466
|
+
if isinstance(expression, expressions.Filtered):
|
|
467
|
+
yield (
|
|
468
|
+
expression.name.value,
|
|
469
|
+
_Location(
|
|
470
|
+
template=template, token=span(expression.name.token, expression.span)
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
for expr in expression.children():
|
|
475
|
+
yield from extract_filters(expr, template, static_context)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def to_locations(d: defaultdict[str, list[_Location]]) -> Locations:
|
|
479
|
+
result: Locations = {}
|
|
480
|
+
|
|
481
|
+
for k, v in d.items():
|
|
482
|
+
a: list[Loc] = []
|
|
483
|
+
|
|
484
|
+
for location in v:
|
|
485
|
+
((start_line, start_column), (end_line, end_column)) = location.span()
|
|
486
|
+
|
|
487
|
+
a.append(
|
|
488
|
+
{
|
|
489
|
+
"start_index": location.token[1],
|
|
490
|
+
"end_index": location.token[2],
|
|
491
|
+
"start_line": start_line,
|
|
492
|
+
"start_column": start_column,
|
|
493
|
+
"end_line": end_line,
|
|
494
|
+
"end_column": end_column,
|
|
495
|
+
"value": location.value(),
|
|
496
|
+
"template_name": location.template.name,
|
|
497
|
+
"template_path": location.template.path,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
result[k] = a
|
|
502
|
+
|
|
503
|
+
return result
|