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.
- checksums.yaml +5 -5
- data/History.md +272 -26
- data/README.md +67 -3
- data/lib/liquid/block.rb +62 -94
- data/lib/liquid/block_body.rb +255 -0
- data/lib/liquid/condition.rb +96 -38
- data/lib/liquid/context.rb +172 -154
- data/lib/liquid/document.rb +57 -9
- data/lib/liquid/drop.rb +33 -14
- data/lib/liquid/errors.rb +56 -10
- data/lib/liquid/expression.rb +45 -0
- data/lib/liquid/extensions.rb +21 -7
- data/lib/liquid/file_system.rb +27 -14
- data/lib/liquid/forloop_drop.rb +92 -0
- data/lib/liquid/i18n.rb +41 -0
- data/lib/liquid/interrupts.rb +3 -2
- data/lib/liquid/lexer.rb +62 -0
- data/lib/liquid/locales/en.yml +29 -0
- data/lib/liquid/parse_context.rb +54 -0
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +102 -0
- data/lib/liquid/parser_switching.rb +45 -0
- data/lib/liquid/partial_cache.rb +24 -0
- data/lib/liquid/profiler/hooks.rb +35 -0
- data/lib/liquid/profiler.rb +139 -0
- data/lib/liquid/range_lookup.rb +47 -0
- data/lib/liquid/registers.rb +51 -0
- data/lib/liquid/resource_limits.rb +62 -0
- data/lib/liquid/standardfilters.rb +789 -118
- data/lib/liquid/strainer_factory.rb +41 -0
- data/lib/liquid/strainer_template.rb +62 -0
- data/lib/liquid/tablerowloop_drop.rb +121 -0
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +21 -0
- data/lib/liquid/tag.rb +49 -10
- data/lib/liquid/tags/assign.rb +61 -19
- data/lib/liquid/tags/break.rb +14 -4
- data/lib/liquid/tags/capture.rb +29 -21
- data/lib/liquid/tags/case.rb +80 -31
- data/lib/liquid/tags/comment.rb +24 -2
- data/lib/liquid/tags/continue.rb +14 -13
- data/lib/liquid/tags/cycle.rb +50 -32
- data/lib/liquid/tags/decrement.rb +24 -26
- data/lib/liquid/tags/echo.rb +41 -0
- data/lib/liquid/tags/for.rb +164 -100
- data/lib/liquid/tags/if.rb +105 -44
- data/lib/liquid/tags/ifchanged.rb +10 -11
- data/lib/liquid/tags/include.rb +85 -65
- data/lib/liquid/tags/increment.rb +24 -22
- data/lib/liquid/tags/inline_comment.rb +43 -0
- data/lib/liquid/tags/raw.rb +50 -11
- data/lib/liquid/tags/render.rb +109 -0
- data/lib/liquid/tags/table_row.rb +88 -0
- data/lib/liquid/tags/unless.rb +37 -21
- data/lib/liquid/template.rb +124 -46
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +39 -0
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +68 -5
- data/lib/liquid/variable.rb +128 -32
- data/lib/liquid/variable_lookup.rb +96 -0
- data/lib/liquid/version.rb +3 -1
- data/lib/liquid.rb +36 -13
- metadata +69 -77
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/liquid/htmltags.rb +0 -73
- data/lib/liquid/module_ex.rb +0 -62
- data/lib/liquid/strainer.rb +0 -53
- data/test/liquid/assign_test.rb +0 -21
- data/test/liquid/block_test.rb +0 -58
- data/test/liquid/capture_test.rb +0 -40
- data/test/liquid/condition_test.rb +0 -127
- data/test/liquid/context_test.rb +0 -478
- data/test/liquid/drop_test.rb +0 -180
- data/test/liquid/error_handling_test.rb +0 -81
- data/test/liquid/file_system_test.rb +0 -29
- data/test/liquid/filter_test.rb +0 -125
- data/test/liquid/hash_ordering_test.rb +0 -25
- data/test/liquid/module_ex_test.rb +0 -87
- data/test/liquid/output_test.rb +0 -116
- data/test/liquid/parsing_quirks_test.rb +0 -52
- data/test/liquid/regexp_test.rb +0 -44
- data/test/liquid/security_test.rb +0 -64
- data/test/liquid/standard_filter_test.rb +0 -263
- data/test/liquid/strainer_test.rb +0 -52
- data/test/liquid/tags/break_tag_test.rb +0 -16
- data/test/liquid/tags/continue_tag_test.rb +0 -16
- data/test/liquid/tags/for_tag_test.rb +0 -297
- data/test/liquid/tags/html_tag_test.rb +0 -63
- data/test/liquid/tags/if_else_tag_test.rb +0 -166
- data/test/liquid/tags/include_tag_test.rb +0 -166
- data/test/liquid/tags/increment_tag_test.rb +0 -24
- data/test/liquid/tags/raw_tag_test.rb +0 -24
- data/test/liquid/tags/standard_tag_test.rb +0 -295
- data/test/liquid/tags/statements_test.rb +0 -134
- data/test/liquid/tags/unless_else_tag_test.rb +0 -26
- data/test/liquid/template_test.rb +0 -146
- data/test/liquid/variable_test.rb +0 -186
- data/test/test_helper.rb +0 -29
- /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/context.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
31
|
-
|
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 ||=
|
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
|
-
|
48
|
-
|
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
|
56
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
127
|
-
|
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
|
131
|
-
|
180
|
+
def key?(key)
|
181
|
+
self[key] != nil
|
132
182
|
end
|
133
183
|
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
187
|
-
|
200
|
+
variable = variable.to_liquid
|
201
|
+
variable.context = self if variable.respond_to?(:context=)
|
188
202
|
|
189
|
-
|
190
|
-
|
203
|
+
variable
|
204
|
+
end
|
191
205
|
|
192
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
231
|
+
def tag_disabled?(tag_name)
|
232
|
+
@disabled_tags.fetch(tag_name, 0) > 0
|
233
|
+
end
|
212
234
|
|
213
|
-
|
214
|
-
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
235
|
+
protected
|
215
236
|
|
216
|
-
|
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
|
-
|
223
|
-
res = lookup_and_evaluate(object, part)
|
224
|
-
object = res.to_liquid
|
239
|
+
private
|
225
240
|
|
226
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
return nil
|
237
|
-
end
|
259
|
+
def check_overflow
|
260
|
+
raise StackLevelError, "Nesting too deep" if overflow?
|
261
|
+
end
|
238
262
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
end
|
263
|
+
def overflow?
|
264
|
+
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
265
|
+
end
|
243
266
|
|
244
|
-
|
245
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
282
|
+
end
|
283
|
+
end # squash_instance_assigns_with_environments
|
265
284
|
end # Context
|
266
|
-
|
267
285
|
end # Liquid
|
data/lib/liquid/document.rb
CHANGED
@@ -1,17 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Liquid
|
2
|
-
class Document
|
3
|
-
|
4
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
14
|
-
|
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
|
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
|
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
|
36
|
+
if self.class.invokable?(method_or_key)
|
37
37
|
send(method_or_key)
|
38
38
|
else
|
39
|
-
|
39
|
+
liquid_method_missing(method_or_key)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
def
|
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
|
-
|
55
|
+
def to_s
|
56
|
+
self.class.name
|
57
|
+
end
|
52
58
|
|
53
|
-
|
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
|
-
|
58
|
-
|
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
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|