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
|
@@ -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)
|