module Tilt TOPOBJECT = defined?(BasicObject) ? BasicObject : Object # Base class for template implementations. Subclasses must implement # the #prepare method and one of the #evaluate or #precompiled_template # methods. class Template # Template source; loaded from a file or given directly. attr_reader :data # The name of the file where the template data was loaded from. attr_reader :file # The line number in #file where template data was loaded from. attr_reader :line # A Hash of template engine specific options. This is passed directly # to the underlying engine and is not used by the generic template # interface. attr_reader :options # Used to determine if this class's initialize_engine method has # been called yet. @engine_initialized = false class << self attr_accessor :engine_initialized alias engine_initialized? engine_initialized attr_accessor :default_mime_type end # Create a new template with the file, line, and options specified. By # default, template data is read from the file. When a block is given, # it should read template data and return as a String. When file is nil, # a block is required. # # All arguments are optional. def initialize(file=nil, line=1, options={}, &block) @file, @line, @options = nil, 1, {} [options, line, file].compact.each do |arg| case when arg.respond_to?(:to_str) ; @file = arg.to_str when arg.respond_to?(:to_int) ; @line = arg.to_int when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup when arg.respond_to?(:path) ; @file = arg.path else raise TypeError end end raise ArgumentError, "file or block required" if (@file || block).nil? # call the initialize_engine method if this is the very first time # an instance of this class has been created. if !self.class.engine_initialized? initialize_engine self.class.engine_initialized = true end # used to hold compiled template methods @compiled_method = {} # used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment) # currently only used if template compiles to ruby @default_encoding = @options.delete :default_encoding # load template data and prepare (uses binread to avoid encoding issues) @reader = block || lambda { |t| File.respond_to?(:binread) ? File.binread(@file) : File.read(@file) } @data = @reader.call(self) prepare end # Render the template in the given scope with the locals specified. If a # block is given, it is typically available within the template via # +yield+. def render(scope=Object.new, locals={}, &block) evaluate scope, locals || {}, &block end # The basename of the template file. def basename(suffix='') File.basename(file, suffix) if file end # The template file's basename with all extensions chomped off. def name basename.split('.', 2).first if basename end # The filename used in backtraces to describe the template. def eval_file file || '(__TEMPLATE__)' end # Whether or not this template engine allows executing Ruby script # within the template. If this is false, +scope+ and +locals+ will # generally not be used, nor will the provided block be avaiable # via +yield+. # This should be overridden by template subclasses. def allows_script? true end protected # Called once and only once for each template subclass the first time # the template class is initialized. This should be used to require the # underlying template library and perform any initial setup. def initialize_engine end # Like Kernel#require but issues a warning urging a manual require when # running under a threaded environment. def require_template_library(name) if Thread.list.size > 1 warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " + "explicit require '#{name}' suggested." end require name end # Do whatever preparation is necessary to setup the underlying template # engine. Called immediately after template data is loaded. Instance # variables set in this method are available when #evaluate is called. # # Subclasses must provide an implementation of this method. def prepare if respond_to?(:compile!) # backward compat with tilt < 0.6; just in case warn 'Tilt::Template#compile! is deprecated; implement #prepare instead.' compile! else raise NotImplementedError end end # Execute the compiled template and return the result string. Template # evaluation is guaranteed to be performed in the scope object with the # locals specified and with support for yielding to the block. # # This method is only used by source generating templates. Subclasses that # override render() may not support all features. def evaluate(scope, locals, &block) method = compiled_method(locals.keys) method.bind(scope).call(locals, &block) end # Generates all template source by combining the preamble, template, and # postamble and returns a two-tuple of the form: [source, offset], where # source is the string containing (Ruby) source code for the template and # offset is the integer line offset where line reporting should begin. # # Template subclasses may override this method when they need complete # control over source generation or want to adjust the default line # offset. In most cases, overriding the #precompiled_template method is # easier and more appropriate. def precompiled(locals) preamble = precompiled_preamble(locals) template = precompiled_template(locals) magic_comment = extract_magic_comment(template) if magic_comment # Magic comment e.g. "# coding: utf-8" has to be in the first line. # So we copy the magic comment to the first line. preamble = magic_comment + "\n" + preamble end parts = [ preamble, template, precompiled_postamble(locals) ] [parts.join("\n"), preamble.count("\n") + 1] end # A string containing the (Ruby) source code for the template. The # default Template#evaluate implementation requires either this method # or the #precompiled method be overridden. When defined, the base # Template guarantees correct file/line handling, locals support, custom # scopes, and support for template compilation when the scope object # allows it. def precompiled_template(locals) raise NotImplementedError end # Generates preamble code for initializing template state, and performing # locals assignment. The default implementation performs locals # assignment only. Lines included in the preamble are subtracted from the # source line offset, so adding code to the preamble does not effect line # reporting in Kernel::caller and backtraces. def precompiled_preamble(locals) locals.map do |k,v| if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/ "#{k} = locals[#{k.inspect}]" else raise "invalid locals key: #{k.inspect} (keys must be variable names)" end end.join("\n") end # Generates postamble code for the precompiled template source. The # string returned from this method is appended to the precompiled # template source. def precompiled_postamble(locals) '' end # The compiled method for the locals keys provided. def compiled_method(locals_keys) @compiled_method[locals_keys] ||= compile_template_method(locals_keys) end private def compile_template_method(locals) source, offset = precompiled(locals) method_name = "__tilt_#{Thread.current.object_id.abs}" method_source = <<-RUBY #{extract_magic_comment source} TOPOBJECT.class_eval do def #{method_name}(locals) Thread.current[:tilt_vars] = [self, locals] class << self this, locals = Thread.current[:tilt_vars] this.instance_eval do RUBY offset += method_source.count("\n") method_source << source method_source << "\nend;end;end;end" Object.class_eval method_source, eval_file, line - offset unbind_compiled_method(method_name) end def unbind_compiled_method(method_name) method = TOPOBJECT.instance_method(method_name) TOPOBJECT.class_eval { remove_method(method_name) } method end def extract_magic_comment(script) comment = script.slice(/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/) if comment && !%w[ascii-8bit binary].include?($1.downcase) comment elsif @default_encoding "# coding: #{@default_encoding}" end end end end