liquid 4.0.0.rc1 → 4.0.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +8 -1
- data/README.md +31 -0
- data/lib/liquid/block_body.rb +3 -0
- data/lib/liquid/context.rb +9 -2
- data/lib/liquid/drop.rb +7 -5
- data/lib/liquid/errors.rb +4 -0
- data/lib/liquid/file_system.rb +1 -1
- data/lib/liquid/parse_context.rb +2 -2
- data/lib/liquid/standardfilters.rb +10 -10
- data/lib/liquid/strainer.rb +10 -3
- data/lib/liquid/tags/assign.rb +16 -4
- data/lib/liquid/tags/for.rb +2 -0
- data/lib/liquid/template.rb +13 -9
- data/lib/liquid/tokenizer.rb +1 -1
- data/lib/liquid/utils.rb +6 -2
- data/lib/liquid/variable_lookup.rb +4 -2
- data/lib/liquid/version.rb +1 -1
- data/test/integration/error_handling_test.rb +1 -1
- data/test/integration/hash_ordering_test.rb +9 -9
- data/test/integration/security_test.rb +4 -0
- data/test/integration/standard_filter_test.rb +29 -4
- data/test/integration/tags/raw_tag_test.rb +3 -3
- data/test/integration/template_test.rb +81 -0
- data/test/unit/condition_unit_test.rb +55 -51
- data/test/unit/strainer_unit_test.rb +56 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a15813568dcc19866675ad50444b7939b5eecfa
|
4
|
+
data.tar.gz: acfb1e49596e266170a25c584978ec8ade8bcf01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20fa5f4ceec720a9c57219f71bb848c7b407529cf46bf7353fba196452bf9945e33d79e25aff8fb92aceb32ac0f6d2299e6204e7fc5d6eec04134224dae725ef
|
7
|
+
data.tar.gz: 4ed81d181edfaf21bd25b303ec4e5085c9e734e3fb9679aa3b7a88f664bced5ab60825bb163ffaf7498b650cedf779901ce78917abad1fcbc5be1ec7b27573ee
|
data/History.md
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
## 4.0.0 / not yet released / branch "master"
|
4
4
|
|
5
5
|
### Changed
|
6
|
+
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
7
|
+
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
8
|
+
* Improve loop performance (#681) [Florian Weingarten]
|
9
|
+
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
6
10
|
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
7
11
|
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
|
8
12
|
* Add compact filter (#600) [Carson Reinke]
|
@@ -15,9 +19,12 @@
|
|
15
19
|
* Ruby 1.9 support dropped (#491) [Justin Li]
|
16
20
|
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
17
21
|
* Remove support for `liquid_methods`
|
18
|
-
*
|
22
|
+
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
19
23
|
|
20
24
|
### Fixed
|
25
|
+
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
26
|
+
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
27
|
+
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
21
28
|
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
|
22
29
|
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
|
23
30
|
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
data/README.md
CHANGED
@@ -73,3 +73,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
|
|
73
73
|
|
74
74
|
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
75
75
|
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
76
|
+
|
77
|
+
### Undefined variables and filters
|
78
|
+
|
79
|
+
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.
|
80
|
+
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
|
81
|
+
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.
|
82
|
+
Here are some examples:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
|
86
|
+
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
|
87
|
+
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
|
88
|
+
template.errors
|
89
|
+
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
|
90
|
+
```
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
|
94
|
+
template.render({ 'x' => 'foo' }, { strict_filters: true })
|
95
|
+
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
|
96
|
+
template.errors
|
97
|
+
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
|
98
|
+
```
|
99
|
+
|
100
|
+
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
template = Liquid::Template.parse("{{x}} {{y}}")
|
104
|
+
template.render!({ 'x' => 1}, { strict_variables: true })
|
105
|
+
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
106
|
+
```
|
data/lib/liquid/block_body.rb
CHANGED
@@ -76,6 +76,9 @@ module Liquid
|
|
76
76
|
end
|
77
77
|
rescue MemoryError => e
|
78
78
|
raise e
|
79
|
+
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
80
|
+
context.handle_error(e, token.line_number)
|
81
|
+
output << nil
|
79
82
|
rescue ::StandardError => e
|
80
83
|
output << context.handle_error(e, token.line_number)
|
81
84
|
end
|
data/lib/liquid/context.rb
CHANGED
@@ -13,7 +13,7 @@ module Liquid
|
|
13
13
|
# context['bob'] #=> nil class Context
|
14
14
|
class Context
|
15
15
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
16
|
-
attr_accessor :exception_handler, :template_name, :partial, :global_filter
|
16
|
+
attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
17
17
|
|
18
18
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
19
19
|
@environments = [environments].flatten
|
@@ -21,6 +21,7 @@ module Liquid
|
|
21
21
|
@registers = registers
|
22
22
|
@errors = []
|
23
23
|
@partial = false
|
24
|
+
@strict_variables = false
|
24
25
|
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
25
26
|
squash_instance_assigns_with_environments
|
26
27
|
|
@@ -205,7 +206,13 @@ module Liquid
|
|
205
206
|
end
|
206
207
|
|
207
208
|
def lookup_and_evaluate(obj, key)
|
208
|
-
if
|
209
|
+
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
|
210
|
+
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
211
|
+
end
|
212
|
+
|
213
|
+
value = obj[key]
|
214
|
+
|
215
|
+
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
209
216
|
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
210
217
|
else
|
211
218
|
value
|
data/lib/liquid/drop.rb
CHANGED
@@ -24,8 +24,9 @@ module Liquid
|
|
24
24
|
attr_writer :context
|
25
25
|
|
26
26
|
# Catch all for the method
|
27
|
-
def liquid_method_missing(
|
28
|
-
nil
|
27
|
+
def liquid_method_missing(method)
|
28
|
+
return nil unless @context && @context.strict_variables
|
29
|
+
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
29
30
|
end
|
30
31
|
|
31
32
|
# called by liquid to invoke a drop
|
@@ -61,16 +62,17 @@ module Liquid
|
|
61
62
|
end
|
62
63
|
|
63
64
|
def self.invokable_methods
|
64
|
-
|
65
|
+
@invokable_methods ||= begin
|
65
66
|
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
67
|
+
|
66
68
|
if include?(Enumerable)
|
67
69
|
blacklist += Enumerable.public_instance_methods
|
68
70
|
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
69
71
|
end
|
72
|
+
|
70
73
|
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
71
|
-
|
74
|
+
Set.new(whitelist.map(&:to_s))
|
72
75
|
end
|
73
|
-
@invokable_methods
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|
data/lib/liquid/errors.rb
CHANGED
@@ -56,4 +56,8 @@ module Liquid
|
|
56
56
|
MemoryError = Class.new(Error)
|
57
57
|
ZeroDivisionError = Class.new(Error)
|
58
58
|
FloatDomainError = Class.new(Error)
|
59
|
+
UndefinedVariable = Class.new(Error)
|
60
|
+
UndefinedDropMethod = Class.new(Error)
|
61
|
+
UndefinedFilter = Class.new(Error)
|
62
|
+
MethodOverrideError = Class.new(Error)
|
59
63
|
end
|
data/lib/liquid/file_system.rb
CHANGED
@@ -65,7 +65,7 @@ module Liquid
|
|
65
65
|
File.join(root, @pattern % template_path)
|
66
66
|
end
|
67
67
|
|
68
|
-
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path)
|
68
|
+
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
69
69
|
|
70
70
|
full_path
|
71
71
|
end
|
data/lib/liquid/parse_context.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Liquid
|
2
2
|
class ParseContext
|
3
|
-
attr_accessor :
|
4
|
-
attr_reader :warnings, :error_mode
|
3
|
+
attr_accessor :locale, :line_number
|
4
|
+
attr_reader :partial, :warnings, :error_mode
|
5
5
|
|
6
6
|
def initialize(options = {})
|
7
7
|
@template_options = options ? options.dup : {}
|
@@ -125,8 +125,6 @@ module Liquid
|
|
125
125
|
[]
|
126
126
|
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
127
127
|
ary.sort { |a, b| a[property] <=> b[property] }
|
128
|
-
elsif ary.first.respond_to?(property)
|
129
|
-
ary.sort { |a, b| a.send(property) <=> b.send(property) }
|
130
128
|
end
|
131
129
|
end
|
132
130
|
|
@@ -141,8 +139,6 @@ module Liquid
|
|
141
139
|
[]
|
142
140
|
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
|
143
141
|
ary.sort { |a, b| a[property].casecmp(b[property]) }
|
144
|
-
elsif ary.first.respond_to?(property)
|
145
|
-
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
|
146
142
|
end
|
147
143
|
end
|
148
144
|
|
@@ -191,8 +187,6 @@ module Liquid
|
|
191
187
|
[]
|
192
188
|
elsif ary.first.respond_to?(:[])
|
193
189
|
ary.reject{ |a| a[property].nil? }
|
194
|
-
elsif ary.first.respond_to?(property)
|
195
|
-
ary.reject { |a| a.send(property).nil? }
|
196
190
|
end
|
197
191
|
end
|
198
192
|
|
@@ -222,6 +216,9 @@ module Liquid
|
|
222
216
|
end
|
223
217
|
|
224
218
|
def concat(input, array)
|
219
|
+
unless array.respond_to?(:to_ary)
|
220
|
+
raise ArgumentError.new("concat filter requires an array argument")
|
221
|
+
end
|
225
222
|
InputIterator.new(input).concat(array)
|
226
223
|
end
|
227
224
|
|
@@ -341,9 +338,12 @@ module Liquid
|
|
341
338
|
raise Liquid::FloatDomainError, e.message
|
342
339
|
end
|
343
340
|
|
344
|
-
def default(input, default_value =
|
345
|
-
|
346
|
-
|
341
|
+
def default(input, default_value = ''.freeze)
|
342
|
+
if !input || input.respond_to?(:empty?) && input.empty?
|
343
|
+
default_value
|
344
|
+
else
|
345
|
+
input
|
346
|
+
end
|
347
347
|
end
|
348
348
|
|
349
349
|
private
|
@@ -373,7 +373,7 @@ module Liquid
|
|
373
373
|
end
|
374
374
|
|
375
375
|
def concat(args)
|
376
|
-
to_a.concat
|
376
|
+
to_a.concat(args)
|
377
377
|
end
|
378
378
|
|
379
379
|
def reverse
|
data/lib/liquid/strainer.rb
CHANGED
@@ -26,10 +26,15 @@ module Liquid
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def self.add_filter(filter)
|
29
|
-
raise ArgumentError, "Expected module but got: #{
|
29
|
+
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
30
30
|
unless self.class.include?(filter)
|
31
|
-
|
32
|
-
|
31
|
+
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
32
|
+
if invokable_non_public_methods.any?
|
33
|
+
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
34
|
+
else
|
35
|
+
send(:include, filter)
|
36
|
+
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
37
|
+
end
|
33
38
|
end
|
34
39
|
end
|
35
40
|
|
@@ -48,6 +53,8 @@ module Liquid
|
|
48
53
|
def invoke(method, *args)
|
49
54
|
if self.class.invokable?(method)
|
50
55
|
send(method, *args)
|
56
|
+
elsif @context && @context.strict_filters
|
57
|
+
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
51
58
|
else
|
52
59
|
args.first
|
53
60
|
end
|
data/lib/liquid/tags/assign.rb
CHANGED
@@ -23,16 +23,28 @@ module Liquid
|
|
23
23
|
def render(context)
|
24
24
|
val = @from.render(context)
|
25
25
|
context.scopes.last[@to] = val
|
26
|
-
|
27
|
-
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
|
28
|
-
context.resource_limits.assign_score += inc
|
29
|
-
|
26
|
+
context.resource_limits.assign_score += assign_score_of(val)
|
30
27
|
''.freeze
|
31
28
|
end
|
32
29
|
|
33
30
|
def blank?
|
34
31
|
true
|
35
32
|
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def assign_score_of(val)
|
37
|
+
if val.instance_of?(String)
|
38
|
+
val.length
|
39
|
+
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
40
|
+
sum = 1
|
41
|
+
# Uses #each to avoid extra allocations.
|
42
|
+
val.each { |child| sum += assign_score_of(child) }
|
43
|
+
sum
|
44
|
+
else
|
45
|
+
1
|
46
|
+
end
|
47
|
+
end
|
36
48
|
end
|
37
49
|
|
38
50
|
Template.register_tag('assign'.freeze, Assign)
|
data/lib/liquid/tags/for.rb
CHANGED
data/lib/liquid/template.rb
CHANGED
@@ -20,7 +20,7 @@ module Liquid
|
|
20
20
|
|
21
21
|
class TagRegistry
|
22
22
|
def initialize
|
23
|
-
@tags
|
23
|
+
@tags = {}
|
24
24
|
@cache = {}
|
25
25
|
end
|
26
26
|
|
@@ -80,11 +80,11 @@ module Liquid
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def error_mode
|
83
|
-
@error_mode
|
83
|
+
@error_mode ||= :lax
|
84
84
|
end
|
85
85
|
|
86
86
|
def taint_mode
|
87
|
-
@taint_mode
|
87
|
+
@taint_mode ||= :lax
|
88
88
|
end
|
89
89
|
|
90
90
|
# Pass a module with filter methods which should be available
|
@@ -107,6 +107,7 @@ module Liquid
|
|
107
107
|
end
|
108
108
|
|
109
109
|
def initialize
|
110
|
+
@rethrow_errors = false
|
110
111
|
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
111
112
|
end
|
112
113
|
|
@@ -181,12 +182,7 @@ module Liquid
|
|
181
182
|
|
182
183
|
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
183
184
|
|
184
|
-
context
|
185
|
-
|
186
|
-
context.global_filter = options[:global_filter] if options[:global_filter]
|
187
|
-
|
188
|
-
context.exception_handler = options[:exception_handler] if options[:exception_handler]
|
189
|
-
|
185
|
+
apply_options_to_context(context, options)
|
190
186
|
when Module, Array
|
191
187
|
context.add_filters(args.pop)
|
192
188
|
end
|
@@ -235,5 +231,13 @@ module Liquid
|
|
235
231
|
yield
|
236
232
|
end
|
237
233
|
end
|
234
|
+
|
235
|
+
def apply_options_to_context(context, options)
|
236
|
+
context.add_filters(options[:filters]) if options[:filters]
|
237
|
+
context.global_filter = options[:global_filter] if options[:global_filter]
|
238
|
+
context.exception_handler = options[:exception_handler] if options[:exception_handler]
|
239
|
+
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
240
|
+
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
241
|
+
end
|
238
242
|
end
|
239
243
|
end
|
data/lib/liquid/tokenizer.rb
CHANGED
data/lib/liquid/utils.rb
CHANGED
@@ -50,9 +50,13 @@ module Liquid
|
|
50
50
|
when Numeric
|
51
51
|
obj
|
52
52
|
when String
|
53
|
-
(obj.strip =~ /\A
|
53
|
+
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
54
54
|
else
|
55
|
-
|
55
|
+
if obj.respond_to?(:to_number)
|
56
|
+
obj.to_number
|
57
|
+
else
|
58
|
+
0
|
59
|
+
end
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
@@ -55,9 +55,11 @@ module Liquid
|
|
55
55
|
object = object.send(key).to_liquid
|
56
56
|
|
57
57
|
# No key was present with the desired value and it wasn't one of the directly supported
|
58
|
-
# keywords either. The only thing we got left is to return nil
|
58
|
+
# keywords either. The only thing we got left is to return nil or
|
59
|
+
# raise an exception if `strict_variables` option is set to true
|
59
60
|
else
|
60
|
-
return nil
|
61
|
+
return nil unless context.strict_variables
|
62
|
+
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
61
63
|
end
|
62
64
|
|
63
65
|
# If we are dealing with a drop here we have to
|
data/lib/liquid/version.rb
CHANGED
@@ -94,7 +94,7 @@ class ErrorHandlingTest < Minitest::Test
|
|
94
94
|
)
|
95
95
|
end
|
96
96
|
|
97
|
-
assert_match
|
97
|
+
assert_match(/Liquid syntax error \(line 4\)/, err.message)
|
98
98
|
end
|
99
99
|
|
100
100
|
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
|
@@ -1,18 +1,18 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class HashOrderingTest < Minitest::Test
|
4
|
+
module MoneyFilter
|
5
|
+
def money(input)
|
6
|
+
sprintf(' %d$ ', input)
|
7
|
+
end
|
6
8
|
end
|
7
|
-
end
|
8
9
|
|
9
|
-
module CanadianMoneyFilter
|
10
|
-
|
11
|
-
|
10
|
+
module CanadianMoneyFilter
|
11
|
+
def money(input)
|
12
|
+
sprintf(' %d$ CAD ', input)
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
|
15
|
-
class HashOrderingTest < Minitest::Test
|
16
16
|
include Liquid
|
17
17
|
|
18
18
|
def test_global_register_order
|
@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
class NumberLikeThing < Liquid::Drop
|
45
|
+
def initialize(amount)
|
46
|
+
@amount = amount
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_number
|
50
|
+
@amount
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
44
54
|
class StandardFiltersTest < Minitest::Test
|
45
55
|
include Liquid
|
46
56
|
|
@@ -364,20 +374,25 @@ class StandardFiltersTest < Minitest::Test
|
|
364
374
|
def test_plus
|
365
375
|
assert_template_result "2", "{{ 1 | plus:1 }}"
|
366
376
|
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
|
377
|
+
|
378
|
+
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
|
367
379
|
end
|
368
380
|
|
369
381
|
def test_minus
|
370
382
|
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
|
371
383
|
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
|
384
|
+
|
385
|
+
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
|
372
386
|
end
|
373
387
|
|
374
388
|
def test_times
|
375
389
|
assert_template_result "12", "{{ 3 | times:4 }}"
|
376
390
|
assert_template_result "0", "{{ 'foo' | times:4 }}"
|
377
|
-
|
378
391
|
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
|
379
|
-
|
380
392
|
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
|
393
|
+
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
|
394
|
+
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
|
395
|
+
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
|
381
396
|
end
|
382
397
|
|
383
398
|
def test_divided_by
|
@@ -391,6 +406,8 @@ class StandardFiltersTest < Minitest::Test
|
|
391
406
|
assert_raises(Liquid::ZeroDivisionError) do
|
392
407
|
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
393
408
|
end
|
409
|
+
|
410
|
+
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
|
394
411
|
end
|
395
412
|
|
396
413
|
def test_modulo
|
@@ -398,6 +415,8 @@ class StandardFiltersTest < Minitest::Test
|
|
398
415
|
assert_raises(Liquid::ZeroDivisionError) do
|
399
416
|
assert_template_result "4", "{{ 1 | modulo: 0 }}"
|
400
417
|
end
|
418
|
+
|
419
|
+
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
|
401
420
|
end
|
402
421
|
|
403
422
|
def test_round
|
@@ -407,6 +426,9 @@ class StandardFiltersTest < Minitest::Test
|
|
407
426
|
assert_raises(Liquid::FloatDomainError) do
|
408
427
|
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
|
409
428
|
end
|
429
|
+
|
430
|
+
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
|
431
|
+
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
|
410
432
|
end
|
411
433
|
|
412
434
|
def test_ceil
|
@@ -415,6 +437,8 @@ class StandardFiltersTest < Minitest::Test
|
|
415
437
|
assert_raises(Liquid::FloatDomainError) do
|
416
438
|
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
|
417
439
|
end
|
440
|
+
|
441
|
+
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
|
418
442
|
end
|
419
443
|
|
420
444
|
def test_floor
|
@@ -423,6 +447,8 @@ class StandardFiltersTest < Minitest::Test
|
|
423
447
|
assert_raises(Liquid::FloatDomainError) do
|
424
448
|
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
|
425
449
|
end
|
450
|
+
|
451
|
+
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
|
426
452
|
end
|
427
453
|
|
428
454
|
def test_append
|
@@ -436,8 +462,7 @@ class StandardFiltersTest < Minitest::Test
|
|
436
462
|
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
|
437
463
|
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
|
438
464
|
|
439
|
-
assert_raises(
|
440
|
-
# no implicit conversion of Fixnum into Array
|
465
|
+
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
|
441
466
|
@filters.concat([1, 2], 10)
|
442
467
|
end
|
443
468
|
end
|
@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_invalid_raw
|
27
|
-
assert_match_syntax_error
|
28
|
-
assert_match_syntax_error
|
29
|
-
assert_match_syntax_error
|
27
|
+
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
|
28
|
+
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
|
29
|
+
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
|
30
30
|
end
|
31
31
|
end
|
@@ -27,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
class DropWithUndefinedMethod < Liquid::Drop
|
31
|
+
def foo
|
32
|
+
'foo'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
30
36
|
class TemplateTest < Minitest::Test
|
31
37
|
include Liquid
|
32
38
|
|
@@ -133,6 +139,17 @@ class TemplateTest < Minitest::Test
|
|
133
139
|
refute_nil t.resource_limits.assign_score
|
134
140
|
end
|
135
141
|
|
142
|
+
def test_resource_limits_assign_score_nested
|
143
|
+
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
|
144
|
+
|
145
|
+
t.resource_limits.assign_score_limit = 3
|
146
|
+
assert_equal "Liquid error: Memory limits exceeded", t.render
|
147
|
+
assert t.resource_limits.reached?
|
148
|
+
|
149
|
+
t.resource_limits.assign_score_limit = 5
|
150
|
+
assert_equal "", t.render!
|
151
|
+
end
|
152
|
+
|
136
153
|
def test_resource_limits_aborts_rendering_after_first_error
|
137
154
|
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
|
138
155
|
t.resource_limits.render_score_limit = 50
|
@@ -225,4 +242,68 @@ class TemplateTest < Minitest::Test
|
|
225
242
|
|
226
243
|
assert_equal 'BOB filtered', rendered_template
|
227
244
|
end
|
245
|
+
|
246
|
+
def test_undefined_variables
|
247
|
+
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
|
248
|
+
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
|
249
|
+
|
250
|
+
assert_equal '33 32 ', result
|
251
|
+
assert_equal 3, t.errors.count
|
252
|
+
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
|
253
|
+
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
|
254
|
+
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
|
255
|
+
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
|
256
|
+
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
|
257
|
+
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
|
258
|
+
end
|
259
|
+
|
260
|
+
def test_undefined_variables_raise
|
261
|
+
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
|
262
|
+
|
263
|
+
assert_raises UndefinedVariable do
|
264
|
+
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_undefined_drop_methods
|
269
|
+
d = DropWithUndefinedMethod.new
|
270
|
+
t = Template.new.parse('{{ foo }} {{ woot }}')
|
271
|
+
result = t.render(d, { strict_variables: true })
|
272
|
+
|
273
|
+
assert_equal 'foo ', result
|
274
|
+
assert_equal 1, t.errors.count
|
275
|
+
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
|
276
|
+
end
|
277
|
+
|
278
|
+
def test_undefined_drop_methods_raise
|
279
|
+
d = DropWithUndefinedMethod.new
|
280
|
+
t = Template.new.parse('{{ foo }} {{ woot }}')
|
281
|
+
|
282
|
+
assert_raises UndefinedDropMethod do
|
283
|
+
t.render!(d, { strict_variables: true })
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_undefined_filters
|
288
|
+
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
|
289
|
+
filters = Module.new do
|
290
|
+
def somefilter3(v)
|
291
|
+
"-#{v}-"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
|
295
|
+
|
296
|
+
assert_equal '123 ', result
|
297
|
+
assert_equal 1, t.errors.count
|
298
|
+
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
|
299
|
+
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
|
300
|
+
end
|
301
|
+
|
302
|
+
def test_undefined_filters_raise
|
303
|
+
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
|
304
|
+
|
305
|
+
assert_raises UndefinedFilter do
|
306
|
+
t.render!({ 'x' => 'foo' }, { strict_filters: true })
|
307
|
+
end
|
308
|
+
end
|
228
309
|
end
|
@@ -3,50 +3,54 @@ require 'test_helper'
|
|
3
3
|
class ConditionUnitTest < Minitest::Test
|
4
4
|
include Liquid
|
5
5
|
|
6
|
+
def setup
|
7
|
+
@context = Liquid::Context.new
|
8
|
+
end
|
9
|
+
|
6
10
|
def test_basic_condition
|
7
11
|
assert_equal false, Condition.new(1, '==', 2).evaluate
|
8
12
|
assert_equal true, Condition.new(1, '==', 1).evaluate
|
9
13
|
end
|
10
14
|
|
11
15
|
def test_default_operators_evalute_true
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
assert_evaluates_true 1, '==', 1
|
17
|
+
assert_evaluates_true 1, '!=', 2
|
18
|
+
assert_evaluates_true 1, '<>', 2
|
19
|
+
assert_evaluates_true 1, '<', 2
|
20
|
+
assert_evaluates_true 2, '>', 1
|
21
|
+
assert_evaluates_true 1, '>=', 1
|
22
|
+
assert_evaluates_true 2, '>=', 1
|
23
|
+
assert_evaluates_true 1, '<=', 2
|
24
|
+
assert_evaluates_true 1, '<=', 1
|
21
25
|
# negative numbers
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
+
assert_evaluates_true 1, '>', -1
|
27
|
+
assert_evaluates_true (-1), '<', 1
|
28
|
+
assert_evaluates_true 1.0, '>', -1.0
|
29
|
+
assert_evaluates_true (-1.0), '<', 1.0
|
26
30
|
end
|
27
31
|
|
28
32
|
def test_default_operators_evalute_false
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
assert_evaluates_false 1, '==', 2
|
34
|
+
assert_evaluates_false 1, '!=', 1
|
35
|
+
assert_evaluates_false 1, '<>', 1
|
36
|
+
assert_evaluates_false 1, '<', 0
|
37
|
+
assert_evaluates_false 2, '>', 4
|
38
|
+
assert_evaluates_false 1, '>=', 3
|
39
|
+
assert_evaluates_false 2, '>=', 4
|
40
|
+
assert_evaluates_false 1, '<=', 0
|
41
|
+
assert_evaluates_false 1, '<=', 0
|
38
42
|
end
|
39
43
|
|
40
44
|
def test_contains_works_on_strings
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
45
|
+
assert_evaluates_true 'bob', 'contains', 'o'
|
46
|
+
assert_evaluates_true 'bob', 'contains', 'b'
|
47
|
+
assert_evaluates_true 'bob', 'contains', 'bo'
|
48
|
+
assert_evaluates_true 'bob', 'contains', 'ob'
|
49
|
+
assert_evaluates_true 'bob', 'contains', 'bob'
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
assert_evaluates_false 'bob', 'contains', 'bob2'
|
52
|
+
assert_evaluates_false 'bob', 'contains', 'a'
|
53
|
+
assert_evaluates_false 'bob', 'contains', '---'
|
50
54
|
end
|
51
55
|
|
52
56
|
def test_invalid_comparation_operator
|
@@ -65,29 +69,29 @@ class ConditionUnitTest < Minitest::Test
|
|
65
69
|
@context['array'] = [1, 2, 3, 4, 5]
|
66
70
|
array_expr = VariableLookup.new("array")
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
assert_evaluates_false array_expr, 'contains', 0
|
73
|
+
assert_evaluates_true array_expr, 'contains', 1
|
74
|
+
assert_evaluates_true array_expr, 'contains', 2
|
75
|
+
assert_evaluates_true array_expr, 'contains', 3
|
76
|
+
assert_evaluates_true array_expr, 'contains', 4
|
77
|
+
assert_evaluates_true array_expr, 'contains', 5
|
78
|
+
assert_evaluates_false array_expr, 'contains', 6
|
79
|
+
assert_evaluates_false array_expr, 'contains', "1"
|
76
80
|
end
|
77
81
|
|
78
82
|
def test_contains_returns_false_for_nil_operands
|
79
83
|
@context = Liquid::Context.new
|
80
|
-
|
81
|
-
|
84
|
+
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
|
85
|
+
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
|
82
86
|
end
|
83
87
|
|
84
88
|
def test_contains_return_false_on_wrong_data_type
|
85
|
-
|
89
|
+
assert_evaluates_false 1, 'contains', 0
|
86
90
|
end
|
87
91
|
|
88
92
|
def test_contains_with_string_left_operand_coerces_right_operand_to_string
|
89
|
-
|
90
|
-
|
93
|
+
assert_evaluates_true ' 1 ', 'contains', 1
|
94
|
+
assert_evaluates_false ' 1 ', 'contains', 2
|
91
95
|
end
|
92
96
|
|
93
97
|
def test_or_condition
|
@@ -121,8 +125,8 @@ class ConditionUnitTest < Minitest::Test
|
|
121
125
|
def test_should_allow_custom_proc_operator
|
122
126
|
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
|
123
127
|
|
124
|
-
|
125
|
-
|
128
|
+
assert_evaluates_true 'bob', 'starts_with', 'b'
|
129
|
+
assert_evaluates_false 'bob', 'starts_with', 'o'
|
126
130
|
ensure
|
127
131
|
Condition.operators.delete 'starts_with'
|
128
132
|
end
|
@@ -131,24 +135,24 @@ class ConditionUnitTest < Minitest::Test
|
|
131
135
|
@context = Liquid::Context.new
|
132
136
|
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
|
133
137
|
|
134
|
-
|
138
|
+
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
|
135
139
|
end
|
136
140
|
|
137
141
|
private
|
138
142
|
|
139
|
-
def
|
140
|
-
assert Condition.new(left, op, right).evaluate(@context
|
143
|
+
def assert_evaluates_true(left, op, right)
|
144
|
+
assert Condition.new(left, op, right).evaluate(@context),
|
141
145
|
"Evaluated false: #{left} #{op} #{right}"
|
142
146
|
end
|
143
147
|
|
144
|
-
def
|
145
|
-
assert !Condition.new(left, op, right).evaluate(@context
|
148
|
+
def assert_evaluates_false(left, op, right)
|
149
|
+
assert !Condition.new(left, op, right).evaluate(@context),
|
146
150
|
"Evaluated true: #{left} #{op} #{right}"
|
147
151
|
end
|
148
152
|
|
149
153
|
def assert_evaluates_argument_error(left, op, right)
|
150
154
|
assert_raises(Liquid::ArgumentError) do
|
151
|
-
Condition.new(left, op, right).evaluate(@context
|
155
|
+
Condition.new(left, op, right).evaluate(@context)
|
152
156
|
end
|
153
157
|
end
|
154
158
|
end # ConditionTest
|
@@ -77,4 +77,60 @@ class StrainerUnitTest < Minitest::Test
|
|
77
77
|
assert_kind_of b, strainer
|
78
78
|
assert_kind_of Liquid::StandardFilters, strainer
|
79
79
|
end
|
80
|
+
|
81
|
+
def test_add_filter_when_wrong_filter_class
|
82
|
+
c = Context.new
|
83
|
+
s = c.strainer
|
84
|
+
wrong_filter = ->(v) { v.reverse }
|
85
|
+
|
86
|
+
assert_raises ArgumentError do
|
87
|
+
s.class.add_filter(wrong_filter)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module PrivateMethodOverrideFilter
|
92
|
+
private
|
93
|
+
|
94
|
+
def public_filter
|
95
|
+
"overriden as private"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
|
100
|
+
strainer = Context.new.strainer
|
101
|
+
|
102
|
+
error = assert_raises(Liquid::MethodOverrideError) do
|
103
|
+
strainer.class.add_filter(PrivateMethodOverrideFilter)
|
104
|
+
end
|
105
|
+
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
|
106
|
+
end
|
107
|
+
|
108
|
+
module ProtectedMethodOverrideFilter
|
109
|
+
protected
|
110
|
+
|
111
|
+
def public_filter
|
112
|
+
"overriden as protected"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
|
117
|
+
strainer = Context.new.strainer
|
118
|
+
|
119
|
+
error = assert_raises(Liquid::MethodOverrideError) do
|
120
|
+
strainer.class.add_filter(ProtectedMethodOverrideFilter)
|
121
|
+
end
|
122
|
+
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
|
123
|
+
end
|
124
|
+
|
125
|
+
module PublicMethodOverrideFilter
|
126
|
+
def public_filter
|
127
|
+
"public"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
|
132
|
+
strainer = Context.new.strainer
|
133
|
+
strainer.class.add_filter(PublicMethodOverrideFilter)
|
134
|
+
assert strainer.class.filter_methods.include?('public_filter')
|
135
|
+
end
|
80
136
|
end # StrainerTest
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: liquid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.0.
|
4
|
+
version: 4.0.0.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tobias Lütke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|