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,7 +1,8 @@
1
1
  require 'test_helper'
2
+ require 'timeout'
2
3
 
3
4
  class TemplateContextDrop < Liquid::Drop
4
- def before_method(method)
5
+ def liquid_method_missing(method)
5
6
  method
6
7
  end
7
8
 
@@ -14,12 +15,10 @@ class TemplateContextDrop < Liquid::Drop
14
15
  end
15
16
  end
16
17
 
17
- class SomethingWithLength
18
+ class SomethingWithLength < Liquid::Drop
18
19
  def length
19
20
  nil
20
21
  end
21
-
22
- liquid_methods :length
23
22
  end
24
23
 
25
24
  class ErroneousDrop < Liquid::Drop
@@ -28,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
28
27
  end
29
28
  end
30
29
 
30
+ class DropWithUndefinedMethod < Liquid::Drop
31
+ def foo
32
+ 'foo'
33
+ end
34
+ end
35
+
31
36
  class TemplateTest < Minitest::Test
32
37
  include Liquid
33
38
 
@@ -37,6 +42,16 @@ class TemplateTest < Minitest::Test
37
42
  assert_equal 'from instance assigns', t.parse("{{ foo }}").render!
38
43
  end
39
44
 
45
+ def test_warnings_is_not_exponential_time
46
+ str = "false"
47
+ 100.times do
48
+ str = "{% if true %}true{% else %}#{str}{% endif %}"
49
+ end
50
+
51
+ t = Template.parse(str)
52
+ assert_equal [], Timeout.timeout(1) { t.warnings }
53
+ end
54
+
40
55
  def test_instance_assigns_persist_on_same_template_parsing_between_renders
41
56
  t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
42
57
  assert_equal 'foo', t.render!
@@ -64,7 +79,7 @@ class TemplateTest < Minitest::Test
64
79
 
65
80
  def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
66
81
  t = Template.new
67
- t.assigns['number'] = lambda { @global ||= 0; @global += 1 }
82
+ t.assigns['number'] = -> { @global ||= 0; @global += 1 }
68
83
  assert_equal '1', t.parse("{{number}}").render!
69
84
  assert_equal '1', t.parse("{{number}}").render!
70
85
  assert_equal '1', t.render!
@@ -73,7 +88,7 @@ class TemplateTest < Minitest::Test
73
88
 
74
89
  def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
75
90
  t = Template.new
76
- assigns = {'number' => lambda { @global ||= 0; @global += 1 }}
91
+ assigns = { 'number' => -> { @global ||= 0; @global += 1 } }
77
92
  assert_equal '1', t.parse("{{number}}").render!(assigns)
78
93
  assert_equal '1', t.parse("{{number}}").render!(assigns)
79
94
  assert_equal '1', t.render!(assigns)
@@ -82,69 +97,103 @@ class TemplateTest < Minitest::Test
82
97
 
83
98
  def test_resource_limits_works_with_custom_length_method
84
99
  t = Template.parse("{% assign foo = bar %}")
85
- t.resource_limits = { :render_length_limit => 42 }
100
+ t.resource_limits.render_length_limit = 42
86
101
  assert_equal "", t.render!("bar" => SomethingWithLength.new)
87
102
  end
88
103
 
89
104
  def test_resource_limits_render_length
90
105
  t = Template.parse("0123456789")
91
- t.resource_limits = { :render_length_limit => 5 }
92
- assert_equal "Liquid error: Memory limits exceeded", t.render()
93
- assert t.resource_limits[:reached]
94
- t.resource_limits = { :render_length_limit => 10 }
95
- assert_equal "0123456789", t.render!()
96
- refute_nil t.resource_limits[:render_length_current]
106
+ t.resource_limits.render_length_limit = 5
107
+ assert_equal "Liquid error: Memory limits exceeded", t.render
108
+ assert t.resource_limits.reached?
109
+
110
+ t.resource_limits.render_length_limit = 10
111
+ assert_equal "0123456789", t.render!
112
+ refute_nil t.resource_limits.render_length
97
113
  end
98
114
 
99
115
  def test_resource_limits_render_score
100
116
  t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
101
- t.resource_limits = { :render_score_limit => 50 }
102
- assert_equal "Liquid error: Memory limits exceeded", t.render()
103
- assert t.resource_limits[:reached]
117
+ t.resource_limits.render_score_limit = 50
118
+ assert_equal "Liquid error: Memory limits exceeded", t.render
119
+ assert t.resource_limits.reached?
120
+
104
121
  t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
105
- t.resource_limits = { :render_score_limit => 50 }
106
- assert_equal "Liquid error: Memory limits exceeded", t.render()
107
- assert t.resource_limits[:reached]
108
- t.resource_limits = { :render_score_limit => 200 }
109
- assert_equal (" foo " * 100), t.render!()
110
- refute_nil t.resource_limits[:render_score_current]
122
+ t.resource_limits.render_score_limit = 50
123
+ assert_equal "Liquid error: Memory limits exceeded", t.render
124
+ assert t.resource_limits.reached?
125
+
126
+ t.resource_limits.render_score_limit = 200
127
+ assert_equal (" foo " * 100), t.render!
128
+ refute_nil t.resource_limits.render_score
111
129
  end
112
130
 
113
131
  def test_resource_limits_assign_score
114
132
  t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
115
- t.resource_limits = { :assign_score_limit => 1 }
116
- assert_equal "Liquid error: Memory limits exceeded", t.render()
117
- assert t.resource_limits[:reached]
118
- t.resource_limits = { :assign_score_limit => 2 }
119
- assert_equal "", t.render!()
120
- refute_nil t.resource_limits[:assign_score_current]
133
+ t.resource_limits.assign_score_limit = 1
134
+ assert_equal "Liquid error: Memory limits exceeded", t.render
135
+ assert t.resource_limits.reached?
136
+
137
+ t.resource_limits.assign_score_limit = 2
138
+ assert_equal "", t.render!
139
+ refute_nil t.resource_limits.assign_score
140
+ end
141
+
142
+ def test_resource_limits_assign_score_nested
143
+ t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
144
+
145
+ t.resource_limits.assign_score_limit = 3
146
+ assert_equal "Liquid error: Memory limits exceeded", t.render
147
+ assert t.resource_limits.reached?
148
+
149
+ t.resource_limits.assign_score_limit = 5
150
+ assert_equal "", t.render!
121
151
  end
122
152
 
123
153
  def test_resource_limits_aborts_rendering_after_first_error
124
154
  t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
125
- t.resource_limits = { :render_score_limit => 50 }
126
- assert_equal "Liquid error: Memory limits exceeded", t.render()
127
- assert t.resource_limits[:reached]
155
+ t.resource_limits.render_score_limit = 50
156
+ assert_equal "Liquid error: Memory limits exceeded", t.render
157
+ assert t.resource_limits.reached?
128
158
  end
129
159
 
130
160
  def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
131
161
  t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
132
- t.render!()
133
- assert t.resource_limits[:assign_score_current] > 0
134
- assert t.resource_limits[:render_score_current] > 0
135
- assert t.resource_limits[:render_length_current] > 0
162
+ t.render!
163
+ assert t.resource_limits.assign_score > 0
164
+ assert t.resource_limits.render_score > 0
165
+ assert t.resource_limits.render_length > 0
166
+ end
167
+
168
+ def test_render_length_persists_between_blocks
169
+ t = Template.parse("{% if true %}aaaa{% endif %}")
170
+ t.resource_limits.render_length_limit = 7
171
+ assert_equal "Liquid error: Memory limits exceeded", t.render
172
+ t.resource_limits.render_length_limit = 8
173
+ assert_equal "aaaa", t.render
174
+
175
+ t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
176
+ t.resource_limits.render_length_limit = 13
177
+ assert_equal "Liquid error: Memory limits exceeded", t.render
178
+ t.resource_limits.render_length_limit = 14
179
+ assert_equal "aaaabbb", t.render
180
+
181
+ t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
182
+ t.resource_limits.render_length_limit = 5
183
+ assert_equal "Liquid error: Memory limits exceeded", t.render
184
+ t.resource_limits.render_length_limit = 11
185
+ assert_equal "Liquid error: Memory limits exceeded", t.render
186
+ t.resource_limits.render_length_limit = 12
187
+ assert_equal "ababab", t.render
136
188
  end
137
189
 
138
190
  def test_default_resource_limits_unaffected_by_render_with_context
139
191
  context = Context.new
140
192
  t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
141
193
  t.render!(context)
142
- assert context.resource_limits[:assign_score_current] > 0
143
- assert context.resource_limits[:render_score_current] > 0
144
- assert context.resource_limits[:render_length_current] > 0
145
- refute Template.default_resource_limits.key?(:assign_score_current)
146
- refute Template.default_resource_limits.key?(:render_score_current)
147
- refute Template.default_resource_limits.key?(:render_length_current)
194
+ assert context.resource_limits.assign_score > 0
195
+ assert context.resource_limits.render_score > 0
196
+ assert context.resource_limits.render_length > 0
148
197
  end
149
198
 
150
199
  def test_can_use_drop_as_context
@@ -157,7 +206,7 @@ class TemplateTest < Minitest::Test
157
206
  end
158
207
 
159
208
  def test_render_bang_force_rethrow_errors_on_passed_context
160
- context = Context.new({'drop' => ErroneousDrop.new})
209
+ context = Context.new({ 'drop' => ErroneousDrop.new })
161
210
  t = Template.new.parse('{{ drop.bad_method }}')
162
211
 
163
212
  e = assert_raises RuntimeError do
@@ -166,17 +215,118 @@ class TemplateTest < Minitest::Test
166
215
  assert_equal 'ruby error in drop', e.message
167
216
  end
168
217
 
169
- def test_exception_handler_doesnt_reraise_if_it_returns_false
218
+ def test_exception_renderer_that_returns_string
170
219
  exception = nil
171
- Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; false })
172
- assert exception.is_a?(ZeroDivisionError)
220
+ handler = ->(e) { exception = e; '<!-- error -->' }
221
+
222
+ output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
223
+
224
+ assert exception.is_a?(Liquid::ZeroDivisionError)
225
+ assert_equal '<!-- error -->', output
173
226
  end
174
227
 
175
- def test_exception_handler_does_reraise_if_it_returns_true
228
+ def test_exception_renderer_that_raises
176
229
  exception = nil
177
- assert_raises(ZeroDivisionError) do
178
- Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_handler: ->(e) { exception = e; true })
230
+ assert_raises(Liquid::ZeroDivisionError) do
231
+ Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise })
179
232
  end
180
- assert exception.is_a?(ZeroDivisionError)
233
+ assert exception.is_a?(Liquid::ZeroDivisionError)
234
+ end
235
+
236
+ def test_global_filter_option_on_render
237
+ global_filter_proc = ->(output) { "#{output} filtered" }
238
+ rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
239
+
240
+ assert_equal 'bob filtered', rendered_template
241
+ end
242
+
243
+ def test_global_filter_option_when_native_filters_exist
244
+ global_filter_proc = ->(output) { "#{output} filtered" }
245
+ rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
246
+
247
+ assert_equal 'BOB filtered', rendered_template
248
+ end
249
+
250
+ def test_undefined_variables
251
+ t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
252
+ result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
253
+
254
+ assert_equal '33 32 ', result
255
+ assert_equal 3, t.errors.count
256
+ assert_instance_of Liquid::UndefinedVariable, t.errors[0]
257
+ assert_equal 'Liquid error: undefined variable y', t.errors[0].message
258
+ assert_instance_of Liquid::UndefinedVariable, t.errors[1]
259
+ assert_equal 'Liquid error: undefined variable b', t.errors[1].message
260
+ assert_instance_of Liquid::UndefinedVariable, t.errors[2]
261
+ assert_equal 'Liquid error: undefined variable d', t.errors[2].message
262
+ end
263
+
264
+ def test_nil_value_does_not_raise
265
+ Liquid::Template.error_mode = :strict
266
+ t = Template.parse("some{{x}}thing")
267
+ result = t.render!({ 'x' => nil }, strict_variables: true)
268
+
269
+ assert_equal 0, t.errors.count
270
+ assert_equal 'something', result
271
+ end
272
+
273
+ def test_undefined_variables_raise
274
+ t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
275
+
276
+ assert_raises UndefinedVariable do
277
+ t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
278
+ end
279
+ end
280
+
281
+ def test_undefined_drop_methods
282
+ d = DropWithUndefinedMethod.new
283
+ t = Template.new.parse('{{ foo }} {{ woot }}')
284
+ result = t.render(d, { strict_variables: true })
285
+
286
+ assert_equal 'foo ', result
287
+ assert_equal 1, t.errors.count
288
+ assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
289
+ end
290
+
291
+ def test_undefined_drop_methods_raise
292
+ d = DropWithUndefinedMethod.new
293
+ t = Template.new.parse('{{ foo }} {{ woot }}')
294
+
295
+ assert_raises UndefinedDropMethod do
296
+ t.render!(d, { strict_variables: true })
297
+ end
298
+ end
299
+
300
+ def test_undefined_filters
301
+ t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
302
+ filters = Module.new do
303
+ def somefilter3(v)
304
+ "-#{v}-"
305
+ end
306
+ end
307
+ result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
308
+
309
+ assert_equal '123 ', result
310
+ assert_equal 1, t.errors.count
311
+ assert_instance_of Liquid::UndefinedFilter, t.errors[0]
312
+ assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
313
+ end
314
+
315
+ def test_undefined_filters_raise
316
+ t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
317
+
318
+ assert_raises UndefinedFilter do
319
+ t.render!({ 'x' => 'foo' }, { strict_filters: true })
320
+ end
321
+ end
322
+
323
+ def test_using_range_literal_works_as_expected
324
+ t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
325
+ result = t.render({ 'x' => 1, 'y' => 5 })
326
+ assert_equal '1..5', result
327
+
328
+ t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
329
+ result = t.render({ 'x' => 1, 'y' => 5 })
330
+ assert_equal '12345', result
181
331
  end
182
332
  end