lib/tilt.rb in tilt-0.2 vs lib/tilt.rb in tilt-0.3

- old
+ new

@@ -1,33 +1,49 @@ module Tilt + VERSION = '0.3' + @template_mappings = {} + # Hash of template path pattern => template implementation + # class mappings. + def self.mappings + @template_mappings + end + # Register a template implementation by file extension. def self.register(ext, template_class) - ext = ext.sub(/^\./, '') - @template_mappings[ext.downcase] = template_class + ext = ext.to_s.sub(/^\./, '') + mappings[ext.downcase] = template_class end # Create a new template for the given file using the file's extension # to determine the the template mapping. def self.new(file, line=nil, options={}, &block) - if template_class = self[File.basename(file)] + if template_class = self[file] template_class.new(file, line, options, &block) else fail "No template engine registered for #{File.basename(file)}" end end # Lookup a template class given for the given filename or file # extension. Return nil when no implementation is found. - def self.[](filename) - ext = filename.to_s.downcase - until ext.empty? - return @template_mappings[ext] if @template_mappings.key?(ext) - ext = ext.sub(/^[^.]*\.?/, '') + def self.[](file) + if @template_mappings.key?(pattern = file.to_s.downcase) + @template_mappings[pattern] + elsif @template_mappings.key?(pattern = File.basename(pattern)) + @template_mappings[pattern] + else + while !pattern.empty? + if @template_mappings.key?(pattern) + return @template_mappings[pattern] + else + pattern = pattern.sub(/^[^.]*\.?/, '') + end + end + nil end - nil end # Base class for template implementations. Subclasses must implement # the #compile! method and one of the #evaluate or #template_source # methods. @@ -50,30 +66,48 @@ # default, template data is read from the file specified. When a block # is given, it should read template data and return as a String. When # file is nil, a block is required. def initialize(file=nil, line=1, options={}, &block) raise ArgumentError, "file or block required" if file.nil? && block.nil? + options, line = line, 1 if line.is_a?(Hash) @file = file @line = line || 1 @options = options || {} @reader = block || lambda { |t| File.read(file) } 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) + # Load template source and compile the template. The template is + # loaded and compiled the first time this method is called; subsequent + # calls are no-ops. + def compile if @data.nil? @data = @reader.call(self) compile! end + 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) + compile 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__)' + file || '(__TEMPLATE__)' end protected # Do whatever preparation is necessary to "compile" the template. # Called immediately after template #data is loaded. Instance variables @@ -105,12 +139,14 @@ source = locals.collect { |k,v| "#{k} = locals[:#{k}]" } [source.join("\n"), source.length] end def require_template_library(name) - warn "WARN: loading '#{name}' library in a non thread-safe way; " + - "explicit require '#{name}' suggested." + if Thread.list.size > 1 + warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " + + "explicit require '#{name}' suggested." + end require name end end # Extremely simple template cache implementation. @@ -150,11 +186,11 @@ # It's suggested that your program require 'erb' at load # time when using this template engine. class ERBTemplate < Template def compile! require_template_library 'erb' unless defined?(::ERB) - @engine = ::ERB.new(data, nil, nil, '@_out_buf') + @engine = ::ERB.new(data, options[:safe], options[:trim], '@_out_buf') end def template_source @engine.src end @@ -186,10 +222,24 @@ end end end %w[erb rhtml].each { |ext| register ext, ERBTemplate } + # Erubis template implementation. See: + # http://www.kuwata-lab.com/erubis/ + # + # It's suggested that your program require 'erubis' at load + # time when using this template engine. + class ErubisTemplate < ERBTemplate + def compile! + require_template_library 'erubis' unless defined?(::Erubis) + Erubis::Eruby.class_eval(%Q{def add_preamble(src) src << "@_out_buf = _buf = '';" end}) + @engine = ::Erubis::Eruby.new(data) + end + end + register 'erubis', ErubisTemplate + # Haml template implementation. See: # http://haml.hamptoncatlin.com/ # # It's suggested that your program require 'haml' at load # time when using this template engine. @@ -291,7 +341,109 @@ def evaluate(scope, locals, &block) @engine.to_html end end register 'markdown', RDiscountTemplate + register 'md', RDiscountTemplate + # Mustache is written and maintained by Chris Wanstrath. See: + # http://github.com/defunkt/mustache + # + # It's suggested that your program require 'mustache' at load + # time when using this template engine. + # + # Mustache templates support the following options: + # + # * :view - The Mustache subclass that should be used a the view. When + # this option is specified, the template file will be determined from + # the view class, and the :namespace and :mustaches options are + # irrelevant. + # + # * :namespace - The class or module where View classes are located. + # If you have Hurl::App::Views, namespace should be Hurl:App. This + # defaults to Object, causing ::Views to be searched for classes. + # + # * :mustaches - Where mustache views (.rb files) are located, or nil + # disable auto-requiring of views based on template names. By default, + # the view file is assumed to be in the same directory as the template + # file. + # + # All other options are assumed to be attribute writer's on the Mustache + # class and are set when a template is compiled. They are: + # + # * :path - The base path where mustache templates (.html files) are + # located. This defaults to the current working directory. + # + # * :template_extension - The file extension used on mustache templates. + # The default is 'html'. + # + class MustacheTemplate < Template + attr_reader :engine + + # Locates and compiles the Mustache object used to create new views. The + def compile! + require_template_library 'mustache' unless defined?(::Mustache) + + @view_name = Mustache.classify(name.to_s) + @namespace = options[:namespace] || Object + + # Figure out which Mustache class to use. + @engine = + if options[:view] + @view_name = options[:view].name + options[:view] + elsif @namespace.const_defined?(:Views) && + @namespace::Views.const_defined?(@view_name) + @namespace::Views.const_get(@view_name) + elsif load_mustache_view + engine = @namespace::Views.const_get(@view_name) + engine.template = data + engine + else + Mustache + end + + # set options on the view class + options.each do |key, value| + next if %w[view namespace mustaches].include?(key.to_s) + @engine.send("#{key}=", value) if @engine.respond_to? "#{key}=" + end + end + + def evaluate(scope=nil, locals={}, &block) + # Create a new instance for playing with + instance = @engine.new + + # Copy instance variables from scope to the view + scope.instance_variables.each do |name| + instance.instance_variable_set(name, scope.instance_variable_get(name)) + end + + # Locals get added to the view's context + locals.each do |local, value| + instance[local] = value + end + + # If we're passed a block it's a subview. Sticking it in yield + # lets us use {{yield}} in layout.html to render the actual page. + instance[:yield] = block.call if block + + instance.template = data unless instance.compiled? + + instance.to_html + end + + # Require the mustache view lib if it exists. + def load_mustache_view + return if name.nil? + path = "#{options[:mustaches]}/#{name}.rb" + if options[:mustaches] && File.exist?(path) + require path.chomp('.rb') + path + elsif File.exist?(path = file.sub(/\.[^\/]+$/, '.rb')) + require path.chomp('.rb') + path + end + end + end + register 'mustache', MustacheTemplate end