Skip to content

Commit

Permalink
Tilt!
Browse files Browse the repository at this point in the history
* AbstractTemplate and StringTemplate
* Add ERBTemplate implementation
* Add HamlTemplate implementation
* Template file extension mappings
* Sass template support
* Be more adament about defaults in initialize
* Builder template support
* Add line file/number backtrace specs for ERB and Haml templates
  • Loading branch information
rtomayko committed Jan 26, 2009
1 parent 7fd462d commit 137cd54
Show file tree
Hide file tree
Showing 14 changed files with 821 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .autotest
@@ -0,0 +1,13 @@
require 'rubygems'
require 'bacon'
require 'autotest/bacon'

class Autotest::Bacon < Autotest
undef make_test_cmd
def make_test_cmd(files_to_test)
args = files_to_test.keys.flatten.join(' ')
args = '-a' if args.empty?
# TODO : make regex to pass to -n using values
"#{ruby} -S bacon -I#{libs} -o TestUnit #{args}"
end
end
18 changes: 18 additions & 0 deletions COPYING
@@ -0,0 +1,18 @@
Copyright (c) 2009 Ryan Tomayko <http://tomayko.com/about>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
62 changes: 62 additions & 0 deletions README.md
@@ -0,0 +1,62 @@
Tilt
====

Tilt wraps multiple template engines and makes them available through a simple
generic interface.

* Custom scope, locals, and yield support
* Backtraces with correct filenames and line numbers
* Template compilation/caching
* Template reloading

Usage
-----

All supported templates have an implementation class under the `Tilt` module.
Each template implementation follows the exact same interface:

template = Tilt::HamlTemplate.new('templates/foo.haml')
output = template.render

The `render` method takes an optional evaluation scope and locals hash
arguments. In the following example, the template is evaluated within the
context of the person object and can access the locals `x` and `y`:

template = Tilt::ERBTemplate.new('templates/foo.erb')
joe = Person.find('joe')
output = template.render(joe, :x => 35, :y => 42)

The `render` method may be called multiple times without creating a new
template object. Continuing the previous example, we can render in Jane's
scope with a different set of locals:

jane = Person.find('jane')
output = template.render(jane, :x => 22, :y => nil)

Blocks can be passed to the render method for templates that support running
arbitrary ruby code and using `yield`. Assuming the following was in a file
named `foo.erb`:

Hey <%= yield %>!

The block passed to the `render` method is invoked on `yield`:

template = Tilt::ERBTemplate.new('foo.erb')
template.render { 'Joe' }
# => "Hey Joe!"


Supported Template Engines
--------------------------

The following template engines are supported:

* ERB
* Interpolated Ruby String
* Haml (with the `haml` gem/library)
* Sass (with the `haml` gem/library)
* Builder (with the `builder` gem/library)
* Liquid (with the `liquid` gem/library)
* Markdown (with the `rdiscount` gem)
* Maruku (with the `maruku` gem)
* Textile (with the `redcloth` gem)
97 changes: 97 additions & 0 deletions Rakefile
@@ -0,0 +1,97 @@
task :default => :spec

# SPECS =====================================================================

desc 'Generate test coverage report'
task :rcov do
sh "rcov -Ilib:test test/*_test.rb"
end
desc 'Run specs with unit test style output'
task :test do |t|
sh 'bacon -qa'
end

desc 'Run specs with story style output'
task :spec do |t|
sh 'bacon -a'
end

# PACKAGING =================================================================

# load gemspec like github's gem builder to surface any SAFE issues.
Thread.new do
require 'rubygems/specification'
$spec = eval("$SAFE=3\n#{File.read('tilt.gemspec')}")
end.join

def package(ext='')
"dist/tilt-#{$spec.version}" + ext
end

desc 'Build packages'
task :package => %w[.gem .tar.gz].map {|e| package(e)}

desc 'Build and install as local gem'
task :install => package('.gem') do
sh "gem install #{package('.gem')}"
end

directory 'dist/'

file package('.gem') => %w[dist/ tilt.gemspec] + $spec.files do |f|
sh "gem build tilt.gemspec"
mv File.basename(f.name), f.name
end

file package('.tar.gz') => %w[dist/] + $spec.files do |f|
sh "git archive --format=tar HEAD | gzip > #{f.name}"
end

desc 'Upload gem and tar.gz distributables to rubyforge'
task :release => [package('.gem'), package('.tar.gz')] do |t|
sh <<-SH
rubyforge add_release wink tilt #{$spec.version} #{package('.gem')} &&
rubyforge add_file wink tilt #{$spec.version} #{package('.tar.gz')}
SH
end

# GEMSPEC ===================================================================

file 'tilt.gemspec' => FileList['{lib,test}/**','Rakefile'] do |f|
# read spec file and split out manifest section
spec = File.read(f.name)
parts = spec.split(" # = MANIFEST =\n")
# determine file list from git ls-files
files = `git ls-files`.
split("\n").sort.reject{ |file| file =~ /^\./ }.
map{ |file| " #{file}" }.join("\n")
# piece file back together and write...
parts[1] = " s.files = %w[\n#{files}\n ]\n"
spec = parts.join(" # = MANIFEST =\n")
spec.sub!(/s.date = '.*'/, "s.date = '#{Time.now.strftime("%Y-%m-%d")}'")
File.open(f.name, 'w') { |io| io.write(spec) }
puts "updated #{f.name}"
end

# DOC =======================================================================

# requires the hanna gem:
# gem install mislav-hanna --source=http://gems.github.com
desc 'Build API documentation (doc/api)'
task 'rdoc' => 'rdoc/index.html'
file 'rdoc/index.html' => FileList['lib/**/*.rb'] do |f|
rm_rf 'rdoc'
sh((<<-SH).gsub(/[\s\n]+/, ' ').strip)
hanna
--op doc/api
--promiscuous
--charset utf8
--fmt html
--inline-source
--line-numbers
--accessor option_accessor=RW
--main Tilt
--title 'Tilt API Documentation'
#{f.prerequisites.join(' ')}
SH
end
197 changes: 197 additions & 0 deletions lib/tilt.rb
@@ -0,0 +1,197 @@
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.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 AbstractTemplate
# Raw template data 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. 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 Abstract#evaluate method 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
end

# The template source is evaluated as a Ruby string. The #{} interpolation
# syntax can be used to generated dynamic output.
class StringTemplate < AbstractTemplate
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
class ERBTemplate < AbstractTemplate
def compile!
require 'erb' unless defined?(::ERB)
@engine = ::ERB.new(data)
end

def template_source
@engine.src
end
end
%w[erb rhtml].each { |ext| register ext, ERBTemplate }

# Haml template implementation. See:
# http://haml.hamptoncatlin.com/
class HamlTemplate < AbstractTemplate
def compile!
require 'haml' unless defined?(::Haml)
@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.
class SassTemplate < AbstractTemplate
def compile!
require 'sass' unless defined?(::Sass)
@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/
class BuilderTemplate < AbstractTemplate
def compile!
require '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
end
File renamed without changes.

0 comments on commit 137cd54

Please sign in to comment.