liquid 3.0.6 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +154 -58
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +33 -0
  5. data/lib/liquid/block.rb +42 -125
  6. data/lib/liquid/block_body.rb +99 -79
  7. data/lib/liquid/condition.rb +52 -32
  8. data/lib/liquid/context.rb +57 -51
  9. data/lib/liquid/document.rb +19 -9
  10. data/lib/liquid/drop.rb +17 -16
  11. data/lib/liquid/errors.rb +20 -24
  12. data/lib/liquid/expression.rb +26 -10
  13. data/lib/liquid/extensions.rb +19 -7
  14. data/lib/liquid/file_system.rb +11 -11
  15. data/lib/liquid/forloop_drop.rb +42 -0
  16. data/lib/liquid/i18n.rb +6 -6
  17. data/lib/liquid/interrupts.rb +1 -2
  18. data/lib/liquid/lexer.rb +12 -8
  19. data/lib/liquid/locales/en.yml +6 -2
  20. data/lib/liquid/parse_context.rb +38 -0
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser_switching.rb +4 -4
  23. data/lib/liquid/profiler/hooks.rb +7 -7
  24. data/lib/liquid/profiler.rb +18 -19
  25. data/lib/liquid/range_lookup.rb +16 -1
  26. data/lib/liquid/resource_limits.rb +23 -0
  27. data/lib/liquid/standardfilters.rb +207 -61
  28. data/lib/liquid/strainer.rb +15 -8
  29. data/lib/liquid/tablerowloop_drop.rb +62 -0
  30. data/lib/liquid/tag.rb +9 -8
  31. data/lib/liquid/tags/assign.rb +25 -4
  32. data/lib/liquid/tags/break.rb +0 -3
  33. data/lib/liquid/tags/capture.rb +1 -1
  34. data/lib/liquid/tags/case.rb +27 -12
  35. data/lib/liquid/tags/comment.rb +2 -2
  36. data/lib/liquid/tags/cycle.rb +16 -8
  37. data/lib/liquid/tags/decrement.rb +1 -4
  38. data/lib/liquid/tags/for.rb +103 -75
  39. data/lib/liquid/tags/if.rb +60 -44
  40. data/lib/liquid/tags/ifchanged.rb +0 -2
  41. data/lib/liquid/tags/include.rb +71 -51
  42. data/lib/liquid/tags/raw.rb +32 -4
  43. data/lib/liquid/tags/table_row.rb +21 -31
  44. data/lib/liquid/tags/unless.rb +3 -4
  45. data/lib/liquid/template.rb +42 -54
  46. data/lib/liquid/tokenizer.rb +31 -0
  47. data/lib/liquid/truffle.rb +5 -0
  48. data/lib/liquid/utils.rb +52 -8
  49. data/lib/liquid/variable.rb +59 -46
  50. data/lib/liquid/variable_lookup.rb +14 -6
  51. data/lib/liquid/version.rb +2 -1
  52. data/lib/liquid.rb +10 -7
  53. data/test/integration/assign_test.rb +8 -8
  54. data/test/integration/blank_test.rb +14 -14
  55. data/test/integration/block_test.rb +12 -0
  56. data/test/integration/context_test.rb +2 -2
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +42 -40
  59. data/test/integration/error_handling_test.rb +96 -43
  60. data/test/integration/filter_test.rb +60 -20
  61. data/test/integration/hash_ordering_test.rb +9 -9
  62. data/test/integration/output_test.rb +26 -27
  63. data/test/integration/parse_tree_visitor_test.rb +247 -0
  64. data/test/integration/parsing_quirks_test.rb +19 -13
  65. data/test/integration/render_profiling_test.rb +20 -20
  66. data/test/integration/security_test.rb +23 -7
  67. data/test/integration/standard_filter_test.rb +426 -46
  68. data/test/integration/tags/break_tag_test.rb +1 -2
  69. data/test/integration/tags/continue_tag_test.rb +0 -1
  70. data/test/integration/tags/for_tag_test.rb +135 -100
  71. data/test/integration/tags/if_else_tag_test.rb +75 -77
  72. data/test/integration/tags/include_tag_test.rb +50 -31
  73. data/test/integration/tags/increment_tag_test.rb +10 -11
  74. data/test/integration/tags/raw_tag_test.rb +7 -1
  75. data/test/integration/tags/standard_tag_test.rb +121 -122
  76. data/test/integration/tags/statements_test.rb +3 -5
  77. data/test/integration/tags/table_row_test.rb +20 -19
  78. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  79. data/test/integration/template_test.rb +199 -49
  80. data/test/integration/trim_mode_test.rb +529 -0
  81. data/test/integration/variable_test.rb +27 -13
  82. data/test/test_helper.rb +33 -6
  83. data/test/truffle/truffle_test.rb +9 -0
  84. data/test/unit/block_unit_test.rb +8 -5
  85. data/test/unit/condition_unit_test.rb +94 -77
  86. data/test/unit/context_unit_test.rb +69 -72
  87. data/test/unit/file_system_unit_test.rb +3 -3
  88. data/test/unit/i18n_unit_test.rb +2 -2
  89. data/test/unit/lexer_unit_test.rb +12 -9
  90. data/test/unit/parser_unit_test.rb +2 -2
  91. data/test/unit/regexp_unit_test.rb +1 -1
  92. data/test/unit/strainer_unit_test.rb +96 -1
  93. data/test/unit/tag_unit_test.rb +7 -2
  94. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  95. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  96. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  97. data/test/unit/template_unit_test.rb +14 -5
  98. data/test/unit/tokenizer_unit_test.rb +24 -7
  99. data/test/unit/variable_unit_test.rb +60 -43
  100. metadata +62 -50
  101. data/lib/liquid/module_ex.rb +0 -62
  102. data/lib/liquid/token.rb +0 -18
  103. data/test/unit/module_ex_unit_test.rb +0 -87
@@ -1,9 +1,11 @@
1
- module Liquid
1
+ require 'liquid/profiler/hooks'
2
2
 
3
+ module Liquid
3
4
  # Profiler enables support for profiling template rendering to help track down performance issues.
4
5
  #
5
- # To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
6
- # <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
6
+ # To enable profiling, first require 'liquid/profiler'.
7
+ # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
8
+ # After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
7
9
  # class via the <tt>Liquid::Template#profiler</tt> method.
8
10
  #
9
11
  # template = Liquid::Template.parse(template_content, profile: true)
@@ -17,7 +19,7 @@ module Liquid
17
19
  # inside of <tt>{% include %}</tt> tags.
18
20
  #
19
21
  # profile.each do |node|
20
- # # Access to the token itself
22
+ # # Access to the node itself
21
23
  # node.code
22
24
  #
23
25
  # # Which template and line number of this node.
@@ -44,17 +46,15 @@ module Liquid
44
46
  class Timing
45
47
  attr_reader :code, :partial, :line_number, :children
46
48
 
47
- def initialize(token, partial)
48
- @code = token.respond_to?(:raw) ? token.raw : token
49
+ def initialize(node, partial)
50
+ @code = node.respond_to?(:raw) ? node.raw : node
49
51
  @partial = partial
50
- @line_number = token.respond_to?(:line_number) ? token.line_number : nil
52
+ @line_number = node.respond_to?(:line_number) ? node.line_number : nil
51
53
  @children = []
52
54
  end
53
55
 
54
- def self.start(token, partial)
55
- new(token, partial).tap do |t|
56
- t.start
57
- end
56
+ def self.start(node, partial)
57
+ new(node, partial).tap(&:start)
58
58
  end
59
59
 
60
60
  def start
@@ -70,11 +70,11 @@ module Liquid
70
70
  end
71
71
  end
72
72
 
73
- def self.profile_token_render(token)
74
- if Profiler.current_profile && token.respond_to?(:render)
75
- Profiler.current_profile.start_token(token)
73
+ def self.profile_node_render(node)
74
+ if Profiler.current_profile && node.respond_to?(:render)
75
+ Profiler.current_profile.start_node(node)
76
76
  output = yield
77
- Profiler.current_profile.end_token(token)
77
+ Profiler.current_profile.end_node(node)
78
78
  output
79
79
  else
80
80
  yield
@@ -132,11 +132,11 @@ module Liquid
132
132
  @root_timing.children.length
133
133
  end
134
134
 
135
- def start_token(token)
136
- @timing_stack.push(Timing.start(token, current_partial))
135
+ def start_node(node)
136
+ @timing_stack.push(Timing.start(node, current_partial))
137
137
  end
138
138
 
139
- def end_token(token)
139
+ def end_node(_node)
140
140
  timing = @timing_stack.pop
141
141
  timing.finish
142
142
 
@@ -154,6 +154,5 @@ module Liquid
154
154
  def pop_partial
155
155
  @partial_stack.pop
156
156
  end
157
-
158
157
  end
159
158
  end
@@ -16,7 +16,22 @@ module Liquid
16
16
  end
17
17
 
18
18
  def evaluate(context)
19
- context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
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
20
35
  end
21
36
  end
22
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,7 +2,6 @@ require 'cgi'
2
2
  require 'bigdecimal'
3
3
 
4
4
  module Liquid
5
-
6
5
  module StandardFilters
7
6
  HTML_ESCAPE = {
8
7
  '&'.freeze => '&amp;'.freeze,
@@ -10,8 +9,14 @@ module Liquid
10
9
  '<'.freeze => '&lt;'.freeze,
11
10
  '"'.freeze => '&quot;'.freeze,
12
11
  "'".freeze => '&#39;'.freeze
13
- }
12
+ }.freeze
14
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
15
20
 
16
21
  # Return the size of an array or of an string
17
22
  def size(input)
@@ -34,7 +39,7 @@ module Liquid
34
39
  end
35
40
 
36
41
  def escape(input)
37
- CGI.escapeHTML(input).untaint rescue input
42
+ CGI.escapeHTML(input.to_s).untaint unless input.nil?
38
43
  end
39
44
  alias_method :h, :escape
40
45
 
@@ -43,12 +48,21 @@ module Liquid
43
48
  end
44
49
 
45
50
  def url_encode(input)
46
- CGI.escape(input) rescue input
51
+ CGI.escape(input.to_s) unless input.nil?
47
52
  end
48
53
 
49
- def slice(input, offset, length=nil)
50
- offset = Integer(offset)
51
- length = length ? Integer(length) : 1
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
52
66
 
53
67
  if input.is_a?(Array)
54
68
  input.slice(offset, length) || []
@@ -59,18 +73,22 @@ module Liquid
59
73
 
60
74
  # Truncate a string down to x characters
61
75
  def truncate(input, length = 50, truncate_string = "...".freeze)
62
- if input.nil? then return end
63
- l = length.to_i - truncate_string.length
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
64
81
  l = 0 if l < 0
65
- input.length > length.to_i ? input[0...l] + truncate_string : input
82
+ input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
66
83
  end
67
84
 
68
85
  def truncatewords(input, words = 15, truncate_string = "...".freeze)
69
- if input.nil? then return end
86
+ return if input.nil?
70
87
  wordlist = input.to_s.split
71
- l = words.to_i - 1
88
+ words = Utils.to_integer(words)
89
+ l = words - 1
72
90
  l = 0 if l < 0
73
- wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
91
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
74
92
  end
75
93
 
76
94
  # Split input string into an array of substrings separated by given pattern.
@@ -79,7 +97,7 @@ module Liquid
79
97
  # <div class="summary">{{ post | split '//' | first }}</div>
80
98
  #
81
99
  def split(input, pattern)
82
- input.to_s.split(pattern)
100
+ input.to_s.split(pattern.to_s)
83
101
  end
84
102
 
85
103
  def strip(input)
@@ -96,7 +114,9 @@ module Liquid
96
114
 
97
115
  def strip_html(input)
98
116
  empty = ''.freeze
99
- input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
117
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
118
+ result.gsub!(STRIP_HTML_TAGS, empty)
119
+ result
100
120
  end
101
121
 
102
122
  # Remove all newlines from the string
@@ -113,12 +133,61 @@ module Liquid
113
133
  # provide optional property with which to sort an array of hashes or drops
114
134
  def sort(input, property = nil)
115
135
  ary = InputIterator.new(input)
136
+
137
+ return [] if ary.empty?
138
+
139
+ if property.nil?
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
+
116
159
  if property.nil?
117
- ary.sort
118
- elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
119
- ary.sort {|a,b| a[property] <=> b[property] }
120
- elsif ary.first.respond_to?(property)
121
- ary.sort {|a,b| a.send(property) <=> b.send(property) }
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
122
191
  end
123
192
  end
124
193
 
@@ -126,10 +195,17 @@ module Liquid
126
195
  # provide optional property with which to determine uniqueness
127
196
  def uniq(input, property = nil)
128
197
  ary = InputIterator.new(input)
198
+
129
199
  if property.nil?
130
- input.uniq
131
- elsif input.first.respond_to?(:[])
132
- input.uniq{ |a| a[property] }
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
133
209
  end
134
210
  end
135
211
 
@@ -147,29 +223,50 @@ module Liquid
147
223
  if property == "to_liquid".freeze
148
224
  e
149
225
  elsif e.respond_to?(:[])
150
- 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)
151
248
  end
152
249
  end
153
250
  end
154
251
 
155
252
  # Replace occurrences of a string with another
156
253
  def replace(input, string, replacement = ''.freeze)
157
- input.to_s.gsub(string, replacement.to_s)
254
+ input.to_s.gsub(string.to_s, replacement.to_s)
158
255
  end
159
256
 
160
257
  # Replace the first occurrences of a string with another
161
258
  def replace_first(input, string, replacement = ''.freeze)
162
- input.to_s.sub(string, replacement.to_s)
259
+ input.to_s.sub(string.to_s, replacement.to_s)
163
260
  end
164
261
 
165
262
  # remove a substring
166
263
  def remove(input, string)
167
- input.to_s.gsub(string, ''.freeze)
264
+ input.to_s.gsub(string.to_s, ''.freeze)
168
265
  end
169
266
 
170
267
  # remove the first occurrences of a substring
171
268
  def remove_first(input, string)
172
- input.to_s.sub(string, ''.freeze)
269
+ input.to_s.sub(string.to_s, ''.freeze)
173
270
  end
174
271
 
175
272
  # add one string to another
@@ -177,6 +274,13 @@ module Liquid
177
274
  input.to_s + string.to_s
178
275
  end
179
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
+
180
284
  # prepend a string to another
181
285
  def prepend(input, string)
182
286
  string.to_s + input.to_s
@@ -221,7 +325,7 @@ module Liquid
221
325
  def date(input, format)
222
326
  return input if format.to_s.empty?
223
327
 
224
- return input unless date = to_date(input)
328
+ return input unless date = Utils.to_date(input)
225
329
 
226
330
  date.strftime(format.to_s)
227
331
  end
@@ -244,6 +348,12 @@ module Liquid
244
348
  array.last if array.respond_to?(:last)
245
349
  end
246
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
+
247
357
  # addition
248
358
  def plus(input, operand)
249
359
  apply_operation(input, operand, :+)
@@ -262,69 +372,88 @@ module Liquid
262
372
  # division
263
373
  def divided_by(input, operand)
264
374
  apply_operation(input, operand, :/)
375
+ rescue ::ZeroDivisionError => e
376
+ raise Liquid::ZeroDivisionError, e.message
265
377
  end
266
378
 
267
379
  def modulo(input, operand)
268
380
  apply_operation(input, operand, :%)
381
+ rescue ::ZeroDivisionError => e
382
+ raise Liquid::ZeroDivisionError, e.message
269
383
  end
270
384
 
271
385
  def round(input, n = 0)
272
- result = to_number(input).round(to_number(n))
386
+ result = Utils.to_number(input).round(Utils.to_number(n))
273
387
  result = result.to_f if result.is_a?(BigDecimal)
274
388
  result = result.to_i if n == 0
275
389
  result
390
+ rescue ::FloatDomainError => e
391
+ raise Liquid::FloatDomainError, e.message
276
392
  end
277
393
 
278
394
  def ceil(input)
279
- to_number(input).ceil.to_i
395
+ Utils.to_number(input).ceil.to_i
396
+ rescue ::FloatDomainError => e
397
+ raise Liquid::FloatDomainError, e.message
280
398
  end
281
399
 
282
400
  def floor(input)
283
- to_number(input).floor.to_i
401
+ Utils.to_number(input).floor.to_i
402
+ rescue ::FloatDomainError => e
403
+ raise Liquid::FloatDomainError, e.message
284
404
  end
285
405
 
286
- def default(input, default_value = "".freeze)
287
- is_blank = input.respond_to?(:empty?) ? input.empty? : !input
288
- is_blank ? default_value : input
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
289
412
  end
290
413
 
291
- private
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
292
421
 
293
- def to_number(obj)
294
- case obj
295
- when Float
296
- BigDecimal.new(obj.to_s)
297
- when Numeric
298
- obj
299
- when String
300
- (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
422
+ def default(input, default_value = ''.freeze)
423
+ if !input || input.respond_to?(:empty?) && input.empty?
424
+ default_value
301
425
  else
302
- 0
426
+ input
303
427
  end
304
428
  end
305
429
 
306
- def to_date(obj)
307
- return obj if obj.respond_to?(:strftime)
430
+ private
308
431
 
309
- case obj
310
- when 'now'.freeze, 'today'.freeze
311
- Time.now
312
- when /\A\d+\z/, Integer
313
- Time.at(obj.to_i)
314
- when String
315
- Time.parse(obj)
316
- else
317
- nil
318
- end
319
- rescue ::ArgumentError
320
- nil
432
+ def raise_property_error(property)
433
+ raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
321
434
  end
322
435
 
323
436
  def apply_operation(input, operand, operation)
324
- result = to_number(input).send(operation, to_number(operand))
437
+ result = Utils.to_number(input).send(operation, Utils.to_number(operand))
325
438
  result.is_a?(BigDecimal) ? result.to_f : result
326
439
  end
327
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
+
328
457
  class InputIterator
329
458
  include Enumerable
330
459
 
@@ -341,13 +470,30 @@ module Liquid
341
470
  end
342
471
 
343
472
  def join(glue)
344
- to_a.join(glue)
473
+ to_a.join(glue.to_s)
474
+ end
475
+
476
+ def concat(args)
477
+ to_a.concat(args)
345
478
  end
346
479
 
347
480
  def reverse
348
481
  reverse_each.to_a
349
482
  end
350
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
+
351
497
  def each
352
498
  @input.each do |e|
353
499
  yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
@@ -1,7 +1,6 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Strainer is the parent class for the filters system.
6
5
  # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
7
6
  #
@@ -22,19 +21,25 @@ module Liquid
22
21
  @context = context
23
22
  end
24
23
 
25
- def self.filter_methods
26
- @filter_methods
24
+ class << self
25
+ attr_reader :filter_methods
27
26
  end
28
27
 
29
28
  def self.add_filter(filter)
30
- raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
31
- unless self.class.include?(filter)
32
- self.send(:include, filter)
33
- @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
29
+ raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
30
+ unless self.include?(filter)
31
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
32
+ if invokable_non_public_methods.any?
33
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
34
+ else
35
+ send(:include, filter)
36
+ @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
37
+ end
34
38
  end
35
39
  end
36
40
 
37
41
  def self.global_filter(filter)
42
+ @@strainer_class_cache.clear
38
43
  @@global_strainer.add_filter(filter)
39
44
  end
40
45
 
@@ -49,11 +54,13 @@ module Liquid
49
54
  def invoke(method, *args)
50
55
  if self.class.invokable?(method)
51
56
  send(method, *args)
57
+ elsif @context && @context.strict_filters
58
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
52
59
  else
53
60
  args.first
54
61
  end
55
62
  rescue ::ArgumentError => e
56
- raise Liquid::ArgumentError.new(e.message)
63
+ raise Liquid::ArgumentError, e.message, e.backtrace
57
64
  end
58
65
  end
59
66
  end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ class TablerowloopDrop < Drop
3
+ def initialize(length, cols)
4
+ @length = length
5
+ @row = 1
6
+ @col = 1
7
+ @cols = cols
8
+ @index = 0
9
+ end
10
+
11
+ attr_reader :length, :col, :row
12
+
13
+ def index
14
+ @index + 1
15
+ end
16
+
17
+ def index0
18
+ @index
19
+ end
20
+
21
+ def col0
22
+ @col - 1
23
+ end
24
+
25
+ def rindex
26
+ @length - @index
27
+ end
28
+
29
+ def rindex0
30
+ @length - @index - 1
31
+ end
32
+
33
+ def first
34
+ @index == 0
35
+ end
36
+
37
+ def last
38
+ @index == @length - 1
39
+ end
40
+
41
+ def col_first
42
+ @col == 1
43
+ end
44
+
45
+ def col_last
46
+ @col == @cols
47
+ end
48
+
49
+ protected
50
+
51
+ def increment!
52
+ @index += 1
53
+
54
+ if @col == @cols
55
+ @col = 1
56
+ @row += 1
57
+ else
58
+ @col += 1
59
+ end
60
+ end
61
+ end
62
+ end