mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-14 08:20:39 -07:00
Compare commits
6
Commits
v6
..
benchmarks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
544b512e7b | ||
|
|
14e70cf5ba | ||
|
|
2420853020 | ||
|
|
baee70ae1f | ||
|
|
41d0d6d51a | ||
|
|
1b1a40c606 |
@@ -1,5 +1,5 @@
|
||||
name: Liquid
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
BUNDLE_JOBS: 4
|
||||
@@ -9,33 +9,30 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 3.3, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: 4.0, allowed-failure: false } # latest stable
|
||||
- { 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: 4.0,
|
||||
ruby: 3.4,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
|
||||
|
||||
# Head can have failures due to being in development
|
||||
- { ruby: head, allowed-failure: true }
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: ruby-head, allowed-failure: false }
|
||||
- {
|
||||
ruby: head,
|
||||
allowed-failure: true,
|
||||
ruby: ruby-head,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: head, allowed-failure: true, rubyopt: "--yjit" }
|
||||
- { ruby: head, allowed-failure: true, rubyopt: "--zjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }} ${{ matrix.entry.rubyopt }} --${{ matrix.entry.allowed-failure && 'allowed-failure' || 'strict' }}
|
||||
- { 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@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
@@ -45,28 +42,11 @@ jobs:
|
||||
env:
|
||||
RUBYOPT: ${{ matrix.entry.rubyopt }}
|
||||
|
||||
spec:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUNDLE_WITH: spec
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
bundler: latest
|
||||
- name: Run liquid-spec for all adapters
|
||||
run: |
|
||||
for adapter in spec/*.rb; do
|
||||
echo "=== Running $adapter ==="
|
||||
bundle exec liquid-spec run "$adapter" --no-max-failures
|
||||
done
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
|
||||
+1
-10
@@ -174,16 +174,7 @@ Style/WordArray:
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns.
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
||||
|
||||
Naming/PredicatePrefix:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# This is intentional - early return from begin/rescue in assignment context
|
||||
Lint/NoReturnInBeginEndBlocks:
|
||||
Exclude:
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
|
||||
@@ -25,13 +25,7 @@ group :development do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'benchmark'
|
||||
gem 'rubocop', '~> 1.82.0'
|
||||
gem 'rubocop-shopify', '~> 2.18.0', require: false
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
end
|
||||
|
||||
group :spec do
|
||||
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main'
|
||||
gem 'activesupport', require: false
|
||||
end
|
||||
|
||||
-10
@@ -1,15 +1,5 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.11.0
|
||||
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
|
||||
* Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
|
||||
|
||||
## 5.10.0
|
||||
* Introduce support for Inline Snippets [Julia Boutin]
|
||||
|
||||
## 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]
|
||||
|
||||
|
||||
@@ -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 = :strict2 # 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`:
|
||||
|
||||
@@ -33,7 +33,7 @@ task :rubocop do
|
||||
end
|
||||
end
|
||||
|
||||
desc('runs test suite with lax, strict, and strict2 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'] = 'strict2'
|
||||
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'] = 'strict2'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
@@ -88,13 +80,8 @@ namespace :benchmark do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict2 parsing"
|
||||
task :strict2 do
|
||||
ruby "./performance/benchmark.rb strict2"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
|
||||
task run: [:lax, :strict, :strict2]
|
||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
||||
task run: [:lax, :strict]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
@@ -148,9 +135,3 @@ end
|
||||
task :console do
|
||||
exec 'irb -I lib -r liquid'
|
||||
end
|
||||
|
||||
desc('run liquid-spec suite across all adapters')
|
||||
task :spec do
|
||||
adapters = Dir['./spec/*.rb'].join(',')
|
||||
sh "bundle exec liquid-spec matrix --adapters=#{adapters} --reference=ruby_liquid"
|
||||
end
|
||||
|
||||
-46
@@ -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: :strict2)
|
||||
.tap { |t| t.registers[:file_system] = VirtualFileSystem.new }
|
||||
.render(assigns)
|
||||
+14
-57
@@ -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
|
||||
@@ -113,67 +113,24 @@ module Liquid
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(MethodLiteral)
|
||||
return call_method_literal(left, right)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(MethodLiteral)
|
||||
return call_method_literal(right, left)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
left == right
|
||||
end
|
||||
|
||||
def call_method_literal(literal, value)
|
||||
method_name = literal.method_name
|
||||
|
||||
# If the object responds to the method (e.g., ActiveSupport is loaded), use it
|
||||
if value.respond_to?(method_name)
|
||||
value.send(method_name)
|
||||
else
|
||||
# Emulate ActiveSupport's blank?/empty? to make Liquid invariant
|
||||
# to whether ActiveSupport is loaded or not
|
||||
case method_name
|
||||
when :blank?
|
||||
liquid_blank?(value)
|
||||
when :empty?
|
||||
liquid_empty?(value)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Implement blank? semantics matching ActiveSupport
|
||||
# blank? returns true for nil, false, empty strings, whitespace-only strings,
|
||||
# empty arrays, and empty hashes
|
||||
def liquid_blank?(value)
|
||||
case value
|
||||
when NilClass, FalseClass
|
||||
true
|
||||
when TrueClass, Numeric
|
||||
false
|
||||
when String
|
||||
# Blank if empty or whitespace only (matches ActiveSupport)
|
||||
value.empty? || value.match?(/\A\s*\z/)
|
||||
when Array, Hash
|
||||
value.empty?
|
||||
else
|
||||
# Fall back to empty? if available, otherwise false
|
||||
value.respond_to?(:empty?) ? value.empty? : false
|
||||
end
|
||||
end
|
||||
|
||||
# Implement empty? semantics
|
||||
# Note: nil is NOT empty. empty? checks if a collection has zero elements.
|
||||
def liquid_empty?(value)
|
||||
case value
|
||||
when String, Array, Hash
|
||||
value.empty?
|
||||
else
|
||||
value.respond_to?(:empty?) ? value.empty? : false
|
||||
end
|
||||
end
|
||||
|
||||
def interpret_condition(left, right, op, context)
|
||||
# If the operator is empty this means that the decision statement is just
|
||||
# a single variable. We can just poll this variable from the context and
|
||||
@@ -197,8 +154,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
||||
"and will be removed from Liquid 6.0.0.")
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
find_variable(key, raise_on_not_found: false) != nil
|
||||
self[key] != nil
|
||||
end
|
||||
|
||||
def evaluate(object)
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ module Liquid
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return unless @context&.strict_variables
|
||||
return nil unless @context&.strict_variables
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
|
||||
@@ -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 :strict2, :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.
|
||||
|
||||
@@ -28,10 +28,6 @@ module Liquid
|
||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||
|
||||
class << self
|
||||
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
|
||||
parse(parser.expression, ss, cache)
|
||||
end
|
||||
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
|
||||
@@ -55,7 +51,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def inner_parse(markup, ss, cache)
|
||||
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
|
||||
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
||||
return RangeLookup.parse(
|
||||
Regexp.last_match(1),
|
||||
Regexp.last_match(2),
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ module Liquid
|
||||
def interpolate(name, vars)
|
||||
name.gsub(/%\{(\w+)\}/) do
|
||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||
vars[Regexp.last_match(1).to_sym].to_s
|
||||
(vars[Regexp.last_match(1).to_sym]).to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
invalid_template_encoding: "Invalid template encoding"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
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"
|
||||
|
||||
@@ -50,22 +50,7 @@ module Liquid
|
||||
)
|
||||
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 == :strict2
|
||||
# parse_expression is a widely used API. To maintain backward
|
||||
# compatibility while raising awareness about strict2 parser standards,
|
||||
# the safe flag supports API users make a deliberate decision.
|
||||
#
|
||||
# In strict2 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 strict2 mode"
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
|
||||
@@ -2,25 +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 strict2 mode where it uses the strict2 parser.
|
||||
#
|
||||
# @deprecated Use {#parse_with_selected_parser} instead.
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
return strict2_parse_with_error_context(markup) if strict2_mode?
|
||||
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :rigid
|
||||
rigid_warn
|
||||
raise
|
||||
when :strict2
|
||||
raise
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
@@ -31,13 +16,11 @@ module Liquid
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :rigid then rigid_warn && strict2_parse_with_error_context(markup)
|
||||
when :strict2 then strict2_parse_with_error_context(markup)
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
strict2_parse_with_error_context(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_parse(markup)
|
||||
@@ -45,24 +28,8 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def strict2_mode?
|
||||
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rigid_warn
|
||||
Deprecations.warn(':rigid', ':strict2')
|
||||
end
|
||||
|
||||
def strict2_parse_with_error_context(markup)
|
||||
strict2_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
|
||||
|
||||
@@ -768,8 +768,6 @@ module Liquid
|
||||
# @liquid_syntax array | first
|
||||
# @liquid_return [untyped]
|
||||
def first(array)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
return array[0] || "" if array.is_a?(String)
|
||||
array.first if array.respond_to?(:first)
|
||||
end
|
||||
|
||||
@@ -781,8 +779,6 @@ module Liquid
|
||||
# @liquid_syntax array | last
|
||||
# @liquid_return [untyped]
|
||||
def last(array)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
return array[-1] || "" if array.is_a?(String)
|
||||
array.last if array.respond_to?(:last)
|
||||
end
|
||||
|
||||
|
||||
+2
-6
@@ -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
|
||||
|
||||
+6
-42
@@ -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 strict2_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 strict2_mode?
|
||||
parse_strict2_when(markup, body)
|
||||
else
|
||||
parse_lax_when(markup, body)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_strict2_when(markup, body)
|
||||
parser = @parse_context.new_parser(markup)
|
||||
|
||||
loop do
|
||||
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
|
||||
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")
|
||||
|
||||
+16
-69
@@ -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,19 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
# cycle [name:] expression(, expression)*
|
||||
def strict2_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)
|
||||
|
||||
# Expression Parser returns cached objects, and we need to dup them to
|
||||
# start the cycle over for each new cycle call.
|
||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
||||
var = parse_expression(Regexp.last_match(1))
|
||||
maybe_dup_lookup(var)
|
||||
var.is_a?(VariableLookup) ? var.dup : var
|
||||
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)
|
||||
|
||||
+7
-11
@@ -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
|
||||
@@ -93,7 +93,7 @@ module Liquid
|
||||
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 strict2_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
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'
|
||||
ElseCondition.new
|
||||
@@ -81,8 +77,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)
|
||||
@@ -124,9 +120,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)
|
||||
|
||||
+18
-44
@@ -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 strict2_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
|
||||
[
|
||||
|
||||
+16
-50
@@ -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?
|
||||
@@ -84,55 +99,6 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
# render (string) (with|for expression)? (as id)? (key: value)*
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@template_name_expr = parse_expression(strict2_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)
|
||||
end
|
||||
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def strict2_template_name(p)
|
||||
p.consume(:string)
|
||||
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
|
||||
[
|
||||
|
||||
@@ -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 strict2_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))
|
||||
|
||||
@@ -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
|
||||
# :strict2 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
|
||||
|
||||
@@ -117,7 +117,7 @@ module Liquid
|
||||
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
|
||||
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
|
||||
|
||||
break unless byte_a
|
||||
|
||||
|
||||
+3
-8
@@ -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
|
||||
@@ -69,14 +66,14 @@ module Liquid
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
if obj.is_a?(String)
|
||||
return if obj.empty?
|
||||
return nil if obj.empty?
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
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)
|
||||
@@ -95,8 +92,6 @@ module Liquid
|
||||
|
||||
def self.to_s(obj, seen = {})
|
||||
case obj
|
||||
when BigDecimal
|
||||
obj.to_s("F")
|
||||
when Hash
|
||||
# If the custom hash implementation overrides `#to_s`, use their
|
||||
# custom implementation. Otherwise we use Liquid's default
|
||||
|
||||
+4
-55
@@ -54,7 +54,7 @@ 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
|
||||
@@ -65,26 +65,15 @@ module Liquid
|
||||
|
||||
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 strict2_parse(markup)
|
||||
@filters = []
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.safe_parse_expression(p)
|
||||
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def parse_filterargs(p)
|
||||
# first argument
|
||||
filterargs = [p.argument]
|
||||
@@ -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 strict2_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
|
||||
|
||||
@@ -70,11 +70,6 @@ module Liquid
|
||||
elsif lookup_command?(i) && object.respond_to?(key)
|
||||
object = object.send(key).to_liquid
|
||||
|
||||
# Handle string first/last like ActiveSupport does (returns first/last character)
|
||||
# ActiveSupport returns "" for empty strings, not nil
|
||||
elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last")
|
||||
object = key == "first" ? (object[0] || "") : (object[-1] || "")
|
||||
|
||||
# No key was present with the desired value and it wasn't one of the directly supported
|
||||
# keywords either. The only thing we got left is to return nil or
|
||||
# raise an exception if `strict_variables` option is set to true
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.11.0"
|
||||
VERSION = "5.8.7"
|
||||
end
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
if defined?(RubyVM::YJIT)
|
||||
RubyVM::YJIT.enable
|
||||
puts "* YJIT enabled"
|
||||
else
|
||||
puts "* YJIT not enabled"
|
||||
end
|
||||
|
||||
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
@@ -18,8 +24,8 @@ Benchmark.ips do |x|
|
||||
|
||||
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("tokenize:") { profiler.tokenize_all } if phase == "all" || phase == "tokenize"
|
||||
x.report("parse:") { profiler.compile_all } if phase == "all" || phase == "parse"
|
||||
x.report("render:") { profiler.render_all } if phase == "all" || phase == "render"
|
||||
x.report("parse & render:") { profiler.run_all } if phase == "all" || phase == "run"
|
||||
end
|
||||
|
||||
@@ -57,7 +57,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
x.profile('render') { runner.render }
|
||||
x.profile('parse') { runner.compile_all }
|
||||
x.profile('render') { runner.render_all }
|
||||
x.tabulate
|
||||
end
|
||||
|
||||
+26
-10
@@ -1,26 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'stackprof'
|
||||
require 'fileutils'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
output_dir = ENV['OUTPUT_DIR'] || "/tmp/liquid-performance"
|
||||
FileUtils.mkdir_p(output_dir)
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
profiler.run_all # warmup
|
||||
|
||||
[:cpu, :object].each do |profile_type|
|
||||
puts "Profiling in #{profile_type} mode..."
|
||||
results = StackProf.run(mode: profile_type) do
|
||||
[:cpu, :object].each do |mode|
|
||||
puts
|
||||
puts "Profiling in #{mode} mode..."
|
||||
puts "writing to #{output_dir}/#{mode}.profile:"
|
||||
puts
|
||||
StackProf.run(mode: mode, raw: true, out: "#{output_dir}/#{mode}.profile") do
|
||||
200.times do
|
||||
profiler.run
|
||||
profiler.run_all
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
result = StackProf.run(mode: mode) do
|
||||
100.times do
|
||||
profiler.run_all
|
||||
end
|
||||
end
|
||||
|
||||
StackProf::Report.new(results).print_text(false, 20)
|
||||
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
|
||||
StackProf::Report.new(result).print_text(false, 30)
|
||||
end
|
||||
|
||||
puts
|
||||
puts "files in #{output_dir}:"
|
||||
Dir.glob("#{output_dir}/*").each do |file|
|
||||
puts " #{file}"
|
||||
end
|
||||
puts "Recommended:"
|
||||
puts " stackprof --d3-flamegraph #{output_dir}/cpu.profile > #{output_dir}/flame.html"
|
||||
puts " stackprof --method #{output_dir}/cpu.profile"
|
||||
puts " etc"
|
||||
|
||||
@@ -32,19 +32,36 @@ module Database
|
||||
end
|
||||
end
|
||||
|
||||
# Some standard direct accessors so that the specialized templates
|
||||
# render correctly
|
||||
db['collection'] = db['collections'].values.first
|
||||
db['product'] = db['products'].values.first
|
||||
db['blog'] = db['blogs'].values.first
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
# Some standard direct accessors so that the specialized templates
|
||||
# render correctly
|
||||
db['collection'] = db['collections'].values.first
|
||||
db['collection']['tags'] = db['collection']['products'].map { |product| product['tags'] }.flatten.uniq.sort
|
||||
|
||||
db['tags'] = db['collection']['tags'][0..1]
|
||||
db['all_tags'] = db['products'].values.map { |product| product['tags'] }.flatten.uniq.sort
|
||||
db['current_tags'] = db['collection']['tags'][0..1]
|
||||
db['handle'] = db['collection']['handle']
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
db['linklists'] = db['link_lists']
|
||||
|
||||
db['shop'] = {
|
||||
'name' => 'Snowdevil',
|
||||
'currency' => 'USD',
|
||||
'money_format' => '${{amount}}',
|
||||
'money_with_currency_format' => '${{amount}} USD',
|
||||
'money_format_with_currency' => 'USD ${{amount}}',
|
||||
}
|
||||
|
||||
db
|
||||
end
|
||||
end
|
||||
|
||||
@@ -345,8 +345,7 @@ products:
|
||||
featured_image: products/arbor_draft.jpg
|
||||
images:
|
||||
- products/arbor_draft.jpg
|
||||
description:
|
||||
The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
|
||||
description: The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
|
||||
variants:
|
||||
- *product-1-var-1
|
||||
- *product-1-var-2
|
||||
@@ -377,8 +376,7 @@ products:
|
||||
featured_image: products/element58.jpg
|
||||
images:
|
||||
- products/element58.jpg
|
||||
description:
|
||||
The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
|
||||
description: The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
|
||||
variants:
|
||||
- *product-2-var-1
|
||||
|
||||
@@ -411,8 +409,7 @@ products:
|
||||
- products/technine1.jpg
|
||||
- products/technine2.jpg
|
||||
- products/technine_detail.jpg
|
||||
description:
|
||||
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
variants:
|
||||
- *product-3-var-1
|
||||
- *product-3-var-2
|
||||
@@ -446,8 +443,7 @@ products:
|
||||
images:
|
||||
- products/technine3.jpg
|
||||
- products/technine4.jpg
|
||||
description:
|
||||
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
|
||||
variants:
|
||||
- *product-4-var-1
|
||||
|
||||
@@ -478,8 +474,7 @@ products:
|
||||
featured_image: products/burton.jpg
|
||||
images:
|
||||
- products/burton.jpg
|
||||
description:
|
||||
The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
|
||||
description: The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
|
||||
variants:
|
||||
- *product-5-var-1
|
||||
- *product-5-var-2
|
||||
@@ -516,8 +511,7 @@ products:
|
||||
featured_image: products/ducati.jpg
|
||||
images:
|
||||
- products/ducati.jpg
|
||||
description:
|
||||
<h3>‘S’ PERFORMANCE</h3>
|
||||
description: <h3>‘S’ PERFORMANCE</h3>
|
||||
<p>Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.</p>
|
||||
<p>The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.</p>
|
||||
variants:
|
||||
@@ -636,7 +630,6 @@ products:
|
||||
- *product-9-var-2
|
||||
- *product-9-var-3
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Line Items
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
@@ -644,8 +637,8 @@ products:
|
||||
line_items:
|
||||
- &line_item-1
|
||||
id: 1
|
||||
title: 'Arbor Draft'
|
||||
subtitle: '151cm'
|
||||
title: "Arbor Draft"
|
||||
subtitle: "151cm"
|
||||
price: 29900
|
||||
line_price: 29900
|
||||
quantity: 1
|
||||
@@ -654,8 +647,8 @@ line_items:
|
||||
|
||||
- &line_item-2
|
||||
id: 2
|
||||
title: 'Comic ~ Orange'
|
||||
subtitle: '159cm'
|
||||
title: "Comic ~ Orange"
|
||||
subtitle: "159cm"
|
||||
price: 19900
|
||||
line_price: 39800
|
||||
quantity: 2
|
||||
@@ -681,7 +674,7 @@ links:
|
||||
- &link-4
|
||||
id: 4
|
||||
title: Powered by Shopify
|
||||
url: 'http://shopify.com'
|
||||
url: "http://shopify.com"
|
||||
- &link-5
|
||||
id: 5
|
||||
title: About Us
|
||||
@@ -715,8 +708,6 @@ links:
|
||||
title: Catalog
|
||||
url: /collections/all
|
||||
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Link Lists
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
@@ -724,8 +715,8 @@ links:
|
||||
link_lists:
|
||||
- &link-list-1
|
||||
id: 1
|
||||
title: 'Main Menu'
|
||||
handle: 'main-menu'
|
||||
title: "Main Menu"
|
||||
handle: "main-menu"
|
||||
links:
|
||||
- *link-12
|
||||
- *link-5
|
||||
@@ -733,8 +724,8 @@ link_lists:
|
||||
- *link-8
|
||||
- &link-list-2
|
||||
id: 1
|
||||
title: 'Footer Menu'
|
||||
handle: 'footer'
|
||||
title: "Footer Menu"
|
||||
handle: "footer"
|
||||
links:
|
||||
- *link-5
|
||||
- *link-6
|
||||
@@ -768,8 +759,7 @@ collections:
|
||||
title: Snowboards
|
||||
handle: snowboards
|
||||
url: /collections/snowboards
|
||||
description:
|
||||
<p>This is a description for my <strong>Snowboards</strong> collection.</p>
|
||||
description: <p>This is a description for my <strong>Snowboards</strong> collection.</p>
|
||||
products:
|
||||
- *product-1
|
||||
- *product-2
|
||||
@@ -787,8 +777,8 @@ collections:
|
||||
- &collection-5
|
||||
id: 5
|
||||
title: Paginated Sale
|
||||
handle: 'paginated-sale'
|
||||
url: '/collections/paginated-sale'
|
||||
handle: "paginated-sale"
|
||||
url: "/collections/paginated-sale"
|
||||
products:
|
||||
- *product-1
|
||||
- *product-2
|
||||
@@ -799,8 +789,8 @@ collections:
|
||||
- &collection-6
|
||||
id: 6
|
||||
title: All products
|
||||
handle: 'all'
|
||||
url: '/collections/all'
|
||||
handle: "all"
|
||||
url: "/collections/all"
|
||||
products:
|
||||
- *product-7
|
||||
- *product-8
|
||||
@@ -812,7 +802,6 @@ collections:
|
||||
- *product-4
|
||||
- *product-5
|
||||
|
||||
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
# Pages and Blogs
|
||||
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||
@@ -823,8 +812,7 @@ pages:
|
||||
handle: contact
|
||||
url: /pages/contact
|
||||
author: Tobi
|
||||
content:
|
||||
"<p>You can contact us via phone under (555) 567-2222.</p>
|
||||
content: "<p>You can contact us via phone under (555) 567-2222.</p>
|
||||
<p>Our retail store is located at <em>Rue d'Avignon 32, Avignon (Provence)</em>.</p>
|
||||
<p><strong>Opening Hours:</strong><br />Monday through Friday: 9am - 6pm<br />Saturday: 10am - 3pm<br />Sunday: closed</p>"
|
||||
created_at: 2005-04-04 12:00
|
||||
@@ -874,12 +862,12 @@ blogs:
|
||||
url: /blogs/news
|
||||
articles:
|
||||
- id: 3
|
||||
title: 'Welcome to the new Foo Shop'
|
||||
title: "Welcome to the new Foo Shop"
|
||||
author: Daniel
|
||||
content: <p><strong>Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.</strong></p><p>To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.</p><p>The text you see here is an article. To edit this article, create new articles or create new pages you can go to the <a href="/admin/pages">Blogs & Pages</a> tab of the administration menu.</p><p>The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the <a href="/admin/products">Products Tab</a> in of the administration menu.</p><p>While you're looking around be sure to check out the <a href="/admin/collections">Collections</a> and <a href="/admin/links">Navigations</a> tabs and soon you will be well on your way to populating your site.</p><p>And of course don't forget to browse the <a href="admin/design/appearance/themes">theme gallery</a> to pick a new look for your shop!</p><p><strong>Shopify is in beta</strong><br />If you would like to make comments or suggestions please visit us in the <a href="http://forums.shopify.com/community">Shopify Forums</a> or drop us an <a href="mailto:[email protected]">email</a>.</p>
|
||||
created_at: 2005-04-04 16:00
|
||||
- id: 4
|
||||
title: 'Breaking News: Restock on all sales products'
|
||||
title: "Breaking News: Restock on all sales products"
|
||||
author: Tobi
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 12:00
|
||||
@@ -891,13 +879,12 @@ blogs:
|
||||
url: /blogs/bigcheese-blog
|
||||
articles:
|
||||
- id: 1
|
||||
title: 'One thing you probably did not know yet...'
|
||||
title: "One thing you probably did not know yet..."
|
||||
author: Justin
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 16:00
|
||||
comments:
|
||||
-
|
||||
id: 1
|
||||
- id: 1
|
||||
author: John Smith
|
||||
email: [email protected]
|
||||
content: Wow...great article man.
|
||||
@@ -905,8 +892,7 @@ blogs:
|
||||
created_at: 2009-01-01 12:00
|
||||
updated_at: 2009-02-01 12:00
|
||||
url: ""
|
||||
-
|
||||
id: 2
|
||||
- id: 2
|
||||
author: John Jones
|
||||
email: [email protected]
|
||||
content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks!
|
||||
@@ -932,7 +918,7 @@ blogs:
|
||||
url: /blogs/paginated-blog
|
||||
articles:
|
||||
- id: 6
|
||||
title: 'One thing you probably did not know yet...'
|
||||
title: "One thing you probably did not know yet..."
|
||||
author: Justin
|
||||
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
created_at: 2005-04-04 16:00
|
||||
|
||||
+73
-61
@@ -25,23 +25,41 @@ class ThemeRunner
|
||||
|
||||
# Initialize a new liquid ThemeRunner instance
|
||||
# Will load all templates into memory, do this now so that we don't profile IO.
|
||||
def initialize
|
||||
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
||||
def initialize(strictness: {})
|
||||
@strictness = strictness
|
||||
@tests = []
|
||||
Dir[__dir__ + '/tests/**/*.liquid'].each do |test|
|
||||
next if File.basename(test) == 'theme.liquid'
|
||||
|
||||
theme_path = File.dirname(test) + '/theme.liquid'
|
||||
{
|
||||
liquid: File.read(test),
|
||||
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
|
||||
template_name: test,
|
||||
}
|
||||
end.compact
|
||||
theme_path = File.realpath(File.dirname(test))
|
||||
theme_name = File.basename(theme_path)
|
||||
test_name = theme_name + "/" + File.basename(test)
|
||||
template_name = File.basename(test, '.liquid')
|
||||
layout_path = theme_path + '/theme.liquid'
|
||||
|
||||
compile_all_tests
|
||||
test = {
|
||||
test_name: test_name,
|
||||
liquid: File.read(test),
|
||||
layout: File.file?(layout_path) ? File.read(layout_path) : nil,
|
||||
template_name: template_name,
|
||||
theme_name: theme_name,
|
||||
theme_path: theme_path,
|
||||
}
|
||||
|
||||
@tests << test
|
||||
end
|
||||
end
|
||||
|
||||
def find_test(test_name)
|
||||
@tests.find do |test_hash|
|
||||
test_hash[:test_name] == test_name
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :tests
|
||||
|
||||
# `compile` will test just the compilation portion of liquid without any templates
|
||||
def compile
|
||||
def compile_all
|
||||
@tests.each do |test_hash|
|
||||
Liquid::Template.new.parse(test_hash[:liquid])
|
||||
Liquid::Template.new.parse(test_hash[:layout])
|
||||
@@ -49,7 +67,7 @@ class ThemeRunner
|
||||
end
|
||||
|
||||
# `tokenize` will just test the tokenizen portion of liquid without any templates
|
||||
def tokenize
|
||||
def tokenize_all
|
||||
ss = StringScanner.new("")
|
||||
@tests.each do |test_hash|
|
||||
tokenizer = Liquid::Tokenizer.new(
|
||||
@@ -62,78 +80,72 @@ class ThemeRunner
|
||||
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|
|
||||
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||
def run_all
|
||||
@tests.each do |test|
|
||||
compile_and_render(test)
|
||||
end
|
||||
end
|
||||
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
def render_all
|
||||
@compiled_tests ||= compile_all_tests
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
layout.render!(assigns)
|
||||
else
|
||||
tmpl.render!(assigns)
|
||||
end
|
||||
render_template(test)
|
||||
end
|
||||
end
|
||||
|
||||
def run_one_test(test_name)
|
||||
test = find_test(test_name)
|
||||
compile_and_render(test)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_layout(template, layout, assigns)
|
||||
assigns['content_for_layout'] = template.render!(assigns)
|
||||
layout&.render!(assigns)
|
||||
def render_template(compiled_test)
|
||||
tmpl, layout, assigns = compiled_test.values_at(:tmpl, :layout, :assigns)
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns, @strictness)
|
||||
rendered_layout = layout.render!(assigns, @strictness)
|
||||
rendered_layout
|
||||
else
|
||||
tmpl.render!(assigns, @strictness)
|
||||
end
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
||||
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
|
||||
def compile_and_render(test)
|
||||
compiled_test = compile_test(test)
|
||||
render_template(compiled_test)
|
||||
end
|
||||
|
||||
def compile_all_tests
|
||||
@compiled_tests = []
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
|
||||
@tests.each do |test_hash|
|
||||
@compiled_tests << compile_test(test_hash)
|
||||
end
|
||||
@compiled_tests
|
||||
end
|
||||
|
||||
def compile_test(template, layout, assigns, page_template, template_file)
|
||||
tmpl = init_template(page_template, template_file)
|
||||
parsed_template = tmpl.parse(template).dup
|
||||
def compile_test(test_hash)
|
||||
theme_path, template_name, layout, liquid = test_hash.values_at(:theme_path, :template_name, :layout, :liquid)
|
||||
|
||||
assigns = Database.tables.dup
|
||||
assigns.merge!({
|
||||
'title' => 'Page title',
|
||||
'page_title' => 'Page title',
|
||||
'content_for_header' => '',
|
||||
'template' => template_name,
|
||||
})
|
||||
|
||||
fs = ThemeRunner::FileSystem.new(theme_path)
|
||||
|
||||
result = {}
|
||||
result[:assigns] = assigns
|
||||
result[:tmpl] = Liquid::Template.parse(liquid, registers: { file_system: fs })
|
||||
|
||||
if layout
|
||||
parsed_layout = tmpl.parse(layout)
|
||||
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
|
||||
else
|
||||
{ tmpl: parsed_template, assigns: assigns }
|
||||
result[:layout] = Liquid::Template.parse(layout, registers: { file_system: fs })
|
||||
end
|
||||
end
|
||||
|
||||
# utility method with similar functionality needed in `compile_all_tests` and `run`
|
||||
def each_test
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
|
||||
@tests.each do |test_hash|
|
||||
# Compute page_template outside of profiler run, uninteresting to profiler
|
||||
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
|
||||
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
|
||||
end
|
||||
end
|
||||
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
tmpl
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation)
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
# Run core Liquid specs
|
||||
config.features = [:core]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
ctx[:template] = Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
# @param ctx [Hash] adapter context containing :template
|
||||
# @param assigns [Hash] environment variables
|
||||
# @param options [Hash] :registers, :strict_errors, :exception_renderer
|
||||
LiquidSpec.render do |ctx, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid with lax parsing mode
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid_lax.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.features = [:core, :lax_parsing]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
# Force lax mode
|
||||
options = options.merge(error_mode: :lax)
|
||||
ctx[:template] = Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
LiquidSpec.render do |ctx, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid with ActiveSupport loaded
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid_with_active_support.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'active_support/all'
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
# Run core Liquid specs plus ActiveSupport SafeBuffer tests
|
||||
config.features = [:core, :activesupport]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
ctx[:template] = Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
# @param ctx [Hash] adapter context containing :template
|
||||
# @param assigns [Hash] environment variables
|
||||
# @param options [Hash] :registers, :strict_errors, :exception_renderer
|
||||
LiquidSpec.render do |ctx, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid with YJIT + strict mode + ActiveSupport
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid_yjit.rb
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
|
||||
# Enable YJIT if available
|
||||
if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
|
||||
RubyVM::YJIT.enable
|
||||
end
|
||||
|
||||
require 'active_support/all'
|
||||
require 'liquid'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.features = [:core, :activesupport]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
# Force strict mode
|
||||
options = { error_mode: :strict }.merge(options)
|
||||
ctx[:template] = Liquid::Template.parse(source, **options)
|
||||
end
|
||||
|
||||
# Render a compiled template with the given context
|
||||
LiquidSpec.render do |ctx, assigns, options|
|
||||
registers = Liquid::Registers.new(options[:registers] || {})
|
||||
|
||||
context = Liquid::Context.build(
|
||||
static_environments: assigns,
|
||||
registers: registers,
|
||||
rethrow_errors: options[:strict_errors],
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
@@ -632,28 +632,13 @@ 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)
|
||||
end
|
||||
end
|
||||
|
||||
def test_key_lookup_will_raise_for_missing_keys_when_strict_variables_is_enabled
|
||||
context = Context.new
|
||||
context.strict_variables = true
|
||||
assert_raises(Liquid::UndefinedVariable) do
|
||||
context['unknown']
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_key_will_not_raise_for_missing_keys_when_strict_variables_is_enabled
|
||||
context = Context.new
|
||||
context.strict_variables = true
|
||||
refute(context.key?('unknown'))
|
||||
assert_empty(context.errors)
|
||||
end
|
||||
|
||||
def test_context_always_uses_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,12 +26,8 @@ class ExpressionTest < Minitest::Test
|
||||
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(0.0, "0.....5")
|
||||
assert_expression_result(0.0, "-0..1")
|
||||
assert_expression_result(1.5, "1.5")
|
||||
|
||||
# this is a unfortunate quirky behavior of Liquid
|
||||
@@ -65,7 +61,6 @@ class ExpressionTest < Minitest::Test
|
||||
assert_template_result(
|
||||
"",
|
||||
"{{ - 'theme.css' - }}",
|
||||
error_mode: :lax,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -152,35 +147,6 @@ class ExpressionTest < Minitest::Test
|
||||
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)
|
||||
|
||||
@@ -88,11 +88,17 @@ class HashRenderingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
|
||||
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => HashWithCustomToS.new })
|
||||
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 = HashWithoutCustomToS.new
|
||||
my_hash = Class.new(Hash).new
|
||||
my_hash[:foo] = :bar
|
||||
|
||||
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||
|
||||
@@ -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,7 +133,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_incomplete_expression
|
||||
with_error_modes(:lax) do
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result("false", "{{ false - }}")
|
||||
assert_template_result("false", "{{ false > }}")
|
||||
assert_template_result("false", "{{ false < }}")
|
||||
|
||||
@@ -59,7 +59,7 @@ class SecurityTest < Minitest::Test
|
||||
|
||||
GC.start
|
||||
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
@@ -70,7 +70,7 @@ class SecurityTest < Minitest::Test
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
|
||||
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
|
||||
@@ -116,7 +116,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_slice_on_arrays
|
||||
input = 'foobar'.split('')
|
||||
input = 'foobar'.split(//)
|
||||
assert_equal(%w(o o b), @filters.slice(input, 1, 3))
|
||||
assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
|
||||
assert_equal(%w(), @filters.slice(input, 1, 0))
|
||||
@@ -294,7 +294,13 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_join_calls_to_liquid_on_each_element
|
||||
assert_equal('i did it, i did it', @filters.join([CustomToLiquidDrop.new('i did it'), CustomToLiquidDrop.new('i did it')], ", "))
|
||||
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
|
||||
@@ -627,40 +633,6 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_nil(@filters.last([]))
|
||||
end
|
||||
|
||||
def test_first_last_on_strings
|
||||
# Ruby's String class does not have first/last methods by default.
|
||||
# ActiveSupport adds String#first and String#last to return the first/last character.
|
||||
# Liquid must work without ActiveSupport, so the first/last filters handle strings specially.
|
||||
#
|
||||
# This enables template patterns like:
|
||||
# {{ product.title | first }} => "S" (for "Snowboard")
|
||||
# {{ customer.name | last }} => "h" (for "Smith")
|
||||
#
|
||||
# Note: ActiveSupport returns "" for empty strings, not nil.
|
||||
assert_equal('f', @filters.first('foo'))
|
||||
assert_equal('o', @filters.last('foo'))
|
||||
assert_equal('', @filters.first(''))
|
||||
assert_equal('', @filters.last(''))
|
||||
end
|
||||
|
||||
def test_first_last_on_unicode_strings
|
||||
# Unicode strings should return the first/last grapheme cluster (character),
|
||||
# not the first/last byte. Ruby's String#[] handles this correctly with index 0/-1.
|
||||
# This ensures international text works properly:
|
||||
# {{ korean_name | first }} => "고" (not a partial byte sequence)
|
||||
assert_equal('고', @filters.first('고스트빈'))
|
||||
assert_equal('빈', @filters.last('고스트빈'))
|
||||
end
|
||||
|
||||
def test_first_last_on_strings_via_template
|
||||
# Integration test to verify the filter works end-to-end in templates.
|
||||
# Empty strings return empty output (nil renders as empty string).
|
||||
assert_template_result('f', '{{ name | first }}', { 'name' => 'foo' })
|
||||
assert_template_result('o', '{{ name | last }}', { 'name' => 'foo' })
|
||||
assert_template_result('', '{{ name | first }}', { 'name' => '' })
|
||||
assert_template_result('', '{{ name | last }}', { 'name' => '' })
|
||||
end
|
||||
|
||||
def test_replace
|
||||
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
|
||||
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
|
||||
@@ -1330,7 +1302,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
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.4, @filters.sum(input, (1..5)))
|
||||
assert_equal(0, @filters.sum(input, nil))
|
||||
assert_equal(0, @filters.sum(input, ""))
|
||||
end
|
||||
|
||||
@@ -3,10 +3,20 @@
|
||||
require 'test_helper'
|
||||
|
||||
class CycleTagTest < Minitest::Test
|
||||
def test_simple_cycle
|
||||
template = <<~LIQUID
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_simple_cycle_inside_for_loop
|
||||
template = <<~LIQUID
|
||||
{%- for i in (1..3) -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{% cycle '1', '2', '3' %}
|
||||
{%- endfor -%}
|
||||
LIQUID
|
||||
|
||||
@@ -26,157 +36,13 @@ class CycleTagTest < Minitest::Test
|
||||
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
|
||||
def test_cycle_tag_always_resets_cycle
|
||||
template = <<~LIQUID
|
||||
{%- assign a = "1" -%}
|
||||
{%- for i in (1..3) -%}
|
||||
{%- cycle a, "2" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
{%- endfor -%}
|
||||
{%- cycle a, "2" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
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(:strict2) 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(:strict2) 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(:strict2) 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(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
assert_template_result("11", template)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -204,32 +204,6 @@ class IncludeTagTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_strict2_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(:strict2) 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(:strict2) 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(:strict2) 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(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end # IncludeTagTest
|
||||
|
||||
@@ -105,33 +105,7 @@ class RenderTagTest < Minitest::Test
|
||||
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
|
||||
end
|
||||
|
||||
def test_strict2_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(:strict2) do
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" | filter %}',
|
||||
)
|
||||
end
|
||||
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',
|
||||
@@ -314,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(:strict2) 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(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@ class StandardTagTest < Minitest::Test
|
||||
assigns = { 'condition' => "bad string here" }
|
||||
assert_template_result(
|
||||
'',
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',
|
||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
|
||||
assigns,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -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_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_limit_attribute_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_offset_attribute_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_range_attribute_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_multiple_attributes_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_variable_collection_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_dotted_access_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_bracketed_access_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_attributes_in_strict2_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(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_in_keyword_in_strict2_mode
|
||||
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
|
||||
|
||||
with_error_modes(:strict2) 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_strict2_mode
|
||||
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
|
||||
|
||||
with_error_modes(:strict2) 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_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
|
||||
expected = <<~OUTPUT
|
||||
<tr class="row1">
|
||||
</tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'empty_array' => [] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_invalid_attribute_strict_vs_strict2
|
||||
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(:strict2) 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_strict2
|
||||
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(:strict2) do
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -133,7 +133,7 @@ class TemplateTest < Minitest::Test
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.render_score_limit = 200
|
||||
assert_equal(" foo " * 100, t.render!)
|
||||
assert_equal((" foo " * 100), t.render!)
|
||||
refute_nil(t.resource_limits.render_score)
|
||||
end
|
||||
|
||||
|
||||
@@ -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(:strict2) 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(:strict2) 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(:strict2) 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(:strict2) 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(:strict2) do
|
||||
assert_template_result('TESTX', template)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+4
-26
@@ -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
|
||||
@@ -199,26 +197,6 @@ class ErrorDrop < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class CustomToLiquidDrop < Liquid::Drop
|
||||
def initialize(value)
|
||||
@value = value
|
||||
super()
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
@value
|
||||
end
|
||||
end
|
||||
|
||||
class HashWithCustomToS < Hash
|
||||
def to_s
|
||||
"kewl"
|
||||
end
|
||||
end
|
||||
|
||||
class HashWithoutCustomToS < Hash
|
||||
end
|
||||
|
||||
class StubFileSystem
|
||||
attr_reader :file_read_count
|
||||
|
||||
|
||||
@@ -161,208 +161,11 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
||||
end
|
||||
|
||||
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
||||
"and will be removed from Liquid 6.0.0."
|
||||
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0."
|
||||
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_strict2_mode_raises_internal_error
|
||||
environment = Environment.build(error_mode: :strict2)
|
||||
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 strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_safe_true_in_strict2_mode
|
||||
environment = Environment.build(error_mode: :strict2)
|
||||
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
|
||||
|
||||
# Tests for blank? comparison without ActiveSupport
|
||||
#
|
||||
# Ruby's standard library does not include blank? on String, Array, Hash, etc.
|
||||
# ActiveSupport adds blank? but Liquid must work without it. These tests verify
|
||||
# that Liquid implements blank? semantics internally for use in templates like:
|
||||
# {% if x == blank %}...{% endif %}
|
||||
#
|
||||
# The blank? semantics match ActiveSupport's behavior:
|
||||
# - nil and false are blank
|
||||
# - Strings are blank if empty or contain only whitespace
|
||||
# - Arrays and Hashes are blank if empty
|
||||
# - true and numbers are never blank
|
||||
|
||||
def test_blank_with_whitespace_string
|
||||
# Template authors expect " " to be blank since it has no visible content.
|
||||
# This matches ActiveSupport's String#blank? which returns true for whitespace-only strings.
|
||||
@context['whitespace'] = ' '
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('whitespace'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_string
|
||||
# An empty string has no content, so it should be considered blank.
|
||||
# This is the most basic case of a blank string.
|
||||
@context['empty_string'] = ''
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_string'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_array
|
||||
# Empty arrays have no elements, so they are blank.
|
||||
# Useful for checking if a collection has items: {% if products == blank %}
|
||||
@context['empty_array'] = []
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_array'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_hash
|
||||
# Empty hashes have no key-value pairs, so they are blank.
|
||||
# Useful for checking if settings/options exist: {% if settings == blank %}
|
||||
@context['empty_hash'] = {}
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_nil
|
||||
# nil represents "nothing" and is the canonical blank value.
|
||||
# Unassigned variables resolve to nil, so this enables: {% if missing_var == blank %}
|
||||
@context['nil_value'] = nil
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('nil_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_false
|
||||
# false is considered blank to match ActiveSupport semantics.
|
||||
# This allows {% if some_flag == blank %} to work when flag is false.
|
||||
@context['false_value'] = false
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('false_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_true
|
||||
# true is a definite value, not blank.
|
||||
# Ensures {% if flag == blank %} works correctly for boolean flags.
|
||||
@context['true_value'] = true
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('true_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_number
|
||||
# Numbers (including zero) are never blank - they represent actual values.
|
||||
# 0 is a valid quantity, not the absence of a value.
|
||||
@context['number'] = 42
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('number'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_string_content
|
||||
# A string with actual content is not blank.
|
||||
# This is the expected behavior for most template string comparisons.
|
||||
@context['string'] = 'hello'
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('string'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_non_empty_array
|
||||
# An array with elements has content, so it's not blank.
|
||||
# Enables patterns like {% unless products == blank %}Show products{% endunless %}
|
||||
@context['array'] = [1, 2, 3]
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('array'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_non_empty_hash
|
||||
# A hash with key-value pairs has content, so it's not blank.
|
||||
# Useful for checking if configuration exists: {% if config != blank %}
|
||||
@context['hash'] = { 'a' => 1 }
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('hash'), '==', blank_literal)
|
||||
end
|
||||
|
||||
# Tests for empty? comparison without ActiveSupport
|
||||
#
|
||||
# empty? is distinct from blank? - it only checks if a collection has zero elements.
|
||||
# For strings, empty? checks length == 0, NOT whitespace content.
|
||||
# Ruby's standard library has empty? on String, Array, and Hash, but Liquid
|
||||
# provides a fallback implementation for consistency.
|
||||
|
||||
def test_empty_with_empty_string
|
||||
# An empty string ("") has length 0, so it's empty.
|
||||
# Different from blank - empty is a stricter check.
|
||||
@context['empty_string'] = ''
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_string'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_whitespace_string_not_empty
|
||||
# Whitespace strings have length > 0, so they are NOT empty.
|
||||
# This is the key difference between empty and blank:
|
||||
# " ".empty? => false, but " ".blank? => true
|
||||
@context['whitespace'] = ' '
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('whitespace'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_empty_array
|
||||
# An array with no elements is empty.
|
||||
# [].empty? => true
|
||||
@context['empty_array'] = []
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_array'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_empty_hash
|
||||
# A hash with no key-value pairs is empty.
|
||||
# {}.empty? => true
|
||||
@context['empty_hash'] = {}
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_nil_is_not_empty
|
||||
# nil is NOT empty - empty? checks if a collection has zero elements.
|
||||
# nil is not a collection, so it cannot be empty.
|
||||
# This differs from blank: nil IS blank, but nil is NOT empty.
|
||||
@context['nil_value'] = nil
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_false(VariableLookup.new('nil_value'), '==', empty_literal)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_evaluates_true(left, op, right)
|
||||
|
||||
@@ -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_strict2 = strict2_parse_context.new_parser('product.title')
|
||||
result_strict2 = strict2_parse_context.safe_parse_expression(parser_strict2)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict)
|
||||
assert_equal('product', result_strict.name)
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict2)
|
||||
assert_equal('product', result_strict2.name)
|
||||
assert_equal(['title'], result_strict2.lookups)
|
||||
end
|
||||
|
||||
def test_safe_parse_expression_raises_syntax_error_for_invalid_expression
|
||||
parser_strict = strict_parse_context.new_parser('')
|
||||
parser_strict2 = strict2_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_strict2 = assert_raises(Liquid::SyntaxError) do
|
||||
strict2_parse_context.safe_parse_expression(parser_strict2)
|
||||
end
|
||||
|
||||
assert_match(/is not a valid expression/, error_strict2.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
|
||||
strict2_parse_context.parse_expression('product.title')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 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_strict2 = strict2_parse_context.parse_expression('product.title', safe: true)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict2)
|
||||
assert_equal('product', result_strict2.name)
|
||||
assert_equal(['title'], result_strict2.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
|
||||
strict2_parse_context.parse_expression('')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 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_strict2 = strict2_parse_context.parse_expression('', safe: true)
|
||||
assert_nil(result_strict2)
|
||||
end
|
||||
|
||||
def test_safe_parse_expression_advances_parser_pointer
|
||||
parser = strict2_parse_context.new_parser('foo, bar')
|
||||
|
||||
# safe_parse_expression consumes "foo"
|
||||
first_result = strict2_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 = strict2_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_strict2_mode
|
||||
result = strict2_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 strict2_parse_context
|
||||
@strict2_parse_context ||= ParseContext.new(
|
||||
environment: Environment.build(error_mode: :strict2),
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -184,7 +184,7 @@ class PartialCacheUnitTest < Minitest::Test
|
||||
},
|
||||
)
|
||||
|
||||
[:lax, :warn, :strict, :strict2].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:strict2"],
|
||||
["my_partial:lax", "my_partial:warn", "my_partial:strict"],
|
||||
context.registers[:cached_partials].keys,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
def test_add_filter_when_wrong_filter_class
|
||||
c = Context.new
|
||||
s = c.strainer
|
||||
wrong_filter = lambda(&:reverse)
|
||||
wrong_filter = ->(v) { v.reverse }
|
||||
|
||||
exception = assert_raises(TypeError) do
|
||||
s.class.add_filter(wrong_filter)
|
||||
|
||||
@@ -9,140 +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(:strict2) 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(:strict2) 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, :strict2) 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, :strict2) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_empty
|
||||
template = <<~LIQUID
|
||||
{%- case x -%}
|
||||
{%- when 2 or empty -%}
|
||||
2 or empty
|
||||
{%- else -%}
|
||||
not 2 or empty
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
assert_template_result("2 or empty", template, { 'x' => 2 })
|
||||
assert_template_result("2 or empty", template, { 'x' => {} })
|
||||
assert_template_result("2 or empty", template, { 'x' => [] })
|
||||
assert_template_result("not 2 or empty", template, { 'x' => { 'a' => 'b' } })
|
||||
assert_template_result("not 2 or empty", template, { 'x' => ['a'] })
|
||||
assert_template_result("not 2 or empty", template, { 'x' => 4 })
|
||||
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(:strict2) 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(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,7 +108,7 @@ class VariableUnitTest < Minitest::Test
|
||||
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
|
||||
assert_equal(VariableLookup.new('foo-bar-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_strict2_filter_argument_parsing
|
||||
with_error_modes(:strict2) 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)
|
||||
|
||||
Reference in New Issue
Block a user