Permalink
Browse files

Add handler pipelining.

Pipelining allows an arbitrary number of renderers to get a shot
at a template before it is returned, so template.html.markdown.erb
will be passed through ERb->Markdown and then finally wrapped in a
layout. This also includes an initial refactoring of template
handling to DRY up some of the functionality, in particular the
handling of layouts.
  • Loading branch information...
1 parent f0dcb3a commit 322251ea7bb96f016ef41e26aa7eece382431b4b @ntalbott ntalbott committed Apr 6, 2012
View
4 Gemfile
@@ -17,10 +17,10 @@ group :development do
gem 'compass', '~> 0.11.1'
gem 'slim', '~> 0.9.4'
gem 'rdiscount', '~> 1.6.8'
- gem 'RedCloth', '~> 4.2.7'
+ gem 'RedCloth', '~> 4.2.9'
gem 'erubis', '~> 2.7.0'
gem 'less', '~> 1.2.21'
- gem 'radius', '~> 0.6.1'
+ gem 'radius', '~> 0.7.3'
gem 'coffee-script', '~> 2.2.0'
end
View
8 Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GEM
remote: http://rubygems.org/
specs:
- RedCloth (4.2.7)
+ RedCloth (4.2.9)
activesupport (3.0.9)
chunky_png (1.2.1)
coffee-script (2.2.0)
@@ -48,7 +48,7 @@ GEM
rack (1.3.2)
rack-test (0.6.1)
rack (>= 1.0)
- radius (0.6.1)
+ radius (0.7.3)
rake (0.9.2)
rdiscount (1.6.8)
rdoc (3.8)
@@ -76,7 +76,7 @@ PLATFORMS
ruby
DEPENDENCIES
- RedCloth (~> 4.2.7)
+ RedCloth (~> 4.2.9)
activesupport (~> 3.0)
coffee-script (~> 2.2.0)
compass (~> 0.11.1)
@@ -89,7 +89,7 @@ DEPENDENCIES
maruku (~> 0.6.0)
rack (~> 1.2)
rack-test (~> 0.5)
- radius (~> 0.6.1)
+ radius (~> 0.7.3)
rake (~> 0.9.0)
rdiscount (~> 1.6.8)
rdoc (~> 3.8.0)
View
1 lib/serve.rb
@@ -9,6 +9,7 @@ def singleton_class
require 'serve/version'
require 'serve/router'
+require 'serve/pipeline'
require 'serve/handlers/file_type_handler'
require 'serve/handlers/dynamic_handler'
require 'serve/handlers/sass_handler'
View
8 lib/serve/handlers/coffee_handler.rb
@@ -2,13 +2,17 @@ module Serve #:nodoc:
class CoffeeHandler < FileTypeHandler #:nodoc:
extension 'coffee'
- def parse(string)
- engine = Tilt::CoffeeScriptTemplate.new { string }
+ def parse(input, context)
+ engine = Tilt::CoffeeScriptTemplate.new { input }
engine.render
end
def content_type
'text/javascript'
end
+
+ def layout?
+ false
+ end
end
end
View
76 lib/serve/handlers/dynamic_handler.rb
@@ -1,4 +1,3 @@
-require 'serve/view_helpers'
require 'tilt'
module Serve #:nodoc:
@@ -13,64 +12,25 @@ def extensions
self.class.extensions
end
- extension *extensions
+ extension(*extensions)
- def process(request, response)
- response.headers['content-type'] = content_type
- response.body = parse(request, response)
- end
-
- def parse(request, response)
- context = Context.new(@root_path, request, response)
- install_view_helpers(context)
- parser = Parser.new(context)
- context.content << parser.parse_file(@script_filename)
- layout = find_layout_for(@script_filename)
- if layout
- parser.parse_file(layout)
- else
- context.content
- end
- end
-
- def find_layout_for(filename)
- root = @root_path
- path = filename[root.size..-1]
- layout = nil
- until layout or path == "/"
- path = File.dirname(path)
- possible_layouts = extensions.map do |ext|
- l = "_layout.#{ext}"
- possible_layout = File.join(root, path, l)
- File.file?(possible_layout) ? possible_layout : false
- end
- layout = possible_layouts.detect { |o| o }
- end
- layout
- end
-
- def install_view_helpers(context)
- view_helpers_file_path = @root_path + '/view_helpers.rb'
- if File.file?(view_helpers_file_path)
- context.singleton_class.module_eval(File.read(view_helpers_file_path) + "\ninclude ViewHelpers", view_helpers_file_path)
- end
+ def parse(input, context)
+ parser = Parser.new(context, @template_path)
+ parser.parse(input, extension)
end
class Parser #:nodoc:
- attr_accessor :context, :script_filename, :script_extension, :engine
+ attr_accessor :context, :script_extension, :engine, :template_path
- def initialize(context)
+ def initialize(context, template_path)
@context = context
@context.parser = self
+ @template_path = template_path
end
- def parse_file(filename, locals={})
- old_script_filename, old_script_extension, old_engine = @script_filename, @script_extension, @engine
-
- @script_filename = filename
-
- ext = File.extname(filename).sub(/^\.html\.|^\./, '').downcase
-
+ def parse(input, ext, locals={})
+ old_script_extension, old_engine = @script_extension, @engine
+
if ext == 'slim' # Ugly, but works
if Thread.list.size > 1
warn "WARN: serve autoloading 'slim' in a non thread-safe way; " +
@@ -81,31 +41,17 @@ def parse_file(filename, locals={})
@script_extension = ext
- @engine = Tilt[ext].new(filename, nil, :outvar => '@_out_buf')
+ @engine = Tilt[ext].new(nil, nil, :outvar => '@_out_buf'){input}
raise "#{ext} extension not supported" if @engine.nil?
@engine.render(context, locals) do |*args|
context.get_content_for(*args)
end
ensure
- @script_filename = old_script_filename
@script_extension = old_script_extension
@engine = old_engine
end
-
- end
-
- class Context #:nodoc:
- attr_accessor :content, :parser
- attr_reader :request, :response
-
- def initialize(root_path, request, response)
- @root_path, @request, @response = root_path, request, response
- @content = ''
- end
-
- include Serve::ViewHelpers
end
end
end
View
34 lib/serve/handlers/file_type_handler.rb
@@ -9,29 +9,37 @@ def self.extension(*extensions)
FileTypeHandler.handlers[ext] = self
end
end
-
- def self.find(path)
- if ext = File.extname(path)
- handlers[ext.sub(/\A\./, '')]
- end
+
+ def self.extensions
+ handlers.keys
end
-
- def initialize(root_path, path)
+
+ def self.handlers_for(path)
+ extensions = File.basename(path).split(".")[1..-1]
+ extensions.collect{|e| [handlers[e], e] if handlers[e]}.compact
+ end
+
+ attr_reader :extension
+ def initialize(root_path, template_path, extension)
@root_path = root_path
- @script_filename = File.join(@root_path, path)
+ @template_path = template_path
+ @extension = extension
end
- def process(request, response)
- response.headers['content-type'] = content_type
- response.body = parse(open(@script_filename){|io| io.read })
+ def process(input, context)
+ parse(input, context)
end
def content_type
'text/html'
end
+
+ def layout?
+ true
+ end
- def parse(string)
- string.dup
+ def parse(input, context)
+ input.dup
end
end
end
View
2 lib/serve/handlers/less_handler.rb
@@ -7,7 +7,7 @@ module Serve #:nodoc:
class LessHandler < FileTypeHandler #:nodoc:
extension 'less'
- def parse(string)
+ def parse(string, context)
require 'less'
Less.parse(string)
end
View
14 lib/serve/handlers/redirect_handler.rb
@@ -2,13 +2,17 @@ module Serve #:nodoc:
class RedirectHandler < FileTypeHandler #:nodoc:
extension 'redirect'
- def process(request, response)
- lines = super.strip.split("\n")
+ def process(input, context)
+ lines = input.strip.split("\n")
url = lines.last.strip
- unless url =~ %r{^\w[\w\d+.-]*:.*}
- url = request.protocol + request.host_with_port + url
+ unless url =~ %r{^\w[\w+.-]*:.*}
+ url = context.request.protocol + context.request.host_with_port + url
end
- response.redirect(url, '302')
+ context.response.redirect(url, '302')
+ end
+
+ def layout?
+ false
end
end
end
View
10 lib/serve/handlers/sass_handler.rb
@@ -7,20 +7,18 @@ module Serve #:nodoc:
class SassHandler < FileTypeHandler #:nodoc:
extension 'sass', 'scss'
- def parse(string)
+ def parse(string, context)
require 'sass'
engine = Sass::Engine.new(string,
:load_paths => [@root_path],
:style => :expanded,
- :filename => @script_filename,
- :syntax => syntax(@script_filename)
+ :syntax => syntax
)
engine.render
end
- def syntax(filename)
- ext = File.extname(@script_filename)
- if ext == '.scss'
+ def syntax
+ if extension == 'scss'
:scss
else
:sass
View
110 lib/serve/pipeline.rb
@@ -0,0 +1,110 @@
+require 'serve/view_helpers'
+
+module Serve
+ class Pipeline
+ def self.handles?(path)
+ !FileTypeHandler.handlers_for(path).empty?
+ end
+
+ def self.build(root, path)
+ return nil unless handles?(path)
+ Pipeline.new(root, path, extensions_for(path))
+ end
+
+ attr_reader :template
+ def initialize(root_path, path)
+ @root_path = root_path
+ @template = Template.new(File.join(@root_path, path))
+ @layout = find_layout_for(@template.path)
+ end
+
+ def find_layout_for(template_path)
+ return Template::Passthrough.new(@template) unless @template.layout?
+ root = @root_path
+ path = template_path[root.size..-1]
+ layout = nil
+ until layout or path == "/"
+ possible_layouts = FileTypeHandler.extensions.map do |ext|
+ l = "_layout.#{ext}"
+ possible_layout = File.join(root, path, l)
+ File.file?(possible_layout) ? possible_layout : false
+ end
+ layout = possible_layouts.detect { |o| o }
+ path = File.dirname(path)
+ end
+ if layout
+ Template.new(layout)
+ else
+ Template::Passthrough.new(@template)
+ end
+ end
+
+ def process(request, response)
+ response.headers['Content-Type'] = @layout.content_type
+ context = Context.new(@root_path, request, response)
+ @template.process(context)
+ @layout.process(context)
+ response.body = context.content
+ end
+
+ class Template
+ attr_reader :path, :handlers
+ def initialize(file)
+ @path = File.dirname(file)
+ @raw = File.read(file)
+ @handlers = FileTypeHandler.handlers_for(file).collect{|h, extension| h.new(@root_path, @path, extension)}
+ end
+
+ def content_type
+ @handlers.first.content_type
+ end
+
+ def process(context)
+ context.content = @handlers.reverse.inject(@raw.dup) do |body, handler|
+ handler.process(body, context)
+ end
+ end
+
+ def layout?
+ @handlers.first.layout?
+ end
+
+ class Passthrough
+ def initialize(template)
+ @template = template
+ end
+
+ def process(context)
+ end
+
+ def layout?
+ false
+ end
+
+ def content_type
+ @template.content_type
+ end
+ end
+ end
+
+ class Context #:nodoc:
+ attr_accessor :content, :parser
+ attr_reader :request, :response
+
+ def initialize(root_path, request, response)
+ @root_path, @request, @response = root_path, request, response
+ @content = ''
+ install_view_helpers
+ end
+
+ def install_view_helpers
+ view_helpers_file_path = @root_path + '/view_helpers.rb'
+ if File.file?(view_helpers_file_path)
+ singleton_class.module_eval(File.read(view_helpers_file_path) + "\ninclude ViewHelpers", view_helpers_file_path)
+ end
+ end
+
+ include Serve::ViewHelpers
+ end
+ end
+end
View
6 lib/serve/rack.rb
@@ -111,11 +111,9 @@ def process(request, response)
path = Serve::Router.resolve(@root, request.path_info)
if path
# Fetch the file handler for a file with a given extension/
- ext = File.extname(path)[1..-1]
- handler = Serve::FileTypeHandler.handlers[ext]
- if handler
+ if Serve::Pipeline.handles?(path)
# Handler exists? Process the request and response.
- handler.new(@root, path).process(request, response)
+ Serve::Pipeline.new(@root, path).process(request, response)
response
else
# Handler doesn't exist? Rewrite the request to use the new path.
View
17 lib/serve/view_helpers.rb
@@ -137,27 +137,26 @@ def render_partial(partial, options={})
end
def render_template(template, options={})
- path = File.dirname(parser.script_filename)
+ path = parser.template_path
if template =~ %r{^/}
template = template[1..-1]
path = @root_path
end
- filename = template_filename(File.join(path, template), :partial => options[:partial])
+ filename = template_filename(path, template, :partial => options[:partial])
if File.file?(filename)
- parser.parse_file(filename, options[:locals])
+ parser.parse(File.read(filename), File.extname(filename).split(".").last, options[:locals])
else
raise "File does not exist #{filename.inspect}"
end
end
private
- def template_filename(name, options)
- path = File.dirname(name)
- template = File.basename(name)
- template = "_" + template if options[:partial]
- template += extname(parser.script_filename) unless name =~ /\.[a-z]+$/
- File.join(path, template)
+ def template_filename(path, template, options)
+ template_path = File.dirname(template)
+ template_file = File.basename(template)
+ template_file = "_" + template_file if options[:partial]
+ File.join(path, Serve::Router.resolve(path, File.join(template_path, template_file)))
end
def extname(filename)
View
0 spec/fixtures/directory/coffee.coffee
No changes.
View
0 spec/fixtures/directory/markdown.html.markdown
No changes.
View
0 spec/fixtures/directory/markdown.markdown
No changes.
View
0 spec/fixtures/directory/markdown_erb.markdown.erb
No changes.
View
52 spec/pipeline_spec.rb
@@ -0,0 +1,52 @@
+require File.dirname(__FILE__) + '/spec_helper.rb'
+require 'serve/pipeline'
+
+describe Serve::Pipeline do
+ before :each do
+ @root = File.expand_path("../fixtures", __FILE__)
+ end
+
+ describe "self.handles?" do
+ it "should not handle .html" do
+ Serve::Pipeline.handles?("dir/file.html").should be_false
+ end
+
+ it "should handle .markdown" do
+ Serve::Pipeline.handles?("dir/file.markdown").should be_true
+ end
+
+ it "should handle .html.markdown" do
+ Serve::Pipeline.handles?("dir/file.html.markdown").should be_true
+ end
+
+ it "should handle .coffee" do
+ Serve::Pipeline.handles?("dir/file.coffee").should be_true
+ end
+
+ it "should handle .markdown.erb" do
+ Serve::Pipeline.handles?("dir/file.markdown.erb").should be_true
+ end
+ end
+
+ describe "initialize" do
+ it "should build a pipeline for .markdown" do
+ pipeline = Serve::Pipeline.new(@root, "directory/markdown.markdown")
+ pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown)
+ end
+
+ it "should build a pipeline for .html.markdown" do
+ pipeline = Serve::Pipeline.new(@root, "directory/markdown.html.markdown")
+ pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown)
+ end
+
+ it "should build a pipeline for .coffee" do
+ pipeline = Serve::Pipeline.new(@root, "directory/coffee.coffee")
+ pipeline.template.handlers.collect{|h| h.extension}.should == %w(coffee)
+ end
+
+ it "should build a pipeline for .markdown.erb" do
+ pipeline = Serve::Pipeline.new(@root, "directory/markdown_erb.markdown.erb")
+ pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown erb)
+ end
+ end
+end
View
2 test_project/markdown.erb/_footer.html.markdown.erb
@@ -0,0 +1,2 @@
+<hr style="clear: both" />
+<p>Copyright &copy; John W. Long. All rights reserved.</p>
View
29 test_project/markdown.erb/_layout.html.erb
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <title><%= @title %></title>
+ <link href="/styles.css" rel="stylesheet" type="text/css" />
+ <style type="text/css">
+ #content {
+ float: left;
+ width: 70%;
+ }
+ #sidebar {
+ float: right;
+ width: 28%;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="content">
+ <h1><%= @title %></h1>
+ <%= yield %>
+ </div>
+ <% if content_for?(:sidebar) %>
+ <div id="sidebar">
+ <%= yield :sidebar %>
+ </div>
+ <% end %>
+ <%= render :partial => "footer" %>
+ </body>
+</html>
View
12 test_project/markdown.erb/index.html.markdown.erb
@@ -0,0 +1,12 @@
+<% @title = "ERB Template" %>
+
+Hello Markdown World!
+=====================
+
+<%= custom_method %>
+
+<% content_for :sidebar do %>
+### Sidebar
+
+Just a test
+<% end %>

2 comments on commit 322251e

@duff

Not sure if it's this commit or not, but when I use the latest released version of the gem, it works in that it picks up a a layout that's in a parent directory.

When I use what's currently in master, my parent layout isn't picked up.

@ntalbott
Collaborator

Can you open an issue for this? Much easier to track that way.

Please sign in to comment.