liquid 2.6.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
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)