Compare commits

..

25 Commits

Author SHA1 Message Date
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
22 changed files with 192 additions and 89 deletions

View File

@@ -3,6 +3,7 @@
## 4.0.0 / not yet released / branch "master"
### Changed
* 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]

View File

@@ -21,6 +21,7 @@ module Liquid
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments

View File

@@ -25,7 +25,7 @@ module Liquid
# Catch all for the method
def liquid_method_missing(method)
return nil unless @context.strict_variables
return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
@@ -62,16 +62,17 @@ module Liquid
end
def self.invokable_methods
unless @invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s))
Set.new(whitelist.map(&:to_s))
end
@invokable_methods
end
end
end

View File

@@ -25,6 +25,12 @@ class Numeric # :nodoc:
end
end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self

View File

@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path
end

View File

@@ -1,7 +1,7 @@
module Liquid
class ParseContext
attr_accessor :partial, :locale, :line_number
attr_reader :warnings, :error_mode
attr_accessor :locale, :line_number
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}

View File

@@ -216,6 +216,9 @@ module Liquid
end
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)
end
@@ -286,6 +289,12 @@ module Liquid
array.last if array.respond_to?(:last)
end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
@@ -370,7 +379,7 @@ module Liquid
end
def concat(args)
to_a.concat args
to_a.concat(args)
end
def reverse

View File

@@ -48,8 +48,10 @@ module Liquid
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)

View File

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

View File

@@ -19,8 +19,10 @@ module Liquid
@@file_system = BlankFileSystem.new
class TagRegistry
include Enumerable
def initialize
@tags = {}
@tags = {}
@cache = {}
end
@@ -41,6 +43,10 @@ module Liquid
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
@@ -80,11 +86,11 @@ module Liquid
end
def error_mode
@error_mode || :lax
@error_mode ||= :lax
end
def taint_mode
@taint_mode || :lax
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available
@@ -107,6 +113,7 @@ module Liquid
end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end

View File

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

View File

@@ -50,9 +50,13 @@ module Liquid
when Numeric
obj
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
0
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end

View File

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

View File

@@ -94,7 +94,7 @@ class ErrorHandlingTest < Minitest::Test
)
end
assert_match /Liquid syntax error \(line 4\)/, err.message
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors

View File

@@ -1,18 +1,18 @@
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
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
include Liquid
def test_global_register_order

View File

@@ -9,6 +9,10 @@ end
class SecurityTest < Minitest::Test
include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )

View File

@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
end
end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test
include Liquid
@@ -364,20 +374,38 @@ class StandardFiltersTest < Minitest::Test
def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end
def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
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
def test_times
assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}"
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 "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end
def test_divided_by
@@ -391,6 +419,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
end
def test_modulo
@@ -398,6 +428,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
end
def test_round
@@ -407,6 +439,9 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
end
def test_ceil
@@ -415,6 +450,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
end
def test_floor
@@ -423,6 +460,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end
def test_append
@@ -436,8 +475,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
end
end

View File

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

View File

@@ -306,4 +306,14 @@ class TemplateTest < Minitest::Test
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

View File

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

View File

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

View File

@@ -67,4 +67,12 @@ class TemplateUnitTest < Minitest::Test
Template.tags.delete('fake')
assert_nil Template.tags['fake']
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