mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-30 07:35:12 -07:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47a93bc93c | ||
|
|
3bdbd4ac2f | ||
|
|
807d45a6b3 | ||
|
|
7a5e45fc47 | ||
|
|
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 |
@@ -22,6 +22,7 @@ jobs:
|
||||
}
|
||||
- { 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 }
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
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@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
BUNDLE_WITH: spec
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
bundler: latest
|
||||
@@ -66,7 +67,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0
|
||||
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.4.1
|
||||
4.0.2
|
||||
|
||||
@@ -32,7 +32,7 @@ group :test do
|
||||
end
|
||||
|
||||
group :spec do
|
||||
# TODO: temporary - using cp-gate-lax-specs-ai branch until lax specs are properly gated
|
||||
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'cp-gate-lax-specs-ai'
|
||||
# Includes the range resource-limit specs from https://github.com/Shopify/liquid-spec/pull/165.
|
||||
gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '84bf25e0edbca5f7e530574788b0f875ab331f50'
|
||||
gem 'activesupport', require: false
|
||||
end
|
||||
|
||||
+12
-38
@@ -1,46 +1,20 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 6.0.0
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
* (TODO) Add support for boolean expressions everywhere
|
||||
* As variable output `{{ a or b }}`
|
||||
* As filter argument `{{ collection | where: 'prop', a or b }}`
|
||||
* As tag argument `{% render 'snip', enabled: a or b %}`
|
||||
* As conditional tag argument `{% if cond %}` (extending previous behaviour)
|
||||
* (TODO) Add support for subexpression prioritization and associativity
|
||||
* In ascending order of priority:
|
||||
* Logical: `and`, `or` (right to left)
|
||||
* Equality: `==`, `!=`, `<>` (left to right)
|
||||
* Comparison: `>`, `>=`, `<`, `<=`, `contains` (left to right)
|
||||
- For example, this is now supported
|
||||
* `{{ a > b == c < d or e == f }}` which is equivalent to
|
||||
* `{{ ((a > b) == (c < d)) or (e == f) }}`
|
||||
- (TODO) Add support for parenthesized expressions
|
||||
* e.g. `(a or b) and c`
|
||||
* Avoid materializing integer ranges in `for` and `tablerow` loops, and account for each visited range item.
|
||||
|
||||
### Architectural changes
|
||||
* `parse_expression` and `safe_parse_expression` have been removed from `Tag` and `ParseContext`
|
||||
* `Parser` methods now produce AST nodes instead of strings
|
||||
* `Parser#expression` produces a value,
|
||||
* `Parser#string` produces a string,
|
||||
* etc.
|
||||
## 5.13.0
|
||||
|
||||
### Breaking changes
|
||||
* The Environment's `error_mode` option has been removed.
|
||||
* `:warn` is no longer supported
|
||||
* `:lax` and `lax_parse` is no longer supported
|
||||
* `:strict` and `strict_parse` is no longer supported
|
||||
* `strict2_parse` is renamed to `parse_markup`
|
||||
* The `warnings` system has been removed.
|
||||
* `Parser#expression` is renamed to `Parser#expression_string`
|
||||
* `safe_parse_expression` methods are replaced by `Parser#expression`
|
||||
* `parse_expression` methods are replaced by `Parser#unsafe_parse_expression`
|
||||
|
||||
### Migrating from `^5.11.0`
|
||||
- In custom tags that include `ParserSwitching`, rename `strict2_parse` to `parse_markup`
|
||||
- Remove code depending on `:error_mode`
|
||||
- Replace `safe_parse_expression` calls with `Parser#expression`
|
||||
* Add TruffleRuby in CI [Benoit Daloze]
|
||||
* Skip slow test raising many exceptions on non-CRuby [Benoit Daloze]
|
||||
* Reject bare-bracket syntax in strict2 and introduce `self` keyword by [Alok Swamy]
|
||||
* Add strict2_parse to assign and capture tags by [Alok Swamy]
|
||||
* Add strict2_parse to increment and decrement tags by [Alok Swamy]
|
||||
* Update liquid-spec adapters for `missing_features` [Ian Ker-Seymer]
|
||||
* Prevent `SelfDrop` context mutation across render boundaries [Guilherme Carreiro]
|
||||
* Fix `SelfDrop` equality [Guilherme Carreiro]
|
||||
* Let environment `self` shadow `SelfDrop` [Ian Ker-Seymer]
|
||||
|
||||
## 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]
|
||||
|
||||
@@ -93,6 +93,31 @@ LIQUID
|
||||
|
||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
|
||||
|
||||
### Error Modes
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with different parsers that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Environment.default.error_mode = :strict2 # Raises a SyntaxError when invalid syntax is used in all tags
|
||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
|
||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
```ruby
|
||||
Liquid::Template.parse(source, error_mode: :strict)
|
||||
```
|
||||
This is useful for doing things like enabling strict mode only in the theme editor.
|
||||
|
||||
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
||||
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
||||
|
||||
### Undefined variables and filters
|
||||
|
||||
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
|
||||
@@ -124,6 +149,13 @@ template.render!({ 'x' => 1}, { strict_variables: true })
|
||||
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
||||
```
|
||||
|
||||
### Resource limits
|
||||
|
||||
`render_score_limit` and `cumulative_render_score_limit` account for each item visited by
|
||||
integer-range `for` and `tablerow` loops, including loops with empty bodies. This bounds range
|
||||
iteration work when a score limit is configured; `render_length_limit` only bounds generated
|
||||
output and does not by itself limit CPU work for output-free loops.
|
||||
|
||||
### Usage tracking
|
||||
|
||||
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
|
||||
|
||||
@@ -33,12 +33,29 @@ task :rubocop do
|
||||
end
|
||||
end
|
||||
|
||||
desc('runs test suite')
|
||||
desc('runs test suite with lax, strict, and strict2 parsers')
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict2'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict2'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
@@ -61,11 +78,24 @@ task release: :build do
|
||||
end
|
||||
|
||||
namespace :benchmark do
|
||||
desc "Run the liquid benchmark"
|
||||
task :run do
|
||||
ruby "./performance/benchmark.rb"
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :lax do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict parsing"
|
||||
task :strict do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict2 parsing"
|
||||
task :strict2 do
|
||||
ruby "./performance/benchmark.rb strict2"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
|
||||
task run: [:lax, :strict, :strict2]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
task :all do
|
||||
@@ -96,6 +126,11 @@ namespace :profile do
|
||||
task :run do
|
||||
ruby "./performance/profile.rb"
|
||||
end
|
||||
|
||||
desc "Run the liquid profile/performance coverage with strict parsing"
|
||||
task :strict do
|
||||
ruby "./performance/profile.rb strict"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :memory_profile do
|
||||
@@ -116,6 +151,8 @@ end
|
||||
|
||||
desc('run liquid-spec suite across all adapters')
|
||||
task :spec do
|
||||
adapters = Dir['./spec/*.rb'].join(',')
|
||||
sh "bundle exec liquid-spec matrix --adapters=#{adapters} --reference=ruby_liquid"
|
||||
Dir['./spec/*.rb'].sort.each do |adapter|
|
||||
puts "=== Running #{adapter} ==="
|
||||
sh 'bundle', 'exec', 'liquid-spec', 'run', adapter, '--no-max-failures'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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'
|
||||
@@ -82,6 +83,7 @@ require 'liquid/resource_limits'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/template'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/range_slice'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
|
||||
@@ -99,7 +99,9 @@ module Liquid
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
error_mode = context.registers.static[:template_error_mode]
|
||||
suppress_error_text = blank_tag && error_mode != :strict2 && error_mode != :rigid
|
||||
unless suppress_error_text # blank-tag suppression is kept for backwards compatibility outside strict2
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,9 +48,8 @@ module Liquid
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(parser)
|
||||
markup = parser.expression_string
|
||||
@@method_literals[markup] || parser.unsafe_parse_expression(markup)
|
||||
def self.parse_expression(parse_context, markup, safe: false)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
|
||||
+26
-4
@@ -60,6 +60,10 @@ module Liquid
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def warnings
|
||||
@warnings ||= []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= @environment.create_strainer(self, @filters)
|
||||
end
|
||||
@@ -153,6 +157,7 @@ module Liquid
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
@@ -175,14 +180,22 @@ module Liquid
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def [](expression)
|
||||
@string_scanner.string = expression
|
||||
evaluate(Parser.new(@string_scanner).expression)
|
||||
evaluate(Expression.parse(expression, @string_scanner))
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
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)
|
||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||
end
|
||||
@@ -193,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=)
|
||||
|
||||
@@ -240,7 +262,7 @@ module Liquid
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :errors, :strainer, :filters, :disabled_tags
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ module Liquid
|
||||
# The Environment is the container for all configuration options of Liquid, such as
|
||||
# the registered tags, filters, and the default error mode.
|
||||
class Environment
|
||||
# The default error mode for all templates. This can be overridden on a
|
||||
# per-template basis.
|
||||
attr_accessor :error_mode
|
||||
|
||||
# The tags that are available to use in the template.
|
||||
attr_accessor :tags
|
||||
|
||||
@@ -29,14 +33,17 @@ module Liquid
|
||||
# the template.
|
||||
# @param file_system The default file system that is used
|
||||
# to load templates from.
|
||||
# @param error_mode [Symbol] The default error mode for all templates
|
||||
# (either :strict2, :strict, :warn, or :lax).
|
||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
||||
# render exceptions.
|
||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
||||
# @return [Environment] The new environment instance.
|
||||
def build(tags: nil, file_system: nil, exception_renderer: nil)
|
||||
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
|
||||
ret = new
|
||||
ret.tags = tags if tags
|
||||
ret.file_system = file_system if file_system
|
||||
ret.error_mode = error_mode if error_mode
|
||||
ret.exception_renderer = exception_renderer if exception_renderer
|
||||
yield ret if block_given?
|
||||
ret.freeze
|
||||
@@ -68,6 +75,7 @@ module Liquid
|
||||
# @api private
|
||||
def initialize
|
||||
@tags = Tags::STANDARD_TAGS.dup
|
||||
@error_mode = :lax
|
||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
||||
klass.add_filter(StandardFilters)
|
||||
end
|
||||
|
||||
+60
-14
@@ -2,6 +2,8 @@
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
SELF = 'self'
|
||||
|
||||
LITERALS = {
|
||||
nil => nil,
|
||||
'nil' => nil,
|
||||
@@ -11,6 +13,9 @@ module Liquid
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => '',
|
||||
# in lax mode, minus sign can be a VariableLookup
|
||||
# For simplicity and performace, we treat it like a literal
|
||||
'-' => VariableLookup.parse("-", nil).freeze,
|
||||
}.freeze
|
||||
|
||||
DOT = ".".ord
|
||||
@@ -25,6 +30,10 @@ module Liquid
|
||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||
|
||||
class << self
|
||||
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
|
||||
parse(parser.expression, ss, cache)
|
||||
end
|
||||
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
|
||||
@@ -49,34 +58,71 @@ module Liquid
|
||||
|
||||
def inner_parse(markup, ss, cache)
|
||||
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
|
||||
start_markup = Regexp.last_match(1)
|
||||
end_markup = Regexp.last_match(2)
|
||||
start_obj = parse(start_markup, ss, cache)
|
||||
end_obj = parse(end_markup, ss, cache)
|
||||
return RangeLookup.create(
|
||||
start_obj,
|
||||
end_obj,
|
||||
start_markup,
|
||||
end_markup,
|
||||
return RangeLookup.parse(
|
||||
Regexp.last_match(1),
|
||||
Regexp.last_match(2),
|
||||
ss,
|
||||
cache,
|
||||
)
|
||||
end
|
||||
|
||||
if (num = parse_number(markup))
|
||||
if (num = parse_number(markup, ss))
|
||||
num
|
||||
else
|
||||
VariableLookup.parse(markup, ss, cache)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_number(markup)
|
||||
def parse_number(markup, ss)
|
||||
# check if the markup is simple integer or float
|
||||
case markup
|
||||
when INTEGER_REGEX
|
||||
Integer(markup, 10)
|
||||
return Integer(markup, 10)
|
||||
when FLOAT_REGEX
|
||||
markup.to_f
|
||||
return markup.to_f
|
||||
end
|
||||
|
||||
ss.string = markup
|
||||
# the first byte must be a digit or a dash
|
||||
byte = ss.scan_byte
|
||||
|
||||
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
||||
|
||||
if byte == DASH
|
||||
peek_byte = ss.peek_byte
|
||||
|
||||
# if it starts with a dash, the next byte must be a digit
|
||||
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
||||
end
|
||||
|
||||
# The markup could be a float with multiple dots
|
||||
first_dot_pos = nil
|
||||
num_end_pos = nil
|
||||
|
||||
while (byte = ss.scan_byte)
|
||||
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
||||
|
||||
# we found our number and now we are just scanning the rest of the string
|
||||
next if num_end_pos
|
||||
|
||||
if byte == DOT
|
||||
if first_dot_pos.nil?
|
||||
first_dot_pos = ss.pos
|
||||
else
|
||||
# we found another dot, so we know that the number ends here
|
||||
num_end_pos = ss.pos - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
num_end_pos = markup.length if ss.eos?
|
||||
|
||||
if num_end_pos
|
||||
# number ends with a number "123.123"
|
||||
markup.byteslice(0, num_end_pos).to_f
|
||||
else
|
||||
false
|
||||
# number ends with a dot "123."
|
||||
markup.byteslice(0, first_dot_pos).to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :environment
|
||||
attr_reader :partial, :warnings, :error_mode, :environment
|
||||
|
||||
def initialize(options = Const::EMPTY_HASH)
|
||||
@environment = options.fetch(:environment, Environment.default)
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
|
||||
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
|
||||
# This StringScanner will be shared by all of them
|
||||
@@ -37,7 +38,7 @@ module Liquid
|
||||
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner, @expression_cache)
|
||||
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)
|
||||
@@ -49,9 +50,30 @@ module Liquid
|
||||
)
|
||||
end
|
||||
|
||||
def safe_parse_expression(parser)
|
||||
Expression.safe_parse(parser, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def parse_expression(markup, safe: false)
|
||||
if !safe && @error_mode == :strict2
|
||||
# parse_expression is a widely used API. To maintain backward
|
||||
# compatibility while raising awareness about strict2 parser standards,
|
||||
# the safe flag supports API users make a deliberate decision.
|
||||
#
|
||||
# In strict2 mode, markup MUST come from a string returned by the parser
|
||||
# (e.g., parser.expression). We're not calling the parser here to
|
||||
# prevent redundant parser overhead.
|
||||
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
|
||||
end
|
||||
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || @environment.error_mode
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
||||
+16
-103
@@ -2,11 +2,11 @@
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input, expression_cache = nil)
|
||||
@ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@cache = expression_cache
|
||||
@tokens = Lexer.tokenize(@ss)
|
||||
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)
|
||||
@@ -48,75 +48,26 @@ module Liquid
|
||||
end
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
case token[0]
|
||||
when :id
|
||||
variable_lookup
|
||||
when :open_square
|
||||
unnamed_variable_lookup
|
||||
when :string
|
||||
string
|
||||
when :number
|
||||
number
|
||||
when :open_round
|
||||
range_lookup
|
||||
else
|
||||
raise SyntaxError, "#{token} is not a valid expression"
|
||||
end
|
||||
end
|
||||
|
||||
def number
|
||||
num = consume(:number)
|
||||
Expression.parse_number(num)
|
||||
end
|
||||
|
||||
def string
|
||||
consume(:string)[1..-2]
|
||||
end
|
||||
|
||||
def variable_lookup
|
||||
name = consume(:id)
|
||||
lookups, command_flags = variable_lookups
|
||||
if Expression::LITERALS.key?(name) && lookups.empty?
|
||||
Expression::LITERALS[name]
|
||||
else
|
||||
VariableLookup.new(name, lookups, command_flags)
|
||||
end
|
||||
end
|
||||
|
||||
def unnamed_variable_lookup
|
||||
name = indexed_lookup
|
||||
lookups, command_flags = variable_lookups
|
||||
VariableLookup.new(name, lookups, command_flags)
|
||||
end
|
||||
|
||||
def range_lookup
|
||||
consume(:open_round)
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
last = expression
|
||||
consume(:close_round)
|
||||
RangeLookup.create(first, last)
|
||||
end
|
||||
|
||||
def expression_string
|
||||
token = @tokens[@p]
|
||||
case token[0]
|
||||
when :id
|
||||
str = consume
|
||||
str << variable_lookups_string
|
||||
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_string
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups_string
|
||||
str << variable_lookups
|
||||
when :string, :number
|
||||
consume
|
||||
when :open_round
|
||||
consume
|
||||
first = expression_string
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
last = expression_string
|
||||
last = expression
|
||||
consume(:close_round)
|
||||
"(#{first}..#{last})"
|
||||
else
|
||||
@@ -124,23 +75,23 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def argument_string
|
||||
def argument
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '
|
||||
end
|
||||
|
||||
str << expression_string
|
||||
str << expression
|
||||
str
|
||||
end
|
||||
|
||||
def variable_lookups_string
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression_string
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
@@ -151,43 +102,5 @@ module Liquid
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
# Assumes safe input. For cases where you need the string.
|
||||
# Don't use this unless you're sure about what you're doing.
|
||||
def unsafe_parse_expression(markup)
|
||||
parse_expression(markup)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup, @ss, @cache)
|
||||
end
|
||||
|
||||
def variable_lookups
|
||||
lookups = []
|
||||
command_flags = 0
|
||||
i = -1
|
||||
loop do
|
||||
i += 1
|
||||
if look(:open_square)
|
||||
lookups << indexed_lookup
|
||||
elsif consume?(:dot)
|
||||
lookup = consume(:id)
|
||||
lookups << lookup
|
||||
command_flags |= 1 << i if VariableLookup::COMMAND_METHODS.include?(lookup)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
[lookups, command_flags]
|
||||
end
|
||||
|
||||
def indexed_lookup
|
||||
consume(:open_square)
|
||||
expr = expression
|
||||
consume(:close_square)
|
||||
expr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,15 +2,74 @@
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
# Do not use this.
|
||||
#
|
||||
# It's basically doing the same thing the {#parse_with_selected_parser},
|
||||
# except this will try the strict parser regardless of the error mode,
|
||||
# and fall back to the lax parser if the error mode is lax or warn,
|
||||
# except when in strict2 mode where it uses the strict2 parser.
|
||||
#
|
||||
# @deprecated Use {#parse_with_selected_parser} instead.
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
return strict2_parse_with_error_context(markup) if strict2_mode?
|
||||
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :rigid
|
||||
rigid_warn
|
||||
raise
|
||||
when :strict2
|
||||
raise
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
parse_context.warnings << e
|
||||
end
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
parse_markup(markup)
|
||||
case parse_context.error_mode
|
||||
when :rigid then rigid_warn && strict2_parse_with_error_context(markup)
|
||||
when :strict2 then strict2_parse_with_error_context(markup)
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
strict2_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strict2_mode?
|
||||
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rigid_warn
|
||||
Deprecations.warn(':rigid', ':strict2')
|
||||
end
|
||||
|
||||
def strict2_parse_with_error_context(markup)
|
||||
strict2_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"#{markup.strip}\""
|
||||
|
||||
@@ -4,7 +4,7 @@ module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = context.registers[:cached_partials]
|
||||
cache_key = template_name.to_s
|
||||
cache_key = "#{template_name}:#{parse_context.error_mode}"
|
||||
cached = cached_partials[cache_key]
|
||||
return cached if cached
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.create(start_obj, end_obj, start_markup = nil, end_markup = nil)
|
||||
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
|
||||
start_obj = Expression.parse(start_markup, string_scanner, cache)
|
||||
end_obj = Expression.parse(end_markup, string_scanner, cache)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
begin
|
||||
start_obj.to_i..end_obj.to_i
|
||||
rescue NoMethodError
|
||||
start_markup = start_obj.to_s unless start_markup
|
||||
end_markup = end_obj.to_s unless end_markup
|
||||
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
|
||||
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
|
||||
if invalid_expr
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class RangeSlice
|
||||
attr_reader :length
|
||||
|
||||
def initialize(range, from, to, resource_limits)
|
||||
range_length = range.end - range.begin
|
||||
range_length += 1 unless range.exclude_end?
|
||||
range_length = 0 if range_length.negative?
|
||||
|
||||
start = [from, 0].max
|
||||
finish = [to || range_length, range_length].min
|
||||
|
||||
@first = range.begin + start
|
||||
@length = [finish - start, 0].max
|
||||
@direction = 1
|
||||
@resource_limits = resource_limits
|
||||
end
|
||||
|
||||
def empty?
|
||||
@length.zero?
|
||||
end
|
||||
|
||||
def each
|
||||
return enum_for(:each) unless block_given?
|
||||
|
||||
value = @first
|
||||
@length.times do
|
||||
@resource_limits.increment_render_score(1)
|
||||
yield value
|
||||
value += @direction
|
||||
end
|
||||
end
|
||||
|
||||
def reverse!
|
||||
unless empty?
|
||||
@first += @direction * (@length - 1)
|
||||
@direction = -@direction
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -65,5 +65,15 @@ module Liquid
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def safe_parse_expression(parser)
|
||||
parse_context.safe_parse_expression(parser)
|
||||
end
|
||||
|
||||
def parse_expression(markup, safe: false)
|
||||
parse_context.parse_expression(markup, safe: safe)
|
||||
end
|
||||
end
|
||||
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)
|
||||
|
||||
+38
-5
@@ -23,6 +23,9 @@ module Liquid
|
||||
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
|
||||
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@@ -83,23 +86,39 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def parse_markup(markup)
|
||||
def strict2_parse(markup)
|
||||
parser = @parse_context.new_parser(markup)
|
||||
@left = parser.expression
|
||||
@left = safe_parse_expression(parser)
|
||||
parser.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = new_body
|
||||
|
||||
parse_when(markup, body)
|
||||
if strict2_mode?
|
||||
parse_strict2_when(markup, body)
|
||||
else
|
||||
parse_lax_when(markup, body)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_when(markup, body)
|
||||
def parse_strict2_when(markup, body)
|
||||
parser = @parse_context.new_parser(markup)
|
||||
|
||||
loop do
|
||||
expr = Condition.parse_expression(parser)
|
||||
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
|
||||
block = Condition.new(@left, '==', expr)
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
@@ -110,6 +129,20 @@ module Liquid
|
||||
parser.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def parse_lax_when(markup, body)
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||
end
|
||||
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
|
||||
|
||||
@@ -15,6 +15,8 @@ module Liquid
|
||||
# @liquid_syntax
|
||||
# {% cycle string, string, ... %}
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
|
||||
|
||||
attr_reader :variables
|
||||
@@ -54,21 +56,21 @@ module Liquid
|
||||
private
|
||||
|
||||
# cycle [name:] expression(, expression)*
|
||||
def parse_markup(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@variables = []
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
|
||||
|
||||
first_expression = p.expression
|
||||
first_expression = safe_parse_expression(p)
|
||||
if p.look(:colon)
|
||||
# cycle name: expr1, expr2, ...
|
||||
@name = first_expression
|
||||
@is_named = true
|
||||
p.consume(:colon)
|
||||
# After the colon, parse the first variable (required for named cycles)
|
||||
@variables << maybe_dup_lookup(p.expression)
|
||||
@variables << maybe_dup_lookup(safe_parse_expression(p))
|
||||
else
|
||||
# cycle expr1, expr2, ...
|
||||
@variables << maybe_dup_lookup(first_expression)
|
||||
@@ -78,7 +80,7 @@ module Liquid
|
||||
while p.consume?(:comma)
|
||||
break if p.look(:end_of_string)
|
||||
|
||||
@variables << maybe_dup_lookup(p.expression)
|
||||
@variables << maybe_dup_lookup(safe_parse_expression(p))
|
||||
end
|
||||
|
||||
p.consume(:end_of_string)
|
||||
@@ -89,6 +91,35 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
@is_named = true
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
next unless Regexp.last_match(1)
|
||||
|
||||
var = parse_expression(Regexp.last_match(1))
|
||||
maybe_dup_lookup(var)
|
||||
end.compact
|
||||
end
|
||||
|
||||
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
|
||||
# we make it so that the @variables.to_s produces different strings for cycles
|
||||
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
|
||||
|
||||
@@ -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
|
||||
|
||||
+31
-10
@@ -25,6 +25,8 @@ module Liquid
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@@ -70,13 +72,28 @@ module Liquid
|
||||
|
||||
protected
|
||||
|
||||
def parse_markup(markup)
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = parse_expression(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for")
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression_string
|
||||
@collection_name = p.unsafe_parse_expression(collection_name)
|
||||
collection_name = p.expression
|
||||
@collection_name = parse_expression(collection_name, safe: true)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
@@ -87,13 +104,17 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
p.consume(:colon)
|
||||
set_attribute(attribute, p)
|
||||
set_attribute(attribute, p.expression, safe: true)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
|
||||
def collection_segment(context)
|
||||
offsets = context.registers[:for] ||= {}
|
||||
|
||||
@@ -109,7 +130,6 @@ module Liquid
|
||||
end
|
||||
|
||||
collection = context.evaluate(@collection_name)
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
limit_value = context.evaluate(@limit)
|
||||
to = if limit_value.nil?
|
||||
@@ -118,7 +138,9 @@ module Liquid
|
||||
Utils.to_integer(limit_value) + from
|
||||
end
|
||||
|
||||
segment = Utils.slice_collection(collection, from, to)
|
||||
segment = Utils.slice_collection_for_iteration(
|
||||
collection, from, to, context.resource_limits, use_range_to_a: true
|
||||
)
|
||||
segment.reverse! if @reversed
|
||||
|
||||
offsets[@name] = from + segment.length
|
||||
@@ -157,17 +179,16 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def set_attribute(key, p)
|
||||
expr = p.expression_string
|
||||
def set_attribute(key, expr, safe: false)
|
||||
case key
|
||||
when 'offset'
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
p.unsafe_parse_expression(expr)
|
||||
parse_expression(expr, safe: safe)
|
||||
end
|
||||
when 'limit'
|
||||
@limit = p.unsafe_parse_expression(expr)
|
||||
@limit = parse_expression(expr, safe: safe)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+33
-5
@@ -14,6 +14,10 @@ module Liquid
|
||||
# @liquid_syntax_keyword condition The condition to evaluate.
|
||||
# @liquid_syntax_keyword expression The expression to render if the condition is met.
|
||||
class If < Block
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@@ -62,6 +66,10 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'
|
||||
ElseCondition.new
|
||||
@@ -73,11 +81,31 @@ module Liquid
|
||||
block.attach(new_body)
|
||||
end
|
||||
|
||||
def parse_expression(parser)
|
||||
Condition.parse_expression(parser)
|
||||
def parse_expression(markup, safe: false)
|
||||
Condition.parse_expression(parse_context, markup, safe: safe)
|
||||
end
|
||||
|
||||
def parse_markup(markup)
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
|
||||
|
||||
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
|
||||
until expressions.empty?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
|
||||
|
||||
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
end
|
||||
|
||||
condition
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
condition = parse_binary_comparisons(p)
|
||||
p.consume(:end_of_string)
|
||||
@@ -96,9 +124,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = parse_expression(p)
|
||||
a = parse_expression(p.expression, safe: true)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = parse_expression(p)
|
||||
b = parse_expression(p.expression, safe: true)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
|
||||
@@ -20,6 +20,10 @@ module Liquid
|
||||
class Include < Tag
|
||||
prepend Tag::Disableable
|
||||
|
||||
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
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@@ -81,12 +85,18 @@ module Liquid
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
def parse_markup(markup)
|
||||
def for_loop?
|
||||
@is_for_loop
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@template_name_expr = p.expression
|
||||
@variable_name_expr = p.expression if p.id?("for") || p.id?("with")
|
||||
@template_name_expr = safe_parse_expression(p)
|
||||
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)
|
||||
|
||||
@@ -94,13 +104,38 @@ module Liquid
|
||||
while p.look(:id)
|
||||
key = p.consume
|
||||
p.consume(:colon)
|
||||
@attributes[key] = p.expression
|
||||
@attributes[key] = safe_parse_expression(p)
|
||||
p.consume?(:comma)
|
||||
end
|
||||
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ SYNTAX
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,6 +27,7 @@ module Liquid
|
||||
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
|
||||
class Render < Tag
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
@@ -84,12 +85,12 @@ module Liquid
|
||||
end
|
||||
|
||||
# render (string) (with|for expression)? (as id)? (key: value)*
|
||||
def parse_markup(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@template_name_expr = template_name(p)
|
||||
@template_name_expr = parse_expression(strict2_template_name(p), safe: true)
|
||||
with_or_for = p.id?("for") || p.id?("with")
|
||||
@variable_name_expr = p.expression if with_or_for
|
||||
@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)
|
||||
|
||||
@@ -99,15 +100,37 @@ module Liquid
|
||||
while p.look(:id)
|
||||
key = p.consume
|
||||
p.consume(:colon)
|
||||
@attributes[key] = p.expression
|
||||
@attributes[key] = safe_parse_expression(p)
|
||||
p.consume?(:comma)
|
||||
end
|
||||
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def template_name(p)
|
||||
p.string
|
||||
def strict2_template_name(p)
|
||||
p.consume(:string)
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
with_or_for = Regexp.last_match(3)
|
||||
variable_name = Regexp.last_match(4)
|
||||
|
||||
@alias_name = Regexp.last_match(6)
|
||||
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
|
||||
@template_name_expr = parse_expression(template_name)
|
||||
@is_for_loop = (with_or_for == FOR)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
|
||||
@@ -24,6 +24,7 @@ module Liquid
|
||||
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
|
||||
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
|
||||
|
||||
attr_reader :variable_name, :collection_name, :attributes
|
||||
@@ -33,7 +34,7 @@ module Liquid
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def parse_markup(markup)
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@variable_name = p.consume(:id)
|
||||
@@ -42,7 +43,7 @@ module Liquid
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
|
||||
end
|
||||
|
||||
@collection_name = p.expression
|
||||
@collection_name = safe_parse_expression(p)
|
||||
|
||||
p.consume?(:comma)
|
||||
|
||||
@@ -54,25 +55,43 @@ module Liquid
|
||||
end
|
||||
|
||||
p.consume(:colon)
|
||||
@attributes[key] = p.expression
|
||||
@attributes[key] = safe_parse_expression(p)
|
||||
p.consume?(:comma)
|
||||
end
|
||||
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = parse_expression(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = parse_expression(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
|
||||
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
collection = Utils.slice_collection_for_iteration(collection, from, to, context.resource_limits, allow_endless: true)
|
||||
length = collection.length
|
||||
|
||||
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
|
||||
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.resource_limits.increment_write_score(output)
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'] = tablerowloop
|
||||
@@ -83,6 +102,7 @@ module Liquid
|
||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||
super
|
||||
output << '</td>'
|
||||
context.resource_limits.increment_write_score(output)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.interrupt?
|
||||
@@ -92,6 +112,7 @@ module Liquid
|
||||
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
context.resource_limits.increment_write_score(output)
|
||||
end
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
@@ -99,6 +120,7 @@ module Liquid
|
||||
end
|
||||
|
||||
output << "</tr>\n"
|
||||
context.resource_limits.increment_write_score(output)
|
||||
output
|
||||
end
|
||||
|
||||
|
||||
+29
-3
@@ -16,11 +16,25 @@ module Liquid
|
||||
#
|
||||
class Template
|
||||
attr_accessor :root, :name
|
||||
attr_reader :resource_limits
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
attr_reader :profiler
|
||||
|
||||
class << self
|
||||
# Sets how strict the parser should be.
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict enforces correct syntax for most tags
|
||||
# :strict2 enforces correct syntax for all tags
|
||||
def error_mode=(mode)
|
||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
||||
Environment.default.error_mode = mode
|
||||
end
|
||||
|
||||
def error_mode
|
||||
Environment.default.error_mode
|
||||
end
|
||||
|
||||
def default_exception_renderer=(renderer)
|
||||
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
|
||||
Environment.default.exception_renderer = renderer
|
||||
@@ -137,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
|
||||
@@ -173,12 +189,20 @@ module Liquid
|
||||
|
||||
context.template_name ||= name
|
||||
|
||||
previous_error_mode = context.registers.static[:template_error_mode]
|
||||
context.registers.static[:template_error_mode] = @error_mode
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
if previous_error_mode
|
||||
context.registers.static[:template_error_mode] = previous_error_mode
|
||||
else
|
||||
context.registers.static.delete(:template_error_mode)
|
||||
end
|
||||
@errors = context.errors
|
||||
end
|
||||
end
|
||||
@@ -209,6 +233,8 @@ module Liquid
|
||||
ParseContext.new(opts)
|
||||
end
|
||||
|
||||
@warnings = parse_context.warnings
|
||||
@error_mode = parse_context.error_mode
|
||||
parse_context
|
||||
end
|
||||
|
||||
|
||||
@@ -13,6 +13,26 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# This is intentionally separate from slice_collection, whose Array-returning
|
||||
# behavior is used outside of the iteration tags.
|
||||
def self.slice_collection_for_iteration(
|
||||
collection, from, to, resource_limits, allow_endless: false, use_range_to_a: false
|
||||
)
|
||||
if integer_range?(collection)
|
||||
RangeSlice.new(collection, from, to, resource_limits)
|
||||
elsif collection.is_a?(Range)
|
||||
if use_range_to_a && range_method_overridden?(collection, :to_a)
|
||||
# For historically honored custom Range#to_a. Charge the resulting
|
||||
# selection before buffering it, just as for a custom #each.
|
||||
slice_collection_for_iteration_using_each(collection.to_a, from, to, resource_limits)
|
||||
else
|
||||
slice_range_using_each(collection, from, to, resource_limits, allow_endless: allow_endless)
|
||||
end
|
||||
else
|
||||
slice_collection(collection, from, to)
|
||||
end
|
||||
end
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
@@ -38,6 +58,55 @@ module Liquid
|
||||
segments
|
||||
end
|
||||
|
||||
# Arithmetic slicing must not bypass a Range subclass's custom #each.
|
||||
def self.integer_range?(collection)
|
||||
collection.instance_of?(Range) && collection.begin.is_a?(Integer) && collection.end.is_a?(Integer)
|
||||
end
|
||||
private_class_method :integer_range?
|
||||
|
||||
# Preserve support for Ruby-supplied string ranges and custom Range#each.
|
||||
# Their selected length cannot be inferred from integer bounds, but the tags
|
||||
# need it before rendering for loop metadata, continuation offsets, and columns.
|
||||
# Buffer the selection so we do not have to replay a potentially custom iterator.
|
||||
def self.slice_range_using_each(collection, from, to, resource_limits, allow_endless:)
|
||||
# TableRow historically accepted an endless subclass when its custom #each
|
||||
# was finite, while For historically raised through Range#to_a.
|
||||
if collection.end.nil? && !(allow_endless && (!to.nil? || range_method_overridden?(collection, :each)))
|
||||
raise RangeError, "cannot convert endless range to an array"
|
||||
end
|
||||
if collection.begin.nil? && !range_method_overridden?(collection, :each)
|
||||
raise TypeError, "can't iterate from NilClass"
|
||||
end
|
||||
|
||||
slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
|
||||
end
|
||||
private_class_method :slice_range_using_each
|
||||
|
||||
# Custom Range#each can make a nominally beginless range finite; standard
|
||||
# beginless ranges were rejected before reaching this budgeted traversal.
|
||||
def self.slice_collection_for_iteration_using_each(collection, from, to, resource_limits)
|
||||
return [] if to && to <= from
|
||||
|
||||
segments = []
|
||||
index = 0
|
||||
collection.each do |item|
|
||||
break if to && to <= index
|
||||
|
||||
# Charge preparation, including skipped offsets, before buffering; checking
|
||||
# only while rendering the buffered values would leave this work unbudgeted.
|
||||
resource_limits.increment_render_score(1)
|
||||
segments << item if from <= index
|
||||
index += 1
|
||||
end
|
||||
segments
|
||||
end
|
||||
private_class_method :slice_collection_for_iteration_using_each
|
||||
|
||||
def self.range_method_overridden?(collection, method_name)
|
||||
collection.method(method_name).owner != Range.instance_method(method_name).owner
|
||||
end
|
||||
private_class_method :range_method_overridden?
|
||||
|
||||
def self.to_integer(num)
|
||||
return num if num.is_a?(Integer)
|
||||
num = num.to_s
|
||||
|
||||
+68
-7
@@ -30,28 +30,73 @@ module Liquid
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
strict_parse_with_error_mode_fallback(markup)
|
||||
end
|
||||
|
||||
def raw
|
||||
@markup
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && name == other.name && filters == other.filters
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"{{#{markup}}}\""
|
||||
end
|
||||
|
||||
def parse_markup(markup)
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = parse_context.parse_expression(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
next unless f =~ /\w+/
|
||||
filtername = Regexp.last_match(0)
|
||||
filterargs = f.scan(FilterArgsRegex).flatten
|
||||
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
@filters = []
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = p.expression
|
||||
@filters << parse_filter_expressions(p) while p.consume?(:pipe)
|
||||
@name = parse_context.safe_parse_expression(p)
|
||||
while p.consume?(:pipe)
|
||||
filtername = p.consume(:id)
|
||||
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
||||
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
||||
end
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
@filters = []
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@name = parse_context.safe_parse_expression(p)
|
||||
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
|
||||
p.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def parse_filterargs(p)
|
||||
# first argument
|
||||
filterargs = [p.argument]
|
||||
# followed by comma separated others
|
||||
filterargs << p.argument while p.consume?(:comma)
|
||||
filterargs
|
||||
end
|
||||
|
||||
def render(context)
|
||||
obj = context.evaluate(@name)
|
||||
|
||||
@@ -92,6 +137,22 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def lax_parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
||||
else
|
||||
filter_args << parse_context.parse_expression(a)
|
||||
end
|
||||
end
|
||||
result = [filter_name, filter_args]
|
||||
result << keyword_args if keyword_args
|
||||
result
|
||||
end
|
||||
|
||||
# Surprisingly, positional and keyword arguments can be mixed.
|
||||
#
|
||||
# filter = filtername [":" filterargs?]
|
||||
@@ -99,7 +160,7 @@ module Liquid
|
||||
# argument = (positional_argument | keyword_argument)
|
||||
# positional_argument = expression
|
||||
# keyword_argument = id ":" expression
|
||||
def parse_filter_expressions(p)
|
||||
def strict2_parse_filter_expressions(p)
|
||||
filtername = p.consume(:id)
|
||||
filter_args = []
|
||||
keyword_args = {}
|
||||
@@ -121,10 +182,10 @@ module Liquid
|
||||
if p.look(:id) && p.look(:colon, 1)
|
||||
key = p.consume(:id)
|
||||
p.consume(:colon)
|
||||
value = p.expression
|
||||
value = parse_context.safe_parse_expression(p)
|
||||
keyword_arguments[key] = value
|
||||
else
|
||||
positional_arguments << p.expression
|
||||
positional_arguments << parse_context.safe_parse_expression(p)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ module Liquid
|
||||
attr_reader :name, :lookups
|
||||
|
||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
new(markup, string_scanner, cache)
|
||||
end
|
||||
|
||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
@@ -17,10 +21,12 @@ module Liquid
|
||||
cache,
|
||||
)
|
||||
end
|
||||
@name = name
|
||||
|
||||
command_flags = 0
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
lookups.each_index do |i|
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(
|
||||
@@ -29,17 +35,9 @@ module Liquid
|
||||
cache,
|
||||
)
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
command_flags |= 1 << i
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
end
|
||||
|
||||
new(name, lookups, command_flags)
|
||||
end
|
||||
|
||||
def initialize(name, lookups, command_flags)
|
||||
@name = name
|
||||
@lookups = lookups
|
||||
@command_flags = command_flags
|
||||
end
|
||||
|
||||
def lookup_command?(lookup_index)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.11.0"
|
||||
VERSION = "5.13.0"
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ class Profiler
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
require 'stackprof'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
profiler.run
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ require "benchmark/ips"
|
||||
|
||||
require 'liquid'
|
||||
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
RubyVM::YJIT.enable
|
||||
|
||||
STRING_MARKUPS = [
|
||||
"\"foo\"",
|
||||
@@ -45,14 +45,22 @@ NUMBER_MARKUPS = [
|
||||
|
||||
RANGE_MARKUPS = [
|
||||
"(1..30)",
|
||||
"(1...30)",
|
||||
"(1..30..5)",
|
||||
"(1.0...30.0)",
|
||||
"(1.........30)",
|
||||
"(1..foo)",
|
||||
"(foo..30)",
|
||||
"(foo..bar)",
|
||||
"(foo...bar...100)",
|
||||
"(foo...bar...100.0)",
|
||||
]
|
||||
|
||||
LITERAL_MARKUPS = [
|
||||
nil,
|
||||
'nil',
|
||||
'null',
|
||||
'',
|
||||
'true',
|
||||
'false',
|
||||
'blank',
|
||||
@@ -67,39 +75,20 @@ MARKUPS = {
|
||||
"range" => RANGE_MARKUPS,
|
||||
}
|
||||
|
||||
module Liquid
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 5, warmup: 5)
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 5, warmup: 5)
|
||||
|
||||
ss = StringScanner.new('')
|
||||
|
||||
MARKUPS.each do |type, markups|
|
||||
x.report("#{type} - Liquid::Expression#parse") do
|
||||
markups.each do |markup|
|
||||
ss.string = markup
|
||||
Expression.parse(markup, ss)
|
||||
end
|
||||
end
|
||||
|
||||
x.report("#{type} - Liquid::Parser#expression") do
|
||||
markups.each do |markup|
|
||||
ss.string = markup
|
||||
Parser.new(ss).expression
|
||||
end
|
||||
end
|
||||
|
||||
x.report("#{type} - Liquid::Expression.parse(Parser#expression_string)") do
|
||||
markups.each do |markup|
|
||||
ss.string = markup
|
||||
Expression.parse(Parser.new(ss).expression_string, ss)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Liquid::Expression#parse: all") do
|
||||
MARKUPS.values.flatten.each do |markup|
|
||||
Expression.parse(markup)
|
||||
MARKUPS.each do |type, markups|
|
||||
x.report("Liquid::Expression#parse: #{type}") do
|
||||
markups.each do |markup|
|
||||
Liquid::Expression.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Liquid::Expression#parse: all") do
|
||||
MARKUPS.values.flatten.each do |markup|
|
||||
Liquid::Expression.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ require "benchmark/ips"
|
||||
|
||||
require 'liquid'
|
||||
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
RubyVM::YJIT.enable
|
||||
|
||||
EXPRESSIONS = [
|
||||
"foo[1..2].baz",
|
||||
@@ -31,12 +31,11 @@ EXPRESSIONS = [
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 10, warmup: 5)
|
||||
ss = StringScanner.new('')
|
||||
|
||||
x.report("Liquid::Lexer#tokenize") do
|
||||
EXPRESSIONS.each do |expr|
|
||||
ss.string = expr
|
||||
Liquid::Lexer.tokenize(ss)
|
||||
l = Liquid::Lexer.new(expr)
|
||||
l.tokenize
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+16
-3
@@ -6,14 +6,24 @@
|
||||
|
||||
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
||||
require 'liquid'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
# Run core Liquid specs
|
||||
config.features = [:core]
|
||||
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
|
||||
|
||||
@@ -28,9 +38,12 @@ LiquidSpec.render do |ctx, assigns, options|
|
||||
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]
|
||||
|
||||
ctx[:template].render(context)
|
||||
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
|
||||
@@ -7,14 +7,23 @@
|
||||
$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|
|
||||
# Run core Liquid specs plus ActiveSupport SafeBuffer tests
|
||||
config.features = [:core, :activesupport]
|
||||
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
|
||||
|
||||
@@ -29,9 +38,12 @@ LiquidSpec.render do |ctx, assigns, options|
|
||||
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]
|
||||
|
||||
ctx[:template].render(context)
|
||||
LiquidSpecAdapterHelper.with_frozen_time do
|
||||
ctx[:template].render(context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Spec Adapter for Shopify/liquid with YJIT + ActiveSupport
|
||||
# Liquid Spec Adapter for Shopify/liquid with YJIT + strict mode + ActiveSupport
|
||||
#
|
||||
# Run with: bundle exec liquid-spec run spec/ruby_liquid_yjit.rb
|
||||
|
||||
@@ -13,13 +13,24 @@ end
|
||||
|
||||
require 'active_support/all'
|
||||
require 'liquid'
|
||||
require_relative 'support/liquid_spec_adapter_helper'
|
||||
|
||||
LiquidSpec.configure do |config|
|
||||
config.features = [:core, :activesupport]
|
||||
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
|
||||
|
||||
@@ -31,9 +42,12 @@ LiquidSpec.render do |ctx, assigns, options|
|
||||
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]
|
||||
|
||||
ctx[:template].render(context)
|
||||
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
|
||||
@@ -39,11 +39,13 @@ class AssignTest < Minitest::Test
|
||||
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
|
||||
end
|
||||
|
||||
def test_assign_throws_on_unsupported_syntax
|
||||
def test_assign_uses_error_mode
|
||||
assert_match_syntax_error(
|
||||
"Expected dotdot but found pipe",
|
||||
"Expected dotdot but found pipe in ",
|
||||
"{% assign foo = ('X' | downcase) %}",
|
||||
error_mode: :strict,
|
||||
)
|
||||
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
|
||||
end
|
||||
|
||||
def test_expression_with_whitespace_in_square_brackets
|
||||
@@ -95,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
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BlankBodyErrorHandlingTest < Minitest::Test
|
||||
COMPARISON_ERROR = 'Liquid error (line 1): comparison of Integer with String failed'
|
||||
INVALID_INTEGER_ERROR = 'Liquid error (line 1): invalid integer'
|
||||
|
||||
def render_inline(source, error_mode:, assigns: {})
|
||||
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render(assigns, render_errors: true)
|
||||
end
|
||||
|
||||
def assert_render_raises(source, error_mode:, assigns: {}, message: nil)
|
||||
error = assert_raises(Liquid::ArgumentError) do
|
||||
Liquid::Template.parse(source, line_numbers: true, error_mode: error_mode).render!(assigns)
|
||||
end
|
||||
assert_includes(error.message, message) if message
|
||||
end
|
||||
|
||||
def test_blank_if_body_suppresses_inline_error_text_in_lax_and_strict
|
||||
[:lax, :strict].each do |mode|
|
||||
assert_equal('', render_inline('{% if 5 > "x" %}{% endif %}', error_mode: mode))
|
||||
end
|
||||
end
|
||||
|
||||
def test_blank_unless_body_suppresses_inline_error_text_in_lax_and_strict
|
||||
[:lax, :strict].each do |mode|
|
||||
assert_equal('', render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: mode))
|
||||
end
|
||||
end
|
||||
|
||||
def test_blank_for_body_suppresses_inline_error_text_in_lax_and_strict
|
||||
[:lax, :strict].each do |mode|
|
||||
assert_equal('', render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }))
|
||||
end
|
||||
end
|
||||
|
||||
def test_strict2_blank_if_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% endif %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_whitespace_if_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %} {% endif %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_assign_if_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% assign a = 1 %}{% endif %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_comment_if_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% comment %}c{% endcomment %}{% endif %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_capture_if_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% capture c %}text{% endcapture %}{% endif %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_blank_unless_body_shows_inline_error_text
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% unless 5 > "x" %} {% endunless %}', error_mode: :strict2))
|
||||
end
|
||||
|
||||
def test_strict2_blank_for_body_shows_inline_error_text
|
||||
assert_equal(INVALID_INTEGER_ERROR, render_inline('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: :strict2, assigns: { 'xs' => 'bad' }))
|
||||
end
|
||||
|
||||
def test_nonblank_bodies_show_inline_error_text_in_all_modes
|
||||
[:lax, :strict, :strict2].each do |mode|
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% echo 1 %}{% endif %}', error_mode: mode))
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{{ "" }}{% endif %}', error_mode: mode))
|
||||
assert_equal(COMPARISON_ERROR, render_inline('{% if 5 > "x" %}{% else %}E{% endif %}', error_mode: mode))
|
||||
end
|
||||
end
|
||||
|
||||
def test_raised_errors_are_not_swallowed_by_blank_if_body
|
||||
[:lax, :strict, :strict2].each do |mode|
|
||||
assert_render_raises('{% if 5 > "x" %}{% endif %}', error_mode: mode, message: 'comparison of Integer with String failed')
|
||||
end
|
||||
end
|
||||
|
||||
def test_raised_errors_are_not_swallowed_by_blank_for_body
|
||||
[:lax, :strict, :strict2].each do |mode|
|
||||
assert_render_raises('{% for i in (1..3) offset: xs %}{% endfor %}', error_mode: mode, assigns: { 'xs' => 'bad' }, message: 'invalid integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -632,9 +632,11 @@ class ContextTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
||||
context = Context.new
|
||||
context.key?('unknown')
|
||||
assert_empty(context.errors)
|
||||
with_error_modes(:strict) do
|
||||
context = Context.new
|
||||
context.key?('unknown')
|
||||
assert_empty(context.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def test_key_lookup_will_raise_for_missing_keys_when_strict_variables_is_enabled
|
||||
|
||||
@@ -67,11 +67,20 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_unrecognized_operator
|
||||
assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal(' Liquid error: Unknown operator =! ', template.render)
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(Liquid::ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
source = <<~LIQUID
|
||||
foobar
|
||||
@@ -95,6 +104,25 @@ class ErrorHandlingTest < Minitest::Test
|
||||
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
|
||||
end
|
||||
|
||||
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
template = Liquid::Template.parse(
|
||||
'
|
||||
foobar
|
||||
|
||||
{% if 1 =! 2 %}ok{% endif %}
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :warn,
|
||||
line_numbers: true,
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message),
|
||||
)
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(
|
||||
@@ -105,6 +133,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
bla
|
||||
',
|
||||
error_mode: :strict,
|
||||
line_numbers: true,
|
||||
)
|
||||
end
|
||||
@@ -128,16 +157,34 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
|
||||
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}')
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
|
||||
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
|
||||
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
|
||||
assert_equal('', template.render)
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
|
||||
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
|
||||
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
def test_exceptions_propagate
|
||||
assert_raises(Exception) do
|
||||
@@ -218,11 +265,13 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_bug_compatible_silencing_of_errors_in_blank_nodes
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("Liquid error: comparison of Integer with String failed0", output)
|
||||
with_error_modes(:lax, :strict) do
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("Liquid error: comparison of Integer with String failed0", output)
|
||||
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("0", output)
|
||||
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
|
||||
assert_equal("0", output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_syntax_error_is_raised_with_template_name
|
||||
|
||||
@@ -27,6 +27,11 @@ class ExpressionTest < Minitest::Test
|
||||
assert_template_result("-17.42", "{{ -17.42 }}")
|
||||
assert_template_result("2.5", "{{ 2.5 }}")
|
||||
|
||||
with_error_modes(:lax) do
|
||||
assert_expression_result(0.0, "0.....5")
|
||||
assert_expression_result(0.0, "-0..1")
|
||||
end
|
||||
|
||||
assert_expression_result(1.5, "1.5")
|
||||
|
||||
# this is a unfortunate quirky behavior of Liquid
|
||||
@@ -46,11 +51,24 @@ class ExpressionTest < Minitest::Test
|
||||
"{{ (false..true) }}",
|
||||
)
|
||||
assert_match_syntax_error(
|
||||
"Liquid syntax error (line 1): Invalid expression type '1..2' in range expression",
|
||||
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
|
||||
"{{ ((1..2)..3) }}",
|
||||
)
|
||||
end
|
||||
|
||||
def test_quirky_negative_sign_expression_markup
|
||||
result = Expression.parse("-", nil)
|
||||
assert(result.is_a?(VariableLookup))
|
||||
assert_equal("-", result.name)
|
||||
|
||||
# for this template, the expression markup is "-"
|
||||
assert_template_result(
|
||||
"",
|
||||
"{{ - 'theme.css' - }}",
|
||||
error_mode: :lax,
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
@@ -67,7 +85,7 @@ class ExpressionTest < Minitest::Test
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
[],
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
@@ -91,7 +109,7 @@ class ExpressionTest < Minitest::Test
|
||||
cache = parse_context.instance_variable_get(:@expression_cache)
|
||||
|
||||
assert_equal(
|
||||
[],
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
@@ -112,7 +130,7 @@ class ExpressionTest < Minitest::Test
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
[],
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
@@ -134,30 +152,30 @@ class ExpressionTest < Minitest::Test
|
||||
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
||||
end
|
||||
|
||||
def test_parser_expression_with_variable_lookup
|
||||
def test_safe_parse_with_variable_lookup
|
||||
parse_context = Liquid::ParseContext.new
|
||||
parser = parse_context.new_parser('product.title')
|
||||
result = parser.expression
|
||||
result = Liquid::Expression.safe_parse(parser)
|
||||
|
||||
assert_instance_of(Liquid::VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_parser_expression_with_number
|
||||
def test_safe_parse_with_number
|
||||
parse_context = Liquid::ParseContext.new
|
||||
parser = parse_context.new_parser('42')
|
||||
result = parser.expression
|
||||
result = Liquid::Expression.safe_parse(parser)
|
||||
|
||||
assert_equal(42, result)
|
||||
end
|
||||
|
||||
def test_parser_expression_raises_syntax_error_for_invalid_expression
|
||||
def test_safe_parse_raises_syntax_error_for_invalid_expression
|
||||
parse_context = Liquid::ParseContext.new
|
||||
parser = parse_context.new_parser('')
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) do
|
||||
parser.expression
|
||||
Liquid::Expression.safe_parse(parser)
|
||||
end
|
||||
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
|
||||
@@ -31,25 +31,58 @@ class ParsingQuirksTest < Minitest::Test
|
||||
def test_error_on_empty_filter
|
||||
assert(Template.parse("{{test}}"))
|
||||
|
||||
assert_raises(Liquid::SyntaxError) { Template.parse("{{|test}}") }
|
||||
assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") }
|
||||
with_error_modes(:lax) do
|
||||
assert(Template.parse("{{|test}}"))
|
||||
end
|
||||
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
|
||||
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
|
||||
end
|
||||
end
|
||||
|
||||
def test_meaningless_parens_error
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_unexpected_characters_syntax_error
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "true && false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "true && false"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
assert_raises(SyntaxError) do
|
||||
markup = "false || true"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
assert_raises(SyntaxError) do
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
|
||||
assert(Template.parse("{{test}}", error_mode: :lax))
|
||||
assert(Template.parse("{{|test|}}", error_mode: :lax))
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_modes(:lax) do
|
||||
assigns = { 'b' => 'bar', 'c' => 'baz' }
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def test_unexpected_characters_silently_eat_logic_lax
|
||||
with_error_modes(:lax) do
|
||||
markup = "true && false"
|
||||
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
|
||||
markup = "false || true"
|
||||
Template.parse("{% if #{markup} %} YES {% endif %}")
|
||||
assert_template_result('', "{% if #{markup} %} YES {% endif %}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,6 +92,32 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_unanchored_filter_arguments
|
||||
with_error_modes(:lax) do
|
||||
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
|
||||
|
||||
assert_template_result('x', "{{ 'X' | downcase) }}")
|
||||
|
||||
# After the messed up quotes a filter without parameters (reverse) should work
|
||||
# but one with parameters (remove) shouldn't be detected.
|
||||
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
|
||||
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_invalid_variables_work
|
||||
with_error_modes(:lax) do
|
||||
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
|
||||
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_extra_dots_in_ranges
|
||||
with_error_modes(:lax) do
|
||||
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_blank_variable_markup
|
||||
assert_template_result('', "{{}}")
|
||||
end
|
||||
@@ -72,4 +131,24 @@ class ParsingQuirksTest < Minitest::Test
|
||||
def test_contains_in_id
|
||||
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
|
||||
end
|
||||
|
||||
def test_incomplete_expression
|
||||
with_error_modes(:lax) do
|
||||
assert_template_result("false", "{{ false - }}")
|
||||
assert_template_result("false", "{{ false > }}")
|
||||
assert_template_result("false", "{{ false < }}")
|
||||
assert_template_result("false", "{{ false = }}")
|
||||
assert_template_result("false", "{{ false ! }}")
|
||||
assert_template_result("false", "{{ false 1 }}")
|
||||
assert_template_result("false", "{{ false a }}")
|
||||
|
||||
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
|
||||
end
|
||||
end
|
||||
end # ParsingQuirksTest
|
||||
|
||||
@@ -44,32 +44,42 @@ 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
|
||||
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!)
|
||||
method_names = Array.new(3) { |index| "untrusted_drop_method_#{object_id}_#{index}" }
|
||||
|
||||
method_names.each do |method_name|
|
||||
assert_equal("", Template.parse("{{ drop.#{method_name} }}").render!(assigns))
|
||||
end
|
||||
|
||||
# JITs can intern internal metadata; untrusted Drop method names must not be interned.
|
||||
assert_equal([], Symbol.all_symbols.map(&:to_s) & method_names)
|
||||
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
|
||||
yield
|
||||
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -1174,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
|
||||
|
||||
@@ -91,21 +91,26 @@ class CycleTagTest < Minitest::Test
|
||||
assert_match(/Syntax Error in 'cycle' - Valid syntax: cycle \[name :\] var/, error.message)
|
||||
end
|
||||
|
||||
def test_cycle_tag_unsupported_legacy_quirk
|
||||
def test_cycle_tag_with_error_mode
|
||||
# QuotedFragment is more permissive than what Parser#expression allows.
|
||||
template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
|
||||
template2 = "{% cycle .5: 'a', 'b' %}"
|
||||
|
||||
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("b", template1)
|
||||
assert_template_result("a", template2)
|
||||
end
|
||||
|
||||
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
|
||||
with_error_modes(:strict2) do
|
||||
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||
|
||||
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
|
||||
|
||||
def test_cycle_with_trailing_elements_legacy_syntax
|
||||
def test_cycle_with_trailing_elements
|
||||
assignments = "{% assign a = 'A' %}{% assign n = 'N' %}"
|
||||
|
||||
template1 = "#{assignments}{% cycle 'a' 'b', 'c' %}"
|
||||
@@ -114,19 +119,29 @@ class CycleTagTest < Minitest::Test
|
||||
template4 = "#{assignments}{% cycle n e: 'a', 'b', 'c' %}"
|
||||
template5 = "#{assignments}{% cycle n e 'a', 'b', 'c' %}"
|
||||
|
||||
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||
error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
|
||||
error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
|
||||
error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("a", template1)
|
||||
assert_template_result("a", template2)
|
||||
assert_template_result("a", template3)
|
||||
assert_template_result("N", template4)
|
||||
assert_template_result("N", template5)
|
||||
end
|
||||
|
||||
expected_error = /Expected end_of_string but found/
|
||||
with_error_modes(:strict2) do
|
||||
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
|
||||
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
|
||||
error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
|
||||
error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
|
||||
error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
|
||||
|
||||
assert_match(expected_error, error1.message)
|
||||
assert_match(expected_error, error2.message)
|
||||
assert_match(expected_error, error3.message)
|
||||
assert_match(expected_error, error4.message)
|
||||
assert_match(expected_error, error5.message)
|
||||
expected_error = /Expected end_of_string but found/
|
||||
|
||||
assert_match(expected_error, error1.message)
|
||||
assert_match(expected_error, error2.message)
|
||||
assert_match(expected_error, error3.message)
|
||||
assert_match(expected_error, error4.message)
|
||||
assert_match(expected_error, error5.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_cycle_name_with_invalid_expression
|
||||
@@ -136,8 +151,14 @@ class CycleTagTest < Minitest::Test
|
||||
{% endfor %}
|
||||
LIQUID
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_cycle_variable_with_invalid_expression
|
||||
@@ -147,7 +168,13 @@ class CycleTagTest < Minitest::Test
|
||||
{% endfor %}
|
||||
LIQUID
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -465,4 +465,85 @@ HERE
|
||||
|
||||
assert(context.registers[:for_stack].empty?)
|
||||
end
|
||||
|
||||
def test_integer_range_is_not_materialized_and_charges_empty_iterations
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse('{% for i in numbers %}{% endfor %}')
|
||||
template.resource_limits.render_score_limit = 3
|
||||
|
||||
assert_raises(Liquid::MemoryError) { template.render!('numbers' => range) }
|
||||
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_equal(4, template.resource_limits.render_score)
|
||||
end
|
||||
|
||||
def test_integer_range_uses_arithmetic_offsets_reversal_and_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% for i in numbers reversed offset:997 limit:2 %}{{ forloop.length }}:{{ i }}{% endfor %}',
|
||||
)
|
||||
template.resource_limits.render_score_limit = 10
|
||||
|
||||
assert_equal('2:9992:998', template.render!('numbers' => range))
|
||||
end
|
||||
|
||||
def test_range_scores_cannot_be_bypassed_by_repeated_renders
|
||||
template = Template.parse('{% for i in (1..2) %}{% endfor %}')
|
||||
template.resource_limits.cumulative_render_score_limit = 3
|
||||
assert_equal('', template.render!)
|
||||
assert_raises(Liquid::MemoryError) { template.render! }
|
||||
end
|
||||
|
||||
def test_range_break_charges_only_visited_items_and_preserves_full_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% for i in numbers reversed %}{{ forloop.length }}:{{ i }}{% break %}{% endfor %}',
|
||||
)
|
||||
template.resource_limits.render_score_limit = 10
|
||||
|
||||
assert_equal('1000:1000', template.render!('numbers' => range))
|
||||
assert_operator(template.resource_limits.render_score, :<=, 10)
|
||||
end
|
||||
|
||||
def test_range_subclass_uses_its_custom_each
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(nil, 3)
|
||||
|
||||
assert_template_result('1020', '{% for i in numbers %}{{ i }}{% endfor %}', { 'numbers' => range })
|
||||
end
|
||||
|
||||
def test_range_subclass_custom_to_a_is_honored_for_finite_and_open_bounds
|
||||
range_class = Class.new(Range) do
|
||||
def to_a
|
||||
[42]
|
||||
end
|
||||
end
|
||||
|
||||
[range_class.new(1, 3), range_class.new(nil, 3), range_class.new(1, nil)].each do |range|
|
||||
assert_template_result('42', '{% for i in numbers %}{{ i }}{% endfor %}', { 'numbers' => range })
|
||||
end
|
||||
end
|
||||
|
||||
def test_endless_range_remains_unsupported_with_a_limit
|
||||
template = Template.parse('{% for i in numbers limit:2 %}{{ i }}{% endfor %}')
|
||||
|
||||
assert_raises(RangeError) { template.render!('numbers' => (1..)) }
|
||||
end
|
||||
|
||||
def test_endless_range_subclass_with_custom_each_remains_unsupported
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(1, nil)
|
||||
|
||||
assert_raises(RangeError) do
|
||||
Template.parse('{% for i in numbers %}{{ i }}{% endfor %}').render!('numbers' => range)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -204,13 +204,23 @@ class IncludeTagTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_parsing_errors_for_legacy_quirk
|
||||
assert_syntax_error(
|
||||
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
assert_syntax_error(
|
||||
'{% include "snippet" | filter %}',
|
||||
)
|
||||
def test_strict2_parsing_errors
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result(
|
||||
'hello value1 value2',
|
||||
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
|
||||
)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_syntax_error(
|
||||
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
assert_syntax_error(
|
||||
'{% include "snippet" | filter %}',
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def test_optional_commas
|
||||
@@ -291,10 +301,16 @@ class IncludeTagTest < Minitest::Test
|
||||
env = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_modes(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_modes(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -349,7 +365,7 @@ class IncludeTagTest < Minitest::Test
|
||||
file_system: StubFileSystem.new('simple' => 'simple'),
|
||||
)
|
||||
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", environment: env)
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
|
||||
template.render(nil, strict_variables: true)
|
||||
|
||||
assert_equal([], template.errors)
|
||||
@@ -388,21 +404,84 @@ class IncludeTagTest < Minitest::Test
|
||||
def test_include_template_with_invalid_expression
|
||||
template = "{% include foo=>bar %}"
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_with_invalid_expression
|
||||
template = '{% include "snippet" with foo=>bar %}'
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_attribute_with_invalid_expression
|
||||
template = '{% include "snippet", key: foo=>bar %}'
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_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,13 +105,23 @@ class RenderTagTest < Minitest::Test
|
||||
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
|
||||
end
|
||||
|
||||
def test_parsing_errors_legacy_syntax
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" | filter %}',
|
||||
)
|
||||
def test_strict2_parsing_errors
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result(
|
||||
'hello value1 value2',
|
||||
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
|
||||
)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
|
||||
)
|
||||
assert_syntax_error(
|
||||
'{% render "snippet" | filter %}',
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def test_optional_commas
|
||||
@@ -164,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
|
||||
|
||||
@@ -307,13 +317,27 @@ class RenderTagTest < Minitest::Test
|
||||
|
||||
def test_render_with_invalid_expression
|
||||
template = '{% render "snippet" with foo=>bar %}'
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_render_attribute_with_invalid_expression
|
||||
template = '{% render "snippet", key: foo=>bar %}'
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
|
||||
with_error_modes(:lax, :strict) do
|
||||
refute_nil(Template.parse(template))
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,6 +188,29 @@ class TableRowTest < Minitest::Test
|
||||
assert_template_result(expected_output, template)
|
||||
end
|
||||
|
||||
def test_table_row_renders_correct_error_message_for_invalid_parameters
|
||||
assert_template_result(
|
||||
"Liquid error (line 1): invalid integer",
|
||||
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
|
||||
error_mode: :warn,
|
||||
render_errors: true,
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"Liquid error (line 1): invalid integer",
|
||||
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
|
||||
error_mode: :warn,
|
||||
render_errors: true,
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"Liquid error (line 1): invalid integer",
|
||||
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
|
||||
render_errors: true,
|
||||
error_mode: :warn,
|
||||
)
|
||||
end
|
||||
|
||||
def test_table_row_handles_interrupts
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
|
||||
@@ -236,7 +259,7 @@ class TableRowTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_with_cols_attribute
|
||||
def test_tablerow_with_cols_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -247,10 +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
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_limit_attribute
|
||||
def test_tablerow_with_limit_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -260,10 +285,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_offset_attribute
|
||||
def test_tablerow_with_offset_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -273,10 +300,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_range_attribute
|
||||
def test_tablerow_with_range_attribute_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -286,10 +315,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_multiple_attributes
|
||||
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
|
||||
@@ -300,10 +331,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_variable_collection
|
||||
def test_tablerow_with_variable_collection_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -314,10 +347,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_dotted_access
|
||||
def test_tablerow_with_dotted_access_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -328,10 +363,12 @@ class TableRowTest < Minitest::Test
|
||||
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_bracketed_access
|
||||
def test_tablerow_with_bracketed_access_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -341,10 +378,12 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">10</td><td class="col2">20</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_attributes
|
||||
def test_tablerow_without_attributes_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -354,24 +393,30 @@ class TableRowTest < Minitest::Test
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template)
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_without_in_keyword
|
||||
def test_tablerow_without_in_keyword_in_strict2_mode
|
||||
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
|
||||
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
|
||||
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
|
||||
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_strict2_mode
|
||||
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
|
||||
|
||||
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)
|
||||
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
|
||||
def test_tablerow_with_empty_collection_in_strict2_mode
|
||||
template = <<~LIQUID.chomp
|
||||
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
|
||||
LIQUID
|
||||
@@ -381,18 +426,133 @@ class TableRowTest < Minitest::Test
|
||||
</tr>
|
||||
OUTPUT
|
||||
|
||||
assert_template_result(expected, template, { 'empty_array' => [] })
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result(expected, template, { 'empty_array' => [] })
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_invalid_attribute
|
||||
def test_tablerow_with_invalid_attribute_strict_vs_strict2
|
||||
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
|
||||
|
||||
expected = <<~OUTPUT
|
||||
<tr class="row1">
|
||||
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td><td class="col4">4</td><td class="col5">5</td></tr>
|
||||
OUTPUT
|
||||
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_tablerow_with_invalid_expression
|
||||
def test_tablerow_with_invalid_expression_strict_vs_strict2
|
||||
template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
|
||||
with_error_modes(:lax, :strict) do
|
||||
expected = <<~OUTPUT
|
||||
<tr class="row1">
|
||||
</tr>
|
||||
OUTPUT
|
||||
assert_template_result(expected, template)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(SyntaxError) { Template.parse(template) }
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_integer_range_is_not_materialized_and_charges_empty_iterations
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse('{% tablerow i in numbers %}{% endtablerow %}')
|
||||
template.resource_limits.render_score_limit = 3
|
||||
|
||||
assert_raises(Liquid::MemoryError) { template.render!('numbers' => range) }
|
||||
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_equal(4, template.resource_limits.render_score)
|
||||
end
|
||||
|
||||
def test_integer_range_uses_arithmetic_offsets_limit_and_full_metadata
|
||||
range = bounded_integer_range_with_tripwires
|
||||
template = Template.parse(
|
||||
'{% tablerow i in numbers offset:997 limit:2 %}{{ tablerowloop.length }}:{{ tablerowloop.index }}:{{ i }}{% endtablerow %}',
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">2:1:998</td><td class=\"col2\">2:2:999</td></tr>\n",
|
||||
template.render!('numbers' => range),
|
||||
)
|
||||
end
|
||||
|
||||
def test_tablerow_checks_generated_output_during_empty_body_iteration
|
||||
template = Template.parse('{% tablerow i in (1..100) %}{% endtablerow %}')
|
||||
template.resource_limits.render_length_limit = 40
|
||||
checked_lengths = []
|
||||
limits = template.resource_limits
|
||||
original_increment_write_score = limits.method(:increment_write_score)
|
||||
limits.define_singleton_method(:increment_write_score) do |output|
|
||||
checked_lengths << output.bytesize
|
||||
original_increment_write_score.call(output)
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Memory limits exceeded', template.render)
|
||||
assert(template.resource_limits.reached?)
|
||||
assert_operator(checked_lengths.last, :<, 1000)
|
||||
end
|
||||
|
||||
def test_tablerow_range_scores_persist_across_renders
|
||||
template = Template.parse('{% tablerow i in (1..2) %}{% endtablerow %}')
|
||||
template.resource_limits.cumulative_render_score_limit = 3
|
||||
template.render!
|
||||
assert_raises(Liquid::MemoryError) { template.render! }
|
||||
end
|
||||
|
||||
def test_range_subclass_uses_its_custom_each_with_beginless_bounds
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(nil, 3)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">10</td><td class=\"col2\">20</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
|
||||
def test_endless_range_subclass_uses_its_custom_each_without_a_limit
|
||||
range = Class.new(Range) do
|
||||
def each
|
||||
yield 10
|
||||
yield 20
|
||||
end
|
||||
end.new(1, nil)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">10</td><td class=\"col2\">20</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
|
||||
def test_range_subclass_custom_to_a_is_not_used
|
||||
range = Class.new(Range) do
|
||||
def to_a
|
||||
[42]
|
||||
end
|
||||
end.new(1, 3)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td><td class=\"col3\">3</td></tr>\n",
|
||||
'{% tablerow i in numbers %}{{ i }}{% endtablerow %}',
|
||||
{ 'numbers' => range },
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,6 +44,16 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
|
||||
end
|
||||
|
||||
def test_warnings_is_not_exponential_time
|
||||
str = "false"
|
||||
100.times do
|
||||
str = "{% if true %}true{% else %}#{str}{% endif %}"
|
||||
end
|
||||
|
||||
t = Template.parse(str)
|
||||
assert_equal([], Timeout.timeout(1) { t.warnings })
|
||||
end
|
||||
|
||||
def test_instance_assigns_persist_on_same_template_parsing_between_renders
|
||||
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
|
||||
assert_equal('foo', t.render!)
|
||||
@@ -122,7 +132,7 @@ class TemplateTest < Minitest::Test
|
||||
assert_equal("Liquid error: Memory limits exceeded", t.render)
|
||||
assert(t.resource_limits.reached?)
|
||||
|
||||
t.resource_limits.render_score_limit = 200
|
||||
t.resource_limits.render_score_limit = 201
|
||||
assert_equal(" foo " * 100, t.render!)
|
||||
refute_nil(t.resource_limits.render_score)
|
||||
end
|
||||
@@ -169,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 %}")
|
||||
@@ -249,7 +339,7 @@ class TemplateTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_nil_value_does_not_raise
|
||||
t = Template.parse("some{{x}}thing")
|
||||
t = Template.parse("some{{x}}thing", error_mode: :strict)
|
||||
result = t.render!({ 'x' => nil }, strict_variables: true)
|
||||
|
||||
assert_equal(0, t.errors.count)
|
||||
|
||||
@@ -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),
|
||||
@@ -176,33 +176,102 @@ class VariableTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_variable_lookup_should_not_hang_with_invalid_syntax
|
||||
Timeout.timeout(1) do
|
||||
assert_template_result(
|
||||
'bar',
|
||||
"{{['foo'}}",
|
||||
{
|
||||
'foo' => 'bar',
|
||||
},
|
||||
error_mode: :lax,
|
||||
)
|
||||
end
|
||||
|
||||
very_long_key = "1234567890" * 100
|
||||
|
||||
template_list = [
|
||||
"{{['#{very_long_key}']}}", # valid
|
||||
"{{['#{very_long_key}'}}", # missing closing bracket
|
||||
"{{[['#{very_long_key}']}}", # extra open bracket
|
||||
]
|
||||
|
||||
template_list.each do |template|
|
||||
Timeout.timeout(1) do
|
||||
assert_template_result(
|
||||
'bar',
|
||||
template,
|
||||
{
|
||||
very_long_key => 'bar',
|
||||
},
|
||||
error_mode: :lax,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_filter_with_single_trailing_comma
|
||||
template = '{{ "hello" | append: "world", }}'
|
||||
|
||||
assert_template_result('helloworld', template)
|
||||
with_error_modes(:strict) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('helloworld', template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_multiple_filters_with_trailing_commas
|
||||
template = '{{ "hello" | append: "1", | append: "2", }}'
|
||||
|
||||
assert_template_result('hello12', template)
|
||||
with_error_modes(:strict) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('hello12', template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_filter_with_colon_but_no_arguments
|
||||
template = '{{ "test" | upcase: }}'
|
||||
|
||||
assert_template_result('TEST', template)
|
||||
with_error_modes(:strict) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TEST', template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_filter_chain_with_colon_no_args
|
||||
template = '{{ "test" | append: "x" | upcase: }}'
|
||||
|
||||
assert_template_result('TESTX', template)
|
||||
with_error_modes(:strict) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TESTX', template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_combining_trailing_comma_and_empty_args
|
||||
template = '{{ "test" | append: "x", | upcase: }}'
|
||||
|
||||
assert_template_result('TESTX', template)
|
||||
with_error_modes(:strict) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
with_error_modes(:strict2) do
|
||||
assert_template_result('TESTX', template)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+32
-6
@@ -8,6 +8,13 @@ $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
|
||||
require 'liquid.rb'
|
||||
require 'liquid/profiler'
|
||||
|
||||
mode = :strict
|
||||
if (env_mode = ENV['LIQUID_PARSER_MODE'])
|
||||
puts "-- #{env_mode.upcase} ERROR MODE"
|
||||
mode = env_mode.to_sym
|
||||
end
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
|
||||
if Minitest.const_defined?('Test')
|
||||
# We're on Minitest 5+. Nothing to do here.
|
||||
else
|
||||
@@ -25,29 +32,38 @@ module Minitest
|
||||
module Assertions
|
||||
include Liquid
|
||||
|
||||
# Exact Range fixture for fast-path tests; singleton tripwires must remain
|
||||
# untouched because the arithmetic path does not materialize or traverse it.
|
||||
def bounded_integer_range_with_tripwires
|
||||
range = (1..1000).dup
|
||||
range.define_singleton_method(:to_a) { raise 'range was materialized' }
|
||||
range.define_singleton_method(:each) { raise 'range was traversed' }
|
||||
range
|
||||
end
|
||||
|
||||
def assert_template_result(
|
||||
expected, template, assigns = {},
|
||||
message: nil, partials: nil, render_errors: false,
|
||||
message: nil, partials: nil, error_mode: Liquid::Environment.default.error_mode, render_errors: false,
|
||||
template_factory: nil
|
||||
)
|
||||
file_system = StubFileSystem.new(partials || {})
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, environment: environment)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
|
||||
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
|
||||
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
|
||||
output = template.render(context)
|
||||
assert_equal(expected, output, message)
|
||||
end
|
||||
|
||||
def assert_match_syntax_error(match, template)
|
||||
def assert_match_syntax_error(match, template, error_mode: nil)
|
||||
exception = assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse(template, line_numbers: true).render
|
||||
Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym).render
|
||||
end
|
||||
assert_match(match, exception.message)
|
||||
end
|
||||
|
||||
def assert_syntax_error(template)
|
||||
assert_match_syntax_error("", template)
|
||||
def assert_syntax_error(template, error_mode: nil)
|
||||
assert_match_syntax_error("", template, error_mode: error_mode)
|
||||
end
|
||||
|
||||
def assert_usage_increment(name, times: 1)
|
||||
@@ -75,6 +91,16 @@ module Minitest
|
||||
Environment.dangerously_override(environment, &blk)
|
||||
end
|
||||
|
||||
def with_error_modes(*modes)
|
||||
old_mode = Liquid::Environment.default.error_mode
|
||||
modes.each do |mode|
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
yield
|
||||
end
|
||||
ensure
|
||||
Liquid::Environment.default.error_mode = old_mode
|
||||
end
|
||||
|
||||
def with_custom_tag(tag_name, tag_class, &block)
|
||||
environment = Liquid::Environment.default.dup
|
||||
environment.register_tag(tag_name, tag_class)
|
||||
|
||||
@@ -82,7 +82,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
def test_contains_works_on_arrays
|
||||
@context = Liquid::Context.new
|
||||
@context['array'] = [1, 2, 3, 4, 5]
|
||||
array_expr = VariableLookup.parse("array")
|
||||
array_expr = VariableLookup.new("array")
|
||||
|
||||
assert_evaluates_false(array_expr, 'contains', 0)
|
||||
assert_evaluates_true(array_expr, 'contains', 1)
|
||||
@@ -96,8 +96,8 @@ class ConditionUnitTest < Minitest::Test
|
||||
|
||||
def test_contains_returns_false_for_nil_operands
|
||||
@context = Liquid::Context.new
|
||||
assert_evaluates_false(VariableLookup.parse('not_assigned'), 'contains', '0')
|
||||
assert_evaluates_false(0, 'contains', VariableLookup.parse('not_assigned'))
|
||||
assert_evaluates_false(VariableLookup.new('not_assigned'), 'contains', '0')
|
||||
assert_evaluates_false(0, 'contains', VariableLookup.new('not_assigned'))
|
||||
end
|
||||
|
||||
def test_contains_return_false_on_wrong_data_type
|
||||
@@ -149,7 +149,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context = Liquid::Context.new
|
||||
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse("one"), '==', VariableLookup.parse("another"))
|
||||
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
|
||||
end
|
||||
|
||||
def test_default_context_is_deprecated
|
||||
@@ -166,24 +166,35 @@ class ConditionUnitTest < Minitest::Test
|
||||
assert_includes(err.lines.map(&:strip), expected)
|
||||
end
|
||||
|
||||
def test_parse_expression
|
||||
environment = Environment.build
|
||||
def test_parse_expression_in_strict_mode
|
||||
environment = Environment.build(error_mode: :strict)
|
||||
parse_context = ParseContext.new(environment: environment)
|
||||
parser = parse_context.new_parser('product.title')
|
||||
result = Condition.parse_expression(parser)
|
||||
result = Condition.parse_expression(parse_context, 'product.title')
|
||||
|
||||
assert_instance_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_parse_expression_returns_method_literal_for_blank_and_empty
|
||||
environment = Environment.build
|
||||
def test_parse_expression_in_strict2_mode_raises_internal_error
|
||||
environment = Environment.build(error_mode: :strict2)
|
||||
parse_context = ParseContext.new(environment: environment)
|
||||
parser = parse_context.new_parser('blank')
|
||||
result = Condition.parse_expression(parser)
|
||||
|
||||
assert_instance_of(Condition::MethodLiteral, result)
|
||||
error = assert_raises(Liquid::InternalError) do
|
||||
Condition.parse_expression(parse_context, 'product.title')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_safe_true_in_strict2_mode
|
||||
environment = Environment.build(error_mode: :strict2)
|
||||
parse_context = ParseContext.new(environment: environment)
|
||||
result = Condition.parse_expression(parse_context, 'product.title', safe: true)
|
||||
|
||||
assert_instance_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
# Tests for blank? comparison without ActiveSupport
|
||||
@@ -205,7 +216,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['whitespace'] = ' '
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('whitespace'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('whitespace'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_string
|
||||
@@ -214,7 +225,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_string'] = ''
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_string'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_array
|
||||
@@ -223,7 +234,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_array'] = []
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_array'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_empty_hash
|
||||
@@ -232,7 +243,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_hash'] = {}
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_nil
|
||||
@@ -241,7 +252,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['nil_value'] = nil
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('nil_value'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('nil_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_blank_with_false
|
||||
@@ -250,7 +261,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['false_value'] = false
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('false_value'), '==', blank_literal)
|
||||
assert_evaluates_true(VariableLookup.new('false_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_true
|
||||
@@ -259,7 +270,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['true_value'] = true
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('true_value'), '==', blank_literal)
|
||||
assert_evaluates_false(VariableLookup.new('true_value'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_number
|
||||
@@ -268,7 +279,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['number'] = 42
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('number'), '==', blank_literal)
|
||||
assert_evaluates_false(VariableLookup.new('number'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_string_content
|
||||
@@ -277,7 +288,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['string'] = 'hello'
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('string'), '==', blank_literal)
|
||||
assert_evaluates_false(VariableLookup.new('string'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_non_empty_array
|
||||
@@ -286,7 +297,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['array'] = [1, 2, 3]
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('array'), '==', blank_literal)
|
||||
assert_evaluates_false(VariableLookup.new('array'), '==', blank_literal)
|
||||
end
|
||||
|
||||
def test_not_blank_with_non_empty_hash
|
||||
@@ -295,7 +306,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['hash'] = { 'a' => 1 }
|
||||
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('hash'), '==', blank_literal)
|
||||
assert_evaluates_false(VariableLookup.new('hash'), '==', blank_literal)
|
||||
end
|
||||
|
||||
# Tests for empty? comparison without ActiveSupport
|
||||
@@ -311,7 +322,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_string'] = ''
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', empty_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_string'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_whitespace_string_not_empty
|
||||
@@ -321,7 +332,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['whitespace'] = ' '
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('whitespace'), '==', empty_literal)
|
||||
assert_evaluates_false(VariableLookup.new('whitespace'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_empty_array
|
||||
@@ -330,7 +341,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_array'] = []
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', empty_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_array'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_empty_with_empty_hash
|
||||
@@ -339,7 +350,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['empty_hash'] = {}
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', empty_literal)
|
||||
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal)
|
||||
end
|
||||
|
||||
def test_nil_is_not_empty
|
||||
@@ -349,7 +360,7 @@ class ConditionUnitTest < Minitest::Test
|
||||
@context['nil_value'] = nil
|
||||
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
|
||||
|
||||
assert_evaluates_false(VariableLookup.parse('nil_value'), '==', empty_literal)
|
||||
assert_evaluates_false(VariableLookup.new('nil_value'), '==', empty_literal)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,56 +5,119 @@ require 'test_helper'
|
||||
class ParseContextUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_parser_expression_with_variable_lookup
|
||||
parser = parse_context.new_parser('product.title')
|
||||
result = parser.expression
|
||||
def test_safe_parse_expression_with_variable_lookup
|
||||
parser_strict = strict_parse_context.new_parser('product.title')
|
||||
result_strict = strict_parse_context.safe_parse_expression(parser_strict)
|
||||
|
||||
assert_instance_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
parser_strict2 = strict2_parse_context.new_parser('product.title')
|
||||
result_strict2 = strict2_parse_context.safe_parse_expression(parser_strict2)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict)
|
||||
assert_equal('product', result_strict.name)
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict2)
|
||||
assert_equal('product', result_strict2.name)
|
||||
assert_equal(['title'], result_strict2.lookups)
|
||||
end
|
||||
|
||||
def test_parser_expression_raises_syntax_error_for_invalid_expression
|
||||
parser = parse_context.new_parser('')
|
||||
def test_safe_parse_expression_raises_syntax_error_for_invalid_expression
|
||||
parser_strict = strict_parse_context.new_parser('')
|
||||
parser_strict2 = strict2_parse_context.new_parser('')
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) do
|
||||
parser.expression
|
||||
error_strict = assert_raises(Liquid::SyntaxError) do
|
||||
strict_parse_context.safe_parse_expression(parser_strict)
|
||||
end
|
||||
assert_match(/is not a valid expression/, error_strict.message)
|
||||
|
||||
error_strict2 = assert_raises(Liquid::SyntaxError) do
|
||||
strict2_parse_context.safe_parse_expression(parser_strict2)
|
||||
end
|
||||
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
assert_match(/is not a valid expression/, error_strict2.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_variable_lookup
|
||||
result = parse_context.new_parser('product.title').expression
|
||||
result_strict = strict_parse_context.parse_expression('product.title')
|
||||
|
||||
assert_instance_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
assert_instance_of(VariableLookup, result_strict)
|
||||
assert_equal('product', result_strict.name)
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
error = assert_raises(Liquid::InternalError) do
|
||||
strict2_parse_context.parse_expression('product.title')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parser_expression_advances_parser_pointer
|
||||
parser = parse_context.new_parser('foo, bar')
|
||||
def test_parse_expression_with_safe_true
|
||||
result_strict = strict_parse_context.parse_expression('product.title', safe: true)
|
||||
|
||||
# parser.expression consumes "foo"
|
||||
first_result = parser.expression
|
||||
assert_instance_of(VariableLookup, result_strict)
|
||||
assert_equal('product', result_strict.name)
|
||||
assert_equal(['title'], result_strict.lookups)
|
||||
|
||||
result_strict2 = strict2_parse_context.parse_expression('product.title', safe: true)
|
||||
|
||||
assert_instance_of(VariableLookup, result_strict2)
|
||||
assert_equal('product', result_strict2.name)
|
||||
assert_equal(['title'], result_strict2.lookups)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_empty_string
|
||||
result_strict = strict_parse_context.parse_expression('')
|
||||
assert_nil(result_strict)
|
||||
|
||||
error = assert_raises(Liquid::InternalError) do
|
||||
strict2_parse_context.parse_expression('')
|
||||
end
|
||||
|
||||
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_empty_string_and_safe_true
|
||||
result_strict = strict_parse_context.parse_expression('', safe: true)
|
||||
assert_nil(result_strict)
|
||||
|
||||
result_strict2 = strict2_parse_context.parse_expression('', safe: true)
|
||||
assert_nil(result_strict2)
|
||||
end
|
||||
|
||||
def test_safe_parse_expression_advances_parser_pointer
|
||||
parser = strict2_parse_context.new_parser('foo, bar')
|
||||
|
||||
# safe_parse_expression consumes "foo"
|
||||
first_result = strict2_parse_context.safe_parse_expression(parser)
|
||||
assert_instance_of(VariableLookup, first_result)
|
||||
assert_equal('foo', first_result.name)
|
||||
|
||||
parser.consume(:comma)
|
||||
|
||||
# parser.expression consumes "bar"
|
||||
second_result = parser.expression
|
||||
# safe_parse_expression consumes "bar"
|
||||
second_result = strict2_parse_context.safe_parse_expression(parser)
|
||||
assert_instance_of(VariableLookup, second_result)
|
||||
assert_equal('bar', second_result.name)
|
||||
|
||||
parser.consume(:end_of_string)
|
||||
end
|
||||
|
||||
def test_parse_expression_with_whitespace_in_strict2_mode
|
||||
result = strict2_parse_context.parse_expression(' ', safe: true)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_context
|
||||
@parse_context ||= ParseContext.new(
|
||||
environment: Environment.build,
|
||||
def strict_parse_context
|
||||
@strict_parse_context ||= ParseContext.new(
|
||||
environment: Environment.build(error_mode: :strict),
|
||||
)
|
||||
end
|
||||
|
||||
def strict2_parse_context
|
||||
@strict2_parse_context ||= ParseContext.new(
|
||||
environment: Environment.build(error_mode: :strict2),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,93 +45,40 @@ class ParserUnitTest < Minitest::Test
|
||||
assert_equal(false, p.look(:number, 1))
|
||||
end
|
||||
|
||||
def test_expression_string
|
||||
def test_expressions
|
||||
p = new_parser("hi.there hi?[5].there? hi.there.bob")
|
||||
assert_equal('hi.there', p.expression_string)
|
||||
assert_equal('hi?[5].there?', p.expression_string)
|
||||
assert_equal('hi.there.bob', p.expression_string)
|
||||
assert_equal('hi.there', p.expression)
|
||||
assert_equal('hi?[5].there?', p.expression)
|
||||
assert_equal('hi.there.bob', p.expression)
|
||||
|
||||
p = new_parser("567 6.0 'lol' \"wut\"")
|
||||
assert_equal('567', p.expression_string)
|
||||
assert_equal('6.0', p.expression_string)
|
||||
assert_equal("'lol'", p.expression_string)
|
||||
assert_equal('"wut"', p.expression_string)
|
||||
end
|
||||
|
||||
def test_expression
|
||||
p = new_parser("hi.there hi?[5].there? hi.there.bob")
|
||||
v1 = p.expression
|
||||
v2 = p.expression
|
||||
v3 = p.expression
|
||||
assert(v1.is_a?(VariableLookup) && v1.name == 'hi' && v1.lookups[0] == 'there')
|
||||
assert(v2.is_a?(VariableLookup) && v2.name == 'hi?' && v2.lookups[0] == 5)
|
||||
assert(v3.is_a?(VariableLookup) && v3.name == 'hi' && v3.lookups[0] == 'there')
|
||||
|
||||
p = new_parser("567 6.0 'lol' \"wut\" true false (0..5)")
|
||||
assert_equal(567, p.expression)
|
||||
assert_equal(6.0, p.expression)
|
||||
assert_equal('lol', p.expression)
|
||||
assert_equal('wut', p.expression)
|
||||
assert_equal(true, p.expression)
|
||||
assert_equal(false, p.expression)
|
||||
assert_equal(0..5, p.expression)
|
||||
end
|
||||
|
||||
def test_number
|
||||
p = new_parser('-1 0 1 2.0')
|
||||
assert_equal(-1, p.number)
|
||||
assert_equal(0, p.number)
|
||||
assert_equal(1, p.number)
|
||||
assert_equal(2.0, p.number)
|
||||
end
|
||||
|
||||
def test_string
|
||||
p = new_parser("'s1' \"s2\" 'this \"s3\"' \"that 's4'\"")
|
||||
assert_equal('s1', p.string)
|
||||
assert_equal('s2', p.string)
|
||||
assert_equal('this "s3"', p.string)
|
||||
assert_equal("that 's4'", p.string)
|
||||
end
|
||||
|
||||
def test_unnamed_variable_lookup
|
||||
p = new_parser('[key].title')
|
||||
v = p.expression
|
||||
assert(v.is_a?(VariableLookup))
|
||||
assert(v.name.is_a?(VariableLookup))
|
||||
assert_equal('key', v.name.name)
|
||||
assert_equal('title', v.lookups[0])
|
||||
end
|
||||
|
||||
def test_range_lookup
|
||||
p = new_parser('(0..5) (a..b)')
|
||||
assert_equal(0..5, p.expression)
|
||||
|
||||
r2 = p.expression
|
||||
assert(r2.is_a?(RangeLookup))
|
||||
assert_equal(1..4, r2.evaluate(Context.new({ 'a' => 1, 'b' => 4 })))
|
||||
assert_equal('567', p.expression)
|
||||
assert_equal('6.0', p.expression)
|
||||
assert_equal("'lol'", p.expression)
|
||||
assert_equal('"wut"', p.expression)
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
|
||||
assert_equal('(5..7)', p.expression_string)
|
||||
assert_equal('(1.5..9.6)', p.expression_string)
|
||||
assert_equal('(young..old)', p.expression_string)
|
||||
assert_equal('(hi[5].wat..old)', p.expression_string)
|
||||
assert_equal('(5..7)', p.expression)
|
||||
assert_equal('(1.5..9.6)', p.expression)
|
||||
assert_equal('(young..old)', p.expression)
|
||||
assert_equal('(hi[5].wat..old)', p.expression)
|
||||
end
|
||||
|
||||
def test_argument_string
|
||||
def test_arguments
|
||||
p = new_parser("filter: hi.there[5], keyarg: 7")
|
||||
assert_equal('filter', p.consume(:id))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
assert_equal('hi.there[5]', p.argument_string)
|
||||
assert_equal('hi.there[5]', p.argument)
|
||||
assert_equal(',', p.consume(:comma))
|
||||
assert_equal('keyarg: 7', p.argument_string)
|
||||
assert_equal('keyarg: 7', p.argument)
|
||||
end
|
||||
|
||||
def test_invalid_expression
|
||||
assert_raises(SyntaxError) do
|
||||
p = new_parser("==")
|
||||
p.expression_string
|
||||
p.expression
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ class PartialCacheUnitTest < Minitest::Test
|
||||
assert_equal('some/path/my_partial', partial.name)
|
||||
end
|
||||
|
||||
def test_cache_key
|
||||
def test_includes_error_mode_into_template_cache
|
||||
template_factory = StubTemplateFactory.new
|
||||
context = Liquid::Context.build(
|
||||
registers: {
|
||||
@@ -184,14 +184,16 @@ class PartialCacheUnitTest < Minitest::Test
|
||||
},
|
||||
)
|
||||
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new,
|
||||
)
|
||||
[:lax, :warn, :strict, :strict2].each do |error_mode|
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new(error_mode: error_mode),
|
||||
)
|
||||
end
|
||||
|
||||
assert_equal(
|
||||
["my_partial"],
|
||||
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:strict2"],
|
||||
context.registers[:cached_partials].keys,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RangeSliceUnitTest < Minitest::Test
|
||||
def test_selects_and_reverses_an_integer_range_without_enumerating_it
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
slice = Liquid::RangeSlice.new(bounded_integer_range_with_tripwires, 997, 999, limits)
|
||||
|
||||
assert_equal(2, slice.length)
|
||||
refute(slice.empty?)
|
||||
slice.reverse!
|
||||
assert_equal([999, 998], slice.each.to_a)
|
||||
assert_equal(2, limits.render_score)
|
||||
end
|
||||
|
||||
def test_charges_only_values_yielded_before_a_break
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
slice = Liquid::RangeSlice.new(bounded_integer_range_with_tripwires, 0, nil, limits)
|
||||
|
||||
slice.each { break }
|
||||
|
||||
assert_equal(1, limits.render_score)
|
||||
end
|
||||
|
||||
def test_non_integer_ranges_are_sliced_without_to_a_and_charge_visited_values
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
range = Class.new(Range) do
|
||||
def to_a
|
||||
raise 'range was materialized'
|
||||
end
|
||||
end.new('a', 'c')
|
||||
|
||||
assert_equal(['b', 'c'], Liquid::Utils.slice_collection_for_iteration(range, 1, nil, limits))
|
||||
assert_equal(3, limits.render_score)
|
||||
|
||||
limited = Liquid::ResourceLimits.new(render_score_limit: 2)
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
Liquid::Utils.slice_collection_for_iteration('a'..'z', 10, 11, limited)
|
||||
end
|
||||
end
|
||||
|
||||
def test_non_integer_range_empty_windows_do_not_visit_a_sentinel_value
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
|
||||
assert_equal([], Liquid::Utils.slice_collection_for_iteration('a'..'z', 2, 2, limits))
|
||||
assert_equal(0, limits.render_score)
|
||||
assert_equal(['a', 'b'], Liquid::Utils.slice_collection_for_iteration('a'..'z', 0, 2, limits))
|
||||
assert_equal(2, limits.render_score)
|
||||
end
|
||||
|
||||
def test_standard_beginless_range_raises_for_empty_windows
|
||||
[[0, 0], [1, 0]].each do |from, to|
|
||||
assert_raises(TypeError) do
|
||||
Liquid::Utils.slice_collection_for_iteration(Range.new(nil, 3), from, to, Liquid::ResourceLimits.new({}))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_range_to_a_is_sliced_with_a_budget
|
||||
range = Class.new(Range) do
|
||||
def to_a
|
||||
[1, 2, 3]
|
||||
end
|
||||
end.new(nil, 3)
|
||||
limits = Liquid::ResourceLimits.new(render_score_limit: 2)
|
||||
|
||||
assert_raises(Liquid::MemoryError) do
|
||||
Liquid::Utils.slice_collection_for_iteration(range, 1, 3, limits, use_range_to_a: true)
|
||||
end
|
||||
assert_equal(3, limits.render_score)
|
||||
end
|
||||
|
||||
def test_preserves_slice_bounds_for_negative_offsets_and_limits
|
||||
limits = Liquid::ResourceLimits.new({})
|
||||
|
||||
assert_equal([1, 2], Liquid::RangeSlice.new(1..5, -2, 2, limits).each.to_a)
|
||||
empty = Liquid::RangeSlice.new(1..5, 2, 1, limits)
|
||||
assert(empty.empty?)
|
||||
assert_equal([], empty.each.to_a)
|
||||
assert_equal(2, limits.render_score) # the empty window performs no work
|
||||
assert(Liquid::RangeSlice.new(5..1, 0, nil, limits).empty?)
|
||||
assert_equal([1, 2, 3, 4], Liquid::RangeSlice.new(1...5, 0, nil, limits).each.to_a)
|
||||
end
|
||||
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
|
||||
@@ -20,9 +20,15 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_with_trailing_element
|
||||
@@ -35,9 +41,15 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Expected end_of_string but found/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_with_comma
|
||||
@@ -50,7 +62,9 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("one", template)
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_with_or
|
||||
@@ -63,7 +77,9 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("one", template)
|
||||
with_error_modes(:lax, :strict, :strict2) do
|
||||
assert_template_result("one", template)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_empty
|
||||
@@ -76,12 +92,14 @@ class CaseTagUnitTest < Minitest::Test
|
||||
{%- endcase -%}
|
||||
LIQUID
|
||||
|
||||
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 })
|
||||
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
|
||||
@@ -95,9 +113,15 @@ class CaseTagUnitTest < Minitest::Test
|
||||
LIQUID
|
||||
assigns = { 'foo' => { 'bar' => 'baz' } }
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("one", template, assigns)
|
||||
end
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_when_with_invalid_expression
|
||||
@@ -111,8 +135,14 @@ class CaseTagUnitTest < Minitest::Test
|
||||
LIQUID
|
||||
assigns = { 'foo' => { 'bar' => 'baz' } }
|
||||
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
|
||||
with_error_modes(:lax, :strict) do
|
||||
assert_template_result("one", template, assigns)
|
||||
end
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
with_error_modes(:strict2) do
|
||||
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
|
||||
|
||||
assert_match(/Unexpected character =/, error.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,20 +7,20 @@ class VariableUnitTest < Minitest::Test
|
||||
|
||||
def test_variable
|
||||
var = create_variable('hello')
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
end
|
||||
|
||||
def test_filters
|
||||
var = create_variable('hello | textileze')
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['textileze', []]], var.filters)
|
||||
|
||||
var = create_variable('hello | textileze | paragraph')
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
|
||||
|
||||
var = create_variable(%( hello | strftime: '%Y'))
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['strftime', ['%Y']]], var.filters)
|
||||
|
||||
var = create_variable(%( 'typo' | link_to: 'Typo', true ))
|
||||
@@ -44,11 +44,11 @@ class VariableUnitTest < Minitest::Test
|
||||
assert_equal([['repeat', [3, 3, 3]]], var.filters)
|
||||
|
||||
var = create_variable(%( hello | strftime: '%Y, okay?'))
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['strftime', ['%Y, okay?']]], var.filters)
|
||||
|
||||
var = create_variable(%( hello | things: "%Y, okay?", 'the other one'))
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters)
|
||||
end
|
||||
|
||||
@@ -60,18 +60,24 @@ class VariableUnitTest < Minitest::Test
|
||||
|
||||
def test_filters_without_whitespace
|
||||
var = create_variable('hello | textileze | paragraph')
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
|
||||
|
||||
var = create_variable('hello|textileze|paragraph')
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
|
||||
|
||||
var = create_variable("hello|replace:'foo','bar'|textileze")
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters)
|
||||
end
|
||||
|
||||
def test_symbol
|
||||
var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax)
|
||||
assert_equal(VariableLookup.new('http://disney.com/logo.gif'), var.name)
|
||||
assert_equal([['image', ['med']]], var.filters)
|
||||
end
|
||||
|
||||
def test_string_to_filter
|
||||
var = create_variable("'http://disney.com/logo.gif' | image: 'med' ")
|
||||
assert_equal('http://disney.com/logo.gif', var.name)
|
||||
@@ -99,12 +105,14 @@ class VariableUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_dashes
|
||||
assert_equal(VariableLookup.parse('foo-bar'), create_variable('foo-bar').name)
|
||||
assert_equal(VariableLookup.parse('foo-bar-2'), create_variable('foo-bar-2').name)
|
||||
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
|
||||
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
|
||||
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
|
||||
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
|
||||
end
|
||||
end
|
||||
|
||||
def test_string_with_special_chars
|
||||
@@ -114,47 +122,79 @@ class VariableUnitTest < Minitest::Test
|
||||
|
||||
def test_string_dot
|
||||
var = create_variable(%( test.test ))
|
||||
assert_equal(VariableLookup.parse('test.test'), var.name)
|
||||
assert_equal(VariableLookup.new('test.test'), var.name)
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye'))
|
||||
assert_equal(VariableLookup.parse('hello'), var.name)
|
||||
assert_equal(VariableLookup.new('hello'), var.name)
|
||||
assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters)
|
||||
end
|
||||
|
||||
def test_filter_argument_parsing
|
||||
# optional colon
|
||||
var = create_variable(%(n | f1 | f2:))
|
||||
assert_equal([['f1', []], ['f2', []]], var.filters)
|
||||
def test_lax_filter_argument_parsing
|
||||
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
|
||||
assert_equal(VariableLookup.new('number_of_comments'), var.name)
|
||||
assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
|
||||
|
||||
# missing argument throws error
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2)) }
|
||||
# missing does not throws error
|
||||
create_variable(%(n | f1: ,), error_mode: :lax)
|
||||
create_variable(%(n | f1: ,| f2), error_mode: :lax)
|
||||
|
||||
# arg requires colon
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
|
||||
|
||||
# trailing comma doesn't throw
|
||||
create_variable(%(n | f1: 1, 2, 3, | f2:))
|
||||
|
||||
# missing comma throws error
|
||||
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3)) }
|
||||
# arg does not require colon, but ignores args :O, also ignores first kwarg since it splits on ':'
|
||||
var = create_variable(%(n | f1 1 | f2 k1: v1), error_mode: :lax)
|
||||
assert_equal([['f1', []], ['f2', [VariableLookup.new('v1')]]], var.filters)
|
||||
|
||||
# positional and kwargs parsing
|
||||
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2))
|
||||
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2), error_mode: :lax)
|
||||
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
|
||||
|
||||
# positional and kwargs mixed
|
||||
var = create_variable(%(n | filter: 'a', 'b', key1: 1, key2: 2, 'c'))
|
||||
assert_equal([["filter", ["a", "b", "c"], { "key1" => 1, "key2" => 2 }]], var.filters)
|
||||
|
||||
# positional and kwargs intermixed (pos1, key1: val1, pos2)
|
||||
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"))
|
||||
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"), error_mode: :lax)
|
||||
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
|
||||
end
|
||||
|
||||
# string key throws
|
||||
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
|
||||
def test_strict_filter_argument_parsing
|
||||
with_error_modes(:strict) do
|
||||
assert_raises(SyntaxError) do
|
||||
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_strict2_filter_argument_parsing
|
||||
with_error_modes(:strict2) do
|
||||
# optional colon
|
||||
var = create_variable(%(n | f1 | f2:))
|
||||
assert_equal([['f1', []], ['f2', []]], var.filters)
|
||||
|
||||
# missing argument throws error
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2)) }
|
||||
|
||||
# arg requires colon
|
||||
assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
|
||||
|
||||
# trailing comma doesn't throw
|
||||
create_variable(%(n | f1: 1, 2, 3, | f2:))
|
||||
|
||||
# missing comma throws error
|
||||
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3)) }
|
||||
|
||||
# positional and kwargs parsing
|
||||
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2))
|
||||
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
|
||||
|
||||
# positional and kwargs mixed
|
||||
var = create_variable(%(n | filter: 'a', 'b', key1: 1, key2: 2, 'c'))
|
||||
assert_equal([["filter", ["a", "b", "c"], { "key1" => 1, "key2" => 2 }]], var.filters)
|
||||
|
||||
# positional and kwargs intermixed (pos1, key1: val1, pos2)
|
||||
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"))
|
||||
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
|
||||
|
||||
# string key throws
|
||||
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_output_raw_source_of_variable
|
||||
@@ -163,7 +203,7 @@ class VariableUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_variable_lookup_interface
|
||||
lookup = VariableLookup.parse('a.b.c')
|
||||
lookup = VariableLookup.new('a.b.c')
|
||||
assert_equal('a', lookup.name)
|
||||
assert_equal(['b', 'c'], lookup.lookups)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user