diff --git a/lib/tilt.rb b/lib/tilt.rb index 1b067368..b0029c4e 100644 --- a/lib/tilt.rb +++ b/lib/tilt.rb @@ -67,6 +67,9 @@ class Template # 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. + # + # The #initialize_engine method is called if this is the very first + # time this template subclass has been initialized. 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) @@ -74,7 +77,21 @@ def initialize(file=nil, line=1, options={}, &block) @line = line || 1 @options = options || {} @reader = block || lambda { |t| File.read(file) } + + if !self.class.engine_initialized + initialize_engine + self.class.engine_initialized = true + end + end + + # 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 + @engine_initialized = false + class << self ; attr_accessor :engine_initialized ; end + # Load template source and compile the template. The template is # loaded and compiled the first time this method is called; subsequent @@ -192,8 +209,11 @@ def initialize(*args) @template_procs = {} end + def initialize_engine + require_template_library 'erb' unless defined? ::ERB + end + def compile! - require_template_library 'erb' unless defined?(::ERB) @engine = ::ERB.new(data, options[:safe], options[:trim], '@_out_buf') end @@ -238,8 +258,11 @@ def local_assignment_code(locals) # Erubis template implementation. See: # http://www.kuwata-lab.com/erubis/ class ErubisTemplate < ERBTemplate + def initialize_engine + require_template_library 'erubis' unless defined? ::Erubis + end + 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, options) end @@ -250,8 +273,11 @@ def compile! # Haml template implementation. See: # http://haml.hamptoncatlin.com/ class HamlTemplate < Template + def initialize_engine + require_template_library 'haml' unless defined? ::Haml::Engine + end + def compile! - require_template_library 'haml' unless defined?(::Haml::Engine) @engine = ::Haml::Engine.new(data, haml_options) end @@ -272,8 +298,11 @@ def haml_options # # Sass templates do not support object scopes, locals, or yield. class SassTemplate < Template + def initialize_engine + require_template_library 'sass' unless defined? ::Sass::Engine + end + def compile! - require_template_library 'sass' unless defined?(::Sass::Engine) @engine = ::Sass::Engine.new(data, sass_options) end @@ -292,10 +321,13 @@ def sass_options # Builder template implementation. See: # http://builder.rubyforge.org/ class BuilderTemplate < Template - def compile! + def initialize_engine require_template_library 'builder' unless defined?(::Builder) end + def compile! + end + def evaluate(scope, locals, &block) xml = ::Builder::XmlMarkup.new(:indent => 2) if data.respond_to?(:to_str) @@ -328,8 +360,11 @@ def template_source # It's suggested that your program require 'liquid' at load # time when using this template engine. class LiquidTemplate < Template + def initialize_engine + require_template_library 'liquid' unless defined? ::Liquid::Template + end + def compile! - require_template_library 'liquid' unless defined?(::Liquid::Template) @engine = ::Liquid::Template.parse(data) end @@ -358,8 +393,11 @@ def flags [:smart, :filter_html].select { |flag| options[flag] } end + def initialize_engine + require_template_library 'rdiscount' unless defined? ::RDiscount + end + def compile! - require_template_library 'rdiscount' unless defined?(::RDiscount) @engine = RDiscount.new(data, *flags) end @@ -381,17 +419,13 @@ def evaluate(scope, locals, &block) 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) + def initialize_engine + require_template_library 'mustache' unless defined? ::Mustache + end - # Set the Mustache view namespace if we can + def compile! Mustache.view_namespace = options[:namespace] - - # Figure out which Mustache class to use. @engine = options[:view] || Mustache.view_class(name) - - # 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}=" @@ -399,20 +433,19 @@ def compile! 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 + # 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 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 + # 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 @@ -426,18 +459,18 @@ def evaluate(scope=nil, locals={}, &block) # RDoc template. See: # http://rdoc.rubyforge.org/ # - # It's suggested that your program require: - # - # require 'rdoc/markup' - # require 'rdoc/markup/to_html' - # - # at load time when using this template engine. + # It's suggested that your program require 'rdoc/markup' and + # 'rdoc/markup/to_html' at load time when using this template + # engine. class RDocTemplate < Template - def compile! + def initialize_engine unless defined?(::RDoc::Markup) require_template_library 'rdoc/markup' require_template_library 'rdoc/markup/to_html' end + end + + def compile! markup = RDoc::Markup::ToHtml.new @engine = markup.convert(data) end @@ -447,5 +480,4 @@ def evaluate(scope, locals, &block) end end register 'rdoc', RDocTemplate - end diff --git a/test/spec_tilt_template.rb b/test/spec_tilt_template.rb index 22c4861a..771fa922 100644 --- a/test/spec_tilt_template.rb +++ b/test/spec_tilt_template.rb @@ -44,9 +44,30 @@ }.should.not.raise end - it "raises NotImplementedError when #compile! not defined" do - inst = Tilt::Template.new { |template| "Hello World!" } - lambda { inst.render }.should.raise NotImplementedError + class InitializingMockTemplate < Tilt::Template + @@initialized_count = 0 + def self.initialized_count + @@initialized_count + end + + def initialize_engine + @@initialized_count += 1 + end + + def compile! + end + end + + it "calls #initialize_engine the very first time " do + InitializingMockTemplate.engine_initialized.should.be.nil + InitializingMockTemplate.initialized_count.should.equal 0 + + InitializingMockTemplate.new { "Hello World!" } + InitializingMockTemplate.engine_initialized.should.equal true + InitializingMockTemplate.initialized_count.should.equal 1 + + InitializingMockTemplate.new { "Hello World!" } + InitializingMockTemplate.initialized_count.should.equal 1 end class CompilingMockTemplate < Tilt::Template @@ -57,6 +78,11 @@ def compile! def compiled? ; @compiled ; end end + it "raises NotImplementedError when #compile! not defined" do + inst = Tilt::Template.new { |template| "Hello World!" } + lambda { inst.render }.should.raise NotImplementedError + end + it "raises NotImplementedError when #evaluate or #template_source not defined" do inst = CompilingMockTemplate.new { |t| "Hello World!" } lambda { inst.render }.should.raise NotImplementedError