mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e451b4307 | ||
|
|
9efca9f718 | ||
|
|
529800f46b | ||
|
|
7b368dffb8 | ||
|
|
742ac3dbf5 | ||
|
|
1954a2655c | ||
|
|
6d81b1b68c | ||
|
|
dfddd8f390 | ||
|
|
95ce7e7fa1 | ||
|
|
197d755e0c | ||
|
|
d0c5444db1 | ||
|
|
c99036046e | ||
|
|
a9c85622dd | ||
|
|
9f4d7e78b8 | ||
|
|
0d5c15a03e | ||
|
|
fd68d076dd | ||
|
|
96aa47d13f | ||
|
|
ad70c5c459 | ||
|
|
d824de701c | ||
|
|
346166b600 | ||
|
|
532b439063 | ||
|
|
dd37353cca | ||
|
|
e80f775f89 | ||
|
|
59d8d0d22d | ||
|
|
5fa36267aa | ||
|
|
a72b604680 | ||
|
|
3e76244cd2 | ||
|
|
d589c51697 | ||
|
|
d897899f66 | ||
|
|
aa817c4cfd | ||
|
|
7d90b524ea | ||
|
|
bbcf8d6ad8 | ||
|
|
51ff08db7b | ||
|
|
eaa9f215bf | ||
|
|
ccd05e869c | ||
|
|
a4a29f3e08 | ||
|
|
0058e4322b | ||
|
|
50e1789537 | ||
|
|
79a2e042ff | ||
|
|
ddee08fb95 | ||
|
|
2988f1a500 | ||
|
|
ef13b2dfd5 | ||
|
|
b0fb0ad83f | ||
|
|
ae26cb29ac | ||
|
|
608a877053 | ||
|
|
d321adae77 | ||
|
|
53641e19ce | ||
|
|
ccd10a986a | ||
|
|
f4890de9d5 | ||
|
|
7e3ccbc188 | ||
|
|
e0b46049af | ||
|
|
19528a9b3f | ||
|
|
34c274d314 | ||
|
|
533d470723 | ||
|
|
05f9c2a030 | ||
|
|
af58800c16 | ||
|
|
361d1d52b1 | ||
|
|
391c0df57a | ||
|
|
0ed29760c0 | ||
|
|
0e3548d39e | ||
|
|
33bac87a5c | ||
|
|
a60a6c0d93 | ||
|
|
bad29caaae | ||
|
|
cbeff64708 | ||
|
|
22e979a6fa | ||
|
|
735d551168 | ||
|
|
fa27bfe6e0 | ||
|
|
32b50ecafe | ||
|
|
cbd8a0a2ee | ||
|
|
9973f3399e | ||
|
|
248f3a412f | ||
|
|
a16ec56a40 | ||
|
|
12fd93fbe2 | ||
|
|
5ceb0e9cec | ||
|
|
98fbd985d8 | ||
|
|
ae05ba071c | ||
|
|
4205131148 | ||
|
|
db350c54ff | ||
|
|
0cc6cdd553 | ||
|
|
40e45e32ac | ||
|
|
d4d2237b90 | ||
|
|
0ceeefba02 | ||
|
|
65fb80a347 | ||
|
|
489a03118c | ||
|
|
99116638fd | ||
|
|
9bcfd32e65 | ||
|
|
c7ad1c90ca | ||
|
|
12bbbc4537 | ||
|
|
1eca707c4a | ||
|
|
ed9c4e31c4 |
@@ -1,5 +1,5 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
BUNDLE_JOBS: 4
|
||||
@@ -9,30 +9,34 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.4, allowed-failure: false } # latest
|
||||
- {
|
||||
ruby: 3.4,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: 3.3, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: ruby-head, allowed-failure: false }
|
||||
- { ruby: 4.0, allowed-failure: false } # latest stable
|
||||
- {
|
||||
ruby: ruby-head,
|
||||
ruby: 4.0,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
|
||||
- { ruby: truffleruby, allowed-failure: false }
|
||||
|
||||
# Head can have failures due to being in development
|
||||
- { ruby: head, allowed-failure: true }
|
||||
- {
|
||||
ruby: head,
|
||||
allowed-failure: true,
|
||||
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' }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
- uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
@@ -42,11 +46,28 @@ 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@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.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@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
|
||||
- uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
|
||||
+10
-1
@@ -174,7 +174,16 @@ Style/WordArray:
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns.
|
||||
# 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'
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.4.1
|
||||
4.0.2
|
||||
|
||||
@@ -25,7 +25,14 @@ group :development do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'benchmark'
|
||||
gem 'rubocop', '~> 1.82.0'
|
||||
gem 'rubocop-shopify', '~> 2.18.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
end
|
||||
|
||||
group :spec do
|
||||
# Using feature branch until https://github.com/Shopify/liquid-spec/pull/144 is merged
|
||||
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'self-drop-env-lookup-specs'
|
||||
gem 'activesupport', require: false
|
||||
end
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 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]
|
||||
|
||||
|
||||
@@ -103,10 +103,10 @@ Liquid also comes with different parsers that can be used when editing templates
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Environment.default.error_mode = :rigid # Raises a SyntaxError when invalid syntax is used in all tags
|
||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
|
||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
||||
Liquid::Environment.default.error_mode = :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.
|
||||
```
|
||||
|
||||
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 rigid parsers')
|
||||
desc('runs test suite with lax, strict, and strict2 parsers')
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].invoke
|
||||
@@ -42,7 +42,7 @@ task :test do
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'rigid'
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict2'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
@@ -55,7 +55,7 @@ task :test do
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'rigid'
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict2'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
@@ -88,13 +88,13 @@ namespace :benchmark do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with rigid parsing"
|
||||
task :rigid do
|
||||
ruby "./performance/benchmark.rb rigid"
|
||||
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 rigid parsing"
|
||||
task run: [:lax, :strict, :rigid]
|
||||
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
|
||||
task run: [:lax, :strict, :strict2]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
@@ -148,3 +148,11 @@ end
|
||||
task :console do
|
||||
exec 'irb -I lib -r liquid'
|
||||
end
|
||||
|
||||
desc('run liquid-spec suite across all adapters')
|
||||
task :spec do
|
||||
Dir['./spec/*.rb'].sort.each do |adapter|
|
||||
puts "=== Running #{adapter} ==="
|
||||
sh 'bundle', 'exec', 'liquid-spec', 'run', adapter, '--no-max-failures'
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -41,6 +41,6 @@ def assigns
|
||||
end
|
||||
|
||||
puts Liquid::Template
|
||||
.parse(source, error_mode: :rigid)
|
||||
.parse(source, error_mode: :strict2)
|
||||
.tap { |t| t.registers[:file_system] = VirtualFileSystem.new }
|
||||
.render(assigns)
|
||||
|
||||
@@ -65,6 +65,7 @@ require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
require 'liquid/i18n'
|
||||
require 'liquid/drop'
|
||||
require 'liquid/self_drop'
|
||||
require 'liquid/tablerowloop_drop'
|
||||
require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
|
||||
+55
-12
@@ -113,24 +113,67 @@ module Liquid
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
return call_method_literal(left, right)
|
||||
end
|
||||
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
return call_method_literal(right, left)
|
||||
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
|
||||
@@ -154,8 +197,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
|
||||
|
||||
|
||||
+20
-2
@@ -184,7 +184,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
self[key] != nil
|
||||
find_variable(key, raise_on_not_found: false) != nil
|
||||
end
|
||||
|
||||
# Checks whether a variable is defined in any scope, including nil-valued keys.
|
||||
# Unlike #key?, this uses Hash#key? so that variables explicitly set to nil
|
||||
# are still considered defined.
|
||||
def variable_defined?(key)
|
||||
@scopes.any? { |s| s.key?(key) } ||
|
||||
@environments.any? { |e| e.key?(key) } ||
|
||||
@static_environments.any? { |e| e.key?(key) }
|
||||
end
|
||||
|
||||
def evaluate(object)
|
||||
@@ -197,12 +206,21 @@ module Liquid
|
||||
# path and find_index() is optimized in MRI to reduce object allocation
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
|
||||
fallback_to_self_drop = key == Expression::SELF && index.nil?
|
||||
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
else
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
try_variable_find_in_environments(
|
||||
key,
|
||||
raise_on_not_found: raise_on_not_found && !fallback_to_self_drop,
|
||||
)
|
||||
end
|
||||
|
||||
# `self` resolves to a SelfDrop (enabling `self['var']` lookups),
|
||||
# but only after the normal environment lookup doesn't find a value.
|
||||
return @self_drop ||= SelfDrop.new(self) if fallback_to_self_drop && variable.nil?
|
||||
|
||||
# update variable's context before invoking #to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ module Liquid
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
return 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 :rigid, :strict, :warn, or :lax).
|
||||
# (either :strict2, :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.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
SELF = 'self'
|
||||
|
||||
LITERALS = {
|
||||
nil => nil,
|
||||
'nil' => nil,
|
||||
@@ -55,7 +57,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
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ module Liquid
|
||||
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner)
|
||||
Parser.new(@string_scanner, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
|
||||
end
|
||||
|
||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||
@@ -55,15 +55,15 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_expression(markup, safe: false)
|
||||
if !safe && @error_mode == :rigid
|
||||
if !safe && @error_mode == :strict2
|
||||
# parse_expression is a widely used API. To maintain backward
|
||||
# compatibility while raising awareness about rigid parser standards,
|
||||
# compatibility while raising awareness about strict2 parser standards,
|
||||
# the safe flag supports API users make a deliberate decision.
|
||||
#
|
||||
# In rigid mode, markup MUST come from a string returned by the parser
|
||||
# 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 rigid mode"
|
||||
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
|
||||
end
|
||||
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
def initialize(input, reject_bare_brackets: false)
|
||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@tokens = Lexer.tokenize(ss)
|
||||
@p = 0 # pointer to current location
|
||||
@reject_bare_brackets = reject_bare_brackets
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -53,6 +54,9 @@ module Liquid
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
if @reject_bare_brackets
|
||||
raise SyntaxError, "Bare bracket access is not allowed. Use #{Expression::SELF}['...'] instead"
|
||||
end
|
||||
str = consume.dup
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
|
||||
@@ -7,16 +7,19 @@ module Liquid
|
||||
# It's basically doing the same thing the {#parse_with_selected_parser},
|
||||
# except this will try the strict parser regardless of the error mode,
|
||||
# and fall back to the lax parser if the error mode is lax or warn,
|
||||
# except when in rigid mode where it uses the rigid parser.
|
||||
# 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 rigid_parse_with_error_context(markup) if rigid_mode?
|
||||
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
|
||||
@@ -28,12 +31,13 @@ module Liquid
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :rigid then rigid_parse_with_error_context(markup)
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :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 :warn
|
||||
begin
|
||||
rigid_parse_with_error_context(markup)
|
||||
strict2_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_parse(markup)
|
||||
@@ -41,14 +45,18 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def rigid_mode?
|
||||
parse_context.error_mode == :rigid
|
||||
def strict2_mode?
|
||||
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rigid_parse_with_error_context(markup)
|
||||
rigid_parse(markup)
|
||||
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)
|
||||
|
||||
@@ -2,24 +2,39 @@
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
attr_accessor :render_length_limit,
|
||||
:render_score_limit,
|
||||
:assign_score_limit,
|
||||
:cumulative_render_score_limit,
|
||||
:cumulative_assign_score_limit
|
||||
attr_reader :render_score,
|
||||
:assign_score,
|
||||
:cumulative_render_score,
|
||||
:cumulative_assign_score
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@cumulative_render_score_limit = limits[:cumulative_render_score_limit]
|
||||
@cumulative_assign_score_limit = limits[:cumulative_assign_score_limit]
|
||||
@cumulative_render_score = 0
|
||||
@cumulative_assign_score = 0
|
||||
reset
|
||||
end
|
||||
|
||||
def increment_render_score(amount)
|
||||
@render_score += amount
|
||||
@cumulative_render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
@cumulative_assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
@@ -47,6 +62,8 @@ module Liquid
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
|
||||
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
|
||||
end
|
||||
|
||||
def with_capture
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name self
|
||||
# @liquid_summary
|
||||
# Provides access to variables through the current scope chain.
|
||||
# @liquid_description
|
||||
# The `self` object resolves variables through the normal lookup hierarchy
|
||||
# (local > file > global) without exposing filters, interrupts, errors,
|
||||
# or other context internals. It's used when bare bracket notation
|
||||
# (`['variable']`) needs to be replaced with an explicit variable lookup.
|
||||
#
|
||||
# If `self` is explicitly assigned as a local variable (e.g. `{% assign self = 'value' %}`),
|
||||
# then the local value takes precedence over the `self` object.
|
||||
# @liquid_access global
|
||||
class SelfDrop < Drop
|
||||
def initialize(self_context)
|
||||
super()
|
||||
@self_context = self_context
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@self_context.find_variable(key)
|
||||
rescue UndefinedVariable
|
||||
nil
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@self_context.variable_defined?(key)
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(SelfDrop) && other.self_context.equal?(@self_context)
|
||||
end
|
||||
|
||||
alias_method :eql?, :==
|
||||
|
||||
def hash
|
||||
@self_context.object_id.hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :self_context
|
||||
|
||||
undef context=
|
||||
end
|
||||
end
|
||||
@@ -8,10 +8,19 @@ module Liquid
|
||||
MAX_I32 = (1 << 31) - 1
|
||||
private_constant :MAX_I32
|
||||
|
||||
MIN_I64 = -(1 << 63)
|
||||
MAX_I64 = (1 << 63) - 1
|
||||
I64_RANGE = MIN_I64..MAX_I64
|
||||
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
|
||||
supports_64bit_indices = begin
|
||||
[][1 << 33, 1 << 33]
|
||||
true
|
||||
rescue RangeError
|
||||
false
|
||||
end
|
||||
|
||||
INDEX_RANGE = if supports_64bit_indices
|
||||
(-(1 << 63))..((1 << 63) - 1)
|
||||
else
|
||||
(-(1 << 31))..((1 << 31) - 1)
|
||||
end
|
||||
private_constant :INDEX_RANGE
|
||||
|
||||
HTML_ESCAPE = {
|
||||
'&' => '&',
|
||||
@@ -214,11 +223,11 @@ module Liquid
|
||||
Utils.to_s(input).slice(offset, length) || ''
|
||||
end
|
||||
rescue RangeError
|
||||
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
|
||||
if INDEX_RANGE.cover?(length) && INDEX_RANGE.cover?(offset)
|
||||
raise # unexpected error
|
||||
end
|
||||
offset = offset.clamp(I64_RANGE)
|
||||
length = length.clamp(I64_RANGE)
|
||||
offset = offset.clamp(INDEX_RANGE)
|
||||
length = length.clamp(INDEX_RANGE)
|
||||
retry
|
||||
end
|
||||
end
|
||||
@@ -293,6 +302,19 @@ module Liquid
|
||||
input.split(pattern)
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
# @liquid_summary
|
||||
# Removes leading and trailing whitespace and collapses consecutive whitespace to a single space.
|
||||
# @liquid_syntax string | squish
|
||||
# @liquid_return [string]
|
||||
def squish(input)
|
||||
return if input.nil?
|
||||
|
||||
Utils.to_s(input).strip.gsub(/\s+/, ' ')
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category string
|
||||
@@ -768,6 +790,8 @@ 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
|
||||
|
||||
@@ -779,6 +803,8 @@ 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
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ module Liquid
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Assign < Tag
|
||||
include ParserSwitching
|
||||
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
# @api private
|
||||
@@ -29,6 +31,10 @@ module Liquid
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
@@ -37,6 +43,25 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
unless markup =~ Syntax
|
||||
self.class.raise_syntax_error(parse_context)
|
||||
end
|
||||
|
||||
lhs = Regexp.last_match(1).strip
|
||||
rhs = Regexp.last_match(2)
|
||||
|
||||
p = @parse_context.new_parser(lhs)
|
||||
@to = p.consume(:id)
|
||||
p.consume(:end_of_string)
|
||||
|
||||
@from = Variable.new(rhs, parse_context)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
|
||||
@@ -20,10 +20,18 @@ module Liquid
|
||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Capture < Block
|
||||
include ParserSwitching
|
||||
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
attr_reader :to
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
else
|
||||
@@ -31,6 +39,16 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup.strip)
|
||||
@to = p.consume(:id)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
|
||||
@@ -86,7 +86,7 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
parser = @parse_context.new_parser(markup)
|
||||
@left = safe_parse_expression(parser)
|
||||
parser.consume(:end_of_string)
|
||||
@@ -107,18 +107,18 @@ module Liquid
|
||||
def record_when_condition(markup)
|
||||
body = new_body
|
||||
|
||||
if rigid_mode?
|
||||
parse_rigid_when(markup, body)
|
||||
if strict2_mode?
|
||||
parse_strict2_when(markup, body)
|
||||
else
|
||||
parse_lax_when(markup, body)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_rigid_when(markup, body)
|
||||
def parse_strict2_when(markup, body)
|
||||
parser = @parse_context.new_parser(markup)
|
||||
|
||||
loop do
|
||||
expr = safe_parse_expression(parser)
|
||||
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
|
||||
block = Condition.new(@left, '==', expr)
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
|
||||
@@ -56,7 +56,7 @@ module Liquid
|
||||
private
|
||||
|
||||
# cycle [name:] expression(, expression)*
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@variables = []
|
||||
|
||||
@@ -23,13 +23,29 @@ module Liquid
|
||||
# {% decrement variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
||||
class Decrement < Tag
|
||||
include ParserSwitching
|
||||
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup.strip)
|
||||
@variable_name = p.consume(:id)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
|
||||
@@ -111,7 +111,7 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ module Liquid
|
||||
class Include < Tag
|
||||
prepend Tag::Disableable
|
||||
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedFragment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
Syntax = SYNTAX
|
||||
|
||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||
@@ -84,12 +85,18 @@ module Liquid
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
def rigid_parse(markup)
|
||||
def for_loop?
|
||||
@is_for_loop
|
||||
end
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
@@ -111,11 +118,13 @@ module Liquid
|
||||
def lax_parse(markup)
|
||||
if markup =~ SYNTAX
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@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|
|
||||
|
||||
@@ -23,13 +23,29 @@ module Liquid
|
||||
# {% increment variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
|
||||
class Increment < Tag
|
||||
include ParserSwitching
|
||||
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup.strip)
|
||||
@variable_name = p.consume(:id)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
|
||||
@@ -85,10 +85,10 @@ module Liquid
|
||||
end
|
||||
|
||||
# render (string) (with|for expression)? (as id)? (key: value)*
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@template_name_expr = parse_expression(rigid_template_name(p), safe: true)
|
||||
@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")
|
||||
@@ -107,7 +107,7 @@ module Liquid
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def rigid_template_name(p)
|
||||
def strict2_template_name(p)
|
||||
p.consume(:string)
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ module Liquid
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def rigid_parse(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@variable_name = p.consume(:id)
|
||||
|
||||
@@ -25,7 +25,7 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict enforces correct syntax for most tags
|
||||
# :rigid enforces correct syntax for all tags
|
||||
# :strict2 enforces correct syntax for all tags
|
||||
def error_mode=(mode)
|
||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
||||
Environment.default.error_mode = mode
|
||||
@@ -151,8 +151,10 @@ module Liquid
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
drop = args.shift
|
||||
c = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
drop.context = c if drop.respond_to?(:context=)
|
||||
c
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
when nil
|
||||
|
||||
@@ -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
-1
@@ -69,7 +69,7 @@ module Liquid
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
return if obj.empty?
|
||||
obj = obj.downcase
|
||||
end
|
||||
|
||||
@@ -95,6 +95,8 @@ 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
|
||||
|
||||
@@ -37,6 +37,10 @@ module Liquid
|
||||
@markup
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && name == other.name && filters == other.filters
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"{{#{markup}}}\""
|
||||
end
|
||||
@@ -74,14 +78,14 @@ module Liquid
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def rigid_parse(markup)
|
||||
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 << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
|
||||
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
@@ -156,7 +160,7 @@ module Liquid
|
||||
# argument = (positional_argument | keyword_argument)
|
||||
# positional_argument = expression
|
||||
# keyword_argument = id ":" expression
|
||||
def rigid_parse_filter_expressions(p)
|
||||
def strict2_parse_filter_expressions(p)
|
||||
filtername = p.consume(:id)
|
||||
filter_args = []
|
||||
keyword_args = {}
|
||||
|
||||
@@ -70,6 +70,11 @@ 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.9.0"
|
||||
VERSION = "5.12.0"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.missing_features = [
|
||||
:activesupport,
|
||||
:lax_parsing,
|
||||
:shopify_filters,
|
||||
:shopify_includes,
|
||||
:shopify_blank,
|
||||
:shopify_error_handling,
|
||||
:shopify_error_format,
|
||||
:shopify_string_access,
|
||||
]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
options[:error_mode] ||= :strict
|
||||
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],
|
||||
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
LiquidSpecAdapterHelper.with_frozen_time do
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.missing_features = [
|
||||
:activesupport,
|
||||
:shopify_filters,
|
||||
:shopify_includes,
|
||||
:shopify_blank,
|
||||
:shopify_error_handling,
|
||||
:shopify_error_format,
|
||||
:shopify_string_access,
|
||||
]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
# Default to lax mode while still honoring specs that explicitly set error_mode.
|
||||
options = { error_mode: :lax }.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],
|
||||
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
LiquidSpecAdapterHelper.with_frozen_time do
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.missing_features = [
|
||||
:lax_parsing,
|
||||
:shopify_filters,
|
||||
:shopify_includes,
|
||||
:shopify_blank,
|
||||
:shopify_error_handling,
|
||||
:shopify_error_format,
|
||||
:shopify_string_access,
|
||||
]
|
||||
end
|
||||
|
||||
# Compile a template string into a Liquid::Template
|
||||
LiquidSpec.compile do |ctx, source, options|
|
||||
options[:error_mode] ||= :strict
|
||||
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],
|
||||
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
LiquidSpecAdapterHelper.with_frozen_time do
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.missing_features = [
|
||||
:lax_parsing,
|
||||
:shopify_filters,
|
||||
:shopify_includes,
|
||||
:shopify_blank,
|
||||
:shopify_error_handling,
|
||||
:shopify_error_format,
|
||||
:shopify_string_access,
|
||||
]
|
||||
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],
|
||||
resource_limits: LiquidSpecAdapterHelper.resource_limits(options),
|
||||
)
|
||||
|
||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
||||
|
||||
LiquidSpecAdapterHelper.with_frozen_time do
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LiquidSpecAdapterHelper
|
||||
extend self
|
||||
|
||||
def resource_limits(render_options)
|
||||
return unless render_options[:resource_limits]
|
||||
|
||||
Liquid::ResourceLimits.new({}).tap do |limits|
|
||||
render_options[:resource_limits].each do |key, value|
|
||||
limits.public_send(:"#{key}=", value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def with_frozen_time(&block)
|
||||
original_tz = ENV['TZ']
|
||||
ENV['TZ'] = 'UTC'
|
||||
|
||||
Liquid::Spec::TimeFreezer.freeze(Liquid::Spec::AdapterRunner::TEST_TIME, &block)
|
||||
ensure
|
||||
ENV['TZ'] = original_tz
|
||||
end
|
||||
end
|
||||
@@ -97,6 +97,46 @@ class AssignTest < Minitest::Test
|
||||
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
|
||||
end
|
||||
|
||||
def test_assign_with_valid_identifier_in_strict2
|
||||
assert_template_result("hello", "{% assign my_var = 'hello' %}{{ my_var }}", error_mode: :strict2)
|
||||
end
|
||||
|
||||
def test_assign_with_hyphen_in_strict2
|
||||
assert_template_result("hello", "{% assign my-var = 'hello' %}{{ my-var }}", error_mode: :strict2)
|
||||
end
|
||||
|
||||
def test_assign_rejects_parentheses_in_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% assign (a(b(c) = 1234 %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_assign_rejects_brackets_in_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% assign [x.y] = 'hello' %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_assign_rejects_dot_in_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% assign a.b = 'hello' %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_assign_rejects_numeric_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% assign 1abc = 'hello' %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_assign_allows_invalid_names_in_lax
|
||||
assert_template_result("1234", "{% assign (a(b(c) = 1234 %}{{ self['(a(b(c)'] }}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_assign_with_filter_in_strict2
|
||||
assert_template_result("HELLO", "{% assign my_var = 'hello' | upcase %}{{ my_var }}", error_mode: :strict2)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class ObjectWrapperDrop < Liquid::Drop
|
||||
|
||||
@@ -6,7 +6,11 @@ class CaptureTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_captures_block_content_in_variable
|
||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
|
||||
assert_template_result("test string", "{% capture var %}test string{% endcapture %}{{var}}", {})
|
||||
end
|
||||
|
||||
def test_captures_block_content_in_quoted_variable_in_lax
|
||||
assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}, error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_variable_name
|
||||
@@ -49,4 +53,35 @@ class CaptureTest < Minitest::Test
|
||||
t.render!
|
||||
assert_equal(9, t.resource_limits.assign_score)
|
||||
end
|
||||
|
||||
def test_capture_with_valid_identifier_in_strict2
|
||||
assert_template_result("hello", "{% capture my_var %}hello{% endcapture %}{{ my_var }}", error_mode: :strict2)
|
||||
end
|
||||
|
||||
def test_capture_with_hyphen_in_strict2
|
||||
assert_template_result("hello", "{% capture my-var %}hello{% endcapture %}{{ my-var }}", error_mode: :strict2)
|
||||
end
|
||||
|
||||
def test_capture_rejects_parentheses_in_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_capture_rejects_dot_in_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% capture a.b %}hello{% endcapture %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_capture_rejects_numeric_variable_name_in_strict2
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Liquid::Template.parse("{% capture 1abc %}hello{% endcapture %}", error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_capture_allows_invalid_names_in_lax
|
||||
t = Liquid::Template.parse("{% capture (x[y %}hello{% endcapture %}", error_mode: :lax)
|
||||
assert_equal("(x[y", t.root.nodelist.first.to)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -296,8 +296,8 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
|
||||
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||
assert_template_result('baz', '{{ foo }}', { "foo" => "baz" })
|
||||
assert_template_result('baz', '{{ self[bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
@@ -639,6 +639,21 @@ class ContextTest < Minitest::Test
|
||||
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,
|
||||
|
||||
@@ -88,17 +88,11 @@ class HashRenderingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
|
||||
my_hash = Class.new(Hash) do
|
||||
def to_s
|
||||
"kewl"
|
||||
end
|
||||
end.new
|
||||
|
||||
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => HashWithCustomToS.new })
|
||||
end
|
||||
|
||||
def test_rendering_hash_without_custom_to_s_uses_default_inspect
|
||||
my_hash = Class.new(Hash).new
|
||||
my_hash = HashWithoutCustomToS.new
|
||||
my_hash[:foo] = :bar
|
||||
|
||||
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
|
||||
|
||||
@@ -44,33 +44,40 @@ class SecurityTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_does_not_permanently_add_filters_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
assert_no_new_symbols do
|
||||
# MRI imprecisely marks objects found on the C stack, which can result
|
||||
# in uninitialized memory being marked. This can even result in the test failing
|
||||
# deterministically for a given compilation of ruby. Using a separate thread will
|
||||
# keep these writes of the symbol pointer on a separate stack that will be garbage
|
||||
# collected after Thread#join.
|
||||
Thread.new do
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
Template.parse(test).render!
|
||||
nil
|
||||
end.join
|
||||
|
||||
# MRI imprecisely marks objects found on the C stack, which can result
|
||||
# in uninitialized memory being marked. This can even result in the test failing
|
||||
# deterministically for a given compilation of ruby. Using a separate thread will
|
||||
# keep these writes of the symbol pointer on a separate stack that will be garbage
|
||||
# collected after Thread#join.
|
||||
Thread.new do
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
Template.parse(test).render!
|
||||
nil
|
||||
end.join
|
||||
|
||||
GC.start
|
||||
|
||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
||||
GC.start
|
||||
end
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
assert_no_new_symbols do
|
||||
assigns = { 'drop' => Drop.new }
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_no_new_symbols
|
||||
# Run once to trigger any first-time initialization which might create some symbols,
|
||||
# for example autoload or lazy method parsing might create symbols on first execution.
|
||||
yield
|
||||
|
||||
# Ensure no new symbols for further runs, i.e. the code does not leak symbols
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
assigns = { 'drop' => Drop.new }
|
||||
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
|
||||
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))
|
||||
yield
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class SelfDropContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_self_drop_passed_as_render_param_preserves_original_scope
|
||||
source = <<~LIQUID
|
||||
{%- assign var = 42 -%}
|
||||
{%- assign s = self -%}
|
||||
{%- render "snippet1", other_self: s -%}
|
||||
LIQUID
|
||||
|
||||
partials = {
|
||||
'snippet1' => <<~LIQUID,
|
||||
{%- assign var = 43 -%}
|
||||
{{- other_self.var }}|{{ self.var -}}
|
||||
LIQUID
|
||||
}
|
||||
|
||||
assert_template_result('42|43', source, partials: partials)
|
||||
end
|
||||
|
||||
def test_self_drop_in_render_without_passing_resolves_inner_scope
|
||||
source = <<~LIQUID
|
||||
{%- assign var = 42 -%}
|
||||
{%- render "snippet1" -%}
|
||||
LIQUID
|
||||
|
||||
partials = {
|
||||
'snippet1' => <<~LIQUID,
|
||||
{%- assign var = 99 -%}
|
||||
{{- self.var -}}
|
||||
LIQUID
|
||||
}
|
||||
|
||||
assert_template_result('99', source, partials: partials)
|
||||
end
|
||||
|
||||
def test_self_drop_passed_to_nested_renders_preserves_each_level
|
||||
source = <<~LIQUID
|
||||
{%- assign a = 1 -%}
|
||||
{%- assign s1 = self -%}
|
||||
{%- render "snippet1", outer: s1 -%}
|
||||
LIQUID
|
||||
|
||||
partials = {
|
||||
'snippet1' => <<~LIQUID,
|
||||
{%- assign a = 2 -%}
|
||||
{%- assign s2 = self -%}
|
||||
{%- render "snippet2", outer: outer, middle: s2 -%}
|
||||
LIQUID
|
||||
'snippet2' => <<~LIQUID,
|
||||
{%- assign a = 3 -%}
|
||||
{{- outer.a }}|{{ middle.a }}|{{ self.a -}}
|
||||
LIQUID
|
||||
}
|
||||
|
||||
assert_template_result('1|2|3', source, partials: partials)
|
||||
end
|
||||
|
||||
def test_self_drop_reflects_variables_assigned_after_creation
|
||||
source = <<~LIQUID
|
||||
{%- assign s = self -%}
|
||||
{%- assign x = 42 %}{{ s.x -}}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('42', source)
|
||||
end
|
||||
|
||||
def test_self_drop_context_setter_is_undefined
|
||||
context = Context.new
|
||||
drop = SelfDrop.new(context)
|
||||
refute(drop.respond_to?(:context=))
|
||||
|
||||
assert_template_result('42', '{{ self.x }}', { 'x' => 42 })
|
||||
end
|
||||
|
||||
def test_self_drop_repeated_lookups_compare_equal_for_same_context
|
||||
context = Context.new
|
||||
drop = context.find_variable("self")
|
||||
cached_drop = context.find_variable("self")
|
||||
|
||||
assert_same(drop, cached_drop)
|
||||
assert_equal(drop.object_id, cached_drop.object_id)
|
||||
assert_equal(drop, cached_drop)
|
||||
end
|
||||
|
||||
def test_assigned_self_drop_compares_equal_to_itself
|
||||
assert_template_result('T', '{% assign s = self %}{% if s == s %}T{% else %}F{% endif %}')
|
||||
end
|
||||
|
||||
def test_distinct_self_assignments_compare_equal_for_same_context
|
||||
assert_template_result('T', '{% assign a = self %}{% assign b = self %}{% if a == b %}T{% else %}F{% endif %}')
|
||||
end
|
||||
|
||||
def test_bare_self_compares_equal_to_bare_self
|
||||
assert_template_result('T', '{% if self == self %}T{% else %}F{% endif %}')
|
||||
end
|
||||
|
||||
def test_self_drop_with_strict_variables_does_not_raise_for_defined_var
|
||||
t = Template.parse('{{ self.x }}')
|
||||
result = t.render({ 'x' => 42 }, strict_variables: true)
|
||||
assert_equal('42', result)
|
||||
end
|
||||
|
||||
def test_self_drop_with_strict_variables_returns_nil_for_undefined_var
|
||||
t = Template.parse('{{ self.x }}')
|
||||
result = t.render({}, strict_variables: true)
|
||||
assert_equal('', result)
|
||||
end
|
||||
|
||||
def test_self_drop_can_be_passed_as_bare_drop_to_render
|
||||
t = Template.parse('{{ self.x }}')
|
||||
drop = SelfDrop.new(Context.new({ 'x' => 42 }))
|
||||
result = t.render(drop)
|
||||
assert_equal('42', result)
|
||||
end
|
||||
end
|
||||
@@ -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))
|
||||
@@ -164,6 +164,13 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_equal(['A', 'Z'], @filters.split('A1Z', 1))
|
||||
end
|
||||
|
||||
def test_squish_filter
|
||||
assert_equal("foo bar boo", Liquid::Template.parse(%({{ " foo bar
|
||||
\t boo " | squish }})).render)
|
||||
assert_equal("", Liquid::Template.parse('{{ nil | squish }}').render)
|
||||
assert_equal("", Liquid::Template.parse('{{ " " | squish }}').render)
|
||||
end
|
||||
|
||||
def test_escape
|
||||
assert_equal('<strong>', @filters.escape('<strong>'))
|
||||
assert_equal('1', @filters.escape(1))
|
||||
@@ -294,13 +301,7 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_join_calls_to_liquid_on_each_element
|
||||
drop = Class.new(Liquid::Drop) do
|
||||
def to_liquid
|
||||
'i did it'
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
|
||||
assert_equal('i did it, i did it', @filters.join([CustomToLiquidDrop.new('i did it'), CustomToLiquidDrop.new('i did it')], ", "))
|
||||
end
|
||||
|
||||
def test_sort
|
||||
@@ -633,6 +634,40 @@ 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))
|
||||
@@ -1146,6 +1181,8 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_all_filters_never_raise_non_liquid_exception
|
||||
skip("too slow on non-CRuby due to many exceptions") unless RUBY_ENGINE == 'ruby'
|
||||
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop.context = Context.new
|
||||
test_enum = TestEnumerable.new
|
||||
@@ -1302,7 +1339,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
|
||||
|
||||
@@ -101,14 +101,12 @@ class CycleTagTest < Minitest::Test
|
||||
assert_template_result("a", template2)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
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)
|
||||
assert_match(/Liquid syntax error:/, error1.message)
|
||||
assert_match(/Liquid syntax error: \[:dot, "."\] is not a valid expression/, error2.message)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,7 +127,7 @@ class CycleTagTest < Minitest::Test
|
||||
assert_template_result("N", template5)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
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) }
|
||||
@@ -157,7 +155,7 @@ class CycleTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
@@ -174,7 +172,7 @@ class CycleTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
|
||||
@@ -204,7 +204,7 @@ class IncludeTagTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_rigid_parsing_errors
|
||||
def test_strict2_parsing_errors
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result(
|
||||
'hello value1 value2',
|
||||
@@ -213,7 +213,7 @@ class IncludeTagTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_syntax_error(
|
||||
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
@@ -408,7 +408,7 @@ class IncludeTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
@@ -421,7 +421,7 @@ class IncludeTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
@@ -434,9 +434,54 @@ class IncludeTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_for_loop_true_with_for_keyword
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
template = Template.parse("{% include 'product' for products %}")
|
||||
include_node = template.root.nodelist.first
|
||||
|
||||
assert(include_node.for_loop?, "Expected for_loop? to be true for 'for' keyword")
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_for_loop_false_with_with_keyword
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
template = Template.parse("{% include 'product' with product %}")
|
||||
include_node = template.root.nodelist.first
|
||||
|
||||
refute(include_node.for_loop?, "Expected for_loop? to be false for 'with' keyword")
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_for_loop_false_without_keyword
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
template = Template.parse("{% include 'header' %}")
|
||||
include_node = template.root.nodelist.first
|
||||
|
||||
refute(include_node.for_loop?, "Expected for_loop? to be false when no keyword")
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_for_loop_with_alias
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
template = Template.parse("{% include 'product' for products as item %}")
|
||||
include_node = template.root.nodelist.first
|
||||
|
||||
assert(include_node.for_loop?, "Expected for_loop? to be true for 'for' with alias")
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_with_keyword_and_alias
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
template = Template.parse("{% include 'product' with products[0] as item %}")
|
||||
include_node = template.root.nodelist.first
|
||||
|
||||
refute(include_node.for_loop?, "Expected for_loop? to be false for 'with' with alias")
|
||||
end
|
||||
end
|
||||
end # IncludeTagTest
|
||||
|
||||
@@ -27,4 +27,50 @@ class IncrementTagTest < Minitest::Test
|
||||
'{%decrement starboard %}',
|
||||
)
|
||||
end
|
||||
|
||||
def test_increment_strict2_rejects_invalid_variable_name
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% increment foo bar %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_increment_strict2_rejects_variable_starting_with_number
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% increment 11aa %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_increment_strict2_accepts_valid_variable_name
|
||||
template = Template.parse('{% increment my-var %}', error_mode: :strict2)
|
||||
assert_equal('0', template.render)
|
||||
end
|
||||
|
||||
def test_decrement_strict2_rejects_invalid_variable_name
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% decrement foo bar %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_decrement_strict2_rejects_variable_starting_with_number
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% decrement 11aa %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_decrement_strict2_accepts_valid_variable_name
|
||||
template = Template.parse('{% decrement my-var %}', error_mode: :strict2)
|
||||
assert_equal('-1', template.render)
|
||||
end
|
||||
|
||||
def test_increment_strict2_rejects_empty_variable_name
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% increment %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
|
||||
def test_decrement_strict2_rejects_empty_variable_name
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse('{% decrement %}', error_mode: :strict2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,7 +105,7 @@ class RenderTagTest < Minitest::Test
|
||||
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
|
||||
end
|
||||
|
||||
def test_rigid_parsing_errors
|
||||
def test_strict2_parsing_errors
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result(
|
||||
'hello value1 value2',
|
||||
@@ -114,7 +114,7 @@ class RenderTagTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
@@ -174,16 +174,16 @@ class RenderTagTest < Minitest::Test
|
||||
def test_increment_is_isolated_between_renders
|
||||
assert_template_result(
|
||||
'010',
|
||||
'{% increment %}{% increment %}{% render "incr" %}',
|
||||
partials: { 'incr' => '{% increment %}' },
|
||||
'{% increment port %}{% increment port %}{% render "incr" %}',
|
||||
partials: { 'incr' => '{% increment port %}' },
|
||||
)
|
||||
end
|
||||
|
||||
def test_decrement_is_isolated_between_renders
|
||||
assert_template_result(
|
||||
'-1-2-1',
|
||||
'{% decrement %}{% decrement %}{% render "decr" %}',
|
||||
partials: { 'decr' => '{% decrement %}' },
|
||||
'{% decrement port %}{% decrement port %}{% render "decr" %}',
|
||||
partials: { 'decr' => '{% decrement port %}' },
|
||||
)
|
||||
end
|
||||
|
||||
@@ -322,7 +322,7 @@ class RenderTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
@@ -335,7 +335,7 @@ class RenderTagTest < Minitest::Test
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
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
|
||||
|
||||
@@ -259,7 +259,7 @@ class TableRowTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_with_cols_attribute_in_rigid_mode
|
||||
def test_tablerow_with_cols_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -270,12 +270,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">4</td><td class="col2">5</td><td class="col3">6</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_limit_attribute_in_rigid_mode
|
||||
def test_tablerow_with_limit_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -285,12 +285,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_offset_attribute_in_rigid_mode
|
||||
def test_tablerow_with_offset_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -300,12 +300,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_range_attribute_in_rigid_mode
|
||||
def test_tablerow_with_range_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -315,12 +315,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_multiple_attributes_in_rigid_mode
|
||||
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
|
||||
@@ -331,12 +331,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_variable_collection_in_rigid_mode
|
||||
def test_tablerow_with_variable_collection_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -347,12 +347,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_dotted_access_in_rigid_mode
|
||||
def test_tablerow_with_dotted_access_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -363,12 +363,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
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_rigid_mode
|
||||
def test_tablerow_with_bracketed_access_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -378,12 +378,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">10</td><td class="col2">20</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_attributes_in_rigid_mode
|
||||
def test_tablerow_without_attributes_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -393,30 +393,30 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_in_keyword_in_rigid_mode
|
||||
def test_tablerow_without_in_keyword_in_strict2_mode
|
||||
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
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_rigid_mode
|
||||
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(:rigid) do
|
||||
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_rigid_mode
|
||||
def test_tablerow_with_empty_collection_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -426,12 +426,12 @@ class TableRowTest < Minitest::Test
|
||||
</tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'empty_array' => [] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_invalid_attribute_strict_vs_rigid
|
||||
def test_tablerow_with_invalid_attribute_strict_vs_strict2
|
||||
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
|
||||
|
||||
expected = <<~OUTPUT
|
||||
@@ -443,13 +443,13 @@ class TableRowTest < Minitest::Test
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
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_rigid
|
||||
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
|
||||
@@ -460,7 +460,7 @@ class TableRowTest < Minitest::Test
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
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
|
||||
|
||||
@@ -179,6 +179,86 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal("すごい", t.render)
|
||||
end
|
||||
|
||||
def test_cumulative_render_score_limit_across_render_tags
|
||||
file_system = StubFileSystem.new(
|
||||
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
|
||||
)
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
|
||||
# Without cumulative limit, all 5 partials render successfully
|
||||
t = Template.parse(
|
||||
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
|
||||
environment: environment,
|
||||
)
|
||||
unlimited_output = t.render!
|
||||
total_cumulative = t.resource_limits.cumulative_render_score
|
||||
|
||||
# With cumulative limit set below the total, rendering stops early
|
||||
t2 = Template.parse(
|
||||
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
|
||||
environment: environment,
|
||||
)
|
||||
t2.resource_limits.cumulative_render_score_limit = total_cumulative / 2
|
||||
limited_output = t2.render
|
||||
assert(t2.resource_limits.reached?)
|
||||
assert_operator(limited_output.length, :<, unlimited_output.length)
|
||||
end
|
||||
|
||||
def test_cumulative_render_score_limit_raises_on_render_bang
|
||||
file_system = StubFileSystem.new(
|
||||
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
|
||||
)
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
t = Template.parse(
|
||||
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
|
||||
environment: environment,
|
||||
)
|
||||
t.resource_limits.cumulative_render_score_limit = 20
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
t.render!
|
||||
end
|
||||
end
|
||||
|
||||
def test_cumulative_assign_score_limit_across_include_tags
|
||||
file_system = StubFileSystem.new(
|
||||
'assign_partial' => '{% assign x = "a long string value here" %}',
|
||||
)
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
|
||||
# Without cumulative limit, all 5 partials render
|
||||
t = Template.parse(
|
||||
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
|
||||
environment: environment,
|
||||
)
|
||||
t.render!
|
||||
total_cumulative = t.resource_limits.cumulative_assign_score
|
||||
|
||||
# With cumulative limit set below the total, rendering stops early
|
||||
t2 = Template.parse(
|
||||
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
|
||||
environment: environment,
|
||||
)
|
||||
t2.resource_limits.cumulative_assign_score_limit = total_cumulative / 2
|
||||
t2.render
|
||||
assert(t2.resource_limits.reached?)
|
||||
end
|
||||
|
||||
def test_cumulative_render_score_tracks_across_partials_without_limit
|
||||
file_system = StubFileSystem.new(
|
||||
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
|
||||
)
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
t = Template.parse(
|
||||
'{% render "loop" %}{% render "loop" %}{% render "loop" %}',
|
||||
environment: environment,
|
||||
)
|
||||
t.render!
|
||||
assert(
|
||||
t.resource_limits.cumulative_render_score > t.resource_limits.render_score,
|
||||
"cumulative should exceed per-template score after multiple partials",
|
||||
)
|
||||
end
|
||||
|
||||
def test_default_resource_limits_unaffected_by_render_with_context
|
||||
context = Context.new
|
||||
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
|
||||
|
||||
@@ -55,7 +55,7 @@ class VariableTest < Minitest::Test
|
||||
|
||||
def test_expression_with_whitespace_in_square_brackets
|
||||
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } })
|
||||
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
|
||||
assert_template_result('result', "{{ a[ self[ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
|
||||
end
|
||||
|
||||
def test_ignore_unknown
|
||||
@@ -135,17 +135,17 @@ class VariableTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_dynamic_find_var
|
||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
end
|
||||
|
||||
def test_raw_value_variable
|
||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
assert_template_result('bar', '{{ self[key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
end
|
||||
|
||||
def test_dynamic_find_var_with_drop
|
||||
assert_template_result(
|
||||
'bar',
|
||||
'{{ [list[settings.zero]] }}',
|
||||
'{{ self[list[settings.zero]] }}',
|
||||
{
|
||||
'list' => ['foo'],
|
||||
'settings' => SettingsDrop.new("zero" => 0),
|
||||
@@ -155,7 +155,7 @@ class VariableTest < Minitest::Test
|
||||
|
||||
assert_template_result(
|
||||
'foo',
|
||||
'{{ [list[settings.zero]["foo"]] }}',
|
||||
'{{ self[list[settings.zero]["foo"]] }}',
|
||||
{
|
||||
'list' => [{ 'foo' => 'bar' }],
|
||||
'settings' => SettingsDrop.new("zero" => 0),
|
||||
@@ -218,7 +218,7 @@ class VariableTest < Minitest::Test
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('helloworld', template)
|
||||
end
|
||||
end
|
||||
@@ -231,7 +231,7 @@ class VariableTest < Minitest::Test
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('hello12', template)
|
||||
end
|
||||
end
|
||||
@@ -244,7 +244,7 @@ class VariableTest < Minitest::Test
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TEST', template)
|
||||
end
|
||||
end
|
||||
@@ -257,7 +257,7 @@ class VariableTest < Minitest::Test
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TESTX', template)
|
||||
end
|
||||
end
|
||||
@@ -270,7 +270,7 @@ class VariableTest < Minitest::Test
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TESTX', template)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -199,6 +199,26 @@ 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,8 +161,8 @@ 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
|
||||
|
||||
@@ -176,19 +176,19 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_parse_expression_in_rigid_mode_raises_internal_error
|
||||
environment = Environment.build(error_mode: :rigid)
|
||||
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 rigid mode/, error.message)
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_safe_true_in_rigid_mode
|
||||
environment = Environment.build(error_mode: :rigid)
|
||||
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)
|
||||
|
||||
@@ -197,6 +197,172 @@ class ConditionUnitTest < Minitest::Test
|
||||
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)
|
||||
|
||||
@@ -9,32 +9,32 @@ class ParseContextUnitTest < Minitest::Test
|
||||
parser_strict = strict_parse_context.new_parser('product.title')
|
||||
result_strict = strict_parse_context.safe_parse_expression(parser_strict)
|
||||
|
||||
parser_rigid = rigid_parse_context.new_parser('product.title')
|
||||
result_rigid = rigid_parse_context.safe_parse_expression(parser_rigid)
|
||||
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_rigid)
|
||||
assert_equal('product', result_rigid.name)
|
||||
assert_equal(['title'], result_rigid.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_rigid = rigid_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_rigid = assert_raises(Liquid::SyntaxError) do
|
||||
rigid_parse_context.safe_parse_expression(parser_rigid)
|
||||
error_strict2 = assert_raises(Liquid::SyntaxError) do
|
||||
strict2_parse_context.safe_parse_expression(parser_strict2)
|
||||
end
|
||||
|
||||
assert_match(/is not a valid expression/, error_rigid.message)
|
||||
assert_match(/is not a valid expression/, error_strict2.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_variable_lookup
|
||||
@@ -45,10 +45,10 @@ class ParseContextUnitTest < Minitest::Test
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
error = assert_raises(Liquid::InternalError) do
|
||||
rigid_parse_context.parse_expression('product.title')
|
||||
strict2_parse_context.parse_expression('product.title')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_safe_true
|
||||
@@ -58,11 +58,11 @@ class ParseContextUnitTest < Minitest::Test
|
||||
assert_equal('product', result_strict.name)
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
result_rigid = rigid_parse_context.parse_expression('product.title', safe: true)
|
||||
result_strict2 = strict2_parse_context.parse_expression('product.title', safe: true)
|
||||
|
||||
assert_instance_of(VariableLookup, result_rigid)
|
||||
assert_equal('product', result_rigid.name)
|
||||
assert_equal(['title'], result_rigid.lookups)
|
||||
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
|
||||
@@ -70,40 +70,40 @@ class ParseContextUnitTest < Minitest::Test
|
||||
assert_nil(result_strict)
|
||||
|
||||
error = assert_raises(Liquid::InternalError) do
|
||||
rigid_parse_context.parse_expression('')
|
||||
strict2_parse_context.parse_expression('')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
|
||||
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_rigid = rigid_parse_context.parse_expression('', safe: true)
|
||||
assert_nil(result_rigid)
|
||||
result_strict2 = strict2_parse_context.parse_expression('', safe: true)
|
||||
assert_nil(result_strict2)
|
||||
end
|
||||
|
||||
def test_safe_parse_expression_advances_parser_pointer
|
||||
parser = rigid_parse_context.new_parser('foo, bar')
|
||||
parser = strict2_parse_context.new_parser('foo, bar')
|
||||
|
||||
# safe_parse_expression consumes "foo"
|
||||
first_result = rigid_parse_context.safe_parse_expression(parser)
|
||||
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 = rigid_parse_context.safe_parse_expression(parser)
|
||||
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_rigid_mode
|
||||
result = rigid_parse_context.parse_expression(' ', safe: true)
|
||||
def test_parse_expression_with_whitespace_in_strict2_mode
|
||||
result = strict2_parse_context.parse_expression(' ', safe: true)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
@@ -115,9 +115,9 @@ class ParseContextUnitTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def rigid_parse_context
|
||||
@rigid_parse_context ||= ParseContext.new(
|
||||
environment: Environment.build(error_mode: :rigid),
|
||||
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, :rigid].each do |error_mode|
|
||||
[:lax, :warn, :strict, :strict2].each do |error_mode|
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
@@ -193,7 +193,7 @@ class PartialCacheUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
assert_equal(
|
||||
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:rigid"],
|
||||
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:strict2"],
|
||||
context.registers[:cached_partials].keys,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ResourceLimitsUnitTest < Minitest::Test
|
||||
def test_cumulative_scores_initialize_to_zero
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
assert_equal(0, limits.cumulative_render_score)
|
||||
assert_equal(0, limits.cumulative_assign_score)
|
||||
end
|
||||
|
||||
def test_cumulative_limits_default_to_nil
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
assert_nil(limits.cumulative_render_score_limit)
|
||||
assert_nil(limits.cumulative_assign_score_limit)
|
||||
end
|
||||
|
||||
def test_cumulative_limits_configurable_via_hash
|
||||
limits = Liquid::ResourceLimits.new(
|
||||
cumulative_render_score_limit: 500,
|
||||
cumulative_assign_score_limit: 300,
|
||||
)
|
||||
assert_equal(500, limits.cumulative_render_score_limit)
|
||||
assert_equal(300, limits.cumulative_assign_score_limit)
|
||||
end
|
||||
|
||||
def test_cumulative_limits_configurable_via_accessor
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
limits.cumulative_render_score_limit = 500
|
||||
assert_equal(500, limits.cumulative_render_score_limit)
|
||||
end
|
||||
|
||||
def test_cumulative_scores_survive_reset
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
limits.increment_render_score(10)
|
||||
limits.increment_assign_score(5)
|
||||
|
||||
limits.reset
|
||||
|
||||
assert_equal(0, limits.render_score)
|
||||
assert_equal(0, limits.assign_score)
|
||||
assert_equal(10, limits.cumulative_render_score)
|
||||
assert_equal(5, limits.cumulative_assign_score)
|
||||
end
|
||||
|
||||
def test_cumulative_scores_accumulate_across_resets
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
limits.increment_render_score(10)
|
||||
limits.reset
|
||||
limits.increment_render_score(20)
|
||||
limits.reset
|
||||
limits.increment_render_score(30)
|
||||
|
||||
assert_equal(30, limits.render_score)
|
||||
assert_equal(60, limits.cumulative_render_score)
|
||||
end
|
||||
|
||||
def test_cumulative_render_score_limit_raises
|
||||
limits = Liquid::ResourceLimits.new(cumulative_render_score_limit: 25)
|
||||
limits.increment_render_score(10)
|
||||
limits.reset
|
||||
limits.increment_render_score(10)
|
||||
limits.reset
|
||||
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
limits.increment_render_score(10)
|
||||
end
|
||||
assert(limits.reached?)
|
||||
end
|
||||
|
||||
def test_cumulative_assign_score_limit_raises
|
||||
limits = Liquid::ResourceLimits.new(cumulative_assign_score_limit: 15)
|
||||
limits.increment_assign_score(8)
|
||||
limits.reset
|
||||
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
limits.increment_assign_score(8)
|
||||
end
|
||||
assert(limits.reached?)
|
||||
end
|
||||
|
||||
def test_per_template_limits_still_work_with_cumulative
|
||||
limits = Liquid::ResourceLimits.new(
|
||||
render_score_limit: 50,
|
||||
cumulative_render_score_limit: 1000,
|
||||
)
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
limits.increment_render_score(51)
|
||||
end
|
||||
end
|
||||
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 = ->(v) { v.reverse }
|
||||
wrong_filter = lambda(&:reverse)
|
||||
|
||||
exception = assert_raises(TypeError) do
|
||||
s.class.add_filter(wrong_filter)
|
||||
|
||||
@@ -24,7 +24,7 @@ class CaseTagUnitTest < Minitest::Test
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
@@ -45,7 +45,7 @@ class CaseTagUnitTest < Minitest::Test
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
@@ -62,7 +62,7 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
with_error_modes(:lax, :strict, :rigid) do
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
end
|
||||
@@ -77,11 +77,31 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
with_error_modes(:lax, :strict, :rigid) do
|
||||
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 -%}
|
||||
@@ -97,7 +117,7 @@ class CaseTagUnitTest < Minitest::Test
|
||||
assert_template_result("one", template, assigns)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
@@ -119,7 +139,7 @@ class CaseTagUnitTest < Minitest::Test
|
||||
assert_template_result("one", template, assigns)
|
||||
end
|
||||
|
||||
with_error_modes(:rigid) do
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
|
||||
@@ -161,8 +161,8 @@ class VariableUnitTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_rigid_filter_argument_parsing
|
||||
with_error_modes(:rigid) do
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user