mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-01 08:05:15 -07:00
Compare commits
119
Commits
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
@@ -14,24 +14,39 @@ 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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.entry.ruby }}
|
ruby-version: ${{ matrix.entry.ruby }}
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
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
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||||
with:
|
with:
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- run: bundle exec rake memory_profile:run
|
- run: bundle exec rake memory_profile:run
|
||||||
|
|||||||
+31
-6
@@ -1,22 +1,47 @@
|
|||||||
# Liquid Change Log
|
# Liquid Change Log
|
||||||
|
|
||||||
## 5.8.0 (unreleased)
|
## 5.10.0
|
||||||
|
* Introduce support for Inline Snippets [Julia Boutin]
|
||||||
|
```
|
||||||
|
{%- snippet snowdevil -%}
|
||||||
|
Snowdevil
|
||||||
|
{%- endsnippet -%}
|
||||||
|
{% render snowdevil %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.9.0
|
||||||
|
* Introduce `:rigid` error mode for stricter, safer parsing of all tags [CP Clermont, Guilherme Carreiro]
|
||||||
|
|
||||||
|
## 5.8.7
|
||||||
|
* Expose body content in the `Doc` tag [James Meng]
|
||||||
|
|
||||||
|
## 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
|
## 5.7.2 2025-01-31
|
||||||
|
|
||||||
* Fix array filters to not support nested properties
|
* Fix array filters to not support nested properties [Guilherme Carreiro]
|
||||||
|
|
||||||
## 5.7.1 2025-01-24
|
## 5.7.1 2025-01-24
|
||||||
|
|
||||||
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays
|
* 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
|
* 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
|
* Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
|
||||||
* Compatibility with Ruby 3.4
|
* Compatibility with Ruby 3.4 [Ian Ker-Seymer]
|
||||||
|
|
||||||
## 5.6.4 2025-01-14
|
## 5.6.4 2025-01-14
|
||||||
|
|
||||||
|
|||||||
@@ -99,14 +99,14 @@ Setting the error mode of Liquid lets you specify how strictly you want your tem
|
|||||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||||
it very hard to debug and can lead to unexpected behaviour.
|
it very hard to debug and can lead to unexpected behaviour.
|
||||||
|
|
||||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
Liquid also comes with different parsers that can be used when editing templates to give better error messages
|
||||||
when templates are invalid. You can enable this new parser like this:
|
when templates are invalid. You can enable this new parser like this:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Liquid::Environment.default.error_mode = :strict
|
Liquid::Environment.default.error_mode = :rigid # Raises a SyntaxError when invalid syntax is used in all tags
|
||||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
|
||||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ task :rubocop do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc('runs test suite with both strict and lax parsers')
|
desc('runs test suite with lax, strict, and rigid parsers')
|
||||||
task :test do
|
task :test do
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||||
Rake::Task['base_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
@@ -42,6 +42,10 @@ task :test do
|
|||||||
Rake::Task['base_test'].reenable
|
Rake::Task['base_test'].reenable
|
||||||
Rake::Task['base_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
|
|
||||||
|
ENV['LIQUID_PARSER_MODE'] = 'rigid'
|
||||||
|
Rake::Task['base_test'].reenable
|
||||||
|
Rake::Task['base_test'].invoke
|
||||||
|
|
||||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||||
Rake::Task['integration_test'].reenable
|
Rake::Task['integration_test'].reenable
|
||||||
@@ -50,6 +54,10 @@ task :test do
|
|||||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||||
Rake::Task['integration_test'].reenable
|
Rake::Task['integration_test'].reenable
|
||||||
Rake::Task['integration_test'].invoke
|
Rake::Task['integration_test'].invoke
|
||||||
|
|
||||||
|
ENV['LIQUID_PARSER_MODE'] = 'rigid'
|
||||||
|
Rake::Task['integration_test'].reenable
|
||||||
|
Rake::Task['integration_test'].invoke
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,8 +88,13 @@ namespace :benchmark do
|
|||||||
ruby "./performance/benchmark.rb strict"
|
ruby "./performance/benchmark.rb strict"
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
desc "Run the liquid benchmark with rigid parsing"
|
||||||
task run: [:lax, :strict]
|
task :rigid do
|
||||||
|
ruby "./performance/benchmark.rb rigid"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run the liquid benchmark with lax, strict, and rigid parsing"
|
||||||
|
task run: [:lax, :strict, :rigid]
|
||||||
|
|
||||||
desc "Run unit benchmarks"
|
desc "Run unit benchmarks"
|
||||||
namespace :unit do
|
namespace :unit do
|
||||||
|
|||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'liquid'
|
||||||
|
|
||||||
|
class VirtualFileSystem
|
||||||
|
def initialize
|
||||||
|
snippet_1 = <<~LIQUID
|
||||||
|
<h1>
|
||||||
|
{{- greating | default: 'Hello' }}, {{ name | default: 'world' -}}!
|
||||||
|
</h1>
|
||||||
|
LIQUID
|
||||||
|
snippet_2 = <<~LIQUID
|
||||||
|
{%- for i in (1..5) -%}
|
||||||
|
> {{ i }}
|
||||||
|
{%- endfor -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
@templates = {
|
||||||
|
'snippet-1' => snippet_1,
|
||||||
|
'snippet-2' => snippet_2,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_template_file(key)
|
||||||
|
@templates[key] || raise(Liquid::FileSystemError, "No such template '#{key}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def source
|
||||||
|
File.read(ARGV[0])
|
||||||
|
rescue StandardError
|
||||||
|
'Usage: bin/render example/server/templates/index.liquid'
|
||||||
|
end
|
||||||
|
|
||||||
|
def assigns
|
||||||
|
{
|
||||||
|
'date' => Time.now,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
puts Liquid::Template
|
||||||
|
.parse(source, error_mode: :rigid)
|
||||||
|
.tap { |t| t.registers[:file_system] = VirtualFileSystem.new }
|
||||||
|
.render(assigns)
|
||||||
@@ -67,6 +67,7 @@ require 'liquid/i18n'
|
|||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
require 'liquid/tablerowloop_drop'
|
require 'liquid/tablerowloop_drop'
|
||||||
require 'liquid/forloop_drop'
|
require 'liquid/forloop_drop'
|
||||||
|
require 'liquid/snippet_drop'
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
require 'liquid/interrupts'
|
require 'liquid/interrupts'
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ module Liquid
|
|||||||
@@operators
|
@@operators
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_expression(parse_context, markup)
|
def self.parse_expression(parse_context, markup, safe: false)
|
||||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :attachment, :child_condition
|
attr_reader :attachment, :child_condition
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ module Liquid
|
|||||||
# @param file_system The default file system that is used
|
# @param file_system The default file system that is used
|
||||||
# to load templates from.
|
# to load templates from.
|
||||||
# @param error_mode [Symbol] The default error mode for all templates
|
# @param error_mode [Symbol] The default error mode for all templates
|
||||||
# (either :strict, :warn, or :lax).
|
# (either :rigid, :strict, :warn, or :lax).
|
||||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
# @param exception_renderer [Proc] The exception renderer that is used to
|
||||||
# render exceptions.
|
# render exceptions.
|
||||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
# @yieldparam environment [Environment] The environment instance that is being built.
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ module Liquid
|
|||||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
|
||||||
|
parse(parser.expression, ss, cache)
|
||||||
|
end
|
||||||
|
|
||||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||||
return unless markup
|
return unless markup
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{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]"
|
||||||
|
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [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) "
|
||||||
@@ -19,7 +20,9 @@
|
|||||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||||
invalid_template_encoding: "Invalid template encoding"
|
invalid_template_encoding: "Invalid template encoding"
|
||||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||||
|
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
|
||||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||||
|
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
|
||||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||||
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
||||||
@@ -28,5 +31,6 @@
|
|||||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||||
argument:
|
argument:
|
||||||
include: "Argument error in tag 'include' - Illegal template name"
|
include: "Argument error in tag 'include' - Illegal template name"
|
||||||
|
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
|
||||||
disabled:
|
disabled:
|
||||||
tag: "usage is not allowed in this context"
|
tag: "usage is not allowed in this context"
|
||||||
|
|||||||
@@ -50,7 +50,22 @@ module Liquid
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_expression(markup)
|
def safe_parse_expression(parser)
|
||||||
|
Expression.safe_parse(parser, @string_scanner, @expression_cache)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_expression(markup, safe: false)
|
||||||
|
if !safe && @error_mode == :rigid
|
||||||
|
# parse_expression is a widely used API. To maintain backward
|
||||||
|
# compatibility while raising awareness about rigid parser standards,
|
||||||
|
# the safe flag supports API users make a deliberate decision.
|
||||||
|
#
|
||||||
|
# In rigid mode, markup MUST come from a string returned by the parser
|
||||||
|
# (e.g., parser.expression). We're not calling the parser here to
|
||||||
|
# prevent redundant parser overhead.
|
||||||
|
raise Liquid::InternalError, "unsafe parse_expression cannot be used in rigid mode"
|
||||||
|
end
|
||||||
|
|
||||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module ParserSwitching
|
module ParserSwitching
|
||||||
|
# Do not use this.
|
||||||
|
#
|
||||||
|
# It's basically doing the same thing the {#parse_with_selected_parser},
|
||||||
|
# except this will try the strict parser regardless of the error mode,
|
||||||
|
# and fall back to the lax parser if the error mode is lax or warn,
|
||||||
|
# except when in rigid mode where it uses the rigid parser.
|
||||||
|
#
|
||||||
|
# @deprecated Use {#parse_with_selected_parser} instead.
|
||||||
def strict_parse_with_error_mode_fallback(markup)
|
def strict_parse_with_error_mode_fallback(markup)
|
||||||
|
return rigid_parse_with_error_context(markup) if rigid_mode?
|
||||||
|
|
||||||
strict_parse_with_error_context(markup)
|
strict_parse_with_error_context(markup)
|
||||||
rescue SyntaxError => e
|
rescue SyntaxError => e
|
||||||
case parse_context.error_mode
|
case parse_context.error_mode
|
||||||
|
when :rigid
|
||||||
|
raise
|
||||||
when :strict
|
when :strict
|
||||||
raise
|
raise
|
||||||
when :warn
|
when :warn
|
||||||
@@ -16,11 +28,12 @@ module Liquid
|
|||||||
|
|
||||||
def parse_with_selected_parser(markup)
|
def parse_with_selected_parser(markup)
|
||||||
case parse_context.error_mode
|
case parse_context.error_mode
|
||||||
|
when :rigid then rigid_parse_with_error_context(markup)
|
||||||
when :strict then strict_parse_with_error_context(markup)
|
when :strict then strict_parse_with_error_context(markup)
|
||||||
when :lax then lax_parse(markup)
|
when :lax then lax_parse(markup)
|
||||||
when :warn
|
when :warn
|
||||||
begin
|
begin
|
||||||
strict_parse_with_error_context(markup)
|
rigid_parse_with_error_context(markup)
|
||||||
rescue SyntaxError => e
|
rescue SyntaxError => e
|
||||||
parse_context.warnings << e
|
parse_context.warnings << e
|
||||||
lax_parse(markup)
|
lax_parse(markup)
|
||||||
@@ -28,8 +41,20 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rigid_mode?
|
||||||
|
parse_context.error_mode == :rigid
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def rigid_parse_with_error_context(markup)
|
||||||
|
rigid_parse(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
e.line_number = line_number
|
||||||
|
e.markup_context = markup_context(markup)
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
def strict_parse_with_error_context(markup)
|
def strict_parse_with_error_context(markup)
|
||||||
strict_parse(markup)
|
strict_parse(markup)
|
||||||
rescue SyntaxError => e
|
rescue SyntaxError => e
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
class SnippetDrop < Drop
|
||||||
|
attr_reader :body, :name, :filename
|
||||||
|
|
||||||
|
def initialize(body, name, filename)
|
||||||
|
super()
|
||||||
|
@body = body
|
||||||
|
@name = name
|
||||||
|
@filename = filename
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_partial
|
||||||
|
@body
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
'SnippetDrop'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
require 'cgi'
|
require 'cgi'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
require 'bigdecimal'
|
require 'bigdecimal'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module StandardFilters
|
module StandardFilters
|
||||||
MAX_I32 = (1 << 31) - 1
|
MAX_I32 = (1 << 31) - 1
|
||||||
@@ -456,7 +455,7 @@ 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, false) { |ary, &block| ary.any?(&block) }
|
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
|
||||||
@@ -712,7 +711,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 +749,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?
|
||||||
@@ -989,7 +997,7 @@ module Liquid
|
|||||||
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 '#{Utils.to_s(property)}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(input, operand, operation)
|
def apply_operation(input, operand, operation)
|
||||||
@@ -1061,7 +1069,10 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def uniq(&block)
|
def uniq(&block)
|
||||||
to_a.uniq(&block)
|
to_a.uniq do |item|
|
||||||
|
item = Utils.to_liquid_value(item)
|
||||||
|
block ? yield(item) : item
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def compact
|
def compact
|
||||||
|
|||||||
+6
-2
@@ -68,8 +68,12 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def parse_expression(markup)
|
def safe_parse_expression(parser)
|
||||||
parse_context.parse_expression(markup)
|
parse_context.safe_parse_expression(parser)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_expression(markup, safe: false)
|
||||||
|
parse_context.parse_expression(markup, safe: safe)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ require_relative "tags/raw"
|
|||||||
require_relative "tags/render"
|
require_relative "tags/render"
|
||||||
require_relative "tags/cycle"
|
require_relative "tags/cycle"
|
||||||
require_relative "tags/doc"
|
require_relative "tags/doc"
|
||||||
|
require_relative "tags/snippet"
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module Tags
|
module Tags
|
||||||
@@ -44,6 +45,7 @@ module Liquid
|
|||||||
'echo' => Echo,
|
'echo' => Echo,
|
||||||
'tablerow' => TableRow,
|
'tablerow' => TableRow,
|
||||||
'doc' => Doc,
|
'doc' => Doc,
|
||||||
|
'snippet' => Snippet,
|
||||||
}.freeze
|
}.freeze
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ module Liquid
|
|||||||
# Creates a new variable.
|
# Creates a new variable.
|
||||||
# @liquid_description
|
# @liquid_description
|
||||||
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
|
# 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
|
# @liquid_syntax
|
||||||
# {% assign variable_name = value %}
|
# {% assign variable_name = value %}
|
||||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
# @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.
|
# Creates a new variable with a string value.
|
||||||
# @liquid_description
|
# @liquid_description
|
||||||
# You can create complex strings with Liquid logic and variables.
|
# 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
|
# @liquid_syntax
|
||||||
# {% capture variable %}
|
# {% capture variable %}
|
||||||
# value
|
# value
|
||||||
|
|||||||
+42
-6
@@ -31,12 +31,7 @@ module Liquid
|
|||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@blocks = []
|
@blocks = []
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
if markup =~ Syntax
|
|
||||||
@left = parse_expression(Regexp.last_match(1))
|
|
||||||
else
|
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
@@ -91,9 +86,50 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
parser = @parse_context.new_parser(markup)
|
||||||
|
@left = safe_parse_expression(parser)
|
||||||
|
parser.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
|
if markup =~ Syntax
|
||||||
|
@left = parse_expression(Regexp.last_match(1))
|
||||||
|
else
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def record_when_condition(markup)
|
def record_when_condition(markup)
|
||||||
body = new_body
|
body = new_body
|
||||||
|
|
||||||
|
if rigid_mode?
|
||||||
|
parse_rigid_when(markup, body)
|
||||||
|
else
|
||||||
|
parse_lax_when(markup, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_rigid_when(markup, body)
|
||||||
|
parser = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
loop do
|
||||||
|
expr = safe_parse_expression(parser)
|
||||||
|
block = Condition.new(@left, '==', expr)
|
||||||
|
block.attach(body)
|
||||||
|
@blocks << block
|
||||||
|
|
||||||
|
break unless parser.id?('or') || parser.consume?(:comma)
|
||||||
|
end
|
||||||
|
|
||||||
|
parser.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_lax_when(markup, body)
|
||||||
while markup
|
while markup
|
||||||
unless markup =~ WhenSyntax
|
unless markup =~ WhenSyntax
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||||
|
|||||||
+69
-16
@@ -17,23 +17,13 @@ module Liquid
|
|||||||
class Cycle < Tag
|
class Cycle < Tag
|
||||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||||
|
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
|
||||||
|
|
||||||
attr_reader :variables
|
attr_reader :variables
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
case markup
|
parse_with_selected_parser(markup)
|
||||||
when NamedSyntax
|
|
||||||
@variables = variables_from_string(Regexp.last_match(2))
|
|
||||||
@name = parse_expression(Regexp.last_match(1))
|
|
||||||
@is_named = true
|
|
||||||
when SimpleSyntax
|
|
||||||
@variables = variables_from_string(markup)
|
|
||||||
@name = @variables.to_s
|
|
||||||
@is_named = !@name.match?(/\w+:0x\h{8}/)
|
|
||||||
else
|
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def named?
|
def named?
|
||||||
@@ -65,19 +55,82 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# cycle [name:] expression(, expression)*
|
||||||
|
def rigid_parse(markup)
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
@variables = []
|
||||||
|
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
|
||||||
|
|
||||||
|
first_expression = safe_parse_expression(p)
|
||||||
|
if p.look(:colon)
|
||||||
|
# cycle name: expr1, expr2, ...
|
||||||
|
@name = first_expression
|
||||||
|
@is_named = true
|
||||||
|
p.consume(:colon)
|
||||||
|
# After the colon, parse the first variable (required for named cycles)
|
||||||
|
@variables << maybe_dup_lookup(safe_parse_expression(p))
|
||||||
|
else
|
||||||
|
# cycle expr1, expr2, ...
|
||||||
|
@variables << maybe_dup_lookup(first_expression)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse remaining comma-separated expressions
|
||||||
|
while p.consume?(:comma)
|
||||||
|
break if p.look(:end_of_string)
|
||||||
|
|
||||||
|
@variables << maybe_dup_lookup(safe_parse_expression(p))
|
||||||
|
end
|
||||||
|
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
|
||||||
|
unless @is_named
|
||||||
|
@name = @variables.to_s
|
||||||
|
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
|
case markup
|
||||||
|
when NamedSyntax
|
||||||
|
@variables = variables_from_string(Regexp.last_match(2))
|
||||||
|
@name = parse_expression(Regexp.last_match(1))
|
||||||
|
@is_named = true
|
||||||
|
when SimpleSyntax
|
||||||
|
@variables = variables_from_string(markup)
|
||||||
|
@name = @variables.to_s
|
||||||
|
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
|
||||||
|
else
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def variables_from_string(markup)
|
def variables_from_string(markup)
|
||||||
markup.split(',').collect do |var|
|
markup.split(',').collect do |var|
|
||||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||||
next unless Regexp.last_match(1)
|
next unless Regexp.last_match(1)
|
||||||
|
|
||||||
# Expression Parser returns cached objects, and we need to dup them to
|
|
||||||
# start the cycle over for each new cycle call.
|
|
||||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
|
||||||
var = parse_expression(Regexp.last_match(1))
|
var = parse_expression(Regexp.last_match(1))
|
||||||
var.is_a?(VariableLookup) ? var.dup : var
|
maybe_dup_lookup(var)
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
|
||||||
|
# we make it so that the @variables.to_s produces different strings for cycles
|
||||||
|
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
|
||||||
|
# This makes it so {% cycle a, b %} and {% cycle a, b %} have independent counters even if a and b share value.
|
||||||
|
# This is not true for literal values, {% cycle "a", "b" %} and {% cycle "a", "b" %} share the same counter.
|
||||||
|
# I was really scratching my head about this one, but migrating away from this would be more headache
|
||||||
|
# than it's worth. So we're keeping this quirk for now.
|
||||||
|
def maybe_dup_lookup(var)
|
||||||
|
var.is_a?(VariableLookup) ? var.dup : var
|
||||||
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
def children
|
def children
|
||||||
Array(@node.variables)
|
Array(@node.variables)
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ module Liquid
|
|||||||
# @liquid_name decrement
|
# @liquid_name decrement
|
||||||
# @liquid_summary
|
# @liquid_summary
|
||||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
# 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
|
# @liquid_description
|
||||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
# 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
|
# 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)
|
# 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
|
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
||||||
|
|||||||
+15
-4
@@ -13,17 +13,20 @@ module Liquid
|
|||||||
# Liquid code inside will be parsed but not executed. This facilitates
|
# Liquid code inside will be parsed but not executed. This facilitates
|
||||||
# tooling support for features like code completion, linting, and inline
|
# tooling support for features like code completion, linting, and inline
|
||||||
# documentation.
|
# documentation.
|
||||||
|
#
|
||||||
|
# For detailed documentation syntax and examples, see the
|
||||||
|
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
|
||||||
|
#
|
||||||
# @liquid_syntax
|
# @liquid_syntax
|
||||||
# {% doc %}
|
# {% doc %}
|
||||||
# Renders a message.
|
# Renders a message.
|
||||||
#
|
#
|
||||||
# @param {string} foo - A foo value.
|
# @param {string} foo - A string value.
|
||||||
# @param {string} [bar] - An optional bar value.
|
# @param {string} [bar] - An optional string value.
|
||||||
#
|
#
|
||||||
# @example
|
# @example
|
||||||
# {% render 'message', foo: 'Hello', bar: 'World' %}
|
# {% render 'message', foo: 'Hello', bar: 'World' %}
|
||||||
# {% enddoc %}
|
# {% enddoc %}
|
||||||
# {{ foo }}, {{ bar }}!
|
|
||||||
class Doc < Block
|
class Doc < Block
|
||||||
NO_UNEXPECTED_ARGS = /\A\s*\z/
|
NO_UNEXPECTED_ARGS = /\A\s*\z/
|
||||||
|
|
||||||
@@ -33,6 +36,8 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
|
@body = +""
|
||||||
|
|
||||||
while (token = tokens.shift)
|
while (token = tokens.shift)
|
||||||
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
|
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
|
||||||
|
|
||||||
@@ -40,8 +45,10 @@ module Liquid
|
|||||||
|
|
||||||
if tag_name == block_delimiter
|
if tag_name == block_delimiter
|
||||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||||
|
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@body << token unless token.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
raise_tag_never_closed(block_name)
|
raise_tag_never_closed(block_name)
|
||||||
@@ -52,7 +59,11 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
true
|
@body.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def nodelist
|
||||||
|
[@body]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
+11
-7
@@ -20,8 +20,8 @@ module Liquid
|
|||||||
# @liquid_syntax_keyword variable The current item in the array.
|
# @liquid_syntax_keyword variable The current item in the array.
|
||||||
# @liquid_syntax_keyword array The array to iterate over.
|
# @liquid_syntax_keyword array The array to iterate over.
|
||||||
# @liquid_syntax_keyword expression The expression to render for each iteration.
|
# @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 limit: [number] The number of iterations to perform.
|
||||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
# @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 range [untyped] A custom numeric range to iterate over.
|
||||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
||||||
class For < Block
|
class For < Block
|
||||||
@@ -93,7 +93,7 @@ module Liquid
|
|||||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||||
|
|
||||||
collection_name = p.expression
|
collection_name = p.expression
|
||||||
@collection_name = parse_expression(collection_name)
|
@collection_name = parse_expression(collection_name, safe: true)
|
||||||
|
|
||||||
@name = "#{@variable_name}-#{collection_name}"
|
@name = "#{@variable_name}-#{collection_name}"
|
||||||
@reversed = p.id?('reversed')
|
@reversed = p.id?('reversed')
|
||||||
@@ -104,13 +104,17 @@ module Liquid
|
|||||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||||
end
|
end
|
||||||
p.consume(:colon)
|
p.consume(:colon)
|
||||||
set_attribute(attribute, p.expression)
|
set_attribute(attribute, p.expression, safe: true)
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
strict_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
def collection_segment(context)
|
def collection_segment(context)
|
||||||
offsets = context.registers[:for] ||= {}
|
offsets = context.registers[:for] ||= {}
|
||||||
|
|
||||||
@@ -174,16 +178,16 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_attribute(key, expr)
|
def set_attribute(key, expr, safe: false)
|
||||||
case key
|
case key
|
||||||
when 'offset'
|
when 'offset'
|
||||||
@from = if expr == 'continue'
|
@from = if expr == 'continue'
|
||||||
:continue
|
:continue
|
||||||
else
|
else
|
||||||
parse_expression(expr)
|
parse_expression(expr, safe: safe)
|
||||||
end
|
end
|
||||||
when 'limit'
|
when 'limit'
|
||||||
@limit = parse_expression(expr)
|
@limit = parse_expression(expr, safe: safe)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
strict_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
def push_block(tag, markup)
|
def push_block(tag, markup)
|
||||||
block = if tag == 'else'
|
block = if tag == 'else'
|
||||||
ElseCondition.new
|
ElseCondition.new
|
||||||
@@ -77,8 +81,8 @@ module Liquid
|
|||||||
block.attach(new_body)
|
block.attach(new_body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_expression(markup)
|
def parse_expression(markup, safe: false)
|
||||||
Condition.parse_expression(parse_context, markup)
|
Condition.parse_expression(parse_context, markup, safe: safe)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
@@ -120,9 +124,9 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parse_comparison(p)
|
def parse_comparison(p)
|
||||||
a = parse_expression(p.expression)
|
a = parse_expression(p.expression, safe: true)
|
||||||
if (op = p.consume?(:comparison))
|
if (op = p.consume?(:comparison))
|
||||||
b = parse_expression(p.expression)
|
b = parse_expression(p.expression, safe: true)
|
||||||
Condition.new(a, op, b)
|
Condition.new(a, op, b)
|
||||||
else
|
else
|
||||||
Condition.new(a)
|
Condition.new(a)
|
||||||
|
|||||||
+45
-19
@@ -6,7 +6,7 @@ module Liquid
|
|||||||
# @liquid_category theme
|
# @liquid_category theme
|
||||||
# @liquid_name include
|
# @liquid_name include
|
||||||
# @liquid_summary
|
# @liquid_summary
|
||||||
# Renders a [snippet](/themes/architecture#snippets).
|
# Renders a [snippet](/themes/architecture/snippets).
|
||||||
# @liquid_description
|
# @liquid_description
|
||||||
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
|
||||||
# snippet.
|
# snippet.
|
||||||
@@ -27,24 +27,7 @@ module Liquid
|
|||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
if markup =~ SYNTAX
|
|
||||||
|
|
||||||
template_name = Regexp.last_match(1)
|
|
||||||
variable_name = Regexp.last_match(3)
|
|
||||||
|
|
||||||
@alias_name = Regexp.last_match(5)
|
|
||||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
|
||||||
@template_name_expr = parse_expression(template_name)
|
|
||||||
@attributes = {}
|
|
||||||
|
|
||||||
markup.scan(TagAttributes) do |key, value|
|
|
||||||
@attributes[key] = parse_expression(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(_tokens)
|
def parse(_tokens)
|
||||||
@@ -101,6 +84,49 @@ module Liquid
|
|||||||
alias_method :parse_context, :options
|
alias_method :parse_context, :options
|
||||||
private :parse_context
|
private :parse_context
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
@template_name_expr = safe_parse_expression(p)
|
||||||
|
@variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with")
|
||||||
|
@alias_name = p.consume(:id) if p.id?("as")
|
||||||
|
|
||||||
|
p.consume?(:comma)
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
while p.look(:id)
|
||||||
|
key = p.consume
|
||||||
|
p.consume(:colon)
|
||||||
|
@attributes[key] = safe_parse_expression(p)
|
||||||
|
p.consume?(:comma)
|
||||||
|
end
|
||||||
|
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
|
if markup =~ SYNTAX
|
||||||
|
template_name = Regexp.last_match(1)
|
||||||
|
variable_name = Regexp.last_match(3)
|
||||||
|
|
||||||
|
@alias_name = Regexp.last_match(5)
|
||||||
|
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||||
|
@template_name_expr = parse_expression(template_name)
|
||||||
|
@attributes = {}
|
||||||
|
|
||||||
|
markup.scan(TagAttributes) do |key, value|
|
||||||
|
@attributes[key] = parse_expression(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
def children
|
def children
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ module Liquid
|
|||||||
# @liquid_name increment
|
# @liquid_name increment
|
||||||
# @liquid_summary
|
# @liquid_summary
|
||||||
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
|
# 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
|
# @liquid_description
|
||||||
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
# 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
|
# 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)
|
# 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
|
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
|
||||||
|
|||||||
+69
-29
@@ -6,7 +6,7 @@ module Liquid
|
|||||||
# @liquid_category theme
|
# @liquid_category theme
|
||||||
# @liquid_name render
|
# @liquid_name render
|
||||||
# @liquid_summary
|
# @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
|
# @liquid_description
|
||||||
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
|
# 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)
|
# 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,7 @@ module Liquid
|
|||||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||||
class Render < Tag
|
class Render < Tag
|
||||||
FOR = 'for'
|
FOR = 'for'
|
||||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||||
|
|
||||||
disable_tags "include"
|
disable_tags "include"
|
||||||
|
|
||||||
@@ -35,22 +35,7 @@ module Liquid
|
|||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
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)
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
@attributes = {}
|
|
||||||
markup.scan(TagAttributes) do |key, value|
|
|
||||||
@attributes[key] = parse_expression(value)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_loop?
|
def for_loop?
|
||||||
@@ -62,21 +47,23 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_tag(context, output)
|
def render_tag(context, output)
|
||||||
# The expression should be a String literal, which parses to a String object
|
template = context.evaluate(@template_name_expr)
|
||||||
template_name = @template_name_expr
|
|
||||||
raise ::ArgumentError unless template_name.is_a?(String)
|
|
||||||
|
|
||||||
partial = PartialCache.load(
|
if @template_name_expr.is_a?(String)
|
||||||
template_name,
|
partial = PartialCache.load(template, context: context, parse_context: parse_context)
|
||||||
context: context,
|
template_name = partial.name
|
||||||
parse_context: parse_context,
|
context_variable_name = @alias_name || template_name.split('/').last
|
||||||
)
|
elsif template.respond_to?(:to_partial) && template.respond_to?(:name)
|
||||||
|
partial = template.to_partial
|
||||||
context_variable_name = @alias_name || template_name.split('/').last
|
template_name = template.filename
|
||||||
|
context_variable_name = @alias_name || template.name
|
||||||
|
else
|
||||||
|
raise ::ArgumentError
|
||||||
|
end
|
||||||
|
|
||||||
render_partial_func = ->(var, forloop) {
|
render_partial_func = ->(var, forloop) {
|
||||||
inner_context = context.new_isolated_subcontext
|
inner_context = context.new_isolated_subcontext
|
||||||
inner_context.template_name = partial.name
|
inner_context.template_name = template_name
|
||||||
inner_context.partial = true
|
inner_context.partial = true
|
||||||
inner_context['forloop'] = forloop if forloop
|
inner_context['forloop'] = forloop if forloop
|
||||||
|
|
||||||
@@ -99,6 +86,59 @@ module Liquid
|
|||||||
output
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# render (string) (with|for expression)? (as id)? (key: value)*
|
||||||
|
def rigid_parse(markup)
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
@template_name_expr = parse_expression(rigid_template_name(p), safe: true)
|
||||||
|
with_or_for = p.id?("for") || p.id?("with")
|
||||||
|
@variable_name_expr = safe_parse_expression(p) if with_or_for
|
||||||
|
@alias_name = p.consume(:id) if p.id?("as")
|
||||||
|
@is_for_loop = (with_or_for == FOR)
|
||||||
|
|
||||||
|
p.consume?(:comma)
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
while p.look(:id)
|
||||||
|
key = p.consume
|
||||||
|
p.consume(:colon)
|
||||||
|
@attributes[key] = safe_parse_expression(p)
|
||||||
|
p.consume?(:comma) # optional comma
|
||||||
|
end
|
||||||
|
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rigid_template_name(p)
|
||||||
|
return p.consume(:string) if p.look(:string)
|
||||||
|
return p.consume(:id) if p.look(:id)
|
||||||
|
|
||||||
|
found = p.consume || "nothing"
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
markup.scan(TagAttributes) do |key, value|
|
||||||
|
@attributes[key] = parse_expression(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||||
def children
|
def children
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
# @liquid_public_docs
|
||||||
|
# @liquid_type tag
|
||||||
|
# @liquid_category variable
|
||||||
|
# @liquid_name snippet
|
||||||
|
# @liquid_summary
|
||||||
|
# Creates a new inline snippet.
|
||||||
|
# @liquid_description
|
||||||
|
# You can create inline snippets to make your Liquid code more modular.
|
||||||
|
# @liquid_syntax
|
||||||
|
# {% snippet snippet_name %}
|
||||||
|
# value
|
||||||
|
# {% endsnippet %}
|
||||||
|
class Snippet < Block
|
||||||
|
def initialize(tag_name, markup, options)
|
||||||
|
super
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
if p.look(:id)
|
||||||
|
@to = p.consume(:id)
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
else
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_to_output_buffer(context, output)
|
||||||
|
snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
|
||||||
|
context.scopes.last[@to] = snippet_drop
|
||||||
|
context.resource_limits.increment_assign_score(1)
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
def blank?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,17 +19,54 @@ module Liquid
|
|||||||
# @liquid_syntax_keyword variable The current item in the array.
|
# @liquid_syntax_keyword variable The current item in the array.
|
||||||
# @liquid_syntax_keyword array The array to iterate over.
|
# @liquid_syntax_keyword array The array to iterate over.
|
||||||
# @liquid_syntax_keyword expression The expression to render.
|
# @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 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 limit: [number] The number of iterations to perform.
|
||||||
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
|
# @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 range [untyped] A custom numeric range to iterate over.
|
||||||
class TableRow < Block
|
class TableRow < Block
|
||||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||||
|
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
|
||||||
|
|
||||||
attr_reader :variable_name, :collection_name, :attributes
|
attr_reader :variable_name, :collection_name, :attributes
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
parse_with_selected_parser(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
@variable_name = p.consume(:id)
|
||||||
|
|
||||||
|
unless p.id?("in")
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
|
||||||
|
end
|
||||||
|
|
||||||
|
@collection_name = safe_parse_expression(p)
|
||||||
|
|
||||||
|
p.consume?(:comma)
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
|
while p.look(:id)
|
||||||
|
key = p.consume
|
||||||
|
unless ALLOWED_ATTRIBUTES.include?(key)
|
||||||
|
raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
|
||||||
|
end
|
||||||
|
|
||||||
|
p.consume(:colon)
|
||||||
|
@attributes[key] = safe_parse_expression(p)
|
||||||
|
p.consume?(:comma)
|
||||||
|
end
|
||||||
|
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict_parse(markup)
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lax_parse(markup)
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@variable_name = Regexp.last_match(1)
|
@variable_name = Regexp.last_match(1)
|
||||||
@collection_name = parse_expression(Regexp.last_match(2))
|
@collection_name = parse_expression(Regexp.last_match(2))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
# Templates are central to 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.
|
# source code you got. During compile time some extensive error checking is performed.
|
||||||
# your code should expect to get some SyntaxErrors.
|
# your code should expect to get some SyntaxErrors.
|
||||||
#
|
#
|
||||||
@@ -24,7 +24,8 @@ module Liquid
|
|||||||
# Sets how strict the parser should be.
|
# Sets how strict the parser should be.
|
||||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||||
# :strict will enforce correct syntax.
|
# :strict enforces correct syntax for most tags
|
||||||
|
# :rigid enforces correct syntax for all tags
|
||||||
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 = mode
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+5
-2
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module Utils
|
module Utils
|
||||||
|
DECIMAL_REGEX = /\A-?\d+\.\d+\z/
|
||||||
|
UNIX_TIMESTAMP_REGEX = /\A\d+\z/
|
||||||
|
|
||||||
def self.slice_collection(collection, from, to)
|
def self.slice_collection(collection, from, to)
|
||||||
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
||||||
collection.load_slice(from, to)
|
collection.load_slice(from, to)
|
||||||
@@ -52,7 +55,7 @@ module Liquid
|
|||||||
when Numeric
|
when Numeric
|
||||||
obj
|
obj
|
||||||
when String
|
when String
|
||||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
||||||
else
|
else
|
||||||
if obj.respond_to?(:to_number)
|
if obj.respond_to?(:to_number)
|
||||||
obj.to_number
|
obj.to_number
|
||||||
@@ -73,7 +76,7 @@ module Liquid
|
|||||||
case obj
|
case obj
|
||||||
when 'now', 'today'
|
when 'now', 'today'
|
||||||
Time.now
|
Time.now
|
||||||
when /\A\d+\z/, Integer
|
when UNIX_TIMESTAMP_REGEX, Integer
|
||||||
Time.at(obj.to_i)
|
Time.at(obj.to_i)
|
||||||
when String
|
when String
|
||||||
Time.parse(obj)
|
Time.parse(obj)
|
||||||
|
|||||||
+55
-4
@@ -54,7 +54,7 @@ module Liquid
|
|||||||
next unless f =~ /\w+/
|
next unless f =~ /\w+/
|
||||||
filtername = Regexp.last_match(0)
|
filtername = Regexp.last_match(0)
|
||||||
filterargs = f.scan(FilterArgsRegex).flatten
|
filterargs = f.scan(FilterArgsRegex).flatten
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -65,15 +65,26 @@ module Liquid
|
|||||||
|
|
||||||
return if p.look(:end_of_string)
|
return if p.look(:end_of_string)
|
||||||
|
|
||||||
@name = parse_context.parse_expression(p.expression)
|
@name = parse_context.safe_parse_expression(p)
|
||||||
while p.consume?(:pipe)
|
while p.consume?(:pipe)
|
||||||
filtername = p.consume(:id)
|
filtername = p.consume(:id)
|
||||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rigid_parse(markup)
|
||||||
|
@filters = []
|
||||||
|
p = @parse_context.new_parser(markup)
|
||||||
|
|
||||||
|
return if p.look(:end_of_string)
|
||||||
|
|
||||||
|
@name = parse_context.safe_parse_expression(p)
|
||||||
|
@filters << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
|
||||||
|
p.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
def parse_filterargs(p)
|
def parse_filterargs(p)
|
||||||
# first argument
|
# first argument
|
||||||
filterargs = [p.argument]
|
filterargs = [p.argument]
|
||||||
@@ -122,7 +133,7 @@ module Liquid
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def parse_filter_expressions(filter_name, unparsed_args)
|
def lax_parse_filter_expressions(filter_name, unparsed_args)
|
||||||
filter_args = []
|
filter_args = []
|
||||||
keyword_args = nil
|
keyword_args = nil
|
||||||
unparsed_args.each do |a|
|
unparsed_args.each do |a|
|
||||||
@@ -138,6 +149,46 @@ module Liquid
|
|||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Surprisingly, positional and keyword arguments can be mixed.
|
||||||
|
#
|
||||||
|
# filter = filtername [":" filterargs?]
|
||||||
|
# filterargs = argument ("," argument)*
|
||||||
|
# argument = (positional_argument | keyword_argument)
|
||||||
|
# positional_argument = expression
|
||||||
|
# keyword_argument = id ":" expression
|
||||||
|
def rigid_parse_filter_expressions(p)
|
||||||
|
filtername = p.consume(:id)
|
||||||
|
filter_args = []
|
||||||
|
keyword_args = {}
|
||||||
|
|
||||||
|
if p.consume?(:colon)
|
||||||
|
# Parse first argument (no leading comma)
|
||||||
|
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
|
||||||
|
|
||||||
|
# Parse remaining arguments (with leading commas) and optional trailing comma
|
||||||
|
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
|
||||||
|
end
|
||||||
|
|
||||||
|
result = [filtername, filter_args]
|
||||||
|
result << keyword_args unless keyword_args.empty?
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def argument(p, positional_arguments, keyword_arguments)
|
||||||
|
if p.look(:id) && p.look(:colon, 1)
|
||||||
|
key = p.consume(:id)
|
||||||
|
p.consume(:colon)
|
||||||
|
value = parse_context.safe_parse_expression(p)
|
||||||
|
keyword_arguments[key] = value
|
||||||
|
else
|
||||||
|
positional_arguments << parse_context.safe_parse_expression(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def end_of_arguments?(p)
|
||||||
|
p.look(:pipe) || p.look(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
||||||
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
||||||
if filter_kwargs
|
if filter_kwargs
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "5.7.2"
|
VERSION = "5.10.0"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ class ContextTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
def test_has_key_will_not_add_an_error_for_missing_keys
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
context = Context.new
|
context = Context.new
|
||||||
context.key?('unknown')
|
context.key?('unknown')
|
||||||
assert_empty(context.errors)
|
assert_empty(context.errors)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ErrorHandlingTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_unrecognized_operator
|
def test_unrecognized_operator
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(SyntaxError) do
|
assert_raises(SyntaxError) do
|
||||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ class ExpressionTest < Minitest::Test
|
|||||||
def test_float
|
def test_float
|
||||||
assert_template_result("-17.42", "{{ -17.42 }}")
|
assert_template_result("-17.42", "{{ -17.42 }}")
|
||||||
assert_template_result("2.5", "{{ 2.5 }}")
|
assert_template_result("2.5", "{{ 2.5 }}")
|
||||||
assert_expression_result(0.0, "0.....5")
|
|
||||||
assert_expression_result(0.0, "-0..1")
|
with_error_modes(:lax) do
|
||||||
|
assert_expression_result(0.0, "0.....5")
|
||||||
|
assert_expression_result(0.0, "-0..1")
|
||||||
|
end
|
||||||
|
|
||||||
assert_expression_result(1.5, "1.5")
|
assert_expression_result(1.5, "1.5")
|
||||||
|
|
||||||
# this is a unfortunate quirky behavior of Liquid
|
# this is a unfortunate quirky behavior of Liquid
|
||||||
@@ -61,6 +65,7 @@ class ExpressionTest < Minitest::Test
|
|||||||
assert_template_result(
|
assert_template_result(
|
||||||
"",
|
"",
|
||||||
"{{ - 'theme.css' - }}",
|
"{{ - 'theme.css' - }}",
|
||||||
|
error_mode: :lax,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -147,6 +152,35 @@ class ExpressionTest < Minitest::Test
|
|||||||
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_safe_parse_with_variable_lookup
|
||||||
|
parse_context = Liquid::ParseContext.new
|
||||||
|
parser = parse_context.new_parser('product.title')
|
||||||
|
result = Liquid::Expression.safe_parse(parser)
|
||||||
|
|
||||||
|
assert_instance_of(Liquid::VariableLookup, result)
|
||||||
|
assert_equal('product', result.name)
|
||||||
|
assert_equal(['title'], result.lookups)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_safe_parse_with_number
|
||||||
|
parse_context = Liquid::ParseContext.new
|
||||||
|
parser = parse_context.new_parser('42')
|
||||||
|
result = Liquid::Expression.safe_parse(parser)
|
||||||
|
|
||||||
|
assert_equal(42, result)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_safe_parse_raises_syntax_error_for_invalid_expression
|
||||||
|
parse_context = Liquid::ParseContext.new
|
||||||
|
parser = parse_context.new_parser('')
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::SyntaxError) do
|
||||||
|
Liquid::Expression.safe_parse(parser)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assert_expression_result(expect, markup, **assigns)
|
def assert_expression_result(expect, markup, **assigns)
|
||||||
|
|||||||
@@ -31,18 +31,18 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
def test_error_on_empty_filter
|
def test_error_on_empty_filter
|
||||||
assert(Template.parse("{{test}}"))
|
assert(Template.parse("{{test}}"))
|
||||||
|
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert(Template.parse("{{|test}}"))
|
assert(Template.parse("{{|test}}"))
|
||||||
end
|
end
|
||||||
|
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
|
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
|
||||||
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
|
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_meaningless_parens_error
|
def test_meaningless_parens_error
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(SyntaxError) do
|
assert_raises(SyntaxError) do
|
||||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||||
@@ -51,7 +51,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_unexpected_characters_syntax_error
|
def test_unexpected_characters_syntax_error
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(SyntaxError) do
|
assert_raises(SyntaxError) do
|
||||||
markup = "true && false"
|
markup = "true && false"
|
||||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||||
@@ -70,7 +70,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_meaningless_parens_lax
|
def test_meaningless_parens_lax
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||||
@@ -78,7 +78,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_unexpected_characters_silently_eat_logic_lax
|
def test_unexpected_characters_silently_eat_logic_lax
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
markup = "true && false"
|
markup = "true && false"
|
||||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
|
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
|
||||||
markup = "false || true"
|
markup = "false || true"
|
||||||
@@ -93,7 +93,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_unanchored_filter_arguments
|
def test_unanchored_filter_arguments
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
|
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
|
||||||
|
|
||||||
assert_template_result('x', "{{ 'X' | downcase) }}")
|
assert_template_result('x', "{{ 'X' | downcase) }}")
|
||||||
@@ -106,14 +106,14 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_invalid_variables_work
|
def test_invalid_variables_work
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
|
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
|
||||||
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
|
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_extra_dots_in_ranges
|
def test_extra_dots_in_ranges
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
|
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -133,7 +133,7 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_incomplete_expression
|
def test_incomplete_expression
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_template_result("false", "{{ false - }}")
|
assert_template_result("false", "{{ false - }}")
|
||||||
assert_template_result("false", "{{ false > }}")
|
assert_template_result("false", "{{ false > }}")
|
||||||
assert_template_result("false", "{{ false < }}")
|
assert_template_result("false", "{{ false < }}")
|
||||||
|
|||||||
@@ -560,12 +560,23 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
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 = [
|
foo = [
|
||||||
[1],
|
[1],
|
||||||
[2],
|
[2],
|
||||||
[3],
|
[3],
|
||||||
]
|
]
|
||||||
|
|
||||||
assert_raises(Liquid::ArgumentError) do
|
assert_raises(Liquid::ArgumentError) do
|
||||||
@filters.map(foo, nil)
|
@filters.map(foo, nil)
|
||||||
end
|
end
|
||||||
@@ -1033,6 +1044,23 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_template_result(expected_output, template, { "array" => array })
|
assert_template_result(expected_output, template, { "array" => array })
|
||||||
end
|
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
|
def test_where_with_value
|
||||||
array = [
|
array = [
|
||||||
{ "handle" => "alpha", "ok" => true },
|
{ "handle" => "alpha", "ok" => true },
|
||||||
@@ -1268,6 +1296,33 @@ 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_non_string_property
|
||||||
|
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))
|
||||||
|
assert_equal(-0.3, @filters.sum(input, 1))
|
||||||
|
assert_equal(0.4, @filters.sum(input, (1..5)))
|
||||||
|
assert_equal(0, @filters.sum(input, nil))
|
||||||
|
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
|
private
|
||||||
|
|
||||||
def with_timezone(tz)
|
def with_timezone(tz)
|
||||||
|
|||||||
@@ -3,20 +3,10 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class CycleTagTest < Minitest::Test
|
class CycleTagTest < Minitest::Test
|
||||||
def test_simple_cycle
|
|
||||||
template = <<~LIQUID
|
|
||||||
{%- cycle '1', '2', '3' -%}
|
|
||||||
{%- cycle '1', '2', '3' -%}
|
|
||||||
{%- cycle '1', '2', '3' -%}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
assert_template_result("123", template)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_simple_cycle_inside_for_loop
|
def test_simple_cycle_inside_for_loop
|
||||||
template = <<~LIQUID
|
template = <<~LIQUID
|
||||||
{%- for i in (1..3) -%}
|
{%- for i in (1..3) -%}
|
||||||
{% cycle '1', '2', '3' %}
|
{%- cycle '1', '2', '3' -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
LIQUID
|
LIQUID
|
||||||
|
|
||||||
@@ -36,13 +26,157 @@ class CycleTagTest < Minitest::Test
|
|||||||
assert_template_result("123", template)
|
assert_template_result("123", template)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cycle_tag_always_resets_cycle
|
def test_cycle_named_groups_string
|
||||||
template = <<~LIQUID
|
template = <<~LIQUID
|
||||||
{%- assign a = "1" -%}
|
{%- for i in (1..3) -%}
|
||||||
{%- cycle a, "2" -%}
|
{%- cycle 'placeholder1': 1, 2, 3 -%}
|
||||||
{%- cycle a, "2" -%}
|
{%- cycle 'placeholder2': 1, 2, 3 -%}
|
||||||
|
{%- endfor -%}
|
||||||
LIQUID
|
LIQUID
|
||||||
|
|
||||||
assert_template_result("11", template)
|
assert_template_result("112233", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_named_groups_vlookup
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- assign placeholder1 = 'placeholder1' -%}
|
||||||
|
{%- assign placeholder2 = 'placeholder2' -%}
|
||||||
|
{%- for i in (1..3) -%}
|
||||||
|
{%- cycle placeholder1: 1, 2, 3 -%}
|
||||||
|
{%- cycle placeholder2: 1, 2, 3 -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result("112233", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unnamed_cycle_have_independent_counters_when_used_with_lookups
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- assign a = "1" -%}
|
||||||
|
{%- for i in (1..3) -%}
|
||||||
|
{%- cycle a, "2" -%}
|
||||||
|
{%- cycle a, "2" -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result("112211", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unnamed_cycle_dependent_counter_when_used_with_literal_values
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- cycle "1", "2" -%}
|
||||||
|
{%- cycle "1", "2" -%}
|
||||||
|
{%- cycle "1", "2" -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result("121", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_optional_trailing_comma
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- cycle "1", "2", -%}
|
||||||
|
{%- cycle "1", "2", -%}
|
||||||
|
{%- cycle "1", "2", -%}
|
||||||
|
{%- cycle "1", -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
assert_template_result("1211", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_tag_without_arguments
|
||||||
|
error = assert_raises(Liquid::SyntaxError) do
|
||||||
|
Template.parse("{% cycle %}")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/Syntax Error in 'cycle' - Valid syntax: cycle \[name :\] var/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_tag_with_error_mode
|
||||||
|
# QuotedFragment is more permissive than what Parser#expression allows.
|
||||||
|
template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
|
||||||
|
template2 = "{% cycle .5: 'a', 'b' %}"
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("b", template1)
|
||||||
|
assert_template_result("a", template2)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||||
|
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||||
|
|
||||||
|
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
|
||||||
|
|
||||||
|
assert_match(expected_error, error1.message)
|
||||||
|
assert_match(expected_error, error2.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_with_trailing_elements
|
||||||
|
assignments = "{% assign a = 'A' %}{% assign n = 'N' %}"
|
||||||
|
|
||||||
|
template1 = "#{assignments}{% cycle 'a' 'b', 'c' %}"
|
||||||
|
template2 = "#{assignments}{% cycle name: 'a' 'b', 'c' %}"
|
||||||
|
template3 = "#{assignments}{% cycle name: 'a', 'b' 'c' %}"
|
||||||
|
template4 = "#{assignments}{% cycle n e: 'a', 'b', 'c' %}"
|
||||||
|
template5 = "#{assignments}{% cycle n e 'a', 'b', 'c' %}"
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("a", template1)
|
||||||
|
assert_template_result("a", template2)
|
||||||
|
assert_template_result("a", template3)
|
||||||
|
assert_template_result("N", template4)
|
||||||
|
assert_template_result("N", template5)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||||
|
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||||
|
error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
|
||||||
|
error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
|
||||||
|
error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
|
||||||
|
|
||||||
|
expected_error = /Expected end_of_string but found/
|
||||||
|
|
||||||
|
assert_match(expected_error, error1.message)
|
||||||
|
assert_match(expected_error, error2.message)
|
||||||
|
assert_match(expected_error, error3.message)
|
||||||
|
assert_match(expected_error, error4.message)
|
||||||
|
assert_match(expected_error, error5.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_name_with_invalid_expression
|
||||||
|
template = <<~LIQUID
|
||||||
|
{% for i in (1..3) %}
|
||||||
|
{% cycle foo=>bar: "a", "b" %}
|
||||||
|
{% endfor %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle_variable_with_invalid_expression
|
||||||
|
template = <<~LIQUID
|
||||||
|
{% for i in (1..3) %}
|
||||||
|
{% cycle foo=>bar, "a", "b" %}
|
||||||
|
{% endfor %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -204,6 +204,32 @@ class IncludeTagTest < Minitest::Test
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_rigid_parsing_errors
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result(
|
||||||
|
'hello value1 value2',
|
||||||
|
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||||
|
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_syntax_error(
|
||||||
|
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||||
|
)
|
||||||
|
assert_syntax_error(
|
||||||
|
'{% include "snippet" | filter %}',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_optional_commas
|
||||||
|
partials = { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' }
|
||||||
|
assert_template_result('hello value1 value2', '{% include "snippet", arg1: "value1", arg2: "value2" %}', partials: partials)
|
||||||
|
assert_template_result('hello value1 value2', '{% include "snippet" arg1: "value1", arg2: "value2" %}', partials: partials)
|
||||||
|
assert_template_result('hello value1 value2', '{% include "snippet" arg1: "value1" arg2: "value2" %}', partials: partials)
|
||||||
|
end
|
||||||
|
|
||||||
def test_include_tag_caches_second_read_of_same_partial
|
def test_include_tag_caches_second_read_of_same_partial
|
||||||
file_system = CountingFileSystem.new
|
file_system = CountingFileSystem.new
|
||||||
environment = Liquid::Environment.build(file_system: file_system)
|
environment = Liquid::Environment.build(file_system: file_system)
|
||||||
@@ -277,13 +303,13 @@ class IncludeTagTest < Minitest::Test
|
|||||||
assert_raises(Liquid::SyntaxError) do
|
assert_raises(Liquid::SyntaxError) do
|
||||||
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
|
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||||
end
|
end
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
|
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||||
end
|
end
|
||||||
assert_raises(Liquid::SyntaxError) do
|
assert_raises(Liquid::SyntaxError) do
|
||||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||||
end
|
end
|
||||||
with_error_mode(:lax) do
|
with_error_modes(:lax) do
|
||||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
|
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -374,4 +400,43 @@ class IncludeTagTest < Minitest::Test
|
|||||||
render_errors: true,
|
render_errors: true,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_include_template_with_invalid_expression
|
||||||
|
template = "{% include foo=>bar %}"
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include_with_invalid_expression
|
||||||
|
template = '{% include "snippet" with foo=>bar %}'
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include_attribute_with_invalid_expression
|
||||||
|
template = '{% include "snippet", key: foo=>bar %}'
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end # IncludeTagTest
|
end # IncludeTagTest
|
||||||
|
|||||||
@@ -101,11 +101,33 @@ class RenderTagTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_dynamically_choosen_templates_are_not_allowed
|
def test_rigid_parsing_errors
|
||||||
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result(
|
||||||
|
'hello value1 value2',
|
||||||
|
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||||
|
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_syntax_error(
|
||||||
|
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||||
|
)
|
||||||
|
assert_syntax_error(
|
||||||
|
'{% render "snippet" | filter %}',
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_include_tag_caches_second_read_of_same_partial
|
def test_optional_commas
|
||||||
|
partials = { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' }
|
||||||
|
assert_template_result('hello value1 value2', '{% render "snippet", arg1: "value1", arg2: "value2" %}', partials: partials)
|
||||||
|
assert_template_result('hello value1 value2', '{% render "snippet" arg1: "value1", arg2: "value2" %}', partials: partials)
|
||||||
|
assert_template_result('hello value1 value2', '{% render "snippet" arg1: "value1" arg2: "value2" %}', partials: partials)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_tag_caches_second_read_of_same_partial
|
||||||
file_system = StubFileSystem.new('snippet' => 'echo')
|
file_system = StubFileSystem.new('snippet' => 'echo')
|
||||||
assert_equal(
|
assert_equal(
|
||||||
'echoecho',
|
'echoecho',
|
||||||
@@ -268,6 +290,13 @@ class RenderTagTest < Minitest::Test
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_render_tag_with_snippet_drop
|
||||||
|
assert_template_result(
|
||||||
|
"Hello from snippet",
|
||||||
|
"{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def test_render_tag_renders_error_with_template_name
|
def test_render_tag_renders_error_with_template_name
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
'Liquid error (foo line 1): standard error',
|
'Liquid error (foo line 1): standard error',
|
||||||
@@ -288,4 +317,30 @@ class RenderTagTest < Minitest::Test
|
|||||||
render_errors: true,
|
render_errors: true,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_render_with_invalid_expression
|
||||||
|
template = '{% render "snippet" with foo=>bar %}'
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_attribute_with_invalid_expression
|
||||||
|
template = '{% render "snippet", key: foo=>bar %}'
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
refute_nil(Template.parse(template))
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -138,7 +138,7 @@ class TableRowTest < Minitest::Test
|
|||||||
|
|
||||||
def test_tablerow_loop_drop_attributes
|
def test_tablerow_loop_drop_attributes
|
||||||
template = <<~LIQUID.chomp
|
template = <<~LIQUID.chomp
|
||||||
{% tablerow i in (1...2) %}
|
{% tablerow i in (1..2) %}
|
||||||
col: {{ tablerowloop.col }}
|
col: {{ tablerowloop.col }}
|
||||||
col0: {{ tablerowloop.col0 }}
|
col0: {{ tablerowloop.col0 }}
|
||||||
col_first: {{ tablerowloop.col_first }}
|
col_first: {{ tablerowloop.col_first }}
|
||||||
@@ -192,12 +192,14 @@ class TableRowTest < Minitest::Test
|
|||||||
assert_template_result(
|
assert_template_result(
|
||||||
"Liquid error (line 1): invalid integer",
|
"Liquid error (line 1): invalid integer",
|
||||||
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
|
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
|
||||||
|
error_mode: :warn,
|
||||||
render_errors: true,
|
render_errors: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
"Liquid error (line 1): invalid integer",
|
"Liquid error (line 1): invalid integer",
|
||||||
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
|
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
|
||||||
|
error_mode: :warn,
|
||||||
render_errors: true,
|
render_errors: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -205,18 +207,19 @@ class TableRowTest < Minitest::Test
|
|||||||
"Liquid error (line 1): invalid integer",
|
"Liquid error (line 1): invalid integer",
|
||||||
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
|
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
|
||||||
render_errors: true,
|
render_errors: true,
|
||||||
|
error_mode: :warn,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_table_row_handles_interrupts
|
def test_table_row_handles_interrupts
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
|
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
|
||||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
|
'{% tablerow n in (1..3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
|
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
|
||||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
|
'{% tablerow n in (1..3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -255,4 +258,211 @@ class TableRowTest < Minitest::Test
|
|||||||
template,
|
template,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_cols_attribute_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||||
|
<tr class="row2"><td class="col1">4</td><td class="col2">5</td><td class="col3">6</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_limit_attribute_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_offset_attribute_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_range_attribute_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_multiple_attributes_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">2</td><td class="col2">3</td></tr>
|
||||||
|
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_variable_collection_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td></tr>
|
||||||
|
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_dotted_access_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td></tr>
|
||||||
|
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_bracketed_access_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">10</td><td class="col2">20</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_without_attributes_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_without_in_keyword_in_rigid_mode
|
||||||
|
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||||
|
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_rigid_mode
|
||||||
|
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||||
|
assert_equal("Liquid syntax error: Invalid attribute 'invalid1' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid1: 5, invalid2: 10\"", error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_empty_collection_in_rigid_mode
|
||||||
|
template = <<~LIQUID.chomp
|
||||||
|
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
</tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result(expected, template, { 'empty_array' => [] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_invalid_attribute_strict_vs_rigid
|
||||||
|
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
|
||||||
|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td><td class="col4">4</td><td class="col5">5</td></tr>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_with_invalid_expression_strict_vs_rigid
|
||||||
|
template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
<tr class="row1">
|
||||||
|
</tr>
|
||||||
|
OUTPUT
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -209,4 +209,69 @@ class VariableTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_filter_with_single_trailing_comma
|
||||||
|
template = '{{ "hello" | append: "world", }}'
|
||||||
|
|
||||||
|
with_error_modes(:strict) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result('helloworld', template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multiple_filters_with_trailing_commas
|
||||||
|
template = '{{ "hello" | append: "1", | append: "2", }}'
|
||||||
|
|
||||||
|
with_error_modes(:strict) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result('hello12', template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_filter_with_colon_but_no_arguments
|
||||||
|
template = '{{ "test" | upcase: }}'
|
||||||
|
|
||||||
|
with_error_modes(:strict) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result('TEST', template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_filter_chain_with_colon_no_args
|
||||||
|
template = '{{ "test" | append: "x" | upcase: }}'
|
||||||
|
|
||||||
|
with_error_modes(:strict) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result('TESTX', template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_combining_trailing_comma_and_empty_args
|
||||||
|
template = '{{ "test" | append: "x", | upcase: }}'
|
||||||
|
|
||||||
|
with_error_modes(:strict) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
assert_match(/is not a valid expression/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
assert_template_result('TESTX', template)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+35
-4
@@ -34,7 +34,7 @@ module Minitest
|
|||||||
|
|
||||||
def assert_template_result(
|
def assert_template_result(
|
||||||
expected, template, assigns = {},
|
expected, template, assigns = {},
|
||||||
message: nil, partials: nil, error_mode: nil, render_errors: false,
|
message: nil, partials: nil, error_mode: Liquid::Environment.default.error_mode, render_errors: false,
|
||||||
template_factory: nil
|
template_factory: nil
|
||||||
)
|
)
|
||||||
file_system = StubFileSystem.new(partials || {})
|
file_system = StubFileSystem.new(partials || {})
|
||||||
@@ -82,10 +82,12 @@ module Minitest
|
|||||||
Environment.dangerously_override(environment, &blk)
|
Environment.dangerously_override(environment, &blk)
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_error_mode(mode)
|
def with_error_modes(*modes)
|
||||||
old_mode = Liquid::Environment.default.error_mode
|
old_mode = Liquid::Environment.default.error_mode
|
||||||
Liquid::Environment.default.error_mode = mode
|
modes.each do |mode|
|
||||||
yield
|
Liquid::Environment.default.error_mode = mode
|
||||||
|
yield
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
Liquid::Environment.default.error_mode = old_mode
|
Liquid::Environment.default.error_mode = old_mode
|
||||||
end
|
end
|
||||||
@@ -146,6 +148,35 @@ class BooleanDrop < Liquid::Drop
|
|||||||
end
|
end
|
||||||
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
|
class ErrorDrop < Liquid::Drop
|
||||||
def standard_error
|
def standard_error
|
||||||
raise Liquid::StandardError, 'standard error'
|
raise Liquid::StandardError, 'standard error'
|
||||||
|
|||||||
@@ -166,6 +166,37 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
assert_includes(err.lines.map(&:strip), expected)
|
assert_includes(err.lines.map(&:strip), expected)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_in_strict_mode
|
||||||
|
environment = Environment.build(error_mode: :strict)
|
||||||
|
parse_context = ParseContext.new(environment: environment)
|
||||||
|
result = Condition.parse_expression(parse_context, 'product.title')
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result)
|
||||||
|
assert_equal('product', result.name)
|
||||||
|
assert_equal(['title'], result.lookups)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_in_rigid_mode_raises_internal_error
|
||||||
|
environment = Environment.build(error_mode: :rigid)
|
||||||
|
parse_context = ParseContext.new(environment: environment)
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::InternalError) do
|
||||||
|
Condition.parse_expression(parse_context, 'product.title')
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_safe_true_in_rigid_mode
|
||||||
|
environment = Environment.build(error_mode: :rigid)
|
||||||
|
parse_context = ParseContext.new(environment: environment)
|
||||||
|
result = Condition.parse_expression(parse_context, 'product.title', safe: true)
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result)
|
||||||
|
assert_equal('product', result.name)
|
||||||
|
assert_equal(['title'], result.lookups)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assert_evaluates_true(left, op, right)
|
def assert_evaluates_true(left, op, right)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,123 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class ParseContextUnitTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_safe_parse_expression_with_variable_lookup
|
||||||
|
parser_strict = strict_parse_context.new_parser('product.title')
|
||||||
|
result_strict = strict_parse_context.safe_parse_expression(parser_strict)
|
||||||
|
|
||||||
|
parser_rigid = rigid_parse_context.new_parser('product.title')
|
||||||
|
result_rigid = rigid_parse_context.safe_parse_expression(parser_rigid)
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result_strict)
|
||||||
|
assert_equal('product', result_strict.name)
|
||||||
|
assert_equal(['title'], result_strict.lookups)
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result_rigid)
|
||||||
|
assert_equal('product', result_rigid.name)
|
||||||
|
assert_equal(['title'], result_rigid.lookups)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_safe_parse_expression_raises_syntax_error_for_invalid_expression
|
||||||
|
parser_strict = strict_parse_context.new_parser('')
|
||||||
|
parser_rigid = rigid_parse_context.new_parser('')
|
||||||
|
|
||||||
|
error_strict = assert_raises(Liquid::SyntaxError) do
|
||||||
|
strict_parse_context.safe_parse_expression(parser_strict)
|
||||||
|
end
|
||||||
|
assert_match(/is not a valid expression/, error_strict.message)
|
||||||
|
|
||||||
|
error_rigid = assert_raises(Liquid::SyntaxError) do
|
||||||
|
rigid_parse_context.safe_parse_expression(parser_rigid)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/is not a valid expression/, error_rigid.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_variable_lookup
|
||||||
|
result_strict = strict_parse_context.parse_expression('product.title')
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result_strict)
|
||||||
|
assert_equal('product', result_strict.name)
|
||||||
|
assert_equal(['title'], result_strict.lookups)
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::InternalError) do
|
||||||
|
rigid_parse_context.parse_expression('product.title')
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_safe_true
|
||||||
|
result_strict = strict_parse_context.parse_expression('product.title', safe: true)
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result_strict)
|
||||||
|
assert_equal('product', result_strict.name)
|
||||||
|
assert_equal(['title'], result_strict.lookups)
|
||||||
|
|
||||||
|
result_rigid = rigid_parse_context.parse_expression('product.title', safe: true)
|
||||||
|
|
||||||
|
assert_instance_of(VariableLookup, result_rigid)
|
||||||
|
assert_equal('product', result_rigid.name)
|
||||||
|
assert_equal(['title'], result_rigid.lookups)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_empty_string
|
||||||
|
result_strict = strict_parse_context.parse_expression('')
|
||||||
|
assert_nil(result_strict)
|
||||||
|
|
||||||
|
error = assert_raises(Liquid::InternalError) do
|
||||||
|
rigid_parse_context.parse_expression('')
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_empty_string_and_safe_true
|
||||||
|
result_strict = strict_parse_context.parse_expression('', safe: true)
|
||||||
|
assert_nil(result_strict)
|
||||||
|
|
||||||
|
result_rigid = rigid_parse_context.parse_expression('', safe: true)
|
||||||
|
assert_nil(result_rigid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_safe_parse_expression_advances_parser_pointer
|
||||||
|
parser = rigid_parse_context.new_parser('foo, bar')
|
||||||
|
|
||||||
|
# safe_parse_expression consumes "foo"
|
||||||
|
first_result = rigid_parse_context.safe_parse_expression(parser)
|
||||||
|
assert_instance_of(VariableLookup, first_result)
|
||||||
|
assert_equal('foo', first_result.name)
|
||||||
|
|
||||||
|
parser.consume(:comma)
|
||||||
|
|
||||||
|
# safe_parse_expression consumes "bar"
|
||||||
|
second_result = rigid_parse_context.safe_parse_expression(parser)
|
||||||
|
assert_instance_of(VariableLookup, second_result)
|
||||||
|
assert_equal('bar', second_result.name)
|
||||||
|
|
||||||
|
parser.consume(:end_of_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_expression_with_whitespace_in_rigid_mode
|
||||||
|
result = rigid_parse_context.parse_expression(' ', safe: true)
|
||||||
|
assert_nil(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def strict_parse_context
|
||||||
|
@strict_parse_context ||= ParseContext.new(
|
||||||
|
environment: Environment.build(error_mode: :strict),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rigid_parse_context
|
||||||
|
@rigid_parse_context ||= ParseContext.new(
|
||||||
|
environment: Environment.build(error_mode: :rigid),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -184,7 +184,7 @@ class PartialCacheUnitTest < Minitest::Test
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
[:lax, :warn, :strict].each do |error_mode|
|
[:lax, :warn, :strict, :rigid].each do |error_mode|
|
||||||
Liquid::PartialCache.load(
|
Liquid::PartialCache.load(
|
||||||
'my_partial',
|
'my_partial',
|
||||||
context: context,
|
context: context,
|
||||||
@@ -193,7 +193,7 @@ class PartialCacheUnitTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
assert_equal(
|
assert_equal(
|
||||||
["my_partial:lax", "my_partial:warn", "my_partial:strict"],
|
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:rigid"],
|
||||||
context.registers[:cached_partials].keys,
|
context.registers[:cached_partials].keys,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,4 +9,120 @@ class CaseTagUnitTest < Minitest::Test
|
|||||||
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
|
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
|
||||||
assert_equal(['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten)
|
assert_equal(['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_case_with_trailing_element
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case 1 bar -%}
|
||||||
|
{%- when 1 -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("one", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
|
||||||
|
assert_match(/Expected end_of_string but found/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_when_with_trailing_element
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case 1 -%}
|
||||||
|
{%- when 1 bar -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("one", template)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
|
||||||
|
assert_match(/Expected end_of_string but found/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_when_with_comma
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case 1 -%}
|
||||||
|
{%- when 2, 1 -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict, :rigid) do
|
||||||
|
assert_template_result("one", template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_when_with_or
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case 1 -%}
|
||||||
|
{%- when 2 or 1 -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict, :rigid) do
|
||||||
|
assert_template_result("one", template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_with_invalid_expression
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case foo=>bar -%}
|
||||||
|
{%- when 'baz' -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
assigns = { 'foo' => { 'bar' => 'baz' } }
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("one", template, assigns)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_when_with_invalid_expression
|
||||||
|
template = <<~LIQUID
|
||||||
|
{%- case 'baz' -%}
|
||||||
|
{%- when foo=>bar -%}
|
||||||
|
one
|
||||||
|
{%- else -%}
|
||||||
|
two
|
||||||
|
{%- endcase -%}
|
||||||
|
LIQUID
|
||||||
|
assigns = { 'foo' => { 'bar' => 'baz' } }
|
||||||
|
|
||||||
|
with_error_modes(:lax, :strict) do
|
||||||
|
assert_template_result("one", template, assigns)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||||
|
|
||||||
|
assert_match(/Unexpected character =/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ class DocTagUnitTest < Minitest::Test
|
|||||||
assert_template_result('', template)
|
assert_template_result('', template)
|
||||||
end
|
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
|
def test_doc_tag_does_not_support_extra_arguments
|
||||||
error = assert_raises(Liquid::SyntaxError) do
|
error = assert_raises(Liquid::SyntaxError) do
|
||||||
template = <<~LIQUID.chomp
|
template = <<~LIQUID.chomp
|
||||||
@@ -116,6 +131,20 @@ class DocTagUnitTest < Minitest::Test
|
|||||||
assert_template_result('', template)
|
assert_template_result('', template)
|
||||||
end
|
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
|
def test_doc_tag_preserves_error_line_numbers
|
||||||
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
|
||||||
{% doc %}
|
{% doc %}
|
||||||
@@ -145,11 +174,11 @@ class DocTagUnitTest < Minitest::Test
|
|||||||
|
|
||||||
def test_doc_tag_delimiter_handling
|
def test_doc_tag_delimiter_handling
|
||||||
assert_template_result('', <<~LIQUID.chomp)
|
assert_template_result('', <<~LIQUID.chomp)
|
||||||
{% if true %}
|
{%- if true -%}
|
||||||
{% doc %}
|
{%- doc -%}
|
||||||
{% docEXTRA %}wut{% enddocEXTRA %}xyz
|
{%- docEXTRA -%}wut{% enddocEXTRA -%}xyz
|
||||||
{% enddoc %}
|
{%- enddoc -%}
|
||||||
{% endif %}
|
{%- endif -%}
|
||||||
LIQUID
|
LIQUID
|
||||||
|
|
||||||
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
|
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
|
||||||
@@ -157,4 +186,102 @@ class DocTagUnitTest < Minitest::Test
|
|||||||
assert_template_result('', "{% doc %}123{% enddoc\nxyz %}")
|
assert_template_result('', "{% doc %}123{% enddoc\nxyz %}")
|
||||||
assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}")
|
assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_doc_tag_visitor
|
||||||
|
template_source = '{% doc %}{% enddoc %}'
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
[Liquid::Doc],
|
||||||
|
visit(template_source),
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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
|
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
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class VariableUnitTest < Minitest::Test
|
|||||||
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
|
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
|
||||||
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
|
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
|
||||||
|
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
|
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
|
||||||
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
|
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
|
||||||
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
|
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
|
||||||
@@ -135,16 +135,68 @@ class VariableUnitTest < Minitest::Test
|
|||||||
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
|
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
|
||||||
assert_equal(VariableLookup.new('number_of_comments'), var.name)
|
assert_equal(VariableLookup.new('number_of_comments'), var.name)
|
||||||
assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
|
assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
|
||||||
|
|
||||||
|
# missing does not throws error
|
||||||
|
create_variable(%(n | f1: ,), error_mode: :lax)
|
||||||
|
create_variable(%(n | f1: ,| f2), error_mode: :lax)
|
||||||
|
|
||||||
|
# arg does not require colon, but ignores args :O, also ignores first kwarg since it splits on ':'
|
||||||
|
var = create_variable(%(n | f1 1 | f2 k1: v1), error_mode: :lax)
|
||||||
|
assert_equal([['f1', []], ['f2', [VariableLookup.new('v1')]]], var.filters)
|
||||||
|
|
||||||
|
# positional and kwargs parsing
|
||||||
|
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2), error_mode: :lax)
|
||||||
|
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
|
||||||
|
|
||||||
|
# positional and kwargs intermixed (pos1, key1: val1, pos2)
|
||||||
|
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"), error_mode: :lax)
|
||||||
|
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strict_filter_argument_parsing
|
def test_strict_filter_argument_parsing
|
||||||
with_error_mode(:strict) do
|
with_error_modes(:strict) do
|
||||||
assert_raises(SyntaxError) do
|
assert_raises(SyntaxError) do
|
||||||
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
|
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_rigid_filter_argument_parsing
|
||||||
|
with_error_modes(:rigid) do
|
||||||
|
# optional colon
|
||||||
|
var = create_variable(%(n | f1 | f2:))
|
||||||
|
assert_equal([['f1', []], ['f2', []]], var.filters)
|
||||||
|
|
||||||
|
# missing argument throws error
|
||||||
|
assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
|
||||||
|
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2)) }
|
||||||
|
|
||||||
|
# arg requires colon
|
||||||
|
assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
|
||||||
|
|
||||||
|
# trailing comma doesn't throw
|
||||||
|
create_variable(%(n | f1: 1, 2, 3, | f2:))
|
||||||
|
|
||||||
|
# missing comma throws error
|
||||||
|
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3)) }
|
||||||
|
|
||||||
|
# positional and kwargs parsing
|
||||||
|
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2))
|
||||||
|
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
|
||||||
|
|
||||||
|
# positional and kwargs mixed
|
||||||
|
var = create_variable(%(n | filter: 'a', 'b', key1: 1, key2: 2, 'c'))
|
||||||
|
assert_equal([["filter", ["a", "b", "c"], { "key1" => 1, "key2" => 2 }]], var.filters)
|
||||||
|
|
||||||
|
# positional and kwargs intermixed (pos1, key1: val1, pos2)
|
||||||
|
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"))
|
||||||
|
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
|
||||||
|
|
||||||
|
# string key throws
|
||||||
|
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_output_raw_source_of_variable
|
def test_output_raw_source_of_variable
|
||||||
var = create_variable(%( name_of_variable | upcase ))
|
var = create_variable(%( name_of_variable | upcase ))
|
||||||
assert_equal(" name_of_variable | upcase ", var.raw)
|
assert_equal(" name_of_variable | upcase ", var.raw)
|
||||||
|
|||||||
Reference in New Issue
Block a user