liquid 2.6.3 → 5.4.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 (100) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +272 -26
  3. data/README.md +67 -3
  4. data/lib/liquid/block.rb +62 -94
  5. data/lib/liquid/block_body.rb +255 -0
  6. data/lib/liquid/condition.rb +96 -38
  7. data/lib/liquid/context.rb +172 -154
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +33 -14
  10. data/lib/liquid/errors.rb +56 -10
  11. data/lib/liquid/expression.rb +45 -0
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +27 -14
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +41 -0
  16. data/lib/liquid/interrupts.rb +3 -2
  17. data/lib/liquid/lexer.rb +62 -0
  18. data/lib/liquid/locales/en.yml +29 -0
  19. data/lib/liquid/parse_context.rb +54 -0
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +102 -0
  22. data/lib/liquid/parser_switching.rb +45 -0
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +35 -0
  25. data/lib/liquid/profiler.rb +139 -0
  26. data/lib/liquid/range_lookup.rb +47 -0
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +789 -118
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +49 -10
  36. data/lib/liquid/tags/assign.rb +61 -19
  37. data/lib/liquid/tags/break.rb +14 -4
  38. data/lib/liquid/tags/capture.rb +29 -21
  39. data/lib/liquid/tags/case.rb +80 -31
  40. data/lib/liquid/tags/comment.rb +24 -2
  41. data/lib/liquid/tags/continue.rb +14 -13
  42. data/lib/liquid/tags/cycle.rb +50 -32
  43. data/lib/liquid/tags/decrement.rb +24 -26
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +164 -100
  46. data/lib/liquid/tags/if.rb +105 -44
  47. data/lib/liquid/tags/ifchanged.rb +10 -11
  48. data/lib/liquid/tags/include.rb +85 -65
  49. data/lib/liquid/tags/increment.rb +24 -22
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +88 -0
  54. data/lib/liquid/tags/unless.rb +37 -21
  55. data/lib/liquid/template.rb +124 -46
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +68 -5
  60. data/lib/liquid/variable.rb +128 -32
  61. data/lib/liquid/variable_lookup.rb +96 -0
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +36 -13
  64. metadata +69 -77
  65. data/lib/extras/liquid_view.rb +0 -51
  66. data/lib/liquid/htmltags.rb +0 -73
  67. data/lib/liquid/module_ex.rb +0 -62
  68. data/lib/liquid/strainer.rb +0 -53
  69. data/test/liquid/assign_test.rb +0 -21
  70. data/test/liquid/block_test.rb +0 -58
  71. data/test/liquid/capture_test.rb +0 -40
  72. data/test/liquid/condition_test.rb +0 -127
  73. data/test/liquid/context_test.rb +0 -478
  74. data/test/liquid/drop_test.rb +0 -180
  75. data/test/liquid/error_handling_test.rb +0 -81
  76. data/test/liquid/file_system_test.rb +0 -29
  77. data/test/liquid/filter_test.rb +0 -125
  78. data/test/liquid/hash_ordering_test.rb +0 -25
  79. data/test/liquid/module_ex_test.rb +0 -87
  80. data/test/liquid/output_test.rb +0 -116
  81. data/test/liquid/parsing_quirks_test.rb +0 -52
  82. data/test/liquid/regexp_test.rb +0 -44
  83. data/test/liquid/security_test.rb +0 -64
  84. data/test/liquid/standard_filter_test.rb +0 -263
  85. data/test/liquid/strainer_test.rb +0 -52
  86. data/test/liquid/tags/break_tag_test.rb +0 -16
  87. data/test/liquid/tags/continue_tag_test.rb +0 -16
  88. data/test/liquid/tags/for_tag_test.rb +0 -297
  89. data/test/liquid/tags/html_tag_test.rb +0 -63
  90. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  91. data/test/liquid/tags/include_tag_test.rb +0 -166
  92. data/test/liquid/tags/increment_tag_test.rb +0 -24
  93. data/test/liquid/tags/raw_tag_test.rb +0 -24
  94. data/test/liquid/tags/standard_tag_test.rb +0 -295
  95. data/test/liquid/tags/statements_test.rb +0 -134
  96. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  97. data/test/liquid/template_test.rb +0 -146
  98. data/test/liquid/variable_test.rb +0 -186
  99. data/test/test_helper.rb +0 -29
  100. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -1,5 +1,6 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
+ module Liquid
3
4
  # Context keeps the variable stack and resolves variables, as well as keywords
4
5
  #
5
6
  # context['variable'] = 'testing'
@@ -13,28 +14,53 @@ module Liquid
13
14
  #
14
15
  # context['bob'] #=> nil class Context
15
16
  class Context
16
- attr_reader :scopes, :errors, :registers, :environments, :resource_limits
17
-
18
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @rethrow_errors = rethrow_errors
24
- @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
25
- 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
19
+
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
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.is_a?(Registers) ? registers : Registers.new(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
+ @registers.static[:cached_partials] ||= {}
43
+ @registers.static[:file_system] ||= Liquid::Template.file_system
44
+ @registers.static[:template_factory] ||= Liquid::TemplateFactory.new
45
+
46
+ self.exception_renderer = Template.default_exception_renderer
47
+ if rethrow_errors
48
+ self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
49
+ end
26
50
 
27
- @interrupts = []
51
+ yield self if block_given?
52
+
53
+ # Do this last, since it could result in this object being passed to a Proc in the environment
54
+ squash_instance_assigns_with_environments
28
55
  end
56
+ # rubocop:enable Metrics/ParameterLists
29
57
 
30
- def resource_limits_reached?
31
- (@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
32
- (@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
33
- (@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
58
+ def warnings
59
+ @warnings ||= []
34
60
  end
35
61
 
36
62
  def strainer
37
- @strainer ||= Strainer.create(self)
63
+ @strainer ||= StrainerFactory.create(self, @filters)
38
64
  end
39
65
 
40
66
  # Adds filters to this context.
@@ -43,17 +69,17 @@ module Liquid
43
69
  # for that
44
70
  def add_filters(filters)
45
71
  filters = [filters].flatten.compact
72
+ @filters += filters
73
+ @strainer = nil
74
+ end
46
75
 
47
- filters.each do |f|
48
- raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
49
- Strainer.add_known_filter(f)
50
- strainer.extend(f)
51
- end
76
+ def apply_global_filter(obj)
77
+ global_filter.nil? ? obj : global_filter.call(obj)
52
78
  end
53
79
 
54
80
  # are there any not handled interrupts?
55
- def has_interrupt?
56
- @interrupts.any?
81
+ def interrupt?
82
+ !@interrupts.empty?
57
83
  end
58
84
 
59
85
  # push an interrupt to the stack. this interrupt is considered not handled.
@@ -66,26 +92,22 @@ module Liquid
66
92
  @interrupts.pop
67
93
  end
68
94
 
69
- def handle_error(e)
95
+ def handle_error(e, line_number = nil)
96
+ e = internal_error unless e.is_a?(Liquid::Error)
97
+ e.template_name ||= template_name
98
+ e.line_number ||= line_number
70
99
  errors.push(e)
71
- raise if @rethrow_errors
72
-
73
- case e
74
- when SyntaxError
75
- "Liquid syntax error: #{e.message}"
76
- else
77
- "Liquid error: #{e.message}"
78
- end
100
+ exception_renderer.call(e).to_s
79
101
  end
80
102
 
81
103
  def invoke(method, *args)
82
- strainer.invoke(method, *args)
104
+ strainer.invoke(method, *args).to_liquid
83
105
  end
84
106
 
85
107
  # Push new local scope on the stack. use <tt>Context#stack</tt> instead
86
- def push(new_scope={})
108
+ def push(new_scope = {})
87
109
  @scopes.unshift(new_scope)
88
- raise StackLevelError, "Nesting too deep" if @scopes.length > 100
110
+ check_overflow
89
111
  end
90
112
 
91
113
  # Merge a hash of variables in the current local scope
@@ -106,14 +128,34 @@ module Liquid
106
128
  # context['var'] = 'hi'
107
129
  # end
108
130
  #
109
- # context['var] #=> nil
110
- def stack(new_scope={})
131
+ # context['var'] #=> nil
132
+ def stack(new_scope = {})
111
133
  push(new_scope)
112
134
  yield
113
135
  ensure
114
136
  pop
115
137
  end
116
138
 
139
+ # Creates a new context inheriting resource limits, filters, environment etc.,
140
+ # but with an isolated scope.
141
+ def new_isolated_subcontext
142
+ check_overflow
143
+
144
+ self.class.build(
145
+ resource_limits: resource_limits,
146
+ static_environments: static_environments,
147
+ registers: Registers.new(registers)
148
+ ).tap do |subcontext|
149
+ subcontext.base_scope_depth = base_scope_depth + 1
150
+ subcontext.exception_renderer = exception_renderer
151
+ subcontext.filters = @filters
152
+ subcontext.strainer = nil
153
+ subcontext.errors = errors
154
+ subcontext.warnings = warnings
155
+ subcontext.disabled_tags = @disabled_tags
156
+ end
157
+ end
158
+
117
159
  def clear_instance_assigns
118
160
  @scopes[0] = {}
119
161
  end
@@ -123,145 +165,121 @@ module Liquid
123
165
  @scopes[0][key] = value
124
166
  end
125
167
 
126
- def [](key)
127
- resolve(key)
168
+ # Look up variable, either resolve directly after considering the name. We can directly handle
169
+ # Strings, digits, floats and booleans (true,false).
170
+ # If no match is made we lookup the variable in the current scope and
171
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
172
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
173
+ #
174
+ # Example:
175
+ # products == empty #=> products.empty?
176
+ def [](expression)
177
+ evaluate(Expression.parse(expression))
128
178
  end
129
179
 
130
- def has_key?(key)
131
- resolve(key) != nil
180
+ def key?(key)
181
+ self[key] != nil
132
182
  end
133
183
 
134
- private
135
- LITERALS = {
136
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
137
- 'true' => true,
138
- 'false' => false,
139
- 'blank' => :blank?,
140
- 'empty' => :empty?
141
- }
142
-
143
- # Look up variable, either resolve directly after considering the name. We can directly handle
144
- # Strings, digits, floats and booleans (true,false).
145
- # If no match is made we lookup the variable in the current scope and
146
- # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
147
- # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
148
- #
149
- # Example:
150
- # products == empty #=> products.empty?
151
- def resolve(key)
152
- if LITERALS.key?(key)
153
- LITERALS[key]
154
- else
155
- case key
156
- when /^'(.*)'$/ # Single quoted strings
157
- $1
158
- when /^"(.*)"$/ # Double quoted strings
159
- $1
160
- when /^(-?\d+)$/ # Integer and floats
161
- $1.to_i
162
- when /^\((\S+)\.\.(\S+)\)$/ # Ranges
163
- (resolve($1).to_i..resolve($2).to_i)
164
- when /^(-?\d[\d\.]+)$/ # Floats
165
- $1.to_f
166
- else
167
- variable(key)
168
- end
169
- end
170
- end
184
+ def evaluate(object)
185
+ object.respond_to?(:evaluate) ? object.evaluate(self) : object
186
+ end
171
187
 
172
- # Fetches an object starting at the local scope and then moving up the hierachy
173
- def find_variable(key)
174
- scope = @scopes.find { |s| s.has_key?(key) }
175
- variable = nil
176
-
177
- if scope.nil?
178
- @environments.each do |e|
179
- if variable = lookup_and_evaluate(e, key)
180
- scope = e
181
- break
182
- end
183
- end
184
- end
188
+ # Fetches an object starting at the local scope and then moving up the hierachy
189
+ def find_variable(key, raise_on_not_found: true)
190
+ # This was changed from find() to find_index() because this is a very hot
191
+ # path and find_index() is optimized in MRI to reduce object allocation
192
+ index = @scopes.find_index { |s| s.key?(key) }
193
+
194
+ variable = if index
195
+ lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
196
+ else
197
+ try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
198
+ end
185
199
 
186
- scope ||= @environments.last || @scopes.last
187
- variable ||= lookup_and_evaluate(scope, key)
200
+ variable = variable.to_liquid
201
+ variable.context = self if variable.respond_to?(:context=)
188
202
 
189
- variable = variable.to_liquid
190
- variable.context = self if variable.respond_to?(:context=)
203
+ variable
204
+ end
191
205
 
192
- return variable
206
+ def lookup_and_evaluate(obj, key, raise_on_not_found: true)
207
+ if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
208
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
193
209
  end
194
210
 
195
- # Resolves namespaced queries gracefully.
196
- #
197
- # Example
198
- # @context['hash'] = {"name" => 'tobi'}
199
- # assert_equal 'tobi', @context['hash.name']
200
- # assert_equal 'tobi', @context['hash["name"]']
201
- def variable(markup)
202
- parts = markup.scan(VariableParser)
203
- square_bracketed = /^\[(.*)\]$/
211
+ value = obj[key]
204
212
 
205
- first_part = parts.shift
213
+ if value.is_a?(Proc) && obj.respond_to?(:[]=)
214
+ obj[key] = value.arity == 0 ? value.call : value.call(self)
215
+ else
216
+ value
217
+ end
218
+ end
206
219
 
207
- if first_part =~ square_bracketed
208
- first_part = resolve($1)
209
- end
220
+ def with_disabled_tags(tag_names)
221
+ tag_names.each do |name|
222
+ @disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
223
+ end
224
+ yield
225
+ ensure
226
+ tag_names.each do |name|
227
+ @disabled_tags[name] -= 1
228
+ end
229
+ end
210
230
 
211
- if object = find_variable(first_part)
231
+ def tag_disabled?(tag_name)
232
+ @disabled_tags.fetch(tag_name, 0) > 0
233
+ end
212
234
 
213
- parts.each do |part|
214
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
235
+ protected
215
236
 
216
- # If object is a hash- or array-like object we look for the
217
- # presence of the key and if its available we return it
218
- if object.respond_to?(:[]) and
219
- ((object.respond_to?(:has_key?) and object.has_key?(part)) or
220
- (object.respond_to?(:fetch) and part.is_a?(Integer)))
237
+ attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
221
238
 
222
- # if its a proc we will replace the entry with the proc
223
- res = lookup_and_evaluate(object, part)
224
- object = res.to_liquid
239
+ private
225
240
 
226
- # Some special cases. If the part wasn't in square brackets and
227
- # no key with the same name was found we interpret following calls
228
- # as commands and call them on the current object
229
- elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
241
+ attr_reader :base_scope_depth
230
242
 
231
- object = object.send(part.intern).to_liquid
243
+ def try_variable_find_in_environments(key, raise_on_not_found:)
244
+ @environments.each do |environment|
245
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
246
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
247
+ return found_variable
248
+ end
249
+ end
250
+ @static_environments.each do |environment|
251
+ found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
252
+ if !found_variable.nil? || @strict_variables && raise_on_not_found
253
+ return found_variable
254
+ end
255
+ end
256
+ nil
257
+ end
232
258
 
233
- # No key was present with the desired value and it wasn't one of the directly supported
234
- # keywords either. The only thing we got left is to return nil
235
- else
236
- return nil
237
- end
259
+ def check_overflow
260
+ raise StackLevelError, "Nesting too deep" if overflow?
261
+ end
238
262
 
239
- # If we are dealing with a drop here we have to
240
- object.context = self if object.respond_to?(:context=)
241
- end
242
- end
263
+ def overflow?
264
+ base_scope_depth + @scopes.length > Block::MAX_DEPTH
265
+ end
243
266
 
244
- object
245
- end # variable
267
+ def internal_error
268
+ # raise and catch to set backtrace and cause on exception
269
+ raise Liquid::InternalError, 'internal'
270
+ rescue Liquid::InternalError => exc
271
+ exc
272
+ end
246
273
 
247
- def lookup_and_evaluate(obj, key)
248
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
249
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
250
- else
251
- value
252
- end
253
- end # lookup_and_evaluate
254
-
255
- def squash_instance_assigns_with_environments
256
- @scopes.last.each_key do |k|
257
- @environments.each do |env|
258
- if env.has_key?(k)
259
- scopes.last[k] = lookup_and_evaluate(env, k)
260
- break
261
- end
274
+ def squash_instance_assigns_with_environments
275
+ @scopes.last.each_key do |k|
276
+ @environments.each do |env|
277
+ if env.key?(k)
278
+ scopes.last[k] = lookup_and_evaluate(env, k)
279
+ break
262
280
  end
263
281
  end
264
- end # squash_instance_assigns_with_environments
282
+ end
283
+ end # squash_instance_assigns_with_environments
265
284
  end # Context
266
-
267
285
  end # Liquid
@@ -1,17 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- class Document < Block
3
- # we don't need markup to open this block
4
- def initialize(tokens)
5
- parse(tokens)
4
+ class Document
5
+ def self.parse(tokens, parse_context)
6
+ doc = new(parse_context)
7
+ doc.parse(tokens, parse_context)
8
+ doc
9
+ end
10
+
11
+ attr_reader :parse_context, :body
12
+
13
+ def initialize(parse_context)
14
+ @parse_context = parse_context
15
+ @body = new_body
16
+ end
17
+
18
+ def nodelist
19
+ @body.nodelist
20
+ end
21
+
22
+ def parse(tokenizer, parse_context)
23
+ while parse_body(tokenizer)
24
+ end
25
+ @body.freeze
26
+ rescue SyntaxError => e
27
+ e.line_number ||= parse_context.line_number
28
+ raise
6
29
  end
7
30
 
8
- # There isn't a real delimiter
9
- def block_delimiter
10
- []
31
+ def unknown_tag(tag, _markup, _tokenizer)
32
+ case tag
33
+ when 'else', 'end'
34
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
35
+ else
36
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
37
+ end
38
+ end
39
+
40
+ def render_to_output_buffer(context, output)
41
+ @body.render_to_output_buffer(context, output)
42
+ end
43
+
44
+ def render(context)
45
+ render_to_output_buffer(context, +'')
46
+ end
47
+
48
+ private
49
+
50
+ def new_body
51
+ parse_context.new_block_body
11
52
  end
12
53
 
13
- # Document blocks don't need to be terminated since they are not actually opened
14
- def assert_missing_delimitation!
54
+ def parse_body(tokenizer)
55
+ @body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
56
+ if unknown_tag_name
57
+ unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
58
+ true
59
+ else
60
+ false
61
+ end
62
+ end
15
63
  end
16
64
  end
17
65
  end
data/lib/liquid/drop.rb CHANGED
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module Liquid
4
-
5
6
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
7
  # Methods of drops are callable.
7
8
  # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,43 +20,61 @@ module Liquid
19
20
  # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
21
  # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
22
  #
22
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
- # catch all.
23
+ # Your drop can either implement the methods sans any parameters
24
+ # or implement the liquid_method_missing(name) method which is a catch all.
24
25
  class Drop
25
26
  attr_writer :context
26
27
 
27
- EMPTY_STRING = ''.freeze
28
-
29
28
  # Catch all for the method
30
- def before_method(method)
31
- nil
29
+ def liquid_method_missing(method)
30
+ return nil unless @context&.strict_variables
31
+ raise Liquid::UndefinedDropMethod, "undefined method #{method}"
32
32
  end
33
33
 
34
34
  # called by liquid to invoke a drop
35
35
  def invoke_drop(method_or_key)
36
- if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
36
+ if self.class.invokable?(method_or_key)
37
37
  send(method_or_key)
38
38
  else
39
- before_method(method_or_key)
39
+ liquid_method_missing(method_or_key)
40
40
  end
41
41
  end
42
42
 
43
- def has_key?(name)
43
+ def key?(_name)
44
44
  true
45
45
  end
46
46
 
47
+ def inspect
48
+ self.class.to_s
49
+ end
50
+
47
51
  def to_liquid
48
52
  self
49
53
  end
50
54
 
51
- alias :[] :invoke_drop
55
+ def to_s
56
+ self.class.name
57
+ end
52
58
 
53
- private
59
+ alias_method :[], :invoke_drop
54
60
 
55
61
  # Check for method existence without invoking respond_to?, which creates symbols
56
62
  def self.invokable?(method_name)
57
- @invokable_methods ||= Set.new(["to_liquid"] + (public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
58
- @invokable_methods.include?(method_name.to_s)
63
+ invokable_methods.include?(method_name.to_s)
64
+ end
65
+
66
+ def self.invokable_methods
67
+ @invokable_methods ||= begin
68
+ blacklist = Liquid::Drop.public_instance_methods + [:each]
69
+
70
+ if include?(Enumerable)
71
+ blacklist += Enumerable.public_instance_methods
72
+ blacklist -= [:sort, :count, :first, :min, :max]
73
+ end
74
+
75
+ whitelist = [:to_liquid] + (public_instance_methods - blacklist)
76
+ Set.new(whitelist.map(&:to_s))
77
+ end
59
78
  end
60
79
  end
61
80
  end
data/lib/liquid/errors.rb CHANGED
@@ -1,12 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- class Error < ::StandardError; end
3
-
4
- class ArgumentError < Error; end
5
- class ContextError < Error; end
6
- class FilterNotFound < Error; end
7
- class FileSystemError < Error; end
8
- class StandardError < Error; end
9
- class SyntaxError < Error; end
10
- class StackLevelError < Error; end
11
- class MemoryError < Error; end
4
+ class Error < ::StandardError
5
+ attr_accessor :line_number
6
+ attr_accessor :template_name
7
+ attr_accessor :markup_context
8
+
9
+ def to_s(with_prefix = true)
10
+ str = +""
11
+ str << message_prefix if with_prefix
12
+ str << super()
13
+
14
+ if markup_context
15
+ str << " "
16
+ str << markup_context
17
+ end
18
+
19
+ str
20
+ end
21
+
22
+ private
23
+
24
+ def message_prefix
25
+ str = +""
26
+ str << if is_a?(SyntaxError)
27
+ "Liquid syntax error"
28
+ else
29
+ "Liquid error"
30
+ end
31
+
32
+ if line_number
33
+ str << " ("
34
+ str << template_name << " " if template_name
35
+ str << "line " << line_number.to_s << ")"
36
+ end
37
+
38
+ str << ": "
39
+ str
40
+ end
41
+ end
42
+
43
+ ArgumentError = Class.new(Error)
44
+ ContextError = Class.new(Error)
45
+ FileSystemError = Class.new(Error)
46
+ StandardError = Class.new(Error)
47
+ SyntaxError = Class.new(Error)
48
+ StackLevelError = Class.new(Error)
49
+ MemoryError = Class.new(Error)
50
+ ZeroDivisionError = Class.new(Error)
51
+ FloatDomainError = Class.new(Error)
52
+ UndefinedVariable = Class.new(Error)
53
+ UndefinedDropMethod = Class.new(Error)
54
+ UndefinedFilter = Class.new(Error)
55
+ MethodOverrideError = Class.new(Error)
56
+ DisabledError = Class.new(Error)
57
+ InternalError = Class.new(Error)
12
58
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Expression
5
+ LITERALS = {
6
+ nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
7
+ 'true' => true,
8
+ 'false' => false,
9
+ 'blank' => '',
10
+ 'empty' => ''
11
+ }.freeze
12
+
13
+ INTEGERS_REGEX = /\A(-?\d+)\z/
14
+ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
15
+
16
+ # Use an atomic group (?>...) to avoid pathological backtracing from
17
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
18
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
19
+
20
+ def self.parse(markup)
21
+ return nil unless markup
22
+
23
+ markup = markup.strip
24
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
25
+ (markup.start_with?("'") && markup.end_with?("'"))
26
+ return markup[1..-2]
27
+ end
28
+
29
+ case markup
30
+ when INTEGERS_REGEX
31
+ Regexp.last_match(1).to_i
32
+ when RANGES_REGEX
33
+ RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
34
+ when FLOATS_REGEX
35
+ Regexp.last_match(1).to_f
36
+ else
37
+ if LITERALS.key?(markup)
38
+ LITERALS[markup]
39
+ else
40
+ VariableLookup.parse(markup)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end