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.
Files changed (212) 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/{builtin/loaders → loaders}/file_system_loader.py +51 -55
  93. liquid/{builtin/loaders → loaders}/mixins.py +39 -51
  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.6.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.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
  127. {python_liquid-2.3.6.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/package_loader.py +0 -132
  154. liquid/builtin/output.py +0 -74
  155. liquid/builtin/tags/__init__.py +0 -0
  156. liquid/builtin/tags/assign_tag.py +0 -75
  157. liquid/builtin/tags/capture_tag.py +0 -98
  158. liquid/builtin/tags/case_tag.py +0 -287
  159. liquid/builtin/tags/comment_tag.py +0 -77
  160. liquid/builtin/tags/cycle_tag.py +0 -132
  161. liquid/builtin/tags/decrement_tag.py +0 -59
  162. liquid/builtin/tags/doc_tag.py +0 -68
  163. liquid/builtin/tags/echo_tag.py +0 -45
  164. liquid/builtin/tags/for_tag.py +0 -337
  165. liquid/builtin/tags/if_tag.py +0 -209
  166. liquid/builtin/tags/ifchanged_tag.py +0 -94
  167. liquid/builtin/tags/include_tag.py +0 -251
  168. liquid/builtin/tags/increment_tag.py +0 -60
  169. liquid/builtin/tags/inline_comment_tag.py +0 -47
  170. liquid/builtin/tags/liquid_tag.py +0 -183
  171. liquid/builtin/tags/render_tag.py +0 -371
  172. liquid/builtin/tags/tablerow_tag.py +0 -320
  173. liquid/builtin/tags/unless_tag.py +0 -218
  174. liquid/context.py +0 -615
  175. liquid/expression.py +0 -41
  176. liquid/extra/__init__.py +0 -98
  177. liquid/extra/filters/__init__.py +0 -33
  178. liquid/extra/filters/_json.py +0 -33
  179. liquid/extra/filters/array.py +0 -60
  180. liquid/extra/filters/babel.py +0 -552
  181. liquid/extra/filters/html.py +0 -32
  182. liquid/extra/filters/translate.py +0 -448
  183. liquid/extra/tags/__init__.py +0 -17
  184. liquid/extra/tags/_with.py +0 -80
  185. liquid/extra/tags/extends_tag.py +0 -576
  186. liquid/extra/tags/macro_tag.py +0 -270
  187. liquid/extra/tags/snippet_tag.py +0 -107
  188. liquid/extra/tags/translate_tag.py +0 -388
  189. liquid/filter.py +0 -230
  190. liquid/future/__init__.py +0 -3
  191. liquid/future/environment.py +0 -30
  192. liquid/future/filters/__init__.py +0 -1
  193. liquid/future/tags/__init__.py +0 -1
  194. liquid/lex.py +0 -260
  195. liquid/limits.py +0 -66
  196. liquid/loader.py +0 -141
  197. liquid/messages.py +0 -357
  198. liquid/mode.py +0 -12
  199. liquid/output.py +0 -35
  200. liquid/parser.py +0 -119
  201. liquid/span.py +0 -35
  202. liquid/static_analysis.py +0 -443
  203. liquid/stream.py +0 -148
  204. liquid/stringify.py +0 -31
  205. liquid/tag.py +0 -47
  206. liquid/template.py +0 -658
  207. liquid/token.py +0 -145
  208. liquid/undefined.py +0 -218
  209. liquid/utils/__init__.py +0 -15
  210. liquid/utils/html.py +0 -57
  211. liquid/utils/text.py +0 -28
  212. python_liquid-2.3.6.dist-info/RECORD +0 -96
liquid/tags/output.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .._context import RenderContext
7
+ from .._io import StringBuffer
8
+ from .._markup import Expression, Markup, Partial
9
+ from ..expressions import Name
10
+ from ..tokens import Token
11
+
12
+
13
+ class OutputStatement:
14
+ blank = False
15
+ tag = ""
16
+
17
+ __slots__ = ("expression", "token")
18
+
19
+ def __init__(self, token: Token, expression: Expression) -> None:
20
+ self.token = token
21
+ self.expression = expression
22
+
23
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
24
+ buffer.write(
25
+ context.env.serialize(
26
+ self.expression.evaluate(context), context, self.expression.span
27
+ )
28
+ )
29
+
30
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
31
+ buffer.write(
32
+ context.env.serialize(
33
+ await self.expression.evaluate_async(context),
34
+ context,
35
+ self.expression.span,
36
+ )
37
+ )
38
+
39
+ def children(self) -> list[Markup]:
40
+ return []
41
+
42
+ async def children_async(self) -> list[Markup]:
43
+ return []
44
+
45
+ def expressions(self) -> list[Expression]:
46
+ return [self.expression]
47
+
48
+ def block_scope(self) -> list[Name]:
49
+ return []
50
+
51
+ def template_scope(self) -> list[Name]:
52
+ return []
53
+
54
+ def partials(self, context: RenderContext) -> list[Partial]:
55
+ return []
56
+
57
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
58
+ return []
liquid/tags/raw.py ADDED
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..tokens import TOKEN_TAG_END, TOKEN_TEXT, token_value
6
+
7
+ if TYPE_CHECKING:
8
+ from .._context import RenderContext
9
+ from .._io import StringBuffer
10
+ from .._markup import Expression, Markup, Partial
11
+ from .._parser import Parser
12
+ from ..expressions import Name
13
+ from ..tokens import Token
14
+
15
+
16
+ class RawTag:
17
+ blank = False
18
+ tag = "raw"
19
+
20
+ __slots__ = ("text", "token")
21
+
22
+ def __init__(self, token: Token, text: str) -> None:
23
+ self.token = token
24
+ self.text = text
25
+
26
+ @staticmethod
27
+ def parse(token: Token, parser: Parser) -> Markup:
28
+ parser.carry_whitespace_control()
29
+ parser.eat(TOKEN_TAG_END)
30
+ text = (
31
+ token_value(parser.eat(TOKEN_TEXT), parser.source)
32
+ if parser.kind() == TOKEN_TEXT
33
+ else ""
34
+ )
35
+ parser.eat_empty_tag("endraw")
36
+ return RawTag(token, text)
37
+
38
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
39
+ buffer.write(self.text)
40
+
41
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
42
+ buffer.write(self.text)
43
+
44
+ def children(self) -> list[Markup]:
45
+ return []
46
+
47
+ def expressions(self) -> list[Expression]:
48
+ return []
49
+
50
+ def block_scope(self) -> list[Name]:
51
+ return []
52
+
53
+ def template_scope(self) -> list[Name]:
54
+ return []
55
+
56
+ def partials(self, context: RenderContext) -> list[Partial]:
57
+ return []
58
+
59
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
60
+ return []
liquid/tags/render.py ADDED
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .._markup import SCOPE_ISOLATED, Partial
6
+ from .._nothing import NOTHING
7
+ from ..drops import ForLoop
8
+ from ..exceptions import NoSuchTemplateError, TemplateNotFoundError
9
+ from ..expressions import Name, StringLiteral
10
+ from ..tokens import TOKEN_COMMA, TOKEN_TAG_END
11
+
12
+ if TYPE_CHECKING:
13
+ from .._context import RenderContext
14
+ from .._io import StringBuffer
15
+ from .._markup import Expression, Markup
16
+ from .._parser import Parser
17
+ from ..expressions import KeywordArgument
18
+ from ..tokens import Token
19
+
20
+
21
+ class RenderTag:
22
+ blank = False
23
+ tag = "render"
24
+
25
+ __slots__ = ("alias", "args", "loop", "name", "token", "variable")
26
+
27
+ def __init__(
28
+ self,
29
+ token: Token,
30
+ *,
31
+ name: StringLiteral,
32
+ loop: bool,
33
+ variable: Expression | None,
34
+ alias: Name | None,
35
+ args: list[KeywordArgument],
36
+ ) -> None:
37
+ self.token = token
38
+ self.name = name
39
+ self.loop = loop
40
+ self.variable = variable
41
+ self.alias = alias
42
+ self.args = args
43
+
44
+ @staticmethod
45
+ def parse(token: Token, parser: Parser) -> Markup:
46
+ name_expr = parser.parse_string_literal()
47
+
48
+ loop = False
49
+ variable: Expression | None = None
50
+ alias: Name | None = None
51
+
52
+ # `for`, `with` and `as` don't have their own tokens kinds.
53
+ ident = parser.value()
54
+ if ident in ("for", "with"):
55
+ loop = ident == "for"
56
+ parser.next()
57
+ variable = parser.parse_expression()
58
+
59
+ if parser.value() == "as":
60
+ parser.next()
61
+ alias = parser.parse_identifier()
62
+
63
+ if parser.kind() == TOKEN_COMMA:
64
+ parser.next()
65
+
66
+ args = parser.parse_keyword_arguments(require_commas=False)
67
+
68
+ parser.carry_whitespace_control()
69
+ parser.eat(TOKEN_TAG_END)
70
+
71
+ return RenderTag(
72
+ token,
73
+ name=name_expr,
74
+ loop=loop,
75
+ variable=variable,
76
+ alias=alias,
77
+ args=args,
78
+ )
79
+
80
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
81
+ template_name = self.name.value
82
+
83
+ try:
84
+ template = context.env.get_template(
85
+ template_name, context=context, tag="render"
86
+ )
87
+ except TemplateNotFoundError as err:
88
+ # Promote TemplateNotFoundError to NoSuchTemplateError
89
+ raise NoSuchTemplateError(
90
+ str(err),
91
+ token=self.name.span,
92
+ source=context.template.source,
93
+ name=context.template.name,
94
+ ) from None
95
+
96
+ bind_key = self.alias.value if self.alias else template.name.split(".", 1)[0]
97
+ bind_value: object
98
+
99
+ if self.variable:
100
+ bind_value = self.variable.evaluate(context)
101
+ else:
102
+ bind_value = context.resolve(template_name)
103
+ if bind_value == NOTHING:
104
+ bind_value = None
105
+
106
+ scope = {arg.name.value: arg.expr.evaluate(context) for arg in self.args}
107
+
108
+ ctx = context.copy(
109
+ scope,
110
+ disabled_tags=("include",),
111
+ block_scope=False,
112
+ carry_loop_iterations=True,
113
+ template=template,
114
+ )
115
+
116
+ if self.loop and isinstance(bind_value, list):
117
+ ctx.raise_for_loop_limit(len(bind_value)) # pyright: ignore[reportUnknownArgumentType]
118
+
119
+ forloop = ForLoop(bind_key, len(bind_value), None) # pyright: ignore[reportUnknownArgumentType]
120
+ scope["forloop"] = forloop
121
+
122
+ for item in bind_value: # pyright: ignore[reportUnknownVariableType]
123
+ scope[bind_key] = item
124
+ forloop.step()
125
+ template.render_with_context(ctx, buffer)
126
+ else:
127
+ scope[bind_key] = bind_value
128
+ template.render_with_context(ctx, buffer)
129
+
130
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
131
+ template_name = self.name.value
132
+
133
+ try:
134
+ template = await context.env.get_template_async(
135
+ template_name, context=context, tag="render"
136
+ )
137
+ except TemplateNotFoundError as err:
138
+ # Promote TemplateNotFoundError to NoSuchTemplateError
139
+ raise NoSuchTemplateError(
140
+ str(err),
141
+ token=self.name.span,
142
+ source=context.template.source,
143
+ name=context.template.name,
144
+ ) from None
145
+
146
+ bind_key = self.alias.value if self.alias else template.name.split(".", 1)[0]
147
+ bind_value: object
148
+
149
+ if self.variable:
150
+ bind_value = await self.variable.evaluate_async(context)
151
+ else:
152
+ bind_value = context.resolve(template_name)
153
+ if bind_value == NOTHING:
154
+ bind_value = None
155
+
156
+ scope = {
157
+ arg.name.value: await arg.expr.evaluate_async(context) for arg in self.args
158
+ }
159
+
160
+ ctx = context.copy(
161
+ scope,
162
+ disabled_tags=("include",),
163
+ block_scope=False,
164
+ carry_loop_iterations=True,
165
+ template=template,
166
+ )
167
+
168
+ if self.loop and isinstance(bind_value, list):
169
+ ctx.raise_for_loop_limit(len(bind_value)) # pyright: ignore[reportUnknownArgumentType]
170
+
171
+ forloop = ForLoop(bind_key, len(bind_value), None) # pyright: ignore[reportUnknownArgumentType]
172
+ scope["forloop"] = forloop
173
+ for item in bind_value: # pyright: ignore[reportUnknownVariableType]
174
+ scope[bind_key] = item
175
+ forloop.step()
176
+ await template.render_with_context_async(ctx, buffer)
177
+ else:
178
+ scope[bind_key] = bind_value
179
+ await template.render_with_context_async(ctx, buffer)
180
+
181
+ def children(self) -> list[Markup]:
182
+ return []
183
+
184
+ def expressions(self) -> list[Expression]:
185
+ result: list[Expression] = []
186
+
187
+ if self.variable:
188
+ result.append(self.variable)
189
+
190
+ result.extend(arg.expr for arg in self.args)
191
+ return result
192
+
193
+ def block_scope(self) -> list[Name]:
194
+ return []
195
+
196
+ def template_scope(self) -> list[Name]:
197
+ return []
198
+
199
+ def partials(self, context: RenderContext) -> list[Partial]:
200
+ template = context.env.get_template(
201
+ self.name.value,
202
+ context=context,
203
+ tag="render",
204
+ )
205
+
206
+ scope: list[Name] = [arg.name for arg in self.args]
207
+
208
+ if self.variable:
209
+ if self.alias:
210
+ scope.append(self.alias)
211
+ else:
212
+ scope.append(Name(self.name.token, self.name.value))
213
+
214
+ return [
215
+ Partial(
216
+ template=template,
217
+ scope=SCOPE_ISOLATED,
218
+ in_scope=scope,
219
+ key=hash((self.name.value, ":".join(n.value for n in scope))),
220
+ )
221
+ ]
222
+
223
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
224
+ template = await context.env.get_template_async(
225
+ self.name.value,
226
+ context=context,
227
+ tag="render",
228
+ )
229
+
230
+ scope: list[Name] = [arg.name for arg in self.args]
231
+
232
+ if self.variable:
233
+ if self.alias:
234
+ scope.append(self.alias)
235
+ else:
236
+ scope.append(Name(self.name.token, self.name.value))
237
+
238
+ return [
239
+ Partial(
240
+ template=template,
241
+ scope=SCOPE_ISOLATED,
242
+ in_scope=scope,
243
+ key=hash((self.name.value, ":".join(n.value for n in scope))),
244
+ )
245
+ ]
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .._drop import liquid_iterable
6
+ from .._markup import render_block, render_block_async
7
+ from .._slice import slice_sequence
8
+ from ..drops import TableRowLoop
9
+ from ..exceptions import LiquidSyntaxError
10
+ from ..tokens import TOKEN_COMMA, TOKEN_IN, TOKEN_TAG_END
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Iterable
14
+
15
+ from .._context import RenderContext
16
+ from .._io import StringBuffer
17
+ from .._markup import Block, Expression, Markup, Partial
18
+ from .._parser import Parser
19
+ from ..expressions import Name
20
+ from ..tokens import Token
21
+
22
+
23
+ class TableRowTag:
24
+ blank = False
25
+ tag = "tablerow"
26
+
27
+ __slots__ = (
28
+ "block",
29
+ "cols",
30
+ "expression",
31
+ "limit",
32
+ "name",
33
+ "offset",
34
+ "token",
35
+ )
36
+
37
+ def __init__(
38
+ self,
39
+ token: Token,
40
+ name: Name,
41
+ expression: Expression,
42
+ block: Block,
43
+ *,
44
+ cols: Expression | None,
45
+ offset: Expression | None,
46
+ limit: Expression | None,
47
+ ) -> None:
48
+ self.token = token
49
+ self.name = name
50
+ self.expression = expression
51
+ self.block: Block = block
52
+ self.offset = offset
53
+ self.limit = limit
54
+ self.cols = cols
55
+
56
+ @staticmethod
57
+ def parse(token: Token, parser: Parser) -> Markup:
58
+ ident = parser.parse_identifier()
59
+ parser.eat(TOKEN_IN, message="missing 'in'")
60
+ parser.expect_expression()
61
+ expr = parser.parse_expression()
62
+
63
+ if parser.kind() == TOKEN_COMMA:
64
+ parser.next()
65
+
66
+ args = parser.parse_keyword_arguments(require_commas=False)
67
+
68
+ cols: Expression | None = None
69
+ offset: Expression | None = None
70
+ limit: Expression | None = None
71
+
72
+ for arg in args:
73
+ if arg.name.value == "offset":
74
+ offset = arg.expr
75
+ elif arg.name.value == "limit":
76
+ limit = arg.expr
77
+ elif arg.name.value == "cols":
78
+ cols = arg.expr
79
+ elif arg.name.value == "range":
80
+ pass
81
+ else:
82
+ raise LiquidSyntaxError(
83
+ f"unknown argument {arg.name.value!r}",
84
+ token=arg.name.token,
85
+ source=parser.source,
86
+ name=parser.template_name,
87
+ )
88
+
89
+ parser.carry_whitespace_control()
90
+ parser.eat(TOKEN_TAG_END)
91
+
92
+ block = parser.parse_block(("endtablerow",))
93
+ parser.eat_empty_tag("endtablerow")
94
+
95
+ return TableRowTag(
96
+ token,
97
+ ident,
98
+ expr,
99
+ block,
100
+ cols=cols,
101
+ offset=offset,
102
+ limit=limit,
103
+ )
104
+
105
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
106
+ offset = _to_int(self.offset, context, 0, 0)
107
+ limit = _to_int(self.limit, context, None, 0)
108
+
109
+ target = self.expression.evaluate(context)
110
+ it: Iterable[object]
111
+
112
+ if context.env.is_nil(target):
113
+ return
114
+
115
+ if liquid_iterable(target):
116
+ it = target.__liquid_iter__(offset, limit, False)
117
+ length = len(it)
118
+ else:
119
+ a = context.env.to_array(target, context, self.expression.span)
120
+ it, length = slice_sequence(a, offset, limit, False)
121
+
122
+ cols = _to_int(self.cols, context, length, 0)
123
+ tablerowloop = TableRowLoop(length, cols)
124
+
125
+ name = self.name.value
126
+ namespace: dict[str, object] = {"tablerowloop": tablerowloop}
127
+
128
+ with context.loop(namespace, tablerowloop):
129
+ buffer.write('<tr class="row1">\n')
130
+
131
+ for item in it:
132
+ namespace[name] = item
133
+ buffer.write(f'<td class="col{tablerowloop["col"]}">')
134
+ render_block(self.block, context, buffer)
135
+ buffer.write("</td>")
136
+
137
+ if context.interrupts and context.interrupts.pop() == "break":
138
+ break
139
+
140
+ if tablerowloop["col_last"] and not tablerowloop["last"]:
141
+ buffer.write(f'</tr>\n<tr class="row{tablerowloop["row"] + 1}">')
142
+
143
+ tablerowloop.step()
144
+
145
+ buffer.write("</tr>\n")
146
+
147
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
148
+ offset = await _to_int_async(self.offset, context, 0, 0)
149
+ limit = await _to_int_async(self.limit, context, None, 0)
150
+
151
+ target = await self.expression.evaluate_async(context)
152
+ it: Iterable[object]
153
+
154
+ if context.env.is_nil(target):
155
+ return
156
+
157
+ if liquid_iterable(target):
158
+ it = target.__liquid_iter__(offset, limit, False)
159
+ length = len(it)
160
+ else:
161
+ a = context.env.to_array(target, context, self.expression.span)
162
+ it, length = slice_sequence(a, offset, limit, False)
163
+
164
+ cols = await _to_int_async(self.cols, context, length, 0)
165
+ tablerowloop = TableRowLoop(length, cols)
166
+
167
+ name = self.name.value
168
+ namespace: dict[str, object] = {"tablerowloop": tablerowloop}
169
+
170
+ with context.loop(namespace, tablerowloop):
171
+ buffer.write('<tr class="row1">\n')
172
+
173
+ for item in it:
174
+ namespace[name] = item
175
+ buffer.write(f'<td class="col{tablerowloop["col"]}">')
176
+ await render_block_async(self.block, context, buffer)
177
+ buffer.write("</td>")
178
+
179
+ if context.interrupts and context.interrupts.pop() == "break":
180
+ break
181
+
182
+ if tablerowloop["col_last"] and not tablerowloop["last"]:
183
+ buffer.write(f'</tr>\n<tr class="row{tablerowloop["row"] + 1}">')
184
+
185
+ tablerowloop.step()
186
+
187
+ buffer.write("</tr>\n")
188
+
189
+ def children(self) -> list[Markup]:
190
+ result: list[Markup] = [
191
+ node for node in self.block if not isinstance(node, str)
192
+ ]
193
+
194
+ return result
195
+
196
+ def expressions(self) -> list[Expression]:
197
+ result = [self.expression]
198
+
199
+ if self.cols:
200
+ result.append(self.cols)
201
+
202
+ if self.offset:
203
+ result.append(self.offset)
204
+
205
+ if self.limit:
206
+ result.append(self.limit)
207
+
208
+ return result
209
+
210
+ def block_scope(self) -> list[Name]:
211
+ return [self.name]
212
+
213
+ def template_scope(self) -> list[Name]:
214
+ return []
215
+
216
+ def partials(self, context: RenderContext) -> list[Partial]:
217
+ return []
218
+
219
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
220
+ return []
221
+
222
+
223
+ def _to_int[T](
224
+ expression: Expression | None,
225
+ context: RenderContext,
226
+ default: T,
227
+ null: T,
228
+ ) -> int | T:
229
+ if expression is None:
230
+ return default
231
+
232
+ value = expression.evaluate(context)
233
+
234
+ if context.env.is_nil(value):
235
+ return null
236
+
237
+ return context.env.to_int(value, context, expression.span)
238
+
239
+
240
+ async def _to_int_async[T](
241
+ expression: Expression | None,
242
+ context: RenderContext,
243
+ default: T,
244
+ null: T,
245
+ ) -> int | T:
246
+ if expression is None:
247
+ return default
248
+
249
+ value = await expression.evaluate_async(context)
250
+
251
+ if context.env.is_nil(value):
252
+ return null
253
+
254
+ return context.env.to_int(value, context, expression.span)
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..tokens import TOKEN_COMMENT, TOKEN_COMMENT_END, token_value
6
+
7
+ if TYPE_CHECKING:
8
+ from .._context import RenderContext
9
+ from .._io import StringBuffer
10
+ from .._markup import Expression, Markup, Partial
11
+ from .._parser import Parser
12
+ from ..expressions import Name
13
+ from ..tokens import Token
14
+
15
+
16
+ class TemplateComment:
17
+ blank = True
18
+ tag = ""
19
+
20
+ __slots__ = ("text", "token")
21
+
22
+ def __init__(self, token: Token, text: str) -> None:
23
+ self.token = token
24
+ self.text = text
25
+
26
+ @staticmethod
27
+ def parse(token: Token, parser: Parser) -> Markup:
28
+ parser.carry_whitespace_control()
29
+ comment = parser.eat(TOKEN_COMMENT)
30
+ parser.carry_whitespace_control()
31
+ parser.eat(TOKEN_COMMENT_END)
32
+ return TemplateComment(token, token_value(comment, parser.source))
33
+
34
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
35
+ return None
36
+
37
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
38
+ return None
39
+
40
+ def children(self) -> list[Markup]:
41
+ return []
42
+
43
+ def expressions(self) -> list[Expression]:
44
+ return []
45
+
46
+ def block_scope(self) -> list[Name]:
47
+ return []
48
+
49
+ def template_scope(self) -> list[Name]:
50
+ return []
51
+
52
+ def partials(self, context: RenderContext) -> list[Partial]:
53
+ return []
54
+
55
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
56
+ return []