Compare commits

..
Author SHA1 Message Date
Michael Go 369789ed2f WIP: more careful IF block merger 2024-12-12 16:00:50 -04:00
Michael Go 6a86fcc411 WIP: Loom 2024-12-11 17:54:00 -04:00
80 changed files with 854 additions and 4486 deletions
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
+5 -21
View File
@@ -13,40 +13,24 @@ jobs:
entry:
- { ruby: 3.0, allowed-failure: false } # minimum supported
- { ruby: 3.2, 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,
rubyopt: "--enable-frozen-string-literal",
}
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: 3.3, allowed-failure: false } # latest
- { 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 }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
bundler: latest
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
env:
RUBYOPT: ${{ matrix.entry.rubyopt }}
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec rake memory_profile:run
+1 -1
View File
@@ -1 +1 @@
3.4.1
3.3.4
-8
View File
@@ -26,11 +26,3 @@
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
## Releasing
* Bump the version in `lib/liquid/version.rb`
* Update the `History.md` file
* Open a PR like [this one](https://github.com/Shopify/liquid/pull/1894) and merge it to `main`
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
-5
View File
@@ -13,17 +13,12 @@ group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
gem 'terminal-table'
gem "lru_redux"
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof'
end
end
group :development do
gem "webrick"
end
group :test do
gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
-84
View File
@@ -1,89 +1,5 @@
# Liquid Change Log
## 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
* Fix array filters to not support nested properties [Guilherme Carreiro]
## 5.7.1 2025-01-24
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays [Guilherme Carreiro]
* Fix the `has` filter to return `false` when filtering empty arrays [Guilherme Carreiro]
## 5.7.0 2025-01-16
### Features
* Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
* Compatibility with Ruby 3.4 [Ian Ker-Seymer]
## 5.6.4 2025-01-14
### Fixes
* Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
## 5.6.3 2025-01-13
* Remove `lru_redux` dependency [Michael Go]
## 5.6.2 2025-01-13
### Fixes
* Preserve the old behavior of requiring floats to start with a digit [Michael Go]
## 5.6.1 2025-01-13
### Performance improvements
* Faster Expression parser / Tokenizer with StringScanner [Michael Go]
## 5.6.0 2024-12-19
### Architectural changes
* Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
* Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
* Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
* Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
### Performance improvements
* Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
* Improved variable rendering with specialized handling for different types [Michael Go]
* Reduced array allocations by using frozen empty constants [Michael Go]
### API changes
* Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
* Added deprecation warnings system [Ian Ker-Seymer]
* Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
### Fixes
* Fixed table row handling of break interrupts [Alex Coco]
* Improved variable output handling for arrays [Ian Ker-Seymer]
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
+6 -6
View File
@@ -91,7 +91,7 @@ Liquid::Template.parse(<<~LIQUID, environment: email_environment)
LIQUID
```
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
### Error Modes
@@ -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
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with different parsers that can be used when editing templates to give better error messages
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
```ruby
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 in some tags
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 = :strict
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
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.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
+8 -29
View File
@@ -33,7 +33,7 @@ task :rubocop do
end
end
desc('runs test suite with lax, strict, and rigid parsers')
desc('runs test suite with both strict and lax parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
@@ -42,10 +42,6 @@ task :test do
Rake::Task['base_test'].reenable
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'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
@@ -54,10 +50,6 @@ task :test do
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'rigid'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
end
@@ -79,7 +71,7 @@ end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :lax do
task :run do
ruby "./performance/benchmark.rb lax"
end
@@ -88,16 +80,9 @@ namespace :benchmark do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with rigid parsing"
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"
namespace :unit do
desc "Run all unit benchmarks"
task :all do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
@@ -105,17 +90,11 @@ namespace :benchmark do
end
end
task :lexer do
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
task :expression do
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
%w[lexer loom].each do |benchmark|
desc "Run the #{benchmark} benchmark"
task benchmark.to_sym do
puts "🧪 Running #{benchmark}"
ruby "./performance/unit/#{benchmark}_benchmark.rb"
end
end
end
-46
View File
@@ -1,46 +0,0 @@
#!/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)
+2 -5
View File
@@ -21,8 +21,6 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require "strscan"
module Liquid
FilterSeparator = /\|/
ArgumentSeparator = ','
@@ -46,7 +44,6 @@ module Liquid
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
end
require "liquid/version"
@@ -67,11 +64,11 @@ require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/snippet_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/tag'
require 'liquid/block_body'
@@ -80,7 +77,6 @@ require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/resource_limits'
require 'liquid/expression'
require 'liquid/template'
require 'liquid/condition'
require 'liquid/utils'
@@ -90,3 +86,4 @@ require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/registers'
require 'liquid/template_factory'
require 'liquid/loom'
+11 -4
View File
@@ -31,10 +31,11 @@ module Liquid
end
end
def freeze
@nodelist.freeze
super
end
# TODO: Freeze the nodelist after optimization
# def freeze
# @nodelist.freeze
# super
# end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
@@ -154,6 +155,12 @@ module Liquid
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
if parse_context.eager_optimize
next if new_tag.nodelist&.all? { !_1.is_a?(String) && _1.nodelist.empty? } # this is an empty block
next if new_tag.is_a?(If) && new_tag.blocks.empty? # this is an empty If block
end
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
+2 -2
View File
@@ -48,8 +48,8 @@ module Liquid
@@operators
end
def self.parse_expression(parse_context, markup, safe: false)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
def self.parse_expression(parse_context, markup)
@@method_literals[markup] || parse_context.parse_expression(markup)
end
attr_reader :attachment, :child_condition
+1 -5
View File
@@ -40,10 +40,6 @@ module Liquid
@global_filter = nil
@disabled_tags = {}
# Instead of constructing new StringScanner objects for each Expression parse,
# we recycle the same one.
@string_scanner = StringScanner.new("")
@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= environment.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
@@ -180,7 +176,7 @@ module Liquid
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(Expression.parse(expression, @string_scanner))
evaluate(Expression.parse(expression))
end
def key?(key)
+1 -1
View File
@@ -34,7 +34,7 @@ module Liquid
# @param file_system The default file system that is used
# to load templates from.
# @param error_mode [Symbol] The default error mode for all templates
# (either :rigid, :strict, :warn, or :lax).
# (either :strict, :warn, or :lax).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
+22 -102
View File
@@ -10,117 +10,37 @@ module Liquid
'true' => true,
'false' => false,
'blank' => '',
'empty' => '',
# in lax mode, minus sign can be a VariableLookup
# For simplicity and performace, we treat it like a literal
'-' => VariableLookup.parse("-", nil).freeze,
'empty' => ''
}.freeze
DOT = ".".ord
ZERO = "0".ord
NINE = "9".ord
DASH = "-".ord
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
parse(parser.expression, ss, cache)
def self.parse(markup)
return nil unless markup
markup = markup.strip
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
end
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
markup = markup.strip # markup can be a frozen string
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
elsif LITERALS.key?(markup)
return LITERALS[markup]
end
# Cache only exists during parsing
if cache
return cache[markup] if cache.key?(markup)
cache[markup] = inner_parse(markup, ss, cache).freeze
case markup
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
if LITERALS.key?(markup)
LITERALS[markup]
else
inner_parse(markup, ss, nil).freeze
end
end
def inner_parse(markup, ss, cache)
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Regexp.last_match(2),
ss,
cache,
)
end
if (num = parse_number(markup, ss))
num
else
VariableLookup.parse(markup, ss, cache)
end
end
def parse_number(markup, ss)
# check if the markup is simple integer or float
case markup
when INTEGER_REGEX
return Integer(markup, 10)
when FLOAT_REGEX
return markup.to_f
end
ss.string = markup
# the first byte must be a digit or a dash
byte = ss.scan_byte
return false if byte != DASH && (byte < ZERO || byte > NINE)
if byte == DASH
peek_byte = ss.peek_byte
# if it starts with a dash, the next byte must be a digit
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
end
# The markup could be a float with multiple dots
first_dot_pos = nil
num_end_pos = nil
while (byte = ss.scan_byte)
return false if byte != DOT && (byte < ZERO || byte > NINE)
# we found our number and now we are just scanning the rest of the string
next if num_end_pos
if byte == DOT
if first_dot_pos.nil?
first_dot_pos = ss.pos
else
# we found another dot, so we know that the number ends here
num_end_pos = ss.pos - 1
end
end
end
num_end_pos = markup.length if ss.eos?
if num_end_pos
# number ends with a number "123.123"
markup.byteslice(0, num_end_pos).to_f
else
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
VariableLookup.parse(markup)
end
end
end
+122 -64
View File
@@ -1,7 +1,66 @@
# frozen_string_literal: true
require "strscan"
module Liquid
class Lexer
class Lexer1
SPECIALS = {
'|' => :pipe,
'.' => :dot,
':' => :colon,
',' => :comma,
'[' => :open_square,
']' => :close_square,
'(' => :open_round,
')' => :close_round,
'?' => :question,
'-' => :dash,
}.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input)
@ss = StringScanner.new(input)
end
def tokenize
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t]
elsif (t = @ss.scan(STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
elsif (t = @ss.scan(IDENTIFIER))
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else
c = @ss.getch
if (s = SPECIALS[c])
[s, c]
else
raise SyntaxError, "Unexpected character #{c}"
end
end
@output << tok
end
@output << [:end_of_string]
end
end
class Lexer2
CLOSE_ROUND = [:close_round, ")"].freeze
CLOSE_SQUARE = [:close_square, "]"].freeze
COLON = [:colon, ":"].freeze
@@ -33,7 +92,6 @@ module Liquid
SINGLE_COMPARISON_TOKENS = [].tap do |table|
table["<".ord] = COMPARISON_LESS_THAN
table[">".ord] = COMPARISON_GREATER_THAN
table.freeze
end
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
@@ -45,17 +103,18 @@ module Liquid
sub_table["=".ord] = COMPARISION_NOT_EQUAL
sub_table.freeze
end
table.freeze
end
COMPARISON_JUMP_TABLE = [].tap do |table|
table["<".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
sub_table.freeze
end
table[">".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
sub_table.freeze
end
table.freeze
@@ -98,82 +157,81 @@ module Liquid
table.freeze
end
def initialize(input)
@ss = StringScanner.new(input)
end
# rubocop:disable Metrics/BlockNesting
class << self
def tokenize(ss)
output = []
def tokenize
@output = []
until ss.eos?
ss.skip(WHITESPACE_OR_NOTHING)
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if ss.eos?
break if @ss.eos?
start_pos = ss.pos
peeked = ss.peek_byte
start_pos = @ss.pos
peeked = @ss.peek_byte
if (special = SPECIAL_TABLE[peeked])
ss.scan_byte
# Special case for ".."
if special == DOT && ss.peek_byte == DOT_ORD
ss.scan_byte
output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
ss.pos -= 1
output << [:number, ss.scan(NUMBER_LITERAL)]
else
output << special
end
if (special = SPECIAL_TABLE[peeked])
@ss.scan_byte
# Special case for ".."
if special == DOT && @ss.peek_byte == DOT_ORD
@ss.scan_byte
@output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
@ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)]
else
output << special
end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
ss.scan_byte
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
output << found
ss.scan_byte
else
raise_syntax_error(start_pos, ss)
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
ss.scan_byte
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
output << found
ss.scan_byte
else
output << SINGLE_COMPARISON_TOKENS[peeked]
@output << special
end
else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
@output << special
end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
raise_syntax_error(start_pos)
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
@output << SINGLE_COMPARISON_TOKENS[peeked]
end
else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
if type && (t = ss.scan(pattern))
# Special case for "contains"
output << if type == :id && t == "contains" && output.last&.first != :dot
COMPARISON_CONTAINS
else
[type, t]
end
if type && (t = @ss.scan(pattern))
# Special case for "contains"
@output << if type == :id && t == "contains" && @output.last&.first != :dot
COMPARISON_CONTAINS
else
raise_syntax_error(start_pos, ss)
[type, t]
end
else
raise_syntax_error(start_pos)
end
end
# rubocop:enable Metrics/BlockNesting
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
# rubocop:enable Metrics/BlockNesting
def raise_syntax_error(start_pos, ss)
ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{ss.getch}"
end
@output << EOS
end
def raise_syntax_error(start_pos)
@ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{@ss.getch}"
end
end
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end
-6
View File
@@ -2,15 +2,12 @@
errors:
syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
for_invalid_in: "For loops require an 'in' clause"
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
@@ -20,9 +17,7 @@
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
@@ -31,6 +26,5 @@
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
argument:
include: "Argument error in tag 'include' - Illegal template name"
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
disabled:
tag: "usage is not allowed in this context"
+123
View File
@@ -0,0 +1,123 @@
# frozen_string_literal: true
module Liquid
class Loom
MERGABLE_IF_OPERATORS = ["==", ">", "<", "!="].freeze
EQUAL_OP = "==".freeze
class << self
def optimize(template)
new(template).optimize
end
end
def initialize template
@root = template.root
end
def optimize
merge_if_blocks
end
def merge_if_blocks
nodelist_list = [@root.nodelist]
while nodelist_list.any?
next_nodelist_list = []
nodelist_list.each do |nodelist|
i = 0
while i < nodelist.length
node = nodelist[i]
chain_if_blocks(nodelist, node, i) if node.is_a?(If)
i += 1
end
end
nodelist_list = next_nodelist_list
end
end
private
def mergable_if_blocks?(target_if, next_if)
target_left = target_if.blocks.first.left
target_right = target_if.blocks.first.right
next_left = next_if.blocks.first.left
next_right = next_if.blocks.first.right
used_variables = Hash.new { |h, k| h[k] = 0 }
[
target_if.blocks.first.left,
target_if.blocks.first.right,
next_if.blocks.first.left,
next_if.blocks.first.right
].each do |var|
if var.is_a?(VariableLookup)
used_variables[var.name] += 1
end
end
return if used_variables.keys.count > 1
most_used_variable_name = used_variables.keys[0]
# TODO: I probably can't do this
# It might be possible to get different result between a > b and b < a
# Move most commonly used variable to the left side
if (target_left.is_a?(VariableLookup) && target_left.name != most_used_variable_name) || (target_right.is_a?(VariableLookup) && target_right.name == most_used_variable_name)
target_left, target_right = target_right, target_left
end
if (next_left.is_a?(VariableLookup) && next_left.name != most_used_variable_name) || (next_right.is_a?(VariableLookup) && next_right.name == most_used_variable_name)
next_left, next_right = next_right, next_left
end
return false unless target_left.is_a?(VariableLookup) && next_left.is_a?(VariableLookup)
return false if target_left.name != next_left.name
return false if target_right.nil? || next_right.nil?
# we need to be conversative here and only can merge ==, >, <, and != operators
target_operator = target_if.blocks.first.operator
next_operator = next_if.blocks.first.operator
return false unless MERGABLE_IF_OPERATORS.include?(target_operator) && MERGABLE_IF_OPERATORS.include?(next_operator)
return false if target_operator == next_operator && target_right == next_right
return false if target_right.is_a?(VariableLookup) || next_right.is_a?(VariableLookup)
true
end
def chain_if_blocks(nodelist, first_if_node, first_if_index)
used_variables = Set.new
# only check the top level Condition (ignore children conditions for now)
first_if_node.blocks.each do |condition|
used_variables << condition.left
used_variables << condition.right if condition.right
end
if_blocks = []
nodelist[first_if_index + 1..-1].each do |node|
break unless node.is_a?(If)
# check if the variables used in the current block are used in the previous block
break unless mergable_if_blocks?(first_if_node, node)
if_blocks << node
end
nodelist.delete_if { |node| if_blocks.include?(node) }
if_blocks.each do |if_block|
first_if_node.blocks << if_block.blocks.first
end
end
end
end
+6 -42
View File
@@ -3,7 +3,7 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment
attr_reader :partial, :warnings, :error_mode, :environment, :eager_optimize
def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@@ -11,18 +11,7 @@ module Liquid
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
@string_scanner = StringScanner.new("")
@expression_cache = if options[:expression_cache].nil?
{}
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
options[:expression_cache]
elsif options[:expression_cache]
{}
end
@eager_optimize = options.fetch(:eager_optimize, ENV["OPTIMIZE"] == "true")
self.depth = 0
self.partial = false
@@ -36,37 +25,12 @@ module Liquid
Liquid::BlockBody.new
end
def new_parser(input)
@string_scanner.string = input
Parser.new(@string_scanner)
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
end
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(
source: source,
string_scanner: @string_scanner,
line_number: start_line_number,
for_liquid_tag: for_liquid_tag,
)
end
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)
def parse_expression(markup)
Expression.parse(markup)
end
def partial=(value)
+2 -2
View File
@@ -3,8 +3,8 @@
module Liquid
class Parser
def initialize(input)
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = Lexer.tokenize(ss)
l = Lexer.new(input)
@tokens = l.tokenize
@p = 0 # pointer to current location
end
+1 -26
View File
@@ -2,22 +2,10 @@
module Liquid
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)
return rigid_parse_with_error_context(markup) if rigid_mode?
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :rigid
raise
when :strict
raise
when :warn
@@ -28,12 +16,11 @@ module Liquid
def parse_with_selected_parser(markup)
case parse_context.error_mode
when :rigid then rigid_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
rigid_parse_with_error_context(markup)
strict_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
lax_parse(markup)
@@ -41,20 +28,8 @@ module Liquid
end
end
def rigid_mode?
parse_context.error_mode == :rigid
end
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)
strict_parse(markup)
rescue SyntaxError => e
+3 -3
View File
@@ -2,9 +2,9 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup, string_scanner, cache)
def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
-22
View File
@@ -1,22 +0,0 @@
# 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
+61 -153
View File
@@ -3,6 +3,7 @@
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_I32 = (1 << 31) - 1
@@ -63,7 +64,7 @@ module Liquid
# @liquid_syntax string | downcase
# @liquid_return [string]
def downcase(input)
Utils.to_s(input).downcase
input.to_s.downcase
end
# @liquid_public_docs
@@ -74,7 +75,7 @@ module Liquid
# @liquid_syntax string | upcase
# @liquid_return [string]
def upcase(input)
Utils.to_s(input).upcase
input.to_s.upcase
end
# @liquid_public_docs
@@ -85,7 +86,7 @@ module Liquid
# @liquid_syntax string | capitalize
# @liquid_return [string]
def capitalize(input)
Utils.to_s(input).capitalize
input.to_s.capitalize
end
# @liquid_public_docs
@@ -96,7 +97,7 @@ module Liquid
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input)
CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
CGI.escapeHTML(input.to_s) unless input.nil?
end
alias_method :h, :escape
@@ -108,7 +109,7 @@ module Liquid
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input)
Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
# @liquid_public_docs
@@ -123,7 +124,7 @@ module Liquid
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input)
CGI.escape(Utils.to_s(input)) unless input.nil?
CGI.escape(input.to_s) unless input.nil?
end
# @liquid_public_docs
@@ -137,7 +138,7 @@ module Liquid
def url_decode(input)
return if input.nil?
result = CGI.unescape(Utils.to_s(input))
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
@@ -151,7 +152,7 @@ module Liquid
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(Utils.to_s(input))
Base64.strict_encode64(input.to_s)
end
# @liquid_public_docs
@@ -162,7 +163,7 @@ module Liquid
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
input = Utils.to_s(input)
input = input.to_s
StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
@@ -176,7 +177,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(Utils.to_s(input))
Base64.urlsafe_encode64(input.to_s)
end
# @liquid_public_docs
@@ -187,7 +188,7 @@ module Liquid
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
input = Utils.to_s(input)
input = input.to_s
StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
@@ -211,7 +212,7 @@ module Liquid
if input.is_a?(Array)
input.slice(offset, length) || []
else
Utils.to_s(input).slice(offset, length) || ''
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
@@ -235,10 +236,10 @@ module Liquid
# @liquid_return [string]
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = Utils.to_s(input)
input_str = input.to_s
length = Utils.to_integer(length)
truncate_string_str = Utils.to_s(truncate_string)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0
@@ -262,7 +263,7 @@ module Liquid
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
input = Utils.to_s(input)
input = input.to_s
words = Utils.to_integer(words)
words = 1 if words <= 0
@@ -276,8 +277,7 @@ module Liquid
return input if wordlist.length <= words
wordlist.pop
truncate_string = Utils.to_s(truncate_string)
wordlist.join(" ").concat(truncate_string)
wordlist.join(" ").concat(truncate_string.to_s)
end
# @liquid_public_docs
@@ -288,9 +288,7 @@ module Liquid
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
def split(input, pattern)
pattern = Utils.to_s(pattern)
input = Utils.to_s(input)
input.split(pattern)
input.to_s.split(pattern.to_s)
end
# @liquid_public_docs
@@ -301,8 +299,7 @@ module Liquid
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input)
input = Utils.to_s(input)
input.strip
input.to_s.strip
end
# @liquid_public_docs
@@ -313,8 +310,7 @@ module Liquid
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input)
input = Utils.to_s(input)
input.lstrip
input.to_s.lstrip
end
# @liquid_public_docs
@@ -325,8 +321,7 @@ module Liquid
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input)
input = Utils.to_s(input)
input.rstrip
input.to_s.rstrip
end
# @liquid_public_docs
@@ -337,9 +332,8 @@ module Liquid
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input)
input = Utils.to_s(input)
empty = ''
result = input.gsub(STRIP_HTML_BLOCKS, empty)
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end
@@ -352,8 +346,7 @@ module Liquid
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
def strip_newlines(input)
input = Utils.to_s(input)
input.gsub(/\r?\n/, '')
input.to_s.gsub(/\r?\n/, '')
end
# @liquid_public_docs
@@ -364,7 +357,6 @@ module Liquid
# @liquid_syntax array | join
# @liquid_return [string]
def join(input, glue = ' ')
glue = Utils.to_s(glue)
InputIterator.new(input, context).join(glue)
end
@@ -432,59 +424,29 @@ module Liquid
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil)
filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
end
ary = InputIterator.new(input, context)
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to exclude items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | reject: string, string
# @liquid_return [array[untyped]]
def reject(input, property, target_value = nil)
filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Tests if any item in an array has a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | has: string, string
# @liquid_return [boolean]
def has(input, property, target_value = nil)
filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find: string, string
# @liquid_return [untyped]
def find(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the index of the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find_index: string, string
# @liquid_return [number]
def find_index(input, property, target_value = nil)
filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
if ary.empty?
[]
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# @liquid_public_docs
@@ -581,10 +543,7 @@ module Liquid
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
def replace(input, string, replacement = '')
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.gsub(string, replacement)
input.to_s.gsub(string.to_s, replacement.to_s)
end
# @liquid_public_docs
@@ -595,10 +554,7 @@ module Liquid
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
def replace_first(input, string, replacement = '')
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = Utils.to_s(input)
input.sub(string, replacement)
input.to_s.sub(string.to_s, replacement.to_s)
end
# @liquid_public_docs
@@ -609,9 +565,9 @@ module Liquid
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
def replace_last(input, string, replacement)
input = Utils.to_s(input)
string = Utils.to_s(string)
replacement = Utils.to_s(replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
start_index = input.rindex(string)
@@ -663,9 +619,7 @@ module Liquid
# @liquid_syntax string | append: string
# @liquid_return [string]
def append(input, string)
input = Utils.to_s(input)
string = Utils.to_s(string)
input + string
input.to_s + string.to_s
end
# @liquid_public_docs
@@ -694,9 +648,7 @@ module Liquid
# @liquid_syntax string | prepend: string
# @liquid_return [string]
def prepend(input, string)
input = Utils.to_s(input)
string = Utils.to_s(string)
string + input
string.to_s + input.to_s
end
# @liquid_public_docs
@@ -707,20 +659,10 @@ module Liquid
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
def newline_to_br(input)
input = Utils.to_s(input)
input.gsub(/\r?\n/, "<br />\n")
input.to_s.gsub(/\r?\n/, "<br />\n")
end
# @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:
# Reformat a date using Ruby's core Time#strftime( string ) -> string
#
# %a - The abbreviated weekday name (``Sun'')
# %A - The full weekday name (``Sunday'')
@@ -749,15 +691,14 @@ module Liquid
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
# @liquid_syntax date | date: string
# @liquid_return [string]
#
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
def date(input, format)
str_format = Utils.to_s(format)
return input if str_format.empty?
return input if format.to_s.empty?
return input unless (date = Utils.to_date(input))
date.strftime(str_format)
date.strftime(format.to_s)
end
# @liquid_public_docs
@@ -977,27 +918,8 @@ module Liquid
attr_reader :context
def filter_array(input, property, target_value, default_value = [], &block)
ary = InputIterator.new(input, context)
return default_value if ary.empty?
block.call(ary) do |item|
if target_value.nil?
item[property]
else
item[property] == target_value
end
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{Utils.to_s(property)}'"
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
def apply_operation(input, operand, operation)
@@ -1046,18 +968,7 @@ module Liquid
end
def join(glue)
first = true
output = +""
each do |item|
if first
first = false
else
output << glue
end
output << Liquid::Utils.to_s(item)
end
output
to_a.join(glue.to_s)
end
def concat(args)
@@ -1069,10 +980,7 @@ module Liquid
end
def uniq(&block)
to_a.uniq do |item|
item = Utils.to_liquid_value(item)
block ? yield(item) : item
end
to_a.uniq(&block)
end
def compact
+2 -6
View File
@@ -68,12 +68,8 @@ module Liquid
private
def safe_parse_expression(parser)
parse_context.safe_parse_expression(parser)
end
def parse_expression(markup, safe: false)
parse_context.parse_expression(markup, safe: safe)
def parse_expression(markup)
parse_context.parse_expression(markup)
end
end
end
-4
View File
@@ -19,8 +19,6 @@ require_relative "tags/comment"
require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
require_relative "tags/doc"
require_relative "tags/snippet"
module Liquid
module Tags
@@ -44,8 +42,6 @@ module Liquid
'if' => If,
'echo' => Echo,
'tablerow' => TableRow,
'doc' => Doc,
'snippet' => Snippet,
}.freeze
end
end
-4
View File
@@ -9,10 +9,6 @@ module Liquid
# Creates a new variable.
# @liquid_description
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
-4
View File
@@ -9,10 +9,6 @@ module Liquid
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_syntax
# {% capture variable %}
# value
+6 -42
View File
@@ -31,7 +31,12 @@ module Liquid
def initialize(tag_name, markup, options)
super
@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
def parse(tokens)
@@ -86,50 +91,9 @@ module Liquid
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)
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
unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
+13 -72
View File
@@ -17,13 +17,23 @@ module Liquid
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
attr_reader :variables
def initialize(tag_name, markup, options)
super
parse_with_selected_parser(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?(/\w+:0x\h{8}/)
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
def named?
@@ -55,82 +65,13 @@ module Liquid
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)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
next unless Regexp.last_match(1)
var = parse_expression(Regexp.last_match(1))
maybe_dup_lookup(var)
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
end.compact
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
def children
Array(@node.variables)
+1 -5
View File
@@ -7,14 +7,10 @@ module Liquid
# @liquid_name decrement
# @liquid_summary
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture/snippets) included in the file.
# [snippets](/themes/architecture#snippets) included in the file.
#
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
-81
View File
@@ -1,81 +0,0 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name doc
# @liquid_summary
# Documents template elements with annotations.
# @liquid_description
# The `doc` tag allows developers to include documentation within Liquid
# templates. Any content inside `doc` tags is not rendered or outputted.
# Liquid code inside will be parsed but not executed. This facilitates
# tooling support for features like code completion, linting, and inline
# documentation.
#
# For detailed documentation syntax and examples, see the
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
#
# @liquid_syntax
# {% doc %}
# Renders a message.
#
# @param {string} foo - A string value.
# @param {string} [bar] - An optional string value.
#
# @example
# {% render 'message', foo: 'Hello', bar: 'World' %}
# {% enddoc %}
class Doc < Block
NO_UNEXPECTED_ARGS = /\A\s*\z/
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@body = +""
while (token = tokens.shift)
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
raise_nested_doc_error if tag_name == @tag_name
if tag_name == block_delimiter
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@body << token unless token.empty?
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output
end
def blank?
@body.empty?
end
def nodelist
[@body]
end
private
def ensure_valid_markup(tag_name, markup, parse_context)
unless NO_UNEXPECTED_ARGS.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
end
end
def raise_nested_doc_error
raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
end
end
end
+8 -12
View File
@@ -20,8 +20,8 @@ module Liquid
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render for each iteration.
# @liquid_optional_param limit: [number] The number of iterations to perform.
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block
@@ -88,12 +88,12 @@ module Liquid
end
def strict_parse(markup)
p = @parse_context.new_parser(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@collection_name = parse_expression(collection_name, safe: true)
@collection_name = parse_expression(collection_name)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
@@ -104,17 +104,13 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume(:colon)
set_attribute(attribute, p.expression, safe: true)
set_attribute(attribute, p.expression)
end
p.consume(:end_of_string)
end
private
def rigid_parse(markup)
strict_parse(markup)
end
def collection_segment(context)
offsets = context.registers[:for] ||= {}
@@ -178,16 +174,16 @@ module Liquid
output
end
def set_attribute(key, expr, safe: false)
def set_attribute(key, expr)
case key
when 'offset'
@from = if expr == 'continue'
:continue
else
parse_expression(expr, safe: safe)
parse_expression(expr)
end
when 'limit'
@limit = parse_expression(expr, safe: safe)
@limit = parse_expression(expr)
end
end
+36 -12
View File
@@ -23,6 +23,7 @@ module Liquid
def initialize(tag_name, markup, options)
super
@blocks = []
@has_else_block = false
push_block('if', markup)
end
@@ -33,17 +34,44 @@ module Liquid
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
if parse_context.eager_optimize && definitive_false_statement?
@blocks.clear
else
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
end
end
def definitive_false_statement?
# check if any blocks have variable lookups
@blocks.each do |condition|
return false if condition.left.is_a?(VariableLookup) || condition.right&.is_a?(VariableLookup)
child_condition = condition.child_condition
while child_condition
return false if child_condition&.left.is_a?(VariableLookup) || child_condition&.right&.is_a?(VariableLookup)
child_condition = child_condition.child_condition
end
end
# check if all blocks are false
@blocks.each do |condition|
return false if condition.evaluate
end
true
end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens)
if ELSE_TAG_NAMES.include?(tag)
@has_else_block = true
push_block(tag, markup)
else
super
@@ -66,10 +94,6 @@ module Liquid
private
def rigid_parse(markup)
strict_parse(markup)
end
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
@@ -81,8 +105,8 @@ module Liquid
block.attach(new_body)
end
def parse_expression(markup, safe: false)
Condition.parse_expression(parse_context, markup, safe: safe)
def parse_expression(markup)
Condition.parse_expression(parse_context, markup)
end
def lax_parse(markup)
@@ -106,7 +130,7 @@ module Liquid
end
def strict_parse(markup)
p = @parse_context.new_parser(markup)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
@@ -124,9 +148,9 @@ module Liquid
end
def parse_comparison(p)
a = parse_expression(p.expression, safe: true)
a = parse_expression(p.expression)
if (op = p.consume?(:comparison))
b = parse_expression(p.expression, safe: true)
b = parse_expression(p.expression)
Condition.new(a, op, b)
else
Condition.new(a)
+19 -45
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category theme
# @liquid_name include
# @liquid_summary
# Renders a [snippet](/themes/architecture/snippets).
# Renders a [snippet](/themes/architecture#snippets).
# @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
# snippet.
@@ -27,7 +27,24 @@ module Liquid
def initialize(tag_name, markup, options)
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
def parse(_tokens)
@@ -84,49 +101,6 @@ module Liquid
alias_method :parse_context, :options
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
def children
[
+1 -5
View File
@@ -7,14 +7,10 @@ module Liquid
# @liquid_name increment
# @liquid_summary
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
#
# > Caution:
# > Predefined Liquid objects can be overridden by variables with the same name.
# > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
# @liquid_description
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture/snippets) included in the file.
# [snippets](/themes/architecture#snippets) included in the file.
#
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
+29 -69
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture/snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
@@ -27,7 +27,7 @@ module Liquid
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
@@ -35,7 +35,22 @@ module Liquid
def initialize(tag_name, markup, options)
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
def for_loop?
@@ -47,23 +62,21 @@ module Liquid
end
def render_tag(context, output)
template = context.evaluate(@template_name_expr)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
if template.respond_to?(:to_partial)
partial = template.to_partial
template_name = template.filename
context_variable_name = @alias_name || template.name
elsif @template_name_expr.is_a?(String)
partial = PartialCache.load(template, context: context, parse_context: parse_context)
template_name = partial.name
context_variable_name = @alias_name || template_name.split('/').last
else
raise ::ArgumentError
end
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.template_name = partial.name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@@ -86,59 +99,6 @@ module Liquid
output
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
def children
[
-45
View File
@@ -1,45 +0,0 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name snippet
# @liquid_summary
# Creates a new inline snippet.
# @liquid_description
# You can create inline snippets to make your Liquid code more modular.
# @liquid_syntax
# {% snippet snippet_name %}
# value
# {% endsnippet %}
class Snippet < Block
def initialize(tag_name, markup, options)
super
p = @parse_context.new_parser(markup)
if p.look(:id)
@to = p.consume(:id)
p.consume(:end_of_string)
else
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end
end
def render_to_output_buffer(context, output)
snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
context.scopes.last[@to] = snippet_drop
context.resource_limits.increment_assign_score(assign_score_of(snippet_drop))
output
end
def blank?
true
end
private
def assign_score_of(snippet_drop)
snippet_drop.body.nodelist.sum { |node| node.to_s.bytesize }
end
end
end
+3 -40
View File
@@ -19,54 +19,17 @@ module Liquid
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols: [number] The number of columns that the table should have.
# @liquid_optional_param limit: [number] The number of iterations to perform.
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param cols [number] The number of columns that the table should have.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
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
@variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2))
+7 -4
View File
@@ -2,7 +2,7 @@
module Liquid
# Templates are central to liquid.
# Interpreting templates is a two step process. First you compile the
# Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed.
# your code should expect to get some SyntaxErrors.
#
@@ -24,8 +24,7 @@ module Liquid
# Sets how strict the parser should be.
# :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.
# :strict enforces correct syntax for most tags
# :rigid enforces correct syntax for all tags
# :strict will enforce correct syntax.
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
@@ -83,7 +82,11 @@ module Liquid
# See Liquid::Profiler for more information
def parse(source, options = {})
environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
template = new(environment: environment).parse(source, options)
Loom.optimize(template) if options[:eager_optimize]
template
end
end
+16 -132
View File
@@ -1,43 +1,20 @@
# frozen_string_literal: true
require "strscan"
module Liquid
class Tokenizer
attr_reader :line_number, :for_liquid_tag
TAG_END = /%\}/
TAG_OR_VARIABLE_START = /\{[\{\%]/
NEWLINE = /\n/
OPEN_CURLEY = "{".ord
CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord
def initialize(
source:,
string_scanner:,
line_numbers: false,
line_number: nil,
for_liquid_tag: false
)
@line_number = line_number || (line_numbers ? 1 : nil)
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@source = source.to_s.to_str
@offset = 0
@tokens = []
if @source
@ss = string_scanner
@ss.string = @source
tokenize
end
@offset = 0
@tokens = tokenize
end
def shift
token = @tokens[@offset]
return unless token
return nil unless token
@offset += 1
@@ -51,111 +28,18 @@ module Liquid
private
def tokenize
if @for_liquid_tag
@tokens = @source.split("\n")
else
@tokens << shift_normal until @ss.eos?
return [] if @source.empty?
return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
if tokens[0]&.empty?
@offset += 1
end
@source = nil
@ss = nil
end
def shift_normal
token = next_token
return unless token
token
end
def next_token
# possible states: :text, :tag, :variable
byte_a = @ss.peek_byte
if byte_a == OPEN_CURLEY
@ss.scan_byte
byte_b = @ss.peek_byte
if byte_b == PERCENTAGE
@ss.scan_byte
return next_tag_token
elsif byte_b == OPEN_CURLEY
@ss.scan_byte
return next_variable_token
end
@ss.pos -= 1
end
next_text_token
end
def next_text_token
start = @ss.pos
unless @ss.skip_until(TAG_OR_VARIABLE_START)
token = @ss.rest
@ss.terminate
return token
end
pos = @ss.pos -= 2
@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
def next_variable_token
start = @ss.pos - 2
byte_a = byte_b = @ss.scan_byte
while byte_b
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
break unless byte_a
if @ss.eos?
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
end
byte_b = @ss.scan_byte
if byte_a == CLOSE_CURLEY
if byte_b == CLOSE_CURLEY
return @source.byteslice(start, @ss.pos - start)
elsif byte_b != CLOSE_CURLEY
@ss.pos -= 1
return @source.byteslice(start, @ss.pos - start)
end
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
return next_tag_token_with_start(start)
end
byte_a = byte_b
end
"{{"
end
def next_tag_token
start = @ss.pos - 2
if (len = @ss.skip_until(TAG_END))
@source.byteslice(start, len + 2)
else
"{%"
end
end
def next_tag_token_with_start(start)
@ss.skip_until(TAG_END)
@source.byteslice(start, @ss.pos - start)
tokens
end
end
end
+2 -101
View File
@@ -2,9 +2,6 @@
module Liquid
module Utils
DECIMAL_REGEX = /\A-?\d+\.\d+\z/
UNIX_TIMESTAMP_REGEX = /\A\d+\z/
def self.slice_collection(collection, from, to)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
@@ -55,7 +52,7 @@ module Liquid
when Numeric
obj
when String
DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
@@ -76,7 +73,7 @@ module Liquid
case obj
when 'now', 'today'
Time.now
when UNIX_TIMESTAMP_REGEX, Integer
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
@@ -92,101 +89,5 @@ module Liquid
# Otherwise return the object itself
obj
end
def self.to_s(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#to_s`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
hash_inspect(obj, seen)
else
obj.to_s
end
when Array
array_inspect(obj, seen)
else
obj.to_s
end
end
def self.inspect(obj, seen = {})
case obj
when Hash
# If the custom hash implementation overrides `#inspect`, use their
# custom implementation. Otherwise we use Liquid's default
# implementation.
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
hash_inspect(obj, seen)
else
obj.inspect
end
when Array
array_inspect(obj, seen)
else
obj.inspect
end
end
def self.array_inspect(arr, seen = {})
if seen[arr.object_id]
return "[...]"
end
seen[arr.object_id] = true
str = +"["
cursor = 0
len = arr.length
while cursor < len
if cursor > 0
str << ", "
end
item_str = inspect(arr[cursor], seen)
str << item_str
cursor += 1
end
str << "]"
str
ensure
seen.delete(arr.object_id)
end
def self.hash_inspect(hash, seen = {})
if seen[hash.object_id]
return "{...}"
end
seen[hash.object_id] = true
str = +"{"
first = true
hash.each do |key, value|
if first
first = false
else
str << ", "
end
key_str = inspect(key, seen)
str << key_str
str << "=>"
value_str = inspect(value, seen)
str << value_str
end
str << "}"
str
ensure
seen.delete(hash.object_id)
end
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
private_constant :HASH_TO_S_METHOD
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
private_constant :HASH_INSPECT_METHOD
end
end
+7 -58
View File
@@ -54,37 +54,26 @@ module Liquid
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
@filters << lax_parse_filter_expressions(filtername, filterargs)
@filters << parse_filter_expressions(filtername, filterargs)
end
end
end
def strict_parse(markup)
@filters = []
p = @parse_context.new_parser(markup)
p = Parser.new(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
@name = parse_context.parse_expression(p.expression)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
@filters << lax_parse_filter_expressions(filtername, filterargs)
@filters << parse_filter_expressions(filtername, filterargs)
end
p.consume(:end_of_string)
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)
# first argument
filterargs = [p.argument]
@@ -118,8 +107,8 @@ module Liquid
obj.each do |o|
render_obj_to_output(o, output)
end
else
output << Liquid::Utils.to_s(obj)
when
output << obj.to_s
end
end
@@ -133,7 +122,7 @@ module Liquid
private
def lax_parse_filter_expressions(filter_name, unparsed_args)
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
@@ -149,46 +138,6 @@ module Liquid
result
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)
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs
+5 -13
View File
@@ -6,20 +6,16 @@ module Liquid
attr_reader :name, :lookups
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup, string_scanner, cache)
def self.parse(markup)
new(markup)
end
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
def initialize(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name&.start_with?('[') && name&.end_with?(']')
name = Expression.parse(
name[1..-2],
string_scanner,
cache,
)
name = Expression.parse(name[1..-2])
end
@name = name
@@ -29,11 +25,7 @@ module Liquid
@lookups.each_index do |i|
lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(
lookup[1..-2],
string_scanner,
cache,
)
lookups[i] = Expression.parse(lookup[1..-2])
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.10.0"
VERSION = "5.6.0.rc2"
end
+1 -1
View File
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
s.require_path = "lib"
s.add_dependency("strscan", ">= 3.1.1")
s.add_dependency("strscan")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0')
+5 -8
View File
@@ -9,17 +9,14 @@ Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 20
x.warmup = 10
x.time = 10
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
phase = ENV["PHASE"] || "all"
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end
-13
View File
@@ -48,19 +48,6 @@ class ThemeRunner
end
end
# `tokenize` will just test the tokenizen portion of liquid without any templates
def tokenize
ss = StringScanner.new("")
@tests.each do |test_hash|
tokenizer = Liquid::Tokenizer.new(
source: test_hash[:liquid],
string_scanner: ss,
line_numbers: true,
)
while tokenizer.shift; end
end
end
# `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
-94
View File
@@ -1,94 +0,0 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
STRING_MARKUPS = [
"\"foo\"",
"\"fooooooooooo\"",
"\"foooooooooooooooooooooooooooooo\"",
"'foo'",
"'fooooooooooo'",
"'foooooooooooooooooooooooooooooo'",
]
VARIABLE_MARKUPS = [
"article",
"article.title",
"article.title.size",
"very_long_variable_name_2024_11_05",
"very_long_variable_name_2024_11_05.size",
]
NUMBER_MARKUPS = [
"0",
"35",
"1241891024912849",
"3.5",
"3.51214128409128",
"12381902839.123819283910283",
"123.456.789",
"-123",
"-12.33",
"-405.231",
"-0",
"0",
"0.0",
"0.0000000000000000000000",
"0.00000000001",
]
RANGE_MARKUPS = [
"(1..30)",
"(1...30)",
"(1..30..5)",
"(1.0...30.0)",
"(1.........30)",
"(1..foo)",
"(foo..30)",
"(foo..bar)",
"(foo...bar...100)",
"(foo...bar...100.0)",
]
LITERAL_MARKUPS = [
nil,
'nil',
'null',
'',
'true',
'false',
'blank',
'empty',
]
MARKUPS = {
"string" => STRING_MARKUPS,
"literal" => LITERAL_MARKUPS,
"variable" => VARIABLE_MARKUPS,
"number" => NUMBER_MARKUPS,
"range" => RANGE_MARKUPS,
}
Benchmark.ips do |x|
x.config(time: 5, warmup: 5)
MARKUPS.each do |type, markups|
x.report("Liquid::Expression#parse: #{type}") do
markups.each do |markup|
Liquid::Expression.parse(markup)
end
end
end
x.report("Liquid::Expression#parse: all") do
MARKUPS.values.flatten.each do |markup|
Liquid::Expression.parse(markup)
end
end
end
+21 -2
View File
@@ -29,12 +29,31 @@ EXPRESSIONS = [
"foo | default: -1",
]
EXPRESSIONS.each do |expr|
lexer_1_result = Liquid::Lexer1.new(expr).tokenize
lexer_2_result = Liquid::Lexer2.new(expr).tokenize
next if lexer_1_result == lexer_2_result
warn "Lexer1 and Lexer2 results are different for expression: #{expr}"
warn "expected: #{lexer_1_result}"
warn "got: #{lexer_2_result}"
abort
end
Benchmark.ips do |x|
x.config(time: 10, warmup: 5)
x.report("Liquid::Lexer#tokenize") do
x.report("Liquid::Lexer1#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer.new(expr)
l = Liquid::Lexer1.new(expr)
l.tokenize
end
end
x.report("Liquid::Lexer2#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer2.new(expr)
l.tokenize
end
end
+56
View File
@@ -0,0 +1,56 @@
# frozen_string_literal: true
require "benchmark/ips"
require 'liquid'
RubyVM::YJIT.enable
TEMPLATE = <<~LIQUID
{% if false %}
{% for i in (1..1000000) %}
{{ "Hello world!" }}
{% endfor %}
{% endif %}
{% assign result = 1 %}
{% if foo == 1 %}{% assign result = 1 %}{% endif %}{% if foo == 2 %}{% assign result = 2 %}{% endif %}{% if foo == 3 %}{% assign result = 3 %}{% endif %}
Result: {{ result }}
LIQUID
baseline_template = Liquid::Template.parse(TEMPLATE, eager_optimize: false)
optimized_template = Liquid::Template.parse(TEMPLATE, eager_optimize: true)
[nil, 1, 2, 3].each do |foo|
baseline_output = baseline_template.render('foo' => foo)
optimized_output = optimized_template.render('foo' => foo)
if baseline_output != optimized_output
puts "WARNING! Baseline and optimized templates render differently for foo=#{foo}"
puts "Baseline: #{baseline_output}"
puts "Optimized: #{optimized_output}"
raise
end
end
def render(template, foo)
template.render('foo' => foo)
end
Benchmark.ips do |x|
x.config(time: 20, warmup: 3)
x.report("baseline") do
[nil, 1, 2, 3].each do |foo|
render(baseline_template, foo)
end
end
x.report("optimized") do
[nil, 1, 2, 3].each do |foo|
render(optimized_template, foo)
end
end
x.compare!
end
+1 -1
View File
@@ -632,7 +632,7 @@ class ContextTest < Minitest::Test
end
def test_has_key_will_not_add_an_error_for_missing_keys
with_error_modes(:strict) do
with_error_mode(:strict) do
context = Context.new
context.key?('unknown')
assert_empty(context.errors)
+1 -1
View File
@@ -67,7 +67,7 @@ class ErrorHandlingTest < Minitest::Test
end
def test_unrecognized_operator
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
-141
View File
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'test_helper'
require 'lru_redux'
class ExpressionTest < Minitest::Test
def test_keyword_literals
@@ -14,7 +13,6 @@ class ExpressionTest < Minitest::Test
assert_template_result("double quoted", '{{"double quoted"}}')
assert_template_result("spaced", "{{ 'spaced' }}")
assert_template_result("spaced2", "{{ 'spaced2' }}")
assert_template_result("emoji🔥", "{{ 'emoji🔥' }}")
end
def test_int
@@ -24,22 +22,8 @@ class ExpressionTest < Minitest::Test
end
def test_float
assert_template_result("-17.42", "{{ -17.42 }}")
assert_template_result("2.5", "{{ 2.5 }}")
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")
# this is a unfortunate quirky behavior of Liquid
result = Expression.parse(".5")
assert_kind_of(Liquid::VariableLookup, result)
result = Expression.parse("-.5")
assert_kind_of(Liquid::VariableLookup, result)
end
def test_range
@@ -56,131 +40,6 @@ class ExpressionTest < Minitest::Test
)
end
def test_quirky_negative_sign_expression_markup
result = Expression.parse("-", nil)
assert(result.is_a?(VariableLookup))
assert_equal("-", result.name)
# for this template, the expression markup is "-"
assert_template_result(
"",
"{{ - 'theme.css' - }}",
error_mode: :lax,
)
end
def test_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
cache = {}
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_expression_cache_with_true_boolean
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
parse_context = ParseContext.new(expression_cache: true)
Liquid::Template.parse(template, parse_context).render
cache = parse_context.instance_variable_get(:@expression_cache)
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_expression_cache_with_lru_redux
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
cache = LruRedux::Cache.new(10)
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_disable_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
parse_context = Liquid::ParseContext.new(expression_cache: false)
Liquid::Template.parse(template, parse_context).render
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
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
def assert_expression_result(expect, markup, **assigns)
-106
View File
@@ -1,106 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class HashRenderingTest < Minitest::Test
def test_render_empty_hash
assert_template_result("{}", "{{ my_hash }}", { "my_hash" => {} })
end
def test_render_hash_with_string_keys_and_values
assert_template_result("{\"key1\"=>\"value1\", \"key2\"=>\"value2\"}", "{{ my_hash }}", { "my_hash" => { "key1" => "value1", "key2" => "value2" } })
end
def test_render_hash_with_symbol_keys_and_integer_values
assert_template_result("{:key1=>1, :key2=>2}", "{{ my_hash }}", { "my_hash" => { key1: 1, key2: 2 } })
end
def test_render_nested_hash
assert_template_result("{\"outer\"=>{\"inner\"=>\"value\"}}", "{{ my_hash }}", { "my_hash" => { "outer" => { "inner" => "value" } } })
end
def test_render_hash_with_array_values
assert_template_result("{\"numbers\"=>[1, 2, 3]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [1, 2, 3] } })
end
def test_render_recursive_hash
recursive_hash = { "self" => {} }
recursive_hash["self"]["self"] = recursive_hash
assert_template_result("{\"self\"=>{\"self\"=>{...}}}", "{{ my_hash }}", { "my_hash" => recursive_hash })
end
def test_hash_with_downcase_filter
assert_template_result("{\"key\"=>\"value\", \"anotherkey\"=>\"anothervalue\"}", "{{ my_hash | downcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_upcase_filter
assert_template_result("{\"KEY\"=>\"VALUE\", \"ANOTHERKEY\"=>\"ANOTHERVALUE\"}", "{{ my_hash | upcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_escape_filter
assert_template_result("{&quot;Key&quot;=&gt;&quot;Value&quot;, &quot;AnotherKey&quot;=&gt;&quot;AnotherValue&quot;}", "{{ my_hash | escape }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_url_encode_filter
assert_template_result("%7B%22Key%22%3D%3E%22Value%22%2C+%22AnotherKey%22%3D%3E%22AnotherValue%22%7D", "{{ my_hash | url_encode }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_html_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip_html }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_truncate__20_filter
assert_template_result("{\"Key\"=>\"Value\", ...", "{{ my_hash | truncate: 20 }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_replace___key____replaced_key__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | replace: 'key', 'replaced_key' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_append____appended_text__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"} appended text", "{{ my_hash | append: ' appended text' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_prepend___prepended_text___filter
assert_template_result("prepended text {\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | prepend: 'prepended text ' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_render_hash_with_array_values_empty
assert_template_result("{\"numbers\"=>[]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [] } })
end
def test_render_hash_with_array_values_hash
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
end
def test_join_filter_with_hash
array = [{ "key1" => "value1" }, { "key2" => "value2" }]
glue = { "lol" => "wut" }
assert_template_result("{\"key1\"=>\"value1\"}{\"lol\"=>\"wut\"}{\"key2\"=>\"value2\"}", "{{ my_array | join: glue }}", { "my_array" => array, "glue" => glue })
end
def test_render_hash_with_hash_key
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
end
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
my_hash = Class.new(Hash) do
def to_s
"kewl"
end
end.new
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
end
def test_rendering_hash_without_custom_to_s_uses_default_inspect
my_hash = Class.new(Hash).new
my_hash[:foo] = :bar
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
end
end
+10 -18
View File
@@ -31,18 +31,18 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter
assert(Template.parse("{{test}}"))
with_error_modes(:lax) do
with_error_mode(:lax) do
assert(Template.parse("{{|test}}"))
end
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end
end
def test_meaningless_parens_error
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
@@ -51,7 +51,7 @@ class ParsingQuirksTest < Minitest::Test
end
def test_unexpected_characters_syntax_error
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(SyntaxError) do
markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}")
@@ -70,7 +70,7 @@ class ParsingQuirksTest < Minitest::Test
end
def test_meaningless_parens_lax
with_error_modes(:lax) do
with_error_mode(:lax) do
assigns = { 'b' => 'bar', 'c' => 'baz' }
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
@@ -78,7 +78,7 @@ class ParsingQuirksTest < Minitest::Test
end
def test_unexpected_characters_silently_eat_logic_lax
with_error_modes(:lax) do
with_error_mode(:lax) do
markup = "true && false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
markup = "false || true"
@@ -93,7 +93,7 @@ class ParsingQuirksTest < Minitest::Test
end
def test_unanchored_filter_arguments
with_error_modes(:lax) do
with_error_mode(:lax) do
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}")
@@ -106,14 +106,14 @@ class ParsingQuirksTest < Minitest::Test
end
def test_invalid_variables_work
with_error_modes(:lax) do
with_error_mode(:lax) do
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
end
end
def test_extra_dots_in_ranges
with_error_modes(:lax) do
with_error_mode(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
@@ -133,15 +133,7 @@ class ParsingQuirksTest < Minitest::Test
end
def test_incomplete_expression
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 1 }}")
assert_template_result("false", "{{ false a }}")
with_error_mode(:lax) do
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
+5 -277
View File
@@ -133,18 +133,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end
def test_find_on_empty_array
assert_nil(@filters.find([], 'foo', 'bar'))
end
def test_find_index_on_empty_array
assert_nil(@filters.find_index([], 'foo', 'bar'))
end
def test_has_on_empty_array
refute(@filters.has([], 'foo', 'bar'))
end
def test_truncate
assert_equal('1234...', @filters.truncate('1234567890', 7))
assert_equal('1234567890', @filters.truncate('1234567890', 20))
@@ -293,16 +281,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
end
def test_join_calls_to_liquid_on_each_element
drop = Class.new(Liquid::Drop) do
def to_liquid
'i did it'
end
end
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
end
def test_sort
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
@@ -560,23 +538,12 @@ class StandardFiltersTest < Minitest::Test
end
end
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
def test_map_returns_empty_with_no_property
foo = [
[1],
[2],
[3],
]
assert_raises(Liquid::ArgumentError) do
@filters.map(foo, nil)
end
@@ -860,233 +827,21 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
def test_reject
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "true"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => false },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "false"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_with_empty_arrays
template = <<~LIQUID
{%- assign has_product = products | has: 'title.content', 'Not found' -%}
{%- unless has_product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_has_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "true"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has_with_false_value_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => true },
{ "handle" => "gamma", "ok" => true },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "false"
assert_template_result(expected_output, template, { "array" => array })
end
def test_find_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign product = products | find: 'price', 3999 -%}
{{- product.title -}}
LIQUID
expected_output = "Alpine jacket"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_with_empty_arrays
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Not found' -%}
{%- unless product -%}
Product not found.
{%- endunless -%}
LIQUID
expected_output = "Product not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_find_index_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign index = products | find_index: 'price', 3999 -%}
{{- index -}}
LIQUID
expected_output = "2"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_index_with_empty_arrays
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
{%- unless index -%}
Index not found.
{%- endunless -%}
LIQUID
expected_output = "Index not found."
assert_template_result(expected_output, template, { "products" => [] })
end
def test_where
array = [
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_empty_string_is_a_no_op
environment = { "array" => ["alpha", "beta", "gamma"] }
expected_output = "alpha beta gamma"
template = "{{ array | where: '' | join: ' ' }}"
assert_template_result(expected_output, template, environment)
end
def test_where_with_nil_is_a_no_op
environment = { "array" => ["alpha", "beta", "gamma"] }
template = "{{ array | where: nil | join: ' ' }}"
assert_raises(Liquid::ArgumentError) do
assert_template_result("alpha beta gamma", template, environment)
end
end
def test_where_with_value
array = [
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal(expectation, @filters.where(input, "ok"))
end
def test_where_string_keys
@@ -1296,33 +1051,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
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
def with_timezone(tz)
-182
View File
@@ -1,182 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class CycleTagTest < Minitest::Test
def test_simple_cycle_inside_for_loop
template = <<~LIQUID
{%- for i in (1..3) -%}
{%- cycle '1', '2', '3' -%}
{%- endfor -%}
LIQUID
assert_template_result("123", template)
end
def test_cycle_with_variables_inside_for_loop
template = <<~LIQUID
{%- assign a = 1 -%}
{%- assign b = 2 -%}
{%- assign c = 3 -%}
{%- for i in (1..3) -%}
{% cycle a, b, c %}
{%- endfor -%}
LIQUID
assert_template_result("123", template)
end
def test_cycle_named_groups_string
template = <<~LIQUID
{%- 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_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
+2 -67
View File
@@ -204,32 +204,6 @@ class IncludeTagTest < Minitest::Test
)
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
file_system = CountingFileSystem.new
environment = Liquid::Environment.build(file_system: file_system)
@@ -303,13 +277,13 @@ class IncludeTagTest < Minitest::Test
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
end
with_error_modes(:lax) do
with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
end
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
end
with_error_modes(:lax) do
with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
end
end
@@ -400,43 +374,4 @@ class IncludeTagTest < Minitest::Test
render_errors: true,
)
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
-1
View File
@@ -16,7 +16,6 @@ class RawTagTest < Minitest::Test
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
assert_template_result("{Hello}", "{% raw %}{{% endraw %}Hello{% raw %}}{% endraw %}")
end
def test_open_tag_in_raw
+3 -58
View File
@@ -101,33 +101,11 @@ class RenderTagTest < Minitest::Test
end
end
def test_rigid_parsing_errors
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
def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
end
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
def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal(
'echoecho',
@@ -290,13 +268,6 @@ class RenderTagTest < Minitest::Test
)
end
def test_render_tag_with_snippet_drop
assert_template_result(
"Hello from snippet",
"{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}",
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
@@ -317,30 +288,4 @@ class RenderTagTest < Minitest::Test
render_errors: true,
)
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
File diff suppressed because it is too large Load Diff
+3 -213
View File
@@ -138,7 +138,7 @@ class TableRowTest < Minitest::Test
def test_tablerow_loop_drop_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1..2) %}
{% tablerow i in (1...2) %}
col: {{ tablerowloop.col }}
col0: {{ tablerowloop.col0 }}
col_first: {{ tablerowloop.col_first }}
@@ -192,14 +192,12 @@ class TableRowTest < Minitest::Test
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
error_mode: :warn,
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
error_mode: :warn,
render_errors: true,
)
@@ -207,19 +205,18 @@ class TableRowTest < Minitest::Test
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
error_mode: :warn,
)
end
def test_table_row_handles_interrupts
assert_template_result(
"<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(
"<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
@@ -258,211 +255,4 @@ class TableRowTest < Minitest::Test
template,
)
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
-65
View File
@@ -209,69 +209,4 @@ class VariableTest < Minitest::Test
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
+4 -35
View File
@@ -34,7 +34,7 @@ module Minitest
def assert_template_result(
expected, template, assigns = {},
message: nil, partials: nil, error_mode: Liquid::Environment.default.error_mode, render_errors: false,
message: nil, partials: nil, error_mode: nil, render_errors: false,
template_factory: nil
)
file_system = StubFileSystem.new(partials || {})
@@ -82,12 +82,10 @@ module Minitest
Environment.dangerously_override(environment, &blk)
end
def with_error_modes(*modes)
def with_error_mode(mode)
old_mode = Liquid::Environment.default.error_mode
modes.each do |mode|
Liquid::Environment.default.error_mode = mode
yield
end
Liquid::Environment.default.error_mode = mode
yield
ensure
Liquid::Environment.default.error_mode = old_mode
end
@@ -148,35 +146,6 @@ class BooleanDrop < Liquid::Drop
end
end
class StringDrop < Liquid::Drop
include Comparable
def initialize(value)
super()
@value = value
end
def to_liquid_value
@value
end
def to_s
@value
end
def to_str
@value
end
def inspect
"#<StringDrop @value=#{@value.inspect}>"
end
def <=>(other)
to_liquid_value <=> Liquid::Utils.to_liquid_value(other)
end
end
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
+1 -7
View File
@@ -47,18 +47,12 @@ class BlockUnitTest < Minitest::Test
)
end
def test_comment_tag_with_block
def test_with_block
template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
assert_equal([String, Comment, String], block_types(template.root.nodelist))
assert_equal(3, template.root.nodelist.size)
end
def test_doc_tag_with_block
template = Liquid::Template.parse(" {% doc %} {% enddoc %} ")
assert_equal([String, Doc, String], block_types(template.root.nodelist))
assert_equal(3, template.root.nodelist.size)
end
private
def block_types(nodelist)
-31
View File
@@ -166,37 +166,6 @@ class ConditionUnitTest < Minitest::Test
assert_includes(err.lines.map(&:strip), expected)
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
def assert_evaluates_true(left, op, right)
+186
View File
@@ -0,0 +1,186 @@
# frozen_string_literal: true
require 'test_helper'
class EagerOptimizeTest < Minitest::Test
include Liquid
def test_remove_empty_blocks
source = <<~LIQUID.gsub(/\n/, '')
{% for i in (1..1000000) %}
{% endfor %}
LIQUID
template = Liquid::Template.parse(source, eager_optimize: true)
assert_equal(0, total_node_count(template))
end
def test_remove_false_if_block
source = <<~LIQUID.gsub(/\n/, '')
{% if false %}
{% if true %}
{% if true %}
{% if true %}
{{ "Hello world!" }}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
LIQUID
template = Liquid::Template.parse(source, eager_optimize: true)
assert_equal(0, total_node_count(template))
source = <<~LIQUID.gsub(/\n/, '')
{% if false %}
{% for i in (1..1000000) %}
{{ "Hello world!" }}
{% endfor %}
{% endif %}
LIQUID
template = Liquid::Template.parse(source, eager_optimize: true)
assert_equal(0, total_node_count(template))
end
def test_remove_multiple_false_if_block
source = <<~LIQUID.gsub(/\n/, '')
{% if false %}
{% if true %}
{% if true %}
{% if true %}
{{ "Hello world!" }}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
LIQUID
template = Liquid::Template.parse(source, eager_optimize: true)
assert_equal(0, total_node_count(template))
end
def test_merge_if_blocks
# for now, work with consecutive if blocks without any String nodes in between
source = <<~LIQUID.gsub(/\n/, '')
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if foo == 2 %}
foo: {{ foo }}
{% endif %}
{% if foo == 3 %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If], source, { "foo" => nil })
assert_optimization([Liquid::If], source, { "foo" => 1 })
assert_optimization([Liquid::If], source, { "foo" => 2 })
assert_optimization([Liquid::If], source, { "foo" => 5 })
source = <<~LIQUID.gsub(/\n/, '')
{% assign bar = "application" %}
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if foo == 2 and bar contains "app" %}
foo: {{ foo }}
{% endif %}
{% if 3 == foo and bar == "application" %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::Assign, Liquid::If], source)
end
def test_does_not_merge_if_blocks
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if k == 1 %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if a == foo %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo %}
foo: {{ foo }}
{% endif %}
{% if foo %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if foo >= 1 %}
foo: {{ foo }}
{% endif %}
LIQUID
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
{% if foo == 1 %}
foo: {{ foo }}
{% endif %}
{% if 1 %}
foo: {{ foo }}
{% endif %}
LIQUID
end
private
def assert_optimization(expected, source, context = { "foo" => 1 })
template = Template.parse(source, eager_optimize: true)
assert_equal(expected, template.root.nodelist.map(&:class),)
baseline_template = Template.parse(source, eager_optimize: false)
assert_equal(
baseline_template.render(context),
template.render(context),
)
end
def total_node_count(template)
root = template.root
children = root.nodelist
count = 0
while children.any?
next_children = []
children.each do |node|
count += 1 unless node.is_a?(Liquid::BlockBody)
next_children.concat(node.nodelist) if node.respond_to?(:nodelist) && node.nodelist
end
children = next_children
end
count
end
end
+3 -4
View File
@@ -36,14 +36,14 @@ class EnvironmentFilterTest < Minitest::Test
assert_equal("public", strainer.invoke("public_filter"))
end
def test_strainer_raises_argument_error
def test_stainer_raises_argument_error
strainer = @environment.create_strainer(@context)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_strainer_argument_error_contains_backtrace
def test_stainer_argument_error_contains_backtrace
strainer = @environment.create_strainer(@context)
exception = assert_raises(Liquid::ArgumentError) do
@@ -54,9 +54,8 @@ class EnvironmentFilterTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message,
)
source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source[0..1].map(&:to_s), exception.backtrace[0].split(':')[0..1])
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
end
def test_strainer_only_invokes_public_filter_methods
-25
View File
@@ -1,25 +0,0 @@
# 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
+1 -11
View File
@@ -131,19 +131,9 @@ class LexerUnitTest < Minitest::Test
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
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
def tokenize(input)
Lexer.tokenize(StringScanner.new(input))
Lexer.new(input).tokenize
end
end
-123
View File
@@ -1,123 +0,0 @@
# 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
+10 -16
View File
@@ -6,20 +6,20 @@ class ParserUnitTest < Minitest::Test
include Liquid
def test_consume
p = new_parser("wat: 7")
p = Parser.new("wat: 7")
assert_equal('wat', p.consume(:id))
assert_equal(':', p.consume(:colon))
assert_equal('7', p.consume(:number))
end
def test_jump
p = new_parser("wat: 7")
p = Parser.new("wat: 7")
p.jump(2)
assert_equal('7', p.consume(:number))
end
def test_consume?
p = new_parser("wat: 7")
p = Parser.new("wat: 7")
assert_equal('wat', p.consume?(:id))
assert_equal(false, p.consume?(:dot))
assert_equal(':', p.consume(:colon))
@@ -27,7 +27,7 @@ class ParserUnitTest < Minitest::Test
end
def test_id?
p = new_parser("wat 6 Peter Hegemon")
p = Parser.new("wat 6 Peter Hegemon")
assert_equal('wat', p.id?('wat'))
assert_equal(false, p.id?('endgame'))
assert_equal('6', p.consume(:number))
@@ -36,7 +36,7 @@ class ParserUnitTest < Minitest::Test
end
def test_look
p = new_parser("wat 6 Peter Hegemon")
p = Parser.new("wat 6 Peter Hegemon")
assert_equal(true, p.look(:id))
assert_equal('wat', p.consume(:id))
assert_equal(false, p.look(:comparison))
@@ -46,12 +46,12 @@ class ParserUnitTest < Minitest::Test
end
def test_expressions
p = new_parser("hi.there hi?[5].there? hi.there.bob")
p = Parser.new("hi.there hi?[5].there? hi.there.bob")
assert_equal('hi.there', p.expression)
assert_equal('hi?[5].there?', p.expression)
assert_equal('hi.there.bob', p.expression)
p = new_parser("567 6.0 'lol' \"wut\"")
p = Parser.new("567 6.0 'lol' \"wut\"")
assert_equal('567', p.expression)
assert_equal('6.0', p.expression)
assert_equal("'lol'", p.expression)
@@ -59,7 +59,7 @@ class ParserUnitTest < Minitest::Test
end
def test_ranges
p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal('(5..7)', p.expression)
assert_equal('(1.5..9.6)', p.expression)
assert_equal('(young..old)', p.expression)
@@ -67,7 +67,7 @@ class ParserUnitTest < Minitest::Test
end
def test_arguments
p = new_parser("filter: hi.there[5], keyarg: 7")
p = Parser.new("filter: hi.there[5], keyarg: 7")
assert_equal('filter', p.consume(:id))
assert_equal(':', p.consume(:colon))
assert_equal('hi.there[5]', p.argument)
@@ -77,14 +77,8 @@ class ParserUnitTest < Minitest::Test
def test_invalid_expression
assert_raises(SyntaxError) do
p = new_parser("==")
p = Parser.new("==")
p.expression
end
end
private
def new_parser(str)
Parser.new(StringScanner.new(str))
end
end
+2 -2
View File
@@ -184,7 +184,7 @@ class PartialCacheUnitTest < Minitest::Test
},
)
[:lax, :warn, :strict, :rigid].each do |error_mode|
[:lax, :warn, :strict].each do |error_mode|
Liquid::PartialCache.load(
'my_partial',
context: context,
@@ -193,7 +193,7 @@ class PartialCacheUnitTest < Minitest::Test
end
assert_equal(
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:rigid"],
["my_partial:lax", "my_partial:warn", "my_partial:strict"],
context.registers[:cached_partials].keys,
)
end
+4 -13
View File
@@ -6,18 +6,18 @@ class TagUnitTest < Minitest::Test
include Liquid
def test_tag
tag = Tag.parse('tag', "", new_tokenizer, ParseContext.new)
tag = Tag.parse('tag', "", Tokenizer.new(""), ParseContext.new)
assert_equal('liquid::tag', tag.name)
assert_equal('', tag.render(Context.new))
end
def test_return_raw_text_of_tag
tag = Tag.parse("long_tag", "param1, param2, param3", new_tokenizer, ParseContext.new)
tag = Tag.parse("long_tag", "param1, param2, param3", Tokenizer.new(""), ParseContext.new)
assert_equal("long_tag param1, param2, param3", tag.raw)
end
def test_tag_name_should_return_name_of_the_tag
tag = Tag.parse("some_tag", "", new_tokenizer, ParseContext.new)
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some_tag', tag.tag_name)
end
@@ -26,16 +26,7 @@ class TagUnitTest < Minitest::Test
end
def test_tag_render_to_output_buffer_nil_value
custom_tag = CustomTag.parse("some_tag", "", new_tokenizer, ParseContext.new)
custom_tag = CustomTag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some string', custom_tag.render_to_output_buffer(Context.new, "some string"))
end
private
def new_tokenizer
Tokenizer.new(
source: "",
string_scanner: StringScanner.new(""),
)
end
end
-116
View File
@@ -9,120 +9,4 @@ class CaseTagUnitTest < Minitest::Test
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
assert_equal(['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten)
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
-287
View File
@@ -1,287 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class DocTagUnitTest < Minitest::Test
def test_doc_tag
template = <<~LIQUID.chomp
{% doc %}
Renders loading-spinner.
@param {string} foo - some foo
@param {string} [bar] - optional bar
@example
{% render 'loading-spinner', foo: 'foo' %}
{% render 'loading-spinner', foo: 'foo', bar: 'bar' %}
{% enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_body_content
doc_content = " Documentation content\n @param {string} foo - test\n"
template_source = "{% doc %}#{doc_content}{% enddoc %}"
doc_tag = nil
ParseTreeVisitor
.for(Template.parse(template_source).root)
.add_callback_for(Liquid::Doc) do |tag|
doc_tag = tag
end
.visit
assert_equal(doc_content, doc_tag.nodelist.first.to_s)
end
def test_doc_tag_does_not_support_extra_arguments
error = assert_raises(Liquid::SyntaxError) do
template = <<~LIQUID.chomp
{% doc extra %}
{% enddoc %}
LIQUID
Liquid::Template.parse(template)
end
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}"
act_error = error.message
assert_equal(exp_error, act_error)
end
def test_doc_tag_must_support_valid_tags
assert_match_syntax_error("Liquid syntax error (line 1): 'doc' tag was never closed", '{% doc %} foo')
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo {% enddoc %}')
assert_match_syntax_error("Liquid syntax error (line 1): Syntax Error in 'doc' - Valid syntax: {% doc %}{% enddoc %}", '{% doc } foo %}{% enddoc %}')
end
def test_doc_tag_ignores_liquid_nodes
template = <<~LIQUID.chomp
{% doc %}
{% if true %}
{% if ... %}
{%- for ? -%}
{% while true %}
{%
unless if
%}
{% endcase %}
{% enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_ignores_unclosed_liquid_tags
template = <<~LIQUID.chomp
{% doc %}
{% if true %}
{% enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_does_not_allow_nested_docs
error = assert_raises(Liquid::SyntaxError) do
template = <<~LIQUID.chomp
{% doc %}
{% doc %}
{% doc %}
{% enddoc %}
LIQUID
Liquid::Template.parse(template)
end
exp_error = "Liquid syntax error: Syntax Error in 'doc' - Nested doc tags are not allowed"
act_error = error.message
assert_equal(exp_error, act_error)
end
def test_doc_tag_ignores_nested_raw_tags
template = <<~LIQUID.chomp
{% doc %}
{% raw %}
{% enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_ignores_unclosed_assign
template = <<~LIQUID.chomp
{% doc %}
{% assign foo = "1"
{% enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_ignores_malformed_syntax
template = <<~LIQUID.chomp
{% doc %}
{% {{ {%- enddoc %}
LIQUID
assert_template_result('', template)
end
def test_doc_tag_captures_token_before_enddoc
template_source = "{% doc %}{{ incomplete{% enddoc %}"
doc_tag = nil
ParseTreeVisitor
.for(Template.parse(template_source).root)
.add_callback_for(Liquid::Doc) do |tag|
doc_tag = tag
end
.visit
assert_equal("{{ incomplete", doc_tag.nodelist.first.to_s)
end
def test_doc_tag_preserves_error_line_numbers
template = Liquid::Template.parse(<<~LIQUID.chomp, line_numbers: true)
{% doc %}
{% if true %}
{% enddoc %}
{{ errors.standard_error }}
LIQUID
expected = <<~TEXT.chomp
Liquid error (line 4): standard error
TEXT
assert_equal(expected, template.render('errors' => ErrorDrop.new))
end
def test_doc_tag_whitespace_control
# Basic whitespace control
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%}Hello!")
assert_template_result("Hello!", "{%- doc -%}123{%- enddoc -%} Hello!")
assert_template_result("Hello!", " {%- doc -%}123{%- enddoc -%} Hello!")
assert_template_result("Hello!", <<~LIQUID.chomp)
{%- doc %}Whitespace control!{% enddoc -%}
Hello!
LIQUID
end
def test_doc_tag_delimiter_handling
assert_template_result('', <<~LIQUID.chomp)
{%- if true -%}
{%- doc -%}
{%- docEXTRA -%}wut{% enddocEXTRA -%}xyz
{%- enddoc -%}
{%- endif -%}
LIQUID
assert_template_result('', "{% doc %}123{% enddoc xyz %}")
assert_template_result('', "{% doc %}123{% enddoc\txyz %}")
assert_template_result('', "{% doc %}123{% enddoc\nxyz %}")
assert_template_result('', "{% doc %}123{% enddoc\n xyz enddoc %}")
end
def test_doc_tag_visitor
template_source = '{% doc %}{% enddoc %}'
assert_equal(
[Liquid::Doc],
visit(template_source),
)
end
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
-11
View File
@@ -35,15 +35,4 @@ class TemplateUnitTest < Minitest::Test
def test_template_inheritance
assert_equal("foo", TemplateSubclass.parse("foo").render)
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
-18
View File
@@ -6,7 +6,6 @@ class TokenizerTest < Minitest::Test
def test_tokenize_strings
assert_equal([' '], tokenize(' '))
assert_equal(['hello world'], tokenize('hello world'))
assert_equal(['{}'], tokenize('{}'))
end
def test_tokenize_variables
@@ -31,23 +30,6 @@ class TokenizerTest < Minitest::Test
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
end
def test_tokenize_with_nil_source_returns_empty_array
assert_equal([], tokenize(nil))
end
def test_incomplete_curly_braces
assert_equal(["{{.}", " "], tokenize('{{.} '))
assert_equal(["{{}", "%}"], tokenize('{{}%}'))
assert_equal(["{{}}", "}"], tokenize('{{}}}'))
end
def test_unmatching_start_and_end
assert_equal(["{{%}"], tokenize('{{%}'))
assert_equal(["{{%%%}}"], tokenize('{{%%%}}'))
assert_equal(["{%", "}}"], tokenize('{%}}'))
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
end
private
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
+2 -54
View File
@@ -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-2'), create_variable('foo-bar-2').name)
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
@@ -135,68 +135,16 @@ class VariableUnitTest < Minitest::Test
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
assert_equal(VariableLookup.new('number_of_comments'), var.name)
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
def test_strict_filter_argument_parsing
with_error_modes(:strict) do
with_error_mode(:strict) do
assert_raises(SyntaxError) do
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
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
var = create_variable(%( name_of_variable | upcase ))
assert_equal(" name_of_variable | upcase ", var.raw)