mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-02 00:25:12 -07:00
Compare commits
30
Commits
@@ -14,8 +14,21 @@ jobs:
|
|||||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||||
- { ruby: 3.2, allowed-failure: false }
|
- { ruby: 3.2, allowed-failure: false }
|
||||||
- { ruby: 3.3, allowed-failure: false }
|
- { ruby: 3.3, allowed-failure: false }
|
||||||
|
- { ruby: 3.3, allowed-failure: false }
|
||||||
- { ruby: 3.4, allowed-failure: false } # latest
|
- { ruby: 3.4, allowed-failure: false } # latest
|
||||||
|
- {
|
||||||
|
ruby: 3.4,
|
||||||
|
allowed-failure: false,
|
||||||
|
rubyopt: "--enable-frozen-string-literal",
|
||||||
|
}
|
||||||
|
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||||
- { ruby: ruby-head, allowed-failure: false }
|
- { ruby: ruby-head, allowed-failure: false }
|
||||||
|
- {
|
||||||
|
ruby: ruby-head,
|
||||||
|
allowed-failure: false,
|
||||||
|
rubyopt: "--enable-frozen-string-literal",
|
||||||
|
}
|
||||||
|
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
|
||||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -26,6 +39,8 @@ jobs:
|
|||||||
bundler: latest
|
bundler: latest
|
||||||
- run: bundle exec rake
|
- run: bundle exec rake
|
||||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||||
|
env:
|
||||||
|
RUBYOPT: ${{ matrix.entry.rubyopt }}
|
||||||
|
|
||||||
memory_profile:
|
memory_profile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.3.6
|
3.4.1
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ group :benchmark, :test do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem "webrick"
|
||||||
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rubocop', '~> 1.61.0'
|
gem 'rubocop', '~> 1.61.0'
|
||||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||||
|
|||||||
+25
-3
@@ -1,12 +1,34 @@
|
|||||||
# Liquid Change Log
|
# Liquid Change Log
|
||||||
|
|
||||||
## 5.8.0 (unreleased)
|
## 5.8.1 (unreleased)
|
||||||
|
|
||||||
|
## 5.8.1
|
||||||
|
|
||||||
|
* Fix `{% doc %}` tag to be visitable [Guilherme Carreiro]
|
||||||
|
|
||||||
|
## 5.8.0
|
||||||
|
|
||||||
|
* Introduce the new `{% doc %}` tag [Guilherme Carreiro]
|
||||||
|
|
||||||
|
## 5.7.3
|
||||||
|
|
||||||
|
* Raise Liquid::SyntaxError when parsing invalidly encoded strings [Chris AtLee]
|
||||||
|
|
||||||
|
## 5.7.2 2025-01-31
|
||||||
|
|
||||||
|
* Fix array filters to not support nested properties [Guilherme Carreiro]
|
||||||
|
|
||||||
|
## 5.7.1 2025-01-24
|
||||||
|
|
||||||
|
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays [Guilherme Carreiro]
|
||||||
|
* Fix the `has` filter to return `false` when filtering empty arrays [Guilherme Carreiro]
|
||||||
|
|
||||||
## 5.7.0 2025-01-16
|
## 5.7.0 2025-01-16
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
* Add `find`, `find_index`, `has`, and `reject` filters to arrays
|
|
||||||
* Compatibility with Ruby 3.4
|
* Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
|
||||||
|
* Compatibility with Ruby 3.4 [Ian Ker-Seymer]
|
||||||
|
|
||||||
## 5.6.4 2025-01-14
|
## 5.6.4 2025-01-14
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ module Liquid
|
|||||||
# The Environment is the container for all configuration options of Liquid, such as
|
# The Environment is the container for all configuration options of Liquid, such as
|
||||||
# the registered tags, filters, and the default error mode.
|
# the registered tags, filters, and the default error mode.
|
||||||
class Environment
|
class Environment
|
||||||
# The default error mode for all templates. This can be overridden on a
|
|
||||||
# per-template basis.
|
|
||||||
attr_accessor :error_mode
|
|
||||||
|
|
||||||
# The tags that are available to use in the template.
|
# The tags that are available to use in the template.
|
||||||
attr_accessor :tags
|
attr_accessor :tags
|
||||||
|
|
||||||
@@ -75,7 +71,7 @@ module Liquid
|
|||||||
# @api private
|
# @api private
|
||||||
def initialize
|
def initialize
|
||||||
@tags = Tags::STANDARD_TAGS.dup
|
@tags = Tags::STANDARD_TAGS.dup
|
||||||
@error_mode = :lax
|
@error_mode = :strict
|
||||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
||||||
klass.add_filter(StandardFilters)
|
klass.add_filter(StandardFilters)
|
||||||
end
|
end
|
||||||
@@ -85,6 +81,14 @@ module Liquid
|
|||||||
@strainer_template_class_cache = {}
|
@strainer_template_class_cache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def error_mode
|
||||||
|
:strict
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_mode=(mode)
|
||||||
|
:strict
|
||||||
|
end
|
||||||
|
|
||||||
# Registers a new tag with the environment.
|
# Registers a new tag with the environment.
|
||||||
#
|
#
|
||||||
# @param name [String] The name of the tag.
|
# @param name [String] The name of the tag.
|
||||||
|
|||||||
@@ -161,6 +161,12 @@ module Liquid
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics/BlockNesting
|
# rubocop:enable Metrics/BlockNesting
|
||||||
output << EOS
|
output << EOS
|
||||||
|
rescue ::ArgumentError => e
|
||||||
|
if e.message == "invalid byte sequence in #{ss.string.encoding}"
|
||||||
|
raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def raise_syntax_error(start_pos, ss)
|
def raise_syntax_error(start_pos, ss)
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
errors:
|
errors:
|
||||||
syntax:
|
syntax:
|
||||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
||||||
|
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
|
||||||
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
||||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
||||||
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
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_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) "
|
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
||||||
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
||||||
|
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
|
||||||
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
||||||
for_invalid_in: "For loops require an 'in' clause"
|
for_invalid_in: "For loops require an 'in' clause"
|
||||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||||
begin
|
begin
|
||||||
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
|
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
||||||
rescue TypeError
|
rescue TypeError
|
||||||
raise_property_error(property)
|
raise_property_error(property)
|
||||||
end
|
end
|
||||||
@@ -416,7 +416,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||||
begin
|
begin
|
||||||
ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
|
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
|
||||||
rescue TypeError
|
rescue TypeError
|
||||||
raise_property_error(property)
|
raise_property_error(property)
|
||||||
end
|
end
|
||||||
@@ -456,10 +456,10 @@ module Liquid
|
|||||||
# Tests if any item in an array has a specific property value.
|
# Tests if any item in an array has a specific property value.
|
||||||
# @liquid_description
|
# @liquid_description
|
||||||
# This requires you to provide both the property name and the associated value.
|
# This requires you to provide both the property name and the associated value.
|
||||||
# @liquid_syntax array | some: string, string
|
# @liquid_syntax array | has: string, string
|
||||||
# @liquid_return [boolean]
|
# @liquid_return [boolean]
|
||||||
def has(input, property, target_value = nil)
|
def has(input, property, target_value = nil)
|
||||||
filter_array(input, property, target_value) { |ary, &block| ary.any?(&block) }
|
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
# @liquid_public_docs
|
||||||
@@ -472,7 +472,7 @@ module Liquid
|
|||||||
# @liquid_syntax array | find: string, string
|
# @liquid_syntax array | find: string, string
|
||||||
# @liquid_return [untyped]
|
# @liquid_return [untyped]
|
||||||
def find(input, property, target_value = nil)
|
def find(input, property, target_value = nil)
|
||||||
filter_array(input, property, target_value) { |ary, &block| ary.find(&block) }
|
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
# @liquid_public_docs
|
||||||
@@ -485,7 +485,7 @@ module Liquid
|
|||||||
# @liquid_syntax array | find_index: string, string
|
# @liquid_syntax array | find_index: string, string
|
||||||
# @liquid_return [number]
|
# @liquid_return [number]
|
||||||
def find_index(input, property, target_value = nil)
|
def find_index(input, property, target_value = nil)
|
||||||
filter_array(input, property, target_value) { |ary, &block| ary.find_index(&block) }
|
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# @liquid_public_docs
|
# @liquid_public_docs
|
||||||
@@ -504,7 +504,7 @@ module Liquid
|
|||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
ary.uniq do |item|
|
ary.uniq do |item|
|
||||||
fetch_property(item, property)
|
item[property]
|
||||||
rescue TypeError
|
rescue TypeError
|
||||||
raise_property_error(property)
|
raise_property_error(property)
|
||||||
rescue NoMethodError
|
rescue NoMethodError
|
||||||
@@ -540,7 +540,7 @@ module Liquid
|
|||||||
if property == "to_liquid"
|
if property == "to_liquid"
|
||||||
e
|
e
|
||||||
elsif e.respond_to?(:[])
|
elsif e.respond_to?(:[])
|
||||||
r = fetch_property(e, property)
|
r = e[property]
|
||||||
r.is_a?(Proc) ? r.call : r
|
r.is_a?(Proc) ? r.call : r
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -564,7 +564,7 @@ module Liquid
|
|||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
ary.reject do |item|
|
ary.reject do |item|
|
||||||
fetch_property(item, property).nil?
|
item[property].nil?
|
||||||
rescue TypeError
|
rescue TypeError
|
||||||
raise_property_error(property)
|
raise_property_error(property)
|
||||||
rescue NoMethodError
|
rescue NoMethodError
|
||||||
@@ -712,7 +712,16 @@ module Liquid
|
|||||||
input.gsub(/\r?\n/, "<br />\n")
|
input.gsub(/\r?\n/, "<br />\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
# @liquid_public_docs
|
||||||
|
# @liquid_type filter
|
||||||
|
# @liquid_category date
|
||||||
|
# @liquid_summary
|
||||||
|
# Formats a date according to a specified format string.
|
||||||
|
# @liquid_description
|
||||||
|
# This filter formats a date using various format specifiers. If the format string is empty,
|
||||||
|
# the original input is returned. If the input cannot be converted to a date, the original input is returned.
|
||||||
|
#
|
||||||
|
# The following format specifiers can be used:
|
||||||
#
|
#
|
||||||
# %a - The abbreviated weekday name (``Sun'')
|
# %a - The abbreviated weekday name (``Sun'')
|
||||||
# %A - The full weekday name (``Sunday'')
|
# %A - The full weekday name (``Sunday'')
|
||||||
@@ -741,8 +750,8 @@ module Liquid
|
|||||||
# %Y - Year with century
|
# %Y - Year with century
|
||||||
# %Z - Time zone name
|
# %Z - Time zone name
|
||||||
# %% - Literal ``%'' character
|
# %% - Literal ``%'' character
|
||||||
#
|
# @liquid_syntax date | date: string
|
||||||
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
|
# @liquid_return [string]
|
||||||
def date(input, format)
|
def date(input, format)
|
||||||
str_format = Utils.to_s(format)
|
str_format = Utils.to_s(format)
|
||||||
return input if str_format.empty?
|
return input if str_format.empty?
|
||||||
@@ -950,7 +959,7 @@ module Liquid
|
|||||||
if property.nil?
|
if property.nil?
|
||||||
item
|
item
|
||||||
elsif item.respond_to?(:[])
|
elsif item.respond_to?(:[])
|
||||||
fetch_property(item, property)
|
item[property]
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
@@ -969,16 +978,16 @@ module Liquid
|
|||||||
|
|
||||||
attr_reader :context
|
attr_reader :context
|
||||||
|
|
||||||
def filter_array(input, property, target_value, &block)
|
def filter_array(input, property, target_value, default_value = [], &block)
|
||||||
ary = InputIterator.new(input, context)
|
ary = InputIterator.new(input, context)
|
||||||
|
|
||||||
return [] if ary.empty?
|
return default_value if ary.empty?
|
||||||
|
|
||||||
block.call(ary) do |item|
|
block.call(ary) do |item|
|
||||||
if target_value.nil?
|
if target_value.nil?
|
||||||
fetch_property(item, property)
|
item[property]
|
||||||
else
|
else
|
||||||
fetch_property(item, property) == target_value
|
item[property] == target_value
|
||||||
end
|
end
|
||||||
rescue TypeError
|
rescue TypeError
|
||||||
raise_property_error(property)
|
raise_property_error(property)
|
||||||
@@ -988,31 +997,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_property(drop, property_or_keys)
|
|
||||||
##
|
|
||||||
# This keeps backward compatibility by supporting properties containing
|
|
||||||
# dots. This is valid in Liquid syntax and used in some runtimes, such as
|
|
||||||
# Shopify with metafields.
|
|
||||||
#
|
|
||||||
# Using this approach, properties like 'price.value' can be accessed in
|
|
||||||
# both of the following examples:
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# [
|
|
||||||
# { 'name' => 'Item 1', 'price.price' => 40000 },
|
|
||||||
# { 'name' => 'Item 2', 'price' => { 'value' => 39900 } }
|
|
||||||
# ]
|
|
||||||
# ```
|
|
||||||
value = drop[property_or_keys]
|
|
||||||
|
|
||||||
return value if !value.nil? || !property_or_keys.is_a?(String)
|
|
||||||
|
|
||||||
keys = property_or_keys.split('.')
|
|
||||||
keys.reduce(drop) do |drop, key|
|
|
||||||
drop.respond_to?(:[]) ? drop[key] : drop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def raise_property_error(property)
|
def raise_property_error(property)
|
||||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||||
end
|
end
|
||||||
@@ -1063,7 +1047,18 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def join(glue)
|
def join(glue)
|
||||||
to_a.join(glue.to_s)
|
first = true
|
||||||
|
output = +""
|
||||||
|
each do |item|
|
||||||
|
if first
|
||||||
|
first = false
|
||||||
|
else
|
||||||
|
output << glue
|
||||||
|
end
|
||||||
|
|
||||||
|
output << Liquid::Utils.to_s(item)
|
||||||
|
end
|
||||||
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
def concat(args)
|
def concat(args)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require_relative "tags/comment"
|
|||||||
require_relative "tags/raw"
|
require_relative "tags/raw"
|
||||||
require_relative "tags/render"
|
require_relative "tags/render"
|
||||||
require_relative "tags/cycle"
|
require_relative "tags/cycle"
|
||||||
|
require_relative "tags/doc"
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module Tags
|
module Tags
|
||||||
@@ -42,6 +43,7 @@ module Liquid
|
|||||||
'if' => If,
|
'if' => If,
|
||||||
'echo' => Echo,
|
'echo' => Echo,
|
||||||
'tablerow' => TableRow,
|
'tablerow' => TableRow,
|
||||||
|
'doc' => Doc,
|
||||||
}.freeze
|
}.freeze
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
# @liquid_public_docs
|
||||||
|
# @liquid_type tag
|
||||||
|
# @liquid_category syntax
|
||||||
|
# @liquid_name doc
|
||||||
|
# @liquid_summary
|
||||||
|
# Documents template elements with annotations.
|
||||||
|
# @liquid_description
|
||||||
|
# The `doc` tag allows developers to include documentation within Liquid
|
||||||
|
# templates. Any content inside `doc` tags is not rendered or outputted.
|
||||||
|
# Liquid code inside will be parsed but not executed. This facilitates
|
||||||
|
# tooling support for features like code completion, linting, and inline
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# For detailed documentation syntax and examples, see the
|
||||||
|
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
|
||||||
|
#
|
||||||
|
# @liquid_syntax
|
||||||
|
# {% doc %}
|
||||||
|
# Renders a message.
|
||||||
|
#
|
||||||
|
# @param {string} foo - A string value.
|
||||||
|
# @param {string} [bar] - An optional string value.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# {% render 'message', foo: 'Hello', bar: 'World' %}
|
||||||
|
# {% enddoc %}
|
||||||
|
# {{ foo }}, {{ bar }}!
|
||||||
|
class Doc < Block
|
||||||
|
NO_UNEXPECTED_ARGS = /\A\s*\z/
|
||||||
|
|
||||||
|
def initialize(tag_name, markup, parse_context)
|
||||||
|
super
|
||||||
|
ensure_valid_markup(tag_name, markup, parse_context)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(tokens)
|
||||||
|
while (token = tokens.shift)
|
||||||
|
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
|
||||||
|
|
||||||
|
raise_nested_doc_error if tag_name == @tag_name
|
||||||
|
|
||||||
|
if tag_name == block_delimiter
|
||||||
|
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raise_tag_never_closed(block_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_to_output_buffer(_context, output)
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
def blank?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def nodelist
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||||
|
unless NO_UNEXPECTED_ARGS.match?(markup)
|
||||||
|
raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_nested_doc_error
|
||||||
|
raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,11 +27,11 @@ module Liquid
|
|||||||
# :strict will enforce correct syntax.
|
# :strict will enforce correct syntax.
|
||||||
def error_mode=(mode)
|
def error_mode=(mode)
|
||||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
||||||
Environment.default.error_mode = mode
|
Environment.default.error_mode = :strict
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_mode
|
def error_mode
|
||||||
Environment.default.error_mode
|
:strict
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_exception_renderer=(renderer)
|
def default_exception_renderer=(renderer)
|
||||||
|
|||||||
@@ -103,6 +103,12 @@ module Liquid
|
|||||||
|
|
||||||
pos = @ss.pos -= 2
|
pos = @ss.pos -= 2
|
||||||
@source.byteslice(start, pos - start)
|
@source.byteslice(start, pos - start)
|
||||||
|
rescue ::ArgumentError => e
|
||||||
|
if e.message == "invalid byte sequence in #{@ss.string.encoding}"
|
||||||
|
raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_variable_token
|
def next_variable_token
|
||||||
|
|||||||
+28
-18
@@ -90,34 +90,38 @@ module Liquid
|
|||||||
obj
|
obj
|
||||||
end
|
end
|
||||||
|
|
||||||
if RUBY_VERSION >= '3.4'
|
def self.to_s(obj, seen = {})
|
||||||
def self.to_s(obj, seen = {})
|
case obj
|
||||||
case obj
|
when Hash
|
||||||
when Hash
|
# If the custom hash implementation overrides `#to_s`, use their
|
||||||
|
# custom implementation. Otherwise we use Liquid's default
|
||||||
|
# implementation.
|
||||||
|
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
|
||||||
hash_inspect(obj, seen)
|
hash_inspect(obj, seen)
|
||||||
when Array
|
|
||||||
array_inspect(obj, seen)
|
|
||||||
else
|
else
|
||||||
obj.to_s
|
obj.to_s
|
||||||
end
|
end
|
||||||
|
when Array
|
||||||
|
array_inspect(obj, seen)
|
||||||
|
else
|
||||||
|
obj.to_s
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.inspect(obj, seen = {})
|
def self.inspect(obj, seen = {})
|
||||||
case obj
|
case obj
|
||||||
when Hash
|
when Hash
|
||||||
|
# If the custom hash implementation overrides `#inspect`, use their
|
||||||
|
# custom implementation. Otherwise we use Liquid's default
|
||||||
|
# implementation.
|
||||||
|
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
|
||||||
hash_inspect(obj, seen)
|
hash_inspect(obj, seen)
|
||||||
when Array
|
|
||||||
array_inspect(obj, seen)
|
|
||||||
else
|
else
|
||||||
obj.inspect
|
obj.inspect
|
||||||
end
|
end
|
||||||
end
|
when Array
|
||||||
else
|
array_inspect(obj, seen)
|
||||||
def self.to_s(obj, seen = nil)
|
else
|
||||||
obj.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.inspect(obj, seen = nil)
|
|
||||||
obj.inspect
|
obj.inspect
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -175,5 +179,11 @@ module Liquid
|
|||||||
ensure
|
ensure
|
||||||
seen.delete(hash.object_id)
|
seen.delete(hash.object_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
|
||||||
|
private_constant :HASH_TO_S_METHOD
|
||||||
|
|
||||||
|
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
|
||||||
|
private_constant :HASH_INSPECT_METHOD
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "5.7.0"
|
VERSION = "5.8.1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -77,7 +77,30 @@ class HashRenderingTest < Minitest::Test
|
|||||||
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
|
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_join_filter_with_hash
|
||||||
|
array = [{ "key1" => "value1" }, { "key2" => "value2" }]
|
||||||
|
glue = { "lol" => "wut" }
|
||||||
|
assert_template_result("{\"key1\"=>\"value1\"}{\"lol\"=>\"wut\"}{\"key2\"=>\"value2\"}", "{{ my_array | join: glue }}", { "my_array" => array, "glue" => glue })
|
||||||
|
end
|
||||||
|
|
||||||
def test_render_hash_with_hash_key
|
def test_render_hash_with_hash_key
|
||||||
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
|
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
|
||||||
|
my_hash = Class.new(Hash) do
|
||||||
|
def to_s
|
||||||
|
"kewl"
|
||||||
|
end
|
||||||
|
end.new
|
||||||
|
|
||||||
|
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rendering_hash_without_custom_to_s_uses_default_inspect
|
||||||
|
my_hash = Class.new(Hash).new
|
||||||
|
my_hash[:foo] = :bar
|
||||||
|
|
||||||
|
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -54,30 +54,6 @@ class TestEnumerable < Liquid::Drop
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestDeepEnumerable < Liquid::Drop
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
class Product < Liquid::Drop
|
|
||||||
attr_reader :title, :price, :premium
|
|
||||||
|
|
||||||
def initialize(title:, price:, premium: nil)
|
|
||||||
@title = { "content" => title, "language" => "en" }
|
|
||||||
@price = { "value" => price, "unit" => "USD" }
|
|
||||||
@premium = { "category" => premium } if premium
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def each(&block)
|
|
||||||
[
|
|
||||||
Product.new(title: "Pro goggles", price: 1299),
|
|
||||||
Product.new(title: "Thermal gloves", price: 1299),
|
|
||||||
Product.new(title: "Alpine jacket", price: 3999, premium: 'Basic'),
|
|
||||||
Product.new(title: "Mountain boots", price: 3899, premium: 'Pro'),
|
|
||||||
Product.new(title: "Safety helmet", price: 1999)
|
|
||||||
].each(&block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class NumberLikeThing < Liquid::Drop
|
class NumberLikeThing < Liquid::Drop
|
||||||
def initialize(amount)
|
def initialize(amount)
|
||||||
@amount = amount
|
@amount = amount
|
||||||
@@ -157,6 +133,18 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_equal([], @filters.slice(input, -(1 << 63), 6))
|
assert_equal([], @filters.slice(input, -(1 << 63), 6))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_find_on_empty_array
|
||||||
|
assert_nil(@filters.find([], 'foo', 'bar'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_find_index_on_empty_array
|
||||||
|
assert_nil(@filters.find_index([], 'foo', 'bar'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_has_on_empty_array
|
||||||
|
refute(@filters.has([], 'foo', 'bar'))
|
||||||
|
end
|
||||||
|
|
||||||
def test_truncate
|
def test_truncate
|
||||||
assert_equal('1234...', @filters.truncate('1234567890', 7))
|
assert_equal('1234...', @filters.truncate('1234567890', 7))
|
||||||
assert_equal('1234567890', @filters.truncate('1234567890', 20))
|
assert_equal('1234567890', @filters.truncate('1234567890', 20))
|
||||||
@@ -305,6 +293,16 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
|
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_join_calls_to_liquid_on_each_element
|
||||||
|
drop = Class.new(Liquid::Drop) do
|
||||||
|
def to_liquid
|
||||||
|
'i did it'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
|
||||||
|
end
|
||||||
|
|
||||||
def test_sort
|
def test_sort
|
||||||
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
|
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
|
||||||
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
|
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
|
||||||
@@ -416,15 +414,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sort_natural_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | sort_natural: 'title.content' | map: 'title.content' | join: ', ' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "Alpine jacket, Mountain boots, Pro goggles, Safety helmet, Thermal gloves"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_legacy_sort_hash
|
def test_legacy_sort_hash
|
||||||
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
|
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
|
||||||
end
|
end
|
||||||
@@ -461,15 +450,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_uniq_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | uniq: 'price.value' | map: "title.content" | join: ', ' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "Pro goggles, Alpine jacket, Mountain boots, Safety helmet"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_compact_empty_array
|
def test_compact_empty_array
|
||||||
assert_equal([], @filters.compact([], "a"))
|
assert_equal([], @filters.compact([], "a"))
|
||||||
end
|
end
|
||||||
@@ -486,15 +466,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_compact_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | compact: 'premium.category' | map: 'title.content' | join: ', ' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "Alpine jacket, Mountain boots"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_reverse
|
def test_reverse
|
||||||
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
|
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
|
||||||
end
|
end
|
||||||
@@ -604,15 +575,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
|
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sort_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | sort: 'price.value' | map: 'title.content' | join: ', ' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "Pro goggles, Thermal gloves, Safety helmet, Mountain boots, Alpine jacket"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_first_and_last_call_to_liquid
|
def test_first_and_last_call_to_liquid
|
||||||
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
|
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
|
||||||
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
|
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
|
||||||
@@ -929,15 +891,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, template, { "array" => array })
|
assert_template_result(expected_output, template, { "array" => array })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_reject_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | reject: 'title.content', 'Pro goggles' | map: 'price.value' | join: ', ' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "1299, 3999, 3899, 1999"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_has
|
def test_has
|
||||||
array = [
|
array = [
|
||||||
{ "handle" => "alpha", "ok" => true },
|
{ "handle" => "alpha", "ok" => true },
|
||||||
@@ -966,6 +919,18 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_has_with_empty_arrays
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
|
||||||
|
{%- unless has_product -%}
|
||||||
|
Product not found.
|
||||||
|
{%- endunless -%}
|
||||||
|
LIQUID
|
||||||
|
expected_output = "Product not found."
|
||||||
|
|
||||||
|
assert_template_result(expected_output, template, { "products" => [] })
|
||||||
|
end
|
||||||
|
|
||||||
def test_has_with_false_value
|
def test_has_with_false_value
|
||||||
array = [
|
array = [
|
||||||
{ "handle" => "alpha", "ok" => true },
|
{ "handle" => "alpha", "ok" => true },
|
||||||
@@ -994,16 +959,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, template, { "array" => array })
|
assert_template_result(expected_output, template, { "array" => array })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_has_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | has: 'title.content', 'Pro goggles' -}},
|
|
||||||
{{- products | has: 'title.content', 'foo' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "true,false"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_with_value
|
def test_find_with_value
|
||||||
products = [
|
products = [
|
||||||
{ "title" => "Pro goggles", "price" => 1299 },
|
{ "title" => "Pro goggles", "price" => 1299 },
|
||||||
@@ -1022,14 +977,16 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, template, { "products" => products })
|
assert_template_result(expected_output, template, { "products" => products })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_find_with_deep_enumerables
|
def test_find_with_empty_arrays
|
||||||
template = <<~LIQUID
|
template = <<~LIQUID
|
||||||
{%- assign product = products | find: 'title.content', 'Pro goggles' -%}
|
{%- assign product = products | find: 'title.content', 'Not found' -%}
|
||||||
{{- product.title.content -}}
|
{%- unless product -%}
|
||||||
|
Product not found.
|
||||||
|
{%- endunless -%}
|
||||||
LIQUID
|
LIQUID
|
||||||
expected_output = "Pro goggles"
|
expected_output = "Product not found."
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
assert_template_result(expected_output, template, { "products" => [] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_find_index_with_value
|
def test_find_index_with_value
|
||||||
@@ -1050,14 +1007,16 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, template, { "products" => products })
|
assert_template_result(expected_output, template, { "products" => products })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_find_index_with_deep_enumerables
|
def test_find_index_with_empty_arrays
|
||||||
template = <<~LIQUID
|
template = <<~LIQUID
|
||||||
{%- assign index = products | find_index: 'title.content', 'Alpine jacket' -%}
|
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
|
||||||
{{- index -}}
|
{%- unless index -%}
|
||||||
|
Index not found.
|
||||||
|
{%- endunless -%}
|
||||||
LIQUID
|
LIQUID
|
||||||
expected_output = "2"
|
expected_output = "Index not found."
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
assert_template_result(expected_output, template, { "products" => [] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_where
|
def test_where
|
||||||
@@ -1158,15 +1117,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_nil(@filters.where([nil], "ok"))
|
assert_nil(@filters.where([nil], "ok"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_where_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | where: 'title.content', 'Pro goggles' | map: 'price.value' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "1299"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_all_filters_never_raise_non_liquid_exception
|
def test_all_filters_never_raise_non_liquid_exception
|
||||||
test_drop = TestDrop.new(value: "test")
|
test_drop = TestDrop.new(value: "test")
|
||||||
test_drop.context = Context.new
|
test_drop.context = Context.new
|
||||||
@@ -1318,15 +1268,6 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
|
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sum_with_deep_enumerables
|
|
||||||
template = <<~LIQUID
|
|
||||||
{{- products | sum: 'price.value' -}}
|
|
||||||
LIQUID
|
|
||||||
expected_output = "12495"
|
|
||||||
|
|
||||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def with_timezone(tz)
|
def with_timezone(tz)
|
||||||
|
|||||||
@@ -47,12 +47,18 @@ class BlockUnitTest < Minitest::Test
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_with_block
|
def test_comment_tag_with_block
|
||||||
template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
|
template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
|
||||||
assert_equal([String, Comment, String], block_types(template.root.nodelist))
|
assert_equal([String, Comment, String], block_types(template.root.nodelist))
|
||||||
assert_equal(3, template.root.nodelist.size)
|
assert_equal(3, template.root.nodelist.size)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_with_block
|
||||||
|
template = Liquid::Template.parse(" {% doc %} {% enddoc %} ")
|
||||||
|
assert_equal([String, Doc, String], block_types(template.root.nodelist))
|
||||||
|
assert_equal(3, template.root.nodelist.size)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def block_types(nodelist)
|
def block_types(nodelist)
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ class LexerUnitTest < Minitest::Test
|
|||||||
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
|
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_error_with_invalid_utf8
|
||||||
|
error = assert_raises(SyntaxError) do
|
||||||
|
tokenize("\x00\xff")
|
||||||
|
end
|
||||||
|
assert_equal(
|
||||||
|
'Liquid syntax error: Invalid byte sequence in UTF-8',
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def tokenize(input)
|
def tokenize(input)
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class DocTagUnitTest < Minitest::Test
|
||||||
|
def test_doc_tag
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
Renders loading-spinner.
|
||||||
|
|
||||||
|
@param {string} foo - some foo
|
||||||
|
@param {string} [bar] - optional bar
|
||||||
|
|
||||||
|
@example
|
||||||
|
{% render 'loading-spinner', foo: 'foo' %}
|
||||||
|
{% render 'loading-spinner', foo: 'foo', bar: 'bar' %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_does_not_support_extra_arguments
|
||||||
|
error = assert_raises(Liquid::SyntaxError) do
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc extra %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
Liquid::Template.parse(template)
|
||||||
|
end
|
||||||
|
|
||||||
|
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}"
|
||||||
|
act_error = error.message
|
||||||
|
|
||||||
|
assert_equal(exp_error, act_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_must_support_valid_tags
|
||||||
|
assert_match_syntax_error("Liquid syntax error (line 1): 'doc' tag was never closed", '{% doc %} foo')
|
||||||
|
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo {% enddoc %}')
|
||||||
|
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo %}{% enddoc %}')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_ignores_liquid_nodes
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% if true %}
|
||||||
|
{% if ... %}
|
||||||
|
{%- for ? -%}
|
||||||
|
{% while true %}
|
||||||
|
{%
|
||||||
|
unless if
|
||||||
|
%}
|
||||||
|
{% endcase %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_ignores_unclosed_liquid_tags
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% if true %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_does_not_allow_nested_docs
|
||||||
|
error = assert_raises(Liquid::SyntaxError) do
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% doc %}
|
||||||
|
{% doc %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
Liquid::Template.parse(template)
|
||||||
|
end
|
||||||
|
|
||||||
|
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Nested doc tags are not allowed"
|
||||||
|
act_error = error.message
|
||||||
|
|
||||||
|
assert_equal(exp_error, act_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_ignores_nested_raw_tags
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% raw %}
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_ignores_unclosed_assign
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% assign foo = "1"
|
||||||
|
{% enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_ignores_malformed_syntax
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% doc %}
|
||||||
|
{% {{ {%- enddoc %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_preserves_error_line_numbers
|
||||||
|
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
||||||
|
{% doc %}
|
||||||
|
{% if true %}
|
||||||
|
{% enddoc %}
|
||||||
|
{{ errors.standard_error }}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~TEXT.chomp
|
||||||
|
|
||||||
|
Liquid error (line 4): standard error
|
||||||
|
TEXT
|
||||||
|
|
||||||
|
assert_equal(expected, template.render('errors' => ErrorDrop.new))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_whitespace_control
|
||||||
|
# Basic whitespace control
|
||||||
|
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!")
|
||||||
|
assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!")
|
||||||
|
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!")
|
||||||
|
assert_template_result("Hello!", <<~LIQUID.chomp)
|
||||||
|
{%- doc %}Whitespace control!{% enddoc -%}
|
||||||
|
Hello!
|
||||||
|
LIQUID
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_delimiter_handling
|
||||||
|
assert_template_result('', <<~LIQUID.chomp)
|
||||||
|
{% if true %}
|
||||||
|
{% doc %}
|
||||||
|
{% docEXTRA %}wut{% enddocEXTRA %}xyz
|
||||||
|
{% enddoc %}
|
||||||
|
{% endif %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
|
||||||
|
assert_template_result('', "{% doc %}123{% enddoc\txyz %}")
|
||||||
|
assert_template_result('', "{% doc %}123{% enddoc\nxyz %}")
|
||||||
|
assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_visitor
|
||||||
|
template_source = '{% doc %}{% enddoc %}'
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
[Liquid::Doc],
|
||||||
|
visit(template_source),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def traversal(template)
|
||||||
|
ParseTreeVisitor
|
||||||
|
.for(Template.parse(template).root)
|
||||||
|
.add_callback_for(Liquid::Doc) do |tag|
|
||||||
|
tag_class = tag.class
|
||||||
|
tag_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit(template)
|
||||||
|
traversal(template).visit.flatten.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -35,4 +35,15 @@ class TemplateUnitTest < Minitest::Test
|
|||||||
def test_template_inheritance
|
def test_template_inheritance
|
||||||
assert_equal("foo", TemplateSubclass.parse("foo").render)
|
assert_equal("foo", TemplateSubclass.parse("foo").render)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_invalid_utf8
|
||||||
|
input = "\xff\x00"
|
||||||
|
error = assert_raises(SyntaxError) do
|
||||||
|
Liquid::Tokenizer.new(source: input, string_scanner: StringScanner.new(input))
|
||||||
|
end
|
||||||
|
assert_equal(
|
||||||
|
'Liquid syntax error: Invalid byte sequence in UTF-8',
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user