python-liquid 2.3.5__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 (213) 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/loaders/file_system_loader.py +143 -0
  93. liquid/{builtin/loaders → loaders}/mixins.py +40 -52
  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.5.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.5.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
  127. {python_liquid-2.3.5.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/file_system_loader.py +0 -146
  154. liquid/builtin/loaders/package_loader.py +0 -132
  155. liquid/builtin/output.py +0 -74
  156. liquid/builtin/tags/__init__.py +0 -0
  157. liquid/builtin/tags/assign_tag.py +0 -75
  158. liquid/builtin/tags/capture_tag.py +0 -98
  159. liquid/builtin/tags/case_tag.py +0 -287
  160. liquid/builtin/tags/comment_tag.py +0 -77
  161. liquid/builtin/tags/cycle_tag.py +0 -132
  162. liquid/builtin/tags/decrement_tag.py +0 -59
  163. liquid/builtin/tags/doc_tag.py +0 -68
  164. liquid/builtin/tags/echo_tag.py +0 -45
  165. liquid/builtin/tags/for_tag.py +0 -337
  166. liquid/builtin/tags/if_tag.py +0 -209
  167. liquid/builtin/tags/ifchanged_tag.py +0 -94
  168. liquid/builtin/tags/include_tag.py +0 -251
  169. liquid/builtin/tags/increment_tag.py +0 -60
  170. liquid/builtin/tags/inline_comment_tag.py +0 -47
  171. liquid/builtin/tags/liquid_tag.py +0 -183
  172. liquid/builtin/tags/render_tag.py +0 -371
  173. liquid/builtin/tags/tablerow_tag.py +0 -320
  174. liquid/builtin/tags/unless_tag.py +0 -218
  175. liquid/context.py +0 -615
  176. liquid/expression.py +0 -41
  177. liquid/extra/__init__.py +0 -98
  178. liquid/extra/filters/__init__.py +0 -33
  179. liquid/extra/filters/_json.py +0 -33
  180. liquid/extra/filters/array.py +0 -60
  181. liquid/extra/filters/babel.py +0 -552
  182. liquid/extra/filters/html.py +0 -32
  183. liquid/extra/filters/translate.py +0 -448
  184. liquid/extra/tags/__init__.py +0 -17
  185. liquid/extra/tags/_with.py +0 -80
  186. liquid/extra/tags/extends_tag.py +0 -576
  187. liquid/extra/tags/macro_tag.py +0 -270
  188. liquid/extra/tags/snippet_tag.py +0 -107
  189. liquid/extra/tags/translate_tag.py +0 -388
  190. liquid/filter.py +0 -230
  191. liquid/future/__init__.py +0 -3
  192. liquid/future/environment.py +0 -30
  193. liquid/future/filters/__init__.py +0 -1
  194. liquid/future/tags/__init__.py +0 -1
  195. liquid/lex.py +0 -260
  196. liquid/limits.py +0 -66
  197. liquid/loader.py +0 -141
  198. liquid/messages.py +0 -357
  199. liquid/mode.py +0 -12
  200. liquid/output.py +0 -35
  201. liquid/parser.py +0 -119
  202. liquid/span.py +0 -35
  203. liquid/static_analysis.py +0 -443
  204. liquid/stream.py +0 -148
  205. liquid/stringify.py +0 -31
  206. liquid/tag.py +0 -47
  207. liquid/template.py +0 -658
  208. liquid/token.py +0 -145
  209. liquid/undefined.py +0 -218
  210. liquid/utils/__init__.py +0 -15
  211. liquid/utils/html.py +0 -57
  212. liquid/utils/text.py +0 -28
  213. python_liquid-2.3.5.dist-info/RECORD +0 -96
@@ -0,0 +1,623 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ._lexer import Lexer, State
7
+ from .tokens import *
8
+
9
+ if TYPE_CHECKING:
10
+ from .environment import Environment
11
+ from .tokens import TokenKind
12
+
13
+ RE_FLOAT = re.compile(r"-?\d+\.\d+")
14
+ RE_IDENT = re.compile(r"[a-zA-Z_][a-zA-Z0-9_-]*\??")
15
+ RE_INT = re.compile(r"-?\d+")
16
+ RE_TRIVIA = re.compile(r"[ \n\r\t\f]+")
17
+ RE_TAG_NAME = re.compile(r"#|[a-zA-Z0-9_]+")
18
+ RE_LINE_TRIVIA = re.compile(r"[ \r\t\f]+")
19
+ RE_LINE_COMMENT_SEGMENT = re.compile(r"\n\s*(comment|endcomment).*")
20
+
21
+
22
+ class ExtendedLexer(Lexer):
23
+ """A tokenizer that accepts alternative markup delimiters."""
24
+
25
+ comment_start: str = "{#"
26
+ comment_end: str = "#}"
27
+ line_comment_start: str = "#"
28
+
29
+ out_start: str = "{{"
30
+ out_end: str = "}}"
31
+
32
+ tag_start: str = "{%"
33
+ tag_end: str = "%}"
34
+
35
+ __slots__ = (
36
+ "re_comment_segment",
37
+ "re_doc_segment",
38
+ "re_end_raw",
39
+ "re_markup_start",
40
+ )
41
+
42
+ def __init__(self, env: Environment, source: str) -> None:
43
+ super().__init__(env, source)
44
+
45
+ self.re_markup_start = re.compile(
46
+ rf"{re.escape(self.out_start)}|"
47
+ rf"{re.escape(self.tag_start)}|"
48
+ rf"{re.escape(self.comment_start)}"
49
+ )
50
+
51
+ self.re_comment_segment = re.compile(
52
+ rf"{re.escape(self.tag_start)}"
53
+ r"-?\s*"
54
+ r"(comment|raw|endcomment|endraw)"
55
+ rf"(?:(?!-?{re.escape(self.tag_end)})[\s\S])*"
56
+ rf"-?{re.escape(self.tag_end)}"
57
+ )
58
+
59
+ self.re_doc_segment = re.compile(
60
+ rf"{re.escape(self.tag_start)}"
61
+ r"-?\s*"
62
+ r"(doc|enddoc)"
63
+ rf"(?:(?!-?{re.escape(self.tag_end)})[\s\S])*"
64
+ rf"-?{re.escape(self.tag_end)}"
65
+ )
66
+
67
+ self.re_end_raw = re.compile(
68
+ rf"{re.escape(self.tag_start)}-?\s*endraw\s*-?{re.escape(self.tag_end)}"
69
+ )
70
+
71
+ def scan_markup(self) -> State | None:
72
+
73
+ while 1:
74
+ if self.source.startswith(self.out_start, self.pos):
75
+ self.pos += len(self.out_start)
76
+ self.emit(TOKEN_OUT_START)
77
+ self.accept_whitespace_control()
78
+ self.accept_expression()
79
+ self.skip(RE_TRIVIA)
80
+ self.accept_whitespace_control()
81
+
82
+ if self.source.startswith(self.out_end, self.pos):
83
+ self.pos += len(self.out_end)
84
+ self.emit(TOKEN_OUT_END)
85
+
86
+ elif self.source.startswith(self.tag_start, self.pos):
87
+ self.pos += len(self.tag_start)
88
+ self.emit(TOKEN_TAG_START)
89
+ self.accept_whitespace_control()
90
+ self.skip(RE_TRIVIA)
91
+ self.accept_tag()
92
+
93
+ elif self.comment_start and self.source.startswith(
94
+ self.comment_start, self.pos
95
+ ):
96
+ self.accept_template_comment()
97
+
98
+ elif self.scan_until(self.re_markup_start):
99
+ self.emit(TOKEN_TEXT)
100
+
101
+ else:
102
+ # No more markup. Emit text to end of string.
103
+ self.pos = self.length
104
+ if self.start < self.pos:
105
+ self.emit(TOKEN_TEXT)
106
+ return None
107
+
108
+ def accept_tag(self) -> None:
109
+ tag_name = self.scan(RE_TAG_NAME)
110
+
111
+ if tag_name is not None:
112
+ self.emit(TOKEN_TAG_NAME)
113
+
114
+ if tag_name == "#":
115
+ limit = self.tag_limit()
116
+ if limit == -1:
117
+ return
118
+ self.accept_inline_comment(limit)
119
+
120
+ elif tag_name == "comment":
121
+ limit = self.tag_limit()
122
+ if limit == -1:
123
+ return
124
+ self.accept_block_comment(limit)
125
+
126
+ elif tag_name == "doc":
127
+ limit = self.tag_limit()
128
+ if limit == -1:
129
+ return
130
+ self.accept_doc_comment(limit)
131
+
132
+ elif tag_name == "raw":
133
+ limit = self.tag_limit()
134
+ if limit == -1:
135
+ return
136
+ self.accept_raw_tag(limit)
137
+
138
+ elif tag_name == "liquid":
139
+ self.accept_line_statements(self.length)
140
+
141
+ else:
142
+ self.accept_expression()
143
+ self.skip(RE_TRIVIA)
144
+ self.accept_whitespace_control()
145
+
146
+ if self.source.startswith(self.tag_end, self.pos):
147
+ self.pos += len(self.tag_end)
148
+ self.emit(TOKEN_TAG_END)
149
+
150
+ def accept_inline_comment(self, limit: int) -> None:
151
+ self.pos = limit
152
+ self.emit(TOKEN_COMMENT)
153
+ self.accept_whitespace_control()
154
+
155
+ if self.source.startswith(self.tag_end, self.pos):
156
+ self.pos += len(self.tag_end)
157
+ self.emit(TOKEN_TAG_END)
158
+
159
+ def accept_block_comment(self, limit: int) -> None:
160
+ self.skip(RE_TRIVIA)
161
+
162
+ # Ignore everything up to the next delimiter.
163
+ self.pos = limit
164
+ self.accept_whitespace_control()
165
+ if self.source.startswith(self.tag_end, self.pos):
166
+ self.pos += len(self.tag_end)
167
+ self.emit(TOKEN_TAG_END)
168
+
169
+ comment_depth = 1
170
+ raw_depth = 0
171
+
172
+ # Find the matching `{% endcomment %}` while allowing fully-formed,
173
+ # nested comments and raw blocks.
174
+ while 1:
175
+ match = self.skip_until(self.re_comment_segment)
176
+
177
+ if not match:
178
+ self.emit(TOKEN_UNKNOWN)
179
+ break
180
+
181
+ tag_name = match.group(1)
182
+
183
+ if tag_name == "comment":
184
+ comment_depth += 1
185
+ self.pos = match.end()
186
+
187
+ elif tag_name == "raw":
188
+ raw_depth += 1
189
+ self.pos = match.end()
190
+
191
+ elif tag_name == "endraw":
192
+ if raw_depth > 0:
193
+ raw_depth -= 1
194
+ self.pos = match.end()
195
+
196
+ elif tag_name == "endcomment":
197
+ if raw_depth > 0:
198
+ self.pos = match.end()
199
+ continue
200
+
201
+ comment_depth -= 1
202
+
203
+ if comment_depth > 0:
204
+ self.pos = match.end()
205
+ continue
206
+
207
+ self.emit(TOKEN_COMMENT)
208
+
209
+ # Leave the `{% endcomment %}` for scan_markup.
210
+ break
211
+
212
+ def accept_doc_comment(self, limit: int) -> None:
213
+ self.skip(RE_TRIVIA)
214
+ self.accept_expression(limit)
215
+ self.pos = limit
216
+ if self.start < self.pos:
217
+ self.emit(TOKEN_UNKNOWN)
218
+ self.skip(RE_TRIVIA)
219
+ self.accept_whitespace_control()
220
+
221
+ if self.source.startswith(self.tag_end, self.pos):
222
+ self.pos += len(self.tag_end)
223
+ self.emit(TOKEN_TAG_END)
224
+
225
+ doc_depth = 1
226
+
227
+ # Find the matching `{% enddoc %}` while allowing fully-formed,
228
+ # nested doc blocks.
229
+ while 1:
230
+ match = self.skip_until(self.re_doc_segment)
231
+
232
+ if not match:
233
+ self.emit(TOKEN_UNKNOWN)
234
+ break
235
+
236
+ tag_name = match.group(1)
237
+
238
+ if tag_name == "doc":
239
+ doc_depth += 1
240
+ self.pos = match.end()
241
+
242
+ elif tag_name == "enddoc":
243
+ doc_depth -= 1
244
+
245
+ if doc_depth > 0:
246
+ self.pos = match.end()
247
+ continue
248
+
249
+ self.emit(TOKEN_COMMENT)
250
+ # Leave the `{% enddoc %}` for scan_markup.
251
+ break
252
+
253
+ def accept_raw_tag(self, limit: int) -> None:
254
+ self.skip(RE_TRIVIA)
255
+ self.pos = limit
256
+ if self.start < self.pos:
257
+ self.emit(TOKEN_UNKNOWN)
258
+ self.skip(RE_TRIVIA)
259
+ self.accept_whitespace_control()
260
+
261
+ if self.source.startswith(self.tag_end, self.pos):
262
+ self.pos += len(self.tag_end)
263
+ self.emit(TOKEN_TAG_END)
264
+
265
+ if self.scan_until(self.re_end_raw):
266
+ self.emit(TOKEN_TEXT)
267
+
268
+ # Leave `{% endraw%}` for scan_markup.
269
+
270
+ def accept_template_comment(self) -> None:
271
+ limit = self.source.find(self.comment_end, self.pos + len(self.comment_start))
272
+
273
+ if limit > 0 and self.source[limit - 1] == "-":
274
+ # Subtract one for whitespace control.
275
+ limit -= 1
276
+
277
+ if limit == -1:
278
+ # No matching end comment delimiter. Treat the start comment delimiter as text.
279
+ self.pos += len(self.comment_start)
280
+ self.emit(TOKEN_TEXT)
281
+ return
282
+
283
+ self.pos += len(self.comment_start)
284
+ self.emit(TOKEN_COMMENT_START)
285
+ self.accept_whitespace_control()
286
+
287
+ self.pos = limit
288
+ if self.start < self.pos:
289
+ self.emit(TOKEN_COMMENT)
290
+
291
+ self.accept_whitespace_control()
292
+ self.pos += len(self.comment_end)
293
+ if self.start < self.pos:
294
+ self.emit(TOKEN_COMMENT_END)
295
+
296
+ def accept_expression(
297
+ self, limit: int | None = None, trivia: re.Pattern[str] = RE_TRIVIA
298
+ ) -> None:
299
+ limit = limit or self.length
300
+
301
+ while self.pos < limit:
302
+ if self.source.startswith((self.tag_end, self.out_end), self.pos) or (
303
+ self.source.startswith("-", self.pos)
304
+ and self.source.startswith((self.tag_end, self.out_end), self.pos + 1)
305
+ ):
306
+ break
307
+
308
+ ch = self.source[self.pos]
309
+
310
+ if ch in {" ", "\t", "\n", "\x0c", "\r"}:
311
+ self.skip(trivia)
312
+
313
+ elif ch == ",":
314
+ self.pos += 1
315
+ self.emit(TOKEN_COMMA)
316
+
317
+ elif ch == "|":
318
+ self.pos += 1
319
+ self.emit(TOKEN_PIPE)
320
+
321
+ elif ch == ":":
322
+ self.pos += 1
323
+ self.emit(TOKEN_COLON)
324
+
325
+ elif ch == "[":
326
+ self.pos += 1
327
+ self.emit(TOKEN_LBRACKET)
328
+
329
+ elif ch == "]":
330
+ self.pos += 1
331
+ self.emit(TOKEN_RBRACKET)
332
+
333
+ elif ch == "(":
334
+ self.pos += 1
335
+ self.emit(TOKEN_LPAREN)
336
+
337
+ elif ch == ")":
338
+ self.pos += 1
339
+ self.emit(TOKEN_RPAREN)
340
+
341
+ elif ch == "#":
342
+ self.pos += 1
343
+ self.emit(TOKEN_HASH)
344
+
345
+ elif ch == ".":
346
+ if self.pos + 1 < limit and self.source[self.pos + 1] == ".":
347
+ self.pos += 2
348
+ self.emit(TOKEN_DOUBLE_DOT)
349
+ else:
350
+ self.pos += 1
351
+ self.emit(TOKEN_DOT)
352
+
353
+ elif ch == "=":
354
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
355
+ self.pos += 2
356
+ self.emit(TOKEN_EQ)
357
+ else:
358
+ self.pos += 1
359
+ self.emit(TOKEN_ASSIGN)
360
+
361
+ elif ch == "<":
362
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
363
+ self.pos += 2
364
+ self.emit(TOKEN_LE)
365
+ elif self.pos + 1 < limit and self.source[self.pos + 1] == ">":
366
+ self.pos += 2
367
+ self.emit(TOKEN_NE)
368
+ else:
369
+ self.pos += 1
370
+ self.emit(TOKEN_LT)
371
+
372
+ elif ch == ">":
373
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
374
+ self.pos += 2
375
+ self.emit(TOKEN_GE)
376
+ else:
377
+ self.pos += 1
378
+ self.emit(TOKEN_GT)
379
+
380
+ elif ch == "!":
381
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
382
+ self.pos += 2
383
+ self.emit(TOKEN_NE)
384
+ else:
385
+ self.pos += 1
386
+ self.emit(TOKEN_UNKNOWN)
387
+
388
+ elif ch == "'":
389
+ self.scan_string_literal(
390
+ "'", TOKEN_SINGLE_QUOTE, TOKEN_SINGLE_QUOTED, limit
391
+ )
392
+
393
+ elif ch == '"':
394
+ self.scan_string_literal(
395
+ '"', TOKEN_DOUBLE_QUOTE, TOKEN_DOUBLE_QUOTED, limit
396
+ )
397
+
398
+ elif _is_name_first_ch(ord(ch)):
399
+ match = self.scan(RE_IDENT)
400
+
401
+ if match == "true":
402
+ self.emit(TOKEN_TRUE)
403
+ elif match == "false":
404
+ self.emit(TOKEN_FALSE)
405
+ elif match in ("nil", "null"):
406
+ self.emit(TOKEN_NIL)
407
+ elif match == "and":
408
+ self.emit(TOKEN_AND)
409
+ elif match == "or":
410
+ self.emit(TOKEN_OR)
411
+ elif match == "contains":
412
+ self.emit(TOKEN_CONTAINS)
413
+ elif match == "in":
414
+ self.emit(TOKEN_IN)
415
+ elif match == "blank":
416
+ self.emit(TOKEN_BLANK)
417
+ elif match == "empty":
418
+ self.emit(TOKEN_EMPTY)
419
+ elif match == "if":
420
+ self.emit(TOKEN_IF)
421
+ elif match == "else":
422
+ self.emit(TOKEN_ELSE)
423
+ elif match == "not":
424
+ self.emit(TOKEN_NOT)
425
+ else:
426
+ self.emit(TOKEN_IDENT)
427
+
428
+ elif _is_number_ch(ord(ch)):
429
+ if match := self.scan(RE_FLOAT):
430
+ self.emit(TOKEN_FLOAT)
431
+
432
+ elif match := self.scan(RE_INT):
433
+ self.emit(TOKEN_INT)
434
+
435
+ elif ch == "-":
436
+ self.pos += 1
437
+ self.emit(TOKEN_SUB)
438
+
439
+ else:
440
+ self.pos += 1
441
+ self.emit(TOKEN_UNKNOWN)
442
+
443
+ elif ch == "+":
444
+ self.pos += 1
445
+ self.emit(TOKEN_ADD)
446
+
447
+ elif ch == "*":
448
+ self.pos += 1
449
+ self.emit(TOKEN_MUL)
450
+
451
+ elif ch == "/":
452
+ self.pos += 1
453
+ self.emit(TOKEN_DIV)
454
+
455
+ elif ch == "%":
456
+ self.pos += 1
457
+ self.emit(TOKEN_MOD)
458
+
459
+ else:
460
+ self.pos += 1
461
+ self.emit(TOKEN_UNKNOWN)
462
+
463
+ def accept_line_statements(self, limit: int) -> None:
464
+ while self.pos < limit:
465
+ self.skip(RE_TRIVIA)
466
+
467
+ if self.source.startswith(self.tag_end, self.pos) or (
468
+ self.source.startswith("-", self.pos)
469
+ and self.source.startswith(self.tag_end, self.pos + 1)
470
+ ):
471
+ break
472
+
473
+ line_limit = self.source.find(("\n"), self.pos)
474
+ if line_limit == -1:
475
+ line_limit = limit
476
+
477
+ line_limit = min(line_limit, limit)
478
+
479
+ if self.source.startswith(self.line_comment_start, self.pos):
480
+ self.pos += len(self.line_comment_start)
481
+ self.emit(TOKEN_COMMENT_START)
482
+ self.pos = line_limit
483
+ self.emit(TOKEN_COMMENT)
484
+ self.emit(TOKEN_COMMENT_END)
485
+ continue
486
+
487
+ self.emit(TOKEN_TAG_START)
488
+ tag_name = self.scan(RE_TAG_NAME)
489
+
490
+ if tag_name == "#":
491
+ self.emit(TOKEN_TAG_NAME)
492
+ self.pos = line_limit
493
+ self.emit(TOKEN_COMMENT)
494
+ self.emit(TOKEN_TAG_END)
495
+
496
+ elif tag_name == "comment":
497
+ self.emit(TOKEN_TAG_NAME)
498
+ self.emit(TOKEN_TAG_END)
499
+ self.accept_line_block_comment(limit)
500
+
501
+ elif tag_name == "doc":
502
+ self.emit(TOKEN_TAG_NAME)
503
+ self.accept_line_doc_comment(limit)
504
+
505
+ elif tag_name == "raw":
506
+ self.emit(TOKEN_TAG_NAME)
507
+ self.accept_line_raw_tag(limit)
508
+
509
+ elif tag_name == "liquid":
510
+ self.emit(TOKEN_TAG_NAME)
511
+ self.accept_line_statements(line_limit)
512
+
513
+ elif tag_name is None:
514
+ # Remove empty TAG_START
515
+ self.tokens.pop()
516
+ self.pos = line_limit
517
+ self.emit(TOKEN_UNKNOWN)
518
+
519
+ else:
520
+ self.emit(TOKEN_TAG_NAME)
521
+ self.accept_expression(line_limit, RE_LINE_TRIVIA)
522
+ self.emit(TOKEN_TAG_END)
523
+
524
+ self.skip(RE_TRIVIA)
525
+ self.accept_whitespace_control()
526
+
527
+ if self.source.startswith(self.tag_end, self.pos):
528
+ self.pos += len(self.tag_end)
529
+
530
+ self.emit(TOKEN_TAG_END)
531
+
532
+ def accept_line_block_comment(self, limit: int) -> None:
533
+ comment_depth = 1
534
+
535
+ while self.pos < limit:
536
+ index = self.index(RE_LINE_COMMENT_SEGMENT)
537
+ if not index or index > limit:
538
+ self.pos = limit
539
+ self.emit(TOKEN_UNKNOWN)
540
+ break
541
+
542
+ match = self.skip_until(RE_LINE_COMMENT_SEGMENT)
543
+
544
+ if not match:
545
+ self.emit(TOKEN_UNKNOWN)
546
+ break
547
+
548
+ tag_name = match.group(1)
549
+
550
+ if tag_name == "comment":
551
+ comment_depth += 1
552
+ self.pos = match.end()
553
+
554
+ elif tag_name == "endcomment":
555
+ comment_depth -= 1
556
+
557
+ if comment_depth > 0:
558
+ self.pos = match.end()
559
+ continue
560
+
561
+ self.emit(TOKEN_COMMENT)
562
+ return
563
+
564
+ else:
565
+ raise AssertionError("unreachable")
566
+
567
+ def accept_line_doc_comment(self, limit: int) -> None:
568
+ # Shopify/liquid always raises a syntax error for `doc` in `{% liquid %}`.
569
+ self.pos = limit
570
+ self.emit(TOKEN_UNKNOWN)
571
+
572
+ def accept_line_raw_tag(self, limit: int) -> None:
573
+ # Shopify/liquid always raises a syntax error for `raw` in `{% liquid %}`.
574
+ self.pos = limit
575
+ self.emit(TOKEN_UNKNOWN)
576
+
577
+ def scan_string_literal(
578
+ self,
579
+ quote: str,
580
+ delim_kind: TokenKind,
581
+ kind: TokenKind,
582
+ limit: int,
583
+ ) -> None:
584
+ self.pos += 1
585
+ self.emit(delim_kind)
586
+
587
+ if self.source.startswith(quote, self.pos):
588
+ # Empty string
589
+ self.pos += 1
590
+ self.emit(delim_kind)
591
+ return
592
+
593
+ index = self.source.find(quote, self.pos)
594
+ if index == -1:
595
+ index = limit
596
+
597
+ self.pos = min(index, limit)
598
+ self.emit(kind)
599
+
600
+ if self.source.startswith(quote, self.pos):
601
+ self.pos += 1
602
+ self.emit(delim_kind)
603
+
604
+ def tag_limit(self) -> int:
605
+ """Find the next tag end delimiter, disregarding string literals.
606
+
607
+ This is only suitable for tags that don't accept expressions that could
608
+ possibly contain string literals. Built-in tags include ``{% comment %}``,
609
+ ``{% # ... %}``, ``{% raw %}`` and ``{% doc%}``.
610
+ """
611
+ limit = self.source.find(self.tag_end, self.pos)
612
+ if limit > 0 and self.source[limit - 1] == "-":
613
+ # Subtract one for whitespace control.
614
+ limit -= 1
615
+ return limit
616
+
617
+
618
+ def _is_name_first_ch(ch: int) -> bool:
619
+ return (ch >= 65 and ch <= 90) or (ch >= 97 and ch <= 122) or ch == 95
620
+
621
+
622
+ def _is_number_ch(ch: int) -> bool:
623
+ return ch == 45 or (ch >= 48 and ch <= 57)