liquid-4-0-2 4.0.2
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 +7 -0
- data/History.md +235 -0
- data/LICENSE +20 -0
- data/README.md +108 -0
- data/lib/liquid.rb +80 -0
- data/lib/liquid/block.rb +77 -0
- data/lib/liquid/block_body.rb +142 -0
- data/lib/liquid/condition.rb +151 -0
- data/lib/liquid/context.rb +226 -0
- data/lib/liquid/document.rb +27 -0
- data/lib/liquid/drop.rb +78 -0
- data/lib/liquid/errors.rb +56 -0
- data/lib/liquid/expression.rb +49 -0
- data/lib/liquid/extensions.rb +74 -0
- data/lib/liquid/file_system.rb +73 -0
- data/lib/liquid/forloop_drop.rb +42 -0
- data/lib/liquid/i18n.rb +39 -0
- data/lib/liquid/interrupts.rb +16 -0
- data/lib/liquid/lexer.rb +55 -0
- data/lib/liquid/locales/en.yml +26 -0
- data/lib/liquid/parse_context.rb +38 -0
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +90 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler.rb +158 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/range_lookup.rb +37 -0
- data/lib/liquid/resource_limits.rb +23 -0
- data/lib/liquid/standardfilters.rb +485 -0
- data/lib/liquid/strainer.rb +66 -0
- data/lib/liquid/tablerowloop_drop.rb +62 -0
- data/lib/liquid/tag.rb +43 -0
- data/lib/liquid/tags/assign.rb +59 -0
- data/lib/liquid/tags/break.rb +18 -0
- data/lib/liquid/tags/capture.rb +38 -0
- data/lib/liquid/tags/case.rb +94 -0
- data/lib/liquid/tags/comment.rb +16 -0
- data/lib/liquid/tags/continue.rb +18 -0
- data/lib/liquid/tags/cycle.rb +65 -0
- data/lib/liquid/tags/decrement.rb +35 -0
- data/lib/liquid/tags/for.rb +203 -0
- data/lib/liquid/tags/if.rb +122 -0
- data/lib/liquid/tags/ifchanged.rb +18 -0
- data/lib/liquid/tags/include.rb +124 -0
- data/lib/liquid/tags/increment.rb +31 -0
- data/lib/liquid/tags/raw.rb +47 -0
- data/lib/liquid/tags/table_row.rb +62 -0
- data/lib/liquid/tags/unless.rb +30 -0
- data/lib/liquid/template.rb +254 -0
- data/lib/liquid/tokenizer.rb +31 -0
- data/lib/liquid/utils.rb +83 -0
- data/lib/liquid/variable.rb +148 -0
- data/lib/liquid/variable_lookup.rb +88 -0
- data/lib/liquid/version.rb +4 -0
- data/test/fixtures/en_locale.yml +9 -0
- data/test/integration/assign_test.rb +48 -0
- data/test/integration/blank_test.rb +106 -0
- data/test/integration/block_test.rb +12 -0
- data/test/integration/capture_test.rb +50 -0
- data/test/integration/context_test.rb +32 -0
- data/test/integration/document_test.rb +19 -0
- data/test/integration/drop_test.rb +273 -0
- data/test/integration/error_handling_test.rb +260 -0
- data/test/integration/filter_test.rb +178 -0
- data/test/integration/hash_ordering_test.rb +23 -0
- data/test/integration/output_test.rb +123 -0
- data/test/integration/parse_tree_visitor_test.rb +247 -0
- data/test/integration/parsing_quirks_test.rb +122 -0
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/integration/security_test.rb +80 -0
- data/test/integration/standard_filter_test.rb +698 -0
- data/test/integration/tags/break_tag_test.rb +15 -0
- data/test/integration/tags/continue_tag_test.rb +15 -0
- data/test/integration/tags/for_tag_test.rb +410 -0
- data/test/integration/tags/if_else_tag_test.rb +188 -0
- data/test/integration/tags/include_tag_test.rb +245 -0
- data/test/integration/tags/increment_tag_test.rb +23 -0
- data/test/integration/tags/raw_tag_test.rb +31 -0
- data/test/integration/tags/standard_tag_test.rb +296 -0
- data/test/integration/tags/statements_test.rb +111 -0
- data/test/integration/tags/table_row_test.rb +64 -0
- data/test/integration/tags/unless_else_tag_test.rb +26 -0
- data/test/integration/template_test.rb +332 -0
- data/test/integration/trim_mode_test.rb +529 -0
- data/test/integration/variable_test.rb +96 -0
- data/test/test_helper.rb +116 -0
- data/test/unit/block_unit_test.rb +58 -0
- data/test/unit/condition_unit_test.rb +166 -0
- data/test/unit/context_unit_test.rb +489 -0
- data/test/unit/file_system_unit_test.rb +35 -0
- data/test/unit/i18n_unit_test.rb +37 -0
- data/test/unit/lexer_unit_test.rb +51 -0
- data/test/unit/parser_unit_test.rb +82 -0
- data/test/unit/regexp_unit_test.rb +44 -0
- data/test/unit/strainer_unit_test.rb +164 -0
- data/test/unit/tag_unit_test.rb +21 -0
- data/test/unit/tags/case_tag_unit_test.rb +10 -0
- data/test/unit/tags/for_tag_unit_test.rb +13 -0
- data/test/unit/tags/if_tag_unit_test.rb +8 -0
- data/test/unit/template_unit_test.rb +78 -0
- data/test/unit/tokenizer_unit_test.rb +55 -0
- data/test/unit/variable_unit_test.rb +162 -0
- metadata +224 -0
data/lib/liquid/block.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Block < Tag
|
3
|
+
MAX_DEPTH = 100
|
4
|
+
|
5
|
+
def initialize(tag_name, markup, options)
|
6
|
+
super
|
7
|
+
@blank = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse(tokens)
|
11
|
+
@body = BlockBody.new
|
12
|
+
while parse_body(@body, tokens)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def render(context)
|
17
|
+
@body.render(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def blank?
|
21
|
+
@blank
|
22
|
+
end
|
23
|
+
|
24
|
+
def nodelist
|
25
|
+
@body.nodelist
|
26
|
+
end
|
27
|
+
|
28
|
+
def unknown_tag(tag, _params, _tokens)
|
29
|
+
if tag == 'else'.freeze
|
30
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
31
|
+
block_name: block_name))
|
32
|
+
elsif tag.start_with?('end'.freeze)
|
33
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
34
|
+
tag: tag,
|
35
|
+
block_name: block_name,
|
36
|
+
block_delimiter: block_delimiter))
|
37
|
+
else
|
38
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def block_name
|
43
|
+
@tag_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def block_delimiter
|
47
|
+
@block_delimiter ||= "end#{block_name}"
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def parse_body(body, tokens)
|
53
|
+
if parse_context.depth >= MAX_DEPTH
|
54
|
+
raise StackLevelError, "Nesting too deep".freeze
|
55
|
+
end
|
56
|
+
parse_context.depth += 1
|
57
|
+
begin
|
58
|
+
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
59
|
+
@blank &&= body.blank?
|
60
|
+
|
61
|
+
return false if end_tag_name == block_delimiter
|
62
|
+
unless end_tag_name
|
63
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
64
|
+
end
|
65
|
+
|
66
|
+
# this tag is not registered with the system
|
67
|
+
# pass it to the current block for special handling or error reporting
|
68
|
+
unknown_tag(end_tag_name, end_tag_params, tokens)
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
parse_context.depth -= 1
|
72
|
+
end
|
73
|
+
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Liquid
|
2
|
+
class BlockBody
|
3
|
+
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
4
|
+
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
5
|
+
WhitespaceOrNothing = /\A\s*\z/
|
6
|
+
TAGSTART = "{%".freeze
|
7
|
+
VARSTART = "{{".freeze
|
8
|
+
|
9
|
+
attr_reader :nodelist
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@nodelist = []
|
13
|
+
@blank = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(tokenizer, parse_context)
|
17
|
+
parse_context.line_number = tokenizer.line_number
|
18
|
+
while token = tokenizer.shift
|
19
|
+
next if token.empty?
|
20
|
+
case
|
21
|
+
when token.start_with?(TAGSTART)
|
22
|
+
whitespace_handler(token, parse_context)
|
23
|
+
unless token =~ FullToken
|
24
|
+
raise_missing_tag_terminator(token, parse_context)
|
25
|
+
end
|
26
|
+
tag_name = $1
|
27
|
+
markup = $2
|
28
|
+
# fetch the tag from registered blocks
|
29
|
+
unless tag = registered_tags[tag_name]
|
30
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
31
|
+
# determine how to proceed
|
32
|
+
return yield tag_name, markup
|
33
|
+
end
|
34
|
+
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
35
|
+
@blank &&= new_tag.blank?
|
36
|
+
@nodelist << new_tag
|
37
|
+
when token.start_with?(VARSTART)
|
38
|
+
whitespace_handler(token, parse_context)
|
39
|
+
@nodelist << create_variable(token, parse_context)
|
40
|
+
@blank = false
|
41
|
+
else
|
42
|
+
if parse_context.trim_whitespace
|
43
|
+
token.lstrip!
|
44
|
+
end
|
45
|
+
parse_context.trim_whitespace = false
|
46
|
+
@nodelist << token
|
47
|
+
@blank &&= !!(token =~ WhitespaceOrNothing)
|
48
|
+
end
|
49
|
+
parse_context.line_number = tokenizer.line_number
|
50
|
+
end
|
51
|
+
|
52
|
+
yield nil, nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def whitespace_handler(token, parse_context)
|
56
|
+
if token[2] == WhitespaceControl
|
57
|
+
previous_token = @nodelist.last
|
58
|
+
if previous_token.is_a? String
|
59
|
+
previous_token.rstrip!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
63
|
+
end
|
64
|
+
|
65
|
+
def blank?
|
66
|
+
@blank
|
67
|
+
end
|
68
|
+
|
69
|
+
def render(context)
|
70
|
+
output = []
|
71
|
+
context.resource_limits.render_score += @nodelist.length
|
72
|
+
|
73
|
+
idx = 0
|
74
|
+
while node = @nodelist[idx]
|
75
|
+
case node
|
76
|
+
when String
|
77
|
+
check_resources(context, node)
|
78
|
+
output << node
|
79
|
+
when Variable
|
80
|
+
render_node_to_output(node, output, context)
|
81
|
+
when Block
|
82
|
+
render_node_to_output(node, output, context, node.blank?)
|
83
|
+
break if context.interrupt? # might have happened in a for-block
|
84
|
+
when Continue, Break
|
85
|
+
# If we get an Interrupt that means the block must stop processing. An
|
86
|
+
# Interrupt is any command that stops block execution such as {% break %}
|
87
|
+
# or {% continue %}
|
88
|
+
context.push_interrupt(node.interrupt)
|
89
|
+
break
|
90
|
+
else # Other non-Block tags
|
91
|
+
render_node_to_output(node, output, context)
|
92
|
+
end
|
93
|
+
idx += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
output.join
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def render_node_to_output(node, output, context, skip_output = false)
|
102
|
+
node_output = node.render(context)
|
103
|
+
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
104
|
+
check_resources(context, node_output)
|
105
|
+
output << node_output unless skip_output
|
106
|
+
rescue MemoryError => e
|
107
|
+
raise e
|
108
|
+
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
109
|
+
context.handle_error(e, node.line_number)
|
110
|
+
output << nil
|
111
|
+
rescue ::StandardError => e
|
112
|
+
line_number = node.is_a?(String) ? nil : node.line_number
|
113
|
+
output << context.handle_error(e, line_number)
|
114
|
+
end
|
115
|
+
|
116
|
+
def check_resources(context, node_output)
|
117
|
+
context.resource_limits.render_length += node_output.length
|
118
|
+
return unless context.resource_limits.reached?
|
119
|
+
raise MemoryError.new("Memory limits exceeded".freeze)
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_variable(token, parse_context)
|
123
|
+
token.scan(ContentOfVariable) do |content|
|
124
|
+
markup = content.first
|
125
|
+
return Variable.new(markup, parse_context)
|
126
|
+
end
|
127
|
+
raise_missing_variable_terminator(token, parse_context)
|
128
|
+
end
|
129
|
+
|
130
|
+
def raise_missing_tag_terminator(token, parse_context)
|
131
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
132
|
+
end
|
133
|
+
|
134
|
+
def raise_missing_variable_terminator(token, parse_context)
|
135
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
136
|
+
end
|
137
|
+
|
138
|
+
def registered_tags
|
139
|
+
Template.tags
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Container for liquid nodes which conveniently wraps decision making logic
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
#
|
6
|
+
# c = Condition.new(1, '==', 1)
|
7
|
+
# c.evaluate #=> true
|
8
|
+
#
|
9
|
+
class Condition #:nodoc:
|
10
|
+
@@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|
|
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
|
26
|
+
}
|
27
|
+
|
28
|
+
def self.operators
|
29
|
+
@@operators
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :attachment, :child_condition
|
33
|
+
attr_accessor :left, :operator, :right
|
34
|
+
|
35
|
+
def initialize(left = nil, operator = nil, right = nil)
|
36
|
+
@left = left
|
37
|
+
@operator = operator
|
38
|
+
@right = right
|
39
|
+
@child_relation = nil
|
40
|
+
@child_condition = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def evaluate(context = Context.new)
|
44
|
+
condition = self
|
45
|
+
result = nil
|
46
|
+
loop do
|
47
|
+
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
48
|
+
|
49
|
+
case condition.child_relation
|
50
|
+
when :or
|
51
|
+
break if result
|
52
|
+
when :and
|
53
|
+
break unless result
|
54
|
+
else
|
55
|
+
break
|
56
|
+
end
|
57
|
+
condition = condition.child_condition
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def or(condition)
|
63
|
+
@child_relation = :or
|
64
|
+
@child_condition = condition
|
65
|
+
end
|
66
|
+
|
67
|
+
def and(condition)
|
68
|
+
@child_relation = :and
|
69
|
+
@child_condition = condition
|
70
|
+
end
|
71
|
+
|
72
|
+
def attach(attachment)
|
73
|
+
@attachment = attachment
|
74
|
+
end
|
75
|
+
|
76
|
+
def else?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
attr_reader :child_relation
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def equal_variables(left, right)
|
91
|
+
if left.is_a?(Liquid::Expression::MethodLiteral)
|
92
|
+
if right.respond_to?(left.method_name)
|
93
|
+
return right.send(left.method_name)
|
94
|
+
else
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if right.is_a?(Liquid::Expression::MethodLiteral)
|
100
|
+
if left.respond_to?(right.method_name)
|
101
|
+
return left.send(right.method_name)
|
102
|
+
else
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
left == right
|
108
|
+
end
|
109
|
+
|
110
|
+
def interpret_condition(left, right, op, context)
|
111
|
+
# If the operator is empty this means that the decision statement is just
|
112
|
+
# a single variable. We can just poll this variable from the context and
|
113
|
+
# return this as the result.
|
114
|
+
return context.evaluate(left) if op.nil?
|
115
|
+
|
116
|
+
left = context.evaluate(left)
|
117
|
+
right = context.evaluate(right)
|
118
|
+
|
119
|
+
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
120
|
+
|
121
|
+
if operation.respond_to?(:call)
|
122
|
+
operation.call(self, left, right)
|
123
|
+
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
124
|
+
begin
|
125
|
+
left.send(operation, right)
|
126
|
+
rescue ::ArgumentError => e
|
127
|
+
raise Liquid::ArgumentError.new(e.message)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
133
|
+
def children
|
134
|
+
[
|
135
|
+
@node.left, @node.right,
|
136
|
+
@node.child_condition, @node.attachment
|
137
|
+
].compact
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class ElseCondition < Condition
|
143
|
+
def else?
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
def evaluate(_context)
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Context keeps the variable stack and resolves variables, as well as keywords
|
3
|
+
#
|
4
|
+
# context['variable'] = 'testing'
|
5
|
+
# context['variable'] #=> 'testing'
|
6
|
+
# context['true'] #=> true
|
7
|
+
# context['10.2232'] #=> 10.2232
|
8
|
+
#
|
9
|
+
# context.stack do
|
10
|
+
# context['bob'] = 'bobsen'
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# context['bob'] #=> nil class Context
|
14
|
+
class Context
|
15
|
+
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
16
|
+
attr_accessor :exception_renderer, :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
|
27
|
+
|
28
|
+
@this_stack_used = false
|
29
|
+
|
30
|
+
self.exception_renderer = Template.default_exception_renderer
|
31
|
+
if rethrow_errors
|
32
|
+
self.exception_renderer = ->(e) { raise }
|
33
|
+
end
|
34
|
+
|
35
|
+
@interrupts = []
|
36
|
+
@filters = []
|
37
|
+
@global_filter = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def warnings
|
41
|
+
@warnings ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
def strainer
|
45
|
+
@strainer ||= Strainer.create(self, @filters)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adds filters to this context.
|
49
|
+
#
|
50
|
+
# Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
|
51
|
+
# for that
|
52
|
+
def add_filters(filters)
|
53
|
+
filters = [filters].flatten.compact
|
54
|
+
@filters += filters
|
55
|
+
@strainer = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def apply_global_filter(obj)
|
59
|
+
global_filter.nil? ? obj : global_filter.call(obj)
|
60
|
+
end
|
61
|
+
|
62
|
+
# are there any not handled interrupts?
|
63
|
+
def interrupt?
|
64
|
+
!@interrupts.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
# push an interrupt to the stack. this interrupt is considered not handled.
|
68
|
+
def push_interrupt(e)
|
69
|
+
@interrupts.push(e)
|
70
|
+
end
|
71
|
+
|
72
|
+
# pop an interrupt from the stack
|
73
|
+
def pop_interrupt
|
74
|
+
@interrupts.pop
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_error(e, line_number = nil)
|
78
|
+
e = internal_error unless e.is_a?(Liquid::Error)
|
79
|
+
e.template_name ||= template_name
|
80
|
+
e.line_number ||= line_number
|
81
|
+
errors.push(e)
|
82
|
+
exception_renderer.call(e).to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
def invoke(method, *args)
|
86
|
+
strainer.invoke(method, *args).to_liquid
|
87
|
+
end
|
88
|
+
|
89
|
+
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
90
|
+
def push(new_scope = {})
|
91
|
+
@scopes.unshift(new_scope)
|
92
|
+
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
|
93
|
+
end
|
94
|
+
|
95
|
+
# Merge a hash of variables in the current local scope
|
96
|
+
def merge(new_scopes)
|
97
|
+
@scopes[0].merge!(new_scopes)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Pop from the stack. use <tt>Context#stack</tt> instead
|
101
|
+
def pop
|
102
|
+
raise ContextError if @scopes.size == 1
|
103
|
+
@scopes.shift
|
104
|
+
end
|
105
|
+
|
106
|
+
# Pushes a new local scope on the stack, pops it at the end of the block
|
107
|
+
#
|
108
|
+
# Example:
|
109
|
+
# context.stack do
|
110
|
+
# context['var'] = 'hi'
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# context['var] #=> nil
|
114
|
+
def stack(new_scope = nil)
|
115
|
+
old_stack_used = @this_stack_used
|
116
|
+
if new_scope
|
117
|
+
push(new_scope)
|
118
|
+
@this_stack_used = true
|
119
|
+
else
|
120
|
+
@this_stack_used = false
|
121
|
+
end
|
122
|
+
|
123
|
+
yield
|
124
|
+
ensure
|
125
|
+
pop if @this_stack_used
|
126
|
+
@this_stack_used = old_stack_used
|
127
|
+
end
|
128
|
+
|
129
|
+
def clear_instance_assigns
|
130
|
+
@scopes[0] = {}
|
131
|
+
end
|
132
|
+
|
133
|
+
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
134
|
+
def []=(key, value)
|
135
|
+
unless @this_stack_used
|
136
|
+
@this_stack_used = true
|
137
|
+
push({})
|
138
|
+
end
|
139
|
+
@scopes[0][key] = value
|
140
|
+
end
|
141
|
+
|
142
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
143
|
+
# Strings, digits, floats and booleans (true,false).
|
144
|
+
# If no match is made we lookup the variable in the current scope and
|
145
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
146
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
147
|
+
#
|
148
|
+
# Example:
|
149
|
+
# products == empty #=> products.empty?
|
150
|
+
def [](expression)
|
151
|
+
evaluate(Expression.parse(expression))
|
152
|
+
end
|
153
|
+
|
154
|
+
def key?(key)
|
155
|
+
self[key] != nil
|
156
|
+
end
|
157
|
+
|
158
|
+
def evaluate(object)
|
159
|
+
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
160
|
+
end
|
161
|
+
|
162
|
+
# Fetches an object starting at the local scope and then moving up the hierachy
|
163
|
+
def find_variable(key, raise_on_not_found: true)
|
164
|
+
# This was changed from find() to find_index() because this is a very hot
|
165
|
+
# path and find_index() is optimized in MRI to reduce object allocation
|
166
|
+
index = @scopes.find_index { |s| s.key?(key) }
|
167
|
+
scope = @scopes[index] if index
|
168
|
+
|
169
|
+
variable = nil
|
170
|
+
|
171
|
+
if scope.nil?
|
172
|
+
@environments.each do |e|
|
173
|
+
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
|
174
|
+
# When lookup returned a value OR there is no value but the lookup also did not raise
|
175
|
+
# then it is the value we are looking for.
|
176
|
+
if !variable.nil? || @strict_variables && raise_on_not_found
|
177
|
+
scope = e
|
178
|
+
break
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
scope ||= @environments.last || @scopes.last
|
184
|
+
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
|
185
|
+
|
186
|
+
variable = variable.to_liquid
|
187
|
+
variable.context = self if variable.respond_to?(:context=)
|
188
|
+
|
189
|
+
variable
|
190
|
+
end
|
191
|
+
|
192
|
+
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
193
|
+
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
194
|
+
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
195
|
+
end
|
196
|
+
|
197
|
+
value = obj[key]
|
198
|
+
|
199
|
+
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
200
|
+
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
201
|
+
else
|
202
|
+
value
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def internal_error
|
209
|
+
# raise and catch to set backtrace and cause on exception
|
210
|
+
raise Liquid::InternalError, 'internal'
|
211
|
+
rescue Liquid::InternalError => exc
|
212
|
+
exc
|
213
|
+
end
|
214
|
+
|
215
|
+
def squash_instance_assigns_with_environments
|
216
|
+
@scopes.last.each_key do |k|
|
217
|
+
@environments.each do |env|
|
218
|
+
if env.key?(k)
|
219
|
+
scopes.last[k] = lookup_and_evaluate(env, k)
|
220
|
+
break
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end # squash_instance_assigns_with_environments
|
225
|
+
end # Context
|
226
|
+
end # Liquid
|