liquid 4.0.0 → 5.10.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 (117) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +235 -2
  3. data/README.md +58 -8
  4. data/lib/liquid/block.rb +51 -20
  5. data/lib/liquid/block_body.rb +216 -82
  6. data/lib/liquid/condition.rb +83 -32
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +130 -59
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/document.rb +47 -9
  11. data/lib/liquid/drop.rb +8 -2
  12. data/lib/liquid/environment.rb +159 -0
  13. data/lib/liquid/errors.rb +23 -20
  14. data/lib/liquid/expression.rb +114 -31
  15. data/lib/liquid/extensions.rb +8 -0
  16. data/lib/liquid/file_system.rb +6 -4
  17. data/lib/liquid/forloop_drop.rb +51 -4
  18. data/lib/liquid/i18n.rb +5 -3
  19. data/lib/liquid/interrupts.rb +3 -1
  20. data/lib/liquid/lexer.rb +165 -39
  21. data/lib/liquid/locales/en.yml +16 -6
  22. data/lib/liquid/parse_context.rb +62 -7
  23. data/lib/liquid/parse_tree_visitor.rb +42 -0
  24. data/lib/liquid/parser.rb +31 -19
  25. data/lib/liquid/parser_switching.rb +42 -3
  26. data/lib/liquid/partial_cache.rb +33 -0
  27. data/lib/liquid/profiler/hooks.rb +26 -14
  28. data/lib/liquid/profiler.rb +67 -86
  29. data/lib/liquid/range_lookup.rb +26 -6
  30. data/lib/liquid/registers.rb +51 -0
  31. data/lib/liquid/resource_limits.rb +47 -8
  32. data/lib/liquid/snippet_drop.rb +22 -0
  33. data/lib/liquid/standardfilters.rb +813 -137
  34. data/lib/liquid/strainer_template.rb +62 -0
  35. data/lib/liquid/tablerowloop_drop.rb +64 -5
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +13 -0
  38. data/lib/liquid/tag.rb +42 -6
  39. data/lib/liquid/tags/assign.rb +46 -18
  40. data/lib/liquid/tags/break.rb +15 -4
  41. data/lib/liquid/tags/capture.rb +26 -18
  42. data/lib/liquid/tags/case.rb +108 -32
  43. data/lib/liquid/tags/comment.rb +76 -4
  44. data/lib/liquid/tags/continue.rb +15 -13
  45. data/lib/liquid/tags/cycle.rb +117 -34
  46. data/lib/liquid/tags/decrement.rb +30 -23
  47. data/lib/liquid/tags/doc.rb +81 -0
  48. data/lib/liquid/tags/echo.rb +39 -0
  49. data/lib/liquid/tags/for.rb +109 -96
  50. data/lib/liquid/tags/if.rb +72 -41
  51. data/lib/liquid/tags/ifchanged.rb +10 -11
  52. data/lib/liquid/tags/include.rb +89 -63
  53. data/lib/liquid/tags/increment.rb +31 -20
  54. data/lib/liquid/tags/inline_comment.rb +28 -0
  55. data/lib/liquid/tags/raw.rb +25 -13
  56. data/lib/liquid/tags/render.rb +151 -0
  57. data/lib/liquid/tags/snippet.rb +45 -0
  58. data/lib/liquid/tags/table_row.rb +104 -21
  59. data/lib/liquid/tags/unless.rb +37 -20
  60. data/lib/liquid/tags.rb +51 -0
  61. data/lib/liquid/template.rb +90 -106
  62. data/lib/liquid/template_factory.rb +9 -0
  63. data/lib/liquid/tokenizer.rb +143 -13
  64. data/lib/liquid/usage.rb +8 -0
  65. data/lib/liquid/utils.rb +114 -5
  66. data/lib/liquid/variable.rb +119 -45
  67. data/lib/liquid/variable_lookup.rb +35 -13
  68. data/lib/liquid/version.rb +3 -1
  69. data/lib/liquid.rb +31 -18
  70. metadata +56 -107
  71. data/lib/liquid/strainer.rb +0 -66
  72. data/test/fixtures/en_locale.yml +0 -9
  73. data/test/integration/assign_test.rb +0 -48
  74. data/test/integration/blank_test.rb +0 -106
  75. data/test/integration/capture_test.rb +0 -50
  76. data/test/integration/context_test.rb +0 -32
  77. data/test/integration/document_test.rb +0 -19
  78. data/test/integration/drop_test.rb +0 -273
  79. data/test/integration/error_handling_test.rb +0 -260
  80. data/test/integration/filter_test.rb +0 -178
  81. data/test/integration/hash_ordering_test.rb +0 -23
  82. data/test/integration/output_test.rb +0 -123
  83. data/test/integration/parsing_quirks_test.rb +0 -118
  84. data/test/integration/render_profiling_test.rb +0 -154
  85. data/test/integration/security_test.rb +0 -66
  86. data/test/integration/standard_filter_test.rb +0 -535
  87. data/test/integration/tags/break_tag_test.rb +0 -15
  88. data/test/integration/tags/continue_tag_test.rb +0 -15
  89. data/test/integration/tags/for_tag_test.rb +0 -410
  90. data/test/integration/tags/if_else_tag_test.rb +0 -188
  91. data/test/integration/tags/include_tag_test.rb +0 -238
  92. data/test/integration/tags/increment_tag_test.rb +0 -23
  93. data/test/integration/tags/raw_tag_test.rb +0 -31
  94. data/test/integration/tags/standard_tag_test.rb +0 -296
  95. data/test/integration/tags/statements_test.rb +0 -111
  96. data/test/integration/tags/table_row_test.rb +0 -64
  97. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  98. data/test/integration/template_test.rb +0 -323
  99. data/test/integration/trim_mode_test.rb +0 -525
  100. data/test/integration/variable_test.rb +0 -92
  101. data/test/test_helper.rb +0 -117
  102. data/test/unit/block_unit_test.rb +0 -58
  103. data/test/unit/condition_unit_test.rb +0 -158
  104. data/test/unit/context_unit_test.rb +0 -483
  105. data/test/unit/file_system_unit_test.rb +0 -35
  106. data/test/unit/i18n_unit_test.rb +0 -37
  107. data/test/unit/lexer_unit_test.rb +0 -51
  108. data/test/unit/parser_unit_test.rb +0 -82
  109. data/test/unit/regexp_unit_test.rb +0 -44
  110. data/test/unit/strainer_unit_test.rb +0 -148
  111. data/test/unit/tag_unit_test.rb +0 -21
  112. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  113. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  114. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  115. data/test/unit/template_unit_test.rb +0 -78
  116. data/test/unit/tokenizer_unit_test.rb +0 -55
  117. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,247 +1,726 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
4
+ require 'base64'
2
5
  require 'bigdecimal'
3
-
4
6
  module Liquid
5
7
  module StandardFilters
8
+ MAX_I32 = (1 << 31) - 1
9
+ private_constant :MAX_I32
10
+
11
+ MIN_I64 = -(1 << 63)
12
+ MAX_I64 = (1 << 63) - 1
13
+ I64_RANGE = MIN_I64..MAX_I64
14
+ private_constant :MIN_I64, :MAX_I64, :I64_RANGE
15
+
6
16
  HTML_ESCAPE = {
7
- '&'.freeze => '&amp;'.freeze,
8
- '>'.freeze => '&gt;'.freeze,
9
- '<'.freeze => '&lt;'.freeze,
10
- '"'.freeze => '&quot;'.freeze,
11
- "'".freeze => '&#39;'.freeze
12
- }
17
+ '&' => '&amp;',
18
+ '>' => '&gt;',
19
+ '<' => '&lt;',
20
+ '"' => '&quot;',
21
+ "'" => '&#39;',
22
+ }.freeze
13
23
  HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
24
+ STRIP_HTML_BLOCKS = Regexp.union(
25
+ %r{<script.*?</script>}m,
26
+ /<!--.*?-->/m,
27
+ %r{<style.*?</style>}m,
28
+ )
29
+ STRIP_HTML_TAGS = /<.*?>/m
30
+
31
+ class << self
32
+ def try_coerce_encoding(input, encoding:)
33
+ original_encoding = input.encoding
34
+ if input.encoding != encoding
35
+ input.force_encoding(encoding)
36
+ unless input.valid_encoding?
37
+ input.force_encoding(original_encoding)
38
+ end
39
+ end
40
+ input
41
+ end
42
+ end
14
43
 
15
- # Return the size of an array or of an string
44
+ # @liquid_public_docs
45
+ # @liquid_type filter
46
+ # @liquid_category array
47
+ # @liquid_summary
48
+ # Returns the size of a string or array.
49
+ # @liquid_description
50
+ # The size of a string is the number of characters that the string includes. The size of an array is the number of items
51
+ # in the array.
52
+ # @liquid_syntax variable | size
53
+ # @liquid_return [number]
16
54
  def size(input)
17
55
  input.respond_to?(:size) ? input.size : 0
18
56
  end
19
57
 
20
- # convert an input string to DOWNCASE
58
+ # @liquid_public_docs
59
+ # @liquid_type filter
60
+ # @liquid_category string
61
+ # @liquid_summary
62
+ # Converts a string to all lowercase characters.
63
+ # @liquid_syntax string | downcase
64
+ # @liquid_return [string]
21
65
  def downcase(input)
22
- input.to_s.downcase
66
+ Utils.to_s(input).downcase
23
67
  end
24
68
 
25
- # convert an input string to UPCASE
69
+ # @liquid_public_docs
70
+ # @liquid_type filter
71
+ # @liquid_category string
72
+ # @liquid_summary
73
+ # Converts a string to all uppercase characters.
74
+ # @liquid_syntax string | upcase
75
+ # @liquid_return [string]
26
76
  def upcase(input)
27
- input.to_s.upcase
77
+ Utils.to_s(input).upcase
28
78
  end
29
79
 
30
- # capitalize words in the input centence
80
+ # @liquid_public_docs
81
+ # @liquid_type filter
82
+ # @liquid_category string
83
+ # @liquid_summary
84
+ # Capitalizes the first word in a string and downcases the remaining characters.
85
+ # @liquid_syntax string | capitalize
86
+ # @liquid_return [string]
31
87
  def capitalize(input)
32
- input.to_s.capitalize
88
+ Utils.to_s(input).capitalize
33
89
  end
34
90
 
91
+ # @liquid_public_docs
92
+ # @liquid_type filter
93
+ # @liquid_category string
94
+ # @liquid_summary
95
+ # Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that don’t have a corresponding escape sequence.".
96
+ # @liquid_syntax string | escape
97
+ # @liquid_return [string]
35
98
  def escape(input)
36
- CGI.escapeHTML(input).untaint unless input.nil?
99
+ CGI.escapeHTML(Utils.to_s(input)) unless input.nil?
37
100
  end
38
101
  alias_method :h, :escape
39
102
 
103
+ # @liquid_public_docs
104
+ # @liquid_type filter
105
+ # @liquid_category string
106
+ # @liquid_summary
107
+ # Escapes a string without changing characters that have already been escaped.
108
+ # @liquid_syntax string | escape_once
109
+ # @liquid_return [string]
40
110
  def escape_once(input)
41
- input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
111
+ Utils.to_s(input).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
42
112
  end
43
113
 
114
+ # @liquid_public_docs
115
+ # @liquid_type filter
116
+ # @liquid_category string
117
+ # @liquid_summary
118
+ # Converts any URL-unsafe characters in a string to the
119
+ # [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
120
+ # @liquid_description
121
+ # > Note:
122
+ # > Spaces are converted to a `+` character, instead of a percent-encoded character.
123
+ # @liquid_syntax string | url_encode
124
+ # @liquid_return [string]
44
125
  def url_encode(input)
45
- CGI.escape(input) unless input.nil?
126
+ CGI.escape(Utils.to_s(input)) unless input.nil?
46
127
  end
47
128
 
129
+ # @liquid_public_docs
130
+ # @liquid_type filter
131
+ # @liquid_category string
132
+ # @liquid_summary
133
+ # Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
134
+ # in a string.
135
+ # @liquid_syntax string | url_decode
136
+ # @liquid_return [string]
48
137
  def url_decode(input)
49
- CGI.unescape(input) unless input.nil?
138
+ return if input.nil?
139
+
140
+ result = CGI.unescape(Utils.to_s(input))
141
+ raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
142
+
143
+ result
144
+ end
145
+
146
+ # @liquid_public_docs
147
+ # @liquid_type filter
148
+ # @liquid_category string
149
+ # @liquid_summary
150
+ # Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
151
+ # @liquid_syntax string | base64_encode
152
+ # @liquid_return [string]
153
+ def base64_encode(input)
154
+ Base64.strict_encode64(Utils.to_s(input))
50
155
  end
51
156
 
157
+ # @liquid_public_docs
158
+ # @liquid_type filter
159
+ # @liquid_category string
160
+ # @liquid_summary
161
+ # Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
162
+ # @liquid_syntax string | base64_decode
163
+ # @liquid_return [string]
164
+ def base64_decode(input)
165
+ input = Utils.to_s(input)
166
+ StandardFilters.try_coerce_encoding(Base64.strict_decode64(input), encoding: input.encoding)
167
+ rescue ::ArgumentError
168
+ raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
169
+ end
170
+
171
+ # @liquid_public_docs
172
+ # @liquid_type filter
173
+ # @liquid_category string
174
+ # @liquid_summary
175
+ # Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
176
+ # @liquid_syntax string | base64_url_safe_encode
177
+ # @liquid_return [string]
178
+ def base64_url_safe_encode(input)
179
+ Base64.urlsafe_encode64(Utils.to_s(input))
180
+ end
181
+
182
+ # @liquid_public_docs
183
+ # @liquid_type filter
184
+ # @liquid_category string
185
+ # @liquid_summary
186
+ # Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
187
+ # @liquid_syntax string | base64_url_safe_decode
188
+ # @liquid_return [string]
189
+ def base64_url_safe_decode(input)
190
+ input = Utils.to_s(input)
191
+ StandardFilters.try_coerce_encoding(Base64.urlsafe_decode64(input), encoding: input.encoding)
192
+ rescue ::ArgumentError
193
+ raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
194
+ end
195
+
196
+ # @liquid_public_docs
197
+ # @liquid_type filter
198
+ # @liquid_category string
199
+ # @liquid_summary
200
+ # Returns a substring or series of array items, starting at a given 0-based index.
201
+ # @liquid_description
202
+ # By default, the substring has a length of one character, and the array series has one array item. However, you can
203
+ # provide a second parameter to specify the number of characters or array items.
204
+ # @liquid_syntax string | slice
205
+ # @liquid_return [string]
52
206
  def slice(input, offset, length = nil)
53
207
  offset = Utils.to_integer(offset)
54
208
  length = length ? Utils.to_integer(length) : 1
55
209
 
56
- if input.is_a?(Array)
57
- input.slice(offset, length) || []
58
- else
59
- input.to_s.slice(offset, length) || ''
210
+ begin
211
+ if input.is_a?(Array)
212
+ input.slice(offset, length) || []
213
+ else
214
+ Utils.to_s(input).slice(offset, length) || ''
215
+ end
216
+ rescue RangeError
217
+ if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
218
+ raise # unexpected error
219
+ end
220
+ offset = offset.clamp(I64_RANGE)
221
+ length = length.clamp(I64_RANGE)
222
+ retry
60
223
  end
61
224
  end
62
225
 
63
- # Truncate a string down to x characters
64
- def truncate(input, length = 50, truncate_string = "...".freeze)
226
+ # @liquid_public_docs
227
+ # @liquid_type filter
228
+ # @liquid_category string
229
+ # @liquid_summary
230
+ # Truncates a string down to a given number of characters.
231
+ # @liquid_description
232
+ # If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
233
+ # the truncated string. The ellipsis is included in the character count of the truncated string.
234
+ # @liquid_syntax string | truncate: number
235
+ # @liquid_return [string]
236
+ def truncate(input, length = 50, truncate_string = "...")
65
237
  return if input.nil?
66
- input_str = input.to_s
67
- length = Utils.to_integer(length)
68
- truncate_string_str = truncate_string.to_s
238
+ input_str = Utils.to_s(input)
239
+ length = Utils.to_integer(length)
240
+
241
+ truncate_string_str = Utils.to_s(truncate_string)
242
+
69
243
  l = length - truncate_string_str.length
70
244
  l = 0 if l < 0
71
- input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
245
+
246
+ input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
72
247
  end
73
248
 
74
- def truncatewords(input, words = 15, truncate_string = "...".freeze)
249
+ # @liquid_public_docs
250
+ # @liquid_type filter
251
+ # @liquid_category string
252
+ # @liquid_summary
253
+ # Truncates a string down to a given number of words.
254
+ # @liquid_description
255
+ # If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
256
+ # the truncated string.
257
+ #
258
+ # > Caution:
259
+ # > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
260
+ # > closing HTML tags can be removed, which can result in unexpected behavior.
261
+ # @liquid_syntax string | truncatewords: number
262
+ # @liquid_return [string]
263
+ def truncatewords(input, words = 15, truncate_string = "...")
75
264
  return if input.nil?
76
- wordlist = input.to_s.split
265
+ input = Utils.to_s(input)
77
266
  words = Utils.to_integer(words)
78
- l = words - 1
79
- l = 0 if l < 0
80
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
267
+ words = 1 if words <= 0
268
+
269
+ wordlist = begin
270
+ input.split(" ", words + 1)
271
+ rescue RangeError
272
+ # integer too big for String#split, but we can semantically assume no truncation is needed
273
+ return input if words + 1 > MAX_I32
274
+ raise # unexpected error
275
+ end
276
+ return input if wordlist.length <= words
277
+
278
+ wordlist.pop
279
+ truncate_string = Utils.to_s(truncate_string)
280
+ wordlist.join(" ").concat(truncate_string)
81
281
  end
82
282
 
83
- # Split input string into an array of substrings separated by given pattern.
84
- #
85
- # Example:
86
- # <div class="summary">{{ post | split '//' | first }}</div>
87
- #
283
+ # @liquid_public_docs
284
+ # @liquid_type filter
285
+ # @liquid_category string
286
+ # @liquid_summary
287
+ # Splits a string into an array of substrings based on a given separator.
288
+ # @liquid_syntax string | split: string
289
+ # @liquid_return [array[string]]
88
290
  def split(input, pattern)
89
- input.to_s.split(pattern.to_s)
291
+ pattern = Utils.to_s(pattern)
292
+ input = Utils.to_s(input)
293
+ input.split(pattern)
90
294
  end
91
295
 
296
+ # @liquid_public_docs
297
+ # @liquid_type filter
298
+ # @liquid_category string
299
+ # @liquid_summary
300
+ # Strips all whitespace from the left and right of a string.
301
+ # @liquid_syntax string | strip
302
+ # @liquid_return [string]
92
303
  def strip(input)
93
- input.to_s.strip
304
+ input = Utils.to_s(input)
305
+ input.strip
94
306
  end
95
307
 
308
+ # @liquid_public_docs
309
+ # @liquid_type filter
310
+ # @liquid_category string
311
+ # @liquid_summary
312
+ # Strips all whitespace from the left of a string.
313
+ # @liquid_syntax string | lstrip
314
+ # @liquid_return [string]
96
315
  def lstrip(input)
97
- input.to_s.lstrip
316
+ input = Utils.to_s(input)
317
+ input.lstrip
98
318
  end
99
319
 
320
+ # @liquid_public_docs
321
+ # @liquid_type filter
322
+ # @liquid_category string
323
+ # @liquid_summary
324
+ # Strips all whitespace from the right of a string.
325
+ # @liquid_syntax string | rstrip
326
+ # @liquid_return [string]
100
327
  def rstrip(input)
101
- input.to_s.rstrip
328
+ input = Utils.to_s(input)
329
+ input.rstrip
102
330
  end
103
331
 
332
+ # @liquid_public_docs
333
+ # @liquid_type filter
334
+ # @liquid_category string
335
+ # @liquid_summary
336
+ # Strips all HTML tags from a string.
337
+ # @liquid_syntax string | strip_html
338
+ # @liquid_return [string]
104
339
  def strip_html(input)
105
- empty = ''.freeze
106
- input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
340
+ input = Utils.to_s(input)
341
+ empty = ''
342
+ result = input.gsub(STRIP_HTML_BLOCKS, empty)
343
+ result.gsub!(STRIP_HTML_TAGS, empty)
344
+ result
107
345
  end
108
346
 
109
- # Remove all newlines from the string
347
+ # @liquid_public_docs
348
+ # @liquid_type filter
349
+ # @liquid_category string
350
+ # @liquid_summary
351
+ # Strips all newline characters (line breaks) from a string.
352
+ # @liquid_syntax string | strip_newlines
353
+ # @liquid_return [string]
110
354
  def strip_newlines(input)
111
- input.to_s.gsub(/\r?\n/, ''.freeze)
355
+ input = Utils.to_s(input)
356
+ input.gsub(/\r?\n/, '')
112
357
  end
113
358
 
114
- # Join elements of the array with certain character between them
115
- def join(input, glue = ' '.freeze)
116
- InputIterator.new(input).join(glue)
359
+ # @liquid_public_docs
360
+ # @liquid_type filter
361
+ # @liquid_category array
362
+ # @liquid_summary
363
+ # Combines all of the items in an array into a single string, separated by a space.
364
+ # @liquid_syntax array | join
365
+ # @liquid_return [string]
366
+ def join(input, glue = ' ')
367
+ glue = Utils.to_s(glue)
368
+ InputIterator.new(input, context).join(glue)
117
369
  end
118
370
 
119
- # Sort elements of the array
120
- # provide optional property with which to sort an array of hashes or drops
371
+ # @liquid_public_docs
372
+ # @liquid_type filter
373
+ # @liquid_category array
374
+ # @liquid_summary
375
+ # Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
376
+ # @liquid_syntax array | sort
377
+ # @liquid_return [array[untyped]]
121
378
  def sort(input, property = nil)
122
- ary = InputIterator.new(input)
379
+ ary = InputIterator.new(input, context)
380
+
381
+ return [] if ary.empty?
382
+
123
383
  if property.nil?
124
- ary.sort
125
- elsif ary.empty? # The next two cases assume a non-empty array.
126
- []
127
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
128
384
  ary.sort do |a, b|
129
- a = a[property]
130
- b = b[property]
131
- if a && b
132
- a <=> b
133
- else
134
- a ? -1 : 1
135
- end
385
+ nil_safe_compare(a, b)
386
+ end
387
+ elsif ary.all? { |el| el.respond_to?(:[]) }
388
+ begin
389
+ ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
390
+ rescue TypeError
391
+ raise_property_error(property)
136
392
  end
137
393
  end
138
394
  end
139
395
 
140
- # Sort elements of an array ignoring case if strings
141
- # provide optional property with which to sort an array of hashes or drops
396
+ # @liquid_public_docs
397
+ # @liquid_type filter
398
+ # @liquid_category array
399
+ # @liquid_summary
400
+ # Sorts the items in an array in case-insensitive alphabetical order.
401
+ # @liquid_description
402
+ # > Caution:
403
+ # > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
404
+ # > string, so sorting on numerical values can lead to unexpected results.
405
+ # @liquid_syntax array | sort_natural
406
+ # @liquid_return [array[untyped]]
142
407
  def sort_natural(input, property = nil)
143
- ary = InputIterator.new(input)
408
+ ary = InputIterator.new(input, context)
409
+
410
+ return [] if ary.empty?
144
411
 
145
412
  if property.nil?
146
- ary.sort { |a, b| a.casecmp(b) }
147
- elsif ary.empty? # The next two cases assume a non-empty array.
148
- []
149
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
150
- ary.sort { |a, b| a[property].casecmp(b[property]) }
413
+ ary.sort do |a, b|
414
+ nil_safe_casecmp(a, b)
415
+ end
416
+ elsif ary.all? { |el| el.respond_to?(:[]) }
417
+ begin
418
+ ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
419
+ rescue TypeError
420
+ raise_property_error(property)
421
+ end
151
422
  end
152
423
  end
153
424
 
154
- # Remove duplicate elements from an array
155
- # provide optional property with which to determine uniqueness
425
+ # @liquid_public_docs
426
+ # @liquid_type filter
427
+ # @liquid_category array
428
+ # @liquid_summary
429
+ # Filters an array to include only items with a specific property value.
430
+ # @liquid_description
431
+ # This requires you to provide both the property name and the associated value.
432
+ # @liquid_syntax array | where: string, string
433
+ # @liquid_return [array[untyped]]
434
+ def where(input, property, target_value = nil)
435
+ filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
436
+ end
437
+
438
+ # @liquid_public_docs
439
+ # @liquid_type filter
440
+ # @liquid_category array
441
+ # @liquid_summary
442
+ # Filters an array to exclude items with a specific property value.
443
+ # @liquid_description
444
+ # This requires you to provide both the property name and the associated value.
445
+ # @liquid_syntax array | reject: string, string
446
+ # @liquid_return [array[untyped]]
447
+ def reject(input, property, target_value = nil)
448
+ filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
449
+ end
450
+
451
+ # @liquid_public_docs
452
+ # @liquid_type filter
453
+ # @liquid_category array
454
+ # @liquid_summary
455
+ # Tests if any item in an array has a specific property value.
456
+ # @liquid_description
457
+ # This requires you to provide both the property name and the associated value.
458
+ # @liquid_syntax array | has: string, string
459
+ # @liquid_return [boolean]
460
+ def has(input, property, target_value = nil)
461
+ filter_array(input, property, target_value, false) { |ary, &block| ary.any?(&block) }
462
+ end
463
+
464
+ # @liquid_public_docs
465
+ # @liquid_type filter
466
+ # @liquid_category array
467
+ # @liquid_summary
468
+ # Returns the first item in an array with a specific property value.
469
+ # @liquid_description
470
+ # This requires you to provide both the property name and the associated value.
471
+ # @liquid_syntax array | find: string, string
472
+ # @liquid_return [untyped]
473
+ def find(input, property, target_value = nil)
474
+ filter_array(input, property, target_value, nil) { |ary, &block| ary.find(&block) }
475
+ end
476
+
477
+ # @liquid_public_docs
478
+ # @liquid_type filter
479
+ # @liquid_category array
480
+ # @liquid_summary
481
+ # Returns the index of the first item in an array with a specific property value.
482
+ # @liquid_description
483
+ # This requires you to provide both the property name and the associated value.
484
+ # @liquid_syntax array | find_index: string, string
485
+ # @liquid_return [number]
486
+ def find_index(input, property, target_value = nil)
487
+ filter_array(input, property, target_value, nil) { |ary, &block| ary.find_index(&block) }
488
+ end
489
+
490
+ # @liquid_public_docs
491
+ # @liquid_type filter
492
+ # @liquid_category array
493
+ # @liquid_summary
494
+ # Removes any duplicate items in an array.
495
+ # @liquid_syntax array | uniq
496
+ # @liquid_return [array[untyped]]
156
497
  def uniq(input, property = nil)
157
- ary = InputIterator.new(input)
498
+ ary = InputIterator.new(input, context)
158
499
 
159
500
  if property.nil?
160
501
  ary.uniq
161
502
  elsif ary.empty? # The next two cases assume a non-empty array.
162
503
  []
163
- elsif ary.first.respond_to?(:[])
164
- ary.uniq{ |a| a[property] }
504
+ else
505
+ ary.uniq do |item|
506
+ item[property]
507
+ rescue TypeError
508
+ raise_property_error(property)
509
+ rescue NoMethodError
510
+ return nil unless item.respond_to?(:[])
511
+ raise
512
+ end
165
513
  end
166
514
  end
167
515
 
168
- # Reverse the elements of an array
516
+ # @liquid_public_docs
517
+ # @liquid_type filter
518
+ # @liquid_category array
519
+ # @liquid_summary
520
+ # Reverses the order of the items in an array.
521
+ # @liquid_syntax array | reverse
522
+ # @liquid_return [array[untyped]]
169
523
  def reverse(input)
170
- ary = InputIterator.new(input)
524
+ ary = InputIterator.new(input, context)
171
525
  ary.reverse
172
526
  end
173
527
 
174
- # map/collect on a given property
528
+ # @liquid_public_docs
529
+ # @liquid_type filter
530
+ # @liquid_category array
531
+ # @liquid_summary
532
+ # Creates an array of values from a specific property of the items in an array.
533
+ # @liquid_syntax array | map: string
534
+ # @liquid_return [array[untyped]]
175
535
  def map(input, property)
176
- InputIterator.new(input).map do |e|
536
+ InputIterator.new(input, context).map do |e|
177
537
  e = e.call if e.is_a?(Proc)
178
538
 
179
- if property == "to_liquid".freeze
539
+ if property == "to_liquid"
180
540
  e
181
541
  elsif e.respond_to?(:[])
182
542
  r = e[property]
183
543
  r.is_a?(Proc) ? r.call : r
184
544
  end
185
545
  end
546
+ rescue TypeError
547
+ raise_property_error(property)
186
548
  end
187
549
 
188
- # Remove nils within an array
189
- # provide optional property with which to check for nil
550
+ # @liquid_public_docs
551
+ # @liquid_type filter
552
+ # @liquid_category array
553
+ # @liquid_summary
554
+ # Removes any `nil` items from an array.
555
+ # @liquid_syntax array | compact
556
+ # @liquid_return [array[untyped]]
190
557
  def compact(input, property = nil)
191
- ary = InputIterator.new(input)
558
+ ary = InputIterator.new(input, context)
192
559
 
193
560
  if property.nil?
194
561
  ary.compact
195
562
  elsif ary.empty? # The next two cases assume a non-empty array.
196
563
  []
197
- elsif ary.first.respond_to?(:[])
198
- ary.reject{ |a| a[property].nil? }
564
+ else
565
+ ary.reject do |item|
566
+ item[property].nil?
567
+ rescue TypeError
568
+ raise_property_error(property)
569
+ rescue NoMethodError
570
+ return nil unless item.respond_to?(:[])
571
+ raise
572
+ end
199
573
  end
200
574
  end
201
575
 
202
- # Replace occurrences of a string with another
203
- def replace(input, string, replacement = ''.freeze)
204
- input.to_s.gsub(string.to_s, replacement.to_s)
576
+ # @liquid_public_docs
577
+ # @liquid_type filter
578
+ # @liquid_category string
579
+ # @liquid_summary
580
+ # Replaces any instance of a substring inside a string with a given string.
581
+ # @liquid_syntax string | replace: string, string
582
+ # @liquid_return [string]
583
+ def replace(input, string, replacement = '')
584
+ string = Utils.to_s(string)
585
+ replacement = Utils.to_s(replacement)
586
+ input = Utils.to_s(input)
587
+ input.gsub(string, replacement)
588
+ end
589
+
590
+ # @liquid_public_docs
591
+ # @liquid_type filter
592
+ # @liquid_category string
593
+ # @liquid_summary
594
+ # Replaces the first instance of a substring inside a string with a given string.
595
+ # @liquid_syntax string | replace_first: string, string
596
+ # @liquid_return [string]
597
+ def replace_first(input, string, replacement = '')
598
+ string = Utils.to_s(string)
599
+ replacement = Utils.to_s(replacement)
600
+ input = Utils.to_s(input)
601
+ input.sub(string, replacement)
205
602
  end
206
603
 
207
- # Replace the first occurrences of a string with another
208
- def replace_first(input, string, replacement = ''.freeze)
209
- input.to_s.sub(string.to_s, replacement.to_s)
604
+ # @liquid_public_docs
605
+ # @liquid_type filter
606
+ # @liquid_category string
607
+ # @liquid_summary
608
+ # Replaces the last instance of a substring inside a string with a given string.
609
+ # @liquid_syntax string | replace_last: string, string
610
+ # @liquid_return [string]
611
+ def replace_last(input, string, replacement)
612
+ input = Utils.to_s(input)
613
+ string = Utils.to_s(string)
614
+ replacement = Utils.to_s(replacement)
615
+
616
+ start_index = input.rindex(string)
617
+
618
+ return input unless start_index
619
+
620
+ output = input.dup
621
+ output[start_index, string.length] = replacement
622
+ output
210
623
  end
211
624
 
212
- # remove a substring
625
+ # @liquid_public_docs
626
+ # @liquid_type filter
627
+ # @liquid_category string
628
+ # @liquid_summary
629
+ # Removes any instance of a substring inside a string.
630
+ # @liquid_syntax string | remove: string
631
+ # @liquid_return [string]
213
632
  def remove(input, string)
214
- input.to_s.gsub(string.to_s, ''.freeze)
633
+ replace(input, string, '')
215
634
  end
216
635
 
217
- # remove the first occurrences of a substring
636
+ # @liquid_public_docs
637
+ # @liquid_type filter
638
+ # @liquid_category string
639
+ # @liquid_summary
640
+ # Removes the first instance of a substring inside a string.
641
+ # @liquid_syntax string | remove_first: string
642
+ # @liquid_return [string]
218
643
  def remove_first(input, string)
219
- input.to_s.sub(string.to_s, ''.freeze)
644
+ replace_first(input, string, '')
220
645
  end
221
646
 
222
- # add one string to another
647
+ # @liquid_public_docs
648
+ # @liquid_type filter
649
+ # @liquid_category string
650
+ # @liquid_summary
651
+ # Removes the last instance of a substring inside a string.
652
+ # @liquid_syntax string | remove_last: string
653
+ # @liquid_return [string]
654
+ def remove_last(input, string)
655
+ replace_last(input, string, '')
656
+ end
657
+
658
+ # @liquid_public_docs
659
+ # @liquid_type filter
660
+ # @liquid_category string
661
+ # @liquid_summary
662
+ # Adds a given string to the end of a string.
663
+ # @liquid_syntax string | append: string
664
+ # @liquid_return [string]
223
665
  def append(input, string)
224
- input.to_s + string.to_s
666
+ input = Utils.to_s(input)
667
+ string = Utils.to_s(string)
668
+ input + string
225
669
  end
226
670
 
671
+ # @liquid_public_docs
672
+ # @liquid_type filter
673
+ # @liquid_category array
674
+ # @liquid_summary
675
+ # Concatenates (combines) two arrays.
676
+ # @liquid_description
677
+ # > Note:
678
+ # > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
679
+ # > [`uniq` filter](/docs/api/liquid/filters/uniq).
680
+ # @liquid_syntax array | concat: array
681
+ # @liquid_return [array[untyped]]
227
682
  def concat(input, array)
228
683
  unless array.respond_to?(:to_ary)
229
- raise ArgumentError.new("concat filter requires an array argument")
684
+ raise ArgumentError, "concat filter requires an array argument"
230
685
  end
231
- InputIterator.new(input).concat(array)
686
+ InputIterator.new(input, context).concat(array)
232
687
  end
233
688
 
234
- # prepend a string to another
689
+ # @liquid_public_docs
690
+ # @liquid_type filter
691
+ # @liquid_category string
692
+ # @liquid_summary
693
+ # Adds a given string to the beginning of a string.
694
+ # @liquid_syntax string | prepend: string
695
+ # @liquid_return [string]
235
696
  def prepend(input, string)
236
- string.to_s + input.to_s
697
+ input = Utils.to_s(input)
698
+ string = Utils.to_s(string)
699
+ string + input
237
700
  end
238
701
 
239
- # Add <br /> tags in front of all newlines in input string
702
+ # @liquid_public_docs
703
+ # @liquid_type filter
704
+ # @liquid_category string
705
+ # @liquid_summary
706
+ # Converts newlines (`\n`) in a string to HTML line breaks (`<br>`).
707
+ # @liquid_syntax string | newline_to_br
708
+ # @liquid_return [string]
240
709
  def newline_to_br(input)
241
- input.to_s.gsub(/\n/, "<br />\n".freeze)
710
+ input = Utils.to_s(input)
711
+ input.gsub(/\r?\n/, "<br />\n")
242
712
  end
243
713
 
244
- # Reformat a date using Ruby's core Time#strftime( string ) -> string
714
+ # @liquid_public_docs
715
+ # @liquid_type filter
716
+ # @liquid_category date
717
+ # @liquid_summary
718
+ # Formats a date according to a specified format string.
719
+ # @liquid_description
720
+ # This filter formats a date using various format specifiers. If the format string is empty,
721
+ # the original input is returned. If the input cannot be converted to a date, the original input is returned.
722
+ #
723
+ # The following format specifiers can be used:
245
724
  #
246
725
  # %a - The abbreviated weekday name (``Sun'')
247
726
  # %A - The full weekday name (``Sunday'')
@@ -270,68 +749,117 @@ module Liquid
270
749
  # %Y - Year with century
271
750
  # %Z - Time zone name
272
751
  # %% - Literal ``%'' character
273
- #
274
- # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
752
+ # @liquid_syntax date | date: string
753
+ # @liquid_return [string]
275
754
  def date(input, format)
276
- return input if format.to_s.empty?
755
+ str_format = Utils.to_s(format)
756
+ return input if str_format.empty?
277
757
 
278
- return input unless date = Utils.to_date(input)
758
+ return input unless (date = Utils.to_date(input))
279
759
 
280
- date.strftime(format.to_s)
760
+ date.strftime(str_format)
281
761
  end
282
762
 
283
- # Get the first element of the passed in array
284
- #
285
- # Example:
286
- # {{ product.images | first | to_img }}
287
- #
763
+ # @liquid_public_docs
764
+ # @liquid_type filter
765
+ # @liquid_category array
766
+ # @liquid_summary
767
+ # Returns the first item in an array.
768
+ # @liquid_syntax array | first
769
+ # @liquid_return [untyped]
288
770
  def first(array)
289
771
  array.first if array.respond_to?(:first)
290
772
  end
291
773
 
292
- # Get the last element of the passed in array
293
- #
294
- # Example:
295
- # {{ product.images | last | to_img }}
296
- #
774
+ # @liquid_public_docs
775
+ # @liquid_type filter
776
+ # @liquid_category array
777
+ # @liquid_summary
778
+ # Returns the last item in an array.
779
+ # @liquid_syntax array | last
780
+ # @liquid_return [untyped]
297
781
  def last(array)
298
782
  array.last if array.respond_to?(:last)
299
783
  end
300
784
 
301
- # absolute value
785
+ # @liquid_public_docs
786
+ # @liquid_type filter
787
+ # @liquid_category math
788
+ # @liquid_summary
789
+ # Returns the absolute value of a number.
790
+ # @liquid_syntax number | abs
791
+ # @liquid_return [number]
302
792
  def abs(input)
303
793
  result = Utils.to_number(input).abs
304
794
  result.is_a?(BigDecimal) ? result.to_f : result
305
795
  end
306
796
 
307
- # addition
797
+ # @liquid_public_docs
798
+ # @liquid_type filter
799
+ # @liquid_category math
800
+ # @liquid_summary
801
+ # Adds two numbers.
802
+ # @liquid_syntax number | plus: number
803
+ # @liquid_return [number]
308
804
  def plus(input, operand)
309
805
  apply_operation(input, operand, :+)
310
806
  end
311
807
 
312
- # subtraction
808
+ # @liquid_public_docs
809
+ # @liquid_type filter
810
+ # @liquid_category math
811
+ # @liquid_summary
812
+ # Subtracts a given number from another number.
813
+ # @liquid_syntax number | minus: number
814
+ # @liquid_return [number]
313
815
  def minus(input, operand)
314
816
  apply_operation(input, operand, :-)
315
817
  end
316
818
 
317
- # multiplication
819
+ # @liquid_public_docs
820
+ # @liquid_type filter
821
+ # @liquid_category math
822
+ # @liquid_summary
823
+ # Multiplies a number by a given number.
824
+ # @liquid_syntax number | times: number
825
+ # @liquid_return [number]
318
826
  def times(input, operand)
319
827
  apply_operation(input, operand, :*)
320
828
  end
321
829
 
322
- # division
830
+ # @liquid_public_docs
831
+ # @liquid_type filter
832
+ # @liquid_category math
833
+ # @liquid_summary
834
+ # Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float.
835
+ # @liquid_syntax number | divided_by: number
836
+ # @liquid_return [number]
323
837
  def divided_by(input, operand)
324
838
  apply_operation(input, operand, :/)
325
839
  rescue ::ZeroDivisionError => e
326
840
  raise Liquid::ZeroDivisionError, e.message
327
841
  end
328
842
 
843
+ # @liquid_public_docs
844
+ # @liquid_type filter
845
+ # @liquid_category math
846
+ # @liquid_summary
847
+ # Returns the remainder of dividing a number by a given number.
848
+ # @liquid_syntax number | modulo: number
849
+ # @liquid_return [number]
329
850
  def modulo(input, operand)
330
851
  apply_operation(input, operand, :%)
331
852
  rescue ::ZeroDivisionError => e
332
853
  raise Liquid::ZeroDivisionError, e.message
333
854
  end
334
855
 
856
+ # @liquid_public_docs
857
+ # @liquid_type filter
858
+ # @liquid_category math
859
+ # @liquid_summary
860
+ # Rounds a number to the nearest integer.
861
+ # @liquid_syntax number | round
862
+ # @liquid_return [number]
335
863
  def round(input, n = 0)
336
864
  result = Utils.to_number(input).round(Utils.to_number(n))
337
865
  result = result.to_f if result.is_a?(BigDecimal)
@@ -341,38 +869,172 @@ module Liquid
341
869
  raise Liquid::FloatDomainError, e.message
342
870
  end
343
871
 
872
+ # @liquid_public_docs
873
+ # @liquid_type filter
874
+ # @liquid_category math
875
+ # @liquid_summary
876
+ # Rounds a number up to the nearest integer.
877
+ # @liquid_syntax number | ceil
878
+ # @liquid_return [number]
344
879
  def ceil(input)
345
880
  Utils.to_number(input).ceil.to_i
346
881
  rescue ::FloatDomainError => e
347
882
  raise Liquid::FloatDomainError, e.message
348
883
  end
349
884
 
885
+ # @liquid_public_docs
886
+ # @liquid_type filter
887
+ # @liquid_category math
888
+ # @liquid_summary
889
+ # Rounds a number down to the nearest integer.
890
+ # @liquid_syntax number | floor
891
+ # @liquid_return [number]
350
892
  def floor(input)
351
893
  Utils.to_number(input).floor.to_i
352
894
  rescue ::FloatDomainError => e
353
895
  raise Liquid::FloatDomainError, e.message
354
896
  end
355
897
 
356
- def default(input, default_value = ''.freeze)
357
- if !input || input.respond_to?(:empty?) && input.empty?
358
- default_value
359
- else
360
- input
898
+ # @liquid_public_docs
899
+ # @liquid_type filter
900
+ # @liquid_category math
901
+ # @liquid_summary
902
+ # Limits a number to a minimum value.
903
+ # @liquid_syntax number | at_least
904
+ # @liquid_return [number]
905
+ def at_least(input, n)
906
+ min_value = Utils.to_number(n)
907
+
908
+ result = Utils.to_number(input)
909
+ result = min_value if min_value > result
910
+ result.is_a?(BigDecimal) ? result.to_f : result
911
+ end
912
+
913
+ # @liquid_public_docs
914
+ # @liquid_type filter
915
+ # @liquid_category math
916
+ # @liquid_summary
917
+ # Limits a number to a maximum value.
918
+ # @liquid_syntax number | at_most
919
+ # @liquid_return [number]
920
+ def at_most(input, n)
921
+ max_value = Utils.to_number(n)
922
+
923
+ result = Utils.to_number(input)
924
+ result = max_value if max_value < result
925
+ result.is_a?(BigDecimal) ? result.to_f : result
926
+ end
927
+
928
+ # @liquid_public_docs
929
+ # @liquid_type filter
930
+ # @liquid_category default
931
+ # @liquid_summary
932
+ # Sets a default value for any variable whose value is one of the following:
933
+ #
934
+ # - [`empty`](/docs/api/liquid/basics#empty)
935
+ # - [`false`](/docs/api/liquid/basics#truthy-and-falsy)
936
+ # - [`nil`](/docs/api/liquid/basics#nil)
937
+ # @liquid_syntax variable | default: variable
938
+ # @liquid_return [untyped]
939
+ # @liquid_optional_param allow_false: [boolean] Whether to use false values instead of the default.
940
+ def default(input, default_value = '', options = {})
941
+ options = {} unless options.is_a?(Hash)
942
+ false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
943
+ false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
944
+ end
945
+
946
+ # @liquid_public_docs
947
+ # @liquid_type filter
948
+ # @liquid_category array
949
+ # @liquid_summary
950
+ # Returns the sum of all elements in an array.
951
+ # @liquid_syntax array | sum
952
+ # @liquid_return [number]
953
+ def sum(input, property = nil)
954
+ ary = InputIterator.new(input, context)
955
+ return 0 if ary.empty?
956
+
957
+ values_for_sum = ary.map do |item|
958
+ if property.nil?
959
+ item
960
+ elsif item.respond_to?(:[])
961
+ item[property]
962
+ else
963
+ 0
964
+ end
965
+ rescue TypeError
966
+ raise_property_error(property)
361
967
  end
968
+
969
+ result = InputIterator.new(values_for_sum, context).sum do |item|
970
+ Utils.to_number(item)
971
+ end
972
+
973
+ result.is_a?(BigDecimal) ? result.to_f : result
362
974
  end
363
975
 
364
976
  private
365
977
 
978
+ attr_reader :context
979
+
980
+ def filter_array(input, property, target_value, default_value = [], &block)
981
+ ary = InputIterator.new(input, context)
982
+
983
+ return default_value if ary.empty?
984
+
985
+ block.call(ary) do |item|
986
+ if target_value.nil?
987
+ item[property]
988
+ else
989
+ item[property] == target_value
990
+ end
991
+ rescue TypeError
992
+ raise_property_error(property)
993
+ rescue NoMethodError
994
+ return nil unless item.respond_to?(:[])
995
+ raise
996
+ end
997
+ end
998
+
999
+ def raise_property_error(property)
1000
+ raise Liquid::ArgumentError, "cannot select the property '#{Utils.to_s(property)}'"
1001
+ end
1002
+
366
1003
  def apply_operation(input, operand, operation)
367
1004
  result = Utils.to_number(input).send(operation, Utils.to_number(operand))
368
1005
  result.is_a?(BigDecimal) ? result.to_f : result
369
1006
  end
370
1007
 
1008
+ def nil_safe_compare(a, b)
1009
+ result = a <=> b
1010
+
1011
+ if result
1012
+ result
1013
+ elsif a.nil?
1014
+ 1
1015
+ elsif b.nil?
1016
+ -1
1017
+ else
1018
+ raise Liquid::ArgumentError, "cannot sort values of incompatible types"
1019
+ end
1020
+ end
1021
+
1022
+ def nil_safe_casecmp(a, b)
1023
+ if !a.nil? && !b.nil?
1024
+ a.to_s.casecmp(b.to_s)
1025
+ elsif a.nil? && b.nil?
1026
+ 0
1027
+ else
1028
+ a.nil? ? 1 : -1
1029
+ end
1030
+ end
1031
+
371
1032
  class InputIterator
372
1033
  include Enumerable
373
1034
 
374
- def initialize(input)
375
- @input = if input.is_a?(Array)
1035
+ def initialize(input, context)
1036
+ @context = context
1037
+ @input = if input.is_a?(Array)
376
1038
  input.flatten
377
1039
  elsif input.is_a?(Hash)
378
1040
  [input]
@@ -384,7 +1046,18 @@ module Liquid
384
1046
  end
385
1047
 
386
1048
  def join(glue)
387
- to_a.join(glue)
1049
+ first = true
1050
+ output = +""
1051
+ each do |item|
1052
+ if first
1053
+ first = false
1054
+ else
1055
+ output << glue
1056
+ end
1057
+
1058
+ output << Liquid::Utils.to_s(item)
1059
+ end
1060
+ output
388
1061
  end
389
1062
 
390
1063
  def concat(args)
@@ -396,7 +1069,10 @@ module Liquid
396
1069
  end
397
1070
 
398
1071
  def uniq(&block)
399
- to_a.uniq(&block)
1072
+ to_a.uniq do |item|
1073
+ item = Utils.to_liquid_value(item)
1074
+ block ? yield(item) : item
1075
+ end
400
1076
  end
401
1077
 
402
1078
  def compact
@@ -410,11 +1086,11 @@ module Liquid
410
1086
 
411
1087
  def each
412
1088
  @input.each do |e|
413
- yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
1089
+ e = e.respond_to?(:to_liquid) ? e.to_liquid : e
1090
+ e.context = @context if e.respond_to?(:context=)
1091
+ yield(e)
414
1092
  end
415
1093
  end
416
1094
  end
417
1095
  end
418
-
419
- Template.register_filter(StandardFilters)
420
1096
  end