liquid 3.0.6 → 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 (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