middleman/middleman-core/lib/middleman-core/application.rb

561 lines
15 KiB
Ruby
Raw Normal View History

2011-11-24 06:59:53 +01:00
# Built on Rack
2011-11-18 04:56:55 +01:00
require "rack"
2011-12-25 01:09:07 +01:00
require "rack/file"
2011-11-24 06:59:53 +01:00
# Using Tilt for templating
2011-11-18 04:56:55 +01:00
require "tilt"
2011-11-24 06:59:53 +01:00
# Use ActiveSupport JSON
require "active_support/json"
# Simple callback library
require "middleman-core/vendor/hooks-0.2.0/lib/hooks"
# Using a cache
require "middleman-core/cache"
require "middleman-core/sitemap"
2011-12-09 19:25:51 +01:00
2011-11-24 06:59:53 +01:00
# Core Middleman Class
module Middleman
class Application
# Uses callbacks
include Hooks
2011-11-24 06:59:53 +01:00
# Before request hook
define_hook :before
2011-11-24 06:59:53 +01:00
# Ready (all loading and parsing of extensions complete) hook
define_hook :ready
2011-11-24 06:59:53 +01:00
class << self
2011-11-24 06:59:53 +01:00
# Reset Rack setup
#
# @private
def reset!
@app = nil
@prototype = nil
end
# The shared Rack instance being build
#
# @private
# @return [Rack::Builder]
def app
@app ||= Rack::Builder.new
end
2011-11-18 04:56:55 +01:00
# Get the static instance
#
# @private
# @return [Middleman::Application]
def inst(&block)
@inst ||= begin
mm = new(&block)
mm.run_hook :ready
mm
end
2011-11-28 07:04:19 +01:00
end
2011-11-18 09:34:56 +01:00
# Set the shared instance
#
# @private
# @param [Middleman::Application] inst
# @return [void]
def inst=(inst)
@inst = inst
end
# Return built Rack app
#
# @private
# @return [Rack::Builder]
def to_rack_app(&block)
inner_app = inst(&block)
(@middleware || []).each do |m|
app.use(m[0], *m[1], &m[2])
end
app.map("/") { run inner_app }
(@mappings || []).each do |m|
app.map(m[0], &m[1])
end
app
end
2011-11-18 09:34:56 +01:00
# Prototype app. Used in config.ru
#
# @private
# @return [Rack::Builder]
def prototype
@prototype ||= to_rack_app
end
# Call prototype, use in config.ru
#
# @private
def call(env)
prototype.call(env)
end
# Use Rack middleware
#
# @param [Class] Middleware
# @return [void]
def use(middleware, *args, &block)
@middleware ||= []
@middleware << [middleware, args, block]
end
# Add Rack App mapped to specific path
#
# @param [String] Path to map
# @return [void]
def map(map, &block)
@mappings ||= []
@mappings << [map, block]
end
2011-11-18 04:56:55 +01:00
# Mix-in helper methods. Accepts either a list of Modules
# and/or a block to be evaluated
# @return [void]
def helpers(*extensions, &block)
class_eval(&block) if block_given?
include(*extensions) if extensions.any?
end
2011-11-18 04:56:55 +01:00
# Access class-wide defaults
#
# @private
# @return [Hash] Hash of default values
def defaults
@defaults ||= {}
end
2011-11-18 04:56:55 +01:00
# Set class-wide defaults
#
# @param [Symbol] Unique key name
# @param Default value
# @return [void]
def set(key, value=nil, &block)
@defaults ||= {}
@defaults[key] = value
@inst.set(key, value, &block) if @inst
end
end
# Set attributes (global variables)
2011-11-24 06:59:53 +01:00
#
# @param [Symbol] Name of the attribue
# @param Attribute value
2011-12-18 05:12:13 +01:00
# @return [void]
def set(key, value=nil, &block)
setter = "#{key}=".to_sym
self.class.send(:attr_accessor, key) if !respond_to?(setter)
value = block if block_given?
send(setter, value)
2011-11-14 06:57:53 +01:00
end
2011-11-18 04:56:55 +01:00
# Root project directory (overwritten in middleman build/server)
# @return [String]
set :root, ENV["MM_ROOT"] || Dir.pwd
2011-11-24 06:59:53 +01:00
# Name of the source directory
# @return [String]
set :source, "source"
2011-11-24 06:59:53 +01:00
# Middleman environment. Defaults to :development, set to :build by the build process
# @return [String]
set :environment, (ENV['MM_ENV'] && ENV['MM_ENV'].to_sym) || :development
2011-11-24 06:59:53 +01:00
# Whether logging is active, disabled by default
# @return [String]
set :logging, false
2011-11-18 04:56:55 +01:00
# Which file should be used for directory indexes
# @return [String]
set :index_file, "index.html"
2011-11-18 04:56:55 +01:00
# Location of javascripts within source. Used by Sprockets.
# @return [String]
set :js_dir, "javascripts"
2011-11-24 06:59:53 +01:00
# Location of stylesheets within source. Used by Compass.
# @return [String]
set :css_dir, "stylesheets"
2011-11-24 06:59:53 +01:00
# Location of images within source. Used by HTML helpers and Compass.
# @return [String]
set :images_dir, "images"
2011-11-18 04:56:55 +01:00
# Where to build output files
# @return [String]
set :build_dir, "build"
2011-11-18 04:56:55 +01:00
# Default prefix for building paths. Used by HTML helpers and Compass.
# @return [String]
set :http_prefix, "/"
2011-11-24 06:59:53 +01:00
# Whether to catch and display exceptions
# @return [Boolean]
set :show_exceptions, true
# Automatically loaded extensions
# @return [Array<Symbol>]
set :default_extensions, [ :lorem ]
2011-11-18 04:56:55 +01:00
# Default layout name
# @return [String, Symbold]
set :layout, :_auto_layout
2011-11-18 04:56:55 +01:00
# Activate custom features and extensions
include Middleman::CoreExtensions::Extensions
# Handle exceptions
register Middleman::CoreExtensions::ShowExceptions
# Add Builder Callbacks
register Middleman::CoreExtensions::Builder
2011-11-18 04:56:55 +01:00
# Add Watcher Callbacks
register Middleman::CoreExtensions::FileWatcher
2011-11-18 04:56:55 +01:00
# Activate Data package
register Middleman::CoreExtensions::Data
2011-11-18 04:56:55 +01:00
# Setup custom rendering
register Middleman::CoreExtensions::Rendering
2011-11-18 04:56:55 +01:00
# Sitemap
register Middleman::Sitemap
# Setup external helpers
register Middleman::CoreExtensions::ExternalHelpers
# Setup default helpers
register Middleman::CoreExtensions::DefaultHelpers
2011-11-18 04:56:55 +01:00
# Setup asset path pipeline
register Middleman::CoreExtensions::Assets
2011-11-18 04:56:55 +01:00
# with_layout and page routing
register Middleman::CoreExtensions::Routing
2011-11-18 04:56:55 +01:00
# Parse YAML from templates
register Middleman::CoreExtensions::FrontMatter
2011-11-18 04:56:55 +01:00
# i18n
register Middleman::CoreExtensions::I18n
2012-03-11 03:57:00 +01:00
# Built-in Extensions
Middleman::Extensions.register(:directory_indexes) {
Middleman::Extensions::DirectoryIndexes }
Middleman::Extensions.register(:lorem) {
Middleman::Extensions::Lorem }
Middleman::Extensions.register(:automatic_image_sizes) {
Middleman::Extensions::AutomaticImageSizes }
Middleman::Extensions.register(:asset_host) {
Middleman::Extensions::AssetHost }
2011-11-24 06:59:53 +01:00
# Backwards-compatibility with old request.path signature
attr :request
2011-12-09 19:25:51 +01:00
# Accessor for current path
# @return [String]
def current_path
@_current_path
end
2011-12-09 19:25:51 +01:00
# Set the current path
#
# @param [String] path The new current path
# @return [void]
def current_path=(path)
@_current_path = path
@request = ::Thor::CoreExt::HashWithIndifferentAccess.new({
:path => path,
:params => req ? ::Thor::CoreExt::HashWithIndifferentAccess.new(req.params) : {}
})
end
2011-12-09 19:25:51 +01:00
# Initialize the Middleman project
def initialize(&block)
# Current path defaults to nil, used in views.
self.current_path = nil
2011-11-21 02:30:53 +01:00
# Clear the static class cache
cache.clear
# Setup the default values from calls to set before initialization
self.class.superclass.defaults.each { |k,v| set(k,v) }
2011-11-18 04:56:55 +01:00
# Evaluate a passed block if given
instance_exec(&block) if block_given?
# Build expanded source path once paths have been parsed
path = root.dup
source_path = ENV["MM_SOURCE"] || self.source
path = File.join(root, source_path) unless source_path.empty?
set :source_dir, path
2011-11-24 06:59:53 +01:00
super
end
# Shared cache instance
#
# @private
# @return [Middleman::Cache] The cache
def self.cache
@_cache ||= ::Middleman::Cache.new
end
delegate :cache, :to => :"self.class"
# Rack env
attr :env
# Rack request
# @return [Rack::Request]
attr :req
# Rack response
# @return [Rack::Response]
attr :res
# Rack Interface
#
# @private
# @param Rack environment
def call(env)
# Store environment, request and response for later
@env = env
@req = Rack::Request.new(env)
@res = Rack::Response.new
2011-11-18 04:56:55 +01:00
if env["PATH_INFO"] == "/__middleman__"
if env["REQUEST_METHOD"] == "POST"
if req.params.has_key?("change")
self.files.did_change(req.params["change"])
elsif req.params.has_key?("delete")
self.files.did_delete(req.params["delete"])
end
2012-04-04 19:26:07 +02:00
end
res.status = 200
return res.finish
end
puts "== Request: #{env["PATH_INFO"]}" if logging?
# Catch :halt exceptions and use that response if given
catch(:halt) do
process_request
2011-11-18 04:56:55 +01:00
res.status = 404
res.finish
end
end
# Halt the current request and return a response
#
# @private
# @param [String] Response value
def halt(response)
throw :halt, response
end
# Whether we're in development mode
# @return [Boolean] If we're in dev mode
def development?; environment == :development; end
# Whether we're in build mode
# @return [Boolean] If we're in build mode
def build?; environment == :build; end
# Core response method. We process the request, check with the sitemap,
# and return the correct file, response or status message.
#
# @private
def process_request
# Normalize the path and add index if we're looking at a directory
@original_path = env["PATH_INFO"].dup
@escaped_path = @original_path.gsub("%20", " ")
@request_path = full_path(@escaped_path)
2011-11-18 23:09:48 +01:00
# Run before callbacks
run_hook :before
if @escaped_path != @request_path
# Get the resource object for this path
resource = sitemap.find_resource_by_destination_path(@escaped_path)
end
2012-04-04 19:26:07 +02:00
# Get the resource object for this full path
resource ||= sitemap.find_resource_by_destination_path(@request_path)
2012-04-04 19:26:07 +02:00
# Return 404 if not in sitemap
return not_found unless resource && !resource.ignored?
2011-11-19 02:37:01 +01:00
# If this path is a static file, send it immediately
return send_file(resource.source_file) unless resource.template?
# Set the current path for use in helpers
self.current_path = @request_path.dup
2011-11-19 05:34:20 +01:00
# Set a HTTP content type based on the request's extensions
content_type resource.mime_type
2011-11-24 06:59:53 +01:00
begin
# Write out the contents of the page
res.write resource.render
2011-11-26 01:09:31 +01:00
# Valid content is a 200 status
res.status = 200
rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e
res.write "Error: #{e.message}"
res.status = 500
end
2011-11-24 06:59:53 +01:00
# End the request
puts "== Finishing Request: #{self.current_path}" if logging?
halt res.finish
end
2011-11-18 04:56:55 +01:00
# Backwards compatibilty with old Sinatra template interface
#
# @return [Middleman::Application]
def settings
self
end
# Whether we're logging
#
# @return [Boolean] If we're logging
def logging?
logging
end
2011-11-18 09:34:56 +01:00
# Expand a path to include the index file if it's a directory
#
# @private
# @param [String] path Request path
# @return [String] Path with index file if necessary
def full_path(path)
cache.fetch(:full_path, path) do
parts = path ? path.split('/') : []
if parts.last.nil? || parts.last.split('.').length == 1
path = File.join(path, index_file)
end
"/" + path.sub(%r{^/}, '')
2011-11-20 03:53:18 +01:00
end
end
2011-11-18 04:56:55 +01:00
# Add a new mime-type for a specific extension
#
# @param [Symbol] type File extension
# @param [String] value Mime type
# @return [void]
def mime_type(type, value=nil)
return type if type.nil? || type.to_s.include?('/')
type = ".#{type}" unless type.to_s[0] == ?.
return ::Rack::Mime.mime_type(type, nil) unless value
::Rack::Mime::MIME_TYPES[type] = value
end
2011-11-24 06:59:53 +01:00
protected
2011-11-24 06:59:53 +01:00
# Halt request and return 404
def not_found
@res.status == 404
@res.write "<html><body><h1>File Not Found</h1><p>#{@request_path}</p></body>"
@res.finish
end
2011-11-18 04:56:55 +01:00
delegate :helpers, :use, :map, :to => :"self.class"
2011-11-24 06:59:53 +01:00
# Immediately send static file
#
# @param [String] path File to send
def send_file(path)
extension = File.extname(path)
matched_mime = mime_type(extension)
matched_mime = "application/octet-stream" if matched_mime.nil?
content_type matched_mime
2011-11-18 04:56:55 +01:00
file = ::Rack::File.new nil
file.path = path
response = file.serving(env)
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension)
halt response
end
# Set the content type for the current request
#
# @param [String] type Content type
# @param [Hash] params
# @return [void]
def content_type(type = nil, params={})
return res['Content-Type'] unless type
default = params.delete :default
mime_type = mime_type(type) || default
throw "Unknown media type: %p" % type if mime_type.nil?
mime_type = mime_type.dup
unless params.include? :charset
params[:charset] = params.delete('charset') || "utf-8"
end
params.delete :charset if mime_type.include? 'charset'
unless params.empty?
mime_type << (mime_type.include?(';') ? ', ' : ';')
mime_type << params.map { |kv| kv.join('=') }.join(', ')
end
res['Content-Type'] = mime_type
end
2011-11-18 04:56:55 +01:00
end
class << self
# Create a new Class which is based on Middleman::Application
# Used to create a safe sandbox into which extensions and
# configuration can be included later without impacting
# other classes and instances.
#
# @return [Class]
def server(&block)
@@servercounter ||= 1
@@servercounter += 1
const_set("MiddlemanApplication#{@@servercounter}", Class.new(Middleman::Application))
end
# Creates a new Rack::Server
#
# @param [Hash] options to pass to Rack::Server.new
# @return [Rack::Server]
def start_server(options={})
require "webrick"
opts = {
:Port => options[:port] || 4567,
:Host => options[:host] || "0.0.0.0",
:AccessLog => []
}
# opts[:Logger] = WEBrick::Log::new("/dev/null", 7) if !options[:logging]
app_class = options[:app] ||= ::Middleman.server.inst
opts[:app] = app_class
# Disable for Beta 1. See if people notice.
require "thin"
::Thin::Logging.silent = !options[:logging]
opts[:server] = 'thin'
# opts[:server] = 'webrick'
server = ::Rack::Server.new(opts)
server.start
server
end
end
end