Support with, for, and as inline snippet syntax

This commit updates the render method to share parts
of the snippet and block rendering logic to enable
inline snippets to support `with`, `for`, and `as`
syntax
This commit is contained in:
Julia Boutin
2025-10-27 10:11:22 -06:00
parent a384e229d8
commit ba5aa0abf6
6 changed files with 130 additions and 122 deletions
+7 -10
View File
@@ -1,15 +1,9 @@
GIT
remote: https://github.com/Shopify/liquid-c.git
revision: 5a786af7284df55e013ea20551c4b688d02e8326
ref: main
specs:
liquid-c (4.2.0)
liquid (>= 5.0.1)
PATH PATH
remote: . remote: .
specs: specs:
liquid (5.6.0.alpha) liquid (5.8.7)
bigdecimal
strscan (>= 3.1.1)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
@@ -17,8 +11,10 @@ GEM
ast (2.4.2) ast (2.4.2)
base64 (0.2.0) base64 (0.2.0)
benchmark-ips (2.13.0) benchmark-ips (2.13.0)
bigdecimal (3.2.3)
json (2.7.2) json (2.7.2)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
lru_redux (1.1.0)
memory_profiler (1.0.1) memory_profiler (1.0.1)
minitest (5.22.3) minitest (5.22.3)
parallel (1.24.0) parallel (1.24.0)
@@ -50,6 +46,7 @@ GEM
rubocop (~> 1.44) rubocop (~> 1.44)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
stackprof (0.2.26) stackprof (0.2.26)
strscan (3.1.5)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
@@ -62,7 +59,7 @@ DEPENDENCIES
base64 base64
benchmark-ips benchmark-ips
liquid! liquid!
liquid-c! lru_redux
memory_profiler memory_profiler
minitest minitest
rake (~> 13.0) rake (~> 13.0)
+13 -4
View File
@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inline Snippets</title> <title>Inline Snippets</title>
<body> <body>
<div class="liquid" style="font-size: 56px;"> <div class="liquid" style="font-size: 56px;">
{% assign foo = true %} {% assign foo = true %}
{% assign link = "variable" %} {% assign linktext = "variable" %}
{% snippet main %} {% snippet main %}
{% assign foo = false %} {% assign foo = false %}
<p>Hi {{ arg | upcase }}!!!</p> <p>Hi {{ arg | upcase }}!!!</p>
@@ -18,7 +18,7 @@
<p>This is an inline snippet</p> <p>This is an inline snippet</p>
<ul> <ul>
<li><a href="/wow-a-link">wow a {{ link }}</a></li> <li><a href="/wow-a-link">wow a {{ linktext }}</a></li>
<li>1 + 1 = {{ 1 | plus: 1 }}</li> <li>1 + 1 = {{ 1 | plus: 1 }}</li>
<li>{% if true %}Yes!{% endif %}</li> <li>{% if true %}Yes!{% endif %}</li>
<li>foo = {% if foo %}true{%else%}false{% endif %}</li> <li>foo = {% if foo %}true{%else%}false{% endif %}</li>
@@ -29,6 +29,15 @@
{% render main, arg: 'lsf', ... %} {% render main, arg: 'lsf', ... %}
{{ foo }} {{ foo }}
{% snippet listitem %}
<li>{{ forloop.index }}: {{ emoji }}</li>
{% endsnippet %}
{% assign emojis = "🌼,🌳,🌸" | split: "," %}
<ul style="list-style-type: none; display: flex; gap: 1em;">
{%- render listitem for emojis as emoji -%}
</ul>
</div> </div>
</body> </body>
</html> </html>
+17 -29
View File
@@ -52,14 +52,24 @@ module Liquid
def render_tag(context, output) def render_tag(context, output)
template_name = @template_name_expr template_name = @template_name_expr
is_inline = template_name.is_a?(VariableLookup)
is_file = template_name.is_a?(String)
# For inline snippets, @template_name_expr is a VariableLookup if is_inline
if template_name.is_a?(VariableLookup) template_name = template_name.name
snippet_drop = context[template_name]
snippet_drop = context[template_name.name]
raise ::ArgumentError unless snippet_drop.is_a?(Liquid::SnippetDrop) raise ::ArgumentError unless snippet_drop.is_a?(Liquid::SnippetDrop)
partial = snippet_drop.body
else
raise ::ArgumentError unless is_file
partial = PartialCache.load(template_name, context: context, parse_context: parse_context)
end
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext inner_context = context.new_isolated_subcontext
if is_file if is_file
@@ -68,7 +78,6 @@ module Liquid
end end
if is_inline && inherit_context? if is_inline && inherit_context?
context.scopes.each do |scope| context.scopes.each do |scope|
scope.each do |key, value| scope.each do |key, value|
inner_context[key] = value inner_context[key] = value
@@ -80,30 +89,9 @@ module Liquid
inner_context[key] = context.evaluate(value) inner_context[key] = context.evaluate(value)
end end
return output << snippet_drop.body.render(inner_context)
end
# Otherwise, the expression should be a String literal, which parses to a String object
raise ::ArgumentError unless template_name.is_a?(String)
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil? inner_context[context_variable_name] = var unless var.nil?
inner_context['forloop'] = forloop if forloop
partial.render_to_output_buffer(inner_context, output) partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!) forloop&.send(:increment!)
} }
+3 -2
View File
@@ -18,8 +18,9 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX p = @parse_context.new_parser(markup)
@to = Regexp.last_match(1) if p.look(:id)
@to = p.consume(:id)
else else
raise SyntaxError, options[:locale].t("errors.syntax.snippet") raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end end
+3 -1
View File
@@ -102,7 +102,9 @@ class RenderTagTest < Minitest::Test
end end
def test_dynamically_choosen_templates_are_not_allowed def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}") assert_raises(::ArgumentError) do
Template.parse('{% assign name = "snippet" %}{% render name %}').render!
end
end end
def test_rigid_parsing_errors def test_rigid_parsing_errors
+87 -76
View File
@@ -319,31 +319,31 @@ class SnippetTest < Minitest::Test
end end
def test_render_inline_snippet_with_outside_context_rigid def test_render_inline_snippet_with_outside_context_rigid
template = <<~LIQUID.strip template = <<~LIQUID.strip
{% assign color_scheme = 'dark' %} {% assign color_scheme = 'dark' %}
{% snippet header %} {% snippet header %}
<div class="header header--{{ color_scheme }}"> <div class="header header--{{ color_scheme }}">
{{ message }} {{ message }}
</div> </div>
{% endsnippet %} {% endsnippet %}
{% render header, ..., message: 'Welcome!' %} {% render header, ..., message: 'Welcome!' %}
LIQUID LIQUID
expected = <<~OUTPUT expected = <<~OUTPUT
<div class="header header--dark"> <div class="header header--dark">
Welcome! Welcome!
</div> </div>
OUTPUT OUTPUT
assert_template_result(expected, template, error_mode: :rigid) assert_template_result(expected, template, error_mode: :rigid)
end end
def test_inline_snippet_local_scope_takes_precedence def test_inline_snippet_local_scope_takes_precedence
@@ -467,83 +467,94 @@ class SnippetTest < Minitest::Test
assert_template_result(expected, template) assert_template_result(expected, template)
end end
# def test_render_inline_snippet_inside_loop def test_render_inline_snippet_forloop
# template = <<~LIQUID.strip template = <<~LIQUID.strip
# {% assign color_scheme = 'dark' %} {% snippet item %}
# {% assign array = '1,2,3' | split: ',' %} <li>{{ forloop.index }}: {{ item }}</li>
{% endsnippet %}
# {% for i in array %} {% assign items = "A,B,C" | split: "," %}
# {% snippet header %} {%- render item for items -%}
# <div class="header header--{{ color_scheme }}"> LIQUID
# {{ message }} {{ i }} expected = <<~OUTPUT
# </div>
# {% endsnippet %}
# {% endfor %}
# {% render header, ..., message: '👉' %}
# LIQUID
# expected = <<~OUTPUT
# <div class="header header--dark">
# 👉 3
# </div>
# OUTPUT
# assert_template_result(expected, template) <li>1: A</li>
# end
# def test_render_inline_snippet_forloop <li>2: B</li>
# template = <<~LIQUID.strip
# {% snippet item %}
# <li>{{ forloop.index }}: {{ item }}</li>
# {% endsnippet %}
# {% assign items = "A,B,C" | split: "," %} <li>3: C</li>
# {%- render item for items -%} OUTPUT
# LIQUID
# expected = <<~OUTPUT
# <li>1: A</li> assert_template_result(expected, template)
end
# <li>2: B</li> def test_render_inline_snippet_with
template = <<~LIQUID.strip
{% snippet header %}
<div>{{ header }}</div>
{% endsnippet %}
# <li>3: C</li> {% assign product = "Apple" %}
# OUTPUT {%- render header with product -%}
LIQUID
expected = <<~OUTPUT
# assert_template_result(expected, template)
# end
# def test_render_inline_snippet_with
# template = <<~LIQUID.strip
# {% snippet header %}
# <div>{{ header }}</div>
# {% endsnippet %}
# {% assign product = "Apple" %} <div>Apple</div>
# {%- render header with product -%} OUTPUT
# LIQUID
# expected = <<~OUTPUT
# <div>Apple</div> assert_template_result(expected, template)
# OUTPUT end
# assert_template_result(expected, template) def test_render_inline_snippet_alias
# end template = <<~LIQUID.strip
{% snippet product_card %}
<div class="product">{{ item }}</div>
{% endsnippet %}
# def test_render_inline_snippet_alias {% assign featured = "Apple" %}
# template = <<~LIQUID.strip {%- render product_card with featured as item -%}
# {% snippet product_card %} LIQUID
# <div class="product">{{ item }}</div> expected = <<~OUTPUT
# {% endsnippet %}
# {% assign featured = "Apple" %}
# {%- render product_card with featured as item -%}
# LIQUID
# expected = <<~OUTPUT
# <div class="product">Apple</div>
# OUTPUT
# assert_template_result(expected, template) <div class="product">Apple</div>
# end OUTPUT
assert_template_result(expected, template)
end
def test_render_inline_snippet_inside_loop
template = <<~LIQUID.strip
{% assign color_scheme = 'dark' %}
{% assign array = '1,2,3' | split: ',' %}
{% for i in array %}
{% snippet header %}
<div class="header header--{{ color_scheme }}">
{{ message }} {{ i }}
</div>
{% endsnippet %}
{% endfor %}
{% render header, ..., message: '👉' %}
LIQUID
expected = <<~OUTPUT
<div class="header header--dark">
👉#{" "}
</div>
OUTPUT
assert_template_result(expected, template)
end
end end