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.
- liquid/__about__.py +1 -0
- liquid/__init__.py +161 -187
- liquid/_base_tag.py +66 -0
- liquid/{utils/chain_map.py → _chain_map.py} +18 -19
- liquid/_context.py +465 -0
- liquid/_drop.py +108 -0
- liquid/_filter.py +174 -0
- liquid/_io.py +18 -0
- liquid/_lax_parser_mixin.py +100 -0
- liquid/_lexer.py +93 -0
- liquid/_lexer_extended.py +623 -0
- liquid/_lexer_legacy.py +536 -0
- liquid/_loader.py +84 -0
- liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
- liquid/_markup.py +253 -0
- liquid/_messages.py +292 -0
- liquid/_missing.py +3 -0
- liquid/_nothing.py +28 -0
- liquid/_parser.py +460 -0
- liquid/_parser_extended.py +293 -0
- liquid/_parser_legacy.py +819 -0
- liquid/_resource_limits.py +50 -0
- liquid/_slice.py +41 -0
- liquid/_static_analysis.py +503 -0
- liquid/_strftime.py +186 -0
- liquid/_template.py +536 -0
- liquid/drops/__init__.py +20 -0
- liquid/drops/forloop.py +78 -0
- liquid/drops/iterable.py +25 -0
- liquid/drops/range.py +76 -0
- liquid/drops/self_.py +45 -0
- liquid/drops/tablerowloop.py +142 -0
- liquid/drops/undefined.py +228 -0
- liquid/environment.py +717 -551
- liquid/exceptions.py +67 -214
- liquid/expressions.py +1184 -0
- liquid/filters/__init__.py +134 -0
- liquid/filters/abs.py +7 -0
- liquid/filters/append.py +5 -0
- liquid/filters/at_least.py +9 -0
- liquid/filters/at_most.py +9 -0
- liquid/filters/babel.py +560 -0
- liquid/filters/base64.py +26 -0
- liquid/filters/capitalize.py +5 -0
- liquid/filters/ceil.py +8 -0
- liquid/filters/compact.py +25 -0
- liquid/filters/concat.py +11 -0
- liquid/filters/date.py +27 -0
- liquid/filters/default.py +25 -0
- liquid/filters/divided_by.py +21 -0
- liquid/filters/downcase.py +5 -0
- liquid/filters/escape.py +56 -0
- liquid/filters/find.py +56 -0
- liquid/filters/first.py +19 -0
- liquid/filters/floor.py +8 -0
- liquid/filters/html.py +23 -0
- liquid/filters/index.py +12 -0
- liquid/filters/join.py +13 -0
- liquid/filters/json_.py +30 -0
- liquid/filters/last.py +17 -0
- liquid/filters/map.py +5 -0
- liquid/filters/minus.py +13 -0
- liquid/filters/modulo.py +18 -0
- liquid/filters/newline_to_br.py +16 -0
- liquid/filters/plus.py +13 -0
- liquid/filters/prepend.py +5 -0
- liquid/filters/reject.py +19 -0
- liquid/filters/remove.py +36 -0
- liquid/filters/replace.py +32 -0
- liquid/filters/reverse.py +5 -0
- liquid/filters/round.py +35 -0
- liquid/filters/size.py +25 -0
- liquid/filters/slice.py +39 -0
- liquid/filters/sort.py +148 -0
- liquid/filters/split.py +34 -0
- liquid/filters/squish.py +9 -0
- liquid/filters/strip.py +13 -0
- liquid/filters/strip_html.py +19 -0
- liquid/filters/strip_newlines.py +17 -0
- liquid/filters/sum.py +21 -0
- liquid/filters/times.py +13 -0
- liquid/filters/translate.py +380 -0
- liquid/filters/truncate.py +39 -0
- liquid/filters/uniq.py +11 -0
- liquid/filters/upcase.py +5 -0
- liquid/filters/url.py +18 -0
- liquid/filters/where.py +18 -0
- liquid/loaders/__init__.py +17 -0
- liquid/loaders/caching_file_system_loader.py +64 -0
- liquid/loaders/choice_loader.py +93 -0
- liquid/loaders/dict_loader.py +56 -0
- liquid/loaders/file_system_loader.py +143 -0
- liquid/{builtin/loaders → loaders}/mixins.py +40 -52
- liquid/loaders/package_loader.py +110 -0
- liquid/tags/__init__.py +61 -0
- liquid/tags/assign.py +68 -0
- liquid/tags/capture.py +83 -0
- liquid/tags/case.py +199 -0
- liquid/tags/comment.py +56 -0
- liquid/tags/cycle.py +131 -0
- liquid/tags/decrement.py +55 -0
- liquid/tags/doc.py +56 -0
- liquid/tags/echo.py +65 -0
- liquid/tags/else_.py +53 -0
- liquid/tags/error.py +51 -0
- liquid/tags/extends.py +448 -0
- liquid/tags/for_.py +421 -0
- liquid/tags/if_.py +156 -0
- liquid/tags/ifchanged.py +69 -0
- liquid/tags/include.py +223 -0
- liquid/tags/increment.py +55 -0
- liquid/tags/inline_comment.py +70 -0
- liquid/tags/liquid.py +56 -0
- liquid/tags/macro.py +281 -0
- liquid/tags/output.py +58 -0
- liquid/tags/raw.py +60 -0
- liquid/tags/render.py +245 -0
- liquid/tags/tablerow.py +254 -0
- liquid/tags/template_comment.py +56 -0
- liquid/tags/translate.py +316 -0
- liquid/tags/unless.py +121 -0
- liquid/tags/with_.py +83 -0
- liquid/tokens.py +370 -0
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
- python_liquid-3.0.0.dist-info/RECORD +128 -0
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
- liquid/analyze_tags.py +0 -214
- liquid/ast.py +0 -252
- liquid/builtin/__init__.py +0 -203
- liquid/builtin/content.py +0 -41
- liquid/builtin/drops/__init__.py +0 -3
- liquid/builtin/drops/drops.py +0 -13
- liquid/builtin/expressions/__init__.py +0 -47
- liquid/builtin/expressions/_tokenize.py +0 -186
- liquid/builtin/expressions/arguments.py +0 -207
- liquid/builtin/expressions/filtered.py +0 -387
- liquid/builtin/expressions/logical.py +0 -672
- liquid/builtin/expressions/loop.py +0 -275
- liquid/builtin/expressions/path.py +0 -163
- liquid/builtin/expressions/primitive.py +0 -417
- liquid/builtin/filters/__init__.py +0 -0
- liquid/builtin/filters/array.py +0 -329
- liquid/builtin/filters/extra.py +0 -83
- liquid/builtin/filters/math.py +0 -129
- liquid/builtin/filters/misc.py +0 -137
- liquid/builtin/filters/string.py +0 -416
- liquid/builtin/illegal.py +0 -28
- liquid/builtin/loaders/__init__.py +0 -0
- liquid/builtin/loaders/caching_file_system_loader.py +0 -61
- liquid/builtin/loaders/choice_loader.py +0 -94
- liquid/builtin/loaders/dict_loader.py +0 -64
- liquid/builtin/loaders/file_system_loader.py +0 -146
- liquid/builtin/loaders/package_loader.py +0 -132
- liquid/builtin/output.py +0 -74
- liquid/builtin/tags/__init__.py +0 -0
- liquid/builtin/tags/assign_tag.py +0 -75
- liquid/builtin/tags/capture_tag.py +0 -98
- liquid/builtin/tags/case_tag.py +0 -287
- liquid/builtin/tags/comment_tag.py +0 -77
- liquid/builtin/tags/cycle_tag.py +0 -132
- liquid/builtin/tags/decrement_tag.py +0 -59
- liquid/builtin/tags/doc_tag.py +0 -68
- liquid/builtin/tags/echo_tag.py +0 -45
- liquid/builtin/tags/for_tag.py +0 -337
- liquid/builtin/tags/if_tag.py +0 -209
- liquid/builtin/tags/ifchanged_tag.py +0 -94
- liquid/builtin/tags/include_tag.py +0 -251
- liquid/builtin/tags/increment_tag.py +0 -60
- liquid/builtin/tags/inline_comment_tag.py +0 -47
- liquid/builtin/tags/liquid_tag.py +0 -183
- liquid/builtin/tags/render_tag.py +0 -371
- liquid/builtin/tags/tablerow_tag.py +0 -320
- liquid/builtin/tags/unless_tag.py +0 -218
- liquid/context.py +0 -615
- liquid/expression.py +0 -41
- liquid/extra/__init__.py +0 -98
- liquid/extra/filters/__init__.py +0 -33
- liquid/extra/filters/_json.py +0 -33
- liquid/extra/filters/array.py +0 -60
- liquid/extra/filters/babel.py +0 -552
- liquid/extra/filters/html.py +0 -32
- liquid/extra/filters/translate.py +0 -448
- liquid/extra/tags/__init__.py +0 -17
- liquid/extra/tags/_with.py +0 -80
- liquid/extra/tags/extends_tag.py +0 -576
- liquid/extra/tags/macro_tag.py +0 -270
- liquid/extra/tags/snippet_tag.py +0 -107
- liquid/extra/tags/translate_tag.py +0 -388
- liquid/filter.py +0 -230
- liquid/future/__init__.py +0 -3
- liquid/future/environment.py +0 -30
- liquid/future/filters/__init__.py +0 -1
- liquid/future/tags/__init__.py +0 -1
- liquid/lex.py +0 -260
- liquid/limits.py +0 -66
- liquid/loader.py +0 -141
- liquid/messages.py +0 -357
- liquid/mode.py +0 -12
- liquid/output.py +0 -35
- liquid/parser.py +0 -119
- liquid/span.py +0 -35
- liquid/static_analysis.py +0 -443
- liquid/stream.py +0 -148
- liquid/stringify.py +0 -31
- liquid/tag.py +0 -47
- liquid/template.py +0 -658
- liquid/token.py +0 -145
- liquid/undefined.py +0 -218
- liquid/utils/__init__.py +0 -15
- liquid/utils/html.py +0 -57
- liquid/utils/text.py +0 -28
- python_liquid-2.3.5.dist-info/RECORD +0 -96
liquid/_lexer_legacy.py
ADDED
|
@@ -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
|
+
)
|