liquid 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)