From 322251ea7bb96f016ef41e26aa7eece382431b4b Mon Sep 17 00:00:00 2001 From: Nathaniel Talbott Date: Thu, 5 Apr 2012 23:37:01 -0400 Subject: [PATCH] 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. --- Gemfile | 4 +- Gemfile.lock | 8 +- lib/serve.rb | 1 + lib/serve/handlers/coffee_handler.rb | 8 +- lib/serve/handlers/dynamic_handler.rb | 76 ++---------- lib/serve/handlers/file_type_handler.rb | 34 +++--- lib/serve/handlers/less_handler.rb | 2 +- lib/serve/handlers/redirect_handler.rb | 14 ++- lib/serve/handlers/sass_handler.rb | 10 +- lib/serve/pipeline.rb | 110 ++++++++++++++++++ lib/serve/rack.rb | 6 +- lib/serve/view_helpers.rb | 17 ++- spec/fixtures/directory/coffee.coffee | 0 .../fixtures/directory/markdown.html.markdown | 0 spec/fixtures/directory/markdown.markdown | 0 .../directory/markdown_erb.markdown.erb | 0 spec/pipeline_spec.rb | 52 +++++++++ .../markdown.erb/_footer.html.markdown.erb | 2 + test_project/markdown.erb/_layout.html.erb | 29 +++++ .../markdown.erb/index.html.markdown.erb | 12 ++ 20 files changed, 274 insertions(+), 111 deletions(-) create mode 100644 lib/serve/pipeline.rb create mode 100644 spec/fixtures/directory/coffee.coffee create mode 100644 spec/fixtures/directory/markdown.html.markdown create mode 100644 spec/fixtures/directory/markdown.markdown create mode 100644 spec/fixtures/directory/markdown_erb.markdown.erb create mode 100644 spec/pipeline_spec.rb create mode 100644 test_project/markdown.erb/_footer.html.markdown.erb create mode 100644 test_project/markdown.erb/_layout.html.erb create mode 100644 test_project/markdown.erb/index.html.markdown.erb diff --git a/Gemfile b/Gemfile index 7f6d1e7..1a63d70 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index de433fa..f9789b0 100644 --- a/Gemfile.lock +++ b/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) diff --git a/lib/serve.rb b/lib/serve.rb index 024e124..ed508b2 100644 --- a/lib/serve.rb +++ b/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' diff --git a/lib/serve/handlers/coffee_handler.rb b/lib/serve/handlers/coffee_handler.rb index a7f382c..4d0e22f 100644 --- a/lib/serve/handlers/coffee_handler.rb +++ b/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 \ No newline at end of file diff --git a/lib/serve/handlers/dynamic_handler.rb b/lib/serve/handlers/dynamic_handler.rb index c409f03..2ad110d 100644 --- a/lib/serve/handlers/dynamic_handler.rb +++ b/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,7 +41,7 @@ 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? @@ -89,23 +49,9 @@ def parse_file(filename, locals={}) 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 diff --git a/lib/serve/handlers/file_type_handler.rb b/lib/serve/handlers/file_type_handler.rb index 91a9d00..66c3ff5 100644 --- a/lib/serve/handlers/file_type_handler.rb +++ b/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 \ No newline at end of file diff --git a/lib/serve/handlers/less_handler.rb b/lib/serve/handlers/less_handler.rb index e15e7e6..b280340 100644 --- a/lib/serve/handlers/less_handler.rb +++ b/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 diff --git a/lib/serve/handlers/redirect_handler.rb b/lib/serve/handlers/redirect_handler.rb index 0278653..94679b6 100644 --- a/lib/serve/handlers/redirect_handler.rb +++ b/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 \ No newline at end of file diff --git a/lib/serve/handlers/sass_handler.rb b/lib/serve/handlers/sass_handler.rb index 18ad6f2..3f25339 100644 --- a/lib/serve/handlers/sass_handler.rb +++ b/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 diff --git a/lib/serve/pipeline.rb b/lib/serve/pipeline.rb new file mode 100644 index 0000000..ef23945 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/lib/serve/rack.rb b/lib/serve/rack.rb index a462de0..2468fa4 100644 --- a/lib/serve/rack.rb +++ b/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. diff --git a/lib/serve/view_helpers.rb b/lib/serve/view_helpers.rb index 280f6f5..4d634eb 100644 --- a/lib/serve/view_helpers.rb +++ b/lib/serve/view_helpers.rb @@ -137,14 +137,14 @@ 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 @@ -152,12 +152,11 @@ def render_template(template, options={}) 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) diff --git a/spec/fixtures/directory/coffee.coffee b/spec/fixtures/directory/coffee.coffee new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/directory/markdown.html.markdown b/spec/fixtures/directory/markdown.html.markdown new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/directory/markdown.markdown b/spec/fixtures/directory/markdown.markdown new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/directory/markdown_erb.markdown.erb b/spec/fixtures/directory/markdown_erb.markdown.erb new file mode 100644 index 0000000..e69de29 diff --git a/spec/pipeline_spec.rb b/spec/pipeline_spec.rb new file mode 100644 index 0000000..904df48 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test_project/markdown.erb/_footer.html.markdown.erb b/test_project/markdown.erb/_footer.html.markdown.erb new file mode 100644 index 0000000..03cb0d8 --- /dev/null +++ b/test_project/markdown.erb/_footer.html.markdown.erb @@ -0,0 +1,2 @@ +
+

Copyright © John W. Long. All rights reserved.

\ No newline at end of file diff --git a/test_project/markdown.erb/_layout.html.erb b/test_project/markdown.erb/_layout.html.erb new file mode 100644 index 0000000..c830a4c --- /dev/null +++ b/test_project/markdown.erb/_layout.html.erb @@ -0,0 +1,29 @@ + + + + <%= @title %> + + + + +
+

<%= @title %>

+ <%= yield %> +
+ <% if content_for?(:sidebar) %> + + <% end %> + <%= render :partial => "footer" %> + + \ No newline at end of file diff --git a/test_project/markdown.erb/index.html.markdown.erb b/test_project/markdown.erb/index.html.markdown.erb new file mode 100644 index 0000000..6a4dfc5 --- /dev/null +++ b/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 %>