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.
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