serve 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0
data/bin/serve CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
+
2
3
  lib = File.dirname(__FILE__) + '/../lib'
3
- require 'rubygems'
4
4
 
5
- gem 'activesupport'
6
- require 'active_support/all'
5
+ require 'rubygems'
7
6
 
8
7
  if File.file?(lib + '/serve/version.rb')
9
8
  $LOAD_PATH << lib
10
9
  else
11
10
  gem 'serve'
12
11
  end
12
+
13
13
  require 'serve'
14
14
  require 'serve/application'
15
15
 
data/lib/serve.rb CHANGED
@@ -8,14 +8,11 @@ module Kernel
8
8
  end
9
9
 
10
10
  require 'serve/version'
11
- require 'serve/file_resolver'
11
+ require 'serve/router'
12
12
  require 'serve/handlers/file_type_handler'
13
- require 'serve/handlers/textile_handler'
14
- require 'serve/handlers/markdown_handler'
15
13
  require 'serve/handlers/dynamic_handler'
16
14
  require 'serve/handlers/sass_handler'
15
+ require 'serve/handlers/less_handler'
17
16
  require 'serve/handlers/email_handler'
18
17
  require 'serve/handlers/redirect_handler'
19
- require 'serve/handlers/static_handler'
20
- require 'serve/response_cache'
21
18
  require 'serve/rack'
@@ -78,8 +78,9 @@ module Serve
78
78
  " #{program} command [arguments] [options]",
79
79
  " ",
80
80
  "Examples:",
81
- " #{program} # start server on port 4000",
82
- " #{program} 2100 # start server on port 2100",
81
+ " #{program} # start Serve in current directory",
82
+ " #{program} 2100 # start Serve on port 2100",
83
+ " #{program} /path/to/project # start Serve for a specific directory",
83
84
  " #{program} create mockups # create a Serve project in mockups directory",
84
85
  " #{program} convert mockups # convert a Compass project in mockups",
85
86
  " ",
@@ -93,9 +94,9 @@ module Serve
93
94
  " ",
94
95
  " http://localhost:4000/",
95
96
  " ",
96
- " If the haml, redcloth, or bluecloth gems are installed the command can handle ",
97
- " Haml, Sass, SCSS, Textile, and Markdown for documents with haml, sass, scss, ",
98
- " textile, and markdown file extensions.",
97
+ " If the haml, slim, redcloth, or bluecloth gems are installed the command can ",
98
+ " handle Haml, Slim, Sass, SCSS, Textile, and Markdown for documents with haml, ",
99
+ " slim, sass, scss, textile, and markdown file extensions.",
99
100
  " ",
100
101
  " If the Rails command script/server exists in the current directory the ",
101
102
  " script will start that instead.",
@@ -216,7 +217,7 @@ module Serve
216
217
  end
217
218
 
218
219
  def run_rack_app
219
- system "rackup -p #{options[:port]} -o #{options[:address]} -E #{options[:environment]} #{rack_config}"
220
+ system "rackup -p #{options[:port]} -o #{options[:address]} -E #{options[:environment]} '#{rack_config}'"
220
221
  end
221
222
 
222
223
  def run_server
@@ -1,8 +1,19 @@
1
1
  require 'serve/view_helpers'
2
+ require 'tilt'
2
3
 
3
4
  module Serve #:nodoc:
4
5
  class DynamicHandler < FileTypeHandler #:nodoc:
5
- extension 'erb', 'html.erb', 'rhtml', 'haml', 'html.haml'
6
+
7
+ def self.extensions
8
+ # Get extensions from Tilt, ugly but it works
9
+ @extensions ||= (Tilt.mappings.map { |k,v| ["#{k}", "html.#{k}"] } << ["slim", "html.slim"]).flatten
10
+ end
11
+
12
+ def extensions
13
+ self.class.extensions
14
+ end
15
+
16
+ extension *extensions
6
17
 
7
18
  def process(request, response)
8
19
  response.headers['content-type'] = content_type
@@ -28,7 +39,8 @@ module Serve #:nodoc:
28
39
  layout = nil
29
40
  until layout or path == "/"
30
41
  path = File.dirname(path)
31
- possible_layouts = ['_layout.haml', '_layout.html.haml', '_layout.erb', '_layout.html.erb'].map do |l|
42
+ possible_layouts = extensions.map do |ext|
43
+ l = "_layout.#{ext}"
32
44
  possible_layout = File.join(root, path, l)
33
45
  File.file?(possible_layout) ? possible_layout : false
34
46
  end
@@ -44,54 +56,44 @@ module Serve #:nodoc:
44
56
  end
45
57
  end
46
58
 
47
- module ERB #:nodoc:
48
- class Engine #:nodoc:
49
- def initialize(string, options = {})
50
- @erb = ::ERB.new(string, nil, '-', '@erbout')
51
- @erb.filename = options[:filename]
52
- end
53
-
54
- def render(context, &block)
55
- # we have to keep track of the old erbout variable for nested renders
56
- # because ERB#result will set it to an empty string before it renders
57
- old_erbout = context.instance_variable_get('@erbout')
58
- result = @erb.result(context.instance_eval { binding })
59
- context.instance_variable_set('@erbout', old_erbout)
60
- result
61
- end
62
- end
63
- end
64
-
65
59
  class Parser #:nodoc:
66
- attr_accessor :context, :script_filename
60
+ attr_accessor :context, :script_filename, :script_extension, :engine
67
61
 
68
62
  def initialize(context)
69
63
  @context = context
70
64
  @context.parser = self
71
65
  end
72
66
 
73
- def parse_file(filename)
74
- old_script_filename = @script_filename
67
+ def parse_file(filename, locals={})
68
+ old_script_filename, old_script_extension, old_engine = @script_filename, @script_extension, @engine
69
+
75
70
  @script_filename = filename
76
- lines = IO.read(filename)
77
- engine = case File.extname(filename).sub(/^./, '').downcase
78
- when 'haml'
79
- require 'haml'
80
- require 'sass'
81
- require 'sass/plugin'
82
- Haml::Engine.new(lines, :attr_wrapper => '"', :filename => filename)
83
- when 'erb'
84
- require 'erb'
85
- ERB::Engine.new(lines, :filename => filename)
86
- else
87
- raise 'extension not supported'
71
+
72
+ ext = File.extname(filename).sub(/^\.html\.|^\./, '').downcase
73
+
74
+ if ext == 'slim' # Ugly, but works
75
+ if Thread.list.size > 1
76
+ warn "WARN: serve autoloading 'slim' in a non thread-safe way; " +
77
+ "explicit require 'slim' suggested."
78
+ end
79
+ require 'slim'
88
80
  end
89
- result = engine.render(context) do |*args|
81
+
82
+ @script_extension = ext
83
+
84
+ @engine = Tilt[ext].new(filename, nil, :outvar => '@_out_buf')
85
+
86
+ raise "#{ext} extension not supported" if @engine.nil?
87
+
88
+ @engine.render(context, locals) do |*args|
90
89
  context.get_content_for(*args)
91
90
  end
91
+ ensure
92
92
  @script_filename = old_script_filename
93
- result
93
+ @script_extension = old_script_extension
94
+ @engine = old_engine
94
95
  end
96
+
95
97
  end
96
98
 
97
99
  class Context #:nodoc:
@@ -103,11 +105,7 @@ module Serve #:nodoc:
103
105
  @content = ''
104
106
  end
105
107
 
106
- def _erbout
107
- @erbout
108
- end
109
-
110
108
  include Serve::ViewHelpers
111
109
  end
112
110
  end
113
- end
111
+ end
@@ -0,0 +1,19 @@
1
+ module Serve #:nodoc:
2
+
3
+ # TODO: Figure out how to remove the Less Handler in favor of Tilt
4
+ # The Less handler seems to be necessary to keep Tilt from applying a layout
5
+ # to Less files. Any one know how to turn this Tilt feature off?
6
+
7
+ class LessHandler < FileTypeHandler #:nodoc:
8
+ extension 'less'
9
+
10
+ def parse(string)
11
+ require 'less'
12
+ Less.parse(string)
13
+ end
14
+
15
+ def content_type
16
+ 'text/css'
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,9 @@
1
1
  module Serve #:nodoc:
2
+
3
+ # TODO: Figure out how to remove the Sass Handler in favor of Tilt
4
+ # The Sass handler seems to be necessary to keep Tilt from applying a layout
5
+ # to Sass files. Any one know how to turn this Tilt feature off?
6
+
2
7
  class SassHandler < FileTypeHandler #:nodoc:
3
8
  extension 'sass', 'scss'
4
9
 
data/lib/serve/out.rb CHANGED
@@ -3,6 +3,13 @@ module Serve #:nodoc:
3
3
  # Utility methods for handling output to the terminal
4
4
  module Out #:nodoc:
5
5
 
6
+ COLORS = {
7
+ :clear => 0,
8
+ :red => 31,
9
+ :green => 32,
10
+ :yellow => 33
11
+ }
12
+
6
13
  def stdout
7
14
  @stdout ||= $stdout
8
15
  end
@@ -24,7 +31,7 @@ module Serve #:nodoc:
24
31
  end
25
32
 
26
33
  def print(*args)
27
- stderr.print(*args)
34
+ stderr.print(color_msg(:green, *args))
28
35
  end
29
36
 
30
37
  def log_action(name, message)
@@ -34,5 +41,16 @@ module Serve #:nodoc:
34
41
  puts message
35
42
  end
36
43
 
44
+ def color_msg(pigment, *args)
45
+ msg = ''
46
+ msg << color(pigment)
47
+ msg << "#{args.join(' ')}"
48
+ msg << color(:clear)
49
+ msg
50
+ end
51
+
52
+ def color(pigment)
53
+ "\e[#{COLORS[pigment.to_sym]}m"
54
+ end
37
55
  end
38
56
  end
data/lib/serve/project.rb CHANGED
@@ -64,7 +64,7 @@ module Serve #:nodoc:
64
64
  views
65
65
  ).each { |path| make_path path }
66
66
  create_file 'config.ru', read_template('config.ru')
67
- create_file 'LICENSE', read_template('LICENSE')
67
+ create_file 'LICENSE', read_template('license')
68
68
  create_file '.gitignore', read_template('gitignore')
69
69
  create_file 'compass.config', read_template('compass.config')
70
70
  create_file 'README.markdown', read_template('README.markdown')
@@ -173,4 +173,4 @@ module Serve #:nodoc:
173
173
  end
174
174
 
175
175
  end
176
- end
176
+ end
data/lib/serve/rack.rb CHANGED
@@ -3,20 +3,35 @@ require 'rack'
3
3
 
4
4
  module Serve
5
5
  class Request < Rack::Request
6
+ # Returns a Hash of the query params for a request's URL.
6
7
  def query
7
8
  @query ||= Rack::Utils.parse_nested_query(query_string)
8
9
  end
9
10
 
11
+ # Returns the protocol part of the URL for a request: "http://", "https://", etc.
10
12
  def protocol
11
13
  @scheme ||= scheme + "://"
12
14
  end
13
15
 
16
+ # Returns a specialized hash of the headers on the request.
14
17
  def headers
15
18
  @headers ||= Headers.new(@env)
16
19
  end
20
+
21
+ # Returns the value of the "REQUEST_URI" environment variable.
22
+ def request_uri
23
+ @env["REQUEST_URI"].to_s
24
+ end
25
+
26
+ # Set the value of the "REQUEST_URI" environment variable.
27
+ def request_uri=(s)
28
+ @env["REQUEST_URI"] = s.to_s
29
+ end
17
30
  end
18
31
 
19
32
  class Response < Rack::Response
33
+
34
+ # Set the body of a response.
20
35
  def body=(value)
21
36
  # needed for Ruby 1.9
22
37
  if value.respond_to? :each
@@ -25,9 +40,11 @@ module Serve
25
40
  super([value])
26
41
  end
27
42
  end
43
+
28
44
  end
29
45
 
30
- # Borrowed from ActionDispatch in Rails
46
+ # A specialized hash for the environment variables on a request.
47
+ # Borrowed from ActionDispatch in Rails.
31
48
  class Headers < Hash
32
49
  extend ActiveSupport::Memoizable
33
50
 
@@ -57,36 +74,59 @@ module Serve
57
74
  end
58
75
 
59
76
  class RackAdapter
77
+
78
+ # Initialize a Rack endpoint for Serve with the root path to
79
+ # the views directory.
60
80
  def initialize(root)
61
81
  @root = root
62
82
  end
63
83
 
84
+ # Called by Rack to process a request.
64
85
  def call(env)
65
86
  request = Request.new(env)
66
87
  response = Response.new()
67
88
  process(request, response).to_a
68
89
  end
69
90
 
70
- def process(request, response)
71
- path = Serve.resolve_file(@root, request.path)
72
- if path
73
- ext = File.extname(path)[1..-1]
74
- handler = Serve::FileTypeHandler.handlers[ext]
75
- if handler
76
- handler.new(@root, path).process(request, response)
77
- response
91
+ protected
92
+
93
+ # Process the request and response. Paths are transformed so that
94
+ # URLs without extensions and directory indexes work.
95
+ def process(request, response)
96
+ path = Serve::Router.resolve(@root, request.path_info)
97
+ if path
98
+ # Fetch the file handler for a file with a given extension/
99
+ ext = File.extname(path)[1..-1]
100
+ handler = Serve::FileTypeHandler.handlers[ext]
101
+ if handler
102
+ # Handler exists? Process the request and response.
103
+ handler.new(@root, path).process(request, response)
104
+ response
105
+ else
106
+ # Handler doesn't exist? Rewrite the request to use the new path.
107
+ # This allows Rack::Cascade or Passenger to deliver a file that is
108
+ # not handled by one of the Serve handlers.
109
+ rewrite(request, response, path)
110
+ end
78
111
  else
79
- default(request, response)
112
+ # Return a 404 response.
113
+ not_found(request, response)
80
114
  end
81
- else
82
- default(request, response)
83
115
  end
84
- end
116
+
117
+ # Returns a 404 response.
118
+ def not_found(request, response)
119
+ response.status = 404
120
+ response.body = "Not found!"
121
+ response
122
+ end
123
+
124
+ # Rewrite the request to use a new path. Return a 404 response so that Rack::Cascade works.
125
+ def rewrite(request, response, path)
126
+ request.request_uri = path + request.query_string
127
+ request.path_info = path
128
+ not_found(request, response)
129
+ end
85
130
 
86
- def default(request, response)
87
- response.status = 404
88
- response.body = "Not found!"
89
- response
90
- end
91
131
  end
92
132
  end
@@ -0,0 +1,43 @@
1
+ module Serve
2
+ module Router
3
+
4
+ # Resolve a path to a valid file name in root path. Return nil if no
5
+ # file exists for that path.
6
+ def self.resolve(root, path)
7
+ path = normalize_path(path)
8
+
9
+ return if path.nil? # If it's not a valid path, return nothing.
10
+
11
+ full_path = File.join(root, path)
12
+
13
+ case
14
+ when File.file?(full_path)
15
+ # A file exists! Return the matching path.
16
+ path
17
+ when File.directory?(full_path)
18
+ # It's a directory? Try a directory index.
19
+ resolve(root, File.join(path, 'index'))
20
+ when path.ends_with?('.css')
21
+ # CSS not found? Try SCSS or Sass.
22
+ alternates = %w{.scss .sass}.map { |ext| path.sub(/\.css\Z/, ext) }
23
+ sass_path = alternates.find do |p|
24
+ File.file?(File.join(root, p))
25
+ end
26
+ else
27
+ # Still no luck? Check to see if a file with an extension exists by that name.
28
+ # TODO: Return a path with an extension based on priority, not just the first found.
29
+ result = Dir.glob(full_path + ".*", File::FNM_CASEFOLD).first
30
+ result.sub(/^#{root}/, '').sub(/^\//, '') if result && File.file?(result)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def self.normalize_path(path)
37
+ path = File.join(path) # path may be array
38
+ path = path.sub(%r{/\Z}, '') # remove trailing slash
39
+ path unless path =~ /\.\./ # guard against evil paths
40
+ end
41
+
42
+ end
43
+ end