liquid-render-tag 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +23 -0
- data/Rakefile +2 -0
- data/lib/liquid-render-tag.rb +20 -0
- data/lib/liquid-render-tag/block_body.rb +219 -0
- data/lib/liquid-render-tag/context.rb +261 -0
- data/lib/liquid-render-tag/partial_cache.rb +24 -0
- data/lib/liquid-render-tag/register.rb +6 -0
- data/lib/liquid-render-tag/registers/disabled_tags.rb +32 -0
- data/lib/liquid-render-tag/static_registers.rb +36 -0
- data/lib/liquid-render-tag/strainer_factory.rb +36 -0
- data/lib/liquid-render-tag/strainer_template.rb +53 -0
- data/lib/liquid-render-tag/tag.rb +73 -0
- data/lib/liquid-render-tag/tags/render.rb +84 -0
- data/lib/liquid-render-tag/template.rb +274 -0
- data/lib/liquid-render-tag/template_factory.rb +9 -0
- data/lib/liquid-render-tag/tokenizer.rb +39 -0
- data/lib/liquid-render-tag/variable.rb +170 -0
- data/lib/liquid-render-tag/version.rb +3 -0
- data/liquid-render-tag.gemspec +27 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8dba018c056179fd88c9210a4c0b310cd6dc90034be347b47e1c450e65b724c3
|
4
|
+
data.tar.gz: ce4dbcf7a26834241ce06402a69ddc9e5b2d3c12dbd202e9acc9dac2c13be373
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: edb9790d7028931a5b0511f117cd21e6e59a9aa8af5d126dede1dc0c4679523d8d0f52dd7e53d1bf1bfa270f55003d6a628083bb378258b766ee4bcfa5c3c383
|
7
|
+
data.tar.gz: e5e51c721402926535d167cb29345011e64419450b8cc4ca8196fdf980f260ca05093671f0f19c9f4d8a17084821a788da0867eb6ae2d10aafd569c5ed3673fa
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2005, 2006 Tobias Luetke
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Liquid::Render
|
2
|
+
|
3
|
+
This gem is simply a backport of code from the master branch of the [Liquid gem](https://github.com/shopify/liquid) to bring
|
4
|
+
support for the `render` tag to Liquid v4.0.3 (the current version released on
|
5
|
+
Rubygems).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
gem 'liquid'
|
12
|
+
gem 'liquid-render-tag'
|
13
|
+
```
|
14
|
+
|
15
|
+
Then require `liquid-render-tag` right after you require `liquid`.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
Information about the `render` tag can be found in [Shopify's Liquid Reference](https://shopify.dev/docs/themes/liquid/reference/tags/theme-tags#render).
|
20
|
+
|
21
|
+
## License
|
22
|
+
|
23
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "liquid"
|
2
|
+
|
3
|
+
p "Liquid!"
|
4
|
+
|
5
|
+
unless defined?(Liquid::Render)
|
6
|
+
tags = Liquid::Template.tags.instance_variable_get(:@cache)
|
7
|
+
|
8
|
+
original_verbosity = $VERBOSE
|
9
|
+
$VERBOSE = nil
|
10
|
+
Dir["#{__dir__}/liquid-render-tag/*.rb"].each { |f| require f }
|
11
|
+
Dir["#{__dir__}/liquid-render-tag/registers/*.rb"].each { |f| require f }
|
12
|
+
Dir["#{__dir__}/liquid-render-tag/tags/*.rb"].each { |f| require f }
|
13
|
+
$VERBOSE = original_verbosity
|
14
|
+
|
15
|
+
Liquid::Template.register_filter(Liquid::StandardFilters)
|
16
|
+
|
17
|
+
tags.each do |name, klass|
|
18
|
+
Liquid::Template.register_tag(name, klass)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
module Liquid
|
6
|
+
class BlockBody
|
7
|
+
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
8
|
+
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
9
|
+
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
10
|
+
WhitespaceOrNothing = /\A\s*\z/
|
11
|
+
TAGSTART = "{%"
|
12
|
+
VARSTART = "{{"
|
13
|
+
|
14
|
+
attr_reader :nodelist
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@nodelist = []
|
18
|
+
@blank = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(tokenizer, parse_context, &block)
|
22
|
+
parse_context.line_number = tokenizer.line_number
|
23
|
+
|
24
|
+
if tokenizer.for_liquid_tag
|
25
|
+
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
26
|
+
else
|
27
|
+
parse_for_document(tokenizer, parse_context, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private def parse_for_liquid_tag(tokenizer, parse_context)
|
32
|
+
while (token = tokenizer.shift)
|
33
|
+
unless token.empty? || token =~ WhitespaceOrNothing
|
34
|
+
unless token =~ LiquidTagToken
|
35
|
+
# line isn't empty but didn't match tag syntax, yield and let the
|
36
|
+
# caller raise a syntax error
|
37
|
+
return yield token, token
|
38
|
+
end
|
39
|
+
tag_name = Regexp.last_match(1)
|
40
|
+
markup = Regexp.last_match(2)
|
41
|
+
unless (tag = registered_tags[tag_name])
|
42
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
43
|
+
# determine how to proceed
|
44
|
+
return yield tag_name, markup
|
45
|
+
end
|
46
|
+
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
47
|
+
@blank &&= new_tag.blank?
|
48
|
+
@nodelist << new_tag
|
49
|
+
end
|
50
|
+
parse_context.line_number = tokenizer.line_number
|
51
|
+
end
|
52
|
+
|
53
|
+
yield nil, nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def self.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup)
|
58
|
+
yield end_tag_name, end_tag_markup
|
59
|
+
ensure
|
60
|
+
Usage.increment("liquid_tag_contains_outer_tag") unless $ERROR_INFO.is_a?(SyntaxError)
|
61
|
+
end
|
62
|
+
|
63
|
+
private def parse_liquid_tag(markup, parse_context, &block)
|
64
|
+
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
65
|
+
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, end_tag_markup|
|
66
|
+
next unless end_tag_name
|
67
|
+
self.class.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup, &block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private def parse_for_document(tokenizer, parse_context, &block)
|
72
|
+
while (token = tokenizer.shift)
|
73
|
+
next if token.empty?
|
74
|
+
case
|
75
|
+
when token.start_with?(TAGSTART)
|
76
|
+
whitespace_handler(token, parse_context)
|
77
|
+
unless token =~ FullToken
|
78
|
+
raise_missing_tag_terminator(token, parse_context)
|
79
|
+
end
|
80
|
+
tag_name = Regexp.last_match(2)
|
81
|
+
markup = Regexp.last_match(4)
|
82
|
+
|
83
|
+
if parse_context.line_number
|
84
|
+
# newlines inside the tag should increase the line number,
|
85
|
+
# particularly important for multiline {% liquid %} tags
|
86
|
+
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
if tag_name == 'liquid'
|
90
|
+
parse_liquid_tag(markup, parse_context, &block)
|
91
|
+
next
|
92
|
+
end
|
93
|
+
|
94
|
+
unless (tag = registered_tags[tag_name])
|
95
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
96
|
+
# determine how to proceed
|
97
|
+
return yield tag_name, markup
|
98
|
+
end
|
99
|
+
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
100
|
+
@blank &&= new_tag.blank?
|
101
|
+
@nodelist << new_tag
|
102
|
+
when token.start_with?(VARSTART)
|
103
|
+
whitespace_handler(token, parse_context)
|
104
|
+
@nodelist << create_variable(token, parse_context)
|
105
|
+
@blank = false
|
106
|
+
else
|
107
|
+
if parse_context.trim_whitespace
|
108
|
+
token.lstrip!
|
109
|
+
end
|
110
|
+
parse_context.trim_whitespace = false
|
111
|
+
@nodelist << token
|
112
|
+
@blank &&= !!(token =~ WhitespaceOrNothing)
|
113
|
+
end
|
114
|
+
parse_context.line_number = tokenizer.line_number
|
115
|
+
end
|
116
|
+
|
117
|
+
yield nil, nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def whitespace_handler(token, parse_context)
|
121
|
+
if token[2] == WhitespaceControl
|
122
|
+
previous_token = @nodelist.last
|
123
|
+
if previous_token.is_a?(String)
|
124
|
+
previous_token.rstrip!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
128
|
+
end
|
129
|
+
|
130
|
+
def blank?
|
131
|
+
@blank
|
132
|
+
end
|
133
|
+
|
134
|
+
def render(context)
|
135
|
+
render_to_output_buffer(context, +'')
|
136
|
+
end
|
137
|
+
|
138
|
+
def render_to_output_buffer(context, output)
|
139
|
+
context.resource_limits.render_score += @nodelist.length
|
140
|
+
|
141
|
+
idx = 0
|
142
|
+
while (node = @nodelist[idx])
|
143
|
+
previous_output_size = output.bytesize
|
144
|
+
|
145
|
+
case node
|
146
|
+
when String
|
147
|
+
output << node
|
148
|
+
when Variable
|
149
|
+
render_node(context, output, node)
|
150
|
+
when Block
|
151
|
+
render_node(context, node.blank? ? +'' : output, node)
|
152
|
+
break if context.interrupt? # might have happened in a for-block
|
153
|
+
when Continue, Break
|
154
|
+
# If we get an Interrupt that means the block must stop processing. An
|
155
|
+
# Interrupt is any command that stops block execution such as {% break %}
|
156
|
+
# or {% continue %}
|
157
|
+
context.push_interrupt(node.interrupt)
|
158
|
+
break
|
159
|
+
else # Other non-Block tags
|
160
|
+
render_node(context, output, node)
|
161
|
+
break if context.interrupt? # might have happened through an include
|
162
|
+
end
|
163
|
+
idx += 1
|
164
|
+
|
165
|
+
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
166
|
+
end
|
167
|
+
|
168
|
+
output
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def render_node(context, output, node)
|
174
|
+
if node.disabled?(context)
|
175
|
+
output << node.disabled_error_message
|
176
|
+
return
|
177
|
+
end
|
178
|
+
disable_tags(context, node.disabled_tags) do
|
179
|
+
node.render_to_output_buffer(context, output)
|
180
|
+
end
|
181
|
+
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
182
|
+
context.handle_error(e, node.line_number)
|
183
|
+
rescue ::StandardError => e
|
184
|
+
line_number = node.is_a?(String) ? nil : node.line_number
|
185
|
+
output << context.handle_error(e, line_number)
|
186
|
+
end
|
187
|
+
|
188
|
+
def disable_tags(context, tags, &block)
|
189
|
+
return yield if tags.empty?
|
190
|
+
context.registers[:disabled_tags].disable(tags, &block)
|
191
|
+
end
|
192
|
+
|
193
|
+
def raise_if_resource_limits_reached(context, length)
|
194
|
+
context.resource_limits.render_length += length
|
195
|
+
return unless context.resource_limits.reached?
|
196
|
+
raise MemoryError, "Memory limits exceeded"
|
197
|
+
end
|
198
|
+
|
199
|
+
def create_variable(token, parse_context)
|
200
|
+
token.scan(ContentOfVariable) do |content|
|
201
|
+
markup = content.first
|
202
|
+
return Variable.new(markup, parse_context)
|
203
|
+
end
|
204
|
+
raise_missing_variable_terminator(token, parse_context)
|
205
|
+
end
|
206
|
+
|
207
|
+
def raise_missing_tag_terminator(token, parse_context)
|
208
|
+
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
209
|
+
end
|
210
|
+
|
211
|
+
def raise_missing_variable_terminator(token, parse_context)
|
212
|
+
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
213
|
+
end
|
214
|
+
|
215
|
+
def registered_tags
|
216
|
+
Template.tags
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
# Context keeps the variable stack and resolves variables, as well as keywords
|
5
|
+
#
|
6
|
+
# context['variable'] = 'testing'
|
7
|
+
# context['variable'] #=> 'testing'
|
8
|
+
# context['true'] #=> true
|
9
|
+
# context['10.2232'] #=> 10.2232
|
10
|
+
#
|
11
|
+
# context.stack do
|
12
|
+
# context['bob'] = 'bobsen'
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# context['bob'] #=> nil class Context
|
16
|
+
class Context
|
17
|
+
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
18
|
+
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
19
|
+
|
20
|
+
# rubocop:disable Metrics/ParameterLists
|
21
|
+
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
|
22
|
+
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
26
|
+
@environments = [environments]
|
27
|
+
@environments.flatten!
|
28
|
+
|
29
|
+
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
30
|
+
@scopes = [(outer_scope || {})]
|
31
|
+
@registers = registers
|
32
|
+
@errors = []
|
33
|
+
@partial = false
|
34
|
+
@strict_variables = false
|
35
|
+
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
36
|
+
@base_scope_depth = 0
|
37
|
+
squash_instance_assigns_with_environments
|
38
|
+
|
39
|
+
self.exception_renderer = Template.default_exception_renderer
|
40
|
+
if rethrow_errors
|
41
|
+
self.exception_renderer = ->(_e) { raise }
|
42
|
+
end
|
43
|
+
|
44
|
+
@interrupts = []
|
45
|
+
@filters = []
|
46
|
+
@global_filter = nil
|
47
|
+
end
|
48
|
+
# rubocop:enable Metrics/ParameterLists
|
49
|
+
|
50
|
+
def warnings
|
51
|
+
@warnings ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def strainer
|
55
|
+
@strainer ||= StrainerFactory.create(self, @filters)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds filters to this context.
|
59
|
+
#
|
60
|
+
# Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
|
61
|
+
# for that
|
62
|
+
def add_filters(filters)
|
63
|
+
filters = [filters].flatten.compact
|
64
|
+
@filters += filters
|
65
|
+
@strainer = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_global_filter(obj)
|
69
|
+
global_filter.nil? ? obj : global_filter.call(obj)
|
70
|
+
end
|
71
|
+
|
72
|
+
# are there any not handled interrupts?
|
73
|
+
def interrupt?
|
74
|
+
!@interrupts.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# push an interrupt to the stack. this interrupt is considered not handled.
|
78
|
+
def push_interrupt(e)
|
79
|
+
@interrupts.push(e)
|
80
|
+
end
|
81
|
+
|
82
|
+
# pop an interrupt from the stack
|
83
|
+
def pop_interrupt
|
84
|
+
@interrupts.pop
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_error(e, line_number = nil)
|
88
|
+
e = internal_error unless e.is_a?(Liquid::Error)
|
89
|
+
e.template_name ||= template_name
|
90
|
+
e.line_number ||= line_number
|
91
|
+
errors.push(e)
|
92
|
+
exception_renderer.call(e).to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
def invoke(method, *args)
|
96
|
+
strainer.invoke(method, *args).to_liquid
|
97
|
+
end
|
98
|
+
|
99
|
+
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
100
|
+
def push(new_scope = {})
|
101
|
+
@scopes.unshift(new_scope)
|
102
|
+
check_overflow
|
103
|
+
end
|
104
|
+
|
105
|
+
# Merge a hash of variables in the current local scope
|
106
|
+
def merge(new_scopes)
|
107
|
+
@scopes[0].merge!(new_scopes)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Pop from the stack. use <tt>Context#stack</tt> instead
|
111
|
+
def pop
|
112
|
+
raise ContextError if @scopes.size == 1
|
113
|
+
@scopes.shift
|
114
|
+
end
|
115
|
+
|
116
|
+
# Pushes a new local scope on the stack, pops it at the end of the block
|
117
|
+
#
|
118
|
+
# Example:
|
119
|
+
# context.stack do
|
120
|
+
# context['var'] = 'hi'
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# context['var] #=> nil
|
124
|
+
def stack(new_scope = {})
|
125
|
+
push(new_scope)
|
126
|
+
yield
|
127
|
+
ensure
|
128
|
+
pop
|
129
|
+
end
|
130
|
+
|
131
|
+
# Creates a new context inheriting resource limits, filters, environment etc.,
|
132
|
+
# but with an isolated scope.
|
133
|
+
def new_isolated_subcontext
|
134
|
+
check_overflow
|
135
|
+
|
136
|
+
Context.build(
|
137
|
+
resource_limits: resource_limits,
|
138
|
+
static_environments: static_environments,
|
139
|
+
registers: StaticRegisters.new(registers)
|
140
|
+
).tap do |subcontext|
|
141
|
+
subcontext.base_scope_depth = base_scope_depth + 1
|
142
|
+
subcontext.exception_renderer = exception_renderer
|
143
|
+
subcontext.filters = @filters
|
144
|
+
subcontext.strainer = nil
|
145
|
+
subcontext.errors = errors
|
146
|
+
subcontext.warnings = warnings
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_instance_assigns
|
151
|
+
@scopes[0] = {}
|
152
|
+
end
|
153
|
+
|
154
|
+
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
155
|
+
def []=(key, value)
|
156
|
+
@scopes[0][key] = value
|
157
|
+
end
|
158
|
+
|
159
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
160
|
+
# Strings, digits, floats and booleans (true,false).
|
161
|
+
# If no match is made we lookup the variable in the current scope and
|
162
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
163
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
164
|
+
#
|
165
|
+
# Example:
|
166
|
+
# products == empty #=> products.empty?
|
167
|
+
def [](expression)
|
168
|
+
evaluate(Expression.parse(expression))
|
169
|
+
end
|
170
|
+
|
171
|
+
def key?(key)
|
172
|
+
self[key] != nil
|
173
|
+
end
|
174
|
+
|
175
|
+
def evaluate(object)
|
176
|
+
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
177
|
+
end
|
178
|
+
|
179
|
+
# Fetches an object starting at the local scope and then moving up the hierachy
|
180
|
+
def find_variable(key, raise_on_not_found: true)
|
181
|
+
# This was changed from find() to find_index() because this is a very hot
|
182
|
+
# path and find_index() is optimized in MRI to reduce object allocation
|
183
|
+
index = @scopes.find_index { |s| s.key?(key) }
|
184
|
+
|
185
|
+
variable = if index
|
186
|
+
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
187
|
+
else
|
188
|
+
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
189
|
+
end
|
190
|
+
|
191
|
+
variable = variable.to_liquid
|
192
|
+
variable.context = self if variable.respond_to?(:context=)
|
193
|
+
|
194
|
+
variable
|
195
|
+
end
|
196
|
+
|
197
|
+
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
198
|
+
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
199
|
+
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
200
|
+
end
|
201
|
+
|
202
|
+
value = obj[key]
|
203
|
+
|
204
|
+
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
205
|
+
obj[key] = value.arity == 0 ? value.call : value.call(self)
|
206
|
+
else
|
207
|
+
value
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
|
213
|
+
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
attr_reader :base_scope_depth
|
218
|
+
|
219
|
+
def try_variable_find_in_environments(key, raise_on_not_found:)
|
220
|
+
@environments.each do |environment|
|
221
|
+
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
222
|
+
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
223
|
+
return found_variable
|
224
|
+
end
|
225
|
+
end
|
226
|
+
@static_environments.each do |environment|
|
227
|
+
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
228
|
+
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
229
|
+
return found_variable
|
230
|
+
end
|
231
|
+
end
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def check_overflow
|
236
|
+
raise StackLevelError, "Nesting too deep" if overflow?
|
237
|
+
end
|
238
|
+
|
239
|
+
def overflow?
|
240
|
+
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
241
|
+
end
|
242
|
+
|
243
|
+
def internal_error
|
244
|
+
# raise and catch to set backtrace and cause on exception
|
245
|
+
raise Liquid::InternalError, 'internal'
|
246
|
+
rescue Liquid::InternalError => exc
|
247
|
+
exc
|
248
|
+
end
|
249
|
+
|
250
|
+
def squash_instance_assigns_with_environments
|
251
|
+
@scopes.last.each_key do |k|
|
252
|
+
@environments.each do |env|
|
253
|
+
if env.key?(k)
|
254
|
+
scopes.last[k] = lookup_and_evaluate(env, k)
|
255
|
+
break
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end # squash_instance_assigns_with_environments
|
260
|
+
end # Context
|
261
|
+
end # Liquid
|