liquid 4.0.0.rc3 → 5.0.0

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.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +93 -2
  3. data/README.md +8 -0
  4. data/lib/liquid.rb +18 -5
  5. data/lib/liquid/block.rb +47 -20
  6. data/lib/liquid/block_body.rb +190 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +122 -76
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -25
  12. data/lib/liquid/expression.rb +30 -31
  13. data/lib/liquid/extensions.rb +8 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +35 -26
  19. data/lib/liquid/locales/en.yml +4 -2
  20. data/lib/liquid/parse_context.rb +17 -4
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +171 -57
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +32 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +41 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +35 -16
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +26 -0
  47. data/lib/liquid/tags/for.rb +79 -47
  48. data/lib/liquid/tags/if.rb +53 -30
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +42 -44
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +32 -20
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +60 -71
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +6 -4
  61. data/lib/liquid/variable.rb +55 -38
  62. data/lib/liquid/variable_lookup.rb +14 -6
  63. data/lib/liquid/version.rb +3 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +58 -0
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +608 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +90 -60
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +24 -8
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +41 -18
  79. data/test/integration/standard_filter_test.rb +523 -205
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +109 -53
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +83 -52
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +128 -121
  97. data/test/integration/trim_mode_test.rb +82 -44
  98. data/test/integration/variable_test.rb +46 -31
  99. data/test/test_helper.rb +75 -23
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +82 -72
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +12 -10
  105. data/test/unit/parse_tree_visitor_test.rb +247 -0
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +19 -17
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +83 -50
  121. data/lib/liquid/strainer.rb +0 -65
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -136
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class RangeLookup
3
5
  def self.parse(start_markup, end_markup)
4
6
  start_obj = Expression.parse(start_markup)
5
- end_obj = Expression.parse(end_markup)
7
+ end_obj = Expression.parse(end_markup)
6
8
  if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
7
9
  new(start_obj, end_obj)
8
10
  else
@@ -12,12 +14,12 @@ module Liquid
12
14
 
13
15
  def initialize(start_obj, end_obj)
14
16
  @start_obj = start_obj
15
- @end_obj = end_obj
17
+ @end_obj = end_obj
16
18
  end
17
19
 
18
20
  def evaluate(context)
19
21
  start_int = to_integer(context.evaluate(@start_obj))
20
- end_int = to_integer(context.evaluate(@end_obj))
22
+ end_int = to_integer(context.evaluate(@end_obj))
21
23
  start_int..end_int
22
24
  end
23
25
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Register
5
+ end
6
+ end
@@ -1,23 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class ResourceLimits
3
- attr_accessor :render_length, :render_score, :assign_score,
4
- :render_length_limit, :render_score_limit, :assign_score_limit
5
+ attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
6
+ attr_reader :render_score, :assign_score
5
7
 
6
8
  def initialize(limits)
7
9
  @render_length_limit = limits[:render_length_limit]
8
- @render_score_limit = limits[:render_score_limit]
9
- @assign_score_limit = limits[:assign_score_limit]
10
+ @render_score_limit = limits[:render_score_limit]
11
+ @assign_score_limit = limits[:assign_score_limit]
10
12
  reset
11
13
  end
12
14
 
15
+ def increment_render_score(amount)
16
+ @render_score += amount
17
+ raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
18
+ end
19
+
20
+ def increment_assign_score(amount)
21
+ @assign_score += amount
22
+ raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
23
+ end
24
+
25
+ # update either render_length or assign_score based on whether or not the writes are captured
26
+ def increment_write_score(output)
27
+ if (last_captured = @last_capture_length)
28
+ captured = output.bytesize
29
+ increment = captured - last_captured
30
+ @last_capture_length = captured
31
+ increment_assign_score(increment)
32
+ elsif @render_length_limit && output.bytesize > @render_length_limit
33
+ raise_limits_reached
34
+ end
35
+ end
36
+
37
+ def raise_limits_reached
38
+ @reached_limit = true
39
+ raise MemoryError, "Memory limits exceeded"
40
+ end
41
+
13
42
  def reached?
14
- (@render_length_limit && @render_length > @render_length_limit) ||
15
- (@render_score_limit && @render_score > @render_score_limit) ||
16
- (@assign_score_limit && @assign_score > @assign_score_limit)
43
+ @reached_limit
17
44
  end
18
45
 
19
46
  def reset
20
- @render_length = @render_score = @assign_score = 0
47
+ @reached_limit = false
48
+ @last_capture_length = nil
49
+ @render_score = @assign_score = 0
50
+ end
51
+
52
+ def with_capture
53
+ old_capture_length = @last_capture_length
54
+ begin
55
+ @last_capture_length = 0
56
+ yield
57
+ ensure
58
+ @last_capture_length = old_capture_length
59
+ end
21
60
  end
22
61
  end
23
62
  end
@@ -1,16 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
2
4
  require 'bigdecimal'
3
5
 
4
6
  module Liquid
5
7
  module StandardFilters
6
8
  HTML_ESCAPE = {
7
- '&'.freeze => '&'.freeze,
8
- '>'.freeze => '>'.freeze,
9
- '<'.freeze => '&lt;'.freeze,
10
- '"'.freeze => '&quot;'.freeze,
11
- "'".freeze => '&#39;'.freeze
12
- }
9
+ '&' => '&amp;',
10
+ '>' => '&gt;',
11
+ '<' => '&lt;',
12
+ '"' => '&quot;',
13
+ "'" => '&#39;',
14
+ }.freeze
13
15
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
16
+ STRIP_HTML_BLOCKS = Regexp.union(
17
+ %r{<script.*?</script>}m,
18
+ /<!--.*?-->/m,
19
+ %r{<style.*?</style>}m
20
+ )
21
+ STRIP_HTML_TAGS = /<.*?>/m
14
22
 
15
23
  # Return the size of an array or of an string
16
24
  def size(input)
@@ -33,7 +41,7 @@ module Liquid
33
41
  end
34
42
 
35
43
  def escape(input)
36
- CGI.escapeHTML(input).untaint unless input.nil?
44
+ CGI.escapeHTML(input.to_s) unless input.nil?
37
45
  end
38
46
  alias_method :h, :escape
39
47
 
@@ -42,11 +50,16 @@ module Liquid
42
50
  end
43
51
 
44
52
  def url_encode(input)
45
- CGI.escape(input) unless input.nil?
53
+ CGI.escape(input.to_s) unless input.nil?
46
54
  end
47
55
 
48
56
  def url_decode(input)
49
- CGI.unescape(input) unless input.nil?
57
+ return if input.nil?
58
+
59
+ result = CGI.unescape(input.to_s)
60
+ raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
61
+
62
+ result
50
63
  end
51
64
 
52
65
  def slice(input, offset, length = nil)
@@ -61,22 +74,28 @@ module Liquid
61
74
  end
62
75
 
63
76
  # Truncate a string down to x characters
64
- def truncate(input, length = 50, truncate_string = "...".freeze)
77
+ def truncate(input, length = 50, truncate_string = "...")
65
78
  return if input.nil?
66
79
  input_str = input.to_s
67
- length = Utils.to_integer(length)
68
- l = length - truncate_string.length
80
+ length = Utils.to_integer(length)
81
+
82
+ truncate_string_str = truncate_string.to_s
83
+
84
+ l = length - truncate_string_str.length
69
85
  l = 0 if l < 0
70
- input_str.length > length ? input_str[0...l] + truncate_string : input_str
86
+
87
+ input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
71
88
  end
72
89
 
73
- def truncatewords(input, words = 15, truncate_string = "...".freeze)
90
+ def truncatewords(input, words = 15, truncate_string = "...")
74
91
  return if input.nil?
75
92
  wordlist = input.to_s.split
76
- words = Utils.to_integer(words)
93
+ words = Utils.to_integer(words)
94
+
77
95
  l = words - 1
78
96
  l = 0 if l < 0
79
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
97
+
98
+ wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
80
99
  end
81
100
 
82
101
  # Split input string into an array of substrings separated by given pattern.
@@ -85,7 +104,7 @@ module Liquid
85
104
  # <div class="summary">{{ post | split '//' | first }}</div>
86
105
  #
87
106
  def split(input, pattern)
88
- input.to_s.split(pattern)
107
+ input.to_s.split(pattern.to_s)
89
108
  end
90
109
 
91
110
  def strip(input)
@@ -101,113 +120,160 @@ module Liquid
101
120
  end
102
121
 
103
122
  def strip_html(input)
104
- empty = ''.freeze
105
- input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
123
+ empty = ''
124
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
125
+ result.gsub!(STRIP_HTML_TAGS, empty)
126
+ result
106
127
  end
107
128
 
108
129
  # Remove all newlines from the string
109
130
  def strip_newlines(input)
110
- input.to_s.gsub(/\r?\n/, ''.freeze)
131
+ input.to_s.gsub(/\r?\n/, '')
111
132
  end
112
133
 
113
134
  # Join elements of the array with certain character between them
114
- def join(input, glue = ' '.freeze)
115
- InputIterator.new(input).join(glue)
135
+ def join(input, glue = ' ')
136
+ InputIterator.new(input, context).join(glue)
116
137
  end
117
138
 
118
139
  # Sort elements of the array
119
140
  # provide optional property with which to sort an array of hashes or drops
120
141
  def sort(input, property = nil)
121
- ary = InputIterator.new(input)
142
+ ary = InputIterator.new(input, context)
143
+
144
+ return [] if ary.empty?
145
+
122
146
  if property.nil?
123
- ary.sort
124
- elsif ary.empty? # The next two cases assume a non-empty array.
125
- []
126
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
127
- ary.sort { |a, b| a[property] <=> b[property] }
147
+ ary.sort do |a, b|
148
+ nil_safe_compare(a, b)
149
+ end
150
+ elsif ary.all? { |el| el.respond_to?(:[]) }
151
+ begin
152
+ ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
153
+ rescue TypeError
154
+ raise_property_error(property)
155
+ end
128
156
  end
129
157
  end
130
158
 
131
159
  # Sort elements of an array ignoring case if strings
132
160
  # provide optional property with which to sort an array of hashes or drops
133
161
  def sort_natural(input, property = nil)
134
- ary = InputIterator.new(input)
162
+ ary = InputIterator.new(input, context)
163
+
164
+ return [] if ary.empty?
135
165
 
136
166
  if property.nil?
137
- ary.sort { |a, b| a.casecmp(b) }
138
- elsif ary.empty? # The next two cases assume a non-empty array.
167
+ ary.sort do |a, b|
168
+ nil_safe_casecmp(a, b)
169
+ end
170
+ elsif ary.all? { |el| el.respond_to?(:[]) }
171
+ begin
172
+ ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
173
+ rescue TypeError
174
+ raise_property_error(property)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Filter the elements of an array to those with a certain property value.
180
+ # By default the target is any truthy value.
181
+ def where(input, property, target_value = nil)
182
+ ary = InputIterator.new(input, context)
183
+
184
+ if ary.empty?
139
185
  []
140
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
141
- ary.sort { |a, b| a[property].casecmp(b[property]) }
186
+ elsif ary.first.respond_to?(:[]) && target_value.nil?
187
+ begin
188
+ ary.select { |item| item[property] }
189
+ rescue TypeError
190
+ raise_property_error(property)
191
+ end
192
+ elsif ary.first.respond_to?(:[])
193
+ begin
194
+ ary.select { |item| item[property] == target_value }
195
+ rescue TypeError
196
+ raise_property_error(property)
197
+ end
142
198
  end
143
199
  end
144
200
 
145
201
  # Remove duplicate elements from an array
146
202
  # provide optional property with which to determine uniqueness
147
203
  def uniq(input, property = nil)
148
- ary = InputIterator.new(input)
204
+ ary = InputIterator.new(input, context)
149
205
 
150
206
  if property.nil?
151
207
  ary.uniq
152
208
  elsif ary.empty? # The next two cases assume a non-empty array.
153
209
  []
154
210
  elsif ary.first.respond_to?(:[])
155
- ary.uniq{ |a| a[property] }
211
+ begin
212
+ ary.uniq { |a| a[property] }
213
+ rescue TypeError
214
+ raise_property_error(property)
215
+ end
156
216
  end
157
217
  end
158
218
 
159
219
  # Reverse the elements of an array
160
220
  def reverse(input)
161
- ary = InputIterator.new(input)
221
+ ary = InputIterator.new(input, context)
162
222
  ary.reverse
163
223
  end
164
224
 
165
225
  # map/collect on a given property
166
226
  def map(input, property)
167
- InputIterator.new(input).map do |e|
227
+ InputIterator.new(input, context).map do |e|
168
228
  e = e.call if e.is_a?(Proc)
169
229
 
170
- if property == "to_liquid".freeze
230
+ if property == "to_liquid"
171
231
  e
172
232
  elsif e.respond_to?(:[])
173
233
  r = e[property]
174
234
  r.is_a?(Proc) ? r.call : r
175
235
  end
176
236
  end
237
+ rescue TypeError
238
+ raise_property_error(property)
177
239
  end
178
240
 
179
241
  # Remove nils within an array
180
242
  # provide optional property with which to check for nil
181
243
  def compact(input, property = nil)
182
- ary = InputIterator.new(input)
244
+ ary = InputIterator.new(input, context)
183
245
 
184
246
  if property.nil?
185
247
  ary.compact
186
248
  elsif ary.empty? # The next two cases assume a non-empty array.
187
249
  []
188
250
  elsif ary.first.respond_to?(:[])
189
- ary.reject{ |a| a[property].nil? }
251
+ begin
252
+ ary.reject { |a| a[property].nil? }
253
+ rescue TypeError
254
+ raise_property_error(property)
255
+ end
190
256
  end
191
257
  end
192
258
 
193
259
  # Replace occurrences of a string with another
194
- def replace(input, string, replacement = ''.freeze)
260
+ def replace(input, string, replacement = '')
195
261
  input.to_s.gsub(string.to_s, replacement.to_s)
196
262
  end
197
263
 
198
264
  # Replace the first occurrences of a string with another
199
- def replace_first(input, string, replacement = ''.freeze)
265
+ def replace_first(input, string, replacement = '')
200
266
  input.to_s.sub(string.to_s, replacement.to_s)
201
267
  end
202
268
 
203
269
  # remove a substring
204
270
  def remove(input, string)
205
- input.to_s.gsub(string.to_s, ''.freeze)
271
+ input.to_s.gsub(string.to_s, '')
206
272
  end
207
273
 
208
274
  # remove the first occurrences of a substring
209
275
  def remove_first(input, string)
210
- input.to_s.sub(string.to_s, ''.freeze)
276
+ input.to_s.sub(string.to_s, '')
211
277
  end
212
278
 
213
279
  # add one string to another
@@ -217,9 +283,9 @@ module Liquid
217
283
 
218
284
  def concat(input, array)
219
285
  unless array.respond_to?(:to_ary)
220
- raise ArgumentError.new("concat filter requires an array argument")
286
+ raise ArgumentError, "concat filter requires an array argument"
221
287
  end
222
- InputIterator.new(input).concat(array)
288
+ InputIterator.new(input, context).concat(array)
223
289
  end
224
290
 
225
291
  # prepend a string to another
@@ -229,7 +295,7 @@ module Liquid
229
295
 
230
296
  # Add <br /> tags in front of all newlines in input string
231
297
  def newline_to_br(input)
232
- input.to_s.gsub(/\n/, "<br />\n".freeze)
298
+ input.to_s.gsub(/\n/, "<br />\n")
233
299
  end
234
300
 
235
301
  # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -266,7 +332,7 @@ module Liquid
266
332
  def date(input, format)
267
333
  return input if format.to_s.empty?
268
334
 
269
- return input unless date = Utils.to_date(input)
335
+ return input unless (date = Utils.to_date(input))
270
336
 
271
337
  date.strftime(format.to_s)
272
338
  end
@@ -344,26 +410,73 @@ module Liquid
344
410
  raise Liquid::FloatDomainError, e.message
345
411
  end
346
412
 
347
- def default(input, default_value = ''.freeze)
348
- if !input || input.respond_to?(:empty?) && input.empty?
349
- default_value
350
- else
351
- input
352
- end
413
+ def at_least(input, n)
414
+ min_value = Utils.to_number(n)
415
+
416
+ result = Utils.to_number(input)
417
+ result = min_value if min_value > result
418
+ result.is_a?(BigDecimal) ? result.to_f : result
419
+ end
420
+
421
+ def at_most(input, n)
422
+ max_value = Utils.to_number(n)
423
+
424
+ result = Utils.to_number(input)
425
+ result = max_value if max_value < result
426
+ result.is_a?(BigDecimal) ? result.to_f : result
427
+ end
428
+
429
+ # Set a default value when the input is nil, false or empty
430
+ #
431
+ # Example:
432
+ # {{ product.title | default: "No Title" }}
433
+ #
434
+ # Use `allow_false` when an input should only be tested against nil or empty and not false.
435
+ #
436
+ # Example:
437
+ # {{ product.title | default: "No Title", allow_false: true }}
438
+ #
439
+ def default(input, default_value = '', options = {})
440
+ options = {} unless options.is_a?(Hash)
441
+ false_check = options['allow_false'] ? input.nil? : !input
442
+ false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
353
443
  end
354
444
 
355
445
  private
356
446
 
447
+ attr_reader :context
448
+
449
+ def raise_property_error(property)
450
+ raise Liquid::ArgumentError, "cannot select the property '#{property}'"
451
+ end
452
+
357
453
  def apply_operation(input, operand, operation)
358
454
  result = Utils.to_number(input).send(operation, Utils.to_number(operand))
359
455
  result.is_a?(BigDecimal) ? result.to_f : result
360
456
  end
361
457
 
458
+ def nil_safe_compare(a, b)
459
+ if !a.nil? && !b.nil?
460
+ a <=> b
461
+ else
462
+ a.nil? ? 1 : -1
463
+ end
464
+ end
465
+
466
+ def nil_safe_casecmp(a, b)
467
+ if !a.nil? && !b.nil?
468
+ a.to_s.casecmp(b.to_s)
469
+ else
470
+ a.nil? ? 1 : -1
471
+ end
472
+ end
473
+
362
474
  class InputIterator
363
475
  include Enumerable
364
476
 
365
- def initialize(input)
366
- @input = if input.is_a?(Array)
477
+ def initialize(input, context)
478
+ @context = context
479
+ @input = if input.is_a?(Array)
367
480
  input.flatten
368
481
  elsif input.is_a?(Hash)
369
482
  [input]
@@ -375,7 +488,7 @@ module Liquid
375
488
  end
376
489
 
377
490
  def join(glue)
378
- to_a.join(glue)
491
+ to_a.join(glue.to_s)
379
492
  end
380
493
 
381
494
  def concat(args)
@@ -401,6 +514,7 @@ module Liquid
401
514
 
402
515
  def each
403
516
  @input.each do |e|
517
+ e.context = @context if e.respond_to?(:context=)
404
518
  yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
405
519
  end
406
520
  end