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
liquid/_parser_legacy.py
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from . import expressions
|
|
6
|
+
from ._parser import (
|
|
7
|
+
PRECEDENCE_COMPARISON,
|
|
8
|
+
PRECEDENCE_FILTER_ARG,
|
|
9
|
+
PRECEDENCE_LOGICAL_AND,
|
|
10
|
+
PRECEDENCE_LOGICAL_OR,
|
|
11
|
+
PRECEDENCE_LOGICAL_RIGHT,
|
|
12
|
+
PRECEDENCE_LOWEST,
|
|
13
|
+
PRECEDENCE_MEMBERSHIP,
|
|
14
|
+
TERMINATE_EXPRESSION,
|
|
15
|
+
Parser,
|
|
16
|
+
Precedence,
|
|
17
|
+
)
|
|
18
|
+
from .exceptions import LiquidSyntaxError, ResourceLimitError
|
|
19
|
+
from .tags import OutputStatement, TemplateComment
|
|
20
|
+
from .tokens import *
|
|
21
|
+
from .tokens import TOKEN_NAMES, TokenKind, span, token_value
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Collection
|
|
25
|
+
|
|
26
|
+
from ._markup import Block, Expression, Markup
|
|
27
|
+
from .environment import Environment
|
|
28
|
+
from .tokens import Token
|
|
29
|
+
|
|
30
|
+
LEGACY_PRECEDENCES: dict[TokenKind, Precedence] = {
|
|
31
|
+
TOKEN_AND: PRECEDENCE_LOGICAL_RIGHT,
|
|
32
|
+
TOKEN_OR: PRECEDENCE_LOGICAL_RIGHT,
|
|
33
|
+
TOKEN_CONTAINS: PRECEDENCE_MEMBERSHIP,
|
|
34
|
+
TOKEN_EQ: PRECEDENCE_COMPARISON,
|
|
35
|
+
TOKEN_NE: PRECEDENCE_COMPARISON,
|
|
36
|
+
TOKEN_LT: PRECEDENCE_COMPARISON,
|
|
37
|
+
TOKEN_LE: PRECEDENCE_COMPARISON,
|
|
38
|
+
TOKEN_GT: PRECEDENCE_COMPARISON,
|
|
39
|
+
TOKEN_GE: PRECEDENCE_COMPARISON,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
CONVENTIONAL_PRECEDENCES: dict[TokenKind, Precedence] = {
|
|
43
|
+
TOKEN_AND: PRECEDENCE_LOGICAL_AND,
|
|
44
|
+
TOKEN_OR: PRECEDENCE_LOGICAL_OR,
|
|
45
|
+
TOKEN_CONTAINS: PRECEDENCE_MEMBERSHIP,
|
|
46
|
+
TOKEN_EQ: PRECEDENCE_COMPARISON,
|
|
47
|
+
TOKEN_NE: PRECEDENCE_COMPARISON,
|
|
48
|
+
TOKEN_LT: PRECEDENCE_COMPARISON,
|
|
49
|
+
TOKEN_LE: PRECEDENCE_COMPARISON,
|
|
50
|
+
TOKEN_GT: PRECEDENCE_COMPARISON,
|
|
51
|
+
TOKEN_GE: PRECEDENCE_COMPARISON,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LegacyParser(Parser):
|
|
56
|
+
"""A single pass template parser compatible with Shopify/liquid v5.12.0."""
|
|
57
|
+
|
|
58
|
+
INFIX_OPERATORS: frozenset[TokenKind] = frozenset(
|
|
59
|
+
[
|
|
60
|
+
TOKEN_AND,
|
|
61
|
+
TOKEN_OR,
|
|
62
|
+
TOKEN_CONTAINS,
|
|
63
|
+
TOKEN_EQ,
|
|
64
|
+
TOKEN_NE,
|
|
65
|
+
TOKEN_LT,
|
|
66
|
+
TOKEN_LE,
|
|
67
|
+
TOKEN_GT,
|
|
68
|
+
TOKEN_GE,
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
TERMINATE_FILTER: frozenset[TokenKind] = frozenset(
|
|
73
|
+
[
|
|
74
|
+
TOKEN_WC,
|
|
75
|
+
TOKEN_OUT_END,
|
|
76
|
+
TOKEN_TAG_END,
|
|
77
|
+
TOKEN_TEXT,
|
|
78
|
+
TOKEN_RPAREN,
|
|
79
|
+
TOKEN_EOI,
|
|
80
|
+
TOKEN_PIPE,
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
PATH_PUNCTUATION: frozenset[TokenKind] = frozenset(
|
|
85
|
+
[
|
|
86
|
+
TOKEN_DOT,
|
|
87
|
+
TOKEN_LBRACKET,
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
STRING_LITERAL_KINDS: frozenset[TokenKind] = frozenset(
|
|
92
|
+
[
|
|
93
|
+
TOKEN_SINGLE_QUOTED,
|
|
94
|
+
TOKEN_DOUBLE_QUOTED,
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
KEYWORD_ARGUMENT_DELIMITERS: frozenset[TokenKind] = frozenset(
|
|
99
|
+
[
|
|
100
|
+
TOKEN_COLON,
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
PATH_NAME_KINDS: frozenset[TokenKind] = frozenset(
|
|
105
|
+
(
|
|
106
|
+
TOKEN_IDENT,
|
|
107
|
+
TOKEN_BLANK,
|
|
108
|
+
TOKEN_EMPTY,
|
|
109
|
+
TOKEN_FALSE,
|
|
110
|
+
TOKEN_TRUE,
|
|
111
|
+
TOKEN_NULL,
|
|
112
|
+
TOKEN_NIL,
|
|
113
|
+
TOKEN_IF,
|
|
114
|
+
TOKEN_ELSE,
|
|
115
|
+
TOKEN_NOT,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
__slots__ = ("precedences",)
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
env: Environment,
|
|
124
|
+
source: str,
|
|
125
|
+
template_name: str,
|
|
126
|
+
tokens: list[Token],
|
|
127
|
+
) -> None:
|
|
128
|
+
super().__init__(env, source, template_name, tokens)
|
|
129
|
+
if env.conventional_operator_precedence:
|
|
130
|
+
self.precedences = CONVENTIONAL_PRECEDENCES
|
|
131
|
+
else:
|
|
132
|
+
self.precedences = LEGACY_PRECEDENCES
|
|
133
|
+
|
|
134
|
+
def parse_block(self, end: Collection[str]) -> Block:
|
|
135
|
+
nodes: Block = []
|
|
136
|
+
|
|
137
|
+
if self.block_depth > self.env.resource_limits.max_block_depth:
|
|
138
|
+
raise ResourceLimitError("maximum block depth exceeded")
|
|
139
|
+
|
|
140
|
+
self.block_depth += 1
|
|
141
|
+
|
|
142
|
+
while 1:
|
|
143
|
+
token = self.next()
|
|
144
|
+
kind = token[0]
|
|
145
|
+
|
|
146
|
+
if kind == TOKEN_TEXT:
|
|
147
|
+
nodes.append(
|
|
148
|
+
self.env.trim(
|
|
149
|
+
token_value(token, self.source),
|
|
150
|
+
self.whitespace_control_carry,
|
|
151
|
+
self.peek_whitespace_control(),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
elif kind == TOKEN_OUT_START:
|
|
156
|
+
nodes.append(self.parse_output())
|
|
157
|
+
|
|
158
|
+
elif kind == TOKEN_TAG_START:
|
|
159
|
+
if self.peek_tag_name() in end:
|
|
160
|
+
self.pos -= 1
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
nodes.append(self.parse_tag())
|
|
164
|
+
|
|
165
|
+
elif kind == TOKEN_EOI:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
elif kind == TOKEN_COMMENT_START:
|
|
169
|
+
nodes.append(TemplateComment.parse(token, self))
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
raise LiquidSyntaxError(
|
|
173
|
+
f"unexpected {TOKEN_NAMES[kind]}",
|
|
174
|
+
token=token,
|
|
175
|
+
source=self.source,
|
|
176
|
+
name=self.template_name,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
self.block_depth -= 1
|
|
180
|
+
return nodes
|
|
181
|
+
|
|
182
|
+
def parse_output(self) -> Markup:
|
|
183
|
+
token = self.tokens[self.pos - 1]
|
|
184
|
+
|
|
185
|
+
if self.kind() == TOKEN_WC and self.peek_kind() == TOKEN_OUT_END:
|
|
186
|
+
# Special case for `{{-}}`
|
|
187
|
+
self.carry_whitespace_control()
|
|
188
|
+
self.eat(TOKEN_OUT_END)
|
|
189
|
+
return OutputStatement(token, expressions.NullLiteral(token))
|
|
190
|
+
|
|
191
|
+
self.skip_whitespace_control()
|
|
192
|
+
expr = self.parse_filtered_expression(PRECEDENCE_LOWEST, infix=True)
|
|
193
|
+
self.carry_whitespace_control()
|
|
194
|
+
self.eat(
|
|
195
|
+
TOKEN_OUT_END, message="malformed expression or missing output delimiter"
|
|
196
|
+
)
|
|
197
|
+
return OutputStatement(token, expr)
|
|
198
|
+
|
|
199
|
+
def parse_tag(self) -> Markup:
|
|
200
|
+
self.skip_whitespace_control()
|
|
201
|
+
token = self.eat(TOKEN_TAG_NAME, message="missing tag name")
|
|
202
|
+
name = token_value(token, self.source)
|
|
203
|
+
tag = self.env.tags.get(name)
|
|
204
|
+
|
|
205
|
+
if tag is None:
|
|
206
|
+
raise LiquidSyntaxError(
|
|
207
|
+
f"unexpected tag {name!r}",
|
|
208
|
+
token=token,
|
|
209
|
+
source=self.source,
|
|
210
|
+
name=self.template_name,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return tag.parse(token, self)
|
|
214
|
+
|
|
215
|
+
def parse_expression(
|
|
216
|
+
self,
|
|
217
|
+
precedence: Precedence = PRECEDENCE_LOWEST,
|
|
218
|
+
*,
|
|
219
|
+
infix: bool = False,
|
|
220
|
+
expect_end_of_expression: bool = False,
|
|
221
|
+
) -> Expression:
|
|
222
|
+
expr = self.parse_primary_expression(precedence, infix=infix)
|
|
223
|
+
|
|
224
|
+
if expect_end_of_expression and self.kind() not in TERMINATE_EXPRESSION:
|
|
225
|
+
raise LiquidSyntaxError(
|
|
226
|
+
"unknown or missing operator",
|
|
227
|
+
token=self.current(),
|
|
228
|
+
source=self.source,
|
|
229
|
+
name=self.template_name,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return expr
|
|
233
|
+
|
|
234
|
+
def parse_primary_expression(
|
|
235
|
+
self, precedence: Precedence = PRECEDENCE_LOWEST, *, infix: bool = False
|
|
236
|
+
) -> Expression:
|
|
237
|
+
self.expression_depth += 1
|
|
238
|
+
if self.expression_depth > self.env.resource_limits.max_expression_depth:
|
|
239
|
+
raise ResourceLimitError("maximum expression depth exceeded")
|
|
240
|
+
|
|
241
|
+
left = self.parse_basic_expression()
|
|
242
|
+
|
|
243
|
+
if not infix:
|
|
244
|
+
self.expression_depth -= 1
|
|
245
|
+
return left
|
|
246
|
+
|
|
247
|
+
while 1:
|
|
248
|
+
kind = self.kind()
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
self.precedences.get(kind, PRECEDENCE_LOWEST) < precedence
|
|
252
|
+
or kind not in self.INFIX_OPERATORS
|
|
253
|
+
):
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
left = self.parse_infix(left)
|
|
257
|
+
|
|
258
|
+
self.expression_depth -= 1
|
|
259
|
+
return left
|
|
260
|
+
|
|
261
|
+
def parse_basic_expression(self) -> Expression:
|
|
262
|
+
kind = self.kind()
|
|
263
|
+
|
|
264
|
+
if kind == TOKEN_BLANK:
|
|
265
|
+
return self.parse_blank()
|
|
266
|
+
|
|
267
|
+
if kind in (TOKEN_DOUBLE_QUOTE, TOKEN_SINGLE_QUOTE):
|
|
268
|
+
return self.parse_string_literal()
|
|
269
|
+
|
|
270
|
+
if kind == TOKEN_EMPTY:
|
|
271
|
+
return self.parse_empty()
|
|
272
|
+
|
|
273
|
+
if kind == TOKEN_FALSE:
|
|
274
|
+
return self.parse_false_literal()
|
|
275
|
+
|
|
276
|
+
if kind == TOKEN_FLOAT:
|
|
277
|
+
return self.parse_float_literal()
|
|
278
|
+
|
|
279
|
+
if kind in (TOKEN_IDENT, TOKEN_IF, TOKEN_ELSE, TOKEN_NOT):
|
|
280
|
+
return self.parse_path()
|
|
281
|
+
|
|
282
|
+
if kind == TOKEN_INT:
|
|
283
|
+
return self.parse_int_literal()
|
|
284
|
+
|
|
285
|
+
if kind == TOKEN_LBRACKET:
|
|
286
|
+
return self.parse_path()
|
|
287
|
+
|
|
288
|
+
if kind == TOKEN_LPAREN:
|
|
289
|
+
return self.parse_range_literal()
|
|
290
|
+
|
|
291
|
+
if kind in (TOKEN_NIL, TOKEN_NULL):
|
|
292
|
+
return self.parse_null_literal()
|
|
293
|
+
|
|
294
|
+
if kind == TOKEN_TRUE:
|
|
295
|
+
return self.parse_true_literal()
|
|
296
|
+
|
|
297
|
+
token = self.current()
|
|
298
|
+
raise LiquidSyntaxError(
|
|
299
|
+
f"expected a basic expression, found {TOKEN_NAMES[token[0]]}",
|
|
300
|
+
token=token,
|
|
301
|
+
source=self.source,
|
|
302
|
+
name=self.template_name,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def parse_blank(self) -> Expression:
|
|
306
|
+
# `blank` is a reserved word when it is not followed by segments.
|
|
307
|
+
if self.peek_kind() in self.PATH_PUNCTUATION:
|
|
308
|
+
return self.parse_path()
|
|
309
|
+
return expressions.Blank(self.next())
|
|
310
|
+
|
|
311
|
+
def parse_empty(self) -> Expression:
|
|
312
|
+
# `empty` is a reserved word when it is not followed by segments.
|
|
313
|
+
if self.peek_kind() in self.PATH_PUNCTUATION:
|
|
314
|
+
return self.parse_path()
|
|
315
|
+
return expressions.Empty(self.next())
|
|
316
|
+
|
|
317
|
+
def parse_false_literal(self) -> Expression:
|
|
318
|
+
# `false` is a reserved word when it is not followed by segments.
|
|
319
|
+
if self.peek_kind() in self.PATH_PUNCTUATION:
|
|
320
|
+
return self.parse_path()
|
|
321
|
+
return expressions.BooleanLiteral(self.next(), False)
|
|
322
|
+
|
|
323
|
+
def parse_true_literal(self) -> Expression:
|
|
324
|
+
# `true` is a reserved word when it is not followed by segments.
|
|
325
|
+
if self.peek_kind() in self.PATH_PUNCTUATION:
|
|
326
|
+
return self.parse_path()
|
|
327
|
+
return expressions.BooleanLiteral(self.next(), True)
|
|
328
|
+
|
|
329
|
+
def parse_null_literal(self) -> Expression:
|
|
330
|
+
# `null` and `nil` are reserved words when it is not followed by segments.
|
|
331
|
+
if self.peek_kind() in self.PATH_PUNCTUATION:
|
|
332
|
+
return self.parse_path()
|
|
333
|
+
return expressions.NullLiteral(self.next())
|
|
334
|
+
|
|
335
|
+
def parse_float_literal(self) -> Expression:
|
|
336
|
+
token = self.next()
|
|
337
|
+
return expressions.FloatLiteral(token, float(token_value(token, self.source)))
|
|
338
|
+
|
|
339
|
+
def parse_int_literal(self) -> Expression:
|
|
340
|
+
token = self.next()
|
|
341
|
+
return expressions.IntegerLiteral(token, int(token_value(token, self.source)))
|
|
342
|
+
|
|
343
|
+
def parse_range_literal(self) -> expressions.RangeLiteral:
|
|
344
|
+
token = self.eat(TOKEN_LPAREN)
|
|
345
|
+
start = self.parse_expression(PRECEDENCE_LOWEST, infix=False)
|
|
346
|
+
|
|
347
|
+
self.eat(TOKEN_DOUBLE_DOT, message="expected a '..'")
|
|
348
|
+
|
|
349
|
+
if self.kind() == TOKEN_DOT:
|
|
350
|
+
raise LiquidSyntaxError(
|
|
351
|
+
"too many dots",
|
|
352
|
+
token=self.current(),
|
|
353
|
+
source=self.source,
|
|
354
|
+
name=self.template_name,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
stop = self.parse_expression(PRECEDENCE_LOWEST, infix=False)
|
|
358
|
+
self.eat(TOKEN_RPAREN, message="missing bracket")
|
|
359
|
+
return expressions.RangeLiteral(token, start, stop)
|
|
360
|
+
|
|
361
|
+
def parse_string_literal(self) -> expressions.StringLiteral:
|
|
362
|
+
token = self.next()
|
|
363
|
+
|
|
364
|
+
if self.kind() == token[0]:
|
|
365
|
+
# Empty string
|
|
366
|
+
return expressions.StringLiteral(token, "", span(token, self.next()))
|
|
367
|
+
|
|
368
|
+
return expressions.StringLiteral(
|
|
369
|
+
token,
|
|
370
|
+
token_value(self.eat_one_of(self.STRING_LITERAL_KINDS), self.source),
|
|
371
|
+
span(token, self.eat(token[0])),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def parse_path(self, depth: int = 0) -> expressions.Variable:
|
|
375
|
+
# Arbitrary limit. It's hard to imagine a legitimate case where there's more
|
|
376
|
+
# than a couple of nested variables.
|
|
377
|
+
if depth > 20:
|
|
378
|
+
raise ResourceLimitError("maximum variable depth reached")
|
|
379
|
+
|
|
380
|
+
token = self.current()
|
|
381
|
+
kind = token[0]
|
|
382
|
+
|
|
383
|
+
root: expressions.Name | expressions.NameSegment | expressions.VariableSegment
|
|
384
|
+
|
|
385
|
+
if kind in self.PATH_NAME_KINDS:
|
|
386
|
+
self.pos += 1
|
|
387
|
+
root = expressions.Name(token, token_value(token, self.source))
|
|
388
|
+
|
|
389
|
+
else:
|
|
390
|
+
lbracket_token = self.eat(TOKEN_LBRACKET)
|
|
391
|
+
|
|
392
|
+
if not self.env.allow_bare_brackets:
|
|
393
|
+
raise LiquidSyntaxError(
|
|
394
|
+
"bare brackets are not allowed, use 'self[...]' instead",
|
|
395
|
+
token=lbracket_token,
|
|
396
|
+
source=self.source,
|
|
397
|
+
name=self.template_name,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if self.kind() in self.PATH_NAME_KINDS:
|
|
401
|
+
var = self.parse_path(depth=depth + 1)
|
|
402
|
+
root = expressions.VariableSegment(
|
|
403
|
+
lbracket_token, var, span(lbracket_token, self.eat(TOKEN_RBRACKET))
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
name = self.parse_string_literal()
|
|
407
|
+
root = expressions.NameSegment(
|
|
408
|
+
lbracket_token,
|
|
409
|
+
name.value,
|
|
410
|
+
span(lbracket_token, self.eat(TOKEN_RBRACKET)),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if self.kind() in (TOKEN_LBRACKET, TOKEN_DOT):
|
|
414
|
+
return expressions.Variable(token, root, self.parse_path_selectors(depth))
|
|
415
|
+
|
|
416
|
+
return expressions.Variable(token, root, None)
|
|
417
|
+
|
|
418
|
+
def parse_path_selectors(self, depth: int) -> list[expressions.PathSegment]:
|
|
419
|
+
selectors: list[expressions.PathSegment] = []
|
|
420
|
+
|
|
421
|
+
while 1:
|
|
422
|
+
kind = self.kind()
|
|
423
|
+
|
|
424
|
+
if kind == TOKEN_LBRACKET:
|
|
425
|
+
selectors.append(self.parse_bracketed_segment(depth))
|
|
426
|
+
|
|
427
|
+
elif kind == TOKEN_DOT:
|
|
428
|
+
self.pos += 1
|
|
429
|
+
token = self.next()
|
|
430
|
+
selector_kind = token[0]
|
|
431
|
+
|
|
432
|
+
if selector_kind in self.PATH_NAME_KINDS:
|
|
433
|
+
selectors.append(
|
|
434
|
+
expressions.Name(token, token_value(token, self.source))
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
elif selector_kind == TOKEN_INT and self.env.shorthand_indexes:
|
|
438
|
+
selectors.append(
|
|
439
|
+
expressions.IntegerLiteral(
|
|
440
|
+
token, int(token_value(token, self.source))
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
elif selector_kind == TOKEN_FLOAT and self.env.shorthand_indexes:
|
|
445
|
+
# XXX: bit of a hack
|
|
446
|
+
# Split a float token into two indexes
|
|
447
|
+
a, b = token_value(token, self.source).split(".")
|
|
448
|
+
selectors.append(expressions.IntegerLiteral(token, int(a)))
|
|
449
|
+
selectors.append(expressions.IntegerLiteral(token, int(b)))
|
|
450
|
+
|
|
451
|
+
else:
|
|
452
|
+
raise LiquidSyntaxError(
|
|
453
|
+
f"expected a shorthand selector, found {TOKEN_NAMES[kind]}",
|
|
454
|
+
token=token,
|
|
455
|
+
source=self.source,
|
|
456
|
+
name=self.template_name,
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
break
|
|
460
|
+
|
|
461
|
+
return selectors
|
|
462
|
+
|
|
463
|
+
def parse_bracketed_segment(self, depth: int) -> expressions.PathSegment:
|
|
464
|
+
lbracket_token = self.eat(TOKEN_LBRACKET)
|
|
465
|
+
token = self.next()
|
|
466
|
+
kind = token[0]
|
|
467
|
+
selector: expressions.PathSegment
|
|
468
|
+
|
|
469
|
+
if kind == TOKEN_INT:
|
|
470
|
+
index = int(token_value(token, self.source))
|
|
471
|
+
return expressions.IndexSegment(
|
|
472
|
+
token, index, span(lbracket_token, self.eat(TOKEN_RBRACKET))
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if kind in self.PATH_NAME_KINDS:
|
|
476
|
+
self.pos -= 1
|
|
477
|
+
var = self.parse_path(depth=depth + 1)
|
|
478
|
+
return expressions.VariableSegment(
|
|
479
|
+
lbracket_token, var, span(lbracket_token, self.eat(TOKEN_RBRACKET))
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if kind in (TOKEN_SINGLE_QUOTE, TOKEN_DOUBLE_QUOTE):
|
|
483
|
+
self.pos -= 1
|
|
484
|
+
return expressions.NameSegment(
|
|
485
|
+
lbracket_token,
|
|
486
|
+
self.parse_string_literal().value,
|
|
487
|
+
span(lbracket_token, self.eat(TOKEN_RBRACKET)),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if kind == TOKEN_RBRACKET:
|
|
491
|
+
raise LiquidSyntaxError(
|
|
492
|
+
"empty bracketed segment",
|
|
493
|
+
token=token,
|
|
494
|
+
source=self.source,
|
|
495
|
+
name=self.template_name,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if kind == TOKEN_LPAREN:
|
|
499
|
+
self.pos -= 1
|
|
500
|
+
selector = self.parse_range_literal()
|
|
501
|
+
self.eat(TOKEN_RBRACKET)
|
|
502
|
+
return selector
|
|
503
|
+
|
|
504
|
+
raise LiquidSyntaxError(
|
|
505
|
+
"expected an integer, identifier or string",
|
|
506
|
+
token=token,
|
|
507
|
+
source=self.source,
|
|
508
|
+
name=self.template_name,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def parse_infix(self, left: Expression) -> Expression:
|
|
512
|
+
op_token = self.next()
|
|
513
|
+
kind = op_token[0]
|
|
514
|
+
right = self.parse_expression(
|
|
515
|
+
self.precedences.get(kind, PRECEDENCE_LOWEST), infix=True
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if kind == TOKEN_OR:
|
|
519
|
+
return expressions.LogicalOr(op_token, left, right)
|
|
520
|
+
|
|
521
|
+
if kind == TOKEN_AND:
|
|
522
|
+
return expressions.LogicalAnd(op_token, left, right)
|
|
523
|
+
|
|
524
|
+
if kind == TOKEN_EQ:
|
|
525
|
+
return expressions.Eq(op_token, left, right)
|
|
526
|
+
|
|
527
|
+
if kind == TOKEN_NE:
|
|
528
|
+
return expressions.Ne(op_token, left, right)
|
|
529
|
+
|
|
530
|
+
if kind == TOKEN_LT:
|
|
531
|
+
return expressions.Lt(op_token, left, right)
|
|
532
|
+
|
|
533
|
+
if kind == TOKEN_LE:
|
|
534
|
+
return expressions.Le(op_token, left, right)
|
|
535
|
+
|
|
536
|
+
if kind == TOKEN_GT:
|
|
537
|
+
return expressions.Gt(op_token, left, right)
|
|
538
|
+
|
|
539
|
+
if kind == TOKEN_GE:
|
|
540
|
+
return expressions.Ge(op_token, left, right)
|
|
541
|
+
|
|
542
|
+
if kind == TOKEN_CONTAINS:
|
|
543
|
+
return expressions.Contains(op_token, left, right)
|
|
544
|
+
|
|
545
|
+
raise LiquidSyntaxError(
|
|
546
|
+
f"unknown infix operator {token_value(op_token, self.source)!r}",
|
|
547
|
+
token=op_token,
|
|
548
|
+
source=self.source,
|
|
549
|
+
name=self.template_name,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def parse_line_statements(self) -> Block:
|
|
553
|
+
nodes: Block = []
|
|
554
|
+
|
|
555
|
+
while 1:
|
|
556
|
+
token = self.current()
|
|
557
|
+
kind = token[0]
|
|
558
|
+
|
|
559
|
+
if kind == TOKEN_TAG_START:
|
|
560
|
+
self.pos += 1
|
|
561
|
+
|
|
562
|
+
if self.block_depth > self.env.resource_limits.max_block_depth:
|
|
563
|
+
raise ResourceLimitError("maximum recursion depth exceeded")
|
|
564
|
+
|
|
565
|
+
self.block_depth += 1
|
|
566
|
+
|
|
567
|
+
nodes.append(self.parse_tag())
|
|
568
|
+
|
|
569
|
+
elif kind == TOKEN_COMMENT_START:
|
|
570
|
+
self.pos += 1
|
|
571
|
+
text = token_value(self.eat(TOKEN_COMMENT), self.source)
|
|
572
|
+
self.eat(TOKEN_COMMENT_END)
|
|
573
|
+
nodes.append(TemplateComment(token, text))
|
|
574
|
+
|
|
575
|
+
elif kind in (TOKEN_WC, TOKEN_TAG_END):
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
else:
|
|
579
|
+
raise LiquidSyntaxError(
|
|
580
|
+
f"unexpected {TOKEN_NAMES[kind]} ({token_value(token, self.source)!r})",
|
|
581
|
+
token=token,
|
|
582
|
+
source=self.source,
|
|
583
|
+
name=self.template_name,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return nodes
|
|
587
|
+
|
|
588
|
+
def parse_filtered_expression(
|
|
589
|
+
self, precedence: Precedence = PRECEDENCE_LOWEST, *, infix: bool = False
|
|
590
|
+
) -> Expression:
|
|
591
|
+
expr = self.parse_expression(precedence, infix=infix)
|
|
592
|
+
|
|
593
|
+
if self.kind() == TOKEN_PIPE:
|
|
594
|
+
expr = self.parse_filters(expr)
|
|
595
|
+
|
|
596
|
+
return expr
|
|
597
|
+
|
|
598
|
+
def parse_filters(self, left: Expression) -> Expression:
|
|
599
|
+
token = self.eat(TOKEN_PIPE)
|
|
600
|
+
filter_expr = self.parse_filter(token, left)
|
|
601
|
+
|
|
602
|
+
while self.kind() == TOKEN_PIPE:
|
|
603
|
+
filter_expr = self.parse_filter(self.next(), filter_expr)
|
|
604
|
+
|
|
605
|
+
return filter_expr
|
|
606
|
+
|
|
607
|
+
def parse_filter(self, token: Token, left: Expression) -> Expression:
|
|
608
|
+
name_token = self.eat(TOKEN_IDENT, message="missing or malformed filter name")
|
|
609
|
+
|
|
610
|
+
if self.kind() in self.TERMINATE_FILTER:
|
|
611
|
+
# No arguments
|
|
612
|
+
return expressions.Filtered(
|
|
613
|
+
token,
|
|
614
|
+
left,
|
|
615
|
+
expressions.Name(name_token, token_value(name_token, self.source)),
|
|
616
|
+
[],
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
self.eat(TOKEN_COLON, message="missing colon or pipe")
|
|
620
|
+
args: list[Expression | expressions.KeywordArgument] = []
|
|
621
|
+
|
|
622
|
+
while 1:
|
|
623
|
+
kind = self.kind()
|
|
624
|
+
|
|
625
|
+
if kind in self.TERMINATE_FILTER:
|
|
626
|
+
break
|
|
627
|
+
|
|
628
|
+
if (
|
|
629
|
+
kind == TOKEN_IDENT
|
|
630
|
+
and self.peek_kind() in self.KEYWORD_ARGUMENT_DELIMITERS
|
|
631
|
+
):
|
|
632
|
+
param = self.parse_identifier()
|
|
633
|
+
self.next()
|
|
634
|
+
args.append(
|
|
635
|
+
expressions.KeywordArgument(
|
|
636
|
+
param.token, param, self.parse_expression(PRECEDENCE_FILTER_ARG)
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
args.append(self.parse_expression(PRECEDENCE_FILTER_ARG))
|
|
641
|
+
|
|
642
|
+
if self.kind() in self.TERMINATE_FILTER:
|
|
643
|
+
break
|
|
644
|
+
|
|
645
|
+
self.eat(TOKEN_COMMA, message="missing comma or pipe")
|
|
646
|
+
|
|
647
|
+
return expressions.Filtered(
|
|
648
|
+
token,
|
|
649
|
+
left,
|
|
650
|
+
expressions.Name(name_token, token_value(name_token, self.source)),
|
|
651
|
+
args,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def parse_identifier(self) -> expressions.Name:
|
|
655
|
+
token = self.eat(TOKEN_IDENT)
|
|
656
|
+
|
|
657
|
+
if self.kind() in self.PATH_PUNCTUATION:
|
|
658
|
+
raise LiquidSyntaxError(
|
|
659
|
+
"expected an identifier, found a path",
|
|
660
|
+
token=token,
|
|
661
|
+
source=self.source,
|
|
662
|
+
name=self.template_name,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
return expressions.Name(token, token_value(token, self.source))
|
|
666
|
+
|
|
667
|
+
def parse_name(self) -> expressions.Name:
|
|
668
|
+
kind = self.kind()
|
|
669
|
+
|
|
670
|
+
if kind == TOKEN_IDENT:
|
|
671
|
+
return self.parse_identifier()
|
|
672
|
+
|
|
673
|
+
if kind in (TOKEN_SINGLE_QUOTE, TOKEN_DOUBLE_QUOTE):
|
|
674
|
+
expr = self.parse_string_literal()
|
|
675
|
+
result = expressions.Name(expr.token, expr.value)
|
|
676
|
+
result.span = expr.span
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
raise LiquidSyntaxError(
|
|
680
|
+
"expected string or identifier",
|
|
681
|
+
token=self.current(),
|
|
682
|
+
source=self.source,
|
|
683
|
+
name=self.template_name,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def parse_arguments(
|
|
687
|
+
self, require_commas: bool = True
|
|
688
|
+
) -> list[Expression | expressions.KeywordArgument]:
|
|
689
|
+
args: list[Expression | expressions.KeywordArgument] = []
|
|
690
|
+
|
|
691
|
+
while 1:
|
|
692
|
+
kind = self.kind()
|
|
693
|
+
|
|
694
|
+
if kind in TERMINATE_EXPRESSION:
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
if (
|
|
698
|
+
kind == TOKEN_IDENT
|
|
699
|
+
and self.peek_kind() in self.KEYWORD_ARGUMENT_DELIMITERS
|
|
700
|
+
):
|
|
701
|
+
# A named argument
|
|
702
|
+
name = self.parse_identifier()
|
|
703
|
+
self.next()
|
|
704
|
+
args.append(
|
|
705
|
+
expressions.KeywordArgument(
|
|
706
|
+
name.token,
|
|
707
|
+
name,
|
|
708
|
+
self.parse_expression(PRECEDENCE_LOWEST, infix=False),
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
else:
|
|
713
|
+
args.append(self.parse_expression(PRECEDENCE_LOWEST, infix=False))
|
|
714
|
+
|
|
715
|
+
kind = self.kind()
|
|
716
|
+
|
|
717
|
+
if require_commas and kind not in TERMINATE_EXPRESSION:
|
|
718
|
+
self.eat(TOKEN_COMMA)
|
|
719
|
+
elif kind == TOKEN_COMMA:
|
|
720
|
+
self.pos += 1
|
|
721
|
+
|
|
722
|
+
return args
|
|
723
|
+
|
|
724
|
+
def parse_keyword_arguments(
|
|
725
|
+
self, require_commas: bool = True
|
|
726
|
+
) -> list[expressions.KeywordArgument]:
|
|
727
|
+
args: list[expressions.KeywordArgument] = []
|
|
728
|
+
|
|
729
|
+
while 1:
|
|
730
|
+
kind = self.kind()
|
|
731
|
+
|
|
732
|
+
if kind in TERMINATE_EXPRESSION:
|
|
733
|
+
break
|
|
734
|
+
|
|
735
|
+
name = self.parse_identifier()
|
|
736
|
+
self.eat_one_of(self.KEYWORD_ARGUMENT_DELIMITERS)
|
|
737
|
+
|
|
738
|
+
args.append(
|
|
739
|
+
expressions.KeywordArgument(
|
|
740
|
+
name.token,
|
|
741
|
+
name,
|
|
742
|
+
self.parse_expression(PRECEDENCE_LOWEST, infix=False),
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
kind = self.kind()
|
|
747
|
+
|
|
748
|
+
if require_commas and kind not in TERMINATE_EXPRESSION:
|
|
749
|
+
self.eat(TOKEN_COMMA)
|
|
750
|
+
elif kind == TOKEN_COMMA:
|
|
751
|
+
self.pos += 1
|
|
752
|
+
|
|
753
|
+
return args
|
|
754
|
+
|
|
755
|
+
def parse_positional_arguments(
|
|
756
|
+
self, require_commas: bool = True
|
|
757
|
+
) -> list[Expression]:
|
|
758
|
+
args: list[Expression] = []
|
|
759
|
+
|
|
760
|
+
while 1:
|
|
761
|
+
kind = self.kind()
|
|
762
|
+
|
|
763
|
+
if kind in TERMINATE_EXPRESSION:
|
|
764
|
+
break
|
|
765
|
+
|
|
766
|
+
args.append(self.parse_expression(PRECEDENCE_LOWEST, infix=False))
|
|
767
|
+
|
|
768
|
+
kind = self.kind()
|
|
769
|
+
|
|
770
|
+
if require_commas and kind not in TERMINATE_EXPRESSION:
|
|
771
|
+
self.eat(TOKEN_COMMA)
|
|
772
|
+
elif kind == TOKEN_COMMA:
|
|
773
|
+
self.pos += 1
|
|
774
|
+
|
|
775
|
+
return args
|
|
776
|
+
|
|
777
|
+
def parse_parameters(
|
|
778
|
+
self, require_commas: bool = True
|
|
779
|
+
) -> list[expressions.Parameter]:
|
|
780
|
+
params: list[expressions.Parameter] = []
|
|
781
|
+
|
|
782
|
+
while 1:
|
|
783
|
+
kind = self.kind()
|
|
784
|
+
|
|
785
|
+
if kind in TERMINATE_EXPRESSION:
|
|
786
|
+
break
|
|
787
|
+
|
|
788
|
+
if kind in self.PATH_NAME_KINDS:
|
|
789
|
+
name = self.parse_identifier()
|
|
790
|
+
|
|
791
|
+
if self.kind() in self.KEYWORD_ARGUMENT_DELIMITERS:
|
|
792
|
+
self.next()
|
|
793
|
+
params.append(
|
|
794
|
+
expressions.Parameter(
|
|
795
|
+
name.token,
|
|
796
|
+
name,
|
|
797
|
+
self.parse_expression(PRECEDENCE_LOWEST, infix=False),
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
else:
|
|
802
|
+
params.append(expressions.Parameter(name.token, name, None))
|
|
803
|
+
|
|
804
|
+
else:
|
|
805
|
+
raise LiquidSyntaxError(
|
|
806
|
+
"expected a parameter list",
|
|
807
|
+
token=self.current(),
|
|
808
|
+
source=self.source,
|
|
809
|
+
name=self.template_name,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
kind = self.kind()
|
|
813
|
+
|
|
814
|
+
if require_commas and kind not in TERMINATE_EXPRESSION:
|
|
815
|
+
self.eat(TOKEN_COMMA)
|
|
816
|
+
elif kind == TOKEN_COMMA:
|
|
817
|
+
self.pos += 1
|
|
818
|
+
|
|
819
|
+
return params
|