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
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import importlib.resources
5
+ from pathlib import Path, PurePath
6
+ from typing import TYPE_CHECKING
7
+
8
+ from .._loader import Loader, TemplateSource
9
+ from ..exceptions import TemplateNotFoundError
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterable
13
+ from types import ModuleType
14
+
15
+ from .._context import RenderContext
16
+ from ..environment import Environment
17
+
18
+
19
+ class PackageLoader(Loader):
20
+ """A template loader that reads templates from Python packages.
21
+
22
+ Parameters
23
+ ----------
24
+ package
25
+ Import name of a package containing Liquid templates.
26
+ package_path
27
+ One or more directories in the package containing Liquid templates.
28
+ encoding
29
+ Encoding of template files.
30
+ ext
31
+ A default file extension to use if one is not provided. Should include
32
+ a leading period.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ package: str | ModuleType,
38
+ *,
39
+ package_path: str | Iterable[str] = "templates",
40
+ encoding: str = "utf-8",
41
+ ext: str = ".liquid",
42
+ ) -> None:
43
+ if isinstance(package_path, str):
44
+ self.paths = [importlib.resources.files(package).joinpath(package_path)]
45
+ else:
46
+ _package = importlib.resources.files(package)
47
+ self.paths = [_package.joinpath(path) for path in package_path]
48
+
49
+ self.encoding = encoding
50
+ self.ext = ext
51
+
52
+ def _resolve_path(self, name: str) -> Path:
53
+ template_path = PurePath(name)
54
+ if template_path.anchor:
55
+ template_path = PurePath(*template_path.parts[1:])
56
+
57
+ # Add suffix self.ext if template name does not have a suffix.
58
+ if not template_path.suffix:
59
+ template_path = template_path.with_suffix(self.ext)
60
+
61
+ for path in self.paths:
62
+ with importlib.resources.as_file(path) as base:
63
+ base_resolved = base.resolve()
64
+ source_path: Path = (base_resolved / template_path).resolve()
65
+
66
+ if not source_path.is_relative_to(base_resolved):
67
+ continue
68
+
69
+ if not source_path.is_file():
70
+ continue
71
+
72
+ # We don't want to expose a fully qualified path in error messages.
73
+ return base / template_path
74
+
75
+ raise TemplateNotFoundError(name)
76
+
77
+ def get_source(
78
+ self,
79
+ env: Environment,
80
+ name: str,
81
+ context: RenderContext | None = None,
82
+ **kwargs: object,
83
+ ) -> TemplateSource:
84
+ source_path = self._resolve_path(name)
85
+ return TemplateSource(
86
+ text=source_path.read_text(self.encoding),
87
+ name=source_path.name,
88
+ path=str(source_path),
89
+ )
90
+
91
+ async def get_source_async(
92
+ self,
93
+ env: Environment,
94
+ name: str,
95
+ context: RenderContext | None = None,
96
+ **kwargs: object,
97
+ ) -> TemplateSource:
98
+ loop = asyncio.get_running_loop()
99
+
100
+ source_path = await loop.run_in_executor(None, self._resolve_path, name)
101
+
102
+ source_text = await loop.run_in_executor(
103
+ None, source_path.read_text, self.encoding
104
+ )
105
+
106
+ return TemplateSource(
107
+ text=source_text,
108
+ name=source_path.name,
109
+ path=str(source_path),
110
+ )
@@ -0,0 +1,61 @@
1
+ from .assign import AssignTag
2
+ from .capture import CaptureTag
3
+ from .case import CaseTag
4
+ from .comment import CommentTag
5
+ from .cycle import CycleTag
6
+ from .decrement import DecrementTag
7
+ from .doc import DocTag
8
+ from .echo import EchoTag
9
+ from .else_ import ElseBlock
10
+ from .error import ErrorNode
11
+ from .extends import BlockTag, ExtendsTag
12
+ from .for_ import BreakTag, ContinueTag, ForTag
13
+ from .if_ import IfTag
14
+ from .ifchanged import IfChangedTag
15
+ from .include import IncludeTag
16
+ from .increment import IncrementTag
17
+ from .inline_comment import InlineCommentTag
18
+ from .liquid import LiquidTag
19
+ from .macro import CallTag, MacroTag
20
+ from .output import OutputStatement
21
+ from .raw import RawTag
22
+ from .render import RenderTag
23
+ from .tablerow import TableRowTag
24
+ from .template_comment import TemplateComment
25
+ from .translate import TranslateTag
26
+ from .unless import UnlessTag
27
+ from .with_ import WithTag
28
+
29
+ __all__ = (
30
+ "AssignTag",
31
+ "BlockTag",
32
+ "BreakTag",
33
+ "CallTag",
34
+ "CaptureTag",
35
+ "CaseTag",
36
+ "CommentTag",
37
+ "ContinueTag",
38
+ "CycleTag",
39
+ "DecrementTag",
40
+ "DocTag",
41
+ "EchoTag",
42
+ "ElseBlock",
43
+ "ErrorNode",
44
+ "ExtendsTag",
45
+ "ForTag",
46
+ "IfChangedTag",
47
+ "IfTag",
48
+ "IncludeTag",
49
+ "IncrementTag",
50
+ "InlineCommentTag",
51
+ "LiquidTag",
52
+ "MacroTag",
53
+ "OutputStatement",
54
+ "RawTag",
55
+ "RenderTag",
56
+ "TableRowTag",
57
+ "TemplateComment",
58
+ "TranslateTag",
59
+ "UnlessTag",
60
+ "WithTag",
61
+ )
liquid/tags/assign.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..exceptions import LiquidSyntaxError
6
+ from ..tokens import TOKEN_ASSIGN, TOKEN_TAG_END
7
+
8
+ if TYPE_CHECKING:
9
+ from .._context import RenderContext
10
+ from .._io import StringBuffer
11
+ from .._markup import Expression, Markup, Partial
12
+ from .._parser import Parser
13
+ from ..expressions import Name
14
+ from ..tokens import Token
15
+
16
+
17
+ class AssignTag:
18
+ blank = True
19
+ tag = "assign"
20
+
21
+ __slots__ = ("expression", "name", "token")
22
+
23
+ def __init__(self, token: Token, name: Name, expression: Expression) -> None:
24
+ self.token = token
25
+ self.name = name
26
+ self.expression = expression
27
+
28
+ @staticmethod
29
+ def parse(token: Token, parser: Parser) -> Markup:
30
+ name = parser.parse_identifier()
31
+
32
+ if name.value.endswith("?"):
33
+ raise LiquidSyntaxError(
34
+ "invalid variable name",
35
+ token=name.span,
36
+ source=parser.source,
37
+ name=parser.template_name,
38
+ )
39
+
40
+ parser.eat(TOKEN_ASSIGN, "bad identifier or missing '='")
41
+ expression = parser.parse_filtered_expression()
42
+ parser.carry_whitespace_control()
43
+ parser.eat(TOKEN_TAG_END)
44
+ return AssignTag(token, name, expression)
45
+
46
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
47
+ context.assign(self.name.value, self.expression.evaluate(context))
48
+
49
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
50
+ context.assign(self.name.value, await self.expression.evaluate_async(context))
51
+
52
+ def children(self) -> list[Markup]:
53
+ return []
54
+
55
+ def expressions(self) -> list[Expression]:
56
+ return [self.expression]
57
+
58
+ def block_scope(self) -> list[Name]:
59
+ return []
60
+
61
+ def template_scope(self) -> list[Name]:
62
+ return [self.name]
63
+
64
+ def partials(self, context: RenderContext) -> list[Partial]:
65
+ return []
66
+
67
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
68
+ return []
liquid/tags/capture.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import markupsafe
6
+
7
+ from .._markup import render_block, render_block_async
8
+ from ..exceptions import LiquidSyntaxError
9
+ from ..tokens import TOKEN_TAG_END
10
+
11
+ if TYPE_CHECKING:
12
+ from .._context import RenderContext
13
+ from .._io import StringBuffer
14
+ from .._markup import Block, Expression, Markup, Partial
15
+ from .._parser import Parser
16
+ from ..expressions import Name
17
+ from ..tokens import Token
18
+
19
+
20
+ class CaptureTag:
21
+ blank = True
22
+ tag = "capture"
23
+
24
+ __slots__ = ("block", "name", "token")
25
+
26
+ def __init__(self, token: Token, name: Name, block: Block) -> None:
27
+ self.token = token
28
+ self.name = name
29
+ self.block = block
30
+
31
+ @staticmethod
32
+ def parse(token: Token, parser: Parser) -> Markup:
33
+ name = parser.parse_identifier()
34
+
35
+ if name.value.endswith("?"):
36
+ raise LiquidSyntaxError(
37
+ "invalid variable name",
38
+ token=name.span,
39
+ source=parser.source,
40
+ name=parser.template_name,
41
+ )
42
+
43
+ parser.carry_whitespace_control()
44
+ parser.eat(TOKEN_TAG_END)
45
+
46
+ block = parser.parse_block(end=("endcapture",))
47
+ parser.eat_empty_tag("endcapture")
48
+ return CaptureTag(token, name, block)
49
+
50
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
51
+ buf = context.env.buffer_factory()
52
+ render_block(self.block, context, buf)
53
+
54
+ if context.env.autoescape:
55
+ context.assign(self.name.value, markupsafe.Markup(buf.getvalue()))
56
+ else:
57
+ context.assign(self.name.value, buf.getvalue())
58
+
59
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
60
+ buf = context.env.buffer_factory()
61
+ await render_block_async(self.block, context, buf)
62
+ if context.env.autoescape:
63
+ context.assign(self.name.value, markupsafe.Markup(buf.getvalue()))
64
+ else:
65
+ context.assign(self.name.value, buf.getvalue())
66
+
67
+ def children(self) -> list[Markup]:
68
+ return [node for node in self.block if not isinstance(node, str)]
69
+
70
+ def expressions(self) -> list[Expression]:
71
+ return []
72
+
73
+ def block_scope(self) -> list[Name]:
74
+ return []
75
+
76
+ def template_scope(self) -> list[Name]:
77
+ return [self.name]
78
+
79
+ def partials(self, context: RenderContext) -> list[Partial]:
80
+ return []
81
+
82
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
83
+ return []
liquid/tags/case.py ADDED
@@ -0,0 +1,199 @@
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_COMMA,
8
+ TOKEN_OR,
9
+ TOKEN_TAG_END,
10
+ TOKEN_TAG_NAME,
11
+ TOKEN_TAG_START,
12
+ TOKEN_TEXT,
13
+ )
14
+ from .else_ import ElseBlock
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+
19
+ from .._context import RenderContext
20
+ from .._io import StringBuffer
21
+ from .._markup import Block, Expression, Markup, Partial
22
+ from .._parser import Parser
23
+ from ..expressions import Name
24
+ from ..tokens import Token
25
+
26
+
27
+ class CaseTag:
28
+ tag = "case"
29
+
30
+ __slots__ = ("blank", "blocks", "expression", "token")
31
+
32
+ def __init__(
33
+ self,
34
+ token: Token,
35
+ expression: Expression,
36
+ blocks: Sequence[WhenBlock | ElseBlock],
37
+ ) -> None:
38
+ self.token = token
39
+ self.expression = expression
40
+ self.blocks = blocks
41
+
42
+ self.blank = all(block.blank for block in blocks)
43
+
44
+ if self.blank:
45
+ # Discard blank strings from all blocks.
46
+ for block in blocks:
47
+ block.filter_strings()
48
+
49
+ @staticmethod
50
+ def parse(token: Token, parser: Parser) -> Markup:
51
+ parser.expect_expression()
52
+ expression = parser.parse_expression(expect_end_of_expression=True)
53
+ parser.carry_whitespace_control()
54
+ parser.eat(TOKEN_TAG_END)
55
+
56
+ if parser.kind() == TOKEN_TEXT:
57
+ # Junk between `{% case %}` and first `{% when %}`.
58
+ parser.eat(TOKEN_TEXT)
59
+
60
+ blocks: list[WhenBlock | ElseBlock] = []
61
+
62
+ while 1:
63
+ tag_name = parser.tags(("when", "else"))
64
+
65
+ if tag_name == "when":
66
+ blocks.append(CaseTag.parse_when(parser))
67
+ elif tag_name == "else":
68
+ name_token = parser.eat_tag("else")
69
+ blocks.append(
70
+ ElseBlock(
71
+ name_token, parser.parse_block(("endcase", "when", "else"))
72
+ )
73
+ )
74
+ else:
75
+ break
76
+
77
+ parser.eat_empty_tag("endcase")
78
+ return CaseTag(token, expression, blocks)
79
+
80
+ @staticmethod
81
+ def parse_when(parser: Parser) -> WhenBlock:
82
+ parser.eat(TOKEN_TAG_START)
83
+ parser.skip_whitespace_control()
84
+ token = parser.eat(TOKEN_TAG_NAME)
85
+
86
+ if parser.kind() == TOKEN_COMMA:
87
+ parser.next()
88
+
89
+ right: list[Expression] = []
90
+
91
+ while 1:
92
+ right.append(parser.parse_expression())
93
+
94
+ if parser.kind() not in (TOKEN_COMMA, TOKEN_OR):
95
+ break
96
+
97
+ parser.next()
98
+
99
+ parser.carry_whitespace_control()
100
+ parser.eat(TOKEN_TAG_END)
101
+ block = parser.parse_block(("endcase", "when", "else"))
102
+ return WhenBlock(token, right, block)
103
+
104
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
105
+ left = self.expression.evaluate(context)
106
+ alt = True
107
+
108
+ # NOTE: This does not stop at the first truthy expression, nor does it stop
109
+ # at the first `else` block.
110
+
111
+ for block in self.blocks:
112
+ if isinstance(block, ElseBlock):
113
+ if alt:
114
+ block.render(context, buffer)
115
+ continue
116
+
117
+ for expr in block.right:
118
+ if context.env.eq(left, expr.evaluate(context), context, expr.span):
119
+ alt = False
120
+ block.render(context, buffer)
121
+
122
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
123
+ left = await self.expression.evaluate_async(context)
124
+ alt = True
125
+
126
+ # NOTE: This does not stop at the first truthy expression, nor does it stop
127
+ # at the first `else` block.
128
+
129
+ for block in self.blocks:
130
+ if isinstance(block, ElseBlock):
131
+ if alt:
132
+ await block.render_async(context, buffer)
133
+ continue
134
+
135
+ for expr in block.right:
136
+ if context.env.eq(
137
+ left, await expr.evaluate_async(context), context, expr.span
138
+ ):
139
+ alt = False
140
+ await block.render_async(context, buffer)
141
+
142
+ def children(self) -> list[Markup]:
143
+ return list(self.blocks)
144
+
145
+ def expressions(self) -> list[Expression]:
146
+ return [self.expression]
147
+
148
+ def block_scope(self) -> list[Name]:
149
+ return []
150
+
151
+ def template_scope(self) -> list[Name]:
152
+ return []
153
+
154
+ def partials(self, context: RenderContext) -> list[Partial]:
155
+ return []
156
+
157
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
158
+ return []
159
+
160
+
161
+ class WhenBlock:
162
+ tag = "when"
163
+
164
+ def __init__(self, token: Token, right: list[Expression], block: Block) -> None:
165
+ self.token = token
166
+ self.right = right
167
+ self.block = block
168
+ self.blank = is_blank_block(block)
169
+
170
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
171
+ render_block(self.block, context, buffer)
172
+
173
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
174
+ await render_block_async(self.block, context, buffer)
175
+
176
+ def children(self) -> list[Markup]:
177
+ return [node for node in self.block if not isinstance(node, str)]
178
+
179
+ async def children_async(self) -> list[Markup]:
180
+ return self.children()
181
+
182
+ def expressions(self) -> list[Expression]:
183
+ return self.right
184
+
185
+ def block_scope(self) -> list[Name]:
186
+ return []
187
+
188
+ def template_scope(self) -> list[Name]:
189
+ return []
190
+
191
+ def partials(self, context: RenderContext) -> list[Partial]:
192
+ return []
193
+
194
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
195
+ return []
196
+
197
+ def filter_strings(self) -> None:
198
+ markup: Block = [node for node in self.block if not isinstance(node, str)]
199
+ self.block = markup
liquid/tags/comment.py ADDED
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..tokens import TOKEN_COMMENT, TOKEN_TAG_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 CommentTag:
17
+ blank = True
18
+ tag = "comment"
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
+ comment = parser.eat(TOKEN_COMMENT)
31
+ parser.eat_empty_tag("endcomment")
32
+ return CommentTag(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 []
liquid/tags/cycle.py ADDED
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ..expressions import Variable
7
+ from ..tokens import TOKEN_COLON, TOKEN_COMMA, TOKEN_TAG_END
8
+
9
+ if TYPE_CHECKING:
10
+ from .._context import RenderContext
11
+ from .._io import StringBuffer
12
+ from .._markup import Expression, Markup, Partial
13
+ from .._parser import Parser
14
+ from ..expressions import Name
15
+ from ..tokens import Token
16
+
17
+
18
+ class CycleTag:
19
+ blank = False
20
+ tag = "cycle"
21
+
22
+ __slots__ = ("group", "items", "static_key", "token")
23
+
24
+ def __init__(
25
+ self, token: Token, group: Expression | None, items: list[Expression]
26
+ ) -> None:
27
+ self.token = token
28
+ self.group = group
29
+ self.items = items
30
+ self.static_key = ""
31
+
32
+ if group is None:
33
+ # This mimics Shopify/liquid by giving every Variable a unique ID.
34
+ self.static_key = str(
35
+ [f"{i}:{id(i)}" if isinstance(i, Variable) else str(i) for i in items]
36
+ )
37
+
38
+ @staticmethod
39
+ def parse(token: Token, parser: Parser) -> Markup:
40
+ group: Expression | None = None
41
+ items: list[Expression] = []
42
+
43
+ first = parser.parse_expression()
44
+
45
+ if parser.kind() == TOKEN_COLON:
46
+ group = first
47
+ parser.next()
48
+ else:
49
+ items.append(first)
50
+
51
+ if parser.kind() == TOKEN_COMMA:
52
+ parser.next()
53
+
54
+ items.extend(parser.parse_positional_arguments())
55
+ parser.carry_whitespace_control()
56
+ parser.eat(TOKEN_TAG_END)
57
+
58
+ return CycleTag(token, group, items)
59
+
60
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
61
+ cycles = context.get_register("cycle", default=register_factory)
62
+ key = self.static_key
63
+
64
+ if self.group:
65
+ key = context.env.to_str(
66
+ self.group.evaluate(context),
67
+ context,
68
+ self.group.span,
69
+ )
70
+
71
+ index = cycles[key]
72
+
73
+ if index < len(self.items):
74
+ expr = self.items[index]
75
+ buffer.write(context.env.to_str(expr.evaluate(context), context, expr.span))
76
+
77
+ index += 1
78
+ if index >= len(self.items):
79
+ index = 0
80
+
81
+ cycles[key] = index
82
+
83
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
84
+ cycles = context.get_register("cycle", default=register_factory)
85
+ key = self.static_key
86
+
87
+ if self.group:
88
+ key = context.env.to_str(
89
+ await self.group.evaluate_async(context),
90
+ context,
91
+ self.group.span,
92
+ )
93
+
94
+ index = cycles[key]
95
+
96
+ if index < len(self.items):
97
+ expr = self.items[index]
98
+
99
+ buffer.write(
100
+ context.env.to_str(
101
+ await expr.evaluate_async(context), context, expr.span
102
+ )
103
+ )
104
+
105
+ index += 1
106
+ if index >= len(self.items):
107
+ index = 0
108
+
109
+ cycles[key] = index
110
+
111
+ def children(self) -> list[Markup]:
112
+ return []
113
+
114
+ def expressions(self) -> list[Expression]:
115
+ return [self.group, *self.items] if self.group else self.items
116
+
117
+ def block_scope(self) -> list[Name]:
118
+ return []
119
+
120
+ def template_scope(self) -> list[Name]:
121
+ return []
122
+
123
+ def partials(self, context: RenderContext) -> list[Partial]:
124
+ return []
125
+
126
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
127
+ return []
128
+
129
+
130
+ def register_factory() -> defaultdict[str, int]:
131
+ return defaultdict(int)