liquid 4.0.0.rc3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +93 -2
  3. data/README.md +8 -0
  4. data/lib/liquid.rb +18 -5
  5. data/lib/liquid/block.rb +47 -20
  6. data/lib/liquid/block_body.rb +190 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +122 -76
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -25
  12. data/lib/liquid/expression.rb +30 -31
  13. data/lib/liquid/extensions.rb +8 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +35 -26
  19. data/lib/liquid/locales/en.yml +4 -2
  20. data/lib/liquid/parse_context.rb +17 -4
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +171 -57
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +32 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +41 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +35 -16
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +26 -0
  47. data/lib/liquid/tags/for.rb +79 -47
  48. data/lib/liquid/tags/if.rb +53 -30
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +42 -44
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +32 -20
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +60 -71
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +6 -4
  61. data/lib/liquid/variable.rb +55 -38
  62. data/lib/liquid/variable_lookup.rb +14 -6
  63. data/lib/liquid/version.rb +3 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +58 -0
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +608 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +90 -60
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +24 -8
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +41 -18
  79. data/test/integration/standard_filter_test.rb +523 -205
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +109 -53
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +83 -52
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +128 -121
  97. data/test/integration/trim_mode_test.rb +82 -44
  98. data/test/integration/variable_test.rb +46 -31
  99. data/test/test_helper.rb +75 -23
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +82 -72
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +12 -10
  105. data/test/unit/parse_tree_visitor_test.rb +247 -0
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +19 -17
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +83 -50
  121. data/lib/liquid/strainer.rb +0 -65
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -136
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Container for liquid nodes which conveniently wraps decision making logic
3
5
  #
@@ -8,58 +10,83 @@ module Liquid
8
10
  #
9
11
  class Condition #:nodoc:
10
12
  @@operators = {
11
- '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
12
- '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
13
- '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
14
- '<'.freeze => :<,
15
- '>'.freeze => :>,
16
- '>='.freeze => :>=,
17
- '<='.freeze => :<=,
18
- 'contains'.freeze => lambda do |cond, left, right|
13
+ '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
14
+ '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
15
+ '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
16
+ '<' => :<,
17
+ '>' => :>,
18
+ '>=' => :>=,
19
+ '<=' => :<=,
20
+ 'contains' => lambda do |_cond, left, right|
19
21
  if left && right && left.respond_to?(:include?)
20
22
  right = right.to_s if left.is_a?(String)
21
23
  left.include?(right)
22
24
  else
23
25
  false
24
26
  end
27
+ end,
28
+ }
29
+
30
+ class MethodLiteral
31
+ attr_reader :method_name, :to_s
32
+
33
+ def initialize(method_name, to_s)
34
+ @method_name = method_name
35
+ @to_s = to_s
25
36
  end
37
+ end
38
+
39
+ @@method_literals = {
40
+ 'blank' => MethodLiteral.new(:blank?, '').freeze,
41
+ 'empty' => MethodLiteral.new(:empty?, '').freeze,
26
42
  }
27
43
 
28
44
  def self.operators
29
45
  @@operators
30
46
  end
31
47
 
32
- attr_reader :attachment
48
+ def self.parse_expression(parse_context, markup)
49
+ @@method_literals[markup] || parse_context.parse_expression(markup)
50
+ end
51
+
52
+ attr_reader :attachment, :child_condition
33
53
  attr_accessor :left, :operator, :right
34
54
 
35
55
  def initialize(left = nil, operator = nil, right = nil)
36
- @left = left
56
+ @left = left
37
57
  @operator = operator
38
- @right = right
58
+ @right = right
59
+
39
60
  @child_relation = nil
40
61
  @child_condition = nil
41
62
  end
42
63
 
43
64
  def evaluate(context = Context.new)
44
- result = interpret_condition(left, right, operator, context)
45
-
46
- case @child_relation
47
- when :or
48
- result || @child_condition.evaluate(context)
49
- when :and
50
- result && @child_condition.evaluate(context)
51
- else
52
- result
65
+ condition = self
66
+ result = nil
67
+ loop do
68
+ result = interpret_condition(condition.left, condition.right, condition.operator, context)
69
+
70
+ case condition.child_relation
71
+ when :or
72
+ break if result
73
+ when :and
74
+ break unless result
75
+ else
76
+ break
77
+ end
78
+ condition = condition.child_condition
53
79
  end
80
+ result
54
81
  end
55
82
 
56
83
  def or(condition)
57
- @child_relation = :or
84
+ @child_relation = :or
58
85
  @child_condition = condition
59
86
  end
60
87
 
61
88
  def and(condition)
62
- @child_relation = :and
89
+ @child_relation = :and
63
90
  @child_condition = condition
64
91
  end
65
92
 
@@ -72,13 +99,17 @@ module Liquid
72
99
  end
73
100
 
74
101
  def inspect
75
- "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
102
+ "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
76
103
  end
77
104
 
105
+ protected
106
+
107
+ attr_reader :child_relation
108
+
78
109
  private
79
110
 
80
111
  def equal_variables(left, right)
81
- if left.is_a?(Liquid::Expression::MethodLiteral)
112
+ if left.is_a?(MethodLiteral)
82
113
  if right.respond_to?(left.method_name)
83
114
  return right.send(left.method_name)
84
115
  else
@@ -86,7 +117,7 @@ module Liquid
86
117
  end
87
118
  end
88
119
 
89
- if right.is_a?(Liquid::Expression::MethodLiteral)
120
+ if right.is_a?(MethodLiteral)
90
121
  if left.respond_to?(right.method_name)
91
122
  return left.send(right.method_name)
92
123
  else
@@ -103,21 +134,30 @@ module Liquid
103
134
  # return this as the result.
104
135
  return context.evaluate(left) if op.nil?
105
136
 
106
- left = context.evaluate(left)
137
+ left = context.evaluate(left)
107
138
  right = context.evaluate(right)
108
139
 
109
- operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
140
+ operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
110
141
 
111
142
  if operation.respond_to?(:call)
112
143
  operation.call(self, left, right)
113
- elsif left.respond_to?(operation) && right.respond_to?(operation)
144
+ elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
114
145
  begin
115
146
  left.send(operation, right)
116
147
  rescue ::ArgumentError => e
117
- raise Liquid::ArgumentError.new(e.message)
148
+ raise Liquid::ArgumentError, e.message
118
149
  end
119
150
  end
120
151
  end
152
+
153
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
154
+ def children
155
+ [
156
+ @node.left, @node.right,
157
+ @node.child_condition, @node.attachment
158
+ ].compact
159
+ end
160
+ end
121
161
  end
122
162
 
123
163
  class ElseCondition < Condition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Context keeps the variable stack and resolves variables, as well as keywords
3
5
  #
@@ -12,36 +14,49 @@ module Liquid
12
14
  #
13
15
  # context['bob'] #=> nil class Context
14
16
  class Context
15
- attr_reader :scopes, :errors, :registers, :environments, :resource_limits
16
- attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
17
-
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @partial = false
24
- @strict_variables = false
25
- @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
26
- squash_instance_assigns_with_environments
17
+ attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
18
+ attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
27
19
 
28
- @this_stack_used = false
20
+ # rubocop:disable Metrics/ParameterLists
21
+ def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
22
+ new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
23
+ end
29
24
 
25
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
26
+ @environments = [environments]
27
+ @environments.flatten!
28
+
29
+ @static_environments = [static_environments].flat_map(&:freeze).freeze
30
+ @scopes = [(outer_scope || {})]
31
+ @registers = registers
32
+ @errors = []
33
+ @partial = false
34
+ @strict_variables = false
35
+ @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
36
+ @base_scope_depth = 0
37
+ @interrupts = []
38
+ @filters = []
39
+ @global_filter = nil
40
+ @disabled_tags = {}
41
+
42
+ self.exception_renderer = Template.default_exception_renderer
30
43
  if rethrow_errors
31
- self.exception_handler = ->(e) { raise }
44
+ self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
32
45
  end
33
46
 
34
- @interrupts = []
35
- @filters = []
36
- @global_filter = nil
47
+ yield self if block_given?
48
+
49
+ # Do this last, since it could result in this object being passed to a Proc in the environment
50
+ squash_instance_assigns_with_environments
37
51
  end
52
+ # rubocop:enable Metrics/ParameterLists
38
53
 
39
54
  def warnings
40
55
  @warnings ||= []
41
56
  end
42
57
 
43
58
  def strainer
44
- @strainer ||= Strainer.create(self, @filters)
59
+ @strainer ||= StrainerFactory.create(self, @filters)
45
60
  end
46
61
 
47
62
  # Adds filters to this context.
@@ -74,30 +89,11 @@ module Liquid
74
89
  end
75
90
 
76
91
  def handle_error(e, line_number = nil)
77
- if e.is_a?(Liquid::Error)
78
- e.template_name ||= template_name
79
- e.line_number ||= line_number
80
- end
81
-
82
- output = nil
83
-
84
- if exception_handler
85
- result = exception_handler.call(e)
86
- case result
87
- when Exception
88
- e = result
89
- if e.is_a?(Liquid::Error)
90
- e.template_name ||= template_name
91
- e.line_number ||= line_number
92
- end
93
- when String
94
- output = result
95
- else
96
- raise if result
97
- end
98
- end
92
+ e = internal_error unless e.is_a?(Liquid::Error)
93
+ e.template_name ||= template_name
94
+ e.line_number ||= line_number
99
95
  errors.push(e)
100
- output || Liquid::Error.render(e)
96
+ exception_renderer.call(e).to_s
101
97
  end
102
98
 
103
99
  def invoke(method, *args)
@@ -107,7 +103,7 @@ module Liquid
107
103
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
108
104
  def push(new_scope = {})
109
105
  @scopes.unshift(new_scope)
110
- raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
106
+ check_overflow
111
107
  end
112
108
 
113
109
  # Merge a hash of variables in the current local scope
@@ -129,19 +125,31 @@ module Liquid
129
125
  # end
130
126
  #
131
127
  # context['var] #=> nil
132
- def stack(new_scope = nil)
133
- old_stack_used = @this_stack_used
134
- if new_scope
135
- push(new_scope)
136
- @this_stack_used = true
137
- else
138
- @this_stack_used = false
139
- end
140
-
128
+ def stack(new_scope = {})
129
+ push(new_scope)
141
130
  yield
142
131
  ensure
143
- pop if @this_stack_used
144
- @this_stack_used = old_stack_used
132
+ pop
133
+ end
134
+
135
+ # Creates a new context inheriting resource limits, filters, environment etc.,
136
+ # but with an isolated scope.
137
+ def new_isolated_subcontext
138
+ check_overflow
139
+
140
+ self.class.build(
141
+ resource_limits: resource_limits,
142
+ static_environments: static_environments,
143
+ registers: StaticRegisters.new(registers)
144
+ ).tap do |subcontext|
145
+ subcontext.base_scope_depth = base_scope_depth + 1
146
+ subcontext.exception_renderer = exception_renderer
147
+ subcontext.filters = @filters
148
+ subcontext.strainer = nil
149
+ subcontext.errors = errors
150
+ subcontext.warnings = warnings
151
+ subcontext.disabled_tags = @disabled_tags
152
+ end
145
153
  end
146
154
 
147
155
  def clear_instance_assigns
@@ -150,10 +158,6 @@ module Liquid
150
158
 
151
159
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
152
160
  def []=(key, value)
153
- unless @this_stack_used
154
- @this_stack_used = true
155
- push({})
156
- end
157
161
  @scopes[0][key] = value
158
162
  end
159
163
 
@@ -178,49 +182,91 @@ module Liquid
178
182
  end
179
183
 
180
184
  # Fetches an object starting at the local scope and then moving up the hierachy
181
- def find_variable(key)
185
+ def find_variable(key, raise_on_not_found: true)
182
186
  # This was changed from find() to find_index() because this is a very hot
183
187
  # path and find_index() is optimized in MRI to reduce object allocation
184
188
  index = @scopes.find_index { |s| s.key?(key) }
185
- scope = @scopes[index] if index
186
189
 
187
- variable = nil
188
-
189
- if scope.nil?
190
- @environments.each do |e|
191
- variable = lookup_and_evaluate(e, key)
192
- unless variable.nil?
193
- scope = e
194
- break
195
- end
196
- end
190
+ variable = if index
191
+ lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
192
+ else
193
+ try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
197
194
  end
198
195
 
199
- scope ||= @environments.last || @scopes.last
200
- variable ||= lookup_and_evaluate(scope, key)
201
-
202
- variable = variable.to_liquid
196
+ variable = variable.to_liquid
203
197
  variable.context = self if variable.respond_to?(:context=)
204
198
 
205
199
  variable
206
200
  end
207
201
 
208
- def lookup_and_evaluate(obj, key)
209
- if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
202
+ def lookup_and_evaluate(obj, key, raise_on_not_found: true)
203
+ if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
210
204
  raise Liquid::UndefinedVariable, "undefined variable #{key}"
211
205
  end
212
206
 
213
207
  value = obj[key]
214
208
 
215
209
  if value.is_a?(Proc) && obj.respond_to?(:[]=)
216
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
210
+ obj[key] = value.arity == 0 ? value.call : value.call(self)
217
211
  else
218
212
  value
219
213
  end
220
214
  end
221
215
 
216
+ def with_disabled_tags(tag_names)
217
+ tag_names.each do |name|
218
+ @disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
219
+ end
220
+ yield
221
+ ensure
222
+ tag_names.each do |name|
223
+ @disabled_tags[name] -= 1
224
+ end
225
+ end
226
+
227
+ def tag_disabled?(tag_name)
228
+ @disabled_tags.fetch(tag_name, 0) > 0
229
+ end
230
+
231
+ protected
232
+
233
+ attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
234
+
222
235
  private
223
236
 
237
+ attr_reader :base_scope_depth
238
+
239
+ def try_variable_find_in_environments(key, raise_on_not_found:)
240
+ @environments.each do |environment|
241
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
242
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
243
+ return found_variable
244
+ end
245
+ end
246
+ @static_environments.each do |environment|
247
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
248
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
249
+ return found_variable
250
+ end
251
+ end
252
+ nil
253
+ end
254
+
255
+ def check_overflow
256
+ raise StackLevelError, "Nesting too deep" if overflow?
257
+ end
258
+
259
+ def overflow?
260
+ base_scope_depth + @scopes.length > Block::MAX_DEPTH
261
+ end
262
+
263
+ def internal_error
264
+ # raise and catch to set backtrace and cause on exception
265
+ raise Liquid::InternalError, 'internal'
266
+ rescue Liquid::InternalError => exc
267
+ exc
268
+ end
269
+
224
270
  def squash_instance_assigns_with_environments
225
271
  @scopes.last.each_key do |k|
226
272
  @environments.each do |env|