liquid-render-tag 0.1.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 +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
|