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,536 @@
1
+ import re
2
+
3
+ from ._lexer import Lexer, State
4
+ from .tokens import *
5
+ from .tokens import TokenKind
6
+
7
+ RE_COMMENT_SEGMENT = re.compile(
8
+ r"\{%-?\s*(comment|raw|endcomment|endraw)(?:(?!-?%\})[\s\S])*-?%\}"
9
+ )
10
+ RE_DOC_SEGMENT = re.compile(r"\{%-?\s*(doc|enddoc)(?:(?!-?%\})[\s\S])*-?%\}")
11
+ RE_END_RAW = re.compile(r"\{%-?\s*endraw\s*-?%\}")
12
+ RE_FLOAT = re.compile(r"-?\d+\.\d+")
13
+ RE_IDENT = re.compile(r"[a-zA-Z_][a-zA-Z0-9_-]*\??")
14
+ RE_INT = re.compile(r"-?\d+")
15
+ RE_MARKUP_START = re.compile(r"\{[%{]")
16
+ RE_OUT_END = re.compile(r"-?(\}\}?|%\}(?!\}))")
17
+ RE_TAG_END = re.compile(r"-?%\}")
18
+ RE_TAG_NAME = re.compile(r"#|[a-zA-Z0-9_]+")
19
+ RE_TRIVIA = re.compile(r"[ \n\r\t\f]+")
20
+ RE_LINE_TRIVIA = re.compile(r"[ \r\t\f]+")
21
+ RE_LINE_COMMENT_SEGMENT = re.compile(r"\n\s*(comment|endcomment).*")
22
+
23
+
24
+ class LegacyLexer(Lexer):
25
+ """A Shopify/liquid v5.12.0 compatible tokenizer with hard coded delimiters."""
26
+
27
+ __slots__ = ()
28
+
29
+ def scan_markup(self) -> State | None:
30
+
31
+ while 1:
32
+ if self.source.startswith("{{", self.pos):
33
+ self.pos += 2
34
+
35
+ # Output statements can be closed by `}}`, `}` or `%}`.
36
+ # Markup delimiters are greedy and not string literal aware.
37
+ limit = self.index(RE_OUT_END)
38
+
39
+ if limit is None:
40
+ # No markup and no more '}'. Emit text to end of string.
41
+ self.pos = self.length
42
+ self.emit(TOKEN_TEXT)
43
+ return None
44
+
45
+ self.emit(TOKEN_OUT_START)
46
+ self.accept_whitespace_control()
47
+ self.accept_expression(limit)
48
+ self.skip(RE_TRIVIA)
49
+ self.accept_whitespace_control()
50
+
51
+ if self.source.startswith("}}", self.pos) or self.source.startswith(
52
+ "%}", self.pos
53
+ ):
54
+ self.pos += 2
55
+ self.emit(TOKEN_OUT_END)
56
+
57
+ elif self.source.startswith("}", self.pos):
58
+ self.pos += 1
59
+ self.emit(TOKEN_OUT_END)
60
+
61
+ elif self.source.startswith("{%", self.pos):
62
+ self.pos += 2
63
+
64
+ # Tags must be closed by `%}`.
65
+ # Markup delimiters are greedy and not string literal aware.
66
+ limit = self.index(RE_TAG_END)
67
+
68
+ if limit is not None:
69
+ self.emit(TOKEN_TAG_START)
70
+ self.accept_whitespace_control()
71
+ self.skip(RE_TRIVIA)
72
+ self.accept_tag(limit)
73
+
74
+ elif self.scan_until(RE_MARKUP_START):
75
+ self.emit(TOKEN_TEXT)
76
+
77
+ else:
78
+ # No more markup. Emit text to end of string.
79
+ self.pos = self.length
80
+ if self.start < self.pos:
81
+ self.emit(TOKEN_TEXT)
82
+ return None
83
+
84
+ elif self.scan_until(RE_MARKUP_START):
85
+ self.emit(TOKEN_TEXT)
86
+
87
+ else:
88
+ # No more markup. Emit text to end of string.
89
+ self.pos = self.length
90
+ if self.start < self.pos:
91
+ self.emit(TOKEN_TEXT)
92
+ return None
93
+
94
+ def accept_expression(
95
+ self, limit: int, trivia: re.Pattern[str] = RE_TRIVIA
96
+ ) -> None:
97
+ while self.pos < limit:
98
+ ch = self.source[self.pos]
99
+
100
+ if ch in {" ", "\t", "\n", "\x0c", "\r"}:
101
+ self.skip(trivia)
102
+
103
+ elif ch == ",":
104
+ self.pos += 1
105
+ self.emit(TOKEN_COMMA)
106
+
107
+ elif ch == "|":
108
+ self.pos += 1
109
+ self.emit(TOKEN_PIPE)
110
+
111
+ elif ch == ":":
112
+ self.pos += 1
113
+ self.emit(TOKEN_COLON)
114
+
115
+ elif ch == "[":
116
+ self.pos += 1
117
+ self.emit(TOKEN_LBRACKET)
118
+
119
+ elif ch == "]":
120
+ self.pos += 1
121
+ self.emit(TOKEN_RBRACKET)
122
+
123
+ elif ch == "(":
124
+ self.pos += 1
125
+ self.emit(TOKEN_LPAREN)
126
+
127
+ elif ch == ")":
128
+ self.pos += 1
129
+ self.emit(TOKEN_RPAREN)
130
+
131
+ elif ch == "#":
132
+ self.pos += 1
133
+ self.emit(TOKEN_HASH)
134
+
135
+ elif ch == ".":
136
+ if self.pos + 1 < limit and self.source[self.pos + 1] == ".":
137
+ self.pos += 2
138
+ self.emit(TOKEN_DOUBLE_DOT)
139
+ else:
140
+ self.pos += 1
141
+ self.emit(TOKEN_DOT)
142
+
143
+ elif ch == "=":
144
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
145
+ self.pos += 2
146
+ self.emit(TOKEN_EQ)
147
+ else:
148
+ self.pos += 1
149
+ self.emit(TOKEN_ASSIGN)
150
+
151
+ elif ch == "<":
152
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
153
+ self.pos += 2
154
+ self.emit(TOKEN_LE)
155
+ elif self.pos + 1 < limit and self.source[self.pos + 1] == ">":
156
+ self.pos += 2
157
+ self.emit(TOKEN_NE)
158
+ else:
159
+ self.pos += 1
160
+ self.emit(TOKEN_LT)
161
+
162
+ elif ch == ">":
163
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
164
+ self.pos += 2
165
+ self.emit(TOKEN_GE)
166
+ else:
167
+ self.pos += 1
168
+ self.emit(TOKEN_GT)
169
+
170
+ elif ch == "!":
171
+ if self.pos + 1 < limit and self.source[self.pos + 1] == "=":
172
+ self.pos += 2
173
+ self.emit(TOKEN_NE)
174
+ else:
175
+ self.pos += 1
176
+ self.emit(TOKEN_UNKNOWN)
177
+
178
+ elif ch == "'":
179
+ self.scan_string_literal(
180
+ "'", TOKEN_SINGLE_QUOTE, TOKEN_SINGLE_QUOTED, limit
181
+ )
182
+
183
+ elif ch == '"':
184
+ self.scan_string_literal(
185
+ '"', TOKEN_DOUBLE_QUOTE, TOKEN_DOUBLE_QUOTED, limit
186
+ )
187
+
188
+ elif _is_name_first_ch(ord(ch)):
189
+ match = self.scan(RE_IDENT)
190
+
191
+ if match == "true":
192
+ self.emit(TOKEN_TRUE)
193
+ elif match == "false":
194
+ self.emit(TOKEN_FALSE)
195
+ elif match in ("nil", "null"):
196
+ self.emit(TOKEN_NIL)
197
+ elif match == "and":
198
+ self.emit(TOKEN_AND)
199
+ elif match == "or":
200
+ self.emit(TOKEN_OR)
201
+ elif match == "contains":
202
+ self.emit(TOKEN_CONTAINS)
203
+ elif match == "in":
204
+ self.emit(TOKEN_IN)
205
+ elif match == "blank":
206
+ self.emit(TOKEN_BLANK)
207
+ elif match == "empty":
208
+ self.emit(TOKEN_EMPTY)
209
+ elif match == "if":
210
+ self.emit(TOKEN_IF)
211
+ elif match == "else":
212
+ self.emit(TOKEN_ELSE)
213
+ elif match == "not":
214
+ self.emit(TOKEN_NOT)
215
+ else:
216
+ self.emit(TOKEN_IDENT)
217
+
218
+ elif _is_number_ch(ord(ch)):
219
+ if match := self.scan(RE_FLOAT):
220
+ self.emit(TOKEN_FLOAT)
221
+
222
+ elif match := self.scan(RE_INT):
223
+ self.emit(TOKEN_INT)
224
+
225
+ elif ch == "-":
226
+ self.pos += 1
227
+ self.emit(TOKEN_SUB)
228
+
229
+ else:
230
+ self.pos += 1
231
+ self.emit(TOKEN_UNKNOWN)
232
+
233
+ elif ch == "+":
234
+ self.pos += 1
235
+ self.emit(TOKEN_ADD)
236
+
237
+ elif ch == "*":
238
+ self.pos += 1
239
+ self.emit(TOKEN_MUL)
240
+
241
+ elif ch == "/":
242
+ self.pos += 1
243
+ self.emit(TOKEN_DIV)
244
+
245
+ elif ch == "%":
246
+ self.pos += 1
247
+ self.emit(TOKEN_MOD)
248
+
249
+ else:
250
+ self.pos += 1
251
+ self.emit(TOKEN_UNKNOWN)
252
+
253
+ def accept_tag(self, limit: int) -> None:
254
+ tag_name = self.scan(RE_TAG_NAME)
255
+
256
+ if tag_name is not None:
257
+ self.emit(TOKEN_TAG_NAME)
258
+
259
+ if tag_name == "#":
260
+ self.accept_inline_comment(limit)
261
+
262
+ elif tag_name == "comment":
263
+ self.accept_block_comment(limit)
264
+
265
+ elif tag_name == "doc":
266
+ self.accept_doc_comment(limit)
267
+
268
+ elif tag_name == "raw":
269
+ self.accept_raw_tag(limit)
270
+
271
+ elif tag_name == "liquid":
272
+ self.accept_line_statements(limit)
273
+
274
+ else:
275
+ self.accept_expression(limit)
276
+ self.skip(RE_TRIVIA)
277
+ self.accept_whitespace_control()
278
+ if self.scan(RE_TAG_END):
279
+ self.emit(TOKEN_TAG_END)
280
+
281
+ def accept_inline_comment(self, limit: int) -> None:
282
+ self.pos = limit
283
+ self.emit(TOKEN_COMMENT)
284
+ self.accept_whitespace_control()
285
+
286
+ if self.source.startswith("%}", self.pos):
287
+ self.pos += 2
288
+ self.emit(TOKEN_TAG_END)
289
+
290
+ def accept_block_comment(self, limit: int) -> None:
291
+ self.skip(RE_TRIVIA)
292
+
293
+ # Ignore everything up to the next delimiter.
294
+ self.pos = limit
295
+ self.accept_whitespace_control()
296
+ if self.source.startswith("%}", self.pos):
297
+ self.pos += 2
298
+ self.emit(TOKEN_TAG_END)
299
+
300
+ comment_depth = 1
301
+ raw_depth = 0
302
+
303
+ # Find the matching `{% endcomment %}` while allowing fully-formed,
304
+ # nested comments and raw blocks.
305
+ while 1:
306
+ match = self.skip_until(RE_COMMENT_SEGMENT)
307
+
308
+ if not match:
309
+ self.emit(TOKEN_UNKNOWN)
310
+ break
311
+
312
+ tag_name = match.group(1)
313
+
314
+ if tag_name == "comment":
315
+ comment_depth += 1
316
+ self.pos = match.end()
317
+
318
+ elif tag_name == "raw":
319
+ raw_depth += 1
320
+ self.pos = match.end()
321
+
322
+ elif tag_name == "endraw":
323
+ if raw_depth > 0:
324
+ raw_depth -= 1
325
+ self.pos = match.end()
326
+
327
+ elif tag_name == "endcomment":
328
+ if raw_depth > 0:
329
+ self.pos = match.end()
330
+ continue
331
+
332
+ comment_depth -= 1
333
+
334
+ if comment_depth > 0:
335
+ self.pos = match.end()
336
+ continue
337
+
338
+ self.emit(TOKEN_COMMENT)
339
+
340
+ # Leave the `{% endcomment %}` for scan_markup.
341
+ break
342
+
343
+ def accept_doc_comment(self, limit: int) -> None:
344
+ self.skip(RE_TRIVIA)
345
+ self.accept_expression(limit)
346
+ # Ignore everything up to the next delimiter.
347
+ self.pos = limit
348
+ self.accept_whitespace_control()
349
+ if self.source.startswith("%}", self.pos):
350
+ self.pos += 2
351
+ self.emit(TOKEN_TAG_END)
352
+
353
+ doc_depth = 1
354
+
355
+ # Find the matching `{% enddoc %}` while allowing fully-formed,
356
+ # nested doc blocks.
357
+ while 1:
358
+ match = self.skip_until(RE_DOC_SEGMENT)
359
+
360
+ if not match:
361
+ self.emit(TOKEN_UNKNOWN)
362
+ break
363
+
364
+ tag_name = match.group(1)
365
+
366
+ if tag_name == "doc":
367
+ doc_depth += 1
368
+ self.pos = match.end()
369
+
370
+ elif tag_name == "enddoc":
371
+ doc_depth -= 1
372
+
373
+ if doc_depth > 0:
374
+ self.pos = match.end()
375
+ continue
376
+
377
+ self.emit(TOKEN_COMMENT)
378
+
379
+ # Leave the `{% enddoc %}` for scan_markup.
380
+ break
381
+
382
+ def accept_raw_tag(self, limit: int) -> None:
383
+ self.accept_expression(limit)
384
+ self.skip(RE_TRIVIA)
385
+ self.accept_whitespace_control()
386
+
387
+ if self.source.startswith("%}", self.pos):
388
+ self.pos += 2
389
+ self.emit(TOKEN_TAG_END)
390
+
391
+ if self.scan_until(RE_END_RAW):
392
+ self.emit(TOKEN_TEXT)
393
+
394
+ # Leave `{% endraw %}` for scan_markup.
395
+
396
+ def accept_line_statements(self, limit: int) -> None:
397
+ while self.pos < limit:
398
+ self.skip(RE_TRIVIA)
399
+
400
+ if self.source.startswith("%}", self.pos) or (
401
+ self.source.startswith("-", self.pos)
402
+ and self.source.startswith("%}", self.pos + 1)
403
+ ):
404
+ break
405
+
406
+ line_limit = self.source.find("\n", self.pos)
407
+ if line_limit == -1:
408
+ line_limit = limit
409
+
410
+ line_limit = min(line_limit, limit)
411
+
412
+ self.emit(TOKEN_TAG_START)
413
+
414
+ tag_name = self.scan(RE_TAG_NAME)
415
+
416
+ if tag_name == "#":
417
+ self.emit(TOKEN_TAG_NAME)
418
+ self.pos = line_limit
419
+ self.emit(TOKEN_COMMENT)
420
+ self.emit(TOKEN_TAG_END)
421
+
422
+ elif tag_name == "comment":
423
+ self.emit(TOKEN_TAG_NAME)
424
+ self.emit(TOKEN_TAG_END)
425
+ self.accept_line_block_comment(limit)
426
+
427
+ elif tag_name == "doc":
428
+ self.emit(TOKEN_TAG_NAME)
429
+ self.accept_line_doc_comment(limit)
430
+
431
+ elif tag_name == "raw":
432
+ self.emit(TOKEN_TAG_NAME)
433
+ self.accept_line_raw_tag(limit)
434
+
435
+ elif tag_name == "liquid":
436
+ self.emit(TOKEN_TAG_NAME)
437
+ self.accept_line_statements(line_limit)
438
+
439
+ elif tag_name is None:
440
+ # Remove empty TAG_START
441
+ self.tokens.pop()
442
+ self.pos = line_limit
443
+ self.emit(TOKEN_UNKNOWN)
444
+
445
+ else:
446
+ self.emit(TOKEN_TAG_NAME)
447
+ self.accept_expression(line_limit, RE_LINE_TRIVIA)
448
+ self.emit(TOKEN_TAG_END)
449
+
450
+ self.skip(RE_TRIVIA)
451
+ self.accept_whitespace_control()
452
+
453
+ if self.source.startswith("%}", self.pos):
454
+ self.pos += 2
455
+
456
+ self.emit(TOKEN_TAG_END)
457
+
458
+ def accept_line_block_comment(self, limit: int) -> None:
459
+ comment_depth = 1
460
+
461
+ while self.pos < limit:
462
+ index = self.index(RE_LINE_COMMENT_SEGMENT)
463
+ if not index or index > limit:
464
+ self.pos = limit
465
+ self.emit(TOKEN_UNKNOWN)
466
+ break
467
+
468
+ match = self.skip_until(RE_LINE_COMMENT_SEGMENT)
469
+
470
+ if not match:
471
+ self.emit(TOKEN_UNKNOWN)
472
+ break
473
+
474
+ tag_name = match.group(1)
475
+
476
+ if tag_name == "comment":
477
+ comment_depth += 1
478
+ self.pos = match.end()
479
+
480
+ elif tag_name == "endcomment":
481
+ comment_depth -= 1
482
+
483
+ if comment_depth > 0:
484
+ self.pos = match.end()
485
+ continue
486
+
487
+ self.emit(TOKEN_COMMENT)
488
+ return
489
+
490
+ else:
491
+ raise AssertionError("unreachable")
492
+
493
+ def accept_line_doc_comment(self, limit: int) -> None:
494
+ # Shopify/liquid always raises a syntax error for `doc` in `{% liquid %}`.
495
+ self.pos = limit
496
+ self.emit(TOKEN_UNKNOWN)
497
+
498
+ def accept_line_raw_tag(self, limit: int) -> None:
499
+ # Shopify/liquid always raises a syntax error for `raw` in `{% liquid %}`.
500
+ self.pos = limit
501
+ self.emit(TOKEN_UNKNOWN)
502
+
503
+ def scan_string_literal(
504
+ self,
505
+ quote: str,
506
+ delim_kind: TokenKind,
507
+ kind: TokenKind,
508
+ limit: int,
509
+ ) -> None:
510
+ self.pos += 1
511
+ self.emit(delim_kind)
512
+
513
+ if self.source.startswith(quote, self.pos):
514
+ # Empty string
515
+ self.pos += 1
516
+ self.emit(delim_kind)
517
+ return
518
+
519
+ index = self.source.find(quote, self.pos)
520
+ if index == -1:
521
+ index = limit
522
+
523
+ self.pos = min(index, limit)
524
+ self.emit(kind)
525
+
526
+ if self.source.startswith(quote, self.pos):
527
+ self.pos += 1
528
+ self.emit(delim_kind)
529
+
530
+
531
+ def _is_name_first_ch(ch: int) -> bool:
532
+ return (ch >= 65 and ch <= 90) or (ch >= 97 and ch <= 122) or ch == 95
533
+
534
+
535
+ def _is_number_ch(ch: int) -> bool:
536
+ return ch == 45 or (ch >= 48 and ch <= 57)
liquid/_loader.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Awaitable, Callable, Mapping
9
+
10
+ from ._context import RenderContext
11
+ from ._template import Template
12
+ from .environment import Environment
13
+
14
+
15
+ @dataclass(slots=True, kw_only=True)
16
+ class TemplateSource:
17
+ """Template source text and meta data."""
18
+
19
+ name: str
20
+ text: str
21
+ path: str | None = None
22
+ matter: Mapping[str, object] | None = None
23
+ up_to_date: Callable[[], bool | Awaitable[bool]] | None = None
24
+
25
+
26
+ class Loader(ABC):
27
+ """Template loader base class."""
28
+
29
+ @abstractmethod
30
+ def get_source(
31
+ self,
32
+ env: Environment,
33
+ name: str,
34
+ context: RenderContext | None = None,
35
+ **kwargs: object,
36
+ ) -> TemplateSource:
37
+ """Return template source text and meta data identified by `name`."""
38
+ ...
39
+
40
+ async def get_source_async(
41
+ self,
42
+ env: Environment,
43
+ name: str,
44
+ context: RenderContext | None = None,
45
+ **kwargs: object,
46
+ ) -> TemplateSource:
47
+ """Return template source text and meta data identified by `name`."""
48
+ return self.get_source(env, name, context, **kwargs)
49
+
50
+ def load(
51
+ self,
52
+ env: Environment,
53
+ name: str,
54
+ globals: Mapping[str, object] | None = None,
55
+ context: RenderContext | None = None,
56
+ **kwargs: object,
57
+ ) -> Template:
58
+ source = self.get_source(env, name, context, **kwargs)
59
+ return env.parse(
60
+ source.text,
61
+ name=source.name,
62
+ path=source.path,
63
+ globals=globals,
64
+ matter=source.matter,
65
+ up_to_date=source.up_to_date,
66
+ )
67
+
68
+ async def load_async(
69
+ self,
70
+ env: Environment,
71
+ name: str,
72
+ globals: Mapping[str, object] | None = None,
73
+ context: RenderContext | None = None,
74
+ **kwargs: object,
75
+ ) -> Template:
76
+ source = await self.get_source_async(env, name, context, **kwargs)
77
+ return env.parse(
78
+ source.text,
79
+ name=source.name,
80
+ path=source.path,
81
+ globals=globals,
82
+ matter=source.matter,
83
+ up_to_date=source.up_to_date,
84
+ )