Merge pull request #1276 from bhollis/extensions

Extensions cleanup
This commit is contained in:
Ben Hollis 2014-05-11 17:20:31 -07:00
commit 5c75d26c86
13 changed files with 332 additions and 204 deletions

View file

@ -7,4 +7,5 @@ middleman-*/lib/**/*.rb
--exclude middleman-cli/lib/middleman-cli/templates/shared/ --exclude middleman-cli/lib/middleman-cli/templates/shared/
--exclude middleman-cli/lib/middleman-cli/templates/extension/ --exclude middleman-cli/lib/middleman-cli/templates/extension/
--no-private --no-private
--hide-void-return --hide-void-return
--markup=markdown

View file

@ -5,4 +5,5 @@ lib/**/*.rb
--exclude lib/middleman-cli/templates/shared/ --exclude lib/middleman-cli/templates/shared/
--exclude lib/middleman-cli/templates/extension/ --exclude lib/middleman-cli/templates/extension/
--no-private --no-private
--hide-void-return --hide-void-return
--markup=markdown

View file

@ -2,4 +2,5 @@ lib/**/*.rb
--exclude lib/vendored-middleman-deps/ --exclude lib/vendored-middleman-deps/
--exclude lib/middleman-core/step_definitions --exclude lib/middleman-core/step_definitions
--no-private --no-private
--hide-void-return --hide-void-return
--markup=markdown

View file

@ -0,0 +1,55 @@
# Add module methods to the Middleman module that allow automatically loading
# extensions defined in gems based on the existance of a special file. This also
# adds a method for iterating over all the Gems on a system.
module Middleman
class << self
# Where to look in gems for extensions to auto-register. Since most extensions are
# called out in a Gemfile, this is really only useful for template extensions that get
# used by "middleman init".
EXTENSION_FILE = File.join('lib', 'middleman_extension.rb') unless const_defined?(:EXTENSION_FILE)
# Automatically load extensions from available RubyGems
# which contain the EXTENSION_FILE
#
# @private
def load_extensions_in_path
require 'rubygems'
extensions = rubygems_latest_specs.select do |spec|
spec_has_file?(spec, EXTENSION_FILE)
end
extensions.each do |spec|
require spec.name
end
end
# Backwards compatible means of finding all the latest gemspecs
# available on the system
#
# @private
# @return [Array] Array of latest Gem::Specification
def rubygems_latest_specs
# If newer Rubygems
if ::Gem::Specification.respond_to? :latest_specs
::Gem::Specification.latest_specs(true)
else
::Gem.source_index.latest_specs
end
end
private
# Where a given Gem::Specification has a specific file. Used
# to discover extensions.
#
# @private
# @param [Gem::Specification] spec
# @param [String] path Path to look for
# @return [Boolean] Whether the file exists
def spec_has_file?(spec, path)
full_path = File.join(spec.full_gem_path, path)
File.exist?(full_path)
end
end
end

View file

@ -82,9 +82,11 @@ module Middleman
# Define a new setting, with optional default and user-friendly description. # Define a new setting, with optional default and user-friendly description.
# Once the configuration manager is finalized, no new settings may be defined. # Once the configuration manager is finalized, no new settings may be defined.
# #
# @param [Symbol] key # @example
# @param [Object] default # config.define_setting :compress, false, 'Whether to compress the output'
# @param [String] description # @param [Symbol] key The name of the option
# @param [Object] default The default value for the option
# @param [String] description A human-readable description of what the option does
# @return [ConfigSetting] # @return [ConfigSetting]
def define_setting(key, default=nil, description=nil) def define_setting(key, default=nil, description=nil)
raise "Setting #{key} doesn't exist" if @finalized raise "Setting #{key} doesn't exist" if @finalized

View file

@ -1,34 +1,3 @@
# Middleman provides an extension API which allows you to hook into the
# lifecycle of a page request, or static build, and manipulate the output.
# Internal to Middleman, these extensions are called "features," but we use
# the exact same API as is made available to the public.
#
# A Middleman extension looks like this:
#
# module MyExtension
# class << self
# def registered(app)
# # My Code
# end
# end
# end
#
# In your `config.rb`, you must load your extension (if it is not defined in
# that file) and call `activate`.
#
# require "my_extension"
# activate MyExtension
#
# This will call the `registered` method in your extension and provide you
# with the `app` parameter which is a Middleman::Application context. From here
# you can choose to respond to requests for certain paths or simply attach
# Rack middleware to the stack.
#
# The built-in features cover a wide range of functions. Some provide helper
# methods to use in your views. Some modify the output on-the-fly. And some
# apply computationally-intensive changes to your final build files.
# Namespace extensions module
module Middleman module Middleman
module CoreExtensions module CoreExtensions
module Extensions module Extensions
@ -47,7 +16,6 @@ module Middleman
app.config[:autoload_sprockets] = (ENV['AUTOLOAD_SPROCKETS'] == 'true') if ENV['AUTOLOAD_SPROCKETS'] app.config[:autoload_sprockets] = (ENV['AUTOLOAD_SPROCKETS'] == 'true') if ENV['AUTOLOAD_SPROCKETS']
app.extend ClassMethods app.extend ClassMethods
app.send :include, InstanceMethods
app.delegate :configure, to: :"self.class" app.delegate :configure, to: :"self.class"
end end
end end
@ -56,121 +24,119 @@ module Middleman
module ClassMethods module ClassMethods
# Add a callback to run in a specific environment # Add a callback to run in a specific environment
# #
# @param [String, Symbol] env The environment to run in # @param [String, Symbol] env The environment to run in (:build, :development)
# @return [void] # @return [void]
def configure(env, &block) def configure(env, &block)
send("#{env}_config", &block) send("#{env}_config", &block)
end end
end end
# Instance methods # This method is available in the project's `config.rb`.
module InstanceMethods # It takes a underscore-separated symbol, finds the appropriate
# This method is available in the project's `config.rb`. # feature module and includes it.
# It takes a underscore-separated symbol, finds the appropriate #
# feature module and includes it. # activate :lorem
# #
# activate :lorem # @param [Symbol, Module] ext Which extension to activate
# # @return [void]
# @param [Symbol, Module] ext Which extension to activate # rubocop:disable BlockNesting
# @return [void] def activate(ext, options={}, &block)
# rubocop:disable BlockNesting extension = ::Middleman::Extensions.load(ext)
def activate(ext, options={}, &block) logger.debug "== Activating: #{ext}"
extension = ::Middleman::Extensions.load(ext)
logger.debug "== Activating: #{ext}"
if extension.supports_multiple_instances? if extension.supports_multiple_instances?
extensions[ext] ||= {} extensions[ext] ||= {}
key = "instance_#{extensions[ext].keys.length}" key = "instance_#{extensions[ext].keys.length}"
extensions[ext][key] = extension.new(self.class, options, &block) extensions[ext][key] = extension.new(self.class, options, &block)
else
if extensions[ext]
raise "#{ext} has already been activated and cannot be re-activated."
else else
if extensions[ext] extensions[ext] = extension.new(self.class, options, &block)
raise "#{ext} has already been activated and cannot be re-activated." end
else end
extensions[ext] = extension.new(self.class, options, &block) end
end
# Access activated extensions
#
# @return [Hash<Symbol,Middleman::Extension|Module>]
def extensions
@extensions ||= {}
end
# Load features before starting server
def initialize
super
self.class.inst = self
# Search the root of the project for required files
$LOAD_PATH.unshift(root)
::Middleman::Extension.clear_after_extension_callbacks
if config[:autoload_sprockets]
begin
require 'middleman-sprockets'
activate(:sprockets)
rescue LoadError
end end
end end
# Access activated extensions run_hook :initialized
#
# @return [Hash<Symbol,Middleman::Extension|Module>] run_hook :before_configuration
def extensions
@extensions ||= {} # Check for and evaluate local configuration
local_config = File.join(root, 'config.rb')
if File.exist? local_config
logger.debug '== Reading: Local config'
config_context.instance_eval File.read(local_config), local_config, 1
end end
# Load features before starting server if build?
def initialize run_hook :build_config
super config_context.execute_configure_callbacks(:build)
end
self.class.inst = self if development?
run_hook :development_config
config_context.execute_configure_callbacks(:development)
end
# Search the root of the project for required files run_hook :instance_available
$LOAD_PATH.unshift(root)
::Middleman::Extension.clear_after_extension_callbacks # This is for making the tests work - since the tests
# don't completely reload middleman, I18n.load_path can get
# polluted with paths from other test app directories that don't
# exist anymore.
if ENV['TEST']
::I18n.load_path.delete_if { |path| path =~ %r{tmp/aruba} }
::I18n.reload!
end
if config[:autoload_sprockets] run_hook :after_configuration
begin config_context.execute_after_configuration_callbacks
require 'middleman-sprockets'
activate(:sprockets) logger.debug 'Loaded extensions:'
rescue LoadError extensions.each do |ext, klass|
if ext.is_a?(Hash)
ext.each do |k, _|
logger.debug "== Extension: #{k}"
end end
else
logger.debug "== Extension: #{ext}"
end end
run_hook :initialized if klass.is_a?(::Middleman::Extension)
# Forward Extension helpers to TemplateContext
run_hook :before_configuration (klass.class.defined_helpers || []).each do |m|
@template_context_class.send(:include, m)
# Check for and evaluate local configuration
local_config = File.join(root, 'config.rb')
if File.exist? local_config
logger.debug '== Reading: Local config'
config_context.instance_eval File.read(local_config), local_config, 1
end
if build?
run_hook :build_config
config_context.execute_configure_callbacks(:build)
end
if development?
run_hook :development_config
config_context.execute_configure_callbacks(:development)
end
run_hook :instance_available
# This is for making the tests work - since the tests
# don't completely reload middleman, I18n.load_path can get
# polluted with paths from other test app directories that don't
# exist anymore.
if ENV['TEST']
::I18n.load_path.delete_if { |path| path =~ %r{tmp/aruba} }
::I18n.reload!
end
run_hook :after_configuration
config_context.execute_after_configuration_callbacks
logger.debug 'Loaded extensions:'
extensions.each do |ext, klass|
if ext.is_a?(Hash)
ext.each do |k, _|
logger.debug "== Extension: #{k}"
end
else
logger.debug "== Extension: #{ext}"
end end
if klass.is_a?(::Middleman::Extension) ::Middleman::Extension.activated_extension(klass)
# Forward Extension helpers to TemplateContext
(klass.class.defined_helpers || []).each do |m|
@template_context_class.send(:include, m)
end
::Middleman::Extension.activated_extension(klass)
end
end end
end end
end end
end end

View file

@ -56,8 +56,6 @@ module Middleman
# Set the shared instance # Set the shared instance
# #
# @private # @private
# @param [Middleman::Application] inst
# @return [void]
attr_writer :inst attr_writer :inst
# Return built Rack app # Return built Rack app
@ -188,7 +186,6 @@ module Middleman
# message. # message.
# #
# @param env # @param env
# @param [Rack::Request] req
# @param [Rack::Response] res # @param [Rack::Response] res
def process_request(env, _, res) def process_request(env, _, res)
start_time = Time.now start_time = Time.now

View file

@ -2,64 +2,179 @@ require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute'
module Middleman module Middleman
# Middleman's Extension API provides the ability to add functionality to Middleman
# and to customize existing features. Internally, most features in Middleman are
# implemented as extensions. A good way to figure out how to write your own extension
# is to look at the source of the built-in extensions or popular extension gems like
# `middleman-blog` or `middleman-syntax`.
#
# The most basic extension looks like:
#
# class MyFeature < Middleman::Extension
# def initialize(app, options_hash={}, &block)
# super
# end
# end
# ::Middleman::Extensions.register(:my_feature, MyFeature)
#
# A more complicated example might look like:
#
# class MyFeature < Middleman::Extension
# option :my_option, 'cool', 'A very cool option'
#
# def initialize(app, options_hash={}, &block)
# super
# puts "My option is #{options.my_option}"
# end
#
# def after_configuration
# puts "The project has been configured"
# end
#
# def manipulate_resource_list(resources)
# resources.each do |resource|
# # Make all .jpg's get built or served with a .jpeg extension.
# if resource.ext == '.jpg'
# resource.destination_path = resource.destination_path.sub('.jpg', '.jpeg')
# end
# end
# end
# end
#
# ::Middleman::Extensions.register :my_feature do
# MyFeature
# end
#
# Extensions can add helpers (via {Extension.helpers}), add to the sitemap or change it (via {#manipulate_resource_list}), or run
# arbitrary code at different parts of the Middleman application's lifecycle. They can have options (defined via {Extension.option} and accessed via {#options}).
#
# Common lifecycle events can be handled by extensions simply by implementing an appropriately-named method:
#
# * {#after_configuration}
# * {#after_build}
# * {#before_build}
# * {#instance_available}
#
# There are also some less common hooks that can be listened to from within an extension's `initialize` method:
#
# * `app.before_render {|body, path, locs, template_class| ... }` - Manipulate template sources before they are rendered.
# * `app.after_render {|content, path, locs, template_class| ... }` - Manipulate output text after a template has been rendered. It is also common to install a Rack middleware to do this instead.
# * `app.ready { ... }` - Run code once Middleman is ready to serve or build files (after `after_configuration`).
# * `app.compass_config { |compass_config| ... }` - Manipulate the Compass configuration after it has been set up.
#
# @see http://middlemanapp.com/advanced/custom/ Middleman Custom Extensions Documentation
class Extension class Extension
# @!attribute supports_multiple_instances
# @!scope class
# @return [Boolean] whether or not an extension can be activated multiple times, generating multiple instances of the extension.
# By default extensions can only be activated once in a project. This is an advanced option.
class_attribute :supports_multiple_instances, instance_reader: false, instance_writer: false class_attribute :supports_multiple_instances, instance_reader: false, instance_writer: false
# @!attribute defined_helpers
# @!scope class
# @api private
# @return [Array<Module>] a list of all the helper modules this extension provides. Set these using {#helpers}.
class_attribute :defined_helpers, instance_reader: false, instance_writer: false class_attribute :defined_helpers, instance_reader: false, instance_writer: false
# @!attribute ext_name
# @!scope class
# @return [Symbol] the name this extension is registered under. This is the symbol used to activate the extension.
class_attribute :ext_name, instance_reader: false, instance_writer: false class_attribute :ext_name, instance_reader: false, instance_writer: false
class << self class << self
# @api private
# @return [Middleman::Configuration::ConfigurationManager] The defined options for this extension.
def config def config
@_config ||= ::Middleman::Configuration::ConfigurationManager.new @_config ||= ::Middleman::Configuration::ConfigurationManager.new
end end
# Add an option to this extension.
# @see Middleman::Configuration::ConfigurationManager#define_setting
# @example
# option :compress, false, 'Whether to compress the output'
# @param [Symbol] key The name of the option
# @param [Object] default The default value for the option
# @param [String] description A human-readable description of what the option does
def option(key, default=nil, description=nil) def option(key, default=nil, description=nil)
config.define_setting(key, default, description) config.define_setting(key, default, description)
end end
# Add helpers to the global Middleman application. # Declare helpers to be added the global Middleman application.
# This accepts either a list of modules to add on behalf # This accepts either a list of modules to add on behalf
# of this extension, or a block whose contents will all # of this extension, or a block whose contents will all
# be used as helpers in a new module. # be used as helpers in a new module.
def helpers(*m, &block) # @example With a block:
# helpers do
# def my_helper
# "I helped!"
# end
# end
# @example With modules:
# helpers FancyHelpers, PlainHelpers
# @param [Array<Module>] modules An optional list of modules to add as helpers
# @param [Proc] block A block which will be evaluated to create a new helper module
# @return [void]
def helpers(*modules, &block)
self.defined_helpers ||= [] self.defined_helpers ||= []
if block_given? if block_given?
mod = Module.new mod = Module.new
mod.module_eval(&block) mod.module_eval(&block)
m = [mod] modules = [mod]
end end
self.defined_helpers += m self.defined_helpers += modules
end
def activate
new(::Middleman::Application)
end end
# Reset all {Extension.after_extension_activated} callbacks.
# @api private
# @return [void]
def clear_after_extension_callbacks def clear_after_extension_callbacks
@_extension_activation_callbacks = {} @_extension_activation_callbacks = {}
end end
# Register to run a block after a named extension is activated.
# @param [Symbol] name The name the extension was registered under
# @param [Proc] block A callback to run when the named extension is activated
# @return [void]
def after_extension_activated(name, &block) def after_extension_activated(name, &block)
@_extension_activation_callbacks ||= {} @_extension_activation_callbacks ||= {}
@_extension_activation_callbacks[name] ||= [] @_extension_activation_callbacks[name] ||= []
@_extension_activation_callbacks[name] << block if block_given? @_extension_activation_callbacks[name] << block if block_given?
end end
# Notify that a particular extension has been activated and run all
# registered {Extension.after_extension_activated} callbacks.
# @api private
# @param [Middleman::Extension] instance Activated extension instance
# @return [void]
def activated_extension(instance) def activated_extension(instance)
name = instance.class.ext_name name = instance.class.ext_name
return unless @_extension_activation_callbacks && @_extension_activation_callbacks[name] return unless @_extension_activation_callbacks && @_extension_activation_callbacks.has_key?(name)
@_extension_activation_callbacks[name].each do |block| @_extension_activation_callbacks[name].each do |block|
block.arity == 1 ? block.call(instance) : block.call block.arity == 1 ? block.call(instance) : block.call
end end
end end
end end
attr_accessor :options # @return [Middleman::Configuration::ConfigurationManager] options for this extension instance.
attr_reader :options
# @return [Middleman::Application] the Middleman application instance.
attr_reader :app attr_reader :app
# @!method after_extension_activated(name, &block)
# Register to run a block after a named extension is activated.
# @param [Symbol] name The name the extension was registered under
# @param [Proc] block A callback to run when the named extension is activated
# @return [void]
delegate :after_extension_activated, to: :"::Middleman::Extension" delegate :after_extension_activated, to: :"::Middleman::Extension"
# Extensions are instantiated when they are activated.
# @param [Class] klass The Middleman::Application class
# @param [Hash] options_hash The raw options hash. Subclasses should not manipulate this directly - it will be turned into {#options}.
# @yield An optional block that can be used to customize options before the extension is activated.
# @yieldparam [Middleman::Configuration::ConfigurationManager] options Extension options
def initialize(klass, options_hash={}, &block) def initialize(klass, options_hash={}, &block)
@_helpers = [] @_helpers = []
@klass = klass @klass = klass
@ -74,6 +189,39 @@ module Middleman
bind_after_build bind_after_build
end end
# @!method before_configuration
# Respond to the `before_configuration` event.
# If a `before_configuration` method is implemented, that method will be run before `config.rb` is run.
# @note Because most extensions are activated from within `config.rb`, they *will not run* any `before_configuration` hook.
# @!method after_configuration
# Respond to the `after_configuration` event.
# If an `after_configuration` method is implemented, that method will be run before `config.rb` is run.
# @!method before_build
# Respond to the `before_build` event.
# If an `before_build` method is implemented, that method will be run before the builder runs.
# @!method after_build
# Respond to the `after_build` event.
# If an `after_build` method is implemented, that method will be run after the builder runs.
# @!method instance_available
# Respond to the `instance_available` event.
# If an `instance_available` method is implemented, that method will be run after `config.rb` is run and after environment-specific config blocks have been run, but before any `after_configuration` callbacks.
# @!method manipulate_resource_list(resources)
# Manipulate the resource list by transforming or adding {Sitemap::Resource}s.
# Sitemap manipulation is a powerful way of interacting with a project, since it can modify each {Sitemap::Resource} or generate new {Sitemap::Resources}. This method is used in a pipeline where each sitemap manipulator is run in turn, with each one being fed the output of the previous manipulator. See the source of built-in Middleman extensions like {Middleman::Extensions::DirectoryIndexes} and {Middleman::Extensions::AssetHash} for examples of how to use this.
# @note This method *must* return the full set of resources, because its return value will be used as the new sitemap.
# @see http://middlemanapp.com/advanced/sitemap/ Sitemap Documentation
# @see Sitemap::Store
# @see Sitemap::Resource
# @param [Array<Sitemap::Resource>] resources A list of all the resources known to the sitemap.
# @return [Array<Sitemap::Resource>] The transformed list of resources.
# Assign the app instance. Used internally.
# @api private
def app=(app) def app=(app)
@app = app @app = app
@ -85,8 +233,10 @@ module Middleman
end end
end end
protected private
# @yield An optional block that can be used to customize options before the extension is activated.
# @yieldparam Middleman::Configuration::ConfigurationManager] options Extension options
def setup_options(options_hash) def setup_options(options_hash)
@options = self.class.config.dup @options = self.class.config.dup
@options.finalize! @options.finalize!

View file

@ -1,4 +1,10 @@
require 'middleman-core/extension'
module Middleman module Middleman
# The Extensions module is used to handle global registration and loading of Middleman Extensions.
#
# The application-facing extension API (activate, etc) is in Middleman::CoreExtensions::Extensions in
# middleman-core/core_extensions/extensions.rb.
module Extensions module Extensions
class << self class << self
def registered def registered
@ -66,55 +72,4 @@ module Middleman
end end
end end
end end
# Where to look in gems for extensions to auto-register. Since most extensions are
# called out in a Gemfile, this is really only useful for template extensions that get
# used by "middleman init".
EXTENSION_FILE = File.join('lib', 'middleman_extension.rb') unless const_defined?(:EXTENSION_FILE)
class << self
# Automatically load extensions from available RubyGems
# which contain the EXTENSION_FILE
#
# @private
def load_extensions_in_path
require 'rubygems'
extensions = rubygems_latest_specs.select do |spec|
spec_has_file?(spec, EXTENSION_FILE)
end
extensions.each do |spec|
require spec.name
end
end
# Backwards compatible means of finding all the latest gemspecs
# available on the system
#
# @private
# @return [Array] Array of latest Gem::Specification
def rubygems_latest_specs
# If newer Rubygems
if ::Gem::Specification.respond_to? :latest_specs
::Gem::Specification.latest_specs(true)
else
::Gem.source_index.latest_specs
end
end
# Where a given Gem::Specification has a specific file. Used
# to discover extensions.
#
# @private
# @param [Gem::Specification] spec
# @param [String] path Path to look for
# @return [Boolean] Whether the file exists
def spec_has_file?(spec, path)
full_path = File.join(spec.full_gem_path, path)
File.exist?(full_path)
end
end
end end
require 'middleman-core/extension'

View file

@ -1,5 +1,6 @@
# Core Pathname library used for traversal # Core Pathname library used for traversal
require 'pathname' require 'pathname'
require 'middleman-core/auto_gem_extensions'
module Middleman module Middleman
class << self class << self

View file

@ -57,21 +57,20 @@ module Middleman
app.config_context.class.send :delegate, :sitemap, to: :app app.config_context.class.send :delegate, :sitemap, to: :app
end end
# Register a klass which can manipulate the main site map list. Best to register # Register an object which can transform the sitemap resource list. Best to register
# these in a before_configuration or after_configuration hook. # these in a `before_configuration` or `after_configuration` hook.
# #
# @param [Symbol] name Name of the manipulator for debugging # @param [Symbol] name Name of the manipulator for debugging
# @param [Class, Module] inst Abstract namespace which can update the resource list # @param [#manipulate_resource_list] manipulator Resource list manipulator
# @return [void] # @return [void]
def register_resource_list_manipulator(name, inst, *) def register_resource_list_manipulator(name, manipulator, *)
@resource_list_manipulators << [name, inst] @resource_list_manipulators << [name, manipulator]
rebuild_resource_list!(:registered_new) rebuild_resource_list!(:registered_new)
end end
# Rebuild the list of resources from scratch, using registed manipulators # Rebuild the list of resources from scratch, using registed manipulators
# rubocop:disable UnusedMethodArgument
# @return [void] # @return [void]
def rebuild_resource_list!(reason=nil) def rebuild_resource_list!(_=nil)
@lock.synchronize do @lock.synchronize do
@needs_sitemap_rebuild = true @needs_sitemap_rebuild = true
end end

View file

@ -60,7 +60,6 @@ module Middleman
# Sinatra/Padrino compatible render method signature referenced by some view # Sinatra/Padrino compatible render method signature referenced by some view
# helpers. Especially partials. # helpers. Especially partials.
# #
# @param [String, Symbol] engine
# @param [String, Symbol] data # @param [String, Symbol] data
# @param [Hash] options # @param [Hash] options
# @return [String] # @return [String]

View file

@ -1,4 +1,5 @@
lib/**/*.rb lib/**/*.rb
--exclude lib/middleman-templates --exclude lib/middleman-templates
--no-private --no-private
--hide-void-return --hide-void-return
--markup=markdown