liquid 2.6.1 → 4.0.3

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +194 -29
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +60 -2
  5. data/lib/liquid.rb +25 -14
  6. data/lib/liquid/block.rb +47 -96
  7. data/lib/liquid/block_body.rb +143 -0
  8. data/lib/liquid/condition.rb +70 -39
  9. data/lib/liquid/context.rb +116 -157
  10. data/lib/liquid/document.rb +19 -9
  11. data/lib/liquid/drop.rb +31 -14
  12. data/lib/liquid/errors.rb +54 -10
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +19 -7
  15. data/lib/liquid/file_system.rb +25 -14
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +2 -3
  19. data/lib/liquid/lexer.rb +55 -0
  20. data/lib/liquid/locales/en.yml +26 -0
  21. data/lib/liquid/parse_context.rb +38 -0
  22. data/lib/liquid/parse_tree_visitor.rb +42 -0
  23. data/lib/liquid/parser.rb +90 -0
  24. data/lib/liquid/parser_switching.rb +31 -0
  25. data/lib/liquid/profiler.rb +158 -0
  26. data/lib/liquid/profiler/hooks.rb +23 -0
  27. data/lib/liquid/range_lookup.rb +37 -0
  28. data/lib/liquid/resource_limits.rb +23 -0
  29. data/lib/liquid/standardfilters.rb +311 -77
  30. data/lib/liquid/strainer.rb +39 -26
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +28 -11
  33. data/lib/liquid/tags/assign.rb +34 -10
  34. data/lib/liquid/tags/break.rb +1 -4
  35. data/lib/liquid/tags/capture.rb +11 -9
  36. data/lib/liquid/tags/case.rb +37 -22
  37. data/lib/liquid/tags/comment.rb +10 -3
  38. data/lib/liquid/tags/continue.rb +1 -4
  39. data/lib/liquid/tags/cycle.rb +20 -14
  40. data/lib/liquid/tags/decrement.rb +4 -8
  41. data/lib/liquid/tags/for.rb +121 -60
  42. data/lib/liquid/tags/if.rb +73 -30
  43. data/lib/liquid/tags/ifchanged.rb +3 -5
  44. data/lib/liquid/tags/include.rb +77 -46
  45. data/lib/liquid/tags/increment.rb +4 -8
  46. data/lib/liquid/tags/raw.rb +35 -10
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +6 -9
  49. data/lib/liquid/template.rb +130 -32
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/truffle.rb +5 -0
  52. data/lib/liquid/utils.rb +57 -4
  53. data/lib/liquid/variable.rb +121 -30
  54. data/lib/liquid/variable_lookup.rb +88 -0
  55. data/lib/liquid/version.rb +2 -1
  56. data/test/fixtures/en_locale.yml +9 -0
  57. data/test/integration/assign_test.rb +48 -0
  58. data/test/integration/blank_test.rb +106 -0
  59. data/test/integration/block_test.rb +12 -0
  60. data/test/{liquid → integration}/capture_test.rb +13 -3
  61. data/test/integration/context_test.rb +32 -0
  62. data/test/integration/document_test.rb +19 -0
  63. data/test/integration/drop_test.rb +273 -0
  64. data/test/integration/error_handling_test.rb +260 -0
  65. data/test/integration/filter_test.rb +178 -0
  66. data/test/integration/hash_ordering_test.rb +23 -0
  67. data/test/integration/output_test.rb +123 -0
  68. data/test/integration/parse_tree_visitor_test.rb +247 -0
  69. data/test/integration/parsing_quirks_test.rb +122 -0
  70. data/test/integration/render_profiling_test.rb +154 -0
  71. data/test/integration/security_test.rb +80 -0
  72. data/test/integration/standard_filter_test.rb +776 -0
  73. data/test/{liquid → integration}/tags/break_tag_test.rb +2 -3
  74. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -2
  75. data/test/integration/tags/for_tag_test.rb +410 -0
  76. data/test/integration/tags/if_else_tag_test.rb +188 -0
  77. data/test/integration/tags/include_tag_test.rb +253 -0
  78. data/test/integration/tags/increment_tag_test.rb +23 -0
  79. data/test/{liquid → integration}/tags/raw_tag_test.rb +9 -2
  80. data/test/integration/tags/standard_tag_test.rb +296 -0
  81. data/test/integration/tags/statements_test.rb +111 -0
  82. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +25 -24
  83. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  84. data/test/integration/template_test.rb +332 -0
  85. data/test/integration/trim_mode_test.rb +529 -0
  86. data/test/integration/variable_test.rb +96 -0
  87. data/test/test_helper.rb +106 -19
  88. data/test/truffle/truffle_test.rb +9 -0
  89. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +9 -9
  90. data/test/unit/condition_unit_test.rb +166 -0
  91. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +85 -74
  92. data/test/unit/file_system_unit_test.rb +35 -0
  93. data/test/unit/i18n_unit_test.rb +37 -0
  94. data/test/unit/lexer_unit_test.rb +51 -0
  95. data/test/unit/parser_unit_test.rb +82 -0
  96. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +4 -4
  97. data/test/unit/strainer_unit_test.rb +164 -0
  98. data/test/unit/tag_unit_test.rb +21 -0
  99. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  100. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  101. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  102. data/test/unit/template_unit_test.rb +78 -0
  103. data/test/unit/tokenizer_unit_test.rb +55 -0
  104. data/test/unit/variable_unit_test.rb +162 -0
  105. metadata +157 -77
  106. data/lib/extras/liquid_view.rb +0 -51
  107. data/lib/liquid/htmltags.rb +0 -74
  108. data/lib/liquid/module_ex.rb +0 -62
  109. data/test/liquid/assign_test.rb +0 -21
  110. data/test/liquid/condition_test.rb +0 -127
  111. data/test/liquid/drop_test.rb +0 -180
  112. data/test/liquid/error_handling_test.rb +0 -81
  113. data/test/liquid/file_system_test.rb +0 -29
  114. data/test/liquid/filter_test.rb +0 -125
  115. data/test/liquid/hash_ordering_test.rb +0 -25
  116. data/test/liquid/module_ex_test.rb +0 -87
  117. data/test/liquid/output_test.rb +0 -116
  118. data/test/liquid/parsing_quirks_test.rb +0 -52
  119. data/test/liquid/security_test.rb +0 -64
  120. data/test/liquid/standard_filter_test.rb +0 -251
  121. data/test/liquid/strainer_test.rb +0 -52
  122. data/test/liquid/tags/for_tag_test.rb +0 -297
  123. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  124. data/test/liquid/tags/include_tag_test.rb +0 -166
  125. data/test/liquid/tags/increment_tag_test.rb +0 -24
  126. data/test/liquid/tags/standard_tag_test.rb +0 -295
  127. data/test/liquid/tags/statements_test.rb +0 -134
  128. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  129. data/test/liquid/template_test.rb +0 -146
  130. data/test/liquid/variable_test.rb +0 -186
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class BlockBody
3
+ def render_node_with_profiling(node, output, context, skip_output = false)
4
+ Profiler.profile_node_render(node) do
5
+ render_node_without_profiling(node, output, context, skip_output)
6
+ end
7
+ end
8
+
9
+ alias_method :render_node_without_profiling, :render_node_to_output
10
+ alias_method :render_node_to_output, :render_node_with_profiling
11
+ end
12
+
13
+ class Include < Tag
14
+ def render_with_profiling(context)
15
+ Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
16
+ render_without_profiling(context)
17
+ end
18
+ end
19
+
20
+ alias_method :render_without_profiling, :render
21
+ alias_method :render, :render_with_profiling
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ module Liquid
2
+ class RangeLookup
3
+ def self.parse(start_markup, end_markup)
4
+ start_obj = Expression.parse(start_markup)
5
+ end_obj = Expression.parse(end_markup)
6
+ if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
7
+ new(start_obj, end_obj)
8
+ else
9
+ start_obj.to_i..end_obj.to_i
10
+ end
11
+ end
12
+
13
+ def initialize(start_obj, end_obj)
14
+ @start_obj = start_obj
15
+ @end_obj = end_obj
16
+ end
17
+
18
+ def evaluate(context)
19
+ start_int = to_integer(context.evaluate(@start_obj))
20
+ end_int = to_integer(context.evaluate(@end_obj))
21
+ start_int..end_int
22
+ end
23
+
24
+ private
25
+
26
+ def to_integer(input)
27
+ case input
28
+ when Integer
29
+ input
30
+ when NilClass, String
31
+ input.to_i
32
+ else
33
+ Utils.to_integer(input)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class ResourceLimits
3
+ attr_accessor :render_length, :render_score, :assign_score,
4
+ :render_length_limit, :render_score_limit, :assign_score_limit
5
+
6
+ def initialize(limits)
7
+ @render_length_limit = limits[:render_length_limit]
8
+ @render_score_limit = limits[:render_score_limit]
9
+ @assign_score_limit = limits[:assign_score_limit]
10
+ reset
11
+ end
12
+
13
+ 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)
17
+ end
18
+
19
+ def reset
20
+ @render_length = @render_score = @assign_score = 0
21
+ end
22
+ end
23
+ end
@@ -2,12 +2,24 @@ require 'cgi'
2
2
  require 'bigdecimal'
3
3
 
4
4
  module Liquid
5
-
6
5
  module StandardFilters
6
+ HTML_ESCAPE = {
7
+ '&'.freeze => '&amp;'.freeze,
8
+ '>'.freeze => '&gt;'.freeze,
9
+ '<'.freeze => '&lt;'.freeze,
10
+ '"'.freeze => '&quot;'.freeze,
11
+ "'".freeze => '&#39;'.freeze
12
+ }.freeze
13
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
14
+ STRIP_HTML_BLOCKS = Regexp.union(
15
+ /<script.*?<\/script>/m,
16
+ /<!--.*?-->/m,
17
+ /<style.*?<\/style>/m
18
+ )
19
+ STRIP_HTML_TAGS = /<.*?>/m
7
20
 
8
21
  # Return the size of an array or of an string
9
22
  def size(input)
10
-
11
23
  input.respond_to?(:size) ? input.size : 0
12
24
  end
13
25
 
@@ -27,32 +39,56 @@ module Liquid
27
39
  end
28
40
 
29
41
  def escape(input)
30
- CGI.escapeHTML(input) rescue input
42
+ CGI.escapeHTML(input.to_s).untaint unless input.nil?
31
43
  end
44
+ alias_method :h, :escape
32
45
 
33
46
  def escape_once(input)
34
- ActionView::Helpers::TagHelper.escape_once(input)
35
- rescue NameError
36
- input
47
+ input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
37
48
  end
38
49
 
39
- alias_method :h, :escape
50
+ def url_encode(input)
51
+ CGI.escape(input.to_s) unless input.nil?
52
+ end
53
+
54
+ def url_decode(input)
55
+ return if input.nil?
56
+
57
+ result = CGI.unescape(input.to_s)
58
+ raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
59
+
60
+ result
61
+ end
62
+
63
+ def slice(input, offset, length = nil)
64
+ offset = Utils.to_integer(offset)
65
+ length = length ? Utils.to_integer(length) : 1
66
+
67
+ if input.is_a?(Array)
68
+ input.slice(offset, length) || []
69
+ else
70
+ input.to_s.slice(offset, length) || ''
71
+ end
72
+ end
40
73
 
41
74
  # Truncate a string down to x characters
42
- def truncate(input, length = 50, truncate_string = "...")
43
- if input.nil? then return end
44
- l = length.to_i - truncate_string.length
75
+ def truncate(input, length = 50, truncate_string = "...".freeze)
76
+ return if input.nil?
77
+ input_str = input.to_s
78
+ length = Utils.to_integer(length)
79
+ truncate_string_str = truncate_string.to_s
80
+ l = length - truncate_string_str.length
45
81
  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
82
+ input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
48
83
  end
49
84
 
50
- def truncatewords(input, words = 15, truncate_string = "...")
51
- if input.nil? then return end
85
+ def truncatewords(input, words = 15, truncate_string = "...".freeze)
86
+ return if input.nil?
52
87
  wordlist = input.to_s.split
53
- l = words.to_i - 1
88
+ words = Utils.to_integer(words)
89
+ l = words - 1
54
90
  l = 0 if l < 0
55
- wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
91
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
56
92
  end
57
93
 
58
94
  # Split input string into an array of substrings separated by given pattern.
@@ -61,75 +97,176 @@ module Liquid
61
97
  # <div class="summary">{{ post | split '//' | first }}</div>
62
98
  #
63
99
  def split(input, pattern)
64
- input.split(pattern)
100
+ input.to_s.split(pattern.to_s)
101
+ end
102
+
103
+ def strip(input)
104
+ input.to_s.strip
105
+ end
106
+
107
+ def lstrip(input)
108
+ input.to_s.lstrip
109
+ end
110
+
111
+ def rstrip(input)
112
+ input.to_s.rstrip
65
113
  end
66
114
 
67
115
  def strip_html(input)
68
- input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
116
+ empty = ''.freeze
117
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
118
+ result.gsub!(STRIP_HTML_TAGS, empty)
119
+ result
69
120
  end
70
121
 
71
122
  # Remove all newlines from the string
72
123
  def strip_newlines(input)
73
- input.to_s.gsub(/\r?\n/, '')
124
+ input.to_s.gsub(/\r?\n/, ''.freeze)
74
125
  end
75
126
 
76
127
  # Join elements of the array with certain character between them
77
- def join(input, glue = ' ')
78
- [input].flatten.join(glue)
128
+ def join(input, glue = ' '.freeze)
129
+ InputIterator.new(input).join(glue)
79
130
  end
80
131
 
81
132
  # Sort elements of the array
82
133
  # provide optional property with which to sort an array of hashes or drops
83
134
  def sort(input, property = nil)
84
- ary = [input].flatten
135
+ ary = InputIterator.new(input)
136
+
137
+ return [] if ary.empty?
138
+
85
139
  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) }
140
+ ary.sort do |a, b|
141
+ nil_safe_compare(a, b)
142
+ end
143
+ elsif ary.all? { |el| el.respond_to?(:[]) }
144
+ begin
145
+ ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
146
+ rescue TypeError
147
+ raise_property_error(property)
148
+ end
149
+ end
150
+ end
151
+
152
+ # Sort elements of an array ignoring case if strings
153
+ # provide optional property with which to sort an array of hashes or drops
154
+ def sort_natural(input, property = nil)
155
+ ary = InputIterator.new(input)
156
+
157
+ return [] if ary.empty?
158
+
159
+ if property.nil?
160
+ ary.sort do |a, b|
161
+ nil_safe_casecmp(a, b)
162
+ end
163
+ elsif ary.all? { |el| el.respond_to?(:[]) }
164
+ begin
165
+ ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
166
+ rescue TypeError
167
+ raise_property_error(property)
168
+ end
169
+ end
170
+ end
171
+
172
+ # Filter the elements of an array to those with a certain property value.
173
+ # By default the target is any truthy value.
174
+ def where(input, property, target_value = nil)
175
+ ary = InputIterator.new(input)
176
+
177
+ if ary.empty?
178
+ []
179
+ elsif ary.first.respond_to?(:[]) && target_value.nil?
180
+ begin
181
+ ary.select { |item| item[property] }
182
+ rescue TypeError
183
+ raise_property_error(property)
184
+ end
185
+ elsif ary.first.respond_to?(:[])
186
+ begin
187
+ ary.select { |item| item[property] == target_value }
188
+ rescue TypeError
189
+ raise_property_error(property)
190
+ end
191
+ end
192
+ end
193
+
194
+ # Remove duplicate elements from an array
195
+ # provide optional property with which to determine uniqueness
196
+ def uniq(input, property = nil)
197
+ ary = InputIterator.new(input)
198
+
199
+ if property.nil?
200
+ ary.uniq
201
+ elsif ary.empty? # The next two cases assume a non-empty array.
202
+ []
203
+ elsif ary.first.respond_to?(:[])
204
+ begin
205
+ ary.uniq { |a| a[property] }
206
+ rescue TypeError
207
+ raise_property_error(property)
208
+ end
91
209
  end
92
210
  end
93
211
 
94
212
  # Reverse the elements of an array
95
213
  def reverse(input)
96
- ary = [input].flatten
214
+ ary = InputIterator.new(input)
97
215
  ary.reverse
98
216
  end
99
217
 
100
218
  # map/collect on a given property
101
219
  def map(input, property)
102
- ary = [input].flatten
103
- ary.map do |e|
220
+ InputIterator.new(input).map do |e|
104
221
  e = e.call if e.is_a?(Proc)
105
- e = e.to_liquid if e.respond_to?(:to_liquid)
106
222
 
107
- if property == "to_liquid"
223
+ if property == "to_liquid".freeze
108
224
  e
109
225
  elsif e.respond_to?(:[])
110
- e[property]
226
+ r = e[property]
227
+ r.is_a?(Proc) ? r.call : r
228
+ end
229
+ end
230
+ rescue TypeError
231
+ raise_property_error(property)
232
+ end
233
+
234
+ # Remove nils within an array
235
+ # provide optional property with which to check for nil
236
+ def compact(input, property = nil)
237
+ ary = InputIterator.new(input)
238
+
239
+ if property.nil?
240
+ ary.compact
241
+ elsif ary.empty? # The next two cases assume a non-empty array.
242
+ []
243
+ elsif ary.first.respond_to?(:[])
244
+ begin
245
+ ary.reject { |a| a[property].nil? }
246
+ rescue TypeError
247
+ raise_property_error(property)
111
248
  end
112
249
  end
113
250
  end
114
251
 
115
252
  # Replace occurrences of a string with another
116
- def replace(input, string, replacement = '')
117
- input.to_s.gsub(string, replacement.to_s)
253
+ def replace(input, string, replacement = ''.freeze)
254
+ input.to_s.gsub(string.to_s, replacement.to_s)
118
255
  end
119
256
 
120
257
  # Replace the first occurrences of a string with another
121
- def replace_first(input, string, replacement = '')
122
- input.to_s.sub(string, replacement.to_s)
258
+ def replace_first(input, string, replacement = ''.freeze)
259
+ input.to_s.sub(string.to_s, replacement.to_s)
123
260
  end
124
261
 
125
262
  # remove a substring
126
263
  def remove(input, string)
127
- input.to_s.gsub(string, '')
264
+ input.to_s.gsub(string.to_s, ''.freeze)
128
265
  end
129
266
 
130
267
  # remove the first occurrences of a substring
131
268
  def remove_first(input, string)
132
- input.to_s.sub(string, '')
269
+ input.to_s.sub(string.to_s, ''.freeze)
133
270
  end
134
271
 
135
272
  # add one string to another
@@ -137,6 +274,13 @@ module Liquid
137
274
  input.to_s + string.to_s
138
275
  end
139
276
 
277
+ def concat(input, array)
278
+ unless array.respond_to?(:to_ary)
279
+ raise ArgumentError.new("concat filter requires an array argument")
280
+ end
281
+ InputIterator.new(input).concat(array)
282
+ end
283
+
140
284
  # prepend a string to another
141
285
  def prepend(input, string)
142
286
  string.to_s + input.to_s
@@ -144,10 +288,10 @@ module Liquid
144
288
 
145
289
  # Add <br /> tags in front of all newlines in input string
146
290
  def newline_to_br(input)
147
- input.to_s.gsub(/\n/, "<br />\n")
291
+ input.to_s.gsub(/\n/, "<br />\n".freeze)
148
292
  end
149
293
 
150
- # Reformat a date
294
+ # Reformat a date using Ruby's core Time#strftime( string ) -> string
151
295
  #
152
296
  # %a - The abbreviated weekday name (``Sun'')
153
297
  # %A - The full weekday name (``Sunday'')
@@ -161,6 +305,7 @@ module Liquid
161
305
  # %m - Month of the year (01..12)
162
306
  # %M - Minute of the hour (00..59)
163
307
  # %p - Meridian indicator (``AM'' or ``PM'')
308
+ # %s - Number of seconds since 1970-01-01 00:00:00 UTC.
164
309
  # %S - Second of the minute (00..60)
165
310
  # %U - Week number of the current year,
166
311
  # starting with the first Sunday as the first
@@ -175,34 +320,14 @@ module Liquid
175
320
  # %Y - Year with century
176
321
  # %Z - Time zone name
177
322
  # %% - Literal ``%'' character
323
+ #
324
+ # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
178
325
  def date(input, format)
326
+ return input if format.to_s.empty?
179
327
 
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
328
+ return input unless date = Utils.to_date(input)
198
329
 
199
- if date.respond_to?(:strftime)
200
- date.strftime(format.to_s)
201
- else
202
- input
203
- end
204
- rescue
205
- input
330
+ date.strftime(format.to_s)
206
331
  end
207
332
 
208
333
  # Get the first element of the passed in array
@@ -223,6 +348,12 @@ module Liquid
223
348
  array.last if array.respond_to?(:last)
224
349
  end
225
350
 
351
+ # absolute value
352
+ def abs(input)
353
+ result = Utils.to_number(input).abs
354
+ result.is_a?(BigDecimal) ? result.to_f : result
355
+ end
356
+
226
357
  # addition
227
358
  def plus(input, operand)
228
359
  apply_operation(input, operand, :+)
@@ -241,31 +372,134 @@ module Liquid
241
372
  # division
242
373
  def divided_by(input, operand)
243
374
  apply_operation(input, operand, :/)
375
+ rescue ::ZeroDivisionError => e
376
+ raise Liquid::ZeroDivisionError, e.message
244
377
  end
245
378
 
246
379
  def modulo(input, operand)
247
380
  apply_operation(input, operand, :%)
381
+ rescue ::ZeroDivisionError => e
382
+ raise Liquid::ZeroDivisionError, e.message
248
383
  end
249
384
 
250
- private
385
+ def round(input, n = 0)
386
+ result = Utils.to_number(input).round(Utils.to_number(n))
387
+ result = result.to_f if result.is_a?(BigDecimal)
388
+ result = result.to_i if n == 0
389
+ result
390
+ rescue ::FloatDomainError => e
391
+ raise Liquid::FloatDomainError, e.message
392
+ end
393
+
394
+ def ceil(input)
395
+ Utils.to_number(input).ceil.to_i
396
+ rescue ::FloatDomainError => e
397
+ raise Liquid::FloatDomainError, e.message
398
+ end
251
399
 
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
400
+ def floor(input)
401
+ Utils.to_number(input).floor.to_i
402
+ rescue ::FloatDomainError => e
403
+ raise Liquid::FloatDomainError, e.message
404
+ end
405
+
406
+ def at_least(input, n)
407
+ min_value = Utils.to_number(n)
408
+
409
+ result = Utils.to_number(input)
410
+ result = min_value if min_value > result
411
+ result.is_a?(BigDecimal) ? result.to_f : result
412
+ end
413
+
414
+ def at_most(input, n)
415
+ max_value = Utils.to_number(n)
416
+
417
+ result = Utils.to_number(input)
418
+ result = max_value if max_value < result
419
+ result.is_a?(BigDecimal) ? result.to_f : result
420
+ end
421
+
422
+ def default(input, default_value = ''.freeze)
423
+ if !input || input.respond_to?(:empty?) && input.empty?
424
+ default_value
260
425
  else
261
- 0
426
+ input
262
427
  end
263
428
  end
264
429
 
430
+ private
431
+
432
+ def raise_property_error(property)
433
+ raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
434
+ end
435
+
265
436
  def apply_operation(input, operand, operation)
266
- result = to_number(input).send(operation, to_number(operand))
437
+ result = Utils.to_number(input).send(operation, Utils.to_number(operand))
267
438
  result.is_a?(BigDecimal) ? result.to_f : result
268
439
  end
440
+
441
+ def nil_safe_compare(a, b)
442
+ if !a.nil? && !b.nil?
443
+ a <=> b
444
+ else
445
+ a.nil? ? 1 : -1
446
+ end
447
+ end
448
+
449
+ def nil_safe_casecmp(a, b)
450
+ if !a.nil? && !b.nil?
451
+ a.to_s.casecmp(b.to_s)
452
+ else
453
+ a.nil? ? 1 : -1
454
+ end
455
+ end
456
+
457
+ class InputIterator
458
+ include Enumerable
459
+
460
+ def initialize(input)
461
+ @input = if input.is_a?(Array)
462
+ input.flatten
463
+ elsif input.is_a?(Hash)
464
+ [input]
465
+ elsif input.is_a?(Enumerable)
466
+ input
467
+ else
468
+ Array(input)
469
+ end
470
+ end
471
+
472
+ def join(glue)
473
+ to_a.join(glue.to_s)
474
+ end
475
+
476
+ def concat(args)
477
+ to_a.concat(args)
478
+ end
479
+
480
+ def reverse
481
+ reverse_each.to_a
482
+ end
483
+
484
+ def uniq(&block)
485
+ to_a.uniq(&block)
486
+ end
487
+
488
+ def compact
489
+ to_a.compact
490
+ end
491
+
492
+ def empty?
493
+ @input.each { return false }
494
+ true
495
+ end
496
+
497
+ def each
498
+ @input.each do |e|
499
+ yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
500
+ end
501
+ end
502
+ end
269
503
  end
270
504
 
271
505
  Template.register_filter(StandardFilters)