diff --git a/.travis.yml b/.travis.yml
index 31cec87..d317b2c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,9 @@
language: ruby
rvm:
- - 2.0
- 2.1
- 2.2
+ - 2.3.3
- ruby-head
- jruby-head
# - rbx-2
@@ -19,6 +19,10 @@ matrix:
allow_failures:
- rvm: jruby-head
+install:
+ - gem install rainbow -v 2.2.1
+ - bundle install
+
script: "bundle exec rake"
notifications:
diff --git a/Gemfile b/Gemfile
index 44e39e7..4229b05 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,12 +3,15 @@ source 'https://rubygems.org'
gemspec
gem 'stackprof', platforms: :mri_21
+group :benchmark, :test do
+ gem 'benchmark-ips'
+end
+
group :test do
gem 'spy', '0.4.1'
- gem 'benchmark-ips'
gem 'rubocop', '0.34.2'
platform :mri do
- gem 'liquid-c', github: 'Shopify/liquid-c', ref: '1fa04f1d3d4fdb5cc33bcc090334ab097ce2c10c'
+ gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end
end
diff --git a/History.md b/History.md
index 8e7a483..2dc8f3d 100644
--- a/History.md
+++ b/History.md
@@ -1,8 +1,10 @@
# Liquid Change Log
-## 4.0.0 / not yet released / branch "master"
+## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### 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]
@@ -18,11 +20,13 @@
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
* 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]
-* Remove support for `liquid_methods`
+* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### 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]
diff --git a/circle.yml b/circle.yml
index fec889a..5e13430 100644
--- a/circle.yml
+++ b/circle.yml
@@ -1,3 +1,3 @@
machine:
ruby:
- version: ruby-2.0
+ version: ruby-2.1
diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb
index 00811c1..6c9b680 100644
--- a/lib/liquid/block_body.rb
+++ b/lib/liquid/block_body.rb
@@ -96,7 +96,8 @@ module Liquid
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
- output << context.handle_error(e, token.line_number)
+ line_number = token.is_a?(String) ? nil : token.line_number
+ output << context.handle_error(e, line_number)
end
end
@@ -106,7 +107,7 @@ module Liquid
private
def render_node(node, context)
- node_output = (node.respond_to?(:render) ? node.render(context) : node)
+ node_output = node.is_a?(String) ? node : node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += node_output.length
diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb
index 9573f6b..7db9cf3 100644
--- a/lib/liquid/condition.rb
+++ b/lib/liquid/condition.rb
@@ -110,7 +110,7 @@ module Liquid
if operation.respond_to?(:call)
operation.call(self, left, right)
- elsif left.respond_to?(operation) && right.respond_to?(operation)
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin
left.send(operation, right)
rescue ::ArgumentError => e
diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb
index d41af49..566c5c6 100644
--- a/lib/liquid/context.rb
+++ b/lib/liquid/context.rb
@@ -13,7 +13,7 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
- attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
+ attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@@ -27,8 +27,9 @@ module Liquid
@this_stack_used = false
+ self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
- self.exception_handler = ->(e) { raise }
+ self.exception_renderer = ->(e) { raise }
end
@interrupts = []
@@ -74,30 +75,11 @@ module Liquid
end
def handle_error(e, line_number = nil)
- if e.is_a?(Liquid::Error)
- e.template_name ||= template_name
- 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
+ e = internal_error unless e.is_a?(Liquid::Error)
+ e.template_name ||= template_name
+ e.line_number ||= line_number
errors.push(e)
- output || Liquid::Error.render(e)
+ exception_renderer.call(e).to_s
end
def invoke(method, *args)
@@ -178,7 +160,7 @@ module Liquid
end
# Fetches an object starting at the local scope and then moving up the hierachy
- def find_variable(key)
+ def find_variable(key, raise_on_not_found: true)
# 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
index = @scopes.find_index { |s| s.key?(key) }
@@ -188,7 +170,7 @@ module Liquid
if scope.nil?
@environments.each do |e|
- variable = lookup_and_evaluate(e, key)
+ variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
unless variable.nil?
scope = e
break
@@ -197,7 +179,7 @@ module Liquid
end
scope ||= @environments.last || @scopes.last
- variable ||= lookup_and_evaluate(scope, key)
+ variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
@@ -205,8 +187,8 @@ module Liquid
variable
end
- def lookup_and_evaluate(obj, key)
- if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
+ def lookup_and_evaluate(obj, key, raise_on_not_found: true)
+ if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
@@ -221,6 +203,13 @@ module Liquid
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
@scopes.last.each_key do |k|
@environments.each do |env|
diff --git a/lib/liquid/errors.rb b/lib/liquid/errors.rb
index 6dcd05e..defa5ea 100644
--- a/lib/liquid/errors.rb
+++ b/lib/liquid/errors.rb
@@ -17,14 +17,6 @@ module Liquid
str
end
- def self.render(e)
- if e.is_a?(Liquid::Error)
- e.to_s
- else
- "Liquid error: #{e}"
- end
- end
-
private
def message_prefix
@@ -60,4 +52,5 @@ module Liquid
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
+ InternalError = Class.new(Error)
end
diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml
index e69eb3e..9a259bf 100644
--- a/lib/liquid/locales/en.yml
+++ b/lib/liquid/locales/en.yml
@@ -22,3 +22,5 @@
tag_never_closed: "'%{block_name}' tag was never closed"
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"
+ argument:
+ include: "Argument error in tag 'include' - Illegal template name"
diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb
index 90dfdd3..48056f3 100644
--- a/lib/liquid/standardfilters.rb
+++ b/lib/liquid/standardfilters.rb
@@ -65,9 +65,10 @@ module Liquid
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
- l = length - truncate_string.length
+ truncate_string_str = truncate_string.to_s
+ l = length - truncate_string_str.length
l = 0 if l < 0
- input_str.length > length ? input_str[0...l] + truncate_string : input_str
+ input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -76,7 +77,7 @@ module Liquid
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -85,7 +86,7 @@ module Liquid
#
{{ post | split '//' | first }}
#
def split(input, pattern)
- input.to_s.split(pattern)
+ input.to_s.split(pattern.to_s)
end
def strip(input)
@@ -124,7 +125,15 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
- ary.sort { |a, b| a[property] <=> b[property] }
+ ary.sort do |a, b|
+ a = a[property]
+ b = b[property]
+ if a && b
+ a <=> b
+ else
+ a ? -1 : 1
+ end
+ end
end
end
@@ -375,7 +384,7 @@ module Liquid
end
def join(glue)
- to_a.join(glue)
+ to_a.join(glue.to_s)
end
def concat(args)
diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb
index 441e335..76d56d2 100644
--- a/lib/liquid/strainer.rb
+++ b/lib/liquid/strainer.rb
@@ -27,7 +27,7 @@ module Liquid
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
- unless self.class.include?(filter)
+ unless self.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -39,6 +39,7 @@ module Liquid
end
def self.global_filter(filter)
+ @@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb
index 55011b3..5d5691f 100644
--- a/lib/liquid/tags/for.rb
+++ b/lib/liquid/tags/for.rb
@@ -23,7 +23,7 @@ module Liquid
# {{ item.name }}
# {% end %}
#
- # To reverse the for loop simply use {% for item in collection reversed %}
+ # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb
index ba970cc..a800703 100644
--- a/lib/liquid/tags/include.rb
+++ b/lib/liquid/tags/include.rb
@@ -42,14 +42,15 @@ module Liquid
def render(context)
template_name = context.evaluate(@template_name_expr)
- partial = load_cached_partial(template_name, context)
+ raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
+ partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
else
- context.find_variable(template_name)
+ context.find_variable(template_name, raise_on_not_found: false)
end
old_template_name = context.template_name
diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb
index 40985ed..31a67e4 100644
--- a/lib/liquid/template.rb
+++ b/lib/liquid/template.rb
@@ -69,6 +69,11 @@ module Liquid
# :error raises an error when tainted output is used
attr_writer :taint_mode
+ attr_accessor :default_exception_renderer
+ Template.default_exception_renderer = lambda do |exception|
+ exception
+ end
+
def file_system
@@file_system
end
@@ -167,7 +172,7 @@ module Liquid
c = args.shift
if @rethrow_errors
- c.exception_handler = ->(e) { raise }
+ c.exception_renderer = ->(e) { raise }
end
c
@@ -241,7 +246,7 @@ module Liquid
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_handler = options[:exception_handler] if options[:exception_handler]
+ 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
diff --git a/lib/liquid/version.rb b/lib/liquid/version.rb
index 823405a..af15e07 100644
--- a/lib/liquid/version.rb
+++ b/lib/liquid/version.rb
@@ -1,4 +1,4 @@
# encoding: utf-8
module Liquid
- VERSION = "4.0.0.rc2"
+ VERSION = "4.0.0"
end
diff --git a/liquid.gemspec b/liquid.gemspec
index 5b00a0d..3affbed 100644
--- a/liquid.gemspec
+++ b/liquid.gemspec
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
s.license = "MIT"
# 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.test_files = Dir.glob("{test}/**/*")
@@ -24,6 +25,6 @@ Gem::Specification.new do |s|
s.require_path = "lib"
- s.add_development_dependency 'rake'
+ s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency 'minitest'
end
diff --git a/performance/benchmark.rb b/performance/benchmark.rb
index a6f8185..68c568c 100644
--- a/performance/benchmark.rb
+++ b/performance/benchmark.rb
@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
- x.time = 60
+ x.time = 10
x.warmup = 5
puts
@@ -13,5 +13,6 @@ Benchmark.ips do |x|
puts
x.report("parse:") { profiler.compile }
- x.report("parse & run:") { profiler.run }
+ x.report("render:") { profiler.render }
+ x.report("parse & render:") { profiler.run }
end
diff --git a/performance/theme_runner.rb b/performance/theme_runner.rb
index 6ac7917..9f6a1fc 100644
--- a/performance/theme_runner.rb
+++ b/performance/theme_runner.rb
@@ -21,53 +21,100 @@ class ThemeRunner
end
end
- # Load all templates into memory, do this now so that
- # we don't profile IO.
+ # Initialize a new liquid ThemeRunner instance
+ # Will load all templates into memory, do this now so that we don't profile IO.
def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid'
-
- [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
+ {
+ liquid: File.read(test),
+ layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
+ template_name: test
+ }
end.compact
+
+ compile_all_tests
end
+ # `compile` will test just the compilation portion of liquid without any templates
def compile
- # Dup assigns because will make some changes to them
-
- @tests.each do |liquid, layout, template_name|
- tmpl = Liquid::Template.new
- tmpl.parse(liquid)
- tmpl = Liquid::Template.new
- tmpl.parse(layout)
+ @tests.each do |test_hash|
+ Liquid::Template.new.parse(test_hash[:liquid])
+ Liquid::Template.new.parse(test_hash[:layout])
end
end
+ # `run` is called to benchmark rendering and compiling at the same time
def run
- # 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))
+ each_test do |liquid, layout, assigns, page_template, template_name|
compile_and_render(liquid, layout, assigns, page_template, template_name)
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)
+ 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.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
-
- 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
+ tmpl
end
end
diff --git a/test/integration/error_handling_test.rb b/test/integration/error_handling_test.rb
index 95d65db..ba81861 100644
--- a/test/integration/error_handling_test.rb
+++ b/test/integration/error_handling_test.rb
@@ -202,22 +202,40 @@ class ErrorHandlingTest < Minitest::Test
end
end
- 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
+ def test_default_exception_renderer_with_internal_error
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 }, exception_handler: handler)
+
+ output = template.render({ 'errors' => ErrorDrop.new })
+
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
- assert_equal [InternalError], template.errors.map(&:class)
+ assert_equal [Liquid::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 '#', exceptions.first.cause.inspect
end
class TestFileSystem
diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb
index a20e1c3..e607b76 100644
--- a/test/integration/standard_filter_test.rb
+++ b/test/integration/standard_filter_test.rb
@@ -115,15 +115,15 @@ class StandardFiltersTest < Minitest::Test
assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
+ assert_equal '12341', @filters.truncate("1234567890", 5, 1)
end
def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~')
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 ['A', 'Z'], @filters.split('A1Z', 1)
end
def test_escape
@@ -154,6 +154,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
+ assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
end
def test_strip_html
@@ -169,6 +170,7 @@ class StandardFiltersTest < Minitest::Test
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 '1121314', @filters.join([1, 2, 3, 4], 1)
end
def test_sort
@@ -176,6 +178,24 @@ 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")
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
diff --git a/test/integration/tags/include_tag_test.rb b/test/integration/tags/include_tag_test.rb
index 87dfe80..25af662 100644
--- a/test/integration/tags/include_tag_test.rb
+++ b/test/integration/tags/include_tag_test.rb
@@ -217,6 +217,17 @@ class IncludeTagTest < Minitest::Test
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
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
@@ -224,4 +235,11 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
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
diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb
index 8ad616d..253b976 100644
--- a/test/integration/template_test.rb
+++ b/test/integration/template_test.rb
@@ -215,16 +215,20 @@ class TemplateTest < Minitest::Test
assert_equal 'ruby error in drop', e.message
end
- def test_exception_handler_doesnt_reraise_if_it_returns_false
+ def test_exception_renderer_that_returns_string
exception = nil
- Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
+ handler = ->(e) { exception = e; '' }
+
+ output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
+
assert exception.is_a?(Liquid::ZeroDivisionError)
+ assert_equal '', output
end
- def test_exception_handler_does_reraise_if_it_returns_true
+ def test_exception_renderer_that_raises
exception = nil
assert_raises(Liquid::ZeroDivisionError) do
- Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
+ Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
end
assert exception.is_a?(Liquid::ZeroDivisionError)
end
diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb
index 8dfc788..6b0cc34 100644
--- a/test/unit/condition_unit_test.rb
+++ b/test/unit/condition_unit_test.rb
@@ -64,6 +64,14 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error '1', '<=', 0
end
+ def test_hash_compare_backwards_compatibility
+ assert_equal nil, Condition.new({}, '>', 2).evaluate
+ assert_equal 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
@context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5]
diff --git a/test/unit/strainer_unit_test.rb b/test/unit/strainer_unit_test.rb
index 5cdf2e8..5ce2100 100644
--- a/test/unit/strainer_unit_test.rb
+++ b/test/unit/strainer_unit_test.rb
@@ -133,4 +133,32 @@ class StrainerUnitTest < Minitest::Test
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