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-30 12:00:44 +01:00
committed by Guilherme Carreiro
parent 9bcfd32e65
commit 99116638fd
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
remote: .
specs:
liquid (5.6.0.alpha)
liquid (5.8.7)
bigdecimal
strscan (>= 3.1.1)
GEM
remote: https://rubygems.org/
@@ -17,8 +11,10 @@ GEM
ast (2.4.2)
base64 (0.2.0)
benchmark-ips (2.13.0)
bigdecimal (3.2.3)
json (2.7.2)
language_server-protocol (3.17.0.3)
lru_redux (1.1.0)
memory_profiler (1.0.1)
minitest (5.22.3)
parallel (1.24.0)
@@ -50,6 +46,7 @@ GEM
rubocop (~> 1.44)
ruby-progressbar (1.13.0)
stackprof (0.2.26)
strscan (3.1.5)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.5.0)
@@ -62,7 +59,7 @@ DEPENDENCIES
base64
benchmark-ips
liquid!
liquid-c!
lru_redux
memory_profiler
minitest
rake (~> 13.0)
+13 -4
View File
@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inline Snippets</title>
<body>
<div class="liquid" style="font-size: 56px;">
{% assign foo = true %}
{% assign link = "variable" %}
{% assign linktext = "variable" %}
{% snippet main %}
{% assign foo = false %}
<p>Hi {{ arg | upcase }}!!!</p>
@@ -18,7 +18,7 @@
<p>This is an inline snippet</p>
<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>{% if true %}Yes!{% endif %}</li>
<li>foo = {% if foo %}true{%else%}false{% endif %}</li>
@@ -29,6 +29,15 @@
{% render main, arg: 'lsf', ... %}
{{ 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>
</body>
</html>
+17 -29
View File
@@ -52,14 +52,24 @@ module Liquid
def render_tag(context, output)
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 template_name.is_a?(VariableLookup)
snippet_drop = context[template_name.name]
if is_inline
template_name = template_name.name
snippet_drop = context[template_name]
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
if is_file
@@ -68,7 +78,6 @@ module Liquid
end
if is_inline && inherit_context?
context.scopes.each do |scope|
scope.each do |key, value|
inner_context[key] = value
@@ -80,30 +89,9 @@ module Liquid
inner_context[key] = context.evaluate(value)
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['forloop'] = forloop if forloop
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
+3 -2
View File
@@ -18,8 +18,9 @@ module Liquid
def initialize(tag_name, markup, options)
super
if markup =~ SYNTAX
@to = Regexp.last_match(1)
p = @parse_context.new_parser(markup)
if p.look(:id)
@to = p.consume(:id)
else
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end
+3 -1
View File
@@ -102,7 +102,9 @@ class RenderTagTest < Minitest::Test
end
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
def test_rigid_parsing_errors
+87 -76
View File
@@ -319,31 +319,31 @@ class SnippetTest < Minitest::Test
end
def test_render_inline_snippet_with_outside_context_rigid
template = <<~LIQUID.strip
{% assign color_scheme = 'dark' %}
template = <<~LIQUID.strip
{% assign color_scheme = 'dark' %}
{% snippet header %}
<div class="header header--{{ color_scheme }}">
{{ message }}
</div>
{% endsnippet %}
{% snippet header %}
<div class="header header--{{ color_scheme }}">
{{ message }}
</div>
{% endsnippet %}
{% render header, ..., message: 'Welcome!' %}
LIQUID
expected = <<~OUTPUT
{% render header, ..., message: 'Welcome!' %}
LIQUID
expected = <<~OUTPUT
<div class="header header--dark">
Welcome!
</div>
OUTPUT
<div class="header header--dark">
Welcome!
</div>
OUTPUT
assert_template_result(expected, template, error_mode: :rigid)
assert_template_result(expected, template, error_mode: :rigid)
end
def test_inline_snippet_local_scope_takes_precedence
@@ -467,83 +467,94 @@ class SnippetTest < Minitest::Test
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: ',' %}
def test_render_inline_snippet_forloop
template = <<~LIQUID.strip
{% snippet item %}
<li>{{ forloop.index }}: {{ item }}</li>
{% endsnippet %}
# {% for i in array %}
# {% snippet header %}
# <div class="header header--{{ color_scheme }}">
# {{ message }} {{ i }}
# </div>
# {% endsnippet %}
# {% endfor %}
{% assign items = "A,B,C" | split: "," %}
{%- render item for items -%}
LIQUID
expected = <<~OUTPUT
# {% render header, ..., message: '👉' %}
# LIQUID
# expected = <<~OUTPUT
# <div class="header header--dark">
# 👉 3
# </div>
# OUTPUT
# assert_template_result(expected, template)
# end
<li>1: A</li>
# def test_render_inline_snippet_forloop
# template = <<~LIQUID.strip
# {% snippet item %}
# <li>{{ forloop.index }}: {{ item }}</li>
# {% endsnippet %}
<li>2: B</li>
# {% assign items = "A,B,C" | split: "," %}
# {%- render item for items -%}
# LIQUID
# expected = <<~OUTPUT
<li>3: C</li>
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>
# OUTPUT
{% assign product = "Apple" %}
{%- 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" %}
# {%- render header with product -%}
# LIQUID
# expected = <<~OUTPUT
<div>Apple</div>
OUTPUT
# <div>Apple</div>
# OUTPUT
assert_template_result(expected, template)
end
# assert_template_result(expected, template)
# end
def test_render_inline_snippet_alias
template = <<~LIQUID.strip
{% snippet product_card %}
<div class="product">{{ item }}</div>
{% endsnippet %}
# def test_render_inline_snippet_alias
# template = <<~LIQUID.strip
# {% snippet product_card %}
# <div class="product">{{ item }}</div>
# {% endsnippet %}
{% assign featured = "Apple" %}
{%- render product_card with featured as item -%}
LIQUID
expected = <<~OUTPUT
# {% assign featured = "Apple" %}
# {%- render product_card with featured as item -%}
# LIQUID
# expected = <<~OUTPUT
# <div class="product">Apple</div>
# OUTPUT
# assert_template_result(expected, template)
# end
<div class="product">Apple</div>
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