255 lines
7.0 KiB
Ruby
255 lines
7.0 KiB
Ruby
module Liquid
|
|
# Templates are central to liquid.
|
|
# Interpretating templates is a two step process. First you compile the
|
|
# source code you got. During compile time some extensive error checking is performed.
|
|
# your code should expect to get some SyntaxErrors.
|
|
#
|
|
# After you have a compiled template you can then <tt>render</tt> it.
|
|
# You can use a compiled template over and over again and keep it cached.
|
|
#
|
|
# Example:
|
|
#
|
|
# template = Liquid::Template.parse(source)
|
|
# template.render('user_name' => 'bob')
|
|
#
|
|
class Template
|
|
attr_accessor :root
|
|
attr_reader :resource_limits, :warnings
|
|
|
|
@@file_system = BlankFileSystem.new
|
|
|
|
class TagRegistry
|
|
include Enumerable
|
|
|
|
def initialize
|
|
@tags = {}
|
|
@cache = {}
|
|
end
|
|
|
|
def [](tag_name)
|
|
return nil unless @tags.key?(tag_name)
|
|
return @cache[tag_name] if Liquid.cache_classes
|
|
|
|
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
|
end
|
|
|
|
def []=(tag_name, klass)
|
|
@tags[tag_name] = klass.name
|
|
@cache[tag_name] = klass
|
|
end
|
|
|
|
def delete(tag_name)
|
|
@tags.delete(tag_name)
|
|
@cache.delete(tag_name)
|
|
end
|
|
|
|
def each(&block)
|
|
@tags.each(&block)
|
|
end
|
|
|
|
private
|
|
|
|
def lookup_class(name)
|
|
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
|
|
end
|
|
end
|
|
|
|
attr_reader :profiler
|
|
|
|
class << self
|
|
# Sets how strict the parser should be.
|
|
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
|
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
|
# :strict will enforce correct syntax.
|
|
attr_writer :error_mode
|
|
|
|
# Sets how strict the taint checker should be.
|
|
# :lax is the default, and ignores the taint flag completely
|
|
# :warn adds a warning, but does not interrupt the rendering
|
|
# :error raises an error when tainted output is used
|
|
attr_writer :taint_mode
|
|
|
|
attr_accessor :default_exception_renderer
|
|
Template.default_exception_renderer = lambda do |exception|
|
|
exception
|
|
end
|
|
|
|
def file_system
|
|
@@file_system
|
|
end
|
|
|
|
def file_system=(obj)
|
|
@@file_system = obj
|
|
end
|
|
|
|
def register_tag(name, klass)
|
|
tags[name.to_s] = klass
|
|
end
|
|
|
|
def tags
|
|
@tags ||= TagRegistry.new
|
|
end
|
|
|
|
def error_mode
|
|
@error_mode ||= :lax
|
|
end
|
|
|
|
def taint_mode
|
|
@taint_mode ||= :lax
|
|
end
|
|
|
|
# Pass a module with filter methods which should be available
|
|
# to all liquid views. Good for registering the standard library
|
|
def register_filter(mod)
|
|
Strainer.global_filter(mod)
|
|
end
|
|
|
|
def default_resource_limits
|
|
@default_resource_limits ||= {}
|
|
end
|
|
|
|
# creates a new <tt>Template</tt> object from liquid source code
|
|
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
|
# See Liquid::Profiler for more information
|
|
def parse(source, options = {})
|
|
template = Template.new
|
|
template.parse(source, options)
|
|
end
|
|
end
|
|
|
|
def initialize
|
|
@rethrow_errors = false
|
|
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
|
end
|
|
|
|
# Parse source code.
|
|
# Returns self for easy chaining
|
|
def parse(source, options = {})
|
|
@options = options
|
|
@profiling = options[:profile]
|
|
@line_numbers = options[:line_numbers] || @profiling
|
|
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
|
@root = Document.parse(tokenize(source), parse_context)
|
|
@warnings = parse_context.warnings
|
|
self
|
|
end
|
|
|
|
def registers
|
|
@registers ||= {}
|
|
end
|
|
|
|
def assigns
|
|
@assigns ||= {}
|
|
end
|
|
|
|
def instance_assigns
|
|
@instance_assigns ||= {}
|
|
end
|
|
|
|
def errors
|
|
@errors ||= []
|
|
end
|
|
|
|
# Render takes a hash with local variables.
|
|
#
|
|
# if you use the same filters over and over again consider registering them globally
|
|
# with <tt>Template.register_filter</tt>
|
|
#
|
|
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
|
|
# will be available via <tt>Template#profiler</tt>
|
|
#
|
|
# Following options can be passed:
|
|
#
|
|
# * <tt>filters</tt> : array with local filters
|
|
# * <tt>registers</tt> : hash with register variables. Those can be accessed from
|
|
# filters and tags and might be useful to integrate liquid more with its host application
|
|
#
|
|
def render(*args)
|
|
return ''.freeze if @root.nil?
|
|
|
|
context = case args.first
|
|
when Liquid::Context
|
|
c = args.shift
|
|
|
|
if @rethrow_errors
|
|
c.exception_renderer = ->(e) { raise }
|
|
end
|
|
|
|
c
|
|
when Liquid::Drop
|
|
drop = args.shift
|
|
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
|
when Hash
|
|
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
|
when nil
|
|
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
|
else
|
|
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
|
end
|
|
|
|
case args.last
|
|
when Hash
|
|
options = args.pop
|
|
|
|
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
|
|
|
apply_options_to_context(context, options)
|
|
when Module, Array
|
|
context.add_filters(args.pop)
|
|
end
|
|
|
|
# Retrying a render resets resource usage
|
|
context.resource_limits.reset
|
|
|
|
begin
|
|
# render the nodelist.
|
|
# for performance reasons we get an array back here. join will make a string out of it.
|
|
result = with_profiling(context) do
|
|
@root.render(context)
|
|
end
|
|
result.respond_to?(:join) ? result.join : result
|
|
rescue Liquid::MemoryError => e
|
|
context.handle_error(e)
|
|
ensure
|
|
@errors = context.errors
|
|
end
|
|
end
|
|
|
|
def render!(*args)
|
|
@rethrow_errors = true
|
|
render(*args)
|
|
end
|
|
|
|
private
|
|
|
|
def tokenize(source)
|
|
Tokenizer.new(source, @line_numbers)
|
|
end
|
|
|
|
def with_profiling(context)
|
|
if @profiling && !context.partial
|
|
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
|
|
|
@profiler = Profiler.new
|
|
@profiler.start
|
|
|
|
begin
|
|
yield
|
|
ensure
|
|
@profiler.stop
|
|
end
|
|
else
|
|
yield
|
|
end
|
|
end
|
|
|
|
def apply_options_to_context(context, options)
|
|
context.add_filters(options[:filters]) if options[:filters]
|
|
context.global_filter = options[:global_filter] if options[:global_filter]
|
|
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
|
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
|
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
|
end
|
|
end
|
|
end
|