Skip to content

Commit

Permalink
Add Shopify compatible case tag.
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Feb 19, 2024
1 parent 215b2ea commit 2b837ae
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 12 deletions.
2 changes: 2 additions & 0 deletions liquid/future/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..environment import Environment as DefaultEnvironment
from ..template import FutureBoundTemplate
from .filters import split
from .tags import LaxCaseTag
from .tags import LaxIfTag
from .tags import LaxUnlessTag

Expand All @@ -27,5 +28,6 @@ def setup_tags_and_filters(self) -> None:
"""Add future tags and filters to this environment."""
super().setup_tags_and_filters()
self.add_filter("split", split)
self.add_tag(LaxCaseTag)
self.add_tag(LaxIfTag)
self.add_tag(LaxUnlessTag)
4 changes: 3 additions & 1 deletion liquid/future/tags/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from ._if_tag import LaxIfTag # noqa: D104
from ._case_tag import LaxCaseTag # noqa: D104
from ._if_tag import LaxIfTag
from ._unless_tag import LaxUnlessTag

__all__ = (
"LaxCaseTag",
"LaxIfTag",
"LaxUnlessTag",
)
190 changes: 190 additions & 0 deletions liquid/future/tags/_case_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import List
from typing import Optional
from typing import TextIO
from typing import Union

from liquid import ast
from liquid.builtin.tags.case_tag import ENDWHENBLOCK
from liquid.builtin.tags.case_tag import TAG_CASE
from liquid.builtin.tags.case_tag import TAG_ELSE
from liquid.builtin.tags.case_tag import TAG_ENDCASE
from liquid.builtin.tags.case_tag import TAG_WHEN
from liquid.builtin.tags.case_tag import CaseTag
from liquid.exceptions import LiquidSyntaxError
from liquid.expression import BooleanExpression
from liquid.expression import InfixExpression
from liquid.parse import expect
from liquid.token import TOKEN_EXPRESSION
from liquid.token import TOKEN_TAG
from liquid.token import Token

if TYPE_CHECKING:
from liquid.context import Context
from liquid.stream import TokenStream


@dataclass
class _Block:
tag: str
node: Union[ast.BlockNode, ast.ConditionalBlockNode]


class LaxCaseNode(ast.Node):
"""Parse tree node for the lax "case" tag."""

__slots__ = ("tok", "blocks", "forced_output")

def __init__(
self,
tok: Token,
blocks: List[_Block],
):
self.tok = tok
self.blocks = blocks

self.forced_output = self.force_output or any(
b.node.forced_output for b in self.blocks
)

def __str__(self) -> str:
buf = (
["if (False) { }"]
if not self.blocks or self.blocks[0].tag == TAG_ELSE
else [f"if {self.blocks[0].node}"]
)

for block in self.blocks:
if block.tag == TAG_ELSE:
buf.append(f"else {block.node}")
elif block.tag == TAG_WHEN:
buf.append(f"elsif {block.node}")

return " ".join(buf)

def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]:
buf = context.get_buffer(buffer)
rendered: Optional[bool] = False

for block in self.blocks:
if block.tag == TAG_WHEN:
rendered = block.node.render(context, buf) or rendered
elif block.tag == TAG_ELSE and not rendered:
block.node.render(context, buf)

val = buf.getvalue()
if self.forced_output or not val.isspace():
buffer.write(val)

return rendered

async def render_to_output_async(
self, context: Context, buffer: TextIO
) -> Optional[bool]:
buf = context.get_buffer(buffer)
rendered: Optional[bool] = False

for block in self.blocks:
if block.tag == TAG_WHEN:
rendered = await block.node.render_async(context, buf) or rendered
elif block.tag == TAG_ELSE and not rendered:
await block.node.render_async(context, buf)

val = buf.getvalue()
if self.forced_output or not val.isspace():
buffer.write(val)

return rendered

def children(self) -> List[ast.ChildNode]:
_children = []

for block in self.blocks:
if isinstance(block.node, ast.BlockNode):
_children.append(
ast.ChildNode(
linenum=block.node.tok.linenum,
node=block.node,
expression=None,
)
)
elif isinstance(block.node, ast.ConditionalBlockNode):
_children.append(
ast.ChildNode(
linenum=block.node.tok.linenum,
node=block.node,
expression=block.node.condition,
)
)

return _children


class LaxCaseTag(CaseTag):
"""A `case` tag that is lax in its handling of extra `else` and `when` blocks."""

def parse(self, stream: TokenStream) -> ast.Node:
expect(stream, TOKEN_TAG, value=TAG_CASE)
tok = stream.current
stream.next_token()

# Parse the case expression.
expect(stream, TOKEN_EXPRESSION)
case = self._parse_case_expression(stream.current.value, stream.current.linenum)
stream.next_token()

# Eat whitespace or junk between `case` and when/else/endcase
while (
stream.current.type != TOKEN_TAG
and stream.current.value not in ENDWHENBLOCK
):
stream.next_token()

blocks: List[_Block] = []

while not stream.current.istag(TAG_ENDCASE):
if stream.current.istag(TAG_ELSE):
stream.next_token()
blocks.append(
_Block(
tag=TAG_ELSE,
node=self.parser.parse_block(stream, ENDWHENBLOCK),
)
)
elif stream.current.istag(TAG_WHEN):
when_tok = stream.next_token()
expect(stream, TOKEN_EXPRESSION) # XXX: empty when expressions?

when_exprs = [
BooleanExpression(InfixExpression(case, "==", expr))
for expr in self._parse_when_expression(
stream.current.value, stream.current.linenum
)
]

stream.next_token()
when_block = self.parser.parse_block(stream, ENDWHENBLOCK)

blocks.extend(
_Block(
tag=TAG_WHEN,
node=ast.ConditionalBlockNode(
tok=when_tok,
condition=expr,
block=when_block,
),
)
for expr in when_exprs
)

else:
raise LiquidSyntaxError(
f"unexpected tag {stream.current.value}",
linenum=stream.current.linenum,
)

expect(stream, TOKEN_TAG, value=TAG_ENDCASE)
return LaxCaseNode(tok, blocks=blocks)
50 changes: 39 additions & 11 deletions liquid/golden/case_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,6 @@
expect="barbar",
globals={"title": "Hello"},
),
Case(
description="mix or and comma separated when expression",
template=(
r"{% case title %}"
r"{% when 'foo' %}foo"
r"{% when 'bar' or 'Hello', 'Hello' %}bar"
r"{% endcase %}"
),
expect="barbar",
globals={"title": "Hello"},
),
Case(
description="unexpected when token",
template=(
Expand Down Expand Up @@ -188,4 +177,43 @@
expect="foo",
globals={"x": ["a", "b", "c"], "y": ["a", "b", "c"]},
),
Case(
description="multiple else blocks",
template=(
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar{% else %}baz{% endcase %}"
),
expect="barbaz",
globals={},
future=True,
),
Case(
description="falsy when before and truthy when after else",
template=(
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar"
r"{% when 'x' %}baz{% endcase %}"
),
expect="barbaz",
globals={},
future=True,
),
Case(
description="falsy when before and truthy when after multiple else blocks",
template=(
r"{% case 'x' %}{% when 'y' %}foo{% else %}bar"
r"{% else %}baz{% when 'x' %}qux{% endcase %}"
),
expect="barbazqux",
globals={},
future=True,
),
Case(
description="truthy when before and after else",
template=(
r"{% case 'x' %}{% when 'x' %}foo"
r"{% else %}bar{% when 'x' %}baz{% endcase %}"
),
expect="foobaz",
globals={},
future=True,
),
]

0 comments on commit 2b837ae

Please sign in to comment.