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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class PartialCache
5
+ def self.load(template_name, context:, parse_context:)
6
+ cached_partials = (context.registers[:cached_partials] ||= {})
7
+ cached = cached_partials[template_name]
8
+ return cached if cached
9
+
10
+ file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
11
+ source = file_system.read_template_file(template_name)
12
+
13
+ parse_context.partial = true
14
+
15
+ template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
16
+ template = template_factory.for(template_name)
17
+
18
+ partial = template.parse(source, parse_context)
19
+ cached_partials[template_name] = partial
20
+ ensure
21
+ parse_context.partial = false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Register
5
+ end
6
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module Liquid
3
+ class DisabledTags < Register
4
+ def initialize
5
+ @disabled_tags = {}
6
+ end
7
+
8
+ def disabled?(tag)
9
+ @disabled_tags.key?(tag) && @disabled_tags[tag] > 0
10
+ end
11
+
12
+ def disable(tags)
13
+ tags.each(&method(:increment))
14
+ yield
15
+ ensure
16
+ tags.each(&method(:decrement))
17
+ end
18
+
19
+ private
20
+
21
+ def increment(tag)
22
+ @disabled_tags[tag] ||= 0
23
+ @disabled_tags[tag] += 1
24
+ end
25
+
26
+ def decrement(tag)
27
+ @disabled_tags[tag] -= 1
28
+ end
29
+ end
30
+
31
+ Template.add_register(:disabled_tags, DisabledTags.new)
32
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class StaticRegisters
5
+ attr_reader :static, :registers
6
+
7
+ def initialize(registers = {})
8
+ @static = registers.is_a?(StaticRegisters) ? registers.static : registers
9
+ @registers = {}
10
+ end
11
+
12
+ def []=(key, value)
13
+ @registers[key] = value
14
+ end
15
+
16
+ def [](key)
17
+ if @registers.key?(key)
18
+ @registers[key]
19
+ else
20
+ @static[key]
21
+ end
22
+ end
23
+
24
+ def delete(key)
25
+ @registers.delete(key)
26
+ end
27
+
28
+ def fetch(key, default = nil)
29
+ key?(key) ? self[key] : default
30
+ end
31
+
32
+ def key?(key)
33
+ @registers.key?(key) || @static.key?(key)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # StrainerFactory is the factory for the filters system.
5
+ module StrainerFactory
6
+ extend self
7
+
8
+ def add_global_filter(filter)
9
+ strainer_class_cache.clear
10
+ global_filters << filter
11
+ end
12
+
13
+ def create(context, filters = [])
14
+ strainer_from_cache(filters).new(context)
15
+ end
16
+
17
+ private
18
+
19
+ def global_filters
20
+ @global_filters ||= []
21
+ end
22
+
23
+ def strainer_from_cache(filters)
24
+ strainer_class_cache[filters] ||= begin
25
+ klass = Class.new(StrainerTemplate)
26
+ global_filters.each { |f| klass.add_filter(f) }
27
+ filters.each { |f| klass.add_filter(f) }
28
+ klass
29
+ end
30
+ end
31
+
32
+ def strainer_class_cache
33
+ @strainer_class_cache ||= {}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Liquid
6
+ # StrainerTemplate is the computed class for the filters system.
7
+ # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
8
+ #
9
+ # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
10
+ # Context#add_filters or Template.register_filter
11
+ class StrainerTemplate
12
+ def initialize(context)
13
+ @context = context
14
+ end
15
+
16
+ class << self
17
+ def add_filter(filter)
18
+ return if include?(filter)
19
+
20
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
21
+ if invokable_non_public_methods.any?
22
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
23
+ end
24
+
25
+ include(filter)
26
+
27
+ filter_methods.merge(filter.public_instance_methods.map(&:to_s))
28
+ end
29
+
30
+ def invokable?(method)
31
+ filter_methods.include?(method.to_s)
32
+ end
33
+
34
+ private
35
+
36
+ def filter_methods
37
+ @filter_methods ||= Set.new
38
+ end
39
+ end
40
+
41
+ def invoke(method, *args)
42
+ if self.class.invokable?(method)
43
+ send(method, *args)
44
+ elsif @context.strict_filters
45
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
46
+ else
47
+ args.first
48
+ end
49
+ rescue ::ArgumentError => e
50
+ raise Liquid::ArgumentError, e.message, e.backtrace
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ attr_reader :nodelist, :tag_name, :line_number, :parse_context
6
+ alias_method :options, :parse_context
7
+ include ParserSwitching
8
+
9
+ class << self
10
+ def parse(tag_name, markup, tokenizer, parse_context)
11
+ tag = new(tag_name, markup, parse_context)
12
+ tag.parse(tokenizer)
13
+ tag
14
+ end
15
+
16
+ def disable_tags(*tags)
17
+ disabled_tags.push(*tags)
18
+ end
19
+
20
+ private :new
21
+
22
+ def disabled_tags
23
+ @disabled_tags ||= []
24
+ end
25
+ end
26
+
27
+ def initialize(tag_name, markup, parse_context)
28
+ @tag_name = tag_name
29
+ @markup = markup
30
+ @parse_context = parse_context
31
+ @line_number = parse_context.line_number
32
+ end
33
+
34
+ def parse(_tokens)
35
+ end
36
+
37
+ def raw
38
+ "#{@tag_name} #{@markup}"
39
+ end
40
+
41
+ def name
42
+ self.class.name.downcase
43
+ end
44
+
45
+ def render(_context)
46
+ ''
47
+ end
48
+
49
+ def disabled?(context)
50
+ context.registers[:disabled_tags].disabled?(tag_name)
51
+ end
52
+
53
+ def disabled_error_message
54
+ "#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
55
+ end
56
+
57
+ # For backwards compatibility with custom tags. In a future release, the semantics
58
+ # of the `render_to_output_buffer` method will become the default and the `render`
59
+ # method will be removed.
60
+ def render_to_output_buffer(context, output)
61
+ output << render(context)
62
+ output
63
+ end
64
+
65
+ def blank?
66
+ false
67
+ end
68
+
69
+ def disabled_tags
70
+ self.class.disabled_tags
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Render < Tag
5
+ FOR = 'for'
6
+ SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
7
+
8
+ disable_tags "include"
9
+
10
+ attr_reader :template_name_expr, :attributes
11
+
12
+ def initialize(tag_name, markup, options)
13
+ super
14
+
15
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
16
+
17
+ template_name = Regexp.last_match(1)
18
+ with_or_for = Regexp.last_match(3)
19
+ variable_name = Regexp.last_match(4)
20
+
21
+ @alias_name = Regexp.last_match(6)
22
+ @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
23
+ @template_name_expr = Expression.parse(template_name)
24
+ @for = (with_or_for == FOR)
25
+
26
+ @attributes = {}
27
+ markup.scan(TagAttributes) do |key, value|
28
+ @attributes[key] = Expression.parse(value)
29
+ end
30
+ end
31
+
32
+ def render_to_output_buffer(context, output)
33
+ render_tag(context, output)
34
+ end
35
+
36
+ def render_tag(context, output)
37
+ # Though we evaluate this here we will only ever parse it as a string literal.
38
+ template_name = context.evaluate(@template_name_expr)
39
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
40
+
41
+ partial = PartialCache.load(
42
+ template_name,
43
+ context: context,
44
+ parse_context: parse_context
45
+ )
46
+
47
+ context_variable_name = @alias_name || template_name.split('/').last
48
+
49
+ render_partial_func = ->(var, forloop) {
50
+ inner_context = context.new_isolated_subcontext
51
+ inner_context.template_name = template_name
52
+ inner_context.partial = true
53
+ inner_context['forloop'] = forloop if forloop
54
+
55
+ @attributes.each do |key, value|
56
+ inner_context[key] = context.evaluate(value)
57
+ end
58
+ inner_context[context_variable_name] = var unless var.nil?
59
+ partial.render_to_output_buffer(inner_context, output)
60
+ forloop&.send(:increment!)
61
+ }
62
+
63
+ variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
64
+ if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
65
+ forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
66
+ variable.each { |var| render_partial_func.call(var, forloop) }
67
+ else
68
+ render_partial_func.call(variable, nil)
69
+ end
70
+
71
+ output
72
+ end
73
+
74
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
75
+ def children
76
+ [
77
+ @node.template_name_expr,
78
+ ] + @node.attributes.values
79
+ end
80
+ end
81
+ end
82
+
83
+ Template.register_tag('render', Render)
84
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # Templates are central to liquid.
5
+ # Interpretating templates is a two step process. First you compile the
6
+ # source code you got. During compile time some extensive error checking is performed.
7
+ # your code should expect to get some SyntaxErrors.
8
+ #
9
+ # After you have a compiled template you can then <tt>render</tt> it.
10
+ # You can use a compiled template over and over again and keep it cached.
11
+ #
12
+ # Example:
13
+ #
14
+ # template = Liquid::Template.parse(source)
15
+ # template.render('user_name' => 'bob')
16
+ #
17
+ class Template
18
+ attr_accessor :root
19
+ attr_reader :resource_limits, :warnings
20
+
21
+ class TagRegistry
22
+ include Enumerable
23
+
24
+ def initialize
25
+ @tags = {}
26
+ @cache = {}
27
+ end
28
+
29
+ def [](tag_name)
30
+ return nil unless @tags.key?(tag_name)
31
+ return @cache[tag_name] if Liquid.cache_classes
32
+
33
+ lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
34
+ end
35
+
36
+ def []=(tag_name, klass)
37
+ @tags[tag_name] = klass.name
38
+ @cache[tag_name] = klass
39
+ end
40
+
41
+ def delete(tag_name)
42
+ @tags.delete(tag_name)
43
+ @cache.delete(tag_name)
44
+ end
45
+
46
+ def each(&block)
47
+ @tags.each(&block)
48
+ end
49
+
50
+ private
51
+
52
+ def lookup_class(name)
53
+ Object.const_get(name)
54
+ end
55
+ end
56
+
57
+ attr_reader :profiler
58
+
59
+ class << self
60
+ # Sets how strict the parser should be.
61
+ # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
62
+ # :warn is the default and will give deprecation warnings when invalid syntax is used.
63
+ # :strict will enforce correct syntax.
64
+ attr_accessor :error_mode
65
+ Template.error_mode = :lax
66
+
67
+ attr_reader :taint_mode
68
+
69
+ # Sets how strict the taint checker should be.
70
+ # :lax is the default, and ignores the taint flag completely
71
+ # :warn adds a warning, but does not interrupt the rendering
72
+ # :error raises an error when tainted output is used
73
+ # @deprecated Since it is being deprecated in ruby itself.
74
+ def taint_mode=(mode)
75
+ taint_supported = Object.new.taint.tainted?
76
+ if mode != :lax && !taint_supported
77
+ raise NotImplementedError, "#{RUBY_ENGINE} #{RUBY_VERSION} doesn't support taint checking"
78
+ end
79
+ @taint_mode = mode
80
+ end
81
+
82
+ Template.taint_mode = :lax
83
+
84
+ attr_accessor :default_exception_renderer
85
+ Template.default_exception_renderer = lambda do |exception|
86
+ exception
87
+ end
88
+
89
+ attr_accessor :file_system
90
+ Template.file_system = BlankFileSystem.new
91
+
92
+ attr_accessor :tags
93
+ Template.tags = TagRegistry.new
94
+ private :tags=
95
+
96
+ def register_tag(name, klass)
97
+ tags[name.to_s] = klass
98
+ end
99
+
100
+ attr_accessor :registers
101
+ Template.registers = {}
102
+ private :registers=
103
+
104
+ def add_register(name, klass)
105
+ registers[name.to_sym] = klass
106
+ end
107
+
108
+ # Pass a module with filter methods which should be available
109
+ # to all liquid views. Good for registering the standard library
110
+ def register_filter(mod)
111
+ StrainerFactory.add_global_filter(mod)
112
+ end
113
+
114
+ attr_accessor :default_resource_limits
115
+ Template.default_resource_limits = {}
116
+ private :default_resource_limits=
117
+
118
+ # creates a new <tt>Template</tt> object from liquid source code
119
+ # To enable profiling, pass in <tt>profile: true</tt> as an option.
120
+ # See Liquid::Profiler for more information
121
+ def parse(source, options = {})
122
+ new.parse(source, options)
123
+ end
124
+ end
125
+
126
+ def initialize
127
+ @rethrow_errors = false
128
+ @resource_limits = ResourceLimits.new(Template.default_resource_limits)
129
+ end
130
+
131
+ # Parse source code.
132
+ # Returns self for easy chaining
133
+ def parse(source, options = {})
134
+ @options = options
135
+ @profiling = options[:profile]
136
+ @line_numbers = options[:line_numbers] || @profiling
137
+ parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
138
+ @root = Document.parse(tokenize(source), parse_context)
139
+ @warnings = parse_context.warnings
140
+ self
141
+ end
142
+
143
+ def registers
144
+ @registers ||= {}
145
+ end
146
+
147
+ def assigns
148
+ @assigns ||= {}
149
+ end
150
+
151
+ def instance_assigns
152
+ @instance_assigns ||= {}
153
+ end
154
+
155
+ def errors
156
+ @errors ||= []
157
+ end
158
+
159
+ # Render takes a hash with local variables.
160
+ #
161
+ # if you use the same filters over and over again consider registering them globally
162
+ # with <tt>Template.register_filter</tt>
163
+ #
164
+ # if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
165
+ # will be available via <tt>Template#profiler</tt>
166
+ #
167
+ # Following options can be passed:
168
+ #
169
+ # * <tt>filters</tt> : array with local filters
170
+ # * <tt>registers</tt> : hash with register variables. Those can be accessed from
171
+ # filters and tags and might be useful to integrate liquid more with its host application
172
+ #
173
+ def render(*args)
174
+ return '' if @root.nil?
175
+
176
+ context = case args.first
177
+ when Liquid::Context
178
+ c = args.shift
179
+
180
+ if @rethrow_errors
181
+ c.exception_renderer = ->(_e) { raise }
182
+ end
183
+
184
+ c
185
+ when Liquid::Drop
186
+ drop = args.shift
187
+ drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
188
+ when Hash
189
+ Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
190
+ when nil
191
+ Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
192
+ else
193
+ raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
194
+ end
195
+
196
+ output = nil
197
+
198
+ context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
199
+
200
+ case args.last
201
+ when Hash
202
+ options = args.pop
203
+ output = options[:output] if options[:output]
204
+
205
+ options[:registers]&.each do |key, register|
206
+ context_register[key] = register
207
+ end
208
+
209
+ apply_options_to_context(context, options)
210
+ when Module, Array
211
+ context.add_filters(args.pop)
212
+ end
213
+
214
+ Template.registers.each do |key, register|
215
+ context_register[key] = register
216
+ end
217
+
218
+ # Retrying a render resets resource usage
219
+ context.resource_limits.reset
220
+
221
+ begin
222
+ # render the nodelist.
223
+ # for performance reasons we get an array back here. join will make a string out of it.
224
+ with_profiling(context) do
225
+ @root.render_to_output_buffer(context, output || +'')
226
+ end
227
+ rescue Liquid::MemoryError => e
228
+ context.handle_error(e)
229
+ ensure
230
+ @errors = context.errors
231
+ end
232
+ end
233
+
234
+ def render!(*args)
235
+ @rethrow_errors = true
236
+ render(*args)
237
+ end
238
+
239
+ def render_to_output_buffer(context, output)
240
+ render(context, output: output)
241
+ end
242
+
243
+ private
244
+
245
+ def tokenize(source)
246
+ Tokenizer.new(source, @line_numbers)
247
+ end
248
+
249
+ def with_profiling(context)
250
+ if @profiling && !context.partial
251
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
252
+
253
+ @profiler = Profiler.new(context.template_name)
254
+ @profiler.start
255
+
256
+ begin
257
+ yield
258
+ ensure
259
+ @profiler.stop
260
+ end
261
+ else
262
+ yield
263
+ end
264
+ end
265
+
266
+ def apply_options_to_context(context, options)
267
+ context.add_filters(options[:filters]) if options[:filters]
268
+ context.global_filter = options[:global_filter] if options[:global_filter]
269
+ context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
270
+ context.strict_variables = options[:strict_variables] if options[:strict_variables]
271
+ context.strict_filters = options[:strict_filters] if options[:strict_filters]
272
+ end
273
+ end
274
+ end