diff --git a/lib/tilt.rb b/lib/tilt.rb new file mode 100644 index 00000000..7688f534 --- /dev/null +++ b/lib/tilt.rb @@ -0,0 +1,297 @@ +module Tilt + @template_mappings = {} + + # Register a template implementation by file extension. + def self.register(ext, template_class) + ext = ext.sub(/^\./, '') + @template_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)] + 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(/^[^.]*\.?/, '') + end + nil + end + + # Base class for template implementations. Subclasses must implement + # the #compile! method and one of the #evaluate or #template_source + # 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 + + # Create a new template with the file, line, and options specified. By + # 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? + @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) + if @data.nil? + @data = @reader.call(self) + compile! + end + evaluate scope, locals || {}, &block + end + + # The filename used in backtraces to describe the template. + def eval_file + @file || '(__TEMPLATE__)' + end + + protected + # Do whatever preparation is necessary to "compile" the template. + # 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 compile! + raise NotImplementedError + end + + # Process the template and return the result. Subclasses should override + # this method unless they implement the #template_source. + def evaluate(scope, locals, &block) + source, offset = local_assignment_code(locals) + source = [source, template_source].join("\n") + scope.instance_eval source, eval_file, line - offset + end + + # Return a string containing the (Ruby) source code for the template. The + # default Template#evaluate implementation requires this method be + # defined. + def template_source + raise NotImplementedError + end + + private + def local_assignment_code(locals) + return ['', 1] if locals.empty? + 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." + require name + end + end + + # Extremely simple template cache implementation. + class Cache + def initialize + @cache = {} + end + + def fetch(*key) + key = key.map { |part| part.to_s }.join(":") + @cache[key] ||= yield + end + + def clear + @cache = {} + end + end + + # Template Implementations ================================================ + + # The template source is evaluated as a Ruby string. The #{} interpolation + # syntax can be used to generated dynamic output. + class StringTemplate < Template + def compile! + @code = "%Q{#{data}}" + end + + def template_source + @code + end + end + register 'str', StringTemplate + + # ERB template implementation. See: + # http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html + # + # 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') + end + + def template_source + @engine.src + end + + def evaluate(scope, locals, &block) + source, offset = local_assignment_code(locals) + source = [source, template_source].join("\n") + + original_out_buf = + scope.instance_variables.any? { |var| var.to_sym == :@_out_buf } && + scope.instance_variable_get(:@_out_buf) + + scope.instance_eval source, eval_file, line - offset + + output = scope.instance_variable_get(:@_out_buf) + scope.instance_variable_set(:@_out_buf, original_out_buf) + + output + end + + private + + # ERB generates a line to specify the character coding of the generated + # source in 1.9. Account for this in the line offset. + if RUBY_VERSION >= '1.9.0' + def local_assignment_code(locals) + source, offset = super + [source, offset + 1] + end + end + end + %w[erb rhtml].each { |ext| register ext, ERBTemplate } + + # Haml template implementation. See: + # http://haml.hamptoncatlin.com/ + # + # It's suggested that your program require 'haml' at load + # time when using this template engine. + class HamlTemplate < Template + def compile! + require_template_library 'haml' unless defined?(::Haml::Engine) + @engine = ::Haml::Engine.new(data, haml_options) + end + + def evaluate(scope, locals, &block) + @engine.render(scope, locals, &block) + end + + private + def haml_options + options.merge(:filename => eval_file, :line => line) + end + end + register 'haml', HamlTemplate + + # Sass template implementation. See: + # http://haml.hamptoncatlin.com/ + # + # Sass templates do not support object scopes, locals, or yield. + # + # It's suggested that your program require 'sass' at load + # time when using this template engine. + class SassTemplate < Template + def compile! + require_template_library 'sass' unless defined?(::Sass::Engine) + @engine = ::Sass::Engine.new(data, sass_options) + end + + def evaluate(scope, locals, &block) + @engine.render + end + + private + def sass_options + options.merge(:filename => eval_file, :line => line) + end + end + register 'sass', SassTemplate + + # Builder template implementation. See: + # http://builder.rubyforge.org/ + # + # It's suggested that your program require 'builder' at load + # time when using this template engine. + class BuilderTemplate < Template + def compile! + require_template_library 'builder' unless defined?(::Builder) + end + + def evaluate(scope, locals, &block) + xml = ::Builder::XmlMarkup.new(:indent => 2) + if data.respond_to?(:to_str) + locals[:xml] = xml + super(scope, locals, &block) + elsif data.kind_of?(Proc) + data.call(xml) + end + xml.target! + end + + def template_source + data.to_str + end + end + register 'builder', BuilderTemplate + + # Liquid template implementation. See: + # http://liquid.rubyforge.org/ + # + # LiquidTemplate does not support scopes or yield blocks. + # + # It's suggested that your program require 'liquid' at load + # time when using this template engine. + class LiquidTemplate < Template + def compile! + require_template_library 'liquid' unless defined?(::Liquid::Template) + @engine = ::Liquid::Template.parse(data) + end + + def evaluate(scope, locals, &block) + locals = locals.inject({}) { |hash,(k,v)| hash[k.to_s] = v ; hash } + @engine.render(locals) + end + end + register 'liquid', LiquidTemplate + + # Discount Markdown implementation. + class RDiscountTemplate < Template + def compile! + require_template_library 'rdiscount' unless defined?(::RDiscount) + @engine = RDiscount.new(data) + end + + def evaluate(scope, locals, &block) + @engine.to_html + end + end + register 'markdown', RDiscountTemplate + +end