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
@@ -1,132 +0,0 @@
1
- """A template loader that reads templates from Python packages."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import os
7
- from pathlib import Path
8
- from typing import TYPE_CHECKING
9
- from typing import Iterable
10
- from typing import Optional
11
- from typing import Union
12
-
13
- import importlib_resources
14
-
15
- from liquid.exceptions import TemplateNotFoundError
16
- from liquid.loader import BaseLoader
17
- from liquid.loader import TemplateSource
18
-
19
- if TYPE_CHECKING:
20
- from types import ModuleType
21
-
22
- from liquid import Environment
23
- from liquid import RenderContext
24
-
25
-
26
- class PackageLoader(BaseLoader):
27
- """A template loader that reads templates from Python packages.
28
-
29
- Args:
30
- package: Import name of a package containing Liquid templates.
31
- package_path: One or more directories in the package containing Liquid
32
- templates.
33
- encoding: Encoding of template files.
34
- ext: A default file extension to use if one is not provided. Should
35
- include a leading period.
36
- """
37
-
38
- def __init__(
39
- self,
40
- package: Union[str, ModuleType],
41
- *,
42
- package_path: Union[str, Iterable[str]] = "templates",
43
- encoding: str = "utf-8",
44
- ext: str = ".liquid",
45
- ) -> None:
46
- if isinstance(package_path, str):
47
- self.paths = [importlib_resources.files(package).joinpath(package_path)]
48
- else:
49
- _package = importlib_resources.files(package)
50
- self.paths = [_package.joinpath(path) for path in package_path]
51
-
52
- self.encoding = encoding
53
- self.ext = ext
54
-
55
- def _resolve_path(self, template_name: str) -> Path:
56
- template_path = Path(template_name)
57
-
58
- if not template_path.name:
59
- raise TemplateNotFoundError(template_name)
60
-
61
- # Add suffix self.ext if template name does not have a suffix.
62
- if not template_path.suffix:
63
- template_path = template_path.with_suffix(self.ext)
64
-
65
- if os.path.pardir in template_path.parts or template_path.is_absolute():
66
- raise TemplateNotFoundError(template_name)
67
-
68
- for path in self.paths:
69
- with importlib_resources.as_file(path) as base:
70
- source_path: Path = base.joinpath(template_path)
71
-
72
- try:
73
- if not source_path.exists() or not source_path.is_file():
74
- continue
75
- except OSError:
76
- # "File name too long", for example
77
- continue
78
-
79
- resolved = source_path.resolve(strict=False)
80
- base_resolved = base.resolve(strict=False)
81
-
82
- if not resolved.is_relative_to(base_resolved):
83
- continue
84
-
85
- return source_path
86
-
87
- raise TemplateNotFoundError(template_name)
88
-
89
- def get_source(
90
- self,
91
- env: Environment, # noqa: ARG002
92
- template_name: str,
93
- *,
94
- context: Optional[RenderContext] = None, # noqa: ARG002
95
- **kwargs: object, # noqa: ARG002
96
- ) -> TemplateSource:
97
- """Get source information for a template."""
98
- source_path = self._resolve_path(template_name)
99
- return TemplateSource(
100
- text=source_path.read_text(self.encoding),
101
- name=str(source_path),
102
- uptodate=None,
103
- )
104
-
105
- async def get_source_async(
106
- self,
107
- env: Environment, # noqa: ARG002
108
- template_name: str,
109
- *,
110
- context: Optional[RenderContext] = None, # noqa: ARG002
111
- **kwargs: object, # noqa: ARG002
112
- ) -> TemplateSource:
113
- """Get source information for a template."""
114
- loop = asyncio.get_running_loop()
115
-
116
- source_path = await loop.run_in_executor(
117
- None,
118
- self._resolve_path,
119
- template_name,
120
- )
121
-
122
- source_text = await loop.run_in_executor(
123
- None,
124
- source_path.read_text,
125
- self.encoding,
126
- )
127
-
128
- return TemplateSource(
129
- text=source_text,
130
- name=str(source_path),
131
- uptodate=None,
132
- )
liquid/builtin/output.py DELETED
@@ -1,74 +0,0 @@
1
- """Parse tree node and pseudo "tag" for output statements."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
- from typing import Iterable
7
- from typing import TextIO
8
-
9
- from liquid.ast import Node
10
- from liquid.builtin.expressions import FilteredExpression
11
- from liquid.builtin.expressions import tokenize
12
- from liquid.stream import TokenStream
13
- from liquid.stringify import to_liquid_string
14
- from liquid.tag import Tag
15
- from liquid.token import TOKEN_EXPRESSION
16
- from liquid.token import TOKEN_OUTPUT
17
- from liquid.token import Token
18
-
19
- if TYPE_CHECKING:
20
- from liquid.context import RenderContext
21
- from liquid.expression import Expression
22
-
23
-
24
- class OutputNode(Node):
25
- """Parse tree node representing an output statement."""
26
-
27
- __slots__ = ("expression",)
28
-
29
- def __init__(self, token: Token, expression: Expression):
30
- super().__init__(token)
31
- self.expression = expression
32
- self.blank = False
33
-
34
- def __str__(self) -> str:
35
- return f"{{{{ {self.expression} }}}}"
36
-
37
- def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
38
- """Render the node to the output buffer."""
39
- return buffer.write(
40
- to_liquid_string(self.expression.evaluate(context), context.autoescape)
41
- )
42
-
43
- async def render_to_output_async(
44
- self, context: RenderContext, buffer: TextIO
45
- ) -> int:
46
- """Render the node to the output buffer."""
47
- return buffer.write(
48
- to_liquid_string(
49
- await self.expression.evaluate_async(context), context.autoescape
50
- )
51
- )
52
-
53
- def expressions(self) -> Iterable[Expression]:
54
- """Return this node's children."""
55
- yield self.expression
56
-
57
-
58
- class Output(Tag):
59
- """Pseudo "tag" to register output statements with the environment."""
60
-
61
- name = TOKEN_OUTPUT
62
- block = False
63
- node_class = OutputNode
64
-
65
- def parse(self, stream: TokenStream) -> OutputNode:
66
- """Parse tokens from _stream_ into an AST node."""
67
- token = stream.eat(TOKEN_OUTPUT)
68
- expr_token = stream.expect(TOKEN_EXPRESSION)
69
- return self.node_class(
70
- token,
71
- FilteredExpression.parse(
72
- self.env, TokenStream(tokenize(expr_token.value, expr_token))
73
- ),
74
- )
File without changes
@@ -1,75 +0,0 @@
1
- """The built-in _assign_ tag."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from typing import TYPE_CHECKING
7
- from typing import Iterable
8
- from typing import TextIO
9
-
10
- from liquid.ast import Node
11
- from liquid.builtin.expressions import FilteredExpression
12
- from liquid.builtin.expressions import Identifier
13
- from liquid.builtin.expressions import parse_identifier
14
- from liquid.tag import Tag
15
- from liquid.token import TOKEN_ASSIGN
16
- from liquid.token import TOKEN_TAG
17
- from liquid.token import Token
18
-
19
- if TYPE_CHECKING:
20
- from liquid.context import RenderContext
21
- from liquid.expression import Expression
22
- from liquid.stream import TokenStream
23
-
24
-
25
- TAG_ASSIGN = sys.intern("assign")
26
-
27
-
28
- class AssignNode(Node):
29
- """The built-in _assign_ tag."""
30
-
31
- __slots__ = ("name", "expression")
32
-
33
- def __init__(self, token: Token, *, name: Identifier, expression: Expression):
34
- super().__init__(token)
35
- self.name = name
36
- self.expression = expression
37
-
38
- def __str__(self) -> str:
39
- return f"{{% assign {self.name} = {self.expression} %}}"
40
-
41
- def render_to_output(self, context: RenderContext, _: TextIO) -> int:
42
- """Render the node to the output buffer."""
43
- context.assign(self.name, self.expression.evaluate(context))
44
- return 0
45
-
46
- async def render_to_output_async(self, context: RenderContext, _: TextIO) -> int:
47
- """Render the node to the output buffer."""
48
- context.assign(self.name, await self.expression.evaluate_async(context))
49
- return 0
50
-
51
- def expressions(self) -> Iterable[Expression]:
52
- """Return this node's expressions."""
53
- yield self.expression
54
-
55
- def template_scope(self) -> Iterable[Identifier]:
56
- """Return variables this node adds to the template local scope."""
57
- yield self.name
58
-
59
-
60
- class AssignTag(Tag):
61
- """The built-in _assign_ tag."""
62
-
63
- name = TAG_ASSIGN
64
- block = False
65
- node_class = AssignNode
66
-
67
- def parse(self, stream: TokenStream) -> AssignNode:
68
- """Parse tokens from _stream_ into an AST node."""
69
- token = stream.eat(TOKEN_TAG)
70
- tokens = stream.into_inner(tag=token, eat=False)
71
- name = parse_identifier(self.env, tokens, allow_trailing_question_mark=False)
72
- tokens.eat(TOKEN_ASSIGN)
73
- return self.node_class(
74
- token, name=name, expression=FilteredExpression.parse(self.env, tokens)
75
- )
@@ -1,98 +0,0 @@
1
- """Parse tree node and tag definition for the built-in "capture" tag."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from typing import TYPE_CHECKING
7
- from typing import Iterable
8
- from typing import TextIO
9
-
10
- from liquid import Markup
11
- from liquid.ast import BlockNode
12
- from liquid.ast import Node
13
- from liquid.builtin.expressions import parse_identifier
14
- from liquid.parser import get_parser
15
- from liquid.tag import Tag
16
- from liquid.token import TOKEN_EOF
17
- from liquid.token import TOKEN_TAG
18
- from liquid.token import Token
19
-
20
- if TYPE_CHECKING:
21
- from io import StringIO
22
-
23
- from liquid.builtin.expressions import Identifier
24
- from liquid.context import RenderContext
25
- from liquid.stream import TokenStream
26
-
27
-
28
- TAG_CAPTURE = sys.intern("capture")
29
- TAG_ENDCAPTURE = sys.intern("endcapture")
30
-
31
- ENDCAPTUREBLOCK = frozenset((TAG_ENDCAPTURE, TOKEN_EOF))
32
-
33
-
34
- class CaptureNode(Node):
35
- """Parse tree node for the built-in "capture" tag."""
36
-
37
- __slots__ = ("name", "block")
38
-
39
- def __init__(self, token: Token, name: Identifier, block: BlockNode):
40
- super().__init__(token)
41
- self.name = name
42
- self.block = block
43
-
44
- def __str__(self) -> str:
45
- return f"{{% capture {self.name} %}}{self.block}{{% endcapture %}}"
46
-
47
- def _assign(self, context: RenderContext, buf: StringIO) -> None:
48
- if context.autoescape:
49
- context.assign(self.name, Markup(buf.getvalue()))
50
- else:
51
- context.assign(self.name, buf.getvalue())
52
-
53
- def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
54
- """Render the node to the output buffer."""
55
- buf = context.get_buffer(buffer)
56
- self.block.render(context, buf)
57
- self._assign(context, buf)
58
- return False
59
-
60
- async def render_to_output_async(
61
- self, context: RenderContext, buffer: TextIO
62
- ) -> int:
63
- """Render the node to the output buffer."""
64
- buf = context.get_buffer(buffer)
65
- await self.block.render_async(context, buf)
66
- self._assign(context, buf)
67
- return False
68
-
69
- def children(
70
- self,
71
- static_context: RenderContext, # noqa: ARG002
72
- *,
73
- include_partials: bool = True, # noqa: ARG002
74
- ) -> Iterable[Node]:
75
- """Return this node's children."""
76
- yield self.block
77
-
78
- def template_scope(self) -> Iterable[Identifier]:
79
- """Return variables this node adds to the template local scope."""
80
- yield self.name
81
-
82
-
83
- class CaptureTag(Tag):
84
- """The built-in capture tag."""
85
-
86
- name = TAG_CAPTURE
87
- end = TAG_ENDCAPTURE
88
- node_class = CaptureNode
89
-
90
- def parse(self, stream: TokenStream) -> CaptureNode:
91
- """Parse tokens from _stream_ into an AST node."""
92
- token = stream.eat(TOKEN_TAG)
93
- tokens = stream.into_inner(tag=token)
94
- name = parse_identifier(self.env, tokens)
95
- tokens.expect_eos()
96
- block = get_parser(self.env).parse_block(stream, ENDCAPTUREBLOCK)
97
- stream.expect(TOKEN_TAG, value=TAG_ENDCAPTURE)
98
- return self.node_class(token, name, block)
@@ -1,287 +0,0 @@
1
- """The built-in _case_ tag."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from typing import TYPE_CHECKING
7
- from typing import Iterable
8
- from typing import TextIO
9
- from typing import Union
10
-
11
- from liquid.ast import BlockNode
12
- from liquid.ast import Node
13
- from liquid.builtin.expressions import parse_primitive
14
- from liquid.builtin.expressions.logical import _eq # type: ignore
15
- from liquid.exceptions import LiquidSyntaxError
16
- from liquid.expression import Expression
17
- from liquid.parser import get_parser
18
- from liquid.tag import Tag
19
- from liquid.token import TOKEN_COMMA
20
- from liquid.token import TOKEN_EOF
21
- from liquid.token import TOKEN_OR
22
- from liquid.token import TOKEN_TAG
23
- from liquid.token import Token
24
-
25
- if TYPE_CHECKING:
26
- from liquid import Environment
27
- from liquid import TokenStream
28
- from liquid.context import RenderContext
29
-
30
-
31
- TAG_CASE = sys.intern("case")
32
- TAG_ENDCASE = sys.intern("endcase")
33
- TAG_WHEN = sys.intern("when")
34
- TAG_ELSE = sys.intern("else")
35
-
36
- ENDWHENBLOCK = frozenset((TAG_ENDCASE, TAG_WHEN, TAG_ELSE, TOKEN_EOF))
37
- ENDCASEBLOCK = frozenset((TAG_ENDCASE, TAG_ELSE))
38
-
39
-
40
- class CaseNode(Node):
41
- """The built-in _case_ tag."""
42
-
43
- __slots__ = ("blocks", "expression")
44
-
45
- def __init__(
46
- self,
47
- token: Token,
48
- expression: Expression,
49
- blocks: list[Union[MultiExpressionBlockNode, BlockNode]],
50
- ):
51
- super().__init__(token)
52
- self.expression = expression
53
- self.blocks = blocks
54
-
55
- self.blank = all(node.blank for node in self.blocks)
56
-
57
- for node in self.blocks:
58
- node.blank = self.blank
59
- if isinstance(node, MultiExpressionBlockNode):
60
- node.block.blank = self.blank
61
-
62
- def __str__(self) -> str:
63
- blocks: list[str] = []
64
- for block in self.blocks:
65
- if isinstance(block, BlockNode):
66
- blocks.append(f"{{% else %}}{block}")
67
- else:
68
- blocks.append(str(block))
69
-
70
- blocks_ = "".join(blocks)
71
-
72
- return f"{{% case {self.expression} %}}\n{blocks_}{{% endcase %}}"
73
-
74
- def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
75
- """Render the node to the output buffer."""
76
- count = 0
77
- default = True
78
-
79
- for block in self.blocks:
80
- # Always render truthy `when` blocks, no matter the order.
81
- if isinstance(block, MultiExpressionBlockNode):
82
- count_ = block.render(context, buffer)
83
- if count_ >= 0:
84
- default = False
85
- count += count_
86
- # Only render `else` blocks if all preceding `when` blocks are falsy.
87
- # Multiple `else` blocks are OK.
88
- elif default:
89
- count += block.render(context, buffer)
90
-
91
- return count
92
-
93
- async def render_to_output_async(
94
- self, context: RenderContext, buffer: TextIO
95
- ) -> int:
96
- """Render the node to the output buffer."""
97
- count = 0
98
- default = True
99
-
100
- for block in self.blocks:
101
- # Always render truthy `when` blocks, no matter the order.
102
- if isinstance(block, MultiExpressionBlockNode):
103
- count_ = await block.render_async(context, buffer)
104
- if count_ >= 0:
105
- default = False
106
- count += count_
107
- # Only render `else` blocks if all preceding `when` blocks are falsy.
108
- # Multiple `else` blocks are OK.
109
- elif default:
110
- count += await block.render_async(context, buffer)
111
-
112
- return count
113
-
114
- def children(
115
- self,
116
- static_context: RenderContext, # noqa: ARG002
117
- *,
118
- include_partials: bool = True, # noqa: ARG002
119
- ) -> Iterable[Node]:
120
- """Return this node's children."""
121
- yield from self.blocks
122
-
123
- def expressions(self) -> Iterable[Expression]:
124
- """Return this node's expressions."""
125
- yield self.expression
126
-
127
-
128
- class CaseTag(Tag):
129
- """The built-in _case_ tag."""
130
-
131
- name = TAG_CASE
132
- end = TAG_ENDCASE
133
- node_class = CaseNode
134
-
135
- def __init__(self, env: Environment):
136
- super().__init__(env)
137
- self.parser = get_parser(self.env)
138
-
139
- def parse(self, stream: TokenStream) -> Node:
140
- """Parse tokens from _stream_ into an AST node."""
141
- token = stream.eat(TOKEN_TAG)
142
- tokens = stream.into_inner(tag=token)
143
- left = parse_primitive(self.env, tokens)
144
- tokens.expect_eos()
145
-
146
- # Eat whitespace or junk between `case` and when/else/endcase
147
- while (
148
- stream.current.kind not in (TOKEN_TAG, TOKEN_EOF)
149
- and stream.current.value not in ENDWHENBLOCK
150
- ):
151
- next(stream)
152
-
153
- parse_block = get_parser(self.env).parse_block
154
- blocks: list[Union[MultiExpressionBlockNode, BlockNode]] = []
155
-
156
- while not stream.current.is_tag(TAG_ENDCASE):
157
- if stream.current.is_tag(TAG_ELSE):
158
- next(stream)
159
- blocks.append(parse_block(stream, ENDWHENBLOCK))
160
- elif stream.current.is_tag(TAG_WHEN):
161
- alternative_token = next(stream)
162
- expressions = self._parse_when_expression(
163
- stream.into_inner(tag=alternative_token)
164
- )
165
- alternative_block = parse_block(stream, ENDWHENBLOCK)
166
-
167
- blocks.append(
168
- MultiExpressionBlockNode(
169
- alternative_token,
170
- alternative_block,
171
- _AnyExpression(alternative_token, left, expressions),
172
- )
173
- )
174
- else:
175
- raise LiquidSyntaxError(
176
- f"unexpected tag {stream.current.value!r}", token=stream.current
177
- )
178
-
179
- stream.expect(TOKEN_TAG, TAG_ENDCASE)
180
-
181
- return self.node_class(
182
- token,
183
- left,
184
- blocks,
185
- )
186
-
187
- def _parse_when_expression(self, stream: TokenStream) -> list[Expression]:
188
- expressions: list[Expression] = [parse_primitive(self.env, stream)]
189
- while stream.current.kind in (TOKEN_COMMA, TOKEN_OR):
190
- next(stream)
191
- try:
192
- expressions.append(parse_primitive(self.env, stream))
193
- except LiquidSyntaxError:
194
- # Use expressions we have so far an discard the rest.
195
- return expressions
196
-
197
- return expressions
198
-
199
-
200
- class _AnyExpression(Expression):
201
- __slots__ = (
202
- "left",
203
- "expressions",
204
- )
205
-
206
- def __init__(
207
- self, token: Token, left: Expression, expressions: list[Expression]
208
- ) -> None:
209
- super().__init__(token)
210
- self.left = left
211
- self.expressions = expressions
212
-
213
- def __str__(self) -> str:
214
- return ", ".join(str(expr) for expr in self.expressions)
215
-
216
- def evaluate(self, context: RenderContext) -> list[bool]:
217
- left = self.left.evaluate(context)
218
- return [_eq(left, right.evaluate(context)) for right in self.expressions]
219
-
220
- async def evaluate_async(self, context: RenderContext) -> list[bool]:
221
- left = await self.left.evaluate_async(context)
222
- return [
223
- _eq(left, await right.evaluate_async(context)) for right in self.expressions
224
- ]
225
-
226
- def children(self) -> list[Expression]:
227
- return self.expressions
228
-
229
-
230
- class MultiExpressionBlockNode(Node):
231
- """A node containing a sequence of nodes guarded by a choice of expressions."""
232
-
233
- __slots__ = ("block", "expression")
234
-
235
- def __init__(
236
- self,
237
- token: Token,
238
- block: BlockNode,
239
- expression: _AnyExpression,
240
- ) -> None:
241
- super().__init__(token)
242
- self.block = block
243
- self.expression = expression
244
- self.blank = self.block.blank
245
-
246
- def __str__(self) -> str:
247
- return f"{{% when {self.expression} %}}{self.block}"
248
-
249
- def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
250
- """Render the node to the output buffer."""
251
- matches = self.expression.evaluate(context)
252
-
253
- if any(matches):
254
- return sum(
255
- [self.block.render(context, buffer) for _ in range(matches.count(True))]
256
- )
257
-
258
- return -1
259
-
260
- async def render_to_output_async(
261
- self, context: RenderContext, buffer: TextIO
262
- ) -> int:
263
- """Render the node to the output buffer."""
264
- matches = await self.expression.evaluate_async(context)
265
-
266
- if any(matches):
267
- return sum(
268
- [
269
- await self.block.render_async(context, buffer)
270
- for _ in range(matches.count(True))
271
- ]
272
- )
273
-
274
- return -1
275
-
276
- def children(
277
- self,
278
- static_context: RenderContext, # noqa: ARG002
279
- *,
280
- include_partials: bool = True, # noqa: ARG002
281
- ) -> Iterable[Node]:
282
- """Return this node's children."""
283
- yield self.block
284
-
285
- def expressions(self) -> Iterable[Expression]:
286
- """Return this node's expressions."""
287
- yield self.expression