liquid 4.0.0.rc3 → 5.0.0

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 (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|