middleman/middleman-core/lib/middleman-core/base.rb

497 lines
12 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"
2011-12-09 19:25:51 +01:00
2011-11-24 06:59:53 +01:00
# Core Middleman Class
2011-11-18 04:56:55 +01:00
class Middleman::Base
2011-11-24 06:59:53 +01:00
# Uses callbacks
2011-11-18 04:56:55 +01:00
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
2011-11-18 04:56:55 +01:00
def reset!
@app = nil
@prototype = nil
end
2011-11-24 06:59:53 +01:00
# The shared Rack instance being build
#
# @private
# @return [Rack::Builder]
2011-11-18 04:56:55 +01:00
def app
@app ||= Rack::Builder.new
end
2011-11-24 06:59:53 +01:00
# Get the static instance
#
# @private
# @return [Middleman::Base]
2011-11-18 09:34:56 +01:00
def inst(&block)
2011-11-28 07:04:19 +01:00
@inst ||= begin
mm = new(&block)
mm.run_hook :ready
mm
end
2011-11-18 09:34:56 +01:00
end
# Set the shared instance
#
# @private
# @param [Middleman::Base] inst
# @return [void]
def inst=(inst)
@inst = inst
end
2011-11-24 06:59:53 +01:00
# Return built Rack app
#
# @private
# @return [Rack::Builder]
2011-11-18 09:34:56 +01:00
def to_rack_app(&block)
2011-11-19 02:03:09 +01:00
inner_app = inst(&block)
(@middleware || []).each do |m|
app.use(m[0], *m[1], &m[2])
end
2011-11-19 02:03:09 +01:00
app.map("/") { run inner_app }
(@mappings || []).each do |m|
app.map(m[0], &m[1])
end
2011-11-18 09:34:56 +01:00
app
end
2011-11-24 06:59:53 +01:00
# Prototype app. Used in config.ru
#
# @private
# @return [Rack::Builder]
2011-11-18 04:56:55 +01:00
def prototype
2011-11-20 04:17:12 +01:00
@prototype ||= to_rack_app
2011-11-18 04:56:55 +01:00
end
2011-11-24 06:59:53 +01:00
# Call prototype, use in config.ru
#
# @private
2011-11-18 04:56:55 +01:00
def call(env)
prototype.call(env)
end
2011-11-24 06:59:53 +01:00
# Use Rack middleware
#
# @param [Class] Middleware
2011-12-18 05:12:13 +01:00
# @return [void]
2011-11-18 04:56:55 +01:00
def use(middleware, *args, &block)
@middleware ||= []
@middleware << [middleware, args, block]
end
2011-11-24 06:59:53 +01:00
# Add Rack App mapped to specific path
#
# @param [String] Path to map
2011-12-18 05:12:13 +01:00
# @return [void]
2011-11-18 04:56:55 +01:00
def map(map, &block)
@mappings ||= []
@mappings << [map, block]
2011-11-18 04:56:55 +01:00
end
2011-11-24 06:59:53 +01:00
# Mix-in helper methods. Accepts either a list of Modules
# and/or a block to be evaluated
2011-12-18 05:12:13 +01:00
# @return [void]
2011-11-18 04:56:55 +01:00
def helpers(*extensions, &block)
class_eval(&block) if block_given?
include(*extensions) if extensions.any?
end
2011-11-24 06:59:53 +01:00
# Access class-wide defaults
#
# @private
# @return [Hash] Hash of default values
2011-11-18 04:56:55 +01:00
def defaults
@defaults ||= {}
end
2011-11-24 06:59:53 +01:00
# Set class-wide defaults
#
# @param [Symbol] Unique key name
# @param Default value
2011-12-18 05:12:13 +01:00
# @return [void]
def set(key, value=nil, &block)
2011-11-18 04:56:55 +01:00
@defaults ||= {}
@defaults[key] = value
@inst.set(key, value, &block) if @inst
2011-11-14 06:57:53 +01:00
end
2011-11-18 04:56:55 +01:00
end
2011-11-30 08:43:01 +01:00
# 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)
2011-11-18 04:56:55 +01:00
setter = "#{key}=".to_sym
self.class.send(:attr_accessor, key) if !respond_to?(setter)
value = block if block_given?
2011-11-18 04:56:55 +01:00
send(setter, value)
end
2011-11-24 06:59:53 +01:00
# Root project directory (overwritten in middleman build/server)
2011-11-30 08:43:01 +01:00
# @return [String]
2011-12-29 00:29:19 +01:00
set :root, ENV["MM_ROOT"] || Dir.pwd
2011-11-24 06:59:53 +01:00
# Name of the source directory
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-18 04:56:55 +01:00
set :source, "source"
2011-11-24 06:59:53 +01:00
2011-11-30 08:43:01 +01:00
# Middleman environment. Defaults to :development, set to :build by the build process
# @return [String]
2011-11-18 04:56:55 +01:00
set :environment, (ENV['MM_ENV'] && ENV['MM_ENV'].to_sym) || :development
2011-11-24 06:59:53 +01:00
2011-11-30 08:43:01 +01:00
# Whether logging is active, disabled by default
# @return [String]
2011-11-18 04:56:55 +01:00
set :logging, false
2011-11-24 06:59:53 +01:00
# Which file should be used for directory indexes
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :index_file, "index.html"
2011-11-18 04:56:55 +01:00
2011-11-24 06:59:53 +01:00
# Location of javascripts within source. Used by Sprockets.
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :js_dir, "javascripts"
# Location of stylesheets within source. Used by Compass.
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :css_dir, "stylesheets"
# Location of images within source. Used by HTML helpers and Compass.
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :images_dir, "images"
2011-11-18 04:56:55 +01:00
2011-11-24 06:59:53 +01:00
# Where to build output files
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :build_dir, "build"
2011-11-18 04:56:55 +01:00
2011-11-24 06:59:53 +01:00
# Default prefix for building paths. Used by HTML helpers and Compass.
2011-11-30 08:43:01 +01:00
# @return [String]
2011-11-24 06:59:53 +01:00
set :http_prefix, "/"
# Whether to catch and display exceptions
# @return [Boolean]
set :show_exceptions, true
2011-11-24 06:59:53 +01:00
# Automatically loaded extensions
2011-11-30 08:43:01 +01:00
# @return [Array<Symbol>]
set :default_extensions, [ :lorem ]
2011-11-18 04:56:55 +01:00
# Default layout name
2011-11-30 08:43:01 +01:00
# @return [String, Symbold]
2011-11-26 01:09:31 +01:00
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
2011-11-18 04:56:55 +01:00
# Add Builder Callbacks
register Middleman::CoreExtensions::Builder
# Add Watcher Callbacks
2011-11-18 04:56:55 +01:00
register Middleman::CoreExtensions::FileWatcher
# Activate Data package
register Middleman::CoreExtensions::Data
# Setup custom rendering
register Middleman::CoreExtensions::Rendering
# Sitemap
register Middleman::CoreExtensions::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
# with_layout and page routing
register Middleman::CoreExtensions::Routing
# Parse YAML from templates
register Middleman::CoreExtensions::FrontMatter
# Built-in Extensions
2011-11-24 06:59:53 +01:00
Middleman::Extensions.register(:directory_indexes) {
Middleman::Extensions::DirectoryIndexes }
Middleman::Extensions.register(:lorem) {
2011-11-24 06:59:53 +01:00
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
2011-12-09 19:25:51 +01:00
# Backwards-compatibility with old request.path signature
attr :request
2011-11-24 06:59:53 +01:00
# Accessor for current path
2011-11-30 08:43:01 +01:00
# @return [String]
2011-12-09 19:25:51 +01:00
def current_path
@_current_path
end
2011-12-18 05:12:13 +01:00
# Set the current path
#
# @param [String] path The new current path
# @return [void]
2011-12-09 19:25:51 +01:00
def current_path=(path)
@_current_path = path
@request = ::Thor::CoreExt::HashWithIndifferentAccess.new({ :path => path })
2011-12-09 19:25:51 +01:00
end
2011-11-24 06:59:53 +01:00
# Initialize the Middleman project
2011-11-18 04:56:55 +01:00
def initialize(&block)
2011-11-24 06:59:53 +01:00
# Current path defaults to nil, used in views.
2011-12-09 19:25:51 +01:00
self.current_path = nil
2011-11-21 02:30:53 +01:00
2011-11-30 08:43:01 +01:00
# Clear the static class cache
cache.clear
2011-11-24 06:59:53 +01:00
# Setup the default values from calls to set before initialization
2011-11-30 08:43:01 +01:00
self.class.superclass.defaults.each { |k,v| set(k,v) }
2011-11-18 04:56:55 +01:00
2011-11-24 06:59:53 +01:00
# Evaluate a passed block if given
instance_exec(&block) if block_given?
2011-11-24 06:59:53 +01:00
# Build expanded source path once paths have been parsed
2011-12-29 00:29:19 +01:00
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
2011-11-18 04:56:55 +01:00
super
end
2011-11-24 06:59:53 +01:00
# Shared cache instance
#
# @private
# @return [Middleman::Cache] The cache
def self.cache
2011-11-20 03:53:18 +01:00
@_cache ||= ::Middleman::Cache.new
end
delegate :cache, :to => :"self.class"
2011-11-24 06:59:53 +01:00
# Rack env
attr :env
2011-11-24 06:59:53 +01:00
# Rack request
2011-11-30 08:43:01 +01:00
# @return [Rack::Request]
attr :req
2011-11-24 06:59:53 +01:00
# Rack response
2011-11-30 08:43:01 +01:00
# @return [Rack::Response]
attr :res
2011-11-21 02:30:53 +01:00
# Rack Interface
2011-11-24 06:59:53 +01:00
#
# @private
# @param Rack environment
2011-11-18 04:56:55 +01:00
def call(env)
2011-11-24 06:59:53 +01:00
# Store environment, request and response for later
2011-11-18 04:56:55 +01:00
@env = env
@req = Rack::Request.new(env)
@res = Rack::Response.new
puts "== Request: #{env["PATH_INFO"]}" if logging?
if env["PATH_INFO"] == "/__middleman__" && 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
res.status = 200
return res.finish
end
2011-11-24 06:59:53 +01:00
# 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
2011-11-24 06:59:53 +01:00
# Halt the current request and return a response
#
# @private
# @param [String] Reponse value
def halt(response)
throw :halt, response
end
2011-11-24 06:59:53 +01:00
# Whether we're in development mode
# @return [Boolean] If we're in dev mode
2011-11-18 04:56:55 +01:00
def development?; environment == :development; end
2011-11-24 06:59:53 +01:00
# Whether we're in build mode
# @return [Boolean] If we're in build mode
2011-11-18 04:56:55 +01:00
def build?; environment == :build; end
2011-11-24 06:59:53 +01:00
# Core repsonse method. We process the request, check with the sitemap,
# and return the correct file, response or status message.
#
# @private
2011-11-18 04:56:55 +01:00
def process_request
# Normalize the path and add index if we're looking at a directory
2011-11-18 23:09:48 +01:00
@original_path = env["PATH_INFO"].dup
@request_path = full_path(env["PATH_INFO"].gsub("%20", " "))
2011-11-24 06:59:53 +01:00
# Run before callbacks
2011-11-18 23:09:48 +01:00
run_hook :before
2011-11-24 06:59:53 +01:00
# Return 404 if not in sitemap
return not_found unless sitemap.exists?(@request_path)
2011-11-24 06:59:53 +01:00
# Get the page object for this path
sitemap_page = sitemap.page(@request_path)
2011-11-24 06:59:53 +01:00
# Return 404 if this path is specifically ignored
return not_found if sitemap_page.ignored?
2011-11-19 02:37:01 +01:00
2011-11-24 06:59:53 +01:00
# If this path is a static file, send it immediately
return send_file(sitemap_page.source_file) unless sitemap_page.template?
2011-11-24 06:59:53 +01:00
# Set the current path for use in helpers
2011-12-09 19:25:51 +01:00
self.current_path = @request_path.dup
2011-11-19 05:34:20 +01:00
2011-11-24 06:59:53 +01:00
# Set a HTTP content type based on the request's extensions
content_type sitemap_page.mime_type
2011-11-24 06:59:53 +01:00
2011-11-26 01:09:31 +01:00
begin
# Write out the contents of the page
res.write sitemap_page.render
# Valid content is a 200 status
res.status = 200
rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e
2011-11-26 01:09:31 +01:00
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
2011-11-18 04:56:55 +01:00
end
# Backwards compatibilty with old Sinatra template interface
2011-11-24 06:59:53 +01:00
#
# @return [Middleman::Base]
def settings
self
end
2011-11-24 06:59:53 +01:00
# Whether we're logging
#
# @return [Boolean] If we're logging
2011-11-18 09:34:56 +01:00
def logging?
logging
end
2011-11-24 06:59:53 +01:00
# Expand a path to include the index file if it's a directory
#
# @private
2011-12-18 05:12:13 +01:00
# @param [String] path Request path
2011-11-24 06:59:53 +01:00
# @return [String] Path with index file if necessary
2011-11-18 04:56:55 +01:00
def full_path(path)
cache.fetch(:full_path, path) do
2011-11-20 03:53:18 +01:00
parts = path ? path.split('/') : []
if parts.last.nil? || parts.last.split('.').length == 1
path = File.join(path, index_file)
end
"/" + path.sub(%r{^/}, '')
end
2011-11-18 04:56:55 +01:00
end
2011-11-24 06:59:53 +01:00
# Add a new mime-type for a specific extension
#
2011-12-18 05:12:13 +01:00
# @param [Symbol] type File extension
# @param [String] value Mime type
# @return [void]
2011-11-24 06:59:53 +01:00
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
protected
# Halt request and return 404
2011-11-18 04:56:55 +01:00
def not_found
@res.status == 404
2011-11-18 23:09:48 +01:00
@res.write "<html><body><h1>File Not Found</h1><p>#{@request_path}</p></body>"
2011-11-18 04:56:55 +01:00
@res.finish
end
delegate :helpers, :use, :map, :to => :"self.class"
2011-11-24 06:59:53 +01:00
# Immediately send static file
#
2011-12-18 05:12:13 +01:00
# @param [String] path File to send
2011-11-18 04:56:55 +01:00
def send_file(path)
2011-11-27 08:09:18 +01:00
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
2011-11-27 08:09:18 +01:00
response = file.serving(env)
response[1]['Content-Encoding'] = 'gzip' if %w(.svgz).include?(extension)
halt response
2011-11-18 04:56:55 +01:00
end
2011-11-24 06:59:53 +01:00
# Set the content type for the current request
#
2011-12-18 05:12:13 +01:00
# @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
end