Compare commits

..

3 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
0e25e5d5e8 seperator -> separator 2015-07-09 17:03:42 -04:00
Dylan Thacker-Smith
77bdccf5ec Try to make the regexes more readable. 2015-07-09 16:14:36 -04:00
Dylan Thacker-Smith
87e3234b58 Add a lax warning mode to see where we can make the lax parser stricter. 2015-07-09 16:14:32 -04:00
63 changed files with 485 additions and 1781 deletions

View File

@@ -118,8 +118,6 @@ Style/ClassVars:
Style/PerlBackrefs: Style/PerlBackrefs:
Enabled: false Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray: Style/WordArray:
Enabled: false Enabled: false

View File

@@ -1,28 +1,19 @@
language: ruby language: ruby
rvm: rvm:
- 2.0
- 2.1 - 2.1
- 2.2 - 2.2
- 2.3.3
- ruby-head - ruby-head
- jruby-head - jruby-head
# - rbx-2 # - rbx-2
sudo: false sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
allow_failures: allow_failures:
- rvm: jruby-head - rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: "bundle exec rake" script: "bundle exec rake"
notifications: notifications:

View File

@@ -4,22 +4,23 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * Features which are likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
* Code that introduces considerable performance degrations * Code which introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks * Code which touches performance critical parts of Liquid and comes without benchmarks
* Features that are not important for most people (we want to keep the core Liquid code small and tidy) * Features which are not important for most people (we want to keep the core Liquid code small and tidy)
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) * Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests * Code which comes without tests
* Code that breaks existing tests * Code which breaks existing tests
## Workflow ## Workflow
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * Create a new branch in your fork
* If it makes sense, add tests for your code and/or run a performance benchmark * If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass
* Create a pull request * Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

13
Gemfile
View File

@@ -1,17 +1,14 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
end
group :test do group :test do
gem 'rubocop', '0.34.2' gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop', '>=0.32.0'
platform :mri do platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd' gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
end end
end end

View File

@@ -1,16 +1,8 @@
# Liquid Change Log # Liquid Change Log
## 4.0.0 / 2016-12-14 / branch "4-0-stable" ## 4.0.0 / not yet released / branch "master"
### Changed ### Changed
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
* Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke] * Add compact filter (#600) [Carson Reinke]
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten] * Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith] * Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
@@ -20,18 +12,9 @@
* Add concat filter to concatenate arrays (#429) [Diogo Beato] * Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li] * Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith] * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement) * Remove support for `liquid_methods`
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed ### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten] * Fix bug in uniq filter (#595) [Florian Weingarten]
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten] * Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
* Fix condition parse order in strict mode (#569) [Justin Li] * Fix condition parse order in strict mode (#569) [Justin Li]
@@ -43,15 +26,7 @@
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li] * Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li] * Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
## 3.0.5 / 2015-07-23 / branch "3-0-stable" ## 3.0.3 / 2015-05-28 / branch "3-0-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 3.0.4 / 2015-07-17
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
## 3.0.3 / 2015-05-28
* Fix condition parse order in strict mode (#569) [Justin Li] * Fix condition parse order in strict mode (#569) [Justin Li]
@@ -99,15 +74,7 @@
* Make map filter work on enumerable drops (#233) [Florian Weingarten] * Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten] * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.3 / 2015-07-23 / branch "2-6-stable" ## 2.6.1 / 2014-01-10 / branch "2-6-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 2.6.2 / 2015-01-23
* Remove duplicate hash key [Parker Moore]
## 2.6.1 / 2014-01-10
Security fix, cherry-picked from master (4e14a65): Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]

View File

@@ -73,34 +73,3 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created. 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. 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.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```

View File

@@ -1,3 +1,3 @@
machine: machine:
ruby: ruby:
version: ruby-2.1 version: ruby-2.0

View File

@@ -24,7 +24,6 @@ module Liquid
ArgumentSeparator = ','.freeze ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-'.freeze
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -35,7 +34,7 @@ module Liquid
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
@@ -49,8 +48,6 @@ require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
require 'liquid/drop' require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'

View File

@@ -1,7 +1,5 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blank = true @blank = true
@@ -50,25 +48,17 @@ module Liquid
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
raise StackLevelError, "Nesting too deep".freeze @blank &&= body.blank?
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
end end
ensure
parse_context.depth -= 1 # this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
end end
true true

View File

@@ -1,7 +1,7 @@
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze TAGSTART = "{%".freeze
VARSTART = "{{".freeze VARSTART = "{{".freeze
@@ -18,7 +18,6 @@ module Liquid
unless token.empty? unless token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
if token =~ FullToken if token =~ FullToken
tag_name = $1 tag_name = $1
markup = $2 markup = $2
@@ -36,14 +35,9 @@ module Liquid
raise_missing_tag_terminator(token, parse_context) raise_missing_tag_terminator(token, parse_context)
end end
when token.start_with?(VARSTART) when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context) @nodelist << create_variable(token, parse_context)
@blank = false @blank = false
else else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= !!(token =~ /\A\s*\z/) @blank &&= !!(token =~ /\A\s*\z/)
end end
@@ -54,16 +48,6 @@ module Liquid
yield nil, nil yield nil, nil
end end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a? String
previous_token.rstrip!
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank? def blank?
@blank @blank
end end
@@ -92,12 +76,8 @@ module Liquid
end end
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number output << context.handle_error(e, token.line_number)
output << context.handle_error(e, line_number)
end end
end end
@@ -107,7 +87,7 @@ module Liquid
private private
def render_node(node, context) def render_node(node, context)
node_output = node.is_a?(String) ? node : node.render(context) node_output = (node.respond_to?(:render) ? node.render(context) : node)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += node_output.length context.resource_limits.render_length += node_output.length

View File

@@ -41,22 +41,16 @@ module Liquid
end end
def evaluate(context = Context.new) def evaluate(context = Context.new)
condition = self result = interpret_condition(left, right, operator, context)
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case condition.child_relation case @child_relation
when :or when :or
break if result result || @child_condition.evaluate(context)
when :and when :and
break unless result result && @child_condition.evaluate(context)
else else
break result
end
condition = condition.child_condition
end end
result
end end
def or(condition) def or(condition)
@@ -81,10 +75,6 @@ module Liquid
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
protected
attr_reader :child_relation, :child_condition
private private
def equal_variables(left, right) def equal_variables(left, right)
@@ -120,7 +110,7 @@ module Liquid
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash) elsif left.respond_to?(operation) && right.respond_to?(operation)
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e

View File

@@ -13,7 +13,7 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_handler, :template_name, :partial
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten @environments = [environments].flatten
@@ -21,20 +21,17 @@ module Liquid
@registers = registers @registers = registers
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = ->(e) { raise } self.exception_handler = ->(e) { raise }
end end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil
end end
def warnings def warnings
@@ -55,10 +52,6 @@ module Liquid
@strainer = nil @strainer = nil
end end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts? # are there any not handled interrupts?
def interrupt? def interrupt?
!@interrupts.empty? !@interrupts.empty?
@@ -75,11 +68,30 @@ module Liquid
end end
def handle_error(e, line_number = nil) def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) if e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
end
output = nil
if exception_handler
result = exception_handler.call(e)
case result
when Exception
e = result
if e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
end
when String
output = result
else
raise if result
end
end
errors.push(e) errors.push(e)
exception_renderer.call(e).to_s output || Liquid::Error.render(e)
end end
def invoke(method, *args) def invoke(method, *args)
@@ -89,7 +101,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -160,7 +172,7 @@ module Liquid
end end
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key, raise_on_not_found: true) def find_variable(key)
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
@@ -170,7 +182,7 @@ module Liquid
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) variable = lookup_and_evaluate(e, key)
unless variable.nil? unless variable.nil?
scope = e scope = e
break break
@@ -179,7 +191,7 @@ module Liquid
end end
scope ||= @environments.last || @scopes.last scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found) variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
@@ -187,14 +199,8 @@ module Liquid
variable variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self) obj[key] = (value.arity == 0) ? value.call : value.call(self)
else else
value value
@@ -203,13 +209,6 @@ module Liquid
private private
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments def squash_instance_assigns_with_environments
@scopes.last.each_key do |k| @scopes.last.each_key do |k|
@environments.each do |env| @environments.each do |env|

View File

@@ -18,23 +18,24 @@ module Liquid
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
# #
# Your drop can either implement the methods sans any parameters # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# or implement the liquid_method_missing(name) method which is a catch all. # catch all.
class Drop class Drop
attr_writer :context attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def before_method(_method)
return nil unless @context && @context.strict_variables nil
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
# called by liquid to invoke a drop # called by liquid to invoke a drop
def invoke_drop(method_or_key) def invoke_drop(method_or_key)
if self.class.invokable?(method_or_key) if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
send(method_or_key) send(method_or_key)
else else
liquid_method_missing(method_or_key) before_method(method_or_key)
end end
end end
@@ -62,17 +63,16 @@ module Liquid
end end
def self.invokable_methods def self.invokable_methods
@invokable_methods ||= begin unless @invokable_methods
blacklist = Liquid::Drop.public_instance_methods + [:each] blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= [:sort, :count, :first, :min, :max, :include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
Set.new(whitelist.map(&:to_s)) @invokable_methods = Set.new(whitelist.map(&:to_s))
end end
@invokable_methods
end end
end end
end end

View File

@@ -17,6 +17,14 @@ module Liquid
str str
end end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e}"
end
end
private private
def message_prefix def message_prefix
@@ -48,9 +56,4 @@ module Liquid
MemoryError = Class.new(Error) MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error) ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error) FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
InternalError = Class.new(Error)
end end

View File

@@ -7,50 +7,44 @@ class String # :nodoc:
end end
end end
class Array # :nodoc: class Array # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Hash # :nodoc: class Hash # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Numeric # :nodoc: class Numeric # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Range # :nodoc: class Time # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class Time # :nodoc: class DateTime < Date # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class DateTime < Date # :nodoc: class Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid def to_liquid
self self
end end
end end
class TrueClass class TrueClass
def to_liquid # :nodoc: def to_liquid # :nodoc:
self self
end end
end end

View File

@@ -8,8 +8,8 @@ module Liquid
# #
# Example: # Example:
# #
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template) # liquid = Liquid::Template.parse(template)
# #
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem class BlankFileSystem
@@ -26,10 +26,10 @@ module Liquid
# #
# Example: # Example:
# #
# file_system = Liquid::LocalFileSystem.new("/some/path") # file_system = Liquid::LocalFileSystem.new("/some/path")
# #
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
# #
# Optionally in the second argument you can specify a custom pattern for template filenames. # Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used. # The Kernel::sprintf format specification is used.
@@ -37,9 +37,9 @@ module Liquid
# #
# Example: # Example:
# #
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
# #
# file_system.full_path("index") # => "/some/path/index.html" # file_system.full_path("index") # => "/some/path/index.html"
# #
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)
end end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root)) raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
full_path full_path
end end

View File

@@ -1,42 +0,0 @@
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :name, :length, :parentloop
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -18,10 +18,10 @@ module Liquid
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input.rstrip)
end end
def tokenize def tokenize
@@ -29,7 +29,6 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(/\s*/) @ss.skip(/\s*/)
break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]

View File

@@ -22,5 +22,3 @@
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}" meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
argument:
include: "Argument error in tag 'include' - Illegal template name"

View File

@@ -1,13 +1,12 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :partial, :locale, :line_number
attr_reader :partial, :warnings, :error_mode attr_reader :warnings, :error_mode
def initialize(options = {}) def initialize(options = {})
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
self.depth = 0
self.partial = false self.partial = false
end end

View File

@@ -75,7 +75,7 @@ module Liquid
def variable_signature def variable_signature
str = consume(:id) str = consume(:id)
while look(:open_square) if look(:open_square)
str << consume str << consume
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)

View File

@@ -3,7 +3,7 @@ module Liquid
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case parse_context.error_mode case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax, :lax_warn then lax_parse(markup)
when :warn when :warn
begin begin
return strict_parse_with_error_context(markup) return strict_parse_with_error_context(markup)

View File

@@ -33,7 +33,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil? CGI.escapeHTML(input).untaint
end end
alias_method :h, :escape alias_method :h, :escape
@@ -42,11 +42,7 @@ module Liquid
end end
def url_encode(input) def url_encode(input)
CGI.escape(input.to_s) unless input.nil? CGI.escape(input) rescue input
end
def url_decode(input)
CGI.unescape(input.to_s) unless input.nil?
end end
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
@@ -63,12 +59,10 @@ module Liquid
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze) def truncate(input, length = 50, truncate_string = "...".freeze)
return if input.nil? return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s l = length - truncate_string.length
l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str input.length > length ? input[0...l] + truncate_string : input
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -77,7 +71,7 @@ module Liquid
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -86,7 +80,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div> # <div class="summary">{{ post | split '//' | first }}</div>
# #
def split(input, pattern) def split(input, pattern)
input.to_s.split(pattern.to_s) input.to_s.split(pattern)
end end
def strip(input) def strip(input)
@@ -122,18 +116,10 @@ module Liquid
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.sort ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b| ary.sort { |a, b| a[property] <=> b[property] }
a = a[property] elsif ary.first.respond_to?(property)
b = b[property] ary.sort { |a, b| a.send(property) <=> b.send(property) }
if a && b
a <=> b
else
a ? -1 : 1
end
end
end end
end end
@@ -144,10 +130,10 @@ module Liquid
if property.nil? if property.nil?
ary.sort { |a, b| a.casecmp(b) } ary.sort { |a, b| a.casecmp(b) }
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property].casecmp(b[property]) } ary.sort { |a, b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
end end
end end
@@ -158,8 +144,6 @@ module Liquid
if property.nil? if property.nil?
ary.uniq ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.uniq{ |a| a[property] } ary.uniq{ |a| a[property] }
end end
@@ -179,8 +163,7 @@ module Liquid
if property == "to_liquid".freeze if property == "to_liquid".freeze
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] e[property]
r.is_a?(Proc) ? r.call : r
end end
end end
end end
@@ -192,10 +175,10 @@ module Liquid
if property.nil? if property.nil?
ary.compact ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? } ary.reject{ |a| a[property].nil? }
elsif ary.first.respond_to?(property)
ary.reject { |a| a.send(property).nil? }
end end
end end
@@ -225,9 +208,6 @@ module Liquid
end end
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array) InputIterator.new(input).concat(array)
end end
@@ -298,12 +278,6 @@ module Liquid
array.last if array.respond_to?(:last) array.last if array.respond_to?(:last)
end end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition # addition
def plus(input, operand) def plus(input, operand)
apply_operation(input, operand, :+) apply_operation(input, operand, :+)
@@ -353,12 +327,9 @@ module Liquid
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
def default(input, default_value = ''.freeze) def default(input, default_value = "".freeze)
if !input || input.respond_to?(:empty?) && input.empty? is_blank = input.respond_to?(:empty?) ? input.empty? : !input
default_value is_blank ? default_value : input
else
input
end
end end
private private
@@ -384,11 +355,11 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue.to_s) to_a.join(glue)
end end
def concat(args) def concat(args)
to_a.concat(args) to_a.concat args
end end
def reverse def reverse
@@ -403,11 +374,6 @@ module Liquid
to_a.compact to_a.compact
end end
def empty?
@input.each { return false }
true
end
def each def each
@input.each do |e| @input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)

View File

@@ -26,20 +26,14 @@ module Liquid
end end
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
unless self.include?(filter) unless self.class.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } send(:include, filter)
if invokable_non_public_methods.any? @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end end
end end
def self.global_filter(filter) def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter) @@global_strainer.add_filter(filter)
end end
@@ -54,13 +48,11 @@ module Liquid
def invoke(method, *args) def invoke(method, *args)
if self.class.invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first
end end
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace raise Liquid::ArgumentError.new(e.message)
end end
end end
end end

View File

@@ -1,62 +0,0 @@
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end

View File

@@ -23,28 +23,16 @@ module Liquid
def render(context) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.freeze ''.freeze
end end
def blank? def blank?
true true
end end
private
def assign_score_of(val)
if val.instance_of?(String)
val.length
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
else
1
end
end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign'.freeze, Assign)

View File

@@ -37,7 +37,7 @@ module Liquid
iteration = context.registers[:cycle][key] iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration]) result = context.evaluate(@variables[iteration])
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
result result
end end

View File

@@ -23,7 +23,7 @@ module Liquid
# {{ item.name }} # {{ item.name }}
# {% end %} # {% end %}
# #
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`) # To reverse the for loop simply use {% for item in collection reversed %}
# #
# == Available variables: # == Available variables:
# #
@@ -46,15 +46,10 @@ module Liquid
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = BlockBody.new @for_block = BlockBody.new
@else_block = nil
end end
def parse(tokens) def parse(tokens)
@@ -72,13 +67,69 @@ module Liquid
end end
def render(context) def render(context)
segment = collection_segment(context) for_offsets = context.registers[:for] ||= Hash.new(0)
for_stack = context.registers[:for_stack] ||= []
if segment.empty? parent_loop = for_stack.last
render_else(context) for_stack.push(nil)
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
from = if @from == :continue
for_offsets[@name].to_i
else else
render_segment(context, segment) context.evaluate(@from).to_i
end end
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
for_offsets[@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
loop_vars = {
'name'.freeze => @name,
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'parentloop'.freeze => parent_loop
}
context['forloop'.freeze] = loop_vars
for_stack[-1] = loop_vars
result << @for_block.render(context)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
ensure
for_stack.pop
end end
protected protected
@@ -101,7 +152,7 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
@@ -119,63 +170,6 @@ module Liquid
private private
def collection_segment(context)
offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue
offsets[@name].to_i
else
context.evaluate(@from).to_i
end
collection = context.evaluate(@collection_name)
collection = collection.step(1).to_a if collection.is_a?(Range)
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
result = ''
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'.freeze] = loop_vars
segment.each_with_index do |item, index|
context[@variable_name] = item
result << @for_block.render(context)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
ensure
for_stack.pop
end
end
result
end
def set_attribute(key, expr) def set_attribute(key, expr)
case key case key
when 'offset'.freeze when 'offset'.freeze

View File

@@ -83,20 +83,17 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
condition = parse_binary_comparisons(p) condition = parse_binary_comparison(p)
p.consume(:end_of_string) p.consume(:end_of_string)
condition condition
end end
def parse_binary_comparisons(p) def parse_binary_comparison(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) condition.send(op, parse_binary_comparison(p))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end end
first_condition condition
end end
def parse_comparison(p) def parse_comparison(p)

View File

@@ -42,15 +42,14 @@ module Liquid
def render(context) def render(context)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = load_cached_partial(template_name, context) partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
else else
context.find_variable(template_name, raise_on_not_found: false) context.find_variable(template_name)
end end
old_template_name = context.template_name old_template_name = context.template_name

View File

@@ -6,7 +6,9 @@ module Liquid
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
ensure_valid_markup(tag_name, markup, parse_context) unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end end
def parse(tokens) def parse(tokens)
@@ -33,14 +35,6 @@ module Liquid
def blank? def blank?
@body.empty? @body.empty?
end end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end
end end
Template.register_tag('raw'.freeze, Raw) Template.register_tag('raw'.freeze, Raw)

View File

@@ -28,21 +28,36 @@ module Liquid
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context.evaluate(@attributes['cols'.freeze]).to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n" result = "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each_with_index do |item, index| collection.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>' col += 1
if tablerowloop.col_last && !tablerowloop.last result << "<td class=\"col#{col}\">" << super << '</td>'
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
if col == cols && (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end end
tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n" result << "</tr>\n"

View File

@@ -19,10 +19,8 @@ module Liquid
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
@@ -43,10 +41,6 @@ module Liquid
@cache.delete(tag_name) @cache.delete(tag_name)
end end
def each(&block)
@tags.each(&block)
end
private private
def lookup_class(name) def lookup_class(name)
@@ -69,11 +63,6 @@ module Liquid
# :error raises an error when tainted output is used # :error raises an error when tainted output is used
attr_writer :taint_mode attr_writer :taint_mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system def file_system
@@file_system @@file_system
end end
@@ -91,11 +80,11 @@ module Liquid
end end
def error_mode def error_mode
@error_mode ||= :lax @error_mode || :lax
end end
def taint_mode def taint_mode
@taint_mode ||= :lax @taint_mode || :lax
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
@@ -118,7 +107,6 @@ module Liquid
end end
def initialize def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits) @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
@@ -172,7 +160,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(e) { raise } c.exception_handler = ->(e) { raise }
end end
c c
@@ -191,10 +179,20 @@ module Liquid
when Hash when Hash
options = args.pop options = args.pop
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) if options[:registers].is_a?(Hash)
registers.merge!(options[:registers])
end
apply_options_to_context(context, options) if options[:filters]
when Module, Array context.add_filters(options[:filters])
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop) context.add_filters(args.pop)
end end
@@ -242,13 +240,5 @@ module Liquid
yield yield
end end
end end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end end
end end

View File

@@ -4,7 +4,7 @@ module Liquid
def initialize(source, line_numbers = false) def initialize(source, line_numbers = false)
@source = source @source = source
@line_number = line_numbers ? 1 : nil @line_number = 1 if line_numbers
@tokens = tokenize @tokens = tokenize
end end

View File

@@ -50,13 +50,9 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) 0
obj.to_number
else
0
end
end end
end end
@@ -76,7 +72,7 @@ module Liquid
when String when String
Time.parse(obj) Time.parse(obj)
end end
rescue ::ArgumentError rescue ArgumentError
nil nil
end end
end end

View File

@@ -10,7 +10,16 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o capture_ignored_variable_prefix = /([\s,\|'"]+?)??/
capture_expression = /(#{QuotedFragment})/o
capture_ignored_filter_prefix = /([^\|]+?)??/
capture_filters = /(#{FilterSeparator}.*)/o
VariableSyntax = /\A\s*#{capture_ignored_variable_prefix}\s*#{capture_expression}\s*(?:#{capture_ignored_filter_prefix}\s*#{capture_filters})?\z/om
capture_lax_separator = /(['"\|]+?)/
capture_filter = /((?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+)/o
FilterParser = /\s*(?:#{FilterSeparator}|#{capture_lax_separator})\s*#{capture_filter}/o
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
attr_reader :parse_context attr_reader :parse_context
alias_method :options, :parse_context alias_method :options, :parse_context
@@ -35,16 +44,19 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ /(#{QuotedFragment})(.*)/om return unless markup =~ VariableSyntax
name_markup = $1 add_syntax_warning("variable prefixed with ignored characters: #{$1.inspect}") if $1
filter_markup = $2 name_markup = $2
add_syntax_warning("variable filter separator prefixed with ignored characters: #{$3.inspect}") if $3
filters_markup = $4
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om if filters_markup
filters = $1.scan(FilterParser) filters_markup.scan(FilterParser) do |lax_sep, f|
filters.each do |f| add_syntax_warning("unterminated quote or multiple pipe characters used as a filter separator: #{lax_sep.inspect}") if lax_sep
next unless f =~ /\w+/ next unless f =~ /\A\s*(\W+)??(\w+)/
filtername = Regexp.last_match(0) add_syntax_warning("ignored characters before filter name: #{$1.inspect}") if $1
filtername = $2
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
@@ -73,20 +85,21 @@ module Liquid
end end
def render(context) def render(context)
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)| @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args) context.invoke(filter_name, output, *filter_args)
end end.tap{ |obj| taint_check(context, obj) }
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
private private
def add_syntax_warning(warning)
return unless parse_context.error_mode == :lax_warn
error = SyntaxError.new(warning)
error.line_number = parse_context.line_number
parse_context.warnings << error
end
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = {} keyword_args = {}

View File

@@ -55,11 +55,9 @@ module Liquid
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil or # keywords either. The only thing we got left is to return nil
# raise an exception if `strict_variables` option is set to true
else else
return nil unless context.strict_variables return nil
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end
# If we are dealing with a drop here we have to # If we are dealing with a drop here we have to

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "4.0.0" VERSION = "4.0.0.alpha"
end end

View File

@@ -15,16 +15,15 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency 'rake', '~> 11.3' s.add_development_dependency 'rake'
s.add_development_dependency 'minitest' s.add_development_dependency 'minitest'
end end

View File

@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 60
x.warmup = 5 x.warmup = 5
puts puts
@@ -13,6 +13,5 @@ Benchmark.ips do |x|
puts puts
x.report("parse:") { profiler.compile } x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render } x.report("parse & run:") { profiler.run }
x.report("parse & render:") { profiler.run }
end end

View File

@@ -21,100 +21,53 @@ class ThemeRunner
end end
end end
# Initialize a new liquid ThemeRunner instance # Load all templates into memory, do this now so that
# Will load all templates into memory, do this now so that we don't profile IO. # we don't profile IO.
def initialize def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid' next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid' theme_path = File.dirname(test) + '/theme.liquid'
{
liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact
compile_all_tests [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
end.compact
end end
# `compile` will test just the compilation portion of liquid without any templates
def compile def compile
@tests.each do |test_hash| # Dup assigns because will make some changes to them
Liquid::Template.new.parse(test_hash[:liquid])
Liquid::Template.new.parse(test_hash[:layout]) @tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end end
end end
# `run` is called to benchmark rendering and compiling at the same time
def run def run
each_test do |liquid, layout, assigns, page_template, template_name| # Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name) compile_and_render(liquid, layout, assigns, page_template, template_name)
end end
end end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
end
end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
end end
end end

View File

@@ -13,7 +13,7 @@ class ContextDrop < Liquid::Drop
@context['forloop.index'] @context['forloop.index']
end end
def liquid_method_missing(method) def before_method(method)
@context[method] @context[method]
end end
end end
@@ -30,8 +30,8 @@ class ProductDrop < Liquid::Drop
end end
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def before_method(method)
'catchall_method: ' << method.to_s 'method: ' << method.to_s
end end
end end
@@ -59,7 +59,7 @@ class ProductDrop < Liquid::Drop
end end
class EnumerableDrop < Liquid::Drop class EnumerableDrop < Liquid::Drop
def liquid_method_missing(method) def before_method(method)
method method
end end
@@ -93,7 +93,7 @@ end
class RealEnumerableDrop < Liquid::Drop class RealEnumerableDrop < Liquid::Drop
include Enumerable include Enumerable
def liquid_method_missing(method) def before_method(method)
method method
end end
@@ -157,14 +157,14 @@ class DropsTest < Minitest::Test
assert_equal ' text1 ', output assert_equal ' text1 ', output
end end
def test_catchall_unknown_method def test_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: unknown ', output assert_equal ' method: unknown ', output
end end
def test_catchall_integer_argument_drop def test_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: 8 ', output assert_equal ' method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
@@ -231,7 +231,7 @@ class DropsTest < Minitest::Test
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)

View File

@@ -94,23 +94,7 @@ class ErrorHandlingTest < Minitest::Test
) )
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match /Liquid syntax error \(line 4\)/, err.message
end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q(
foobar
{%- "cat" | foobar -%}
bla
),
line_numbers: true
)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
@@ -202,40 +186,22 @@ class ErrorHandlingTest < Minitest::Test
end end
end end
def test_default_exception_renderer_with_internal_error def test_exception_handler_with_string_result
template = Liquid::Template.parse('This is an argument error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: ->(e) { '' })
assert_equal 'This is an argument error: ', output
assert_equal [ArgumentError], template.errors.map(&:class)
end
class InternalError < Liquid::Error
end
def test_exception_handler_with_exception_result
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
handler = ->(e) { e.is_a?(Liquid::Error) ? e : InternalError.new('internal') }
output = template.render({ 'errors' => ErrorDrop.new }) output = template.render({ 'errors' => ErrorDrop.new }, exception_handler: handler)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class) assert_equal [InternalError], template.errors.map(&:class)
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new })
assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end
def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = []
handler = ->(e) { exceptions << e; e.cause }
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
assert_equal 'This is a runtime error: runtime error', output
assert_equal [Liquid::InternalError], exceptions.map(&:class)
assert_equal exceptions, template.errors
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
end end
class TestFileSystem class TestFileSystem

View File

@@ -1,18 +1,18 @@
require 'test_helper' require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
class HashOrderingTest < Minitest::Test class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
include Liquid include Liquid
def test_global_register_order def test_global_register_order

View File

@@ -43,14 +43,6 @@ class OutputTest < Minitest::Test
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render!(@assigns)
end end
def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal "it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" }
)
end
def test_variable_traversing def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )

View File

@@ -115,8 +115,4 @@ class ParsingQuirksTest < Minitest::Test
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}") assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end end
end end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -9,10 +9,6 @@ end
class SecurityTest < Minitest::Test class SecurityTest < Minitest::Test
include Liquid include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
@@ -63,18 +59,4 @@ class SecurityTest < Minitest::Test
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end
def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render!
end
def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do
Template.parse(code).render!
end
end
end # SecurityTest end # SecurityTest

View File

@@ -41,16 +41,6 @@ class TestEnumerable < Liquid::Drop
end end
end end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test class StandardFiltersTest < Minitest::Test
include Liquid include Liquid
@@ -115,29 +105,20 @@ class StandardFiltersTest < Minitest::Test
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890') assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1)
end end
def test_split def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~') assert_equal ['12', '34'], @filters.split('12~34', '~')
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~') assert_equal ['A?Z'], @filters.split('A?Z', '~')
# Regexp works although Liquid does not support.
assert_equal ['A', 'Z'], @filters.split('AxZ', /x/)
assert_equal [], @filters.split(nil, ' ') assert_equal [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
end end
def test_escape def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>') assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal '1', @filters.escape(1)
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
assert_nil @filters.escape(nil)
end
def test_h
assert_equal '&lt;strong&gt;', @filters.h('<strong>') assert_equal '&lt;strong&gt;', @filters.h('<strong>')
assert_equal '1', @filters.h(1)
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
assert_nil @filters.h(nil)
end end
def test_escape_once def test_escape_once
@@ -146,18 +127,7 @@ class StandardFiltersTest < Minitest::Test
def test_url_encode def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com') assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal '1', @filters.url_encode(1) assert_equal nil, @filters.url_encode(nil)
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
assert_nil @filters.url_encode(nil)
end
def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar')
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
end end
def test_truncatewords def test_truncatewords
@@ -166,7 +136,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5) assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
end end
def test_strip_html def test_strip_html
@@ -182,7 +151,6 @@ class StandardFiltersTest < Minitest::Test
def test_join def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4]) assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ') assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
end end
def test_sort def test_sort
@@ -190,32 +158,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end end
def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort(input, "price")
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 }) assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
end end
@@ -235,14 +177,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end end
def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a")
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
end
def test_reverse def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]) assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
end end
@@ -291,19 +225,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result "testfoo", templ, "procs" => [p]
end end
def test_map_over_drops_returning_procs
drops = [
{
"proc" => ->{ "foo" },
},
{
"proc" => ->{ "bar" },
},
]
templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops
end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end end
@@ -317,10 +238,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new] assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
end end
def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -342,21 +259,19 @@ class StandardFiltersTest < Minitest::Test
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_equal nil, @filters.date(nil, "%B")
assert_equal '', @filters.date('', "%B") assert_equal '', @filters.date('', "%B")
with_timezone("UTC") do assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y") assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
end end
def test_first_last def test_first_last
assert_equal 1, @filters.first([1, 2, 3]) assert_equal 1, @filters.first([1, 2, 3])
assert_equal 3, @filters.last([1, 2, 3]) assert_equal 3, @filters.last([1, 2, 3])
assert_nil @filters.first([]) assert_equal nil, @filters.first([])
assert_nil @filters.last([]) assert_equal nil, @filters.last([])
end end
def test_replace def test_replace
@@ -406,38 +321,20 @@ class StandardFiltersTest < Minitest::Test
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end
def test_abs
assert_template_result "17", "{{ 17 | abs }}"
assert_template_result "17", "{{ -17 | abs }}"
assert_template_result "17", "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}"
assert_template_result "0", "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}"
assert_template_result "17.42", "{{ -17.42 | abs }}"
assert_template_result "17.42", "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}"
end end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}" assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end end
def test_divided_by def test_divided_by
@@ -451,8 +348,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result "4", "{{ 1 | modulo: 0 }}"
end end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
end end
def test_modulo def test_modulo
@@ -460,8 +355,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result "4", "{{ 1 | modulo: 0 }}"
end end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
end end
def test_round def test_round
@@ -471,9 +364,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}" assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
end end
def test_ceil def test_ceil
@@ -482,8 +372,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}" assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
end end
def test_floor def test_floor
@@ -492,8 +380,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}" assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end end
def test_append def test_append
@@ -507,7 +393,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a']) assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10]) assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
@filters.concat([1, 2], 10) @filters.concat([1, 2], 10)
end end
end end
@@ -530,19 +417,4 @@ class StandardFiltersTest < Minitest::Test
def test_cannot_access_private_methods def test_cannot_access_private_methods
assert_template_result('a', "{{ 'a' | to_number }}") assert_template_result('a', "{{ 'a' | to_number }}")
end end
def test_date_raises_nothing
assert_template_result('', "{{ '' | date: '%D' }}")
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
private
def with_timezone(tz)
old_tz = ENV['TZ']
ENV['TZ'] = tz
yield
ensure
ENV['TZ'] = old_tz
end
end # StandardFiltersTest end # StandardFiltersTest

View File

@@ -48,10 +48,6 @@ HERE
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1..3.0))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3.0))
assert_template_result(' 1.5 2.5 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.5..3))
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range

View File

@@ -29,10 +29,10 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false) assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true) assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false) assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end end
def test_if_or_with_operators def test_if_or_with_operators

View File

@@ -137,7 +137,7 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError, SystemStackError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}").render!
end end
end end
@@ -217,17 +217,6 @@ class IncludeTagTest < Minitest::Test
end end
end end
def test_render_raise_argument_error_when_template_is_undefined
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include undefined_variable %}')
template.render!
end
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
template.render!
end
end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}" assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
@@ -235,11 +224,4 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
end end
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal [], template.errors
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
end end
def test_invalid_raw def test_invalid_raw
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo') assert_match_syntax_error /tag was never closed/, '{% raw %} foo'
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}') assert_match_syntax_error /Valid syntax/, '{% raw } foo {% endraw %}'
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}') assert_match_syntax_error /Valid syntax/, '{% raw } foo %}{% endraw %}'
end end
end end

View File

@@ -2,7 +2,7 @@ require 'test_helper'
require 'timeout' require 'timeout'
class TemplateContextDrop < Liquid::Drop class TemplateContextDrop < Liquid::Drop
def liquid_method_missing(method) def before_method(method)
method method
end end
@@ -27,12 +27,6 @@ class ErroneousDrop < Liquid::Drop
end end
end end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test class TemplateTest < Minitest::Test
include Liquid include Liquid
@@ -139,17 +133,6 @@ class TemplateTest < Minitest::Test
refute_nil t.resource_limits.assign_score refute_nil t.resource_limits.assign_score
end end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
@@ -215,109 +198,17 @@ class TemplateTest < Minitest::Test
assert_equal 'ruby error in drop', e.message assert_equal 'ruby error in drop', e.message
end end
def test_exception_renderer_that_returns_string def test_exception_handler_doesnt_reraise_if_it_returns_false
exception = nil exception = nil
handler = ->(e) { exception = e; '<!-- error -->' } Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert exception.is_a?(Liquid::ZeroDivisionError) assert exception.is_a?(Liquid::ZeroDivisionError)
assert_equal '<!-- error -->', output
end end
def test_exception_renderer_that_raises def test_exception_handler_does_reraise_if_it_returns_true
exception = nil exception = nil
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise }) Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
end end
assert exception.is_a?(Liquid::ZeroDivisionError) assert exception.is_a?(Liquid::ZeroDivisionError)
end end
def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'bob filtered', rendered_template
end
def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'BOB filtered', rendered_template
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result
assert_equal 3, t.errors.count
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
t.render!(d, { strict_variables: true })
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, { strict_filters: true })
end
end
def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '1..5', result
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '12345', result
end
end end

View File

@@ -1,525 +0,0 @@
require 'test_helper'
class TrimModeTest < Minitest::Test
include Liquid
# Make sure the trim isn't applied to standard output
def test_standard_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_variable_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_tag_output_with_multiple_blank_lines
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't applied to standard tags
def test_standard_tags
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if true %}
yes
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
yes
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false %}
no
{% endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_output
text = '<p>{{- \'John\' -}}</p>'
expected = '<p>John</p>'
assert_template_result(expected, text)
end
# Make sure the trim isn't too agressive
def test_no_trim_tags
text = '<p>{%- if true -%}yes{%- endif -%}</p>'
expected = '<p>yes</p>'
assert_template_result(expected, text)
text = '<p>{%- if false -%}no{%- endif -%}</p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_outer_tag
text = '<p> {%- if true %} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {% endif -%} </p>'
expected = '<p></p>'
assert_template_result(expected, text)
end
def test_single_line_inner_tag
text = '<p> {% if true -%} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_post_tag
text = '<p> {% if true -%} yes {% endif -%} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {% if false -%} no {% endif -%} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_single_line_pre_tag
text = '<p> {%- if true %} yes {%- endif %} </p>'
expected = '<p> yes </p>'
assert_template_result(expected, text)
text = '<p> {%- if false %} no {%- endif %} </p>'
expected = '<p> </p>'
assert_template_result(expected, text)
end
def test_pre_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' }}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{ 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_pre_and_post_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true %}
yes
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false %}
no
{% endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_post_and_pre_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{% if true -%}
yes
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
yes
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
whitespace = ' '
text = <<-END_TEMPLATE
<div>
<p>
{% if false -%}
no
{%- endif %}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
#{whitespace}
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}},
{{- '30' -}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John,30</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_whitespace_trim_tags
text = <<-END_TEMPLATE
<div>
<p>
{%- if true -%}
yes
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>yes</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
text = <<-END_TEMPLATE
<div>
<p>
{%- if false -%}
no
{%- endif -%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p></p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim_output
text = <<-END_TEMPLATE
<div>
<p>
{{- 'John' -}}
{{- '30' -}}
</p>
<b>
{{ 'John' -}}
{{- '30' }}
</b>
<i>
{{- 'John' }}
{{ '30' -}}
</i>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>John30</p>
<b>
John30
</b>
<i>John
30</i>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_complex_trim
text = <<-END_TEMPLATE
<div>
{%- if true -%}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{%- endif -%}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div><p>John</p></div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_raw_output
whitespace = ' '
text = <<-END_TEMPLATE
<div>
{% raw %}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
{% endraw %}
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
#{whitespace}
{%- if true -%}
<p>
{{- 'John' -}}
</p>
{%- endif -%}
#{whitespace}
</div>
END_EXPECTED
assert_template_result(expected, text)
end
end # TrimModeTest

View File

@@ -89,4 +89,32 @@ class VariableTest < Minitest::Test
def test_multiline_variable def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked') assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end end
def test_lax_warnings_for_variable_ignored_prefix
ignored_chars = %(|,'")
template = Liquid::Template.parse("{{ #{ignored_chars}test }}", error_mode: :lax_warn)
assert_equal "works", template.render!('test' => 'works')
assert_equal [Liquid::SyntaxError.new("variable prefixed with ignored characters: #{ignored_chars.inspect}")], template.warnings
end
def test_lax_warnings_for_ignored_variable_filter_prefix
ignored_chars = ",wat? lax!"
template = Liquid::Template.parse("{{ test#{ignored_chars} | noop }}", error_mode: :lax_warn)
assert_equal "works", template.render!('test' => 'works')
assert_equal [Liquid::SyntaxError.new("variable filter separator prefixed with ignored characters: #{ignored_chars.inspect}")], template.warnings
end
def test_lax_warnings_for_weird_filter_chars
template = Liquid::Template.parse("{{ test | upcase \" prepend: 'it ' || append: ' surprisingly' }}", error_mode: :lax_warn)
assert_equal "it WORKS surprisingly", template.render!('test' => 'works')
expected_warnings = ['"', '||'].map{ |sep| Liquid::SyntaxError.new("unterminated quote or multiple pipe characters used as a filter separator: #{sep.inspect}") }
assert_equal expected_warnings, template.warnings
end
def test_lax_warnings_for_ignored_filter_name_prefix
ignored_chars = "'': '"
template = Liquid::Template.parse("{{ test | '': 'prepend ' }}", error_mode: :lax_warn)
assert_equal "prepend wat", template.render!('test' => 'wat')
assert_equal [Liquid::SyntaxError.new("ignored characters before filter name: #{ignored_chars.inspect}")], template.warnings
end
end end

View File

@@ -2,6 +2,7 @@
ENV["MT_NO_EXPECTATIONS"] = "1" ENV["MT_NO_EXPECTATIONS"] = "1"
require 'minitest/autorun' require 'minitest/autorun'
require 'spy/integration'
$LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
require 'liquid.rb' require 'liquid.rb'

View File

@@ -46,8 +46,6 @@ class BlockUnitTest < Minitest::Test
def test_with_custom_tag def test_with_custom_tag
Liquid::Template.register_tag("testtag", Block) Liquid::Template.register_tag("testtag", Block)
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
ensure
Liquid::Template.tags.delete('testtag')
end end
private private

View File

@@ -3,54 +3,50 @@ require 'test_helper'
class ConditionUnitTest < Minitest::Test class ConditionUnitTest < Minitest::Test
include Liquid include Liquid
def setup
@context = Liquid::Context.new
end
def test_basic_condition def test_basic_condition
assert_equal false, Condition.new(1, '==', 2).evaluate assert_equal false, Condition.new(1, '==', 2).evaluate
assert_equal true, Condition.new(1, '==', 1).evaluate assert_equal true, Condition.new(1, '==', 1).evaluate
end end
def test_default_operators_evalute_true def test_default_operators_evalute_true
assert_evaluates_true 1, '==', 1 assert_evalutes_true 1, '==', 1
assert_evaluates_true 1, '!=', 2 assert_evalutes_true 1, '!=', 2
assert_evaluates_true 1, '<>', 2 assert_evalutes_true 1, '<>', 2
assert_evaluates_true 1, '<', 2 assert_evalutes_true 1, '<', 2
assert_evaluates_true 2, '>', 1 assert_evalutes_true 2, '>', 1
assert_evaluates_true 1, '>=', 1 assert_evalutes_true 1, '>=', 1
assert_evaluates_true 2, '>=', 1 assert_evalutes_true 2, '>=', 1
assert_evaluates_true 1, '<=', 2 assert_evalutes_true 1, '<=', 2
assert_evaluates_true 1, '<=', 1 assert_evalutes_true 1, '<=', 1
# negative numbers # negative numbers
assert_evaluates_true 1, '>', -1 assert_evalutes_true 1, '>', -1
assert_evaluates_true (-1), '<', 1 assert_evalutes_true -1, '<', 1
assert_evaluates_true 1.0, '>', -1.0 assert_evalutes_true 1.0, '>', -1.0
assert_evaluates_true (-1.0), '<', 1.0 assert_evalutes_true -1.0, '<', 1.0
end end
def test_default_operators_evalute_false def test_default_operators_evalute_false
assert_evaluates_false 1, '==', 2 assert_evalutes_false 1, '==', 2
assert_evaluates_false 1, '!=', 1 assert_evalutes_false 1, '!=', 1
assert_evaluates_false 1, '<>', 1 assert_evalutes_false 1, '<>', 1
assert_evaluates_false 1, '<', 0 assert_evalutes_false 1, '<', 0
assert_evaluates_false 2, '>', 4 assert_evalutes_false 2, '>', 4
assert_evaluates_false 1, '>=', 3 assert_evalutes_false 1, '>=', 3
assert_evaluates_false 2, '>=', 4 assert_evalutes_false 2, '>=', 4
assert_evaluates_false 1, '<=', 0 assert_evalutes_false 1, '<=', 0
assert_evaluates_false 1, '<=', 0 assert_evalutes_false 1, '<=', 0
end end
def test_contains_works_on_strings def test_contains_works_on_strings
assert_evaluates_true 'bob', 'contains', 'o' assert_evalutes_true 'bob', 'contains', 'o'
assert_evaluates_true 'bob', 'contains', 'b' assert_evalutes_true 'bob', 'contains', 'b'
assert_evaluates_true 'bob', 'contains', 'bo' assert_evalutes_true 'bob', 'contains', 'bo'
assert_evaluates_true 'bob', 'contains', 'ob' assert_evalutes_true 'bob', 'contains', 'ob'
assert_evaluates_true 'bob', 'contains', 'bob' assert_evalutes_true 'bob', 'contains', 'bob'
assert_evaluates_false 'bob', 'contains', 'bob2' assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evaluates_false 'bob', 'contains', 'a' assert_evalutes_false 'bob', 'contains', 'a'
assert_evaluates_false 'bob', 'contains', '---' assert_evalutes_false 'bob', 'contains', '---'
end end
def test_invalid_comparation_operator def test_invalid_comparation_operator
@@ -64,42 +60,34 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error '1', '<=', 0 assert_evaluates_argument_error '1', '<=', 0
end end
def test_hash_compare_backwards_compatibility
assert_nil Condition.new({}, '>', 2).evaluate
assert_nil Condition.new(2, '>', {}).evaluate
assert_equal false, Condition.new({}, '==', 2).evaluate
assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array") array_expr = VariableLookup.new("array")
assert_evaluates_false array_expr, 'contains', 0 assert_evalutes_false array_expr, 'contains', 0
assert_evaluates_true array_expr, 'contains', 1 assert_evalutes_true array_expr, 'contains', 1
assert_evaluates_true array_expr, 'contains', 2 assert_evalutes_true array_expr, 'contains', 2
assert_evaluates_true array_expr, 'contains', 3 assert_evalutes_true array_expr, 'contains', 3
assert_evaluates_true array_expr, 'contains', 4 assert_evalutes_true array_expr, 'contains', 4
assert_evaluates_true array_expr, 'contains', 5 assert_evalutes_true array_expr, 'contains', 5
assert_evaluates_false array_expr, 'contains', 6 assert_evalutes_false array_expr, 'contains', 6
assert_evaluates_false array_expr, 'contains', "1" assert_evalutes_false array_expr, 'contains', "1"
end end
def test_contains_returns_false_for_nil_operands def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new @context = Liquid::Context.new
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0' assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned') assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
end end
def test_contains_return_false_on_wrong_data_type def test_contains_return_false_on_wrong_data_type
assert_evaluates_false 1, 'contains', 0 assert_evalutes_false 1, 'contains', 0
end end
def test_contains_with_string_left_operand_coerces_right_operand_to_string def test_contains_with_string_left_operand_coerces_right_operand_to_string
assert_evaluates_true ' 1 ', 'contains', 1 assert_evalutes_true ' 1 ', 'contains', 1
assert_evaluates_false ' 1 ', 'contains', 2 assert_evalutes_false ' 1 ', 'contains', 2
end end
def test_or_condition def test_or_condition
@@ -133,8 +121,8 @@ class ConditionUnitTest < Minitest::Test
def test_should_allow_custom_proc_operator def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} } Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
assert_evaluates_true 'bob', 'starts_with', 'b' assert_evalutes_true 'bob', 'starts_with', 'b'
assert_evaluates_false 'bob', 'starts_with', 'o' assert_evalutes_false 'bob', 'starts_with', 'o'
ensure ensure
Condition.operators.delete 'starts_with' Condition.operators.delete 'starts_with'
end end
@@ -143,24 +131,24 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another") assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
end end
private private
def assert_evaluates_true(left, op, right) def assert_evalutes_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context), assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
"Evaluated false: #{left} #{op} #{right}" "Evaluated false: #{left} #{op} #{right}"
end end
def assert_evaluates_false(left, op, right) def assert_evalutes_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context), assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
"Evaluated true: #{left} #{op} #{right}" "Evaluated true: #{left} #{op} #{right}"
end end
def assert_evaluates_argument_error(left, op, right) def assert_evaluates_argument_error(left, op, right)
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
Condition.new(left, op, right).evaluate(@context) Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
end end
end end
end # ConditionTest end # ConditionTest

View File

@@ -70,6 +70,10 @@ class ContextUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
end end
def teardown
Spy.teardown
end
def test_variables def test_variables
@context['string'] = 'string' @context['string'] = 'string'
assert_equal 'string', @context['string'] assert_equal 'string', @context['string']
@@ -94,12 +98,12 @@ class ContextUnitTest < Minitest::Test
assert_equal false, @context['bool'] assert_equal false, @context['bool']
@context['nil'] = nil @context['nil'] = nil
assert_nil @context['nil'] assert_equal nil, @context['nil']
assert_nil @context['nil'] assert_equal nil, @context['nil']
end end
def test_variables_not_existing def test_variables_not_existing
assert_nil @context['does_not_exist'] assert_equal nil, @context['does_not_exist']
end end
def test_scoping def test_scoping
@@ -181,7 +185,7 @@ class ContextUnitTest < Minitest::Test
@context['test'] = 'test' @context['test'] = 'test'
assert_equal 'test', @context['test'] assert_equal 'test', @context['test']
@context.pop @context.pop
assert_nil @context['test'] assert_equal nil, @context['test']
end end
def test_hierachical_data def test_hierachical_data
@@ -263,7 +267,7 @@ class ContextUnitTest < Minitest::Test
def test_access_hashes_with_hash_notation def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 5, @context['products["count"]'] assert_equal 5, @context['products["count"]']
assert_equal 'deepsnow', @context['products["tags"][0]'] assert_equal 'deepsnow', @context['products["tags"][0]']
@@ -296,12 +300,12 @@ class ContextUnitTest < Minitest::Test
@context['hash'] = { 'first' => 'Hello' } @context['hash'] = { 'first' => 'Hello' }
assert_equal 1, @context['array.first'] assert_equal 1, @context['array.first']
assert_nil @context['array["first"]'] assert_equal nil, @context['array["first"]']
assert_equal 'Hello', @context['hash["first"]'] assert_equal 'Hello', @context['hash["first"]']
end end
def test_first_can_appear_in_middle_of_callchain def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 'draft151cm', @context['product.variants[0].title'] assert_equal 'draft151cm', @context['product.variants[0].title']
assert_equal 'element151cm', @context['product.variants[1].title'] assert_equal 'element151cm', @context['product.variants[1].title']
@@ -446,10 +450,14 @@ class ContextUnitTest < Minitest::Test
assert_equal @context, @context['category'].context assert_equal @context, @context['category'].context
end end
def test_interrupt_avoids_object_allocations def test_use_empty_instead_of_any_in_interrupt_handling_to_avoid_lots_of_unnecessary_object_allocations
assert_no_object_allocations do mock_any = Spy.on_instance_method(Array, :any?)
@context.interrupt? mock_empty = Spy.on_instance_method(Array, :empty?)
end
@context.interrupt?
refute mock_any.has_been_called?
assert mock_empty.has_been_called?
end end
def test_context_initialization_with_a_proc_in_environment def test_context_initialization_with_a_proc_in_environment
@@ -458,32 +466,4 @@ class ContextUnitTest < Minitest::Test
assert contx assert contx
assert_nil contx['poutine'] assert_nil contx['poutine']
end end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal 'hi filtered', context.apply_global_filter('hi')
end
def test_apply_global_filter_when_no_global_filter_exist
context = Context.new
assert_equal 'hi', context.apply_global_filter('hi')
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip "stackprof needed to count object allocations"
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal 0, profile[:samples]
end
end # ContextTest end # ContextTest

View File

@@ -19,7 +19,7 @@ class LexerUnitTest < Minitest::Test
end end
def test_comparison def test_comparison
tokens = Lexer.new('== <> contains ').tokenize tokens = Lexer.new('== <> contains').tokenize
assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
end end

View File

@@ -29,18 +29,6 @@ class StrainerUnitTest < Minitest::Test
end end
end end
def test_stainer_argument_error_contains_backtrace
strainer = Strainer.create(nil)
begin
strainer.invoke("public_filter", 1)
rescue Liquid::ArgumentError => e
assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
e.message)
assert_equal e.backtrace[0].split(':')[0], __FILE__
end
end
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil) strainer = Strainer.create(nil)
assert_equal false, strainer.class.invokable?('__test__') assert_equal false, strainer.class.invokable?('__test__')
@@ -77,88 +65,4 @@ class StrainerUnitTest < Minitest::Test
assert_kind_of b, strainer assert_kind_of b, strainer
assert_kind_of Liquid::StandardFilters, strainer assert_kind_of Liquid::StandardFilters, strainer
end end
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
assert_raises ArgumentError do
s.class.add_filter(wrong_filter)
end
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert strainer.class.filter_methods.include?('public_filter')
end
module LateAddedFilter
def late_added_filter(input)
"filtered"
end
end
def test_global_filter_clears_cache
assert_equal 'input', Strainer.create(nil).invoke('late_added_filter', 'input')
Strainer.global_filter(LateAddedFilter)
assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input')
end
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal 1, mod.include_count
end
end # StrainerTest end # StrainerTest

View File

@@ -67,12 +67,4 @@ class TemplateUnitTest < Minitest::Test
Template.tags.delete('fake') Template.tags.delete('fake')
assert_nil Template.tags['fake'] assert_nil Template.tags['fake']
end end
def test_tags_can_be_looped_over
Template.register_tag('fake', FakeTag)
result = Template.tags.map { |name, klass| [name, klass] }
assert result.include?(["fake", "TemplateUnitTest::FakeTag"])
ensure
Template.tags.delete('fake')
end
end end