liquid 4.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,13 @@
1
1
  module Liquid
2
2
  class ParseContext
3
- attr_accessor :locale, :line_number, :trim_whitespace
3
+ attr_accessor :locale, :line_number, :trim_whitespace, :depth
4
4
  attr_reader :partial, :warnings, :error_mode
5
5
 
6
6
  def initialize(options = {})
7
7
  @template_options = options ? options.dup : {}
8
8
  @locale = @template_options[:locale] ||= I18n.new
9
9
  @warnings = []
10
+ self.depth = 0
10
11
  self.partial = false
11
12
  end
12
13
 
@@ -1,13 +1,13 @@
1
1
  module Liquid
2
2
  class BlockBody
3
- def render_node_with_profiling(node, context)
3
+ def render_node_with_profiling(node, output, context, skip_output = false)
4
4
  Profiler.profile_node_render(node) do
5
- render_node_without_profiling(node, context)
5
+ render_node_without_profiling(node, output, context, skip_output)
6
6
  end
7
7
  end
8
8
 
9
- alias_method :render_node_without_profiling, :render_node
10
- alias_method :render_node, :render_node_with_profiling
9
+ alias_method :render_node_without_profiling, :render_node_to_output
10
+ alias_method :render_node_to_output, :render_node_with_profiling
11
11
  end
12
12
 
13
13
  class Include < Tag
@@ -33,7 +33,7 @@ module Liquid
33
33
  end
34
34
 
35
35
  def escape(input)
36
- CGI.escapeHTML(input).untaint unless input.nil?
36
+ CGI.escapeHTML(input.to_s).untaint unless input.nil?
37
37
  end
38
38
  alias_method :h, :escape
39
39
 
@@ -42,11 +42,11 @@ module Liquid
42
42
  end
43
43
 
44
44
  def url_encode(input)
45
- CGI.escape(input) unless input.nil?
45
+ CGI.escape(input.to_s) unless input.nil?
46
46
  end
47
47
 
48
48
  def url_decode(input)
49
- CGI.unescape(input) unless input.nil?
49
+ CGI.unescape(input.to_s) unless input.nil?
50
50
  end
51
51
 
52
52
  def slice(input, offset, length = nil)
@@ -121,17 +121,23 @@ module Liquid
121
121
  def sort(input, property = nil)
122
122
  ary = InputIterator.new(input)
123
123
  if property.nil?
124
- ary.sort
124
+ ary.sort do |a, b|
125
+ if !a.nil? && !b.nil?
126
+ a <=> b
127
+ else
128
+ a.nil? ? 1 : -1
129
+ end
130
+ end
125
131
  elsif ary.empty? # The next two cases assume a non-empty array.
126
132
  []
127
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
133
+ elsif ary.all? { |el| el.respond_to?(:[]) }
128
134
  ary.sort do |a, b|
129
135
  a = a[property]
130
136
  b = b[property]
131
- if a && b
137
+ if !a.nil? && !b.nil?
132
138
  a <=> b
133
139
  else
134
- a ? -1 : 1
140
+ a.nil? ? 1 : -1
135
141
  end
136
142
  end
137
143
  end
@@ -143,11 +149,25 @@ module Liquid
143
149
  ary = InputIterator.new(input)
144
150
 
145
151
  if property.nil?
146
- ary.sort { |a, b| a.casecmp(b) }
152
+ ary.sort do |a, b|
153
+ if !a.nil? && !b.nil?
154
+ a.to_s.casecmp(b.to_s)
155
+ else
156
+ a.nil? ? 1 : -1
157
+ end
158
+ end
147
159
  elsif ary.empty? # The next two cases assume a non-empty array.
148
160
  []
149
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
150
- ary.sort { |a, b| a[property].casecmp(b[property]) }
161
+ elsif ary.all? { |el| el.respond_to?(:[]) }
162
+ ary.sort do |a, b|
163
+ a = a[property]
164
+ b = b[property]
165
+ if !a.nil? && !b.nil?
166
+ a.to_s.casecmp(b.to_s)
167
+ else
168
+ a.nil? ? 1 : -1
169
+ end
170
+ end
151
171
  end
152
172
  end
153
173
 
@@ -353,6 +373,22 @@ module Liquid
353
373
  raise Liquid::FloatDomainError, e.message
354
374
  end
355
375
 
376
+ def at_least(input, n)
377
+ min_value = Utils.to_number(n)
378
+
379
+ result = Utils.to_number(input)
380
+ result = min_value if min_value > result
381
+ result.is_a?(BigDecimal) ? result.to_f : result
382
+ end
383
+
384
+ def at_most(input, n)
385
+ max_value = Utils.to_number(n)
386
+
387
+ result = Utils.to_number(input)
388
+ result = max_value if max_value < result
389
+ result.is_a?(BigDecimal) ? result.to_f : result
390
+ end
391
+
356
392
  def default(input, default_value = ''.freeze)
357
393
  if !input || input.respond_to?(:empty?) && input.empty?
358
394
  default_value
@@ -384,7 +420,7 @@ module Liquid
384
420
  end
385
421
 
386
422
  def join(glue)
387
- to_a.join(glue)
423
+ to_a.join(glue.to_s)
388
424
  end
389
425
 
390
426
  def concat(args)
@@ -27,7 +27,7 @@ module Liquid
27
27
 
28
28
  def self.add_filter(filter)
29
29
  raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
30
- unless self.class.include?(filter)
30
+ unless self.include?(filter)
31
31
  invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
32
32
  if invokable_non_public_methods.any?
33
33
  raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -30,11 +30,11 @@ module Liquid
30
30
  end
31
31
 
32
32
  def render(context)
33
- context.registers[:cycle] ||= Hash.new(0)
33
+ context.registers[:cycle] ||= {}
34
34
 
35
35
  context.stack do
36
36
  key = context.evaluate(@name)
37
- iteration = context.registers[:cycle][key]
37
+ iteration = context.registers[:cycle][key].to_i
38
38
  result = context.evaluate(@variables[iteration])
39
39
  iteration += 1
40
40
  iteration = 0 if iteration >= @variables.size
@@ -23,7 +23,7 @@ module Liquid
23
23
  # {{ item.name }}
24
24
  # {% end %}
25
25
  #
26
- # To reverse the for loop simply use {% for item in collection reversed %}
26
+ # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
27
27
  #
28
28
  # == Available variables:
29
29
  #
@@ -46,6 +46,9 @@ module Liquid
46
46
  class For < Block
47
47
  Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
48
 
49
+ attr_reader :collection_name
50
+ attr_reader :variable_name
51
+
49
52
  def initialize(tag_name, markup, options)
50
53
  super
51
54
  @from = @limit = nil
@@ -117,7 +120,7 @@ module Liquid
117
120
  private
118
121
 
119
122
  def collection_segment(context)
120
- offsets = context.registers[:for] ||= Hash.new(0)
123
+ offsets = context.registers[:for] ||= {}
121
124
 
122
125
  from = if @from == :continue
123
126
  offsets[@name].to_i
@@ -153,7 +156,7 @@ module Liquid
153
156
  begin
154
157
  context['forloop'.freeze] = loop_vars
155
158
 
156
- segment.each_with_index do |item, index|
159
+ segment.each do |item|
157
160
  context[@variable_name] = item
158
161
  result << @for_block.render(context)
159
162
  loop_vars.send(:increment!)
@@ -83,17 +83,20 @@ module Liquid
83
83
 
84
84
  def strict_parse(markup)
85
85
  p = Parser.new(markup)
86
- condition = parse_binary_comparison(p)
86
+ condition = parse_binary_comparisons(p)
87
87
  p.consume(:end_of_string)
88
88
  condition
89
89
  end
90
90
 
91
- def parse_binary_comparison(p)
91
+ def parse_binary_comparisons(p)
92
92
  condition = parse_comparison(p)
93
- if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
94
- condition.send(op, parse_binary_comparison(p))
93
+ first_condition = condition
94
+ while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
95
+ child_condition = parse_comparison(p)
96
+ condition.send(op, child_condition)
97
+ condition = child_condition
95
98
  end
96
- condition
99
+ first_condition
97
100
  end
98
101
 
99
102
  def parse_comparison(p)
@@ -50,7 +50,7 @@ module Liquid
50
50
  variable = if @variable_name_expr
51
51
  context.evaluate(@variable_name_expr)
52
52
  else
53
- context.find_variable(template_name)
53
+ context.find_variable(template_name, raise_on_not_found: false)
54
54
  end
55
55
 
56
56
  old_template_name = context.template_name
@@ -33,7 +33,7 @@ module Liquid
33
33
  tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
34
34
  context['tablerowloop'.freeze] = tablerowloop
35
35
 
36
- collection.each_with_index do |item, index|
36
+ collection.each do |item|
37
37
  context[@variable_name] = item
38
38
 
39
39
  result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
@@ -46,11 +46,11 @@ module Liquid
46
46
  def self.to_number(obj)
47
47
  case obj
48
48
  when Float
49
- BigDecimal.new(obj.to_s)
49
+ BigDecimal(obj.to_s)
50
50
  when Numeric
51
51
  obj
52
52
  when String
53
- (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
53
+ (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
54
54
  else
55
55
  if obj.respond_to?(:to_number)
56
56
  obj.to_number
@@ -10,10 +10,16 @@ module Liquid
10
10
  # {{ user | link }}
11
11
  #
12
12
  class Variable
13
+ FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
13
14
  FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
15
+ FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
16
+ JustTagAttributes = /\A#{TagAttributes}\z/o
17
+ MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
18
+
14
19
  attr_accessor :filters, :name, :line_number
15
20
  attr_reader :parse_context
16
21
  alias_method :options, :parse_context
22
+
17
23
  include ParserSwitching
18
24
 
19
25
  def initialize(markup, parse_context)
@@ -35,17 +41,17 @@ module Liquid
35
41
 
36
42
  def lax_parse(markup)
37
43
  @filters = []
38
- return unless markup =~ /(#{QuotedFragment})(.*)/om
44
+ return unless markup =~ MarkupWithQuotedFragment
39
45
 
40
46
  name_markup = $1
41
47
  filter_markup = $2
42
48
  @name = Expression.parse(name_markup)
43
- if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
49
+ if filter_markup =~ FilterMarkupRegex
44
50
  filters = $1.scan(FilterParser)
45
51
  filters.each do |f|
46
52
  next unless f =~ /\w+/
47
53
  filtername = Regexp.last_match(0)
48
- filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
54
+ filterargs = f.scan(FilterArgsRegex).flatten
49
55
  @filters << parse_filter_expressions(filtername, filterargs)
50
56
  end
51
57
  end
@@ -91,7 +97,7 @@ module Liquid
91
97
  filter_args = []
92
98
  keyword_args = {}
93
99
  unparsed_args.each do |a|
94
- if matches = a.match(/\A#{TagAttributes}\z/o)
100
+ if matches = a.match(JustTagAttributes)
95
101
  keyword_args[matches[1]] = Expression.parse(matches[2])
96
102
  else
97
103
  filter_args << Expression.parse(a)
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Liquid
3
- VERSION = "4.0.0"
3
+ VERSION = "4.0.1"
4
4
  end
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+
3
+ class BlockTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_unexpected_end_tag
7
+ exc = assert_raises(SyntaxError) do
8
+ Template.parse("{% if true %}{% endunless %}")
9
+ end
10
+ assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
11
+ end
12
+ end
@@ -115,4 +115,8 @@ class ParsingQuirksTest < Minitest::Test
115
115
  assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
116
116
  end
117
117
  end
118
+
119
+ def test_contains_in_id
120
+ assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
121
+ end
118
122
  end # ParsingQuirksTest
@@ -63,4 +63,18 @@ class SecurityTest < Minitest::Test
63
63
 
64
64
  assert_equal [], (Symbol.all_symbols - current_symbols)
65
65
  end
66
+
67
+ def test_max_depth_nested_blocks_does_not_raise_exception
68
+ depth = Liquid::Block::MAX_DEPTH
69
+ code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
70
+ assert_equal "rendered", Template.parse(code).render!
71
+ end
72
+
73
+ def test_more_than_max_depth_nested_blocks_raises_exception
74
+ depth = Liquid::Block::MAX_DEPTH + 1
75
+ code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
76
+ assert_raises(Liquid::StackLevelError) do
77
+ Template.parse(code).render!
78
+ end
79
+ end
66
80
  end # SecurityTest
@@ -128,8 +128,16 @@ class StandardFiltersTest < Minitest::Test
128
128
 
129
129
  def test_escape
130
130
  assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
131
- assert_equal nil, @filters.escape(nil)
131
+ assert_equal '1', @filters.escape(1)
132
+ assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
133
+ assert_nil @filters.escape(nil)
134
+ end
135
+
136
+ def test_h
132
137
  assert_equal '&lt;strong&gt;', @filters.h('<strong>')
138
+ assert_equal '1', @filters.h(1)
139
+ assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
140
+ assert_nil @filters.h(nil)
133
141
  end
134
142
 
135
143
  def test_escape_once
@@ -138,14 +146,18 @@ class StandardFiltersTest < Minitest::Test
138
146
 
139
147
  def test_url_encode
140
148
  assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
141
- assert_equal nil, @filters.url_encode(nil)
149
+ assert_equal '1', @filters.url_encode(1)
150
+ assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
151
+ assert_nil @filters.url_encode(nil)
142
152
  end
143
153
 
144
154
  def test_url_decode
145
155
  assert_equal 'foo bar', @filters.url_decode('foo+bar')
146
156
  assert_equal 'foo bar', @filters.url_decode('foo%20bar')
147
157
  assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
148
- assert_equal nil, @filters.url_decode(nil)
158
+ assert_equal '1', @filters.url_decode(1)
159
+ assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
160
+ assert_nil @filters.url_decode(nil)
149
161
  end
150
162
 
151
163
  def test_truncatewords
@@ -170,6 +182,7 @@ class StandardFiltersTest < Minitest::Test
170
182
  def test_join
171
183
  assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
172
184
  assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
185
+ assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
173
186
  end
174
187
 
175
188
  def test_sort
@@ -177,6 +190,11 @@ class StandardFiltersTest < Minitest::Test
177
190
  assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
178
191
  end
179
192
 
193
+ def test_sort_with_nils
194
+ assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
195
+ assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
196
+ end
197
+
180
198
  def test_sort_when_property_is_sometimes_missing_puts_nils_last
181
199
  input = [
182
200
  { "price" => 4, "handle" => "alpha" },
@@ -195,6 +213,57 @@ class StandardFiltersTest < Minitest::Test
195
213
  assert_equal expectation, @filters.sort(input, "price")
196
214
  end
197
215
 
216
+ def test_sort_natural
217
+ assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
218
+ assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
219
+ end
220
+
221
+ def test_sort_natural_with_nils
222
+ assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
223
+ assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
224
+ end
225
+
226
+ def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
227
+ input = [
228
+ { "price" => "4", "handle" => "alpha" },
229
+ { "handle" => "beta" },
230
+ { "price" => "1", "handle" => "gamma" },
231
+ { "handle" => "delta" },
232
+ { "price" => 2, "handle" => "epsilon" }
233
+ ]
234
+ expectation = [
235
+ { "price" => "1", "handle" => "gamma" },
236
+ { "price" => 2, "handle" => "epsilon" },
237
+ { "price" => "4", "handle" => "alpha" },
238
+ { "handle" => "delta" },
239
+ { "handle" => "beta" }
240
+ ]
241
+ assert_equal expectation, @filters.sort_natural(input, "price")
242
+ end
243
+
244
+ def test_sort_natural_case_check
245
+ input = [
246
+ { "key" => "X" },
247
+ { "key" => "Y" },
248
+ { "key" => "Z" },
249
+ { "fake" => "t" },
250
+ { "key" => "a" },
251
+ { "key" => "b" },
252
+ { "key" => "c" }
253
+ ]
254
+ expectation = [
255
+ { "key" => "a" },
256
+ { "key" => "b" },
257
+ { "key" => "c" },
258
+ { "key" => "X" },
259
+ { "key" => "Y" },
260
+ { "key" => "Z" },
261
+ { "fake" => "t" }
262
+ ]
263
+ assert_equal expectation, @filters.sort_natural(input, "key")
264
+ assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
265
+ end
266
+
198
267
  def test_sort_empty_array
199
268
  assert_equal [], @filters.sort([], "a")
200
269
  end
@@ -329,7 +398,7 @@ class StandardFiltersTest < Minitest::Test
329
398
  assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
330
399
  assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
331
400
 
332
- assert_equal nil, @filters.date(nil, "%B")
401
+ assert_nil @filters.date(nil, "%B")
333
402
 
334
403
  assert_equal '', @filters.date('', "%B")
335
404
 
@@ -342,8 +411,8 @@ class StandardFiltersTest < Minitest::Test
342
411
  def test_first_last
343
412
  assert_equal 1, @filters.first([1, 2, 3])
344
413
  assert_equal 3, @filters.last([1, 2, 3])
345
- assert_equal nil, @filters.first([])
346
- assert_equal nil, @filters.last([])
414
+ assert_nil @filters.first([])
415
+ assert_nil @filters.last([])
347
416
  end
348
417
 
349
418
  def test_replace
@@ -483,6 +552,28 @@ class StandardFiltersTest < Minitest::Test
483
552
  assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
484
553
  end
485
554
 
555
+ def test_at_most
556
+ assert_template_result "4", "{{ 5 | at_most:4 }}"
557
+ assert_template_result "5", "{{ 5 | at_most:5 }}"
558
+ assert_template_result "5", "{{ 5 | at_most:6 }}"
559
+
560
+ assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
561
+ assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
562
+ assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
563
+ assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
564
+ end
565
+
566
+ def test_at_least
567
+ assert_template_result "5", "{{ 5 | at_least:4 }}"
568
+ assert_template_result "5", "{{ 5 | at_least:5 }}"
569
+ assert_template_result "6", "{{ 5 | at_least:6 }}"
570
+
571
+ assert_template_result "5", "{{ 4.5 | at_least:5 }}"
572
+ assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
573
+ assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
574
+ assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
575
+ end
576
+
486
577
  def test_append
487
578
  assigns = { 'a' => 'bc', 'b' => 'd' }
488
579
  assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)