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 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 Template object from liquid source code # To enable profiling, pass in profile: true 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 Template.register_filter # # if profiling was enabled in Template#parse then the resulting profiling information # will be available via Template#profiler # # 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 ''.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