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 render 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
DEFAULT_OPTIONS = {
:locale => I18n.new
}
attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new
class << self
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 ||= {}
end
# Disabled (false) for better performance
def count_lines=(flag)
@count_lines = flag
end
def count_lines
@count_lines || false
end
# 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.
def error_mode=(mode)
@error_mode = mode
end
def error_mode
@error_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
# creates a new Template object from liquid source code
def parse(source, options = {})
template = Template.new
template.parse(source, options)
template
end
end
# creates a new Template from an array of tokens. Use Template.parse instead
def initialize
@resource_limits = {}
end
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
_options = { template: self }.merge(DEFAULT_OPTIONS).merge(options)
@root = Document.new(tokenize(source), _options)
@warnings = nil
self
end
def warnings
return [] unless @root
@warnings ||= @root.warnings
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 Template.register_filter
#
# Following options can be passed:
#
# * filters : array with local filters
# * registers : 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 '' if @root.nil?
context = case args.first
when Liquid::Context
args.shift
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
if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
if options[:filters]
context.add_filters(options[:filters])
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop)
end
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
result = @root.render(context)
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
def walk(memo = {}, &block)
# puts @root.nodelist.inspect
self._walk(@root.nodelist, memo, &block)
end
def _walk(list, memo = {}, &block)
list.each do |node|
saved_memo = memo.clone
# puts "fetch ! #{node.respond_to?(:name) ? node.name : 'String'} / #{node.respond_to?(:nodelist)}"
if block_given?
# puts "youpi ! #{node.name}"
_memo = yield(node, memo) || {}
memo.merge!(_memo)
end
if node.respond_to?(:nodelist) && !node.nodelist.blank?
self._walk(node.nodelist, memo, &block)
end
memo = saved_memo
end
memo
end
private
# Uses the Liquid::TemplateParser regexp to tokenize the passed source
def tokenize(source)
source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
end
end