mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-14 00:10:39 -07:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
786381c762 | ||
|
|
8ad91b5e36 | ||
|
|
9bb7fbf123 | ||
|
|
8555fd8a20 | ||
|
|
9bd408f5d0 | ||
|
|
79b831d96c | ||
|
|
aebd75e5e8 | ||
|
|
7f2f8a226b | ||
|
|
7b2b25fda1 | ||
|
|
8548b96a97 | ||
|
|
fc96e66e14 | ||
|
|
79a771d724 | ||
|
|
65b1dedac5 | ||
|
|
f375d7b3aa | ||
|
|
6b3f6c6fb4 | ||
|
|
aefd48e341 |
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
@@ -45,8 +45,8 @@ jobs:
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.8.1 (unreleased)
|
||||
## 5.8.7
|
||||
* Expose body content in the `Doc` tag [James Meng]
|
||||
|
||||
## 5.8.1
|
||||
|
||||
|
||||
@@ -64,33 +64,6 @@ 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
|
||||
|
||||
@@ -9,6 +9,10 @@ module Liquid
|
||||
# Creates a new variable.
|
||||
# @liquid_description
|
||||
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
|
||||
#
|
||||
# > Caution:
|
||||
# > Predefined Liquid objects can be overridden by variables with the same name.
|
||||
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
|
||||
# @liquid_syntax
|
||||
# {% assign variable_name = value %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||
|
||||
@@ -9,6 +9,10 @@ module Liquid
|
||||
# Creates a new variable with a string value.
|
||||
# @liquid_description
|
||||
# You can create complex strings with Liquid logic and variables.
|
||||
#
|
||||
# > Caution:
|
||||
# > Predefined Liquid objects can be overridden by variables with the same name.
|
||||
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
|
||||
# @liquid_syntax
|
||||
# {% capture variable %}
|
||||
# value
|
||||
|
||||
@@ -7,6 +7,10 @@ module Liquid
|
||||
# @liquid_name decrement
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
||||
#
|
||||
# > Caution:
|
||||
# > Predefined Liquid objects can be overridden by variables with the same name.
|
||||
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
|
||||
# @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
|
||||
|
||||
@@ -36,6 +36,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = +""
|
||||
|
||||
while (token = tokens.shift)
|
||||
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
|
||||
|
||||
@@ -43,8 +45,10 @@ module Liquid
|
||||
|
||||
if tag_name == block_delimiter
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
@@ -55,11 +59,11 @@ module Liquid
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
@body.empty?
|
||||
end
|
||||
|
||||
def nodelist
|
||||
[]
|
||||
[@body]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -20,8 +20,8 @@ module Liquid
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render for each iteration.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param limit: [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
||||
class For < Block
|
||||
|
||||
@@ -7,6 +7,10 @@ module Liquid
|
||||
# @liquid_name increment
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
||||
#
|
||||
# > Caution:
|
||||
# > Predefined Liquid objects can be overridden by variables with the same name.
|
||||
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
|
||||
# @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
|
||||
|
||||
+11
-37
@@ -27,11 +27,7 @@ module Liquid
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
class Render < Tag
|
||||
FOR = 'for'
|
||||
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
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
@@ -43,11 +39,10 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
@after = !!Regexp.last_match(2)
|
||||
with_or_for = Regexp.last_match(4)
|
||||
variable_name = Regexp.last_match(5)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(7)
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
@@ -66,10 +61,6 @@ 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
|
||||
@@ -83,43 +74,26 @@ module Liquid
|
||||
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
evaluated_attributes = @attributes.transform_values { |value| context.evaluate(value) }
|
||||
|
||||
render_partial_func = ->(var, forloop, render_output) {
|
||||
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
|
||||
|
||||
evaluated_attributes.each do |key, value|
|
||||
inner_context[key] = value
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
inner_context[context_variable_name] = var unless var.nil?
|
||||
partial.render_to_output_buffer(inner_context, render_output)
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
forloop&.send(:increment!)
|
||||
}
|
||||
|
||||
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
|
||||
|
||||
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)
|
||||
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, output) }
|
||||
variable.each { |var| render_partial_func.call(var, forloop) }
|
||||
else
|
||||
render_partial_func.call(variable, nil, output)
|
||||
render_partial_func.call(variable, nil)
|
||||
end
|
||||
|
||||
output
|
||||
|
||||
@@ -19,9 +19,9 @@ module Liquid
|
||||
# @liquid_syntax_keyword variable The current item in the array.
|
||||
# @liquid_syntax_keyword array The array to iterate over.
|
||||
# @liquid_syntax_keyword expression The expression to render.
|
||||
# @liquid_optional_param cols [number] The number of columns that the table should have.
|
||||
# @liquid_optional_param limit [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param cols: [number] The number of columns that the table should have.
|
||||
# @liquid_optional_param limit: [number] The number of iterations to perform.
|
||||
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module Liquid
|
||||
# Templates are central to liquid.
|
||||
# Interpretating templates is a two step process. First you compile the
|
||||
# Interpreting templates is a two step process. First you compile the
|
||||
# source code you got. During compile time some extensive error checking is performed.
|
||||
# your code should expect to get some SyntaxErrors.
|
||||
#
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.8.6"
|
||||
VERSION = "5.8.7"
|
||||
end
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
# 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,70 +46,6 @@ 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(
|
||||
'',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class EnvironmentTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
class UnsubscribeFooter < Liquid::Tag
|
||||
def render(_context)
|
||||
'Unsubscribe Footer'
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_tag
|
||||
email_environment = Liquid::Environment.build do |environment|
|
||||
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
|
||||
end
|
||||
|
||||
assert(email_environment.tags["unsubscribe_footer"])
|
||||
assert(email_environment.tag_for_name("unsubscribe_footer"))
|
||||
template = Liquid::Template.parse("{% unsubscribe_footer %}", environment: email_environment)
|
||||
|
||||
assert_equal('Unsubscribe Footer', template.render)
|
||||
end
|
||||
end
|
||||
@@ -20,6 +20,21 @@ class DocTagUnitTest < Minitest::Test
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_body_content
|
||||
doc_content = " Documentation content\n @param {string} foo - test\n"
|
||||
template_source = "{% doc %}#{doc_content}{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal(doc_content, doc_tag.nodelist.first.to_s)
|
||||
end
|
||||
|
||||
def test_doc_tag_does_not_support_extra_arguments
|
||||
error = assert_raises(Liquid::SyntaxError) do
|
||||
template = <<~LIQUID.chomp
|
||||
@@ -116,6 +131,20 @@ class DocTagUnitTest < Minitest::Test
|
||||
assert_template_result('', template)
|
||||
end
|
||||
|
||||
def test_doc_tag_captures_token_before_enddoc
|
||||
template_source = "{% doc %}{{ incomplete{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal("{{ incomplete", doc_tag.nodelist.first.to_s)
|
||||
end
|
||||
|
||||
def test_doc_tag_preserves_error_line_numbers
|
||||
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
||||
{% doc %}
|
||||
@@ -145,11 +174,11 @@ class DocTagUnitTest < Minitest::Test
|
||||
|
||||
def test_doc_tag_delimiter_handling
|
||||
assert_template_result('', <<~LIQUID.chomp)
|
||||
{% if true %}
|
||||
{% doc %}
|
||||
{% docEXTRA %}wut{% enddocEXTRA %}xyz
|
||||
{% enddoc %}
|
||||
{% endif %}
|
||||
{%- if true -%}
|
||||
{%- doc -%}
|
||||
{%- docEXTRA -%}wut{% enddocEXTRA -%}xyz
|
||||
{%- enddoc -%}
|
||||
{%- endif -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
|
||||
@@ -167,6 +196,80 @@ class DocTagUnitTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_doc_tag_blank_with_empty_content
|
||||
template_source = "{% doc %}{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal(true, doc_tag.blank?)
|
||||
end
|
||||
|
||||
def test_doc_tag_blank_with_content
|
||||
template_source = "{% doc %}Some documentation{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal(false, doc_tag.blank?)
|
||||
end
|
||||
|
||||
def test_doc_tag_blank_with_whitespace_only
|
||||
template_source = "{% doc %} {% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal(false, doc_tag.blank?)
|
||||
end
|
||||
|
||||
def test_doc_tag_nodelist_returns_array_with_body
|
||||
doc_content = "Documentation content\n@param {string} foo"
|
||||
template_source = "{% doc %}#{doc_content}{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal([doc_content], doc_tag.nodelist)
|
||||
assert_equal(1, doc_tag.nodelist.length)
|
||||
assert_equal(doc_content, doc_tag.nodelist.first)
|
||||
end
|
||||
|
||||
def test_doc_tag_nodelist_with_empty_content
|
||||
template_source = "{% doc %}{% enddoc %}"
|
||||
|
||||
doc_tag = nil
|
||||
ParseTreeVisitor
|
||||
.for(Template.parse(template_source).root)
|
||||
.add_callback_for(Liquid::Doc) do |tag|
|
||||
doc_tag = tag
|
||||
end
|
||||
.visit
|
||||
|
||||
assert_equal([""], doc_tag.nodelist)
|
||||
assert_equal(1, doc_tag.nodelist.length)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def traversal(template)
|
||||
|
||||
Reference in New Issue
Block a user