liquid-render-tag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in liquid-render-tag.gemspec
6
+ gemspec
@@ -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.
@@ -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).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -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