Compare commits

...
Author SHA1 Message Date
Marco Concetto Rudilosso c4902e792a Add deferred partial rendering with Declarative Partial Updates
Introduce an `after` modifier for the render tag using Google's
proposed Declarative Partial Updates format. Initial rendering emits
processing-instruction markers, queues isolated partial renders, and
flushes `<template for=...>` replacement patches from the context.
2026-05-25 12:34:10 +02:00
James MengandGitHub c6f05eaf83 Merge pull request #1955 from Shopify/jm/trim_doc_tag_desc
Remove {{ foo }} and {{ bar }} from `doc` tag description
2025-05-06 15:29:29 -07:00
James Meng eabbd5cb6f Remove {{ foo }} and {{ bar }} from doc tag description 2025-05-06 15:20:35 -07:00
Marco Concetto RudilossoandGitHub ea864f1177 Merge pull request #1951 from Shopify/actual-revert
Fully revert calling to_s on filter array
2025-04-14 17:49:47 +02:00
Marco Concetto Rudilosso c34dd812c5 Fully revert calling to_s on filter array 2025-04-14 17:36:45 +02:00
Marco Concetto RudilossoandGitHub cc04892e54 Merge pull request #1943 from Shopify/revert-to-s
Revert `Utils.to_s` on all array filters
2025-04-14 17:22:10 +02:00
Marco Concetto Rudilosso f676e699a4 rollback changes to tests 2025-04-14 11:24:01 +02:00
Marco Concetto Rudilosso fc0f7f44c1 use Utils.to_s on property error 2025-04-14 11:17:59 +02:00
Marco Concetto Rudilosso b459eb656f remove test 2025-04-14 11:17:46 +02:00
Marco Concetto Rudilosso c6dcf3e714 add test 2025-04-14 11:17:46 +02:00
Marco Concetto Rudilosso 4dae678c63 Revert "Stringify properties before filtering (#1929)"
This reverts commit f5d6a36574.
2025-04-14 11:17:46 +02:00
Marco Concetto Rudilosso 4c07ff920b Revert "Always stringify properties in all array filters (#1936)"
This reverts commit aa1640035f.
2025-04-14 11:17:46 +02:00
Ian Ker-SeymerandGitHub dbe709c3bf Use to_liquid_value in uniq filter (#1948)
* Use to_liquid_value in uniq filter

* Bump version to 5.8.4
2025-04-09 15:00:01 -04:00
2b75bfaff4 Fix regression when using empty/nil properties with array filters (#1944)
* Make all array filters that use `filter_array` util process empty string and nil correctly

* up version

* fix ordering of checks

* also do it for map

* Do not raise property error

* Gracefully empty property in map filter

---------

Co-authored-by: Marco Concetto Rudilosso <[email protected]>
2025-04-04 11:27:29 -04:00
James MengandGitHub 87bc6e7cfa Merge pull request #1940 from Shopify/jm/update_snippet_url_links
Update snippet URLs in documentation links to point to newly created snippet page
2025-04-01 10:58:54 -07:00
James Meng 7f122aeed2 Fix snippet URLs in documentation links 2025-03-31 17:08:31 -07:00
12 changed files with 422 additions and 45 deletions
+27
View File
@@ -64,6 +64,33 @@ module Liquid
@warnings ||= []
end
def after_render_jobs
@registers.static[:after_render_jobs] ||= []
end
def next_after_render_id
@registers.static[:after_render_sequence] ||= 0
@registers.static[:after_render_sequence] += 1
"liquid-after-#{@registers.static[:after_render_sequence]}"
end
def enqueue_after_render(job)
after_render_jobs << job
end
def render_after_tags_to_output_buffer(output)
while (job = after_render_jobs.shift)
output << %(<template for="#{job[:id]}">)
job[:renderer].call(output)
output << %(</template>)
end
output
end
def render_after_tags
render_after_tags_to_output_buffer(+'')
end
def strainer
@strainer ||= @environment.create_strainer(self, @filters)
end
+6 -13
View File
@@ -3,7 +3,6 @@
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_I32 = (1 << 31) - 1
@@ -387,7 +386,6 @@ module Liquid
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
property = Utils.to_s(property)
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
@@ -417,7 +415,6 @@ module Liquid
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
property = Utils.to_s(property)
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
@@ -505,7 +502,6 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
property = Utils.to_s(property)
ary.uniq do |item|
item[property]
rescue TypeError
@@ -537,7 +533,6 @@ module Liquid
# @liquid_syntax array | map: string
# @liquid_return [array[untyped]]
def map(input, property)
property = Utils.to_s(property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
@@ -567,7 +562,6 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
else
property = Liquid::Utils.to_s(property)
ary.reject do |item|
item[property].nil?
rescue TypeError
@@ -957,8 +951,6 @@ module Liquid
# @liquid_syntax array | sum
# @liquid_return [number]
def sum(input, property = nil)
property = property.nil? ? nil : Utils.to_s(property)
ary = InputIterator.new(input, context)
return 0 if ary.empty?
@@ -986,10 +978,8 @@ module Liquid
attr_reader :context
def filter_array(input, property, target_value, default_value = [], &block)
property = Liquid::Utils.to_s(property)
return default_value if property.empty?
ary = InputIterator.new(input, context)
return default_value if ary.empty?
block.call(ary) do |item|
@@ -1007,7 +997,7 @@ module Liquid
end
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
raise Liquid::ArgumentError, "cannot select the property '#{Utils.to_s(property)}'"
end
def apply_operation(input, operand, operation)
@@ -1079,7 +1069,10 @@ module Liquid
end
def uniq(&block)
to_a.uniq(&block)
to_a.uniq do |item|
item = Utils.to_liquid_value(item)
block ? yield(item) : item
end
end
def compact
+1 -1
View File
@@ -10,7 +10,7 @@ module Liquid
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# [snippets](/themes/architecture/snippets) included in the file.
#
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
-1
View File
@@ -27,7 +27,6 @@ module Liquid
# @example
# {% render 'message', foo: 'Hello', bar: 'World' %}
# {% enddoc %}
# {{ foo }}, {{ bar }}!
class Doc < Block
NO_UNEXPECTED_ARGS = /\A\s*\z/
+1 -1
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category theme
# @liquid_name include
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets).
# Renders a [snippet](/themes/architecture/snippets).
# @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
# snippet.
+1 -1
View File
@@ -10,7 +10,7 @@ module Liquid
# @liquid_description
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# [snippets](/themes/architecture/snippets) included in the file.
#
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
+38 -12
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# Renders a [snippet](/themes/architecture/snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
@@ -27,7 +27,11 @@ 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
AFTER = 'after'
AFTER_MARKUP = /\s+#{AFTER}(?=\s|,|\z)/o
WITH_OR_FOR_MARKUP = /\s+(with|#{FOR})\s+(#{QuotedFragment}+)/o
ALIAS_MARKUP = /\s+(?:as)\s+(#{VariableSegment}+)/o
SYNTAX = /(#{QuotedString}+)(#{AFTER_MARKUP})?(#{WITH_OR_FOR_MARKUP})?(#{ALIAS_MARKUP})?/o
disable_tags "include"
@@ -39,10 +43,11 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@after = !!Regexp.last_match(2)
with_or_for = Regexp.last_match(4)
variable_name = Regexp.last_match(5)
@alias_name = Regexp.last_match(6)
@alias_name = Regexp.last_match(7)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@@ -61,6 +66,10 @@ module Liquid
render_tag(context, output)
end
def after?
@after
end
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
@@ -74,26 +83,43 @@ module Liquid
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
evaluated_attributes = @attributes.transform_values { |value| context.evaluate(value) }
render_partial_func = ->(var, forloop, render_output) {
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)
evaluated_attributes.each do |key, value|
inner_context[key] = value
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
partial.render_to_output_buffer(inner_context, render_output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
if @after
id = context.next_after_render_id
context.enqueue_after_render(
id: id,
renderer: ->(after_output) {
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop, after_output) }
else
render_partial_func.call(variable, nil, after_output)
end
}
)
output << %(<?marker name="#{id}">)
elsif @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
variable.each { |var| render_partial_func.call(var, forloop, output) }
else
render_partial_func.call(variable, nil)
render_partial_func.call(variable, nil, output)
end
output
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.8.2"
VERSION = "5.8.6"
end
+208
View File
@@ -0,0 +1,208 @@
# Proposal: Deferred `render` with `after`
## Summary
Add an optional `after` modifier to Liquid's `{% render %}` tag:
```liquid
{% render 'product-card' after, product: product %}
```
When `after` is present, Liquid does not render the partial inline. Instead, it emits a stable HTML placeholder marker into the output and records enough information on the current `Liquid::Context` to render the partial later. A new context API then renders all deferred partials, ideally as a stream of out-of-order HTML replacement patches.
This is inspired by Chrome's Declarative Partial Updates proposal: https://developer.chrome.com/blog/declarative-partial-updates. The browser-side idea is to let HTML declare patch targets and stream their replacement content later, enabling the initial shell to be sent quickly while slower islands arrive when ready.
For Liquid, the equivalent is server-side syntax for declaring that a snippet can be delayed without changing template structure.
## Motivation
Liquid templates often have a mix of cheap layout work and expensive isolated snippets. Today, an expensive snippet blocks all subsequent output because `{% render %}` is synchronous and inline.
`render after` would allow templates to produce the main document quickly, reserve exact DOM locations for deferred snippets, and render those snippets later using the same Liquid render semantics.
Example use cases:
- Product recommendations below the fold.
- Expensive merchandising or personalization blocks.
- Analytics or SEO metadata fragments that can be patched into known locations.
- App blocks where the outer page shell should not wait on the block.
## Goals
- Add a small, Liquid-native API for deferring isolated snippet rendering.
- Preserve existing `{% render %}` isolation semantics.
- Emit processing-instruction placeholders that can be targeted by a later replacement patch.
- Store deferred render work in `Liquid::Context`.
- Add a context method to flush/enumerate/render deferred work.
- Keep the first prototype simple and non-streaming, while shaping the API so true streaming can be added later.
## Non-goals
- Implement browser support for Declarative Partial Updates.
- Require JavaScript for the Liquid-side primitive.
- Make arbitrary tags asynchronous.
- Allow deferred snippets to mutate the parent scope after the placeholder is emitted.
- Solve scheduling, prioritization, cancellation, or parallel execution in the first prototype.
## Syntax
The proposed syntax is:
```liquid
{% render 'snippet' after %}
{% render 'snippet' after, product: product %}
{% render 'snippet' after with product as item %}
{% render 'snippet' after for products as product %}
```
`after` is a render modifier with no value. It is intentionally boolean and reserved in this position.
The prototype supports bare `after` immediately after the rendered template name, because it reads like a render modifier rather than data passed into the snippet. `after: value` remains a normal named argument passed to the snippet.
## Output shape
When a deferred render is encountered, Liquid emits a Chrome-style processing-instruction placeholder marker with a unique id:
```html
<?marker name="liquid-after-1">
```
Later, flushing the deferred renders produces replacement patches. The target shape should be compatible with the direction of Declarative Partial Updates. For example:
```html
<template for="liquid-after-1">
...rendered snippet HTML...
</template>
```
The exact patch attribute names should track the platform proposal as it evolves. Until browser APIs stabilize, Liquid can expose a server-side patch format behind a small formatter object.
For the prototype, the replacement payload is a concatenated HTML patch string:
```ruby
context.render_after_tags
# => "<template for=...>...</template>"
```
## Semantics
### Evaluation timing
When `{% render 'snippet' after ... %}` is encountered:
1. Liquid evaluates the snippet name expression.
2. Liquid evaluates the `with` / `for` expression, if present.
3. Liquid evaluates all named render arguments.
4. Liquid records a deferred render job containing the evaluated values and render metadata.
5. Liquid emits a placeholder marker.
This means deferred renders capture values at enqueue time, not flush time. That avoids surprising behavior when variables change later in the template.
### Isolation
Deferred render jobs should use the same isolation semantics as normal `{% render %}`:
- The snippet receives only explicitly-passed variables plus globals/environments available to render today.
- Variables assigned inside the snippet do not leak into the parent template.
- The `include` tag remains disabled inside rendered snippets.
### Ordering
The queue is FIFO by default. Placeholder ids are monotonically increasing per context render:
```html
<?marker name="liquid-after-1">
<?marker name="liquid-after-2">
```
The streaming API may later render jobs as they become ready, but the prototype can preserve source order.
### Error handling
Deferred renders should use Liquid's existing error handling through `Context#handle_error` and `exception_renderer`.
Open question: if an error occurs while flushing deferred renders after the main template was already sent, should the replacement patch contain the rendered error string, an empty patch, or an out-of-band error? The prototype should match inline render behavior and place the rendered error into the patch body.
## Proposed API
Add queue APIs to `Liquid::Context`:
```ruby
context.enqueue_after_render(job) # internal
context.after_render_jobs # inspection/testing
context.render_after_tags # prototype: returns a string of patches
context.render_after_tags_to_output_buffer(output) # streaming-ready shape
```
Possible streaming-oriented API:
```ruby
context.each_after_render_patch do |patch|
response.write(patch)
end
```
or:
```ruby
context.render_after_tags_to_output_buffer(response_stream)
```
The first implementation may buffer each snippet internally. The API should still write to an output object so callers can later stream each completed patch without changing template code.
## Example
Template:
```liquid
<h1>{{ product.title }}</h1>
{% render 'price', product: product %}
<section>
{% render 'recommendations' after, product: product %}
</section>
```
Initial output:
```html
<h1>Snowboard</h1>
<span>$699.00</span>
<section>
<?marker name="liquid-after-1">
</section>
```
Deferred patch output:
```html
<template for="liquid-after-1">
<ul class="recommendations">...</ul>
</template>
```
A Rack-like integration could do:
```ruby
context = Liquid::Context.build(...)
body = template.render!(context)
response.write(body)
context.render_after_tags_to_output_buffer(response)
```
The prototype can buffer `body` first. A production integration would stream `body` immediately, then stream each deferred patch as soon as it completes.
## Compatibility
Existing templates are unaffected unless they use bare `after` immediately after the rendered template name.
Because bare `after` becomes reserved syntax for the render tag in that position, this could conflict with unusual templates that currently rely on that token being ignored. Snippets currently receiving an `after:` keyword argument continue to work:
```liquid
{% render 'divider', after: 'label' %}
```
This proposal only reserves bare `after`; `after: value` continues to be passed as a normal snippet attribute. That minimizes compatibility risk.
+46 -15
View File
@@ -560,12 +560,23 @@ class StandardFiltersTest < Minitest::Test
end
end
def test_map_returns_empty_with_no_property
def test_map_with_value_property
array = [
{ "handle" => "alpha", "value" => "A" },
{ "handle" => "beta", "value" => "B" },
{ "handle" => "gamma", "value" => "C" }
]
assert_template_result("A B C", "{{ array | map: 'value' | join: ' ' }}", { "array" => array })
end
def test_map_returns_input_with_no_property
foo = [
[1],
[2],
[3],
]
assert_raises(Liquid::ArgumentError) do
@filters.map(foo, nil)
end
@@ -1033,6 +1044,23 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_empty_string_is_a_no_op
environment = { "array" => ["alpha", "beta", "gamma"] }
expected_output = "alpha beta gamma"
template = "{{ array | where: '' | join: ' ' }}"
assert_template_result(expected_output, template, environment)
end
def test_where_with_nil_is_a_no_op
environment = { "array" => ["alpha", "beta", "gamma"] }
template = "{{ array | where: nil | join: ' ' }}"
assert_raises(Liquid::ArgumentError) do
assert_template_result("alpha beta gamma", template, environment)
end
end
def test_where_with_value
array = [
{ "handle" => "alpha", "ok" => true },
@@ -1061,19 +1089,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_non_string_property
array = [
{ "handle" => "alpha", "{}" => true },
{ "handle" => "beta", "{}" => false },
{ "handle" => "gamma", "{}" => false },
{ "handle" => "delta", "{}" => true },
]
template = "{{ array | where: some_property, true | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array, "some_property" => {} })
end
def test_where_string_keys
input = [
"alpha", "beta", "gamma", "delta"
@@ -1282,7 +1297,7 @@ class StandardFiltersTest < Minitest::Test
end
def test_sum_with_non_string_property
input = [{ "true" => 1 }, { "1.0" => 0.2, "1" => -0.3 }, { "1..5" => 0.4 }]
input = [{ true => 1 }, { 1.0 => 0.2, 1 => -0.3 }, { 1..5 => 0.4 }]
assert_equal(1, @filters.sum(input, true))
assert_equal(0.2, @filters.sum(input, 1.0))
@@ -1292,6 +1307,22 @@ class StandardFiltersTest < Minitest::Test
assert_equal(0, @filters.sum(input, ""))
end
def test_uniq_with_to_liquid_value
input = [StringDrop.new("foo"), StringDrop.new("bar"), "foo"]
expected = [StringDrop.new("foo"), StringDrop.new("bar")]
result = @filters.uniq(input)
assert_equal(expected, result)
end
def test_uniq_with_to_liquid_value_pick_correct_classes
input = ["foo", StringDrop.new("foo"), StringDrop.new("bar")]
expected = [String, StringDrop]
result = @filters.uniq(input).map(&:class)
assert_equal(expected, result)
end
private
def with_timezone(tz)
+64
View File
@@ -46,6 +46,70 @@ class RenderTagTest < Minitest::Test
)
end
def test_render_after_defers_partial_until_context_flush
file_system = StubFileSystem.new('snippet' => 'deferred {{ value }}')
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse('before {% render "snippet" after, value: 1 %} after', environment: environment)
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(registers: registers, environment: environment)
assert_equal('before <?marker name="liquid-after-1"> after', template.render(context))
assert_equal('<template for="liquid-after-1">deferred 1</template>', context.render_after_tags)
assert_empty(context.after_render_jobs)
end
def test_render_after_evaluates_attributes_when_enqueued
file_system = StubFileSystem.new('snippet' => '{{ value }}')
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse('{% assign value = 1 %}{% render "snippet" after, value: value %}{% assign value = 2 %}', environment: environment)
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(registers: registers, environment: environment)
assert_equal('<?marker name="liquid-after-1">', template.render(context))
assert_equal('<template for="liquid-after-1">1</template>', context.render_after_tags)
end
def test_render_after_supports_with_and_as
file_system = StubFileSystem.new('snippet' => '{{ item }}')
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse('{% render "snippet" after with value as item %}', environment: environment)
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(static_environments: { 'value' => 'captured' }, registers: registers, environment: environment)
assert_equal('<?marker name="liquid-after-1">', template.render(context))
assert_equal('<template for="liquid-after-1">captured</template>', context.render_after_tags)
end
def test_render_after_supports_for_and_as
file_system = StubFileSystem.new('snippet' => '{{ forloop.index }}:{{ item }};')
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse('{% render "snippet" after for values as item %}', environment: environment)
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(static_environments: { 'values' => ['a', 'b'] }, registers: registers, environment: environment)
assert_equal('<?marker name="liquid-after-1">', template.render(context))
assert_equal('<template for="liquid-after-1">1:a;2:b;</template>', context.render_after_tags)
end
def test_render_after_preserves_fifo_order
file_system = StubFileSystem.new('snippet' => '{{ value }}')
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse('{% render "snippet" after, value: 1 %}{% render "snippet" after, value: 2 %}', environment: environment)
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(registers: registers, environment: environment)
assert_equal('<?marker name="liquid-after-1"><?marker name="liquid-after-2">', template.render(context))
assert_equal('<template for="liquid-after-1">1</template><template for="liquid-after-2">2</template>', context.render_after_tags)
end
def test_render_after_colon_remains_a_named_argument
assert_template_result(
'later',
'{% render "snippet", after: "later" %}',
partials: { 'snippet' => '{{ after }}' },
)
end
def test_render_does_not_inherit_parent_scope_variables
assert_template_result(
'',
+29
View File
@@ -146,6 +146,35 @@ class BooleanDrop < Liquid::Drop
end
end
class StringDrop < Liquid::Drop
include Comparable
def initialize(value)
super()
@value = value
end
def to_liquid_value
@value
end
def to_s
@value
end
def to_str
@value
end
def inspect
"#<StringDrop @value=#{@value.inspect}>"
end
def <=>(other)
to_liquid_value <=> Liquid::Utils.to_liquid_value(other)
end
end
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'