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
@@ -13,13 +13,12 @@ class ContextDrop < Liquid::Drop
13
13
  @context['forloop.index']
14
14
  end
15
15
 
16
- def before_method(method)
17
- return @context[method]
16
+ def liquid_method_missing(method)
17
+ @context[method]
18
18
  end
19
19
  end
20
20
 
21
21
  class ProductDrop < Liquid::Drop
22
-
23
22
  class TextDrop < Liquid::Drop
24
23
  def array
25
24
  ['text1', 'text2']
@@ -31,8 +30,8 @@ class ProductDrop < Liquid::Drop
31
30
  end
32
31
 
33
32
  class CatchallDrop < Liquid::Drop
34
- def before_method(method)
35
- return 'method: ' << method.to_s
33
+ def liquid_method_missing(method)
34
+ 'catchall_method: ' << method.to_s
36
35
  end
37
36
  end
38
37
 
@@ -53,13 +52,14 @@ class ProductDrop < Liquid::Drop
53
52
  end
54
53
 
55
54
  protected
56
- def callmenot
57
- "protected"
58
- end
55
+
56
+ def callmenot
57
+ "protected"
58
+ end
59
59
  end
60
60
 
61
61
  class EnumerableDrop < Liquid::Drop
62
- def before_method(method)
62
+ def liquid_method_missing(method)
63
63
  method
64
64
  end
65
65
 
@@ -93,7 +93,7 @@ end
93
93
  class RealEnumerableDrop < Liquid::Drop
94
94
  include Enumerable
95
95
 
96
- def before_method(method)
96
+ def liquid_method_missing(method)
97
97
  method
98
98
  end
99
99
 
@@ -124,8 +124,10 @@ class DropsTest < Minitest::Test
124
124
  def test_rendering_warns_on_tainted_attr
125
125
  with_taint_mode(:warn) do
126
126
  tpl = Liquid::Template.parse('{{ product.user_input }}')
127
- tpl.render!('product' => ProductDrop.new)
128
- assert_match /tainted/, tpl.warnings.first
127
+ context = Context.new('product' => ProductDrop.new)
128
+ tpl.render!(context)
129
+ assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
130
+ assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
129
131
  end
130
132
  end
131
133
 
@@ -151,37 +153,37 @@ class DropsTest < Minitest::Test
151
153
  end
152
154
 
153
155
  def test_text_drop
154
- output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new)
156
+ output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
155
157
  assert_equal ' text1 ', output
156
158
  end
157
159
 
158
- def test_unknown_method
159
- output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render!('product' => ProductDrop.new)
160
- assert_equal ' method: unknown ', output
160
+ def test_catchall_unknown_method
161
+ output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
162
+ assert_equal ' catchall_method: unknown ', output
161
163
  end
162
164
 
163
- def test_integer_argument_drop
164
- output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render!('product' => ProductDrop.new)
165
- assert_equal ' method: 8 ', output
165
+ def test_catchall_integer_argument_drop
166
+ output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
167
+ assert_equal ' catchall_method: 8 ', output
166
168
  end
167
169
 
168
170
  def test_text_array_drop
169
- output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new)
171
+ output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
170
172
  assert_equal ' text1 text2 ', output
171
173
  end
172
174
 
173
175
  def test_context_drop
174
- output = Liquid::Template.parse( ' {{ context.bar }} ' ).render!('context' => ContextDrop.new, 'bar' => "carrot")
176
+ output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
175
177
  assert_equal ' carrot ', output
176
178
  end
177
179
 
178
180
  def test_nested_context_drop
179
- output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey")
181
+ output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
180
182
  assert_equal ' monkey ', output
181
183
  end
182
184
 
183
185
  def test_protected
184
- output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new)
186
+ output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
185
187
  assert_equal ' ', output
186
188
  end
187
189
 
@@ -193,43 +195,43 @@ class DropsTest < Minitest::Test
193
195
  end
194
196
 
195
197
  def test_scope
196
- assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new)
197
- assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
198
- assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
198
+ assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
199
+ assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
200
+ assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
199
201
  end
200
202
 
201
203
  def test_scope_though_proc
202
- assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] })
203
- assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
204
- assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
204
+ assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
205
+ assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
206
+ assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
205
207
  end
206
208
 
207
209
  def test_scope_with_assigns
208
- assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new)
209
- assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
210
- assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new)
211
- assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new)
210
+ assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
211
+ assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
212
+ assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
213
+ assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
212
214
  end
213
215
 
214
216
  def test_scope_from_tags
215
- assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
216
- assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
217
- assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1])
217
+ assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
218
+ assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
219
+ assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
218
220
  end
219
221
 
220
222
  def test_access_context_from_drop
221
- assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3])
223
+ assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
222
224
  end
223
225
 
224
226
  def test_enumerable_drop
225
- assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
227
+ assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
226
228
  end
227
229
 
228
230
  def test_enumerable_drop_size
229
- assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render!('collection' => EnumerableDrop.new)
231
+ assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
230
232
  end
231
233
 
232
- def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
234
+ def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
233
235
  ["select", "each", "map", "cycle"].each do |method|
234
236
  assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
235
237
  assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
@@ -1,24 +1,5 @@
1
1
  require 'test_helper'
2
2
 
3
- class ErrorDrop < Liquid::Drop
4
- def standard_error
5
- raise Liquid::StandardError, 'standard error'
6
- end
7
-
8
- def argument_error
9
- raise Liquid::ArgumentError, 'argument error'
10
- end
11
-
12
- def syntax_error
13
- raise Liquid::SyntaxError, 'syntax error'
14
- end
15
-
16
- def exception
17
- raise Exception, 'exception'
18
- end
19
-
20
- end
21
-
22
3
  class ErrorHandlingTest < Minitest::Test
23
4
  include Liquid
24
5
 
@@ -56,7 +37,7 @@ class ErrorHandlingTest < Minitest::Test
56
37
  end
57
38
 
58
39
  def test_standard_error
59
- template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
40
+ template = Liquid::Template.parse(' {{ errors.standard_error }} ')
60
41
  assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
61
42
 
62
43
  assert_equal 1, template.errors.size
@@ -64,7 +45,7 @@ class ErrorHandlingTest < Minitest::Test
64
45
  end
65
46
 
66
47
  def test_syntax
67
- template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
48
+ template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
68
49
  assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
69
50
 
70
51
  assert_equal 1, template.errors.size
@@ -72,7 +53,7 @@ class ErrorHandlingTest < Minitest::Test
72
53
  end
73
54
 
74
55
  def test_argument
75
- template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
56
+ template = Liquid::Template.parse(' {{ errors.argument_error }} ')
76
57
  assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
77
58
 
78
59
  assert_equal 1, template.errors.size
@@ -94,7 +75,7 @@ class ErrorHandlingTest < Minitest::Test
94
75
  end
95
76
 
96
77
  def test_lax_unrecognized_operator
97
- template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
78
+ template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
98
79
  assert_equal ' Liquid error: Unknown operator =! ', template.render
99
80
  assert_equal 1, template.errors.size
100
81
  assert_equal Liquid::ArgumentError, template.errors.first.class
@@ -102,30 +83,46 @@ class ErrorHandlingTest < Minitest::Test
102
83
 
103
84
  def test_with_line_numbers_adds_numbers_to_parser_errors
104
85
  err = assert_raises(SyntaxError) do
105
- template = Liquid::Template.parse(%q{
86
+ Liquid::Template.parse(%q(
106
87
  foobar
107
88
 
108
89
  {% "cat" | foobar %}
109
90
 
110
91
  bla
111
- },
112
- :line_numbers => true
92
+ ),
93
+ line_numbers: true
94
+ )
95
+ end
96
+
97
+ assert_match(/Liquid syntax error \(line 4\)/, err.message)
98
+ end
99
+
100
+ def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
101
+ err = assert_raises(SyntaxError) do
102
+ Liquid::Template.parse(%q(
103
+ foobar
104
+
105
+ {%- "cat" | foobar -%}
106
+
107
+ bla
108
+ ),
109
+ line_numbers: true
113
110
  )
114
111
  end
115
112
 
116
- assert_match /Liquid syntax error \(line 4\)/, err.message
113
+ assert_match(/Liquid syntax error \(line 4\)/, err.message)
117
114
  end
118
115
 
119
116
  def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
120
- template = Liquid::Template.parse(%q{
117
+ template = Liquid::Template.parse('
121
118
  foobar
122
119
 
123
120
  {% if 1 =! 2 %}ok{% endif %}
124
121
 
125
122
  bla
126
- },
127
- :error_mode => :warn,
128
- :line_numbers => true
123
+ ',
124
+ error_mode: :warn,
125
+ line_numbers: true
129
126
  )
130
127
 
131
128
  assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
@@ -134,15 +131,15 @@ class ErrorHandlingTest < Minitest::Test
134
131
 
135
132
  def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
136
133
  err = assert_raises(SyntaxError) do
137
- Liquid::Template.parse(%q{
134
+ Liquid::Template.parse('
138
135
  foobar
139
136
 
140
137
  {% if 1 =! 2 %}ok{% endif %}
141
138
 
142
139
  bla
143
- },
144
- :error_mode => :strict,
145
- :line_numbers => true
140
+ ',
141
+ error_mode: :strict,
142
+ line_numbers: true
146
143
  )
147
144
  end
148
145
 
@@ -151,7 +148,7 @@ class ErrorHandlingTest < Minitest::Test
151
148
 
152
149
  def test_syntax_errors_in_nested_blocks_have_correct_line_number
153
150
  err = assert_raises(SyntaxError) do
154
- Liquid::Template.parse(%q{
151
+ Liquid::Template.parse('
155
152
  foobar
156
153
 
157
154
  {% if 1 != 2 %}
@@ -159,8 +156,8 @@ class ErrorHandlingTest < Minitest::Test
159
156
  {% endif %}
160
157
 
161
158
  bla
162
- },
163
- :line_numbers => true
159
+ ',
160
+ line_numbers: true
164
161
  )
165
162
  end
166
163
 
@@ -169,18 +166,18 @@ class ErrorHandlingTest < Minitest::Test
169
166
 
170
167
  def test_strict_error_messages
171
168
  err = assert_raises(SyntaxError) do
172
- Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
169
+ Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
173
170
  end
174
171
  assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
175
172
 
176
173
  err = assert_raises(SyntaxError) do
177
- Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
174
+ Liquid::Template.parse('{{%%%}}', error_mode: :strict)
178
175
  end
179
176
  assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
180
177
  end
181
178
 
182
179
  def test_warnings
183
- template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
180
+ template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
184
181
  assert_equal 3, template.warnings.size
185
182
  assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
186
183
  assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
@@ -189,12 +186,12 @@ class ErrorHandlingTest < Minitest::Test
189
186
  end
190
187
 
191
188
  def test_warning_line_numbers
192
- template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
189
+ template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
193
190
  assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
194
191
  assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
195
192
  assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
196
193
  assert_equal 3, template.warnings.size
197
- assert_equal [1,2,3], template.warnings.map(&:line_number)
194
+ assert_equal [1, 2, 3], template.warnings.map(&:line_number)
198
195
  end
199
196
 
200
197
  # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
@@ -204,4 +201,60 @@ class ErrorHandlingTest < Minitest::Test
204
201
  template.render('errors' => ErrorDrop.new)
205
202
  end
206
203
  end
204
+
205
+ def test_default_exception_renderer_with_internal_error
206
+ template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
207
+
208
+ output = template.render({ 'errors' => ErrorDrop.new })
209
+
210
+ assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
211
+ assert_equal [Liquid::InternalError], template.errors.map(&:class)
212
+ end
213
+
214
+ def test_setting_default_exception_renderer
215
+ old_exception_renderer = Liquid::Template.default_exception_renderer
216
+ exceptions = []
217
+ Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
218
+ template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
219
+
220
+ output = template.render({ 'errors' => ErrorDrop.new })
221
+
222
+ assert_equal 'This is a runtime error: ', output
223
+ assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
224
+ ensure
225
+ Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
226
+ end
227
+
228
+ def test_exception_renderer_exposing_non_liquid_error
229
+ template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
230
+ exceptions = []
231
+ handler = ->(e) { exceptions << e; e.cause }
232
+
233
+ output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
234
+
235
+ assert_equal 'This is a runtime error: runtime error', output
236
+ assert_equal [Liquid::InternalError], exceptions.map(&:class)
237
+ assert_equal exceptions, template.errors
238
+ assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
239
+ end
240
+
241
+ class TestFileSystem
242
+ def read_template_file(template_path)
243
+ "{{ errors.argument_error }}"
244
+ end
245
+ end
246
+
247
+ def test_included_template_name_with_line_numbers
248
+ old_file_system = Liquid::Template.file_system
249
+
250
+ begin
251
+ Liquid::Template.file_system = TestFileSystem.new
252
+ template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
253
+ page = template.render('errors' => ErrorDrop.new)
254
+ ensure
255
+ Liquid::Template.file_system = old_file_system
256
+ end
257
+ assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
258
+ assert_equal "product", template.errors.first.template_name
259
+ end
207
260
  end
@@ -17,7 +17,7 @@ module CanadianMoneyFilter
17
17
  end
18
18
 
19
19
  module SubstituteFilter
20
- def substitute(input, params={})
20
+ def substitute(input, params = {})
21
21
  input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
22
22
  end
23
23
  end
@@ -39,13 +39,13 @@ class FiltersTest < Minitest::Test
39
39
  @context['var'] = 1000
40
40
  @context.add_filters(MoneyFilter)
41
41
 
42
- assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
42
+ assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
43
43
  end
44
44
 
45
45
  def test_underscore_in_filter_name
46
46
  @context['var'] = 1000
47
47
  @context.add_filters(MoneyFilter)
48
- assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
48
+ assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
49
49
  end
50
50
 
51
51
  def test_second_filter_overwrites_first
@@ -53,67 +53,100 @@ class FiltersTest < Minitest::Test
53
53
  @context.add_filters(MoneyFilter)
54
54
  @context.add_filters(CanadianMoneyFilter)
55
55
 
56
- assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
56
+ assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
57
57
  end
58
58
 
59
59
  def test_size
60
60
  @context['var'] = 'abcd'
61
61
  @context.add_filters(MoneyFilter)
62
62
 
63
- assert_equal 4, Variable.new("var | size").render(@context)
63
+ assert_equal '4', Template.parse("{{var | size}}").render(@context)
64
64
  end
65
65
 
66
66
  def test_join
67
- @context['var'] = [1,2,3,4]
67
+ @context['var'] = [1, 2, 3, 4]
68
68
 
69
- assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
69
+ assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
70
70
  end
71
71
 
72
72
  def test_sort
73
73
  @context['value'] = 3
74
- @context['numbers'] = [2,1,4,3]
74
+ @context['numbers'] = [2, 1, 4, 3]
75
75
  @context['words'] = ['expected', 'as', 'alphabetic']
76
76
  @context['arrays'] = ['flower', 'are']
77
+ @context['case_sensitive'] = ['sensitive', 'Expected', 'case']
77
78
 
78
- assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
79
- assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
80
- assert_equal [3], Variable.new("value | sort").render(@context)
81
- assert_equal ['are', 'flower'], Variable.new("arrays | sort").render(@context)
79
+ assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
80
+ assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
81
+ assert_equal '3', Template.parse("{{value | sort}}").render(@context)
82
+ assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
83
+ assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
84
+ end
85
+
86
+ def test_sort_natural
87
+ @context['words'] = ['case', 'Assert', 'Insensitive']
88
+ @context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
89
+ @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
90
+
91
+ # Test strings
92
+ assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
93
+
94
+ # Test hashes
95
+ assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
96
+
97
+ # Test objects
98
+ assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
99
+ end
100
+
101
+ def test_compact
102
+ @context['words'] = ['a', nil, 'b', nil, 'c']
103
+ @context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
104
+ @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
105
+
106
+ # Test strings
107
+ assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
108
+
109
+ # Test hashes
110
+ assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
111
+
112
+ # Test objects
113
+ assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
82
114
  end
83
115
 
84
116
  def test_strip_html
85
117
  @context['var'] = "<b>bla blub</a>"
86
118
 
87
- assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
119
+ assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
88
120
  end
89
121
 
90
122
  def test_strip_html_ignore_comments_with_html
91
123
  @context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
92
124
 
93
- assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
125
+ assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
94
126
  end
95
127
 
96
128
  def test_capitalize
97
129
  @context['var'] = "blub"
98
130
 
99
- assert_equal "Blub", Variable.new("var | capitalize").render(@context)
131
+ assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
100
132
  end
101
133
 
102
134
  def test_nonexistent_filter_is_ignored
103
135
  @context['var'] = 1000
104
136
 
105
- assert_equal 1000, Variable.new("var | xyzzy").render(@context)
137
+ assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
106
138
  end
107
139
 
108
140
  def test_filter_with_keyword_arguments
109
141
  @context['surname'] = 'john'
142
+ @context['input'] = 'hello %{first_name}, %{last_name}'
110
143
  @context.add_filters(SubstituteFilter)
111
- output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
144
+ output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
112
145
  assert_equal 'hello john, doe', output
113
146
  end
114
147
 
115
148
  def test_override_object_method_in_filter
116
- assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, :filters => [OverrideObjectMethodFilter])
149
+ assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
117
150
 
118
151
  # tap still treated as a non-existent filter
119
152
  assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
@@ -126,8 +159,8 @@ class FiltersInTemplate < Minitest::Test
126
159
  def test_local_global
127
160
  with_global_filter(MoneyFilter) do
128
161
  assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
129
- assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter)
130
- assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
162
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
163
+ assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
131
164
  end
132
165
  end
133
166
 
@@ -136,3 +169,10 @@ class FiltersInTemplate < Minitest::Test
136
169
  assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
137
170
  end
138
171
  end # FiltersTest
172
+
173
+ class TestObject < Liquid::Drop
174
+ attr_accessor :a
175
+ def initialize(a)
176
+ @a = a
177
+ end
178
+ end
@@ -1,18 +1,18 @@
1
1
  require 'test_helper'
2
2
 
3
- module MoneyFilter
4
- def money(input)
5
- sprintf(' %d$ ', input)
3
+ class HashOrderingTest < Minitest::Test
4
+ module MoneyFilter
5
+ def money(input)
6
+ sprintf(' %d$ ', input)
7
+ end
6
8
  end
7
- end
8
9
 
9
- module CanadianMoneyFilter
10
- def money(input)
11
- sprintf(' %d$ CAD ', input)
10
+ module CanadianMoneyFilter
11
+ def money(input)
12
+ sprintf(' %d$ CAD ', input)
13
+ end
12
14
  end
13
- end
14
15
 
15
- class HashOrderingTest < Minitest::Test
16
16
  include Liquid
17
17
 
18
18
  def test_global_register_order