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
liquid/_nothing.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ._context import RenderContext
7
+ from ._drop import ContextHint
8
+
9
+
10
+ class Nothing:
11
+ def __str__(self) -> str:
12
+ return ""
13
+
14
+ def __liquid__(self, hint: ContextHint, context: RenderContext) -> object:
15
+ match hint:
16
+ case "output" | "string":
17
+ return ""
18
+ case "index" | "property" | "object":
19
+ return None
20
+ case "numeric":
21
+ return 0
22
+ case "boolean":
23
+ return False
24
+ case "array":
25
+ return []
26
+
27
+
28
+ NOTHING = Nothing()
liquid/_parser.py ADDED
@@ -0,0 +1,460 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Literal
5
+
6
+ from .exceptions import LiquidSyntaxError
7
+ from .tokens import *
8
+ from .tokens import TOKEN_NAMES, span, token_value
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Collection
12
+
13
+ from ._markup import Block, Expression
14
+ from .environment import Environment
15
+ from .expressions import KeywordArgument, Name, Parameter, StringLiteral
16
+ from .tokens import Token, TokenKind
17
+
18
+ PRECEDENCE_LOWEST: Literal[1] = 1
19
+ PRECEDENCE_LOGICAL_OR: Literal[2] = 2
20
+ PRECEDENCE_LOGICAL_AND: Literal[3] = 3
21
+ PRECEDENCE_LOGICAL_NOT: Literal[4] = 4
22
+ PRECEDENCE_LOGICAL_RIGHT: Literal[5] = 5
23
+ PRECEDENCE_COMPARISON: Literal[6] = 6
24
+ PRECEDENCE_MEMBERSHIP: Literal[7] = 7
25
+ PRECEDENCE_PIPE: Literal[8] = 8
26
+ PRECEDENCE_FILTER_ARG: Literal[9] = 9
27
+ PRECEDENCE_ADD: Literal[10] = 10
28
+ PRECEDENCE_MULL: Literal[11] = 11
29
+ PRECEDENCE_NEG: Literal[12] = 12
30
+ PRECEDENCE_PREFIX: Literal[13] = 13
31
+
32
+ type Precedence = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
33
+
34
+
35
+ TERMINATE_EXPRESSION: frozenset[TokenKind] = frozenset(
36
+ [
37
+ TOKEN_WC,
38
+ TOKEN_OUT_END,
39
+ TOKEN_TAG_END,
40
+ TOKEN_TEXT,
41
+ TOKEN_RPAREN,
42
+ TOKEN_EOI,
43
+ TOKEN_IF,
44
+ TOKEN_ELSE,
45
+ TOKEN_INTERPOLATION_END,
46
+ ]
47
+ )
48
+
49
+
50
+ class Parser(ABC):
51
+ """A base parser from which different parser behavior can be derived."""
52
+
53
+ __slots__ = (
54
+ "block_depth",
55
+ "env",
56
+ "eoi",
57
+ "expression_depth",
58
+ "length",
59
+ "pos",
60
+ "source",
61
+ "template_name",
62
+ "tokens",
63
+ "whitespace_control_carry",
64
+ )
65
+
66
+ def __init__(
67
+ self,
68
+ env: Environment,
69
+ source: str,
70
+ template_name: str,
71
+ tokens: list[Token],
72
+ ) -> None:
73
+ self.env = env
74
+ self.source = source
75
+ self.template_name = template_name
76
+ self.tokens = tokens
77
+ self.length = len(tokens)
78
+ self.pos: int = 0
79
+ self.eoi: Token = (TOKEN_EOI, len(source), len(source))
80
+ self.whitespace_control_carry: Literal["-"] | None = None
81
+ self.block_depth: int = 0
82
+ self.expression_depth: int = 0
83
+
84
+ @classmethod
85
+ def parse(cls, env: Environment, source: str, template_name: str) -> Block:
86
+ return cls(env, source, template_name, env.tokenize(source)).parse_block(())
87
+
88
+ @abstractmethod
89
+ def parse_block(self, end: Collection[str]) -> Block:
90
+ """Parse text and markup until reaching a named tag.
91
+
92
+ Parameters
93
+ ----------
94
+ end
95
+ Possible tag names that terminate the block.
96
+
97
+ Returns
98
+ -------
99
+ block
100
+ A sequence of nodes in the template syntax tree.
101
+ """
102
+
103
+ @abstractmethod
104
+ def parse_expression(
105
+ self,
106
+ precedence: Precedence = PRECEDENCE_LOWEST,
107
+ *,
108
+ infix: bool = False,
109
+ expect_end_of_expression: bool = False,
110
+ ) -> Expression:
111
+ """Parse a "common" expression from the token stream.
112
+
113
+ Parameters
114
+ ----------
115
+ precedence
116
+ The binding power of this sub expression.
117
+ infix
118
+ When true, accept infix operators when parsing the expression.
119
+ expect_end_of_expression
120
+ When ``True``, raise a syntax error if we don't finish on an expression
121
+ termination token.
122
+ """
123
+
124
+ @abstractmethod
125
+ def parse_line_statements(self) -> Block:
126
+ """Parse newline terminated markup from the `{% liquid %}` tag."""
127
+
128
+ @abstractmethod
129
+ def parse_filtered_expression(
130
+ self, precedence: Precedence = PRECEDENCE_LOWEST, *, infix: bool = False
131
+ ) -> Expression:
132
+ """Parse a "common" expression optionally followed by one or more filters.
133
+
134
+ Parameters
135
+ ----------
136
+ precedence
137
+ The binding power of this sub expression.
138
+ infix
139
+ When true, accept infix operators when parsing the expression.
140
+ """
141
+
142
+ @abstractmethod
143
+ def parse_identifier(self) -> Name:
144
+ """Parse an identifier
145
+
146
+ Raises
147
+ ------
148
+ LiquidSyntaxError
149
+ If the identifier is followed by path segments.
150
+ """
151
+
152
+ @abstractmethod
153
+ def parse_name(self) -> Name:
154
+ """Parse an identifier, possibly surrounded by quotes.
155
+
156
+ Raises
157
+ ------
158
+ LiquidSyntaxError
159
+ If the identifier is followed by path segments.
160
+ """
161
+
162
+ @abstractmethod
163
+ def parse_string_literal(self) -> StringLiteral:
164
+ """Parse a literal string surrounded by single or double quotes."""
165
+
166
+ @abstractmethod
167
+ def parse_arguments(
168
+ self, require_commas: bool = True
169
+ ) -> list[Expression | KeywordArgument]:
170
+ """Parse positional and/or keyword arguments from the token stream.
171
+
172
+ Assumes any leading commas have been consumed by the caller, if they are allowed.
173
+
174
+ Parameters
175
+ ----------
176
+ require_commas
177
+ When true, throw a syntax error if arguments are not separated by a comma.
178
+ """
179
+
180
+ @abstractmethod
181
+ def parse_keyword_arguments(
182
+ self, require_commas: bool = True
183
+ ) -> list[KeywordArgument]:
184
+ """Parse zero or more keyword arguments from the token stream.
185
+
186
+ Assumes any leading commas have been consumed by the caller, if they are allowed.
187
+
188
+ Parameters
189
+ ----------
190
+ require_commas
191
+ When true, throw a syntax error if arguments are not separated by a comma.
192
+ """
193
+
194
+ @abstractmethod
195
+ def parse_positional_arguments(
196
+ self, require_commas: bool = True
197
+ ) -> list[Expression]:
198
+ """Parse zero or more positional arguments from the token stream.
199
+
200
+ Assumes any leading commas have been consumed by the caller, if they are allowed.
201
+
202
+ Parameters
203
+ ----------
204
+ require_commas
205
+ When true, throw a syntax error if arguments are not separated by a comma.
206
+ """
207
+
208
+ @abstractmethod
209
+ def parse_parameters(self, require_commas: bool = True) -> list[Parameter]:
210
+ """Parse zero or more parameters from the token stream.
211
+
212
+ A parameter is a Name with an optional default expression.
213
+
214
+ Assumes any leading commas have been consumed by the caller, if they are allowed.
215
+
216
+ Parameters
217
+ ----------
218
+ require_commas
219
+ When true, throw a syntax error if arguments are not separated by a comma.
220
+ """
221
+
222
+ def current(self) -> Token:
223
+ """Return the current token without advancing the pointer."""
224
+ if self.pos < self.length:
225
+ return self.tokens[self.pos]
226
+ return self.eoi
227
+
228
+ def kind(self) -> TokenKind:
229
+ """Return the kind of the current token without advancing the pointer."""
230
+ if self.pos < self.length:
231
+ return self.tokens[self.pos][0]
232
+ return TOKEN_EOI
233
+
234
+ def value(self) -> str:
235
+ """Return the string value associated with the current token."""
236
+ if self.pos < self.length:
237
+ return token_value(self.tokens[self.pos], self.source)
238
+ return ""
239
+
240
+ def next(self) -> Token:
241
+ """Return the current token and advance the pointer."""
242
+ if self.pos < self.length:
243
+ token = self.tokens[self.pos]
244
+ self.pos += 1
245
+ return token
246
+ return self.eoi
247
+
248
+ def peek(self, n: int = 1) -> Token:
249
+ """Return the token at pos+n without advancing the pointer."""
250
+ if self.pos + n < self.length:
251
+ return self.tokens[self.pos + n]
252
+ return self.eoi
253
+
254
+ def peek_kind(self, n: int = 1) -> TokenKind:
255
+ """Return the token kind at pos+n without advancing the pointer."""
256
+ if self.pos + n < self.length:
257
+ return self.tokens[self.pos + n][0]
258
+ return TOKEN_EOI
259
+
260
+ def eat(self, kind: TokenKind, message: str | None = None) -> Token:
261
+ """Consume and return a token matching `kind`.
262
+
263
+ Parameters
264
+ ----------
265
+ kind
266
+ The expected type of the next token in the stream.
267
+ message
268
+ An custom error message to use in the event of a syntax error.
269
+
270
+ Raises
271
+ ------
272
+ LiquidSyntaxError
273
+ If the current token's kind does not match `kind`.
274
+ """
275
+ token = self.current()
276
+ if token[0] != kind:
277
+ raise LiquidSyntaxError(
278
+ message or f"unexpected {token_value(token, self.source)!r}",
279
+ token=token,
280
+ source=self.source,
281
+ name=self.template_name,
282
+ )
283
+
284
+ self.pos += 1
285
+ return token
286
+
287
+ def eat_one_of(self, kinds: Collection[TokenKind]) -> Token:
288
+ token = self.next()
289
+ if token[0] not in kinds:
290
+ raise LiquidSyntaxError(
291
+ f"unexpected {TOKEN_NAMES[token[0]]!r}",
292
+ token=token,
293
+ source=self.source,
294
+ name=self.template_name,
295
+ )
296
+
297
+ return token
298
+
299
+ def skip(self, kind: int) -> bool:
300
+ if self.kind() == kind:
301
+ self.pos += 1
302
+ return True
303
+ return False
304
+
305
+ def carry_whitespace_control(self) -> None:
306
+ if self.kind() == TOKEN_WC:
307
+ self.pos += 1
308
+ self.whitespace_control_carry = "-"
309
+ else:
310
+ self.whitespace_control_carry = None
311
+
312
+ def eat_tag(self, tag_name: str) -> Token:
313
+ self.eat(TOKEN_TAG_START, message=f"expected tag {tag_name!r}")
314
+
315
+ if self.kind() == TOKEN_WC:
316
+ self.pos += 1
317
+
318
+ token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {tag_name!r}")
319
+ value = token_value(token, self.source)
320
+
321
+ if not value == tag_name:
322
+ raise LiquidSyntaxError(
323
+ f"unexpected tag {value!r}",
324
+ token=token,
325
+ source=self.source,
326
+ name=self.template_name,
327
+ )
328
+
329
+ # Ignore everything between the tag name and the closing tag delimiter.
330
+ while self.kind() not in TERMINATE_EXPRESSION:
331
+ self.pos += 1
332
+
333
+ self.carry_whitespace_control()
334
+ self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
335
+ return token
336
+
337
+ def eat_tag_prefix(self, prefix: str) -> Token:
338
+ self.eat(TOKEN_TAG_START, message=f"expected tag {prefix!r}")
339
+
340
+ if self.kind() == TOKEN_WC:
341
+ self.pos += 1
342
+
343
+ token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {prefix!r}")
344
+ value = token_value(token, self.source)
345
+
346
+ if not value.startswith(prefix):
347
+ raise LiquidSyntaxError(
348
+ f"unexpected tag {value!r}",
349
+ token=token,
350
+ source=self.source,
351
+ name=self.template_name,
352
+ )
353
+
354
+ # Ignore everything between the tag name and the closing tag delimiter.
355
+ while self.kind() not in TERMINATE_EXPRESSION:
356
+ self.pos += 1
357
+
358
+ self.carry_whitespace_control()
359
+ self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
360
+ return token
361
+
362
+ def eat_empty_tag(self, tag_name: str) -> Token:
363
+ self.eat(TOKEN_TAG_START, message=f"expected tag {tag_name!r}")
364
+
365
+ if self.kind() == TOKEN_WC:
366
+ self.pos += 1
367
+
368
+ token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {tag_name!r}")
369
+ value = token_value(token, self.source)
370
+
371
+ if value != tag_name:
372
+ raise LiquidSyntaxError(
373
+ f"expected tag {tag_name!r}, found {value!r}",
374
+ token=token,
375
+ source=self.source,
376
+ name=self.template_name,
377
+ )
378
+
379
+ self.carry_whitespace_control()
380
+ self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
381
+ return token
382
+
383
+ def expect_expression(self) -> None:
384
+ if self.kind() in TERMINATE_EXPRESSION:
385
+ raise LiquidSyntaxError(
386
+ "missing expression",
387
+ token=self.current(),
388
+ source=self.source,
389
+ name=self.template_name,
390
+ )
391
+
392
+ def raw_expression(self) -> str:
393
+ """Return source text up to but not including the next closing tag delimiter.
394
+
395
+ Calls ``carry_whitespace_control()`` and consumes the closing tag delimiter,
396
+ leaving the parser ready to parse the next block.
397
+ """
398
+ start = self.current()
399
+
400
+ while self.kind() not in (TOKEN_WC, TOKEN_TAG_END, TOKEN_EOI):
401
+ self.pos += 1
402
+
403
+ end = self.tokens[self.pos - 1]
404
+
405
+ self.carry_whitespace_control()
406
+ self.eat(TOKEN_TAG_END)
407
+
408
+ if start == end:
409
+ return ""
410
+
411
+ return token_value(span(start, end), self.source)
412
+
413
+ def peek_tag_name(self) -> str:
414
+ token = self.current()
415
+ if token[0] == TOKEN_WC:
416
+ token = self.peek()
417
+
418
+ if token[0] != TOKEN_TAG_NAME:
419
+ raise LiquidSyntaxError(
420
+ "missing tag name",
421
+ token=token,
422
+ source=self.source,
423
+ name=self.template_name,
424
+ )
425
+
426
+ return token_value(token, self.source)
427
+
428
+ def peek_whitespace_control(self) -> str | None:
429
+ token = self.peek()
430
+ if token[0] == TOKEN_WC:
431
+ return token_value(token, self.source)
432
+ return None
433
+
434
+ def skip_whitespace_control(self) -> None:
435
+ if self.kind() == TOKEN_WC:
436
+ self.pos += 1
437
+
438
+ def tag(self, name: str) -> bool:
439
+ """Return true if we're at the start of a tag named `name`."""
440
+ token = self.peek()
441
+ if token[0] == TOKEN_WC:
442
+ token = self.peek(2)
443
+
444
+ return token[0] == TOKEN_TAG_NAME and token_value(token, self.source) == name
445
+
446
+ def tags(self, names: Collection[str]) -> str | None:
447
+ """Return true if we're at the start of a tag and that tag's name is in `names`."""
448
+ token = self.peek()
449
+ if token[0] == TOKEN_WC:
450
+ token = self.peek(2)
451
+
452
+ if token[0] != TOKEN_TAG_NAME:
453
+ return None
454
+
455
+ name = token_value(token, self.source)
456
+
457
+ if name in names:
458
+ return name
459
+
460
+ return None