liquid-4-0-2 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +235 -0
  3. data/LICENSE +20 -0
  4. data/README.md +108 -0
  5. data/lib/liquid.rb +80 -0
  6. data/lib/liquid/block.rb +77 -0
  7. data/lib/liquid/block_body.rb +142 -0
  8. data/lib/liquid/condition.rb +151 -0
  9. data/lib/liquid/context.rb +226 -0
  10. data/lib/liquid/document.rb +27 -0
  11. data/lib/liquid/drop.rb +78 -0
  12. data/lib/liquid/errors.rb +56 -0
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +74 -0
  15. data/lib/liquid/file_system.rb +73 -0
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +16 -0
  19. data/lib/liquid/lexer.rb +55 -0
  20. data/lib/liquid/locales/en.yml +26 -0
  21. data/lib/liquid/parse_context.rb +38 -0
  22. data/lib/liquid/parse_tree_visitor.rb +42 -0
  23. data/lib/liquid/parser.rb +90 -0
  24. data/lib/liquid/parser_switching.rb +31 -0
  25. data/lib/liquid/profiler.rb +158 -0
  26. data/lib/liquid/profiler/hooks.rb +23 -0
  27. data/lib/liquid/range_lookup.rb +37 -0
  28. data/lib/liquid/resource_limits.rb +23 -0
  29. data/lib/liquid/standardfilters.rb +485 -0
  30. data/lib/liquid/strainer.rb +66 -0
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +43 -0
  33. data/lib/liquid/tags/assign.rb +59 -0
  34. data/lib/liquid/tags/break.rb +18 -0
  35. data/lib/liquid/tags/capture.rb +38 -0
  36. data/lib/liquid/tags/case.rb +94 -0
  37. data/lib/liquid/tags/comment.rb +16 -0
  38. data/lib/liquid/tags/continue.rb +18 -0
  39. data/lib/liquid/tags/cycle.rb +65 -0
  40. data/lib/liquid/tags/decrement.rb +35 -0
  41. data/lib/liquid/tags/for.rb +203 -0
  42. data/lib/liquid/tags/if.rb +122 -0
  43. data/lib/liquid/tags/ifchanged.rb +18 -0
  44. data/lib/liquid/tags/include.rb +124 -0
  45. data/lib/liquid/tags/increment.rb +31 -0
  46. data/lib/liquid/tags/raw.rb +47 -0
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +30 -0
  49. data/lib/liquid/template.rb +254 -0
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/utils.rb +83 -0
  52. data/lib/liquid/variable.rb +148 -0
  53. data/lib/liquid/variable_lookup.rb +88 -0
  54. data/lib/liquid/version.rb +4 -0
  55. data/test/fixtures/en_locale.yml +9 -0
  56. data/test/integration/assign_test.rb +48 -0
  57. data/test/integration/blank_test.rb +106 -0
  58. data/test/integration/block_test.rb +12 -0
  59. data/test/integration/capture_test.rb +50 -0
  60. data/test/integration/context_test.rb +32 -0
  61. data/test/integration/document_test.rb +19 -0
  62. data/test/integration/drop_test.rb +273 -0
  63. data/test/integration/error_handling_test.rb +260 -0
  64. data/test/integration/filter_test.rb +178 -0
  65. data/test/integration/hash_ordering_test.rb +23 -0
  66. data/test/integration/output_test.rb +123 -0
  67. data/test/integration/parse_tree_visitor_test.rb +247 -0
  68. data/test/integration/parsing_quirks_test.rb +122 -0
  69. data/test/integration/render_profiling_test.rb +154 -0
  70. data/test/integration/security_test.rb +80 -0
  71. data/test/integration/standard_filter_test.rb +698 -0
  72. data/test/integration/tags/break_tag_test.rb +15 -0
  73. data/test/integration/tags/continue_tag_test.rb +15 -0
  74. data/test/integration/tags/for_tag_test.rb +410 -0
  75. data/test/integration/tags/if_else_tag_test.rb +188 -0
  76. data/test/integration/tags/include_tag_test.rb +245 -0
  77. data/test/integration/tags/increment_tag_test.rb +23 -0
  78. data/test/integration/tags/raw_tag_test.rb +31 -0
  79. data/test/integration/tags/standard_tag_test.rb +296 -0
  80. data/test/integration/tags/statements_test.rb +111 -0
  81. data/test/integration/tags/table_row_test.rb +64 -0
  82. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  83. data/test/integration/template_test.rb +332 -0
  84. data/test/integration/trim_mode_test.rb +529 -0
  85. data/test/integration/variable_test.rb +96 -0
  86. data/test/test_helper.rb +116 -0
  87. data/test/unit/block_unit_test.rb +58 -0
  88. data/test/unit/condition_unit_test.rb +166 -0
  89. data/test/unit/context_unit_test.rb +489 -0
  90. data/test/unit/file_system_unit_test.rb +35 -0
  91. data/test/unit/i18n_unit_test.rb +37 -0
  92. data/test/unit/lexer_unit_test.rb +51 -0
  93. data/test/unit/parser_unit_test.rb +82 -0
  94. data/test/unit/regexp_unit_test.rb +44 -0
  95. data/test/unit/strainer_unit_test.rb +164 -0
  96. data/test/unit/tag_unit_test.rb +21 -0
  97. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  98. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  99. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  100. data/test/unit/template_unit_test.rb +78 -0
  101. data/test/unit/tokenizer_unit_test.rb +55 -0
  102. data/test/unit/variable_unit_test.rb +162 -0
  103. metadata +224 -0
@@ -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