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