From b27e5e6d17beb7b3dc8689e4eeacdf8af49035eb Mon Sep 17 00:00:00 2001 From: James Prior Date: Mon, 19 Jan 2026 18:04:48 +0000 Subject: [PATCH 1/2] Call the `snippet` tag experimental --- CHANGES.md | 2 +- docs/experimental_tags.md | 80 +++++++++++++++++++ docs/tag_reference.md | 79 ------------------ liquid/builtin/__init__.py | 2 - liquid/builtin/tags/case_tag.py | 6 +- liquid/builtin/tags/if_tag.py | 2 +- liquid/extra/__init__.py | 3 +- liquid/extra/tags/__init__.py | 2 + .../snippet.py => extra/tags/snippet_tag.py} | 0 mkdocs.yml | 1 + tests/golden-liquid | 2 +- tests/test_compliance.py | 6 +- tests/test_static_analysis.py | 5 +- 13 files changed, 98 insertions(+), 92 deletions(-) create mode 100644 docs/experimental_tags.md rename liquid/{builtin/tags/snippet.py => extra/tags/snippet_tag.py} (100%) diff --git a/CHANGES.md b/CHANGES.md index 7df44aca..938946df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## Version 2.2.0 (unreleased) -- Added the `{% snippet %}` tag. +- Added an **experimental** `{% snippet %}` tag. Shopify/liquid released then quickly removed `{% snippet %}`. We're calling it "experimental" and keeping it disabled by default pending more activity from Shopify/liquid. - Improved static analysis of partial templates. Previously we would visit a partial template only once, regardless of how many times it is rendered with `{% render %}`. Now we visit partial templates once for each distinct set of arguments passed to `{% render %}`, potentially reporting "global" variables that we'd previously missed. ## Version 2.1.0 diff --git a/docs/experimental_tags.md b/docs/experimental_tags.md new file mode 100644 index 00000000..5b82d148 --- /dev/null +++ b/docs/experimental_tags.md @@ -0,0 +1,80 @@ +The tags described on this page are considered experimental and might change without warning and following semantic versioning. + +## snippet + +**_New in version 2.2.0_** + +```plain +{% snippet %} + +{% endsnippet %} +``` + +A snippet is a reusable block of Liquid markup. Traditionally we'd save a snippet to a file and include it in a template with the `{% render %}` tag. + +```liquid +{% render "some_snippet" %} +``` + +With the `{% snippet %}` tag we can define blocks for reuse inside a single template, potentially reducing the number of snippet files we need. + +```liquid +{% snippet div %} +
+ {{ content }} +
+{% endsnippet %} +``` + +Defining a snippet does not render it. We use `{% render snippet_name %}` to render a snippet, where `snippet_name` is the name of your snippet without quotes (file-based snippet names must be quoted). + +```liquid +{% snippet div %} +
+ {{ content }} +
+{% endsnippet %} + +{% render div, content: "Some content" %} +{% render div, content: "Other content" %} +``` + +```html title="output" +
Some content
+ +
Other content
+``` + +Inline snippets share the same namespace as variables defined with `{% assign %}` and `{% capture %}`, so be wary of accidentally overwriting snippets with non-snippet data. + +```liquid +{% snippet foo %}Hello{% endsnippet %} +{% foo = 42 %} +{% render foo %} {% # error %} +``` + +Snippets can be nested and follow the same scoping rules as file-based snippets. + +```liquid +{% snippet a %} + b + {% snippet c %} + d + {% endsnippet %} + {% render c %} +{% endsnippet %} + +{% render a %} +{% render c %} {% # error, c is out of scope %} +``` + +Snippet blocks are bound to their names late. You can conditionally define multiple snippets with the same name and pick one at render time. + +```liquid +{% if x %} + {% snippet a %}b{% endsnippet %} +{% else %} + {% snippet a %}c{% endsnippet %} +{% endif %} +{% render a %} +``` diff --git a/docs/tag_reference.md b/docs/tag_reference.md index 3aefd95a..852d30df 100644 --- a/docs/tag_reference.md +++ b/docs/tag_reference.md @@ -540,85 +540,6 @@ Additional keyword arguments given to the `render` tag will be added to the rend {% render "partial_template" greeting: "Hello", num: 3, skip: 2 %} ``` -## snippet - -**_New in version 2.2.0_** - -```plain -{% snippet %} - -{% endsnippet %} -``` - -A snippet is a reusable block of Liquid markup. Traditionally we'd save a snippet to a file and include it in a template with the `{% render %}` tag. - -```liquid -{% render "some_snippet" %} -``` - -With the `{% snippet %}` tag we can define blocks for reuse inside a single template, potentially reducing the number of snippet files we need. - -```liquid -{% snippet div %} -
- {{ content }} -
-{% endsnippet %} -``` - -Defining a snippet does not render it. We use `{% render snippet_name %}` to render a snippet, where `snippet_name` is the name of your snippet without quotes (file-based snippet names must be quoted). - -```liquid -{% snippet div %} -
- {{ content }} -
-{% endsnippet %} - -{% render div, content: "Some content" %} -{% render div, content: "Other content" %} -``` - -```html title="output" -
Some content
- -
Other content
-``` - -Inline snippets share the same namespace as variables defined with `{% assign %}` and `{% capture %}`, so be wary of accidentally overwriting snippets with non-snippet data. - -```liquid -{% snippet foo %}Hello{% endsnippet %} -{% foo = 42 %} -{% render foo %} {% # error %} -``` - -Snippets can be nested and follow the same scoping rules as file-based snippets. - -```liquid -{% snippet a %} - b - {% snippet c %} - d - {% endsnippet %} - {% render c %} -{% endsnippet %} - -{% render a %} -{% render c %} {% # error, c is out of scope %} -``` - -Snippet blocks are bound to their names late. You can conditionally define multiple snippets with the same name and pick one at render time. - -```liquid -{% if x %} - {% snippet a %}b{% endsnippet %} -{% else %} - {% snippet a %}c{% endsnippet %} -{% endif %} -{% render a %} -``` - ## tablerow ```plain diff --git a/liquid/builtin/__init__.py b/liquid/builtin/__init__.py index b68e3e3c..03206784 100644 --- a/liquid/builtin/__init__.py +++ b/liquid/builtin/__init__.py @@ -90,7 +90,6 @@ from .tags import inline_comment_tag from .tags import liquid_tag from .tags import render_tag -from .tags import snippet from .tags import tablerow_tag from .tags import unless_tag @@ -134,7 +133,6 @@ def register(env: Environment) -> None: # noqa: PLR0915 env.add_tag(ifchanged_tag.IfChangedTag) env.add_tag(inline_comment_tag.InlineCommentTag) env.add_tag(doc_tag.DocTag) - env.add_tag(snippet.SnippetTag) env.add_filter("abs", abs_) env.add_filter("at_most", at_most) diff --git a/liquid/builtin/tags/case_tag.py b/liquid/builtin/tags/case_tag.py index e262ebd4..7958bbc1 100644 --- a/liquid/builtin/tags/case_tag.py +++ b/liquid/builtin/tags/case_tag.py @@ -11,7 +11,7 @@ from liquid.ast import BlockNode from liquid.ast import Node from liquid.builtin.expressions import parse_primitive -from liquid.builtin.expressions.logical import _eq +from liquid.builtin.expressions.logical import _eq # type: ignore from liquid.exceptions import LiquidSyntaxError from liquid.expression import Expression from liquid.parser import get_parser @@ -85,7 +85,7 @@ def render_to_output(self, context: RenderContext, buffer: TextIO) -> int: count += count_ # Only render `else` blocks if all preceding `when` blocks are falsy. # Multiple `else` blocks are OK. - elif isinstance(block, BlockNode) and default: + elif default: count += block.render(context, buffer) return count @@ -106,7 +106,7 @@ async def render_to_output_async( count += count_ # Only render `else` blocks if all preceding `when` blocks are falsy. # Multiple `else` blocks are OK. - elif isinstance(block, BlockNode) and default: + elif default: count += await block.render_async(context, buffer) return count diff --git a/liquid/builtin/tags/if_tag.py b/liquid/builtin/tags/if_tag.py index 05e30dc8..4c4814e5 100644 --- a/liquid/builtin/tags/if_tag.py +++ b/liquid/builtin/tags/if_tag.py @@ -152,7 +152,7 @@ def parse(self, stream: TokenStream) -> Node: condition = BooleanExpression.parse(self.env, tokens) parse_block = get_parser(self.env).parse_block consequence = parse_block(stream, ENDIFBLOCK) - alternatives = [] + alternatives: list[ConditionalBlockNode] = [] while stream.current.is_tag(TAG_ELSIF): # If the expression can't be parsed, eat the "elsif" block and diff --git a/liquid/extra/__init__.py b/liquid/extra/__init__.py index 26849dd7..2827048f 100644 --- a/liquid/extra/__init__.py +++ b/liquid/extra/__init__.py @@ -21,6 +21,7 @@ from .tags import CallTag from .tags import ExtendsTag from .tags import MacroTag +from .tags import SnippetTag from .tags import TranslateTag from .tags import WithTag @@ -38,7 +39,6 @@ "DateTime", "ExtendsTag", "GetText", - "IfNotTag", "index", "JSON", "MacroTag", @@ -49,6 +49,7 @@ "script_tag", "sort_numeric", "stylesheet_tag", + "SnippetTag", "Translate", "Unit", "WithTag", diff --git a/liquid/extra/tags/__init__.py b/liquid/extra/tags/__init__.py index 24298eae..9daa6f36 100644 --- a/liquid/extra/tags/__init__.py +++ b/liquid/extra/tags/__init__.py @@ -3,6 +3,7 @@ from .extends_tag import ExtendsTag from .macro_tag import CallTag from .macro_tag import MacroTag +from .snippet_tag import SnippetTag from .translate_tag import TranslateTag __all__ = ( @@ -11,5 +12,6 @@ "ExtendsTag", "CallTag", "MacroTag", + "SnippetTag", "TranslateTag", ) diff --git a/liquid/builtin/tags/snippet.py b/liquid/extra/tags/snippet_tag.py similarity index 100% rename from liquid/builtin/tags/snippet.py rename to liquid/extra/tags/snippet_tag.py diff --git a/mkdocs.yml b/mkdocs.yml index e3424b0a..a5dec506 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,7 @@ nav: - Tag reference: - Default tags: "tag_reference.md" - Extra tags: "optional_tags.md" + - Experimental tags: "experimental_tags.md" - Filter reference: - Default filters: "filter_reference.md" - Extra filters: "optional_filters.md" diff --git a/tests/golden-liquid b/tests/golden-liquid index 68da2e73..b6386e7a 160000 --- a/tests/golden-liquid +++ b/tests/golden-liquid @@ -1 +1 @@ -Subproject commit 68da2e73f2393fa7dd596e9a99b564365f315b2e +Subproject commit b6386e7adf964517546fec6564ef36e12c4b498e diff --git a/tests/test_compliance.py b/tests/test_compliance.py index 23bc0308..1a08f5cd 100644 --- a/tests/test_compliance.py +++ b/tests/test_compliance.py @@ -20,19 +20,19 @@ class Case: name: str template: str - data: dict[str, Any] = field(default_factory=dict) + data: dict[str, Any] = field(default_factory=dict) # type: ignore templates: Optional[dict[str, str]] = None result: Optional[str] = None results: Optional[list[str]] = None invalid: Optional[bool] = None - tags: list[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) # type: ignore FILENAME = "tests/golden-liquid/golden_liquid.json" SKIP = { "filters, has, array of ints, default value": "Ruby behavioral quirk", - "tags, case, unexpected when token, rigid": "TODO", + "tags, case, unexpected when token, strict2": "TODO", } diff --git a/tests/test_static_analysis.py b/tests/test_static_analysis.py index bc93b002..3c1a9a38 100644 --- a/tests/test_static_analysis.py +++ b/tests/test_static_analysis.py @@ -11,6 +11,7 @@ from liquid import Environment from liquid.builtin import DictLoader from liquid.exceptions import TemplateNotFoundError +from liquid.extra import SnippetTag from liquid.span import Span from liquid.static_analysis import Variable @@ -25,7 +26,9 @@ class MockEnv(Environment): @pytest.fixture def env() -> Environment: # noqa: D103 - return MockEnv(extra=True) + _env = MockEnv(extra=True) + _env.add_tag(SnippetTag) # Experimental tag + return _env def _assert( From f5da3f985803e2e7b4d091a473ae103079a70912 Mon Sep 17 00:00:00 2001 From: James Prior Date: Mon, 19 Jan 2026 18:10:05 +0000 Subject: [PATCH 2/2] Fix docs typo --- docs/experimental_tags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/experimental_tags.md b/docs/experimental_tags.md index 5b82d148..b55628c8 100644 --- a/docs/experimental_tags.md +++ b/docs/experimental_tags.md @@ -1,4 +1,4 @@ -The tags described on this page are considered experimental and might change without warning and following semantic versioning. +The tags described on this page are considered experimental and might change without warning and not follow semantic versioning. ## snippet