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,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