Compare commits

...
Author SHA1 Message Date
Marco Concetto Rudilosso d29c8db8cf Adds parent template tracking to snippet drops
Enhances snippet drops to track their parent template name, enabling better error reporting and context awareness within rendered snippets.

Updates the render tag to use parent template names for proper context variable naming and improves error handling flow.
2025-10-29 15:53:57 +01:00
Julia Boutin b02ece20e7 Display more specific error when snippet cannot be found 2025-10-29 07:33:46 -06:00
Julia Boutin 99b0b38806 Raise error on invalid snippet name 2025-10-28 11:30:24 -06:00
Julia Boutin d109bc9242 Remove unneeded read method 2025-10-27 12:41:59 -06:00
Julia Boutin 47288ccca0 Add liquid_public_docs yard tag to snippet tag 2025-10-27 10:13:05 -06:00
Julia Boutin de23ed82d5 Extract snippet resource scoring logic into assign_score_of 2025-10-27 10:13:05 -06:00
Julia Boutin 1069b944a0 Allow render tag to recognize drops that respond to to_partial 2025-10-27 10:13:04 -06:00
Julia Boutin c6c342ab32 Remove ... syntax references 2025-10-27 10:13:04 -06:00
Julia Boutin 3556c3371e Raise syntax error on incorrect render identifier type 2025-10-27 10:13:04 -06:00
Julia Boutin 96a44927b5 Support prop spreading 2025-10-27 10:13:04 -06:00
Julia Boutin fb6cf17099 Implement resource limits and remove leftover string references 2025-10-27 10:12:43 -06:00
Julia Boutin 9409dd8f4a Render arguments should maintain correct precedence 2025-10-27 10:12:43 -06:00
Julia Boutin b5ecd4d0f8 Remove inline snippet specific example files 2025-10-27 10:11:22 -06:00
Julia Boutin ba5aa0abf6 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
2025-10-27 10:11:22 -06:00
Julia Boutin a384e229d8 Support ... inline snippet syntax 2025-10-27 10:11:22 -06:00
Julia Boutin db474d5b57 Change inline snippet identifier from string to variable
Currently, snippet files identified by strings. This
PR makes changes to render to allow for new inline
snippets to use variables as identifiers instead
2025-10-27 10:11:22 -06:00
Julia Boutin 3a13ac7e6c Create SnippetDrop and set in scope 2025-10-27 10:11:22 -06:00
Julia BoutinandOrlando Qiu 4d13f030f8 Update inline snippets syntax
Previously, inline snippets syntax looked a bit
different, they:

- used strings as tag identifiers
- defined tag arguments {% snippet "input" |type| %}

This PR updates snippets to better reflect
the currently proposed syntax

Co-authored-by: Orlando Qiu <[email protected]>
2025-10-27 10:11:21 -06:00
Josh FaiganandJulia Boutin 8c6149782d Introduce new inline snippets tag
Inline snippets will reduce code duplication and
improve the developer experience, eliminating the
need for one-off snippet files
2025-10-27 10:11:21 -06:00
8 changed files with 1152 additions and 15 deletions
+1
View File
@@ -67,6 +67,7 @@ require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/snippet_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
+3
View File
@@ -5,6 +5,7 @@
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
@@ -19,6 +20,7 @@
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
@@ -29,5 +31,6 @@
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
argument:
include: "Argument error in tag 'include' - Illegal template name"
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
disabled:
tag: "usage is not allowed in this context"
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Liquid
class SnippetDrop < Drop
attr_reader :body, :name, :parent_name
def initialize(body, name, parent_name)
super()
@body = body
@name = name
@parent_name = parent_name
end
def to_partial
@body
end
def to_s
'SnippetDrop'
end
end
end
+2
View File
@@ -20,6 +20,7 @@ require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
require_relative "tags/doc"
require_relative "tags/snippet"
module Liquid
module Tags
@@ -44,6 +45,7 @@ module Liquid
'echo' => Echo,
'tablerow' => TableRow,
'doc' => Doc,
'snippet' => Snippet,
}.freeze
end
end
+21 -13
View File
@@ -27,7 +27,7 @@ module Liquid
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
@@ -47,21 +47,25 @@ module Liquid
end
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
template = context.evaluate(@template_name_expr)
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
if template.respond_to?(:to_partial)
partial = template.to_partial
template_name = template.parent_name
context_variable_name = @alias_name || template.name.split('/').last
context_variable_name = @alias_name || template_name.split('/').last
elsif @template_name_expr.is_a?(String)
partial = PartialCache.load(template, context: context, parse_context: parse_context)
template_name = partial.name
context_variable_name = @alias_name || template_name.split('/').last
else
raise ::ArgumentError, parse_context.locale.t("errors.argument.render")
end
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name
inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@@ -101,14 +105,18 @@ module Liquid
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
p.consume?(:comma) # optional comma
end
p.consume(:end_of_string)
end
def rigid_template_name(p)
p.consume(:string)
return p.consume(:string) if p.look(:string)
return p.consume(:id) if p.look(:id)
found = p.consume || "nothing"
raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found)
end
def strict_parse(markup)
+45
View File
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name snippet
# @liquid_summary
# Creates a new inline snippet.
# @liquid_description
# You can create inline snippets to make your Liquid code more modular.
# @liquid_syntax
# {% snippet snippet_name %}
# value
# {% endsnippet %}
class Snippet < Block
def initialize(tag_name, markup, options)
super
p = @parse_context.new_parser(markup)
if p.look(:id)
@to = p.consume(:id)
p.consume(:end_of_string)
else
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end
end
def render_to_output_buffer(context, output)
snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
context.scopes.last[@to] = snippet_drop
context.resource_limits.increment_assign_score(assign_score_of(snippet_drop))
output
end
def blank?
true
end
private
def assign_score_of(snippet_drop)
snippet_drop.body.nodelist.sum { |node| node.to_s.bytesize }
end
end
end
+12 -2
View File
@@ -101,8 +101,11 @@ class RenderTagTest < Minitest::Test
end
end
def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
def test_dynamically_chosen_templates_are_not_allowed
error = assert_raises(::ArgumentError) do
Template.parse('{% assign name = "snippet" %}{% render name %}').render!
end
assert_equal("Argument error in tag 'render' - Dynamically chosen templates are not allowed", error.message)
end
def test_rigid_parsing_errors
@@ -294,6 +297,13 @@ class RenderTagTest < Minitest::Test
)
end
def test_render_tag_with_snippet_drop
assert_template_result(
"Hello from snippet",
"{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}",
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
File diff suppressed because it is too large Load Diff