liquid 3.0.0 → 4.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.
- checksums.yaml +4 -4
- data/History.md +130 -62
- data/README.md +31 -0
- data/lib/liquid/block.rb +31 -124
- data/lib/liquid/block_body.rb +75 -59
- data/lib/liquid/condition.rb +23 -22
- data/lib/liquid/context.rb +51 -60
- data/lib/liquid/document.rb +19 -9
- data/lib/liquid/drop.rb +17 -16
- data/lib/liquid/errors.rb +20 -24
- data/lib/liquid/expression.rb +15 -3
- data/lib/liquid/extensions.rb +13 -7
- data/lib/liquid/file_system.rb +11 -11
- data/lib/liquid/forloop_drop.rb +42 -0
- data/lib/liquid/i18n.rb +5 -5
- data/lib/liquid/interrupts.rb +1 -2
- data/lib/liquid/lexer.rb +6 -4
- data/lib/liquid/locales/en.yml +5 -1
- data/lib/liquid/parse_context.rb +37 -0
- data/lib/liquid/parser.rb +1 -1
- data/lib/liquid/parser_switching.rb +4 -4
- data/lib/liquid/profiler/hooks.rb +7 -7
- data/lib/liquid/profiler.rb +18 -19
- data/lib/liquid/range_lookup.rb +16 -1
- data/lib/liquid/resource_limits.rb +23 -0
- data/lib/liquid/standardfilters.rb +121 -61
- data/lib/liquid/strainer.rb +32 -29
- data/lib/liquid/tablerowloop_drop.rb +62 -0
- data/lib/liquid/tag.rb +9 -8
- data/lib/liquid/tags/assign.rb +17 -4
- data/lib/liquid/tags/break.rb +0 -3
- data/lib/liquid/tags/capture.rb +2 -2
- data/lib/liquid/tags/case.rb +19 -12
- data/lib/liquid/tags/comment.rb +2 -2
- data/lib/liquid/tags/cycle.rb +6 -6
- data/lib/liquid/tags/decrement.rb +1 -4
- data/lib/liquid/tags/for.rb +95 -75
- data/lib/liquid/tags/if.rb +48 -43
- data/lib/liquid/tags/ifchanged.rb +0 -2
- data/lib/liquid/tags/include.rb +61 -52
- data/lib/liquid/tags/raw.rb +32 -4
- data/lib/liquid/tags/table_row.rb +12 -31
- data/lib/liquid/tags/unless.rb +4 -5
- data/lib/liquid/template.rb +42 -54
- data/lib/liquid/tokenizer.rb +31 -0
- data/lib/liquid/utils.rb +52 -8
- data/lib/liquid/variable.rb +46 -45
- data/lib/liquid/variable_lookup.rb +9 -5
- data/lib/liquid/version.rb +1 -1
- data/lib/liquid.rb +9 -7
- data/test/integration/assign_test.rb +18 -8
- data/test/integration/blank_test.rb +14 -14
- data/test/integration/capture_test.rb +10 -0
- data/test/integration/context_test.rb +2 -2
- data/test/integration/document_test.rb +19 -0
- data/test/integration/drop_test.rb +42 -40
- data/test/integration/error_handling_test.rb +99 -46
- data/test/integration/filter_test.rb +72 -19
- data/test/integration/hash_ordering_test.rb +9 -9
- data/test/integration/output_test.rb +34 -27
- data/test/integration/parsing_quirks_test.rb +15 -13
- data/test/integration/render_profiling_test.rb +20 -20
- data/test/integration/security_test.rb +9 -7
- data/test/integration/standard_filter_test.rb +198 -42
- data/test/integration/tags/break_tag_test.rb +1 -2
- data/test/integration/tags/continue_tag_test.rb +0 -1
- data/test/integration/tags/for_tag_test.rb +133 -98
- data/test/integration/tags/if_else_tag_test.rb +96 -77
- data/test/integration/tags/include_tag_test.rb +34 -30
- data/test/integration/tags/increment_tag_test.rb +10 -11
- data/test/integration/tags/raw_tag_test.rb +7 -1
- data/test/integration/tags/standard_tag_test.rb +121 -122
- data/test/integration/tags/statements_test.rb +3 -5
- data/test/integration/tags/table_row_test.rb +20 -19
- data/test/integration/tags/unless_else_tag_test.rb +6 -6
- data/test/integration/template_test.rb +190 -49
- data/test/integration/trim_mode_test.rb +525 -0
- data/test/integration/variable_test.rb +23 -13
- data/test/test_helper.rb +44 -9
- data/test/unit/block_unit_test.rb +8 -5
- data/test/unit/condition_unit_test.rb +86 -77
- data/test/unit/context_unit_test.rb +48 -57
- data/test/unit/file_system_unit_test.rb +3 -3
- data/test/unit/i18n_unit_test.rb +2 -2
- data/test/unit/lexer_unit_test.rb +11 -8
- data/test/unit/parser_unit_test.rb +2 -2
- data/test/unit/regexp_unit_test.rb +1 -1
- data/test/unit/strainer_unit_test.rb +85 -8
- data/test/unit/tag_unit_test.rb +7 -2
- data/test/unit/tags/case_tag_unit_test.rb +1 -1
- data/test/unit/tags/for_tag_unit_test.rb +2 -2
- data/test/unit/tags/if_tag_unit_test.rb +1 -1
- data/test/unit/template_unit_test.rb +14 -5
- data/test/unit/tokenizer_unit_test.rb +24 -7
- data/test/unit/variable_unit_test.rb +66 -43
- metadata +55 -50
- data/lib/liquid/module_ex.rb +0 -62
- data/lib/liquid/token.rb +0 -18
- data/test/unit/module_ex_unit_test.rb +0 -87
- /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/block_body.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Liquid
|
2
2
|
class BlockBody
|
3
|
-
FullToken = /\A#{TagStart}
|
4
|
-
ContentOfVariable = /\A#{VariableStart}(
|
3
|
+
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
4
|
+
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
5
5
|
TAGSTART = "{%".freeze
|
6
6
|
VARSTART = "{{".freeze
|
7
7
|
|
@@ -12,88 +12,91 @@ module Liquid
|
|
12
12
|
@blank = true
|
13
13
|
end
|
14
14
|
|
15
|
-
def parse(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@nodelist << new_tag
|
31
|
-
else
|
32
|
-
# end parsing if we reach an unknown tag and let the caller decide
|
33
|
-
# determine how to proceed
|
34
|
-
return yield tag_name, markup
|
35
|
-
end
|
15
|
+
def parse(tokenizer, parse_context)
|
16
|
+
parse_context.line_number = tokenizer.line_number
|
17
|
+
while token = tokenizer.shift
|
18
|
+
unless token.empty?
|
19
|
+
case
|
20
|
+
when token.start_with?(TAGSTART)
|
21
|
+
whitespace_handler(token, parse_context)
|
22
|
+
if token =~ FullToken
|
23
|
+
tag_name = $1
|
24
|
+
markup = $2
|
25
|
+
# fetch the tag from registered blocks
|
26
|
+
if tag = registered_tags[tag_name]
|
27
|
+
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
28
|
+
@blank &&= new_tag.blank?
|
29
|
+
@nodelist << new_tag
|
36
30
|
else
|
37
|
-
|
31
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
32
|
+
# determine how to proceed
|
33
|
+
return yield tag_name, markup
|
38
34
|
end
|
39
|
-
when token.start_with?(VARSTART)
|
40
|
-
new_var = create_variable(token, options)
|
41
|
-
new_var.line_number = token.line_number if token.is_a?(Token)
|
42
|
-
@nodelist << new_var
|
43
|
-
@blank = false
|
44
35
|
else
|
45
|
-
|
46
|
-
|
36
|
+
raise_missing_tag_terminator(token, parse_context)
|
37
|
+
end
|
38
|
+
when token.start_with?(VARSTART)
|
39
|
+
whitespace_handler(token, parse_context)
|
40
|
+
@nodelist << create_variable(token, parse_context)
|
41
|
+
@blank = false
|
42
|
+
else
|
43
|
+
if parse_context.trim_whitespace
|
44
|
+
token.lstrip!
|
47
45
|
end
|
46
|
+
parse_context.trim_whitespace = false
|
47
|
+
@nodelist << token
|
48
|
+
@blank &&= !!(token =~ /\A\s*\z/)
|
48
49
|
end
|
49
|
-
rescue SyntaxError => e
|
50
|
-
e.set_line_number_from_token(token)
|
51
|
-
raise
|
52
50
|
end
|
51
|
+
parse_context.line_number = tokenizer.line_number
|
53
52
|
end
|
54
53
|
|
55
54
|
yield nil, nil
|
56
55
|
end
|
57
56
|
|
58
|
-
def
|
59
|
-
|
57
|
+
def whitespace_handler(token, parse_context)
|
58
|
+
if token[2] == WhitespaceControl
|
59
|
+
previous_token = @nodelist.last
|
60
|
+
if previous_token.is_a? String
|
61
|
+
previous_token.rstrip!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
60
65
|
end
|
61
66
|
|
62
|
-
def
|
63
|
-
|
64
|
-
nodelist.each do |node|
|
65
|
-
all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
|
66
|
-
end
|
67
|
-
all_warnings
|
67
|
+
def blank?
|
68
|
+
@blank
|
68
69
|
end
|
69
70
|
|
70
71
|
def render(context)
|
71
72
|
output = []
|
72
|
-
context.resource_limits
|
73
|
-
context.resource_limits[:render_score_current] += @nodelist.length
|
73
|
+
context.resource_limits.render_score += @nodelist.length
|
74
74
|
|
75
75
|
@nodelist.each do |token|
|
76
76
|
# Break out if we have any unhanded interrupts.
|
77
|
-
break if context.
|
77
|
+
break if context.interrupt?
|
78
78
|
|
79
79
|
begin
|
80
80
|
# If we get an Interrupt that means the block must stop processing. An
|
81
81
|
# Interrupt is any command that stops block execution such as {% break %}
|
82
82
|
# or {% continue %}
|
83
|
-
if token.is_a?(Continue)
|
83
|
+
if token.is_a?(Continue) || token.is_a?(Break)
|
84
84
|
context.push_interrupt(token.interrupt)
|
85
85
|
break
|
86
86
|
end
|
87
87
|
|
88
|
-
|
88
|
+
node_output = render_node(token, context)
|
89
89
|
|
90
90
|
unless token.is_a?(Block) && token.blank?
|
91
|
-
output <<
|
91
|
+
output << node_output
|
92
92
|
end
|
93
93
|
rescue MemoryError => e
|
94
94
|
raise e
|
95
|
+
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
96
|
+
context.handle_error(e, token.line_number, token.raw)
|
97
|
+
output << nil
|
95
98
|
rescue ::StandardError => e
|
96
|
-
output << context.handle_error(e, token)
|
99
|
+
output << context.handle_error(e, token.line_number, token.raw)
|
97
100
|
end
|
98
101
|
end
|
99
102
|
|
@@ -102,22 +105,35 @@ module Liquid
|
|
102
105
|
|
103
106
|
private
|
104
107
|
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
def render_node(node, context)
|
109
|
+
node_output = (node.respond_to?(:render) ? node.render(context) : node)
|
110
|
+
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
111
|
+
|
112
|
+
context.resource_limits.render_length += node_output.length
|
113
|
+
if context.resource_limits.reached?
|
110
114
|
raise MemoryError.new("Memory limits exceeded".freeze)
|
111
115
|
end
|
112
|
-
|
116
|
+
node_output
|
113
117
|
end
|
114
118
|
|
115
|
-
def create_variable(token,
|
119
|
+
def create_variable(token, parse_context)
|
116
120
|
token.scan(ContentOfVariable) do |content|
|
117
|
-
markup =
|
118
|
-
return Variable.new(markup,
|
121
|
+
markup = content.first
|
122
|
+
return Variable.new(markup, parse_context)
|
119
123
|
end
|
120
|
-
|
124
|
+
raise_missing_variable_terminator(token, parse_context)
|
125
|
+
end
|
126
|
+
|
127
|
+
def raise_missing_tag_terminator(token, parse_context)
|
128
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
129
|
+
end
|
130
|
+
|
131
|
+
def raise_missing_variable_terminator(token, parse_context)
|
132
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
133
|
+
end
|
134
|
+
|
135
|
+
def registered_tags
|
136
|
+
Template.tags
|
121
137
|
end
|
122
138
|
end
|
123
139
|
end
|
data/lib/liquid/condition.rb
CHANGED
@@ -3,21 +3,26 @@ module Liquid
|
|
3
3
|
#
|
4
4
|
# Example:
|
5
5
|
#
|
6
|
-
# c = Condition.new(
|
6
|
+
# c = Condition.new(1, '==', 1)
|
7
7
|
# c.evaluate #=> true
|
8
8
|
#
|
9
9
|
class Condition #:nodoc:
|
10
10
|
@@operators = {
|
11
|
-
'=='.freeze =>
|
12
|
-
'!='.freeze =>
|
13
|
-
'<>'.freeze =>
|
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
14
|
'<'.freeze => :<,
|
15
15
|
'>'.freeze => :>,
|
16
16
|
'>='.freeze => :>=,
|
17
17
|
'<='.freeze => :<=,
|
18
|
-
'contains'.freeze => lambda
|
19
|
-
left && right && left.respond_to?(:include?)
|
20
|
-
|
18
|
+
'contains'.freeze => lambda do |cond, left, right|
|
19
|
+
if left && right && left.respond_to?(:include?)
|
20
|
+
right = right.to_s if left.is_a?(String)
|
21
|
+
left.include?(right)
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
21
26
|
}
|
22
27
|
|
23
28
|
def self.operators
|
@@ -73,17 +78,17 @@ module Liquid
|
|
73
78
|
private
|
74
79
|
|
75
80
|
def equal_variables(left, right)
|
76
|
-
if left.is_a?(
|
77
|
-
if right.respond_to?(left)
|
78
|
-
return right.send(left.
|
81
|
+
if left.is_a?(Liquid::Expression::MethodLiteral)
|
82
|
+
if right.respond_to?(left.method_name)
|
83
|
+
return right.send(left.method_name)
|
79
84
|
else
|
80
85
|
return nil
|
81
86
|
end
|
82
87
|
end
|
83
88
|
|
84
|
-
if right.is_a?(
|
85
|
-
if left.respond_to?(right)
|
86
|
-
return left.send(right.
|
89
|
+
if right.is_a?(Liquid::Expression::MethodLiteral)
|
90
|
+
if left.respond_to?(right.method_name)
|
91
|
+
return left.send(right.method_name)
|
87
92
|
else
|
88
93
|
return nil
|
89
94
|
end
|
@@ -96,36 +101,32 @@ module Liquid
|
|
96
101
|
# If the operator is empty this means that the decision statement is just
|
97
102
|
# a single variable. We can just poll this variable from the context and
|
98
103
|
# return this as the result.
|
99
|
-
return context
|
104
|
+
return context.evaluate(left) if op.nil?
|
100
105
|
|
101
|
-
left = context
|
102
|
-
right = context
|
106
|
+
left = context.evaluate(left)
|
107
|
+
right = context.evaluate(right)
|
103
108
|
|
104
109
|
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
105
110
|
|
106
111
|
if operation.respond_to?(:call)
|
107
112
|
operation.call(self, left, right)
|
108
|
-
elsif left.respond_to?(operation)
|
113
|
+
elsif left.respond_to?(operation) && right.respond_to?(operation)
|
109
114
|
begin
|
110
115
|
left.send(operation, right)
|
111
116
|
rescue ::ArgumentError => e
|
112
117
|
raise Liquid::ArgumentError.new(e.message)
|
113
118
|
end
|
114
|
-
else
|
115
|
-
nil
|
116
119
|
end
|
117
120
|
end
|
118
121
|
end
|
119
122
|
|
120
|
-
|
121
123
|
class ElseCondition < Condition
|
122
124
|
def else?
|
123
125
|
true
|
124
126
|
end
|
125
127
|
|
126
|
-
def evaluate(
|
128
|
+
def evaluate(_context)
|
127
129
|
true
|
128
130
|
end
|
129
131
|
end
|
130
|
-
|
131
132
|
end
|
data/lib/liquid/context.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Liquid
|
2
|
-
|
3
2
|
# Context keeps the variable stack and resolves variables, as well as keywords
|
4
3
|
#
|
5
4
|
# context['variable'] = 'testing'
|
@@ -14,41 +13,32 @@ module Liquid
|
|
14
13
|
# context['bob'] #=> nil class Context
|
15
14
|
class Context
|
16
15
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
17
|
-
attr_accessor :
|
16
|
+
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
18
17
|
|
19
18
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
20
19
|
@environments = [environments].flatten
|
21
20
|
@scopes = [(outer_scope || {})]
|
22
21
|
@registers = registers
|
23
22
|
@errors = []
|
24
|
-
@
|
25
|
-
@
|
26
|
-
@resource_limits
|
27
|
-
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
|
23
|
+
@partial = false
|
24
|
+
@strict_variables = false
|
25
|
+
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
28
26
|
squash_instance_assigns_with_environments
|
29
27
|
|
30
28
|
@this_stack_used = false
|
31
29
|
|
30
|
+
self.exception_renderer = Template.default_exception_renderer
|
32
31
|
if rethrow_errors
|
33
|
-
self.
|
32
|
+
self.exception_renderer = ->(e) { raise }
|
34
33
|
end
|
35
34
|
|
36
35
|
@interrupts = []
|
37
36
|
@filters = []
|
37
|
+
@global_filter = nil
|
38
38
|
end
|
39
39
|
|
40
|
-
def
|
41
|
-
@
|
42
|
-
obj.length
|
43
|
-
else
|
44
|
-
1
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def resource_limits_reached?
|
49
|
-
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
|
50
|
-
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
|
51
|
-
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
|
40
|
+
def warnings
|
41
|
+
@warnings ||= []
|
52
42
|
end
|
53
43
|
|
54
44
|
def strainer
|
@@ -61,25 +51,16 @@ module Liquid
|
|
61
51
|
# for that
|
62
52
|
def add_filters(filters)
|
63
53
|
filters = [filters].flatten.compact
|
64
|
-
filters
|
65
|
-
|
66
|
-
|
67
|
-
end
|
54
|
+
@filters += filters
|
55
|
+
@strainer = nil
|
56
|
+
end
|
68
57
|
|
69
|
-
|
70
|
-
|
71
|
-
# cached class based API, which avoids busting the method cache.
|
72
|
-
if @strainer
|
73
|
-
filters.each do |f|
|
74
|
-
strainer.extend(f)
|
75
|
-
end
|
76
|
-
else
|
77
|
-
@filters.concat filters
|
78
|
-
end
|
58
|
+
def apply_global_filter(obj)
|
59
|
+
global_filter.nil? ? obj : global_filter.call(obj)
|
79
60
|
end
|
80
61
|
|
81
62
|
# are there any not handled interrupts?
|
82
|
-
def
|
63
|
+
def interrupt?
|
83
64
|
!@interrupts.empty?
|
84
65
|
end
|
85
66
|
|
@@ -93,15 +74,12 @@ module Liquid
|
|
93
74
|
@interrupts.pop
|
94
75
|
end
|
95
76
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
101
|
-
|
77
|
+
def handle_error(e, line_number = nil, raw_token = nil)
|
78
|
+
e = internal_error unless e.is_a?(Liquid::Error)
|
79
|
+
e.template_name ||= template_name
|
80
|
+
e.line_number ||= line_number
|
102
81
|
errors.push(e)
|
103
|
-
|
104
|
-
Liquid::Error.render(e)
|
82
|
+
exception_renderer.call(e).to_s
|
105
83
|
end
|
106
84
|
|
107
85
|
def invoke(method, *args)
|
@@ -109,7 +87,7 @@ module Liquid
|
|
109
87
|
end
|
110
88
|
|
111
89
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
112
|
-
def push(new_scope={})
|
90
|
+
def push(new_scope = {})
|
113
91
|
@scopes.unshift(new_scope)
|
114
92
|
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
|
115
93
|
end
|
@@ -133,7 +111,7 @@ module Liquid
|
|
133
111
|
# end
|
134
112
|
#
|
135
113
|
# context['var] #=> nil
|
136
|
-
def stack(new_scope=nil)
|
114
|
+
def stack(new_scope = nil)
|
137
115
|
old_stack_used = @this_stack_used
|
138
116
|
if new_scope
|
139
117
|
push(new_scope)
|
@@ -170,10 +148,10 @@ module Liquid
|
|
170
148
|
# Example:
|
171
149
|
# products == empty #=> products.empty?
|
172
150
|
def [](expression)
|
173
|
-
evaluate(
|
151
|
+
evaluate(Expression.parse(expression))
|
174
152
|
end
|
175
153
|
|
176
|
-
def
|
154
|
+
def key?(key)
|
177
155
|
self[key] != nil
|
178
156
|
end
|
179
157
|
|
@@ -183,10 +161,9 @@ module Liquid
|
|
183
161
|
|
184
162
|
# Fetches an object starting at the local scope and then moving up the hierachy
|
185
163
|
def find_variable(key)
|
186
|
-
|
187
164
|
# This was changed from find() to find_index() because this is a very hot
|
188
165
|
# path and find_index() is optimized in MRI to reduce object allocation
|
189
|
-
index = @scopes.find_index { |s| s.
|
166
|
+
index = @scopes.find_index { |s| s.key?(key) }
|
190
167
|
scope = @scopes[index] if index
|
191
168
|
|
192
169
|
variable = nil
|
@@ -201,17 +178,23 @@ module Liquid
|
|
201
178
|
end
|
202
179
|
end
|
203
180
|
|
204
|
-
scope
|
205
|
-
variable
|
181
|
+
scope ||= @environments.last || @scopes.last
|
182
|
+
variable ||= lookup_and_evaluate(scope, key)
|
206
183
|
|
207
184
|
variable = variable.to_liquid
|
208
185
|
variable.context = self if variable.respond_to?(:context=)
|
209
186
|
|
210
|
-
|
187
|
+
variable
|
211
188
|
end
|
212
189
|
|
213
190
|
def lookup_and_evaluate(obj, key)
|
214
|
-
if
|
191
|
+
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
|
192
|
+
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
193
|
+
end
|
194
|
+
|
195
|
+
value = obj[key]
|
196
|
+
|
197
|
+
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
215
198
|
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
216
199
|
else
|
217
200
|
value
|
@@ -219,15 +202,23 @@ module Liquid
|
|
219
202
|
end
|
220
203
|
|
221
204
|
private
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
205
|
+
|
206
|
+
def internal_error
|
207
|
+
# raise and catch to set backtrace and cause on exception
|
208
|
+
raise Liquid::InternalError, 'internal'
|
209
|
+
rescue Liquid::InternalError => exc
|
210
|
+
exc
|
211
|
+
end
|
212
|
+
|
213
|
+
def squash_instance_assigns_with_environments
|
214
|
+
@scopes.last.each_key do |k|
|
215
|
+
@environments.each do |env|
|
216
|
+
if env.key?(k)
|
217
|
+
scopes.last[k] = lookup_and_evaluate(env, k)
|
218
|
+
break
|
229
219
|
end
|
230
220
|
end
|
231
|
-
end
|
221
|
+
end
|
222
|
+
end # squash_instance_assigns_with_environments
|
232
223
|
end # Context
|
233
224
|
end # Liquid
|
data/lib/liquid/document.rb
CHANGED
@@ -1,17 +1,27 @@
|
|
1
1
|
module Liquid
|
2
|
-
class Document <
|
3
|
-
def self.parse(tokens,
|
4
|
-
|
5
|
-
|
2
|
+
class Document < BlockBody
|
3
|
+
def self.parse(tokens, parse_context)
|
4
|
+
doc = new
|
5
|
+
doc.parse(tokens, parse_context)
|
6
|
+
doc
|
6
7
|
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def parse(tokens, parse_context)
|
10
|
+
super do |end_tag_name, end_tag_params|
|
11
|
+
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
12
|
+
end
|
13
|
+
rescue SyntaxError => e
|
14
|
+
e.line_number ||= parse_context.line_number
|
15
|
+
raise
|
11
16
|
end
|
12
17
|
|
13
|
-
|
14
|
-
|
18
|
+
def unknown_tag(tag, parse_context)
|
19
|
+
case tag
|
20
|
+
when 'else'.freeze, 'end'.freeze
|
21
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
|
22
|
+
else
|
23
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
24
|
+
end
|
15
25
|
end
|
16
26
|
end
|
17
27
|
end
|
data/lib/liquid/drop.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
3
|
module Liquid
|
4
|
-
|
5
4
|
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
6
5
|
# Methods of drops are callable.
|
7
6
|
# The main use for liquid drops is to implement lazy loaded objects.
|
@@ -19,28 +18,27 @@ module Liquid
|
|
19
18
|
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
20
19
|
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
21
20
|
#
|
22
|
-
# Your drop can either implement the methods sans any parameters
|
23
|
-
# catch all.
|
21
|
+
# Your drop can either implement the methods sans any parameters
|
22
|
+
# or implement the liquid_method_missing(name) method which is a catch all.
|
24
23
|
class Drop
|
25
24
|
attr_writer :context
|
26
25
|
|
27
|
-
EMPTY_STRING = ''.freeze
|
28
|
-
|
29
26
|
# Catch all for the method
|
30
|
-
def
|
31
|
-
nil
|
27
|
+
def liquid_method_missing(method)
|
28
|
+
return nil unless @context && @context.strict_variables
|
29
|
+
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
32
30
|
end
|
33
31
|
|
34
32
|
# called by liquid to invoke a drop
|
35
33
|
def invoke_drop(method_or_key)
|
36
|
-
if
|
34
|
+
if self.class.invokable?(method_or_key)
|
37
35
|
send(method_or_key)
|
38
36
|
else
|
39
|
-
|
37
|
+
liquid_method_missing(method_or_key)
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
43
|
-
def
|
41
|
+
def key?(_name)
|
44
42
|
true
|
45
43
|
end
|
46
44
|
|
@@ -56,22 +54,25 @@ module Liquid
|
|
56
54
|
self.class.name
|
57
55
|
end
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
private
|
57
|
+
alias_method :[], :invoke_drop
|
62
58
|
|
63
59
|
# Check for method existence without invoking respond_to?, which creates symbols
|
64
60
|
def self.invokable?(method_name)
|
65
|
-
|
61
|
+
invokable_methods.include?(method_name.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.invokable_methods
|
65
|
+
@invokable_methods ||= begin
|
66
66
|
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
67
|
+
|
67
68
|
if include?(Enumerable)
|
68
69
|
blacklist += Enumerable.public_instance_methods
|
69
70
|
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
70
71
|
end
|
72
|
+
|
71
73
|
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
72
|
-
|
74
|
+
Set.new(whitelist.map(&:to_s))
|
73
75
|
end
|
74
|
-
@invokable_methods.include?(method_name.to_s)
|
75
76
|
end
|
76
77
|
end
|
77
78
|
end
|