Skip to content

Commit 173e7b7

Browse files
authored
Add Shopify compatible case tag. (#155)
* Add Shopify compatible case tag. * Use isinstance rather than a tagged union. * Empty `when` tags are always an error. * Add comments. This odd behavior is deliberate. * Test truthy and empty `when` block. * Update change log.
1 parent 215b2ea commit 173e7b7

File tree

6 files changed

+240
-13
lines changed

6 files changed

+240
-13
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Python Liquid Change Log
22

3+
## Version 1.12.2
4+
5+
**Fixes**
6+
7+
- Fixed `{% case %}` / `{% when %}` behavior. When using [`liquid.future.Environment`](https://jg-rp.github.io/liquid/api/future-environment), we now render any number of `{% else %}` blocks and allow `{% when %}` tags to appear after `{% else %}` tags. The default `Environment` continues to raise a `LiquidSyntaxError` in such cases.
8+
39
## Version 1.12.1
410

511
**Fixes**

liquid/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
from . import future
4848

49-
__version__ = "1.12.1"
49+
__version__ = "1.12.2"
5050

5151
__all__ = (
5252
"AwareBoundTemplate",

liquid/future/environment.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..environment import Environment as DefaultEnvironment
77
from ..template import FutureBoundTemplate
88
from .filters import split
9+
from .tags import LaxCaseTag
910
from .tags import LaxIfTag
1011
from .tags import LaxUnlessTag
1112

@@ -27,5 +28,6 @@ def setup_tags_and_filters(self) -> None:
2728
"""Add future tags and filters to this environment."""
2829
super().setup_tags_and_filters()
2930
self.add_filter("split", split)
31+
self.add_tag(LaxCaseTag)
3032
self.add_tag(LaxIfTag)
3133
self.add_tag(LaxUnlessTag)

liquid/future/tags/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from ._if_tag import LaxIfTag # noqa: D104
1+
from ._case_tag import LaxCaseTag # noqa: D104
2+
from ._if_tag import LaxIfTag
23
from ._unless_tag import LaxUnlessTag
34

45
__all__ = (
6+
"LaxCaseTag",
57
"LaxIfTag",
68
"LaxUnlessTag",
79
)

liquid/future/tags/_case_tag.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import List
5+
from typing import Optional
6+
from typing import TextIO
7+
from typing import Union
8+
9+
from liquid import ast
10+
from liquid.builtin.tags.case_tag import ENDWHENBLOCK
11+
from liquid.builtin.tags.case_tag import TAG_CASE
12+
from liquid.builtin.tags.case_tag import TAG_ELSE
13+
from liquid.builtin.tags.case_tag import TAG_ENDCASE
14+
from liquid.builtin.tags.case_tag import TAG_WHEN
15+
from liquid.builtin.tags.case_tag import CaseTag
16+
from liquid.exceptions import LiquidSyntaxError
17+
from liquid.expression import BooleanExpression
18+
from liquid.expression import InfixExpression
19+
from liquid.parse import expect
20+
from liquid.token import TOKEN_EXPRESSION
21+
from liquid.token import TOKEN_TAG
22+
from liquid.token import Token
23+
24+
if TYPE_CHECKING:
25+
from liquid.context import Context
26+
from liquid.stream import TokenStream
27+
28+
29+
class LaxCaseNode(ast.Node):
30+
"""Parse tree node for the lax "case" tag."""
31+
32+
__slots__ = ("tok", "blocks", "forced_output")
33+
34+
def __init__(
35+
self,
36+
tok: Token,
37+
blocks: List[Union[ast.BlockNode, ast.ConditionalBlockNode]],
38+
):
39+
self.tok = tok
40+
self.blocks = blocks
41+
42+
self.forced_output = self.force_output or any(
43+
b.forced_output for b in self.blocks
44+
)
45+
46+
def __str__(self) -> str:
47+
buf = (
48+
["if (False) { }"]
49+
if not self.blocks or isinstance(self.blocks[0], ast.BlockNode)
50+
else [f"if {self.blocks[0]}"]
51+
)
52+
53+
for block in self.blocks:
54+
if isinstance(block, ast.BlockNode):
55+
buf.append(f"else {block}")
56+
if isinstance(block, ast.ConditionalBlockNode):
57+
buf.append(f"elsif {block}")
58+
59+
return " ".join(buf)
60+
61+
def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
62+
buf = context.get_buffer(buffer)
63+
rendered: Optional[bool] = False
64+
65+
for block in self.blocks:
66+
# Always render truthy `when` blocks, no matter the order.
67+
if isinstance(block, ast.ConditionalBlockNode):
68+
rendered = block.render(context, buf) or rendered
69+
# Only render `else` blocks if all preceding `when` blocks are falsy.
70+
# Multiple `else` blocks are OK.
71+
elif isinstance(block, ast.BlockNode) and not rendered:
72+
block.render(context, buf)
73+
74+
val = buf.getvalue()
75+
if self.forced_output or not val.isspace():
76+
buffer.write(val)
77+
78+
return rendered
79+
80+
async def render_to_output_async(
81+
self, context: Context, buffer: TextIO
82+
) -> Optional[bool]:
83+
buf = context.get_buffer(buffer)
84+
rendered: Optional[bool] = False
85+
86+
for block in self.blocks:
87+
if isinstance(block, ast.ConditionalBlockNode):
88+
rendered = await block.render_async(context, buf) or rendered
89+
elif isinstance(block, ast.BlockNode) and not rendered:
90+
await block.render_async(context, buf)
91+
92+
val = buf.getvalue()
93+
if self.forced_output or not val.isspace():
94+
buffer.write(val)
95+
96+
return rendered
97+
98+
def children(self) -> List[ast.ChildNode]:
99+
_children = []
100+
101+
for block in self.blocks:
102+
if isinstance(block, ast.BlockNode):
103+
_children.append(
104+
ast.ChildNode(
105+
linenum=block.tok.linenum,
106+
node=block,
107+
expression=None,
108+
)
109+
)
110+
elif isinstance(block, ast.ConditionalBlockNode):
111+
_children.append(
112+
ast.ChildNode(
113+
linenum=block.tok.linenum,
114+
node=block,
115+
expression=block.condition,
116+
)
117+
)
118+
119+
return _children
120+
121+
122+
class LaxCaseTag(CaseTag):
123+
"""A `case` tag that is lax in its handling of extra `else` and `when` blocks."""
124+
125+
def parse(self, stream: TokenStream) -> ast.Node:
126+
expect(stream, TOKEN_TAG, value=TAG_CASE)
127+
tok = stream.current
128+
stream.next_token()
129+
130+
# Parse the case expression.
131+
expect(stream, TOKEN_EXPRESSION)
132+
case = self._parse_case_expression(stream.current.value, stream.current.linenum)
133+
stream.next_token()
134+
135+
# Eat whitespace or junk between `case` and when/else/endcase
136+
while (
137+
stream.current.type != TOKEN_TAG
138+
and stream.current.value not in ENDWHENBLOCK
139+
):
140+
stream.next_token()
141+
142+
# Collect all `when` and `else` tags regardless of te order n which they appear.
143+
blocks: List[Union[ast.BlockNode, ast.ConditionalBlockNode]] = []
144+
145+
while not stream.current.istag(TAG_ENDCASE):
146+
if stream.current.istag(TAG_ELSE):
147+
stream.next_token()
148+
blocks.append(self.parser.parse_block(stream, ENDWHENBLOCK))
149+
elif stream.current.istag(TAG_WHEN):
150+
when_tok = stream.next_token()
151+
expect(stream, TOKEN_EXPRESSION)
152+
153+
# Transform each comma or "or" separated when expression into a block
154+
# node of its own, just as if each expression had its own `when` tag.
155+
when_exprs = [
156+
BooleanExpression(InfixExpression(case, "==", expr))
157+
for expr in self._parse_when_expression(
158+
stream.current.value, stream.current.linenum
159+
)
160+
]
161+
162+
stream.next_token()
163+
when_block = self.parser.parse_block(stream, ENDWHENBLOCK)
164+
165+
# Reuse the same block.
166+
blocks.extend(
167+
ast.ConditionalBlockNode(
168+
tok=when_tok,
169+
condition=expr,
170+
block=when_block,
171+
)
172+
for expr in when_exprs
173+
)
174+
175+
else:
176+
raise LiquidSyntaxError(
177+
f"unexpected tag {stream.current.value}",
178+
linenum=stream.current.linenum,
179+
)
180+
181+
expect(stream, TOKEN_TAG, value=TAG_ENDCASE)
182+
return LaxCaseNode(tok, blocks=blocks)

liquid/golden/case_tag.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,6 @@
142142
expect="barbar",
143143
globals={"title": "Hello"},
144144
),
145-
Case(
146-
description="mix or and comma separated when expression",
147-
template=(
148-
r"{% case title %}"
149-
r"{% when 'foo' %}foo"
150-
r"{% when 'bar' or 'Hello', 'Hello' %}bar"
151-
r"{% endcase %}"
152-
),
153-
expect="barbar",
154-
globals={"title": "Hello"},
155-
),
156145
Case(
157146
description="unexpected when token",
158147
template=(
@@ -188,4 +177,50 @@
188177
expect="foo",
189178
globals={"x": ["a", "b", "c"], "y": ["a", "b", "c"]},
190179
),
180+
Case(
181+
description="multiple else blocks",
182+
template=(
183+
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar{% else %}baz{% endcase %}"
184+
),
185+
expect="barbaz",
186+
globals={},
187+
future=True,
188+
),
189+
Case(
190+
description="falsy when before and truthy when after else",
191+
template=(
192+
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar"
193+
r"{% when 'x' %}baz{% endcase %}"
194+
),
195+
expect="barbaz",
196+
globals={},
197+
future=True,
198+
),
199+
Case(
200+
description="falsy when before and truthy when after multiple else blocks",
201+
template=(
202+
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar"
203+
r"{% else %}baz{% when 'x' %}qux{% endcase %}"
204+
),
205+
expect="barbazqux",
206+
globals={},
207+
future=True,
208+
),
209+
Case(
210+
description="truthy when before and after else",
211+
template=(
212+
r"{% case 'x' %}{% when 'x' %}foo"
213+
r"{% else %}bar{% when 'x' %}baz{% endcase %}"
214+
),
215+
expect="foobaz",
216+
globals={},
217+
future=True,
218+
),
219+
Case(
220+
description="truthy and empty when block before else",
221+
template=(r"{% case 'x' %}{% when 'x' %}{% else %}bar{% endcase %}"),
222+
expect="",
223+
globals={},
224+
future=True,
225+
),
191226
]

0 commit comments

Comments
 (0)