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
@@ -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)