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.
Files changed (212) hide show
  1. liquid/__about__.py +1 -0
  2. liquid/__init__.py +161 -187
  3. liquid/_base_tag.py +66 -0
  4. liquid/{utils/chain_map.py → _chain_map.py} +18 -19
  5. liquid/_context.py +465 -0
  6. liquid/_drop.py +108 -0
  7. liquid/_filter.py +174 -0
  8. liquid/_io.py +18 -0
  9. liquid/_lax_parser_mixin.py +100 -0
  10. liquid/_lexer.py +93 -0
  11. liquid/_lexer_extended.py +623 -0
  12. liquid/_lexer_legacy.py +536 -0
  13. liquid/_loader.py +84 -0
  14. liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
  15. liquid/_markup.py +253 -0
  16. liquid/_messages.py +292 -0
  17. liquid/_missing.py +3 -0
  18. liquid/_nothing.py +28 -0
  19. liquid/_parser.py +460 -0
  20. liquid/_parser_extended.py +293 -0
  21. liquid/_parser_legacy.py +819 -0
  22. liquid/_resource_limits.py +50 -0
  23. liquid/_slice.py +41 -0
  24. liquid/_static_analysis.py +503 -0
  25. liquid/_strftime.py +186 -0
  26. liquid/_template.py +536 -0
  27. liquid/drops/__init__.py +20 -0
  28. liquid/drops/forloop.py +78 -0
  29. liquid/drops/iterable.py +25 -0
  30. liquid/drops/range.py +76 -0
  31. liquid/drops/self_.py +45 -0
  32. liquid/drops/tablerowloop.py +142 -0
  33. liquid/drops/undefined.py +228 -0
  34. liquid/environment.py +717 -551
  35. liquid/exceptions.py +67 -214
  36. liquid/expressions.py +1184 -0
  37. liquid/filters/__init__.py +134 -0
  38. liquid/filters/abs.py +7 -0
  39. liquid/filters/append.py +5 -0
  40. liquid/filters/at_least.py +9 -0
  41. liquid/filters/at_most.py +9 -0
  42. liquid/filters/babel.py +560 -0
  43. liquid/filters/base64.py +26 -0
  44. liquid/filters/capitalize.py +5 -0
  45. liquid/filters/ceil.py +8 -0
  46. liquid/filters/compact.py +25 -0
  47. liquid/filters/concat.py +11 -0
  48. liquid/filters/date.py +27 -0
  49. liquid/filters/default.py +25 -0
  50. liquid/filters/divided_by.py +21 -0
  51. liquid/filters/downcase.py +5 -0
  52. liquid/filters/escape.py +56 -0
  53. liquid/filters/find.py +56 -0
  54. liquid/filters/first.py +19 -0
  55. liquid/filters/floor.py +8 -0
  56. liquid/filters/html.py +23 -0
  57. liquid/filters/index.py +12 -0
  58. liquid/filters/join.py +13 -0
  59. liquid/filters/json_.py +30 -0
  60. liquid/filters/last.py +17 -0
  61. liquid/filters/map.py +5 -0
  62. liquid/filters/minus.py +13 -0
  63. liquid/filters/modulo.py +18 -0
  64. liquid/filters/newline_to_br.py +16 -0
  65. liquid/filters/plus.py +13 -0
  66. liquid/filters/prepend.py +5 -0
  67. liquid/filters/reject.py +19 -0
  68. liquid/filters/remove.py +36 -0
  69. liquid/filters/replace.py +32 -0
  70. liquid/filters/reverse.py +5 -0
  71. liquid/filters/round.py +35 -0
  72. liquid/filters/size.py +25 -0
  73. liquid/filters/slice.py +39 -0
  74. liquid/filters/sort.py +148 -0
  75. liquid/filters/split.py +34 -0
  76. liquid/filters/squish.py +9 -0
  77. liquid/filters/strip.py +13 -0
  78. liquid/filters/strip_html.py +19 -0
  79. liquid/filters/strip_newlines.py +17 -0
  80. liquid/filters/sum.py +21 -0
  81. liquid/filters/times.py +13 -0
  82. liquid/filters/translate.py +380 -0
  83. liquid/filters/truncate.py +39 -0
  84. liquid/filters/uniq.py +11 -0
  85. liquid/filters/upcase.py +5 -0
  86. liquid/filters/url.py +18 -0
  87. liquid/filters/where.py +18 -0
  88. liquid/loaders/__init__.py +17 -0
  89. liquid/loaders/caching_file_system_loader.py +64 -0
  90. liquid/loaders/choice_loader.py +93 -0
  91. liquid/loaders/dict_loader.py +56 -0
  92. liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
  93. liquid/{builtin/loaders → loaders}/mixins.py +39 -51
  94. liquid/loaders/package_loader.py +110 -0
  95. liquid/tags/__init__.py +61 -0
  96. liquid/tags/assign.py +68 -0
  97. liquid/tags/capture.py +83 -0
  98. liquid/tags/case.py +199 -0
  99. liquid/tags/comment.py +56 -0
  100. liquid/tags/cycle.py +131 -0
  101. liquid/tags/decrement.py +55 -0
  102. liquid/tags/doc.py +56 -0
  103. liquid/tags/echo.py +65 -0
  104. liquid/tags/else_.py +53 -0
  105. liquid/tags/error.py +51 -0
  106. liquid/tags/extends.py +448 -0
  107. liquid/tags/for_.py +421 -0
  108. liquid/tags/if_.py +156 -0
  109. liquid/tags/ifchanged.py +69 -0
  110. liquid/tags/include.py +223 -0
  111. liquid/tags/increment.py +55 -0
  112. liquid/tags/inline_comment.py +70 -0
  113. liquid/tags/liquid.py +56 -0
  114. liquid/tags/macro.py +281 -0
  115. liquid/tags/output.py +58 -0
  116. liquid/tags/raw.py +60 -0
  117. liquid/tags/render.py +245 -0
  118. liquid/tags/tablerow.py +254 -0
  119. liquid/tags/template_comment.py +56 -0
  120. liquid/tags/translate.py +316 -0
  121. liquid/tags/unless.py +121 -0
  122. liquid/tags/with_.py +83 -0
  123. liquid/tokens.py +370 -0
  124. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
  125. python_liquid-3.0.0.dist-info/RECORD +128 -0
  126. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
  127. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
  128. liquid/analyze_tags.py +0 -214
  129. liquid/ast.py +0 -252
  130. liquid/builtin/__init__.py +0 -203
  131. liquid/builtin/content.py +0 -41
  132. liquid/builtin/drops/__init__.py +0 -3
  133. liquid/builtin/drops/drops.py +0 -13
  134. liquid/builtin/expressions/__init__.py +0 -47
  135. liquid/builtin/expressions/_tokenize.py +0 -186
  136. liquid/builtin/expressions/arguments.py +0 -207
  137. liquid/builtin/expressions/filtered.py +0 -387
  138. liquid/builtin/expressions/logical.py +0 -672
  139. liquid/builtin/expressions/loop.py +0 -275
  140. liquid/builtin/expressions/path.py +0 -163
  141. liquid/builtin/expressions/primitive.py +0 -417
  142. liquid/builtin/filters/__init__.py +0 -0
  143. liquid/builtin/filters/array.py +0 -329
  144. liquid/builtin/filters/extra.py +0 -83
  145. liquid/builtin/filters/math.py +0 -129
  146. liquid/builtin/filters/misc.py +0 -137
  147. liquid/builtin/filters/string.py +0 -416
  148. liquid/builtin/illegal.py +0 -28
  149. liquid/builtin/loaders/__init__.py +0 -0
  150. liquid/builtin/loaders/caching_file_system_loader.py +0 -61
  151. liquid/builtin/loaders/choice_loader.py +0 -94
  152. liquid/builtin/loaders/dict_loader.py +0 -64
  153. liquid/builtin/loaders/package_loader.py +0 -132
  154. liquid/builtin/output.py +0 -74
  155. liquid/builtin/tags/__init__.py +0 -0
  156. liquid/builtin/tags/assign_tag.py +0 -75
  157. liquid/builtin/tags/capture_tag.py +0 -98
  158. liquid/builtin/tags/case_tag.py +0 -287
  159. liquid/builtin/tags/comment_tag.py +0 -77
  160. liquid/builtin/tags/cycle_tag.py +0 -132
  161. liquid/builtin/tags/decrement_tag.py +0 -59
  162. liquid/builtin/tags/doc_tag.py +0 -68
  163. liquid/builtin/tags/echo_tag.py +0 -45
  164. liquid/builtin/tags/for_tag.py +0 -337
  165. liquid/builtin/tags/if_tag.py +0 -209
  166. liquid/builtin/tags/ifchanged_tag.py +0 -94
  167. liquid/builtin/tags/include_tag.py +0 -251
  168. liquid/builtin/tags/increment_tag.py +0 -60
  169. liquid/builtin/tags/inline_comment_tag.py +0 -47
  170. liquid/builtin/tags/liquid_tag.py +0 -183
  171. liquid/builtin/tags/render_tag.py +0 -371
  172. liquid/builtin/tags/tablerow_tag.py +0 -320
  173. liquid/builtin/tags/unless_tag.py +0 -218
  174. liquid/context.py +0 -615
  175. liquid/expression.py +0 -41
  176. liquid/extra/__init__.py +0 -98
  177. liquid/extra/filters/__init__.py +0 -33
  178. liquid/extra/filters/_json.py +0 -33
  179. liquid/extra/filters/array.py +0 -60
  180. liquid/extra/filters/babel.py +0 -552
  181. liquid/extra/filters/html.py +0 -32
  182. liquid/extra/filters/translate.py +0 -448
  183. liquid/extra/tags/__init__.py +0 -17
  184. liquid/extra/tags/_with.py +0 -80
  185. liquid/extra/tags/extends_tag.py +0 -576
  186. liquid/extra/tags/macro_tag.py +0 -270
  187. liquid/extra/tags/snippet_tag.py +0 -107
  188. liquid/extra/tags/translate_tag.py +0 -388
  189. liquid/filter.py +0 -230
  190. liquid/future/__init__.py +0 -3
  191. liquid/future/environment.py +0 -30
  192. liquid/future/filters/__init__.py +0 -1
  193. liquid/future/tags/__init__.py +0 -1
  194. liquid/lex.py +0 -260
  195. liquid/limits.py +0 -66
  196. liquid/loader.py +0 -141
  197. liquid/messages.py +0 -357
  198. liquid/mode.py +0 -12
  199. liquid/output.py +0 -35
  200. liquid/parser.py +0 -119
  201. liquid/span.py +0 -35
  202. liquid/static_analysis.py +0 -443
  203. liquid/stream.py +0 -148
  204. liquid/stringify.py +0 -31
  205. liquid/tag.py +0 -47
  206. liquid/template.py +0 -658
  207. liquid/token.py +0 -145
  208. liquid/undefined.py +0 -218
  209. liquid/utils/__init__.py +0 -15
  210. liquid/utils/html.py +0 -57
  211. liquid/utils/text.py +0 -28
  212. python_liquid-2.3.6.dist-info/RECORD +0 -96
@@ -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