serve 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +18 -18
- data/Gemfile.lock +64 -61
- data/README.rdoc +14 -2
- data/VERSION +1 -1
- data/lib/serve.rb +2 -0
- data/lib/serve/application.rb +1 -1
- data/lib/serve/bootstrap/Gemfile +4 -1
- data/lib/serve/bootstrap/README.md +2 -7
- data/lib/serve/bootstrap/config.ru +6 -2
- data/lib/serve/handlers/coffee_handler.rb +18 -0
- data/lib/serve/handlers/dynamic_handler.rb +11 -65
- data/lib/serve/handlers/file_type_handler.rb +34 -14
- data/lib/serve/handlers/less_handler.rb +1 -1
- data/lib/serve/handlers/redirect_handler.rb +9 -5
- data/lib/serve/handlers/sass_handler.rb +6 -8
- data/lib/serve/pipeline.rb +119 -0
- data/lib/serve/rack.rb +3 -8
- data/lib/serve/router.rb +12 -8
- data/lib/serve/templates/default/views/view_helpers.rb +2 -2
- data/lib/serve/view_helpers.rb +14 -12
- data/spec/fixtures/directory/_layout.erb +3 -0
- data/spec/fixtures/directory/coffee.coffee +0 -0
- data/spec/fixtures/directory/markdown.html.markdown +0 -0
- data/spec/fixtures/directory/markdown.markdown +0 -0
- data/spec/fixtures/directory/markdown_erb.markdown.erb +0 -0
- data/spec/fixtures/directory/subdirectory/test.erb +1 -0
- data/spec/fixtures/file.erb +1 -0
- data/spec/pipeline_spec.rb +67 -0
- data/spec/router_spec.rb +4 -2
- data/spec/views/view_helpers.rb +7 -7
- metadata +259 -152
@@ -9,29 +9,49 @@ module Serve #:nodoc:
|
|
9
9
|
FileTypeHandler.handlers[ext] = self
|
10
10
|
end
|
11
11
|
end
|
12
|
-
|
13
|
-
def self.
|
14
|
-
|
15
|
-
handlers[ext.sub(/\A\./, '')]
|
16
|
-
end
|
12
|
+
|
13
|
+
def self.extensions
|
14
|
+
handlers.keys
|
17
15
|
end
|
18
|
-
|
19
|
-
def
|
16
|
+
|
17
|
+
def self.handlers_for(path)
|
18
|
+
extensions = File.basename(path).split(".")[1..-1]
|
19
|
+
extensions.collect{|e| [handlers[e], e] if handlers[e]}.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure(extension, options)
|
23
|
+
extension_options[extension.to_sym].merge!(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.extension_options
|
27
|
+
@extension_options ||= Hash.new{|h,k| h[k] = {}}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.options_for(extension)
|
31
|
+
extension_options[extension.to_sym]
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :extension
|
35
|
+
def initialize(root_path, template_path, extension)
|
20
36
|
@root_path = root_path
|
21
|
-
@
|
37
|
+
@template_path = template_path
|
38
|
+
@extension = extension
|
22
39
|
end
|
23
40
|
|
24
|
-
def process(
|
25
|
-
|
26
|
-
response.body = parse(open(@script_filename){|io| io.read })
|
41
|
+
def process(input, context)
|
42
|
+
parse(input, context)
|
27
43
|
end
|
28
44
|
|
29
45
|
def content_type
|
30
46
|
'text/html'
|
31
47
|
end
|
48
|
+
|
49
|
+
def layout?
|
50
|
+
true
|
51
|
+
end
|
32
52
|
|
33
|
-
def parse(
|
34
|
-
|
53
|
+
def parse(input, context)
|
54
|
+
input.dup
|
35
55
|
end
|
36
56
|
end
|
37
|
-
end
|
57
|
+
end
|
@@ -2,13 +2,17 @@ module Serve #:nodoc:
|
|
2
2
|
class RedirectHandler < FileTypeHandler #:nodoc:
|
3
3
|
extension 'redirect'
|
4
4
|
|
5
|
-
def process(
|
6
|
-
lines =
|
5
|
+
def process(input, context)
|
6
|
+
lines = input.strip.split("\n")
|
7
7
|
url = lines.last.strip
|
8
|
-
unless url =~ %r{^\w[\w
|
9
|
-
url = request.protocol + request.host_with_port + url
|
8
|
+
unless url =~ %r{^\w[\w+.-]*:.*}
|
9
|
+
url = context.request.protocol + context.request.host_with_port + url
|
10
10
|
end
|
11
|
-
response.redirect(url, '302')
|
11
|
+
context.response.redirect(url, '302')
|
12
|
+
end
|
13
|
+
|
14
|
+
def layout?
|
15
|
+
false
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|
@@ -7,20 +7,18 @@ module Serve #:nodoc:
|
|
7
7
|
class SassHandler < FileTypeHandler #:nodoc:
|
8
8
|
extension 'sass', 'scss'
|
9
9
|
|
10
|
-
def parse(string)
|
10
|
+
def parse(string, context)
|
11
11
|
require 'sass'
|
12
12
|
engine = Sass::Engine.new(string,
|
13
|
-
:load_paths => [@root_path],
|
13
|
+
:load_paths => [@root_path] + Sass::Engine::DEFAULT_OPTIONS[:load_paths],
|
14
14
|
:style => :expanded,
|
15
|
-
:
|
16
|
-
:syntax => syntax(@script_filename)
|
15
|
+
:syntax => syntax
|
17
16
|
)
|
18
17
|
engine.render
|
19
18
|
end
|
20
19
|
|
21
|
-
def syntax
|
22
|
-
|
23
|
-
if ext == '.scss'
|
20
|
+
def syntax
|
21
|
+
if extension == 'scss'
|
24
22
|
:scss
|
25
23
|
else
|
26
24
|
:sass
|
@@ -31,4 +29,4 @@ module Serve #:nodoc:
|
|
31
29
|
'text/css'
|
32
30
|
end
|
33
31
|
end
|
34
|
-
end
|
32
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'serve/view_helpers'
|
2
|
+
|
3
|
+
module Serve
|
4
|
+
class Pipeline
|
5
|
+
def self.handles?(path)
|
6
|
+
!FileTypeHandler.handlers_for(path).empty?
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.build(root, path)
|
10
|
+
return nil unless handles?(path)
|
11
|
+
Pipeline.new(root, path, extensions_for(path))
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :template, :layout
|
15
|
+
def initialize(root_path, path)
|
16
|
+
@root_path = root_path
|
17
|
+
@template = Template.new(File.join(@root_path, path))
|
18
|
+
@layout = find_layout_for(@template.path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_layout_for(template_path)
|
22
|
+
return Template::Passthrough.new(@template) unless @template.layout?
|
23
|
+
root = @root_path
|
24
|
+
layout = nil
|
25
|
+
search = File.split(template_path[root.size..-1])
|
26
|
+
until(layout || search.empty?)
|
27
|
+
possible_layouts = FileTypeHandler.extensions.map do |ext|
|
28
|
+
l = "_layout.#{ext}"
|
29
|
+
possible_layout = File.join(File.join(root, *search), l)
|
30
|
+
File.file?(possible_layout) ? possible_layout : false
|
31
|
+
end
|
32
|
+
layout = possible_layouts.detect { |o| o }
|
33
|
+
search.pop
|
34
|
+
end
|
35
|
+
if layout
|
36
|
+
Template.new(layout)
|
37
|
+
else
|
38
|
+
Template::Passthrough.new(@template)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def process(request, response)
|
43
|
+
response.headers['Content-Type'] = @layout.content_type
|
44
|
+
context = Context.new(@root_path, request, response)
|
45
|
+
@template.process(context)
|
46
|
+
@layout.process(context)
|
47
|
+
response.body = context.content
|
48
|
+
end
|
49
|
+
|
50
|
+
class Template
|
51
|
+
attr_reader :file, :path, :handlers
|
52
|
+
def initialize(file)
|
53
|
+
@file = File.basename(file)
|
54
|
+
@path = File.dirname(file)
|
55
|
+
@raw = File.read(file)
|
56
|
+
@handlers = FileTypeHandler.handlers_for(file).collect{|h, extension| h.new(@root_path, @path, extension)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def content_type
|
60
|
+
@handlers.first.content_type
|
61
|
+
end
|
62
|
+
|
63
|
+
def process(context)
|
64
|
+
context.content = @handlers.reverse.inject(@raw.dup) do |body, handler|
|
65
|
+
handler.process(body, context)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def layout?
|
70
|
+
@handlers.first.layout?
|
71
|
+
end
|
72
|
+
|
73
|
+
def passthrough?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
class Passthrough
|
78
|
+
def initialize(template)
|
79
|
+
@template = template
|
80
|
+
end
|
81
|
+
|
82
|
+
def process(context)
|
83
|
+
end
|
84
|
+
|
85
|
+
def layout?
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
def content_type
|
90
|
+
@template.content_type
|
91
|
+
end
|
92
|
+
|
93
|
+
def passthrough?
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Context #:nodoc:
|
100
|
+
attr_accessor :content, :parser
|
101
|
+
attr_reader :request, :response
|
102
|
+
|
103
|
+
def initialize(root_path, request, response)
|
104
|
+
@root_path, @request, @response = root_path, request, response
|
105
|
+
@content = ''
|
106
|
+
install_view_helpers
|
107
|
+
end
|
108
|
+
|
109
|
+
def install_view_helpers
|
110
|
+
view_helpers_file_path = @root_path + '/view_helpers.rb'
|
111
|
+
if File.file?(view_helpers_file_path)
|
112
|
+
singleton_class.module_eval(File.read(view_helpers_file_path) + "\ninclude ViewHelpers", view_helpers_file_path)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
include Serve::ViewHelpers
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/serve/rack.rb
CHANGED
@@ -61,8 +61,6 @@ module Serve
|
|
61
61
|
# A specialized hash for the environment variables on a request.
|
62
62
|
# Borrowed from ActionDispatch in Rails.
|
63
63
|
class Headers < Hash
|
64
|
-
extend ActiveSupport::Memoizable
|
65
|
-
|
66
64
|
def initialize(*args)
|
67
65
|
if args.size == 1 && args[0].is_a?(Hash)
|
68
66
|
super()
|
@@ -83,9 +81,8 @@ module Serve
|
|
83
81
|
private
|
84
82
|
# Converts a HTTP header name to an environment variable name.
|
85
83
|
def env_name(header_name)
|
86
|
-
"HTTP_#{header_name.upcase.gsub(/-/, '_')}"
|
84
|
+
@env_name ||= "HTTP_#{header_name.upcase.gsub(/-/, '_')}"
|
87
85
|
end
|
88
|
-
memoize :env_name
|
89
86
|
end
|
90
87
|
|
91
88
|
class RackAdapter
|
@@ -111,11 +108,9 @@ module Serve
|
|
111
108
|
path = Serve::Router.resolve(@root, request.path_info)
|
112
109
|
if path
|
113
110
|
# Fetch the file handler for a file with a given extension/
|
114
|
-
|
115
|
-
handler = Serve::FileTypeHandler.handlers[ext]
|
116
|
-
if handler
|
111
|
+
if Serve::Pipeline.handles?(path)
|
117
112
|
# Handler exists? Process the request and response.
|
118
|
-
|
113
|
+
Serve::Pipeline.new(@root, path).process(request, response)
|
119
114
|
response
|
120
115
|
else
|
121
116
|
# Handler doesn't exist? Rewrite the request to use the new path.
|
data/lib/serve/router.rb
CHANGED
@@ -11,23 +11,23 @@ module Serve
|
|
11
11
|
full_path = File.join(root, path)
|
12
12
|
|
13
13
|
case
|
14
|
-
when File.file?(full_path)
|
14
|
+
when File.file?(best_match(full_path))
|
15
15
|
# A file exists! Return the matching path.
|
16
16
|
path
|
17
|
-
when File.directory?(full_path)
|
17
|
+
when File.directory?(best_match(full_path))
|
18
18
|
# It's a directory? Try a directory index.
|
19
19
|
resolve(root, File.join(path, 'index'))
|
20
|
-
when path
|
20
|
+
when path =~ /\.css\Z/i
|
21
21
|
# CSS not found? Try SCSS or Sass.
|
22
22
|
alternates = %w{.scss .sass}.map { |ext| path.sub(/\.css\Z/, ext) }
|
23
|
-
|
24
|
-
File.file?(File.join(root, p))
|
25
|
-
end
|
23
|
+
alternates.find do |p|
|
24
|
+
File.file?(best_match(File.join(root, p)))
|
25
|
+
end
|
26
26
|
else
|
27
27
|
# Still no luck? Check to see if a file with an extension exists by that name.
|
28
28
|
# TODO: Return a path with an extension based on priority, not just the first found.
|
29
|
-
result =
|
30
|
-
result.sub(/^#{root}
|
29
|
+
result = best_match(full_path + ".*")
|
30
|
+
result.sub(/^#{root}/i, '').sub(/^\//, '') if result && File.file?(result)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -38,6 +38,10 @@ module Serve
|
|
38
38
|
path = path.sub(%r{/\Z}, '') # remove trailing slash
|
39
39
|
path unless path =~ /\.\./ # guard against evil paths
|
40
40
|
end
|
41
|
+
|
42
|
+
def self.best_match(path)
|
43
|
+
(Dir.glob(path, File::FNM_CASEFOLD).first || path)
|
44
|
+
end
|
41
45
|
|
42
46
|
end
|
43
47
|
end
|
data/lib/serve/view_helpers.rb
CHANGED
@@ -35,8 +35,10 @@ module Serve #:nodoc:
|
|
35
35
|
end
|
36
36
|
|
37
37
|
module ContentHelpers
|
38
|
-
def content_for(symbol, &block)
|
39
|
-
|
38
|
+
def content_for(symbol, content = nil, &block)
|
39
|
+
content = capture(&block) if block_given?
|
40
|
+
set_content_for(symbol, content) if content
|
41
|
+
get_content_for(symbol) unless content
|
40
42
|
end
|
41
43
|
|
42
44
|
def content_for?(symbol)
|
@@ -135,14 +137,14 @@ module Serve #:nodoc:
|
|
135
137
|
end
|
136
138
|
|
137
139
|
def render_template(template, options={})
|
138
|
-
path =
|
140
|
+
path = parser.template_path
|
139
141
|
if template =~ %r{^/}
|
140
142
|
template = template[1..-1]
|
141
143
|
path = @root_path
|
142
144
|
end
|
143
|
-
filename = template_filename(
|
144
|
-
if File.file?(filename)
|
145
|
-
parser.
|
145
|
+
filename = template_filename(path, template, :partial => options[:partial])
|
146
|
+
if filename && File.file?(filename)
|
147
|
+
parser.parse(File.read(filename), File.extname(filename).split(".").last, options[:locals])
|
146
148
|
else
|
147
149
|
raise "File does not exist #{filename.inspect}"
|
148
150
|
end
|
@@ -150,12 +152,12 @@ module Serve #:nodoc:
|
|
150
152
|
|
151
153
|
private
|
152
154
|
|
153
|
-
def template_filename(
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
File.join(path,
|
155
|
+
def template_filename(path, template, options)
|
156
|
+
template_path = File.dirname(template)
|
157
|
+
template_file = File.basename(template)
|
158
|
+
template_file = "_" + template_file if options[:partial]
|
159
|
+
route = Serve::Router.resolve(path, File.join(template_path, template_file))
|
160
|
+
(route && File.join(path, route))
|
159
161
|
end
|
160
162
|
|
161
163
|
def extname(filename)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Test
|
@@ -0,0 +1 @@
|
|
1
|
+
Test
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
require 'serve/pipeline'
|
3
|
+
|
4
|
+
describe Serve::Pipeline do
|
5
|
+
before :each do
|
6
|
+
@root = File.expand_path("../fixtures", __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "self.handles?" do
|
10
|
+
it "should not handle .html" do
|
11
|
+
Serve::Pipeline.handles?("dir/file.html").should be_false
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should handle .markdown" do
|
15
|
+
Serve::Pipeline.handles?("dir/file.markdown").should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should handle .html.markdown" do
|
19
|
+
Serve::Pipeline.handles?("dir/file.html.markdown").should be_true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should handle .coffee" do
|
23
|
+
Serve::Pipeline.handles?("dir/file.coffee").should be_true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should handle .markdown.erb" do
|
27
|
+
Serve::Pipeline.handles?("dir/file.markdown.erb").should be_true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "initialize" do
|
32
|
+
it "should build a pipeline for .markdown" do
|
33
|
+
pipeline = Serve::Pipeline.new(@root, "directory/markdown.markdown")
|
34
|
+
pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should build a pipeline for .html.markdown" do
|
38
|
+
pipeline = Serve::Pipeline.new(@root, "directory/markdown.html.markdown")
|
39
|
+
pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should build a pipeline for .coffee" do
|
43
|
+
pipeline = Serve::Pipeline.new(@root, "directory/coffee.coffee")
|
44
|
+
pipeline.template.handlers.collect{|h| h.extension}.should == %w(coffee)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should build a pipeline for .markdown.erb" do
|
48
|
+
pipeline = Serve::Pipeline.new(@root, "directory/markdown_erb.markdown.erb")
|
49
|
+
pipeline.template.handlers.collect{|h| h.extension}.should == %w(markdown erb)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "layout search" do
|
54
|
+
it "should return no layout for a file at the root" do
|
55
|
+
pipeline = Serve::Pipeline.new(@root, "file.erb")
|
56
|
+
pipeline.layout.should be_passthrough
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should search up the directory tree for layouts" do
|
60
|
+
root = File.join(@root, "directory")
|
61
|
+
pipeline = Serve::Pipeline.new(root, "subdirectory/test.erb")
|
62
|
+
pipeline.layout.should_not be_passthrough
|
63
|
+
pipeline.layout.path.should == root
|
64
|
+
pipeline.layout.file.should == "_layout.erb"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|