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