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
@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
41
41
  end
42
42
  end
43
43
 
44
+ class NumberLikeThing < Liquid::Drop
45
+ def initialize(amount)
46
+ @amount = amount
47
+ end
48
+
49
+ def to_number
50
+ @amount
51
+ end
52
+ end
53
+
44
54
  class StandardFiltersTest < Minitest::Test
45
55
  include Liquid
46
56
 
@@ -49,7 +59,7 @@ class StandardFiltersTest < Minitest::Test
49
59
  end
50
60
 
51
61
  def test_size
52
- assert_equal 3, @filters.size([1,2,3])
62
+ assert_equal 3, @filters.size([1, 2, 3])
53
63
  assert_equal 0, @filters.size([])
54
64
  assert_equal 0, @filters.size(nil)
55
65
  end
@@ -76,20 +86,27 @@ class StandardFiltersTest < Minitest::Test
76
86
  assert_equal '', @filters.slice(nil, 0)
77
87
  assert_equal '', @filters.slice('foobar', 100, 10)
78
88
  assert_equal '', @filters.slice('foobar', -100, 10)
89
+ assert_equal 'oob', @filters.slice('foobar', '1', '3')
90
+ assert_raises(Liquid::ArgumentError) do
91
+ @filters.slice('foobar', nil)
92
+ end
93
+ assert_raises(Liquid::ArgumentError) do
94
+ @filters.slice('foobar', 0, "")
95
+ end
79
96
  end
80
97
 
81
98
  def test_slice_on_arrays
82
99
  input = 'foobar'.split(//)
83
- assert_equal %w{o o b}, @filters.slice(input, 1, 3)
84
- assert_equal %w{o o b a r}, @filters.slice(input, 1, 1000)
85
- assert_equal %w{}, @filters.slice(input, 1, 0)
86
- assert_equal %w{o}, @filters.slice(input, 1, 1)
87
- assert_equal %w{b a r}, @filters.slice(input, 3, 3)
88
- assert_equal %w{a r}, @filters.slice(input, -2, 2)
89
- assert_equal %w{a r}, @filters.slice(input, -2, 1000)
90
- assert_equal %w{r}, @filters.slice(input, -1)
91
- assert_equal %w{}, @filters.slice(input, 100, 10)
92
- assert_equal %w{}, @filters.slice(input, -100, 10)
100
+ assert_equal %w(o o b), @filters.slice(input, 1, 3)
101
+ assert_equal %w(o o b a r), @filters.slice(input, 1, 1000)
102
+ assert_equal %w(), @filters.slice(input, 1, 0)
103
+ assert_equal %w(o), @filters.slice(input, 1, 1)
104
+ assert_equal %w(b a r), @filters.slice(input, 3, 3)
105
+ assert_equal %w(a r), @filters.slice(input, -2, 2)
106
+ assert_equal %w(a r), @filters.slice(input, -2, 1000)
107
+ assert_equal %w(r), @filters.slice(input, -1)
108
+ assert_equal %w(), @filters.slice(input, 100, 10)
109
+ assert_equal %w(), @filters.slice(input, -100, 10)
93
110
  end
94
111
 
95
112
  def test_truncate
@@ -98,20 +115,29 @@ class StandardFiltersTest < Minitest::Test
98
115
  assert_equal '...', @filters.truncate('1234567890', 0)
99
116
  assert_equal '1234567890', @filters.truncate('1234567890')
100
117
  assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
118
+ assert_equal '12341', @filters.truncate("1234567890", 5, 1)
101
119
  end
102
120
 
103
121
  def test_split
104
- assert_equal ['12','34'], @filters.split('12~34', '~')
105
- assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
122
+ assert_equal ['12', '34'], @filters.split('12~34', '~')
123
+ assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
106
124
  assert_equal ['A?Z'], @filters.split('A?Z', '~')
107
- # Regexp works although Liquid does not support.
108
- assert_equal ['A','Z'], @filters.split('AxZ', /x/)
109
125
  assert_equal [], @filters.split(nil, ' ')
126
+ assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
110
127
  end
111
128
 
112
129
  def test_escape
113
130
  assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
131
+ assert_equal '1', @filters.escape(1)
132
+ assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
133
+ assert_nil @filters.escape(nil)
134
+ end
135
+
136
+ def test_h
114
137
  assert_equal '&lt;strong&gt;', @filters.h('<strong>')
138
+ assert_equal '1', @filters.h(1)
139
+ assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
140
+ assert_nil @filters.h(nil)
115
141
  end
116
142
 
117
143
  def test_escape_once
@@ -120,7 +146,22 @@ class StandardFiltersTest < Minitest::Test
120
146
 
121
147
  def test_url_encode
122
148
  assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
123
- assert_equal nil, @filters.url_encode(nil)
149
+ assert_equal '1', @filters.url_encode(1)
150
+ assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
151
+ assert_nil @filters.url_encode(nil)
152
+ end
153
+
154
+ def test_url_decode
155
+ assert_equal 'foo bar', @filters.url_decode('foo+bar')
156
+ assert_equal 'foo bar', @filters.url_decode('foo%20bar')
157
+ assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
158
+ assert_equal '1', @filters.url_decode(1)
159
+ assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
160
+ assert_nil @filters.url_decode(nil)
161
+ exception = assert_raises Liquid::ArgumentError do
162
+ @filters.url_decode('%ff')
163
+ end
164
+ assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
124
165
  end
125
166
 
126
167
  def test_truncatewords
@@ -129,6 +170,7 @@ class StandardFiltersTest < Minitest::Test
129
170
  assert_equal 'one two three', @filters.truncatewords('one two three')
130
171
  assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
131
172
  assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
173
+ assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
132
174
  end
133
175
 
134
176
  def test_strip_html
@@ -139,48 +181,191 @@ class StandardFiltersTest < Minitest::Test
139
181
  assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
140
182
  assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
141
183
  assert_equal '', @filters.strip_html(nil)
184
+
185
+ # Quirk of the existing implementation
186
+ assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
142
187
  end
143
188
 
144
189
  def test_join
145
- assert_equal '1 2 3 4', @filters.join([1,2,3,4])
146
- assert_equal '1 - 2 - 3 - 4', @filters.join([1,2,3,4], ' - ')
190
+ assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
191
+ assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
192
+ assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
147
193
  end
148
194
 
149
195
  def test_sort
150
- assert_equal [1,2,3,4], @filters.sort([4,3,2,1])
151
- assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
196
+ assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
197
+ assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
198
+ end
199
+
200
+ def test_sort_with_nils
201
+ assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
202
+ assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
203
+ end
204
+
205
+ def test_sort_when_property_is_sometimes_missing_puts_nils_last
206
+ input = [
207
+ { "price" => 4, "handle" => "alpha" },
208
+ { "handle" => "beta" },
209
+ { "price" => 1, "handle" => "gamma" },
210
+ { "handle" => "delta" },
211
+ { "price" => 2, "handle" => "epsilon" }
212
+ ]
213
+ expectation = [
214
+ { "price" => 1, "handle" => "gamma" },
215
+ { "price" => 2, "handle" => "epsilon" },
216
+ { "price" => 4, "handle" => "alpha" },
217
+ { "handle" => "delta" },
218
+ { "handle" => "beta" }
219
+ ]
220
+ assert_equal expectation, @filters.sort(input, "price")
221
+ end
222
+
223
+ def test_sort_natural
224
+ assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
225
+ assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
226
+ end
227
+
228
+ def test_sort_natural_with_nils
229
+ assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
230
+ assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
231
+ end
232
+
233
+ def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
234
+ input = [
235
+ { "price" => "4", "handle" => "alpha" },
236
+ { "handle" => "beta" },
237
+ { "price" => "1", "handle" => "gamma" },
238
+ { "handle" => "delta" },
239
+ { "price" => 2, "handle" => "epsilon" }
240
+ ]
241
+ expectation = [
242
+ { "price" => "1", "handle" => "gamma" },
243
+ { "price" => 2, "handle" => "epsilon" },
244
+ { "price" => "4", "handle" => "alpha" },
245
+ { "handle" => "delta" },
246
+ { "handle" => "beta" }
247
+ ]
248
+ assert_equal expectation, @filters.sort_natural(input, "price")
249
+ end
250
+
251
+ def test_sort_natural_case_check
252
+ input = [
253
+ { "key" => "X" },
254
+ { "key" => "Y" },
255
+ { "key" => "Z" },
256
+ { "fake" => "t" },
257
+ { "key" => "a" },
258
+ { "key" => "b" },
259
+ { "key" => "c" }
260
+ ]
261
+ expectation = [
262
+ { "key" => "a" },
263
+ { "key" => "b" },
264
+ { "key" => "c" },
265
+ { "key" => "X" },
266
+ { "key" => "Y" },
267
+ { "key" => "Z" },
268
+ { "fake" => "t" }
269
+ ]
270
+ assert_equal expectation, @filters.sort_natural(input, "key")
271
+ assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
272
+ end
273
+
274
+ def test_sort_empty_array
275
+ assert_equal [], @filters.sort([], "a")
276
+ end
277
+
278
+ def test_sort_invalid_property
279
+ foo = [
280
+ [1],
281
+ [2],
282
+ [3]
283
+ ]
284
+
285
+ assert_raises Liquid::ArgumentError do
286
+ @filters.sort(foo, "bar")
287
+ end
288
+ end
289
+
290
+ def test_sort_natural_empty_array
291
+ assert_equal [], @filters.sort_natural([], "a")
292
+ end
293
+
294
+ def test_sort_natural_invalid_property
295
+ foo = [
296
+ [1],
297
+ [2],
298
+ [3]
299
+ ]
300
+
301
+ assert_raises Liquid::ArgumentError do
302
+ @filters.sort_natural(foo, "bar")
303
+ end
152
304
  end
153
305
 
154
306
  def test_legacy_sort_hash
155
- assert_equal [{a:1, b:2}], @filters.sort({a:1, b:2})
307
+ assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
156
308
  end
157
309
 
158
310
  def test_numerical_vs_lexicographical_sort
159
311
  assert_equal [2, 10], @filters.sort([10, 2])
160
- assert_equal [{"a" => 2}, {"a" => 10}], @filters.sort([{"a" => 10}, {"a" => 2}], "a")
312
+ assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")
161
313
  assert_equal ["10", "2"], @filters.sort(["10", "2"])
162
- assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a")
314
+ assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")
163
315
  end
164
316
 
165
317
  def test_uniq
166
- assert_equal [1,3,2,4], @filters.uniq([1,1,3,2,3,1,4,3,2,1])
167
- assert_equal [{"a" => 1}, {"a" => 3}, {"a" => 2}], @filters.uniq([{"a" => 1}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
318
+ assert_equal ["foo"], @filters.uniq("foo")
319
+ assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
320
+ assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
168
321
  testdrop = TestDrop.new
169
322
  assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
170
323
  end
171
324
 
325
+ def test_uniq_empty_array
326
+ assert_equal [], @filters.uniq([], "a")
327
+ end
328
+
329
+ def test_uniq_invalid_property
330
+ foo = [
331
+ [1],
332
+ [2],
333
+ [3]
334
+ ]
335
+
336
+ assert_raises Liquid::ArgumentError do
337
+ @filters.uniq(foo, "bar")
338
+ end
339
+ end
340
+
341
+ def test_compact_empty_array
342
+ assert_equal [], @filters.compact([], "a")
343
+ end
344
+
345
+ def test_compact_invalid_property
346
+ foo = [
347
+ [1],
348
+ [2],
349
+ [3]
350
+ ]
351
+
352
+ assert_raises Liquid::ArgumentError do
353
+ @filters.compact(foo, "bar")
354
+ end
355
+ end
356
+
172
357
  def test_reverse
173
- assert_equal [4,3,2,1], @filters.reverse([1,2,3,4])
358
+ assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
174
359
  end
175
360
 
176
361
  def test_legacy_reverse_hash
177
- assert_equal [{a:1, b:2}], @filters.reverse(a:1, b:2)
362
+ assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2)
178
363
  end
179
364
 
180
365
  def test_map
181
- assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a')
366
+ assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')
182
367
  assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
183
- 'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}]
368
+ 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
184
369
  end
185
370
 
186
371
  def test_map_doesnt_call_arbitrary_stuff
@@ -212,15 +397,51 @@ class StandardFiltersTest < Minitest::Test
212
397
 
213
398
  def test_map_over_proc
214
399
  drop = TestDrop.new
215
- p = Proc.new{ drop }
400
+ p = proc{ drop }
216
401
  templ = '{{ procs | map: "test" }}'
217
402
  assert_template_result "testfoo", templ, "procs" => [p]
218
403
  end
219
404
 
405
+ def test_map_over_drops_returning_procs
406
+ drops = [
407
+ {
408
+ "proc" => ->{ "foo" },
409
+ },
410
+ {
411
+ "proc" => ->{ "bar" },
412
+ },
413
+ ]
414
+ templ = '{{ drops | map: "proc" }}'
415
+ assert_template_result "foobar", templ, "drops" => drops
416
+ end
417
+
220
418
  def test_map_works_on_enumerables
221
419
  assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
222
420
  end
223
421
 
422
+ def test_map_returns_empty_on_2d_input_array
423
+ foo = [
424
+ [1],
425
+ [2],
426
+ [3]
427
+ ]
428
+
429
+ assert_raises Liquid::ArgumentError do
430
+ @filters.map(foo, "bar")
431
+ end
432
+ end
433
+
434
+ def test_map_returns_empty_with_no_property
435
+ foo = [
436
+ [1],
437
+ [2],
438
+ [3]
439
+ ]
440
+ assert_raises Liquid::ArgumentError do
441
+ @filters.map(foo, nil)
442
+ end
443
+ end
444
+
224
445
  def test_sort_works_on_enumerables
225
446
  assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
226
447
  end
@@ -230,6 +451,10 @@ class StandardFiltersTest < Minitest::Test
230
451
  assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
231
452
  end
232
453
 
454
+ def test_truncate_calls_to_liquid
455
+ assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
456
+ end
457
+
233
458
  def test_date
234
459
  assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
235
460
  assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -247,10 +472,13 @@ class StandardFiltersTest < Minitest::Test
247
472
  assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
248
473
 
249
474
  assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
250
- assert_equal "#{Date.today.year}", @filters.date('now', '%Y')
251
- assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
475
+ assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
476
+ assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
477
+ assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
478
+
479
+ assert_nil @filters.date(nil, "%B")
252
480
 
253
- assert_equal nil, @filters.date(nil, "%B")
481
+ assert_equal '', @filters.date('', "%B")
254
482
 
255
483
  with_timezone("UTC") do
256
484
  assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
@@ -259,21 +487,25 @@ class StandardFiltersTest < Minitest::Test
259
487
  end
260
488
 
261
489
  def test_first_last
262
- assert_equal 1, @filters.first([1,2,3])
263
- assert_equal 3, @filters.last([1,2,3])
264
- assert_equal nil, @filters.first([])
265
- assert_equal nil, @filters.last([])
490
+ assert_equal 1, @filters.first([1, 2, 3])
491
+ assert_equal 3, @filters.last([1, 2, 3])
492
+ assert_nil @filters.first([])
493
+ assert_nil @filters.last([])
266
494
  end
267
495
 
268
496
  def test_replace
269
497
  assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
498
+ assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
270
499
  assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
500
+ assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
271
501
  assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
272
502
  end
273
503
 
274
504
  def test_remove
275
505
  assert_equal ' ', @filters.remove("a a a a", 'a')
506
+ assert_equal ' ', @filters.remove("1 1 1 1", 1)
276
507
  assert_equal 'a a a', @filters.remove_first("a a a a", 'a ')
508
+ assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
277
509
  assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
278
510
  end
279
511
 
@@ -308,20 +540,38 @@ class StandardFiltersTest < Minitest::Test
308
540
  def test_plus
309
541
  assert_template_result "2", "{{ 1 | plus:1 }}"
310
542
  assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
543
+
544
+ assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
311
545
  end
312
546
 
313
547
  def test_minus
314
548
  assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
315
549
  assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
550
+
551
+ assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
552
+ end
553
+
554
+ def test_abs
555
+ assert_template_result "17", "{{ 17 | abs }}"
556
+ assert_template_result "17", "{{ -17 | abs }}"
557
+ assert_template_result "17", "{{ '17' | abs }}"
558
+ assert_template_result "17", "{{ '-17' | abs }}"
559
+ assert_template_result "0", "{{ 0 | abs }}"
560
+ assert_template_result "0", "{{ '0' | abs }}"
561
+ assert_template_result "17.42", "{{ 17.42 | abs }}"
562
+ assert_template_result "17.42", "{{ -17.42 | abs }}"
563
+ assert_template_result "17.42", "{{ '17.42' | abs }}"
564
+ assert_template_result "17.42", "{{ '-17.42' | abs }}"
316
565
  end
317
566
 
318
567
  def test_times
319
568
  assert_template_result "12", "{{ 3 | times:4 }}"
320
569
  assert_template_result "0", "{{ 'foo' | times:4 }}"
321
-
322
570
  assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
323
-
324
571
  assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
572
+ assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
573
+ assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
574
+ assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
325
575
  end
326
576
 
327
577
  def test_divided_by
@@ -332,38 +582,96 @@ class StandardFiltersTest < Minitest::Test
332
582
  assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
333
583
 
334
584
  assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
585
+ assert_raises(Liquid::ZeroDivisionError) do
586
+ assert_template_result "4", "{{ 1 | modulo: 0 }}"
587
+ end
588
+
589
+ assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
335
590
  end
336
591
 
337
592
  def test_modulo
338
593
  assert_template_result "1", "{{ 3 | modulo:2 }}"
594
+ assert_raises(Liquid::ZeroDivisionError) do
595
+ assert_template_result "4", "{{ 1 | modulo: 0 }}"
596
+ end
597
+
598
+ assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
339
599
  end
340
600
 
341
601
  def test_round
342
602
  assert_template_result "5", "{{ input | round }}", 'input' => 4.6
343
603
  assert_template_result "4", "{{ '4.3' | round }}"
344
604
  assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
605
+ assert_raises(Liquid::FloatDomainError) do
606
+ assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
607
+ end
608
+
609
+ assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
610
+ assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
345
611
  end
346
612
 
347
613
  def test_ceil
348
614
  assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
349
615
  assert_template_result "5", "{{ '4.3' | ceil }}"
616
+ assert_raises(Liquid::FloatDomainError) do
617
+ assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
618
+ end
619
+
620
+ assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
350
621
  end
351
622
 
352
623
  def test_floor
353
624
  assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
354
625
  assert_template_result "4", "{{ '4.3' | floor }}"
626
+ assert_raises(Liquid::FloatDomainError) do
627
+ assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
628
+ end
629
+
630
+ assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
631
+ end
632
+
633
+ def test_at_most
634
+ assert_template_result "4", "{{ 5 | at_most:4 }}"
635
+ assert_template_result "5", "{{ 5 | at_most:5 }}"
636
+ assert_template_result "5", "{{ 5 | at_most:6 }}"
637
+
638
+ assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
639
+ assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
640
+ assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
641
+ assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
642
+ end
643
+
644
+ def test_at_least
645
+ assert_template_result "5", "{{ 5 | at_least:4 }}"
646
+ assert_template_result "5", "{{ 5 | at_least:5 }}"
647
+ assert_template_result "6", "{{ 5 | at_least:6 }}"
648
+
649
+ assert_template_result "5", "{{ 4.5 | at_least:5 }}"
650
+ assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
651
+ assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
652
+ assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
355
653
  end
356
654
 
357
655
  def test_append
358
- assigns = {'a' => 'bc', 'b' => 'd' }
359
- assert_template_result('bcd',"{{ a | append: 'd'}}",assigns)
360
- assert_template_result('bcd',"{{ a | append: b}}",assigns)
656
+ assigns = { 'a' => 'bc', 'b' => 'd' }
657
+ assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
658
+ assert_template_result('bcd', "{{ a | append: b}}", assigns)
659
+ end
660
+
661
+ def test_concat
662
+ assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
663
+ assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
664
+ assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
665
+
666
+ assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
667
+ @filters.concat([1, 2], 10)
668
+ end
361
669
  end
362
670
 
363
671
  def test_prepend
364
- assigns = {'a' => 'bc', 'b' => 'a' }
365
- assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns)
366
- assert_template_result('abc',"{{ a | prepend: b}}",assigns)
672
+ assigns = { 'a' => 'bc', 'b' => 'a' }
673
+ assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
674
+ assert_template_result('abc', "{{ a | prepend: b}}", assigns)
367
675
  end
368
676
 
369
677
  def test_default
@@ -376,7 +684,7 @@ class StandardFiltersTest < Minitest::Test
376
684
  end
377
685
 
378
686
  def test_cannot_access_private_methods
379
- assert_template_result('a',"{{ 'a' | to_number }}")
687
+ assert_template_result('a', "{{ 'a' | to_number }}")
380
688
  end
381
689
 
382
690
  def test_date_raises_nothing
@@ -384,6 +692,78 @@ class StandardFiltersTest < Minitest::Test
384
692
  assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
385
693
  end
386
694
 
695
+ def test_where
696
+ input = [
697
+ { "handle" => "alpha", "ok" => true },
698
+ { "handle" => "beta", "ok" => false },
699
+ { "handle" => "gamma", "ok" => false },
700
+ { "handle" => "delta", "ok" => true }
701
+ ]
702
+
703
+ expectation = [
704
+ { "handle" => "alpha", "ok" => true },
705
+ { "handle" => "delta", "ok" => true }
706
+ ]
707
+
708
+ assert_equal expectation, @filters.where(input, "ok", true)
709
+ assert_equal expectation, @filters.where(input, "ok")
710
+ end
711
+
712
+ def test_where_no_key_set
713
+ input = [
714
+ { "handle" => "alpha", "ok" => true },
715
+ { "handle" => "beta" },
716
+ { "handle" => "gamma" },
717
+ { "handle" => "delta", "ok" => true }
718
+ ]
719
+
720
+ expectation = [
721
+ { "handle" => "alpha", "ok" => true },
722
+ { "handle" => "delta", "ok" => true }
723
+ ]
724
+
725
+ assert_equal expectation, @filters.where(input, "ok", true)
726
+ assert_equal expectation, @filters.where(input, "ok")
727
+ end
728
+
729
+ def test_where_non_array_map_input
730
+ assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
731
+ assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
732
+ end
733
+
734
+ def test_where_indexable_but_non_map_value
735
+ assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) }
736
+ assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") }
737
+ end
738
+
739
+ def test_where_non_boolean_value
740
+ input = [
741
+ { "message" => "Bonjour!", "language" => "French" },
742
+ { "message" => "Hello!", "language" => "English" },
743
+ { "message" => "Hallo!", "language" => "German" }
744
+ ]
745
+
746
+ assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
747
+ assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
748
+ assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
749
+ end
750
+
751
+ def test_where_array_of_only_unindexable_values
752
+ assert_nil @filters.where([nil], "ok", true)
753
+ assert_nil @filters.where([nil], "ok")
754
+ end
755
+
756
+ def test_where_no_target_value
757
+ input = [
758
+ { "foo" => false },
759
+ { "foo" => true },
760
+ { "foo" => "for sure" },
761
+ { "bar" => true }
762
+ ]
763
+
764
+ assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
765
+ end
766
+
387
767
  private
388
768
 
389
769
  def with_timezone(tz)