python-liquid 2.3.6__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/{builtin/loaders → loaders}/file_system_loader.py +51 -55
- liquid/{builtin/loaders → loaders}/mixins.py +39 -51
- 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.6.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.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.6.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/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.6.dist-info/RECORD +0 -96
liquid/tags/for_.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .._drop import liquid_async_iterable, liquid_iterable
|
|
7
|
+
from .._markup import is_blank_block, render_block, render_block_async
|
|
8
|
+
from .._slice import slice_sequence
|
|
9
|
+
from ..drops import AsyncIterableDrop, ForLoop, Undefined
|
|
10
|
+
from ..exceptions import LiquidSyntaxError
|
|
11
|
+
from ..expressions import KeywordArgument, Name, Variable
|
|
12
|
+
from ..tokens import TOKEN_COMMA, TOKEN_IN, TOKEN_TAG_END
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
|
|
17
|
+
from .._context import RenderContext
|
|
18
|
+
from .._io import StringBuffer
|
|
19
|
+
from .._markup import Block, Expression, Markup, Partial
|
|
20
|
+
from .._parser import Parser
|
|
21
|
+
from ..tokens import Token
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ForTag:
|
|
25
|
+
tag = "for"
|
|
26
|
+
|
|
27
|
+
__slots__ = (
|
|
28
|
+
"blank",
|
|
29
|
+
"block",
|
|
30
|
+
"default",
|
|
31
|
+
"expression",
|
|
32
|
+
"limit",
|
|
33
|
+
"name",
|
|
34
|
+
"offset",
|
|
35
|
+
"offset_key",
|
|
36
|
+
"reversed",
|
|
37
|
+
"token",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
token: Token,
|
|
43
|
+
name: Name,
|
|
44
|
+
expression: Expression,
|
|
45
|
+
block: Block,
|
|
46
|
+
*,
|
|
47
|
+
offset: Expression | None,
|
|
48
|
+
limit: Expression | None,
|
|
49
|
+
reversed: bool,
|
|
50
|
+
default: Block | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.token = token
|
|
53
|
+
self.name = name
|
|
54
|
+
self.expression = expression
|
|
55
|
+
self.block: Block = block
|
|
56
|
+
self.offset = offset
|
|
57
|
+
self.limit = limit
|
|
58
|
+
self.reversed = reversed
|
|
59
|
+
self.default: Block | None = default
|
|
60
|
+
|
|
61
|
+
self.blank = is_blank_block(self.block) and (
|
|
62
|
+
not default or is_blank_block(default)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if self.blank:
|
|
66
|
+
self.block = [node for node in self.block if not isinstance(node, str)]
|
|
67
|
+
if self.default:
|
|
68
|
+
self.default = [
|
|
69
|
+
node for node in self.default if not isinstance(node, str)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
self.offset_key = f"{self.name.value}-{self.expression}"
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def parse(token: Token, parser: Parser) -> Markup:
|
|
76
|
+
ident = parser.parse_identifier()
|
|
77
|
+
parser.eat(TOKEN_IN, message="missing 'in'")
|
|
78
|
+
parser.expect_expression()
|
|
79
|
+
expr = parser.parse_expression()
|
|
80
|
+
|
|
81
|
+
if parser.kind() == TOKEN_COMMA:
|
|
82
|
+
parser.next()
|
|
83
|
+
|
|
84
|
+
reversed_, offset, limit = ForTag.unpack_args(
|
|
85
|
+
parser, parser.parse_arguments(require_commas=False)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parser.carry_whitespace_control()
|
|
89
|
+
parser.eat(TOKEN_TAG_END)
|
|
90
|
+
|
|
91
|
+
block = parser.parse_block(("else", "endfor"))
|
|
92
|
+
default: Block | None = None
|
|
93
|
+
|
|
94
|
+
if parser.tag("else"):
|
|
95
|
+
parser.eat_empty_tag("else")
|
|
96
|
+
default = parser.parse_block(("else", "endfor"))
|
|
97
|
+
|
|
98
|
+
parser.eat_empty_tag("endfor")
|
|
99
|
+
|
|
100
|
+
return ForTag(
|
|
101
|
+
token,
|
|
102
|
+
ident,
|
|
103
|
+
expr,
|
|
104
|
+
block,
|
|
105
|
+
offset=offset,
|
|
106
|
+
limit=limit,
|
|
107
|
+
reversed=reversed_,
|
|
108
|
+
default=default,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def unpack_args(
|
|
113
|
+
parser: Parser, args: list[Expression | KeywordArgument]
|
|
114
|
+
) -> tuple[bool, Expression | None, Expression | None]:
|
|
115
|
+
reversed_: bool = False
|
|
116
|
+
offset: Expression | None = None
|
|
117
|
+
limit: Expression | None = None
|
|
118
|
+
|
|
119
|
+
for arg in args:
|
|
120
|
+
if isinstance(arg, KeywordArgument):
|
|
121
|
+
if arg.name.value == "offset":
|
|
122
|
+
offset = arg.expr
|
|
123
|
+
elif arg.name.value == "limit":
|
|
124
|
+
limit = arg.expr
|
|
125
|
+
else:
|
|
126
|
+
raise LiquidSyntaxError(
|
|
127
|
+
f"unknown argument {arg.name.value!r}",
|
|
128
|
+
token=arg.name.token,
|
|
129
|
+
source=parser.source,
|
|
130
|
+
name=parser.template_name,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
elif (
|
|
134
|
+
isinstance(arg, Variable)
|
|
135
|
+
and isinstance(arg.root, Name)
|
|
136
|
+
and arg.root.value == "reversed"
|
|
137
|
+
and not arg.selectors
|
|
138
|
+
):
|
|
139
|
+
reversed_ = True
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
raise LiquidSyntaxError(
|
|
143
|
+
"unexpected argument",
|
|
144
|
+
token=arg.token,
|
|
145
|
+
source=parser.source,
|
|
146
|
+
name=parser.template_name,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return reversed_, offset, limit
|
|
150
|
+
|
|
151
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
152
|
+
target = self.expression.evaluate(context)
|
|
153
|
+
|
|
154
|
+
offsets = context.get_register("loop_offset", default=register_factory)
|
|
155
|
+
|
|
156
|
+
if self.offset:
|
|
157
|
+
offset_ = self.offset.evaluate(context)
|
|
158
|
+
|
|
159
|
+
if isinstance(offset_, Undefined) and offset_.path == "continue":
|
|
160
|
+
offset = offsets[self.offset_key]
|
|
161
|
+
else:
|
|
162
|
+
offset = context.env.to_int(offset_, context, self.offset.span)
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
offset = 0
|
|
166
|
+
|
|
167
|
+
if self.limit:
|
|
168
|
+
limit = context.env.to_int(
|
|
169
|
+
self.limit.evaluate(context), context, self.limit.span
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
limit = None
|
|
173
|
+
|
|
174
|
+
it: Iterable[object]
|
|
175
|
+
|
|
176
|
+
if liquid_iterable(target):
|
|
177
|
+
it = target.__liquid_iter__(offset, limit, self.reversed)
|
|
178
|
+
length = len(it)
|
|
179
|
+
|
|
180
|
+
else:
|
|
181
|
+
a = context.env.to_array(target, context, self.expression.span)
|
|
182
|
+
if not offset and not limit:
|
|
183
|
+
it = a if not self.reversed else reversed(a)
|
|
184
|
+
length = len(a)
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
it, length = slice_sequence(a, offset, limit, self.reversed)
|
|
188
|
+
|
|
189
|
+
offsets[self.offset_key] = offset + length
|
|
190
|
+
|
|
191
|
+
if not length:
|
|
192
|
+
if self.default:
|
|
193
|
+
render_block(self.default, context, buffer)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
name = self.name.value
|
|
197
|
+
parent_loop = context.parent_loop()
|
|
198
|
+
|
|
199
|
+
forloop = ForLoop(f"{name}-{self.expression}", length, parent_loop)
|
|
200
|
+
namespace: dict[str, object] = {"forloop": forloop}
|
|
201
|
+
|
|
202
|
+
with context.loop(namespace, forloop):
|
|
203
|
+
for obj in it:
|
|
204
|
+
namespace[name] = obj
|
|
205
|
+
forloop.step()
|
|
206
|
+
render_block(self.block, context, buffer)
|
|
207
|
+
|
|
208
|
+
if context.interrupts:
|
|
209
|
+
interrupt = context.interrupts.pop()
|
|
210
|
+
if interrupt == "break":
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if interrupt == "continue":
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
context.interrupts.append(interrupt)
|
|
217
|
+
|
|
218
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
219
|
+
target = await self.expression.evaluate_async(context)
|
|
220
|
+
|
|
221
|
+
offsets = context.get_register("loop_offset", default=register_factory)
|
|
222
|
+
|
|
223
|
+
if self.offset:
|
|
224
|
+
offset_ = await self.offset.evaluate_async(context)
|
|
225
|
+
|
|
226
|
+
if isinstance(offset_, Undefined) and offset_.path == "continue":
|
|
227
|
+
offset = offsets[self.offset_key]
|
|
228
|
+
else:
|
|
229
|
+
offset = context.env.to_int(offset_, context, self.offset.span)
|
|
230
|
+
else:
|
|
231
|
+
offset = 0
|
|
232
|
+
|
|
233
|
+
if self.limit:
|
|
234
|
+
limit = context.env.to_int(
|
|
235
|
+
await self.limit.evaluate_async(context), context, self.limit.span
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
limit = None
|
|
239
|
+
|
|
240
|
+
it: Iterable[object] | AsyncIterableDrop[object]
|
|
241
|
+
|
|
242
|
+
if liquid_async_iterable(target):
|
|
243
|
+
it = await target.__liquid_iter_async__(offset, limit, self.reversed)
|
|
244
|
+
length = len(it)
|
|
245
|
+
|
|
246
|
+
elif liquid_iterable(target):
|
|
247
|
+
it = target.__liquid_iter__(offset, limit, self.reversed)
|
|
248
|
+
length = len(it)
|
|
249
|
+
|
|
250
|
+
else:
|
|
251
|
+
a = context.env.to_array(target, context, self.expression.span)
|
|
252
|
+
if not offset and not limit:
|
|
253
|
+
it = a if not self.reversed else reversed(a)
|
|
254
|
+
length = len(a)
|
|
255
|
+
else:
|
|
256
|
+
it, length = slice_sequence(a, offset, limit, self.reversed)
|
|
257
|
+
|
|
258
|
+
offsets[self.offset_key] = offset + length
|
|
259
|
+
|
|
260
|
+
if not length:
|
|
261
|
+
if self.default:
|
|
262
|
+
await render_block_async(self.default, context, buffer)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
name = self.name.value
|
|
266
|
+
parent_loop = context.parent_loop()
|
|
267
|
+
|
|
268
|
+
forloop = ForLoop(f"{name}-{self.expression}", length, parent_loop)
|
|
269
|
+
namespace: dict[str, object] = {"forloop": forloop}
|
|
270
|
+
|
|
271
|
+
with context.loop(namespace, forloop):
|
|
272
|
+
if isinstance(it, AsyncIterableDrop):
|
|
273
|
+
async for obj in it:
|
|
274
|
+
namespace[name] = obj
|
|
275
|
+
forloop.step()
|
|
276
|
+
await render_block_async(self.block, context, buffer)
|
|
277
|
+
|
|
278
|
+
if context.interrupts:
|
|
279
|
+
interrupt = context.interrupts.pop()
|
|
280
|
+
if interrupt == "break":
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
if interrupt == "continue":
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
context.interrupts.append(interrupt)
|
|
287
|
+
|
|
288
|
+
else:
|
|
289
|
+
for obj in it:
|
|
290
|
+
namespace[name] = obj
|
|
291
|
+
forloop.step()
|
|
292
|
+
await render_block_async(self.block, context, buffer)
|
|
293
|
+
|
|
294
|
+
if context.interrupts:
|
|
295
|
+
interrupt = context.interrupts.pop()
|
|
296
|
+
if interrupt == "break":
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
if interrupt == "continue":
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
context.interrupts.append(interrupt)
|
|
303
|
+
|
|
304
|
+
def children(self) -> list[Markup]:
|
|
305
|
+
result: list[Markup] = [
|
|
306
|
+
node for node in self.block if not isinstance(node, str)
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
if self.default:
|
|
310
|
+
for node in self.default:
|
|
311
|
+
if not isinstance(node, str):
|
|
312
|
+
result.append(node)
|
|
313
|
+
|
|
314
|
+
return result
|
|
315
|
+
|
|
316
|
+
def expressions(self) -> list[Expression]:
|
|
317
|
+
result = [self.expression]
|
|
318
|
+
|
|
319
|
+
if self.offset:
|
|
320
|
+
result.append(self.offset)
|
|
321
|
+
|
|
322
|
+
if self.limit:
|
|
323
|
+
result.append(self.limit)
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
def block_scope(self) -> list[Name]:
|
|
328
|
+
return [self.name]
|
|
329
|
+
|
|
330
|
+
def template_scope(self) -> list[Name]:
|
|
331
|
+
return []
|
|
332
|
+
|
|
333
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class BreakTag:
|
|
341
|
+
blank = True
|
|
342
|
+
tag = "break"
|
|
343
|
+
|
|
344
|
+
__slots__ = ("token",)
|
|
345
|
+
|
|
346
|
+
def __init__(self, token: Token) -> None:
|
|
347
|
+
self.token = token
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def parse(token: Token, parser: Parser) -> Markup:
|
|
351
|
+
parser.carry_whitespace_control()
|
|
352
|
+
parser.eat(TOKEN_TAG_END)
|
|
353
|
+
return BreakTag(token)
|
|
354
|
+
|
|
355
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
356
|
+
context.interrupts.append("break")
|
|
357
|
+
|
|
358
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
359
|
+
context.interrupts.append("break")
|
|
360
|
+
|
|
361
|
+
def children(self) -> list[Markup]:
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
def expressions(self) -> list[Expression]:
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
def block_scope(self) -> list[Name]:
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
def template_scope(self) -> list[Name]:
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class ContinueTag:
|
|
381
|
+
blank = True
|
|
382
|
+
tag = "continue"
|
|
383
|
+
|
|
384
|
+
__slots__ = ("token",)
|
|
385
|
+
|
|
386
|
+
def __init__(self, token: Token) -> None:
|
|
387
|
+
self.token = token
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def parse(token: Token, parser: Parser) -> Markup:
|
|
391
|
+
parser.carry_whitespace_control()
|
|
392
|
+
parser.eat(TOKEN_TAG_END)
|
|
393
|
+
return ContinueTag(token)
|
|
394
|
+
|
|
395
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
396
|
+
context.interrupts.append("continue")
|
|
397
|
+
|
|
398
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
399
|
+
context.interrupts.append("continue")
|
|
400
|
+
|
|
401
|
+
def children(self) -> list[Markup]:
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
def expressions(self) -> list[Expression]:
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
def block_scope(self) -> list[Name]:
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
def template_scope(self) -> list[Name]:
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
414
|
+
return []
|
|
415
|
+
|
|
416
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
417
|
+
return []
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def register_factory() -> defaultdict[str, int]:
|
|
421
|
+
return defaultdict(int)
|
liquid/tags/if_.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .._markup import is_blank_block, render_block, render_block_async
|
|
6
|
+
from ..tokens import (
|
|
7
|
+
TOKEN_TAG_END,
|
|
8
|
+
TOKEN_TAG_NAME,
|
|
9
|
+
TOKEN_TAG_START,
|
|
10
|
+
)
|
|
11
|
+
from .else_ import ElseBlock
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .._context import RenderContext
|
|
15
|
+
from .._io import StringBuffer
|
|
16
|
+
from .._markup import Block, Expression, Markup, Partial
|
|
17
|
+
from .._parser import Parser
|
|
18
|
+
from ..expressions import Name
|
|
19
|
+
from ..tokens import Token
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IfTag:
|
|
23
|
+
tag = ""
|
|
24
|
+
|
|
25
|
+
__slots__ = ("blank", "blocks", "token")
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, token: Token, blocks: list[IfBlock | ElsIfBlock | ElseBlock]
|
|
29
|
+
) -> None:
|
|
30
|
+
self.token = token
|
|
31
|
+
self.blocks = blocks
|
|
32
|
+
self.blank = all(block.blank for block in self.blocks)
|
|
33
|
+
|
|
34
|
+
if self.blank:
|
|
35
|
+
for block in self.blocks:
|
|
36
|
+
block.filter_strings()
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def parse(cls, token: Token, parser: Parser) -> Markup:
|
|
40
|
+
blocks: list[IfBlock | ElsIfBlock | ElseBlock] = []
|
|
41
|
+
|
|
42
|
+
parser.expect_expression()
|
|
43
|
+
expression = parser.parse_expression(infix=True, expect_end_of_expression=True)
|
|
44
|
+
parser.carry_whitespace_control()
|
|
45
|
+
parser.eat(TOKEN_TAG_END)
|
|
46
|
+
|
|
47
|
+
block = parser.parse_block(("else", "elsif", "endif"))
|
|
48
|
+
blocks.append(IfBlock(token, expression, block))
|
|
49
|
+
|
|
50
|
+
while 1:
|
|
51
|
+
tag_name = parser.tags(("else", "elsif"))
|
|
52
|
+
|
|
53
|
+
if tag_name == "elsif":
|
|
54
|
+
blocks.append(cls.parse_else_if(parser))
|
|
55
|
+
elif tag_name == "else":
|
|
56
|
+
name_token = parser.eat_tag("else")
|
|
57
|
+
blocks.append(
|
|
58
|
+
ElseBlock(
|
|
59
|
+
name_token, parser.parse_block(("else", "elsif", "endif"))
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
parser.eat_empty_tag("endif")
|
|
66
|
+
return IfTag(token, blocks)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def parse_else_if(cls, parser: Parser) -> ElsIfBlock:
|
|
70
|
+
parser.eat(TOKEN_TAG_START)
|
|
71
|
+
parser.skip_whitespace_control()
|
|
72
|
+
token = parser.eat(TOKEN_TAG_NAME)
|
|
73
|
+
parser.expect_expression()
|
|
74
|
+
expression = parser.parse_expression(infix=True, expect_end_of_expression=True)
|
|
75
|
+
parser.carry_whitespace_control()
|
|
76
|
+
parser.eat(TOKEN_TAG_END)
|
|
77
|
+
return ElsIfBlock(
|
|
78
|
+
token, expression, parser.parse_block(("else", "elsif", "endif"))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
82
|
+
for block in self.blocks:
|
|
83
|
+
if context.env.truthy(block.expression.evaluate(context), context):
|
|
84
|
+
return block.render(context, buffer)
|
|
85
|
+
|
|
86
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
87
|
+
for block in self.blocks:
|
|
88
|
+
if context.env.truthy(
|
|
89
|
+
await block.expression.evaluate_async(context), context
|
|
90
|
+
):
|
|
91
|
+
return await block.render_async(context, buffer)
|
|
92
|
+
|
|
93
|
+
def children(self) -> list[Markup]:
|
|
94
|
+
return list(self.blocks)
|
|
95
|
+
|
|
96
|
+
def expressions(self) -> list[Expression]:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
def block_scope(self) -> list[Name]:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
def template_scope(self) -> list[Name]:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class IfBlock:
|
|
113
|
+
tag = "if"
|
|
114
|
+
|
|
115
|
+
__slots__ = ("blank", "block", "expression", "token")
|
|
116
|
+
|
|
117
|
+
def __init__(self, token: Token, expression: Expression, block: Block) -> None:
|
|
118
|
+
self.token = token
|
|
119
|
+
self.expression = expression
|
|
120
|
+
self.block = block
|
|
121
|
+
self.blank = is_blank_block(block)
|
|
122
|
+
|
|
123
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
124
|
+
render_block(self.block, context, buffer)
|
|
125
|
+
|
|
126
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
127
|
+
await render_block_async(self.block, context, buffer)
|
|
128
|
+
|
|
129
|
+
def children(self) -> list[Markup]:
|
|
130
|
+
return [node for node in self.block if not isinstance(node, str)]
|
|
131
|
+
|
|
132
|
+
async def children_async(self) -> list[Markup]:
|
|
133
|
+
return self.children()
|
|
134
|
+
|
|
135
|
+
def expressions(self) -> list[Expression]:
|
|
136
|
+
return [self.expression]
|
|
137
|
+
|
|
138
|
+
def block_scope(self) -> list[Name]:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
def template_scope(self) -> list[Name]:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
def filter_strings(self) -> None:
|
|
151
|
+
markup: Block = [node for node in self.block if not isinstance(node, str)]
|
|
152
|
+
self.block = markup
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ElsIfBlock(IfBlock):
|
|
156
|
+
tag = "elsif"
|
liquid/tags/ifchanged.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .._markup import render_block, render_block_async
|
|
6
|
+
from ..tokens import TOKEN_TAG_END
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .._context import RenderContext
|
|
10
|
+
from .._io import StringBuffer
|
|
11
|
+
from .._markup import Block, Expression, Markup, Partial
|
|
12
|
+
from .._parser import Parser
|
|
13
|
+
from ..expressions import Name
|
|
14
|
+
from ..tokens import Token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IfChangedTag:
|
|
18
|
+
blank = False
|
|
19
|
+
tag = "ifchanged"
|
|
20
|
+
|
|
21
|
+
__slots__ = ("block", "token")
|
|
22
|
+
|
|
23
|
+
def __init__(self, token: Token, block: Block) -> None:
|
|
24
|
+
self.token = token
|
|
25
|
+
self.block = block
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def parse(token: Token, parser: Parser) -> Markup:
|
|
29
|
+
parser.carry_whitespace_control()
|
|
30
|
+
parser.eat(TOKEN_TAG_END)
|
|
31
|
+
block = parser.parse_block(end=("endifchanged",))
|
|
32
|
+
parser.eat_empty_tag("endifchanged")
|
|
33
|
+
return IfChangedTag(token, block)
|
|
34
|
+
|
|
35
|
+
def render(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
36
|
+
buf = context.env.buffer_factory()
|
|
37
|
+
render_block(self.block, context, buf)
|
|
38
|
+
buffered = buf.getvalue()
|
|
39
|
+
|
|
40
|
+
if buffered != context.get_register("ifchanged", str):
|
|
41
|
+
buffer.write(buffered)
|
|
42
|
+
context.registers["ifchanged"] = buffered
|
|
43
|
+
|
|
44
|
+
async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
45
|
+
buf = context.env.buffer_factory()
|
|
46
|
+
await render_block_async(self.block, context, buf)
|
|
47
|
+
buffered = buf.getvalue()
|
|
48
|
+
|
|
49
|
+
if buffered != context.get_register("ifchanged", str):
|
|
50
|
+
buffer.write(buffered)
|
|
51
|
+
context.registers["ifchanged"] = buffered
|
|
52
|
+
|
|
53
|
+
def children(self) -> list[Markup]:
|
|
54
|
+
return [node for node in self.block if not isinstance(node, str)]
|
|
55
|
+
|
|
56
|
+
def expressions(self) -> list[Expression]:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
def block_scope(self) -> list[Name]:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
def template_scope(self) -> list[Name]:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
def partials(self, context: RenderContext) -> list[Partial]:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
async def partials_async(self, context: RenderContext) -> list[Partial]:
|
|
69
|
+
return []
|