diff --git a/.yardopts b/.yardopts index 980f0e4c..8a5eba70 100644 --- a/.yardopts +++ b/.yardopts @@ -7,4 +7,5 @@ middleman-*/lib/**/*.rb --exclude middleman-cli/lib/middleman-cli/templates/shared/ --exclude middleman-cli/lib/middleman-cli/templates/extension/ --no-private ---hide-void-return \ No newline at end of file +--hide-void-return +--markup=markdown \ No newline at end of file diff --git a/middleman-cli/.yardopts b/middleman-cli/.yardopts index 47ae041b..b816052e 100644 --- a/middleman-cli/.yardopts +++ b/middleman-cli/.yardopts @@ -5,4 +5,5 @@ lib/**/*.rb --exclude lib/middleman-cli/templates/shared/ --exclude lib/middleman-cli/templates/extension/ --no-private ---hide-void-return \ No newline at end of file +--hide-void-return +--markup=markdown \ No newline at end of file diff --git a/middleman-core/.yardopts b/middleman-core/.yardopts index 29455a55..ee16b7ac 100644 --- a/middleman-core/.yardopts +++ b/middleman-core/.yardopts @@ -2,4 +2,5 @@ lib/**/*.rb --exclude lib/vendored-middleman-deps/ --exclude lib/middleman-core/step_definitions --no-private ---hide-void-return \ No newline at end of file +--hide-void-return +--markup=markdown \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/auto_gem_extensions.rb b/middleman-core/lib/middleman-core/auto_gem_extensions.rb new file mode 100644 index 00000000..430106b2 --- /dev/null +++ b/middleman-core/lib/middleman-core/auto_gem_extensions.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/configuration.rb b/middleman-core/lib/middleman-core/configuration.rb index dce449e8..7c39a405 100644 --- a/middleman-core/lib/middleman-core/configuration.rb +++ b/middleman-core/lib/middleman-core/configuration.rb @@ -82,9 +82,11 @@ module Middleman # Define a new setting, with optional default and user-friendly description. # Once the configuration manager is finalized, no new settings may be defined. # - # @param [Symbol] key - # @param [Object] default - # @param [String] description + # @example + # config.define_setting :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 # @return [ConfigSetting] def define_setting(key, default=nil, description=nil) raise "Setting #{key} doesn't exist" if @finalized diff --git a/middleman-core/lib/middleman-core/core_extensions/extensions.rb b/middleman-core/lib/middleman-core/core_extensions/extensions.rb index b48175c4..8aad51f9 100644 --- a/middleman-core/lib/middleman-core/core_extensions/extensions.rb +++ b/middleman-core/lib/middleman-core/core_extensions/extensions.rb @@ -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 CoreExtensions module Extensions @@ -47,7 +16,6 @@ module Middleman app.config[:autoload_sprockets] = (ENV['AUTOLOAD_SPROCKETS'] == 'true') if ENV['AUTOLOAD_SPROCKETS'] app.extend ClassMethods - app.send :include, InstanceMethods app.delegate :configure, to: :"self.class" end end @@ -56,121 +24,119 @@ module Middleman module ClassMethods # 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] def configure(env, &block) send("#{env}_config", &block) end end - # Instance methods - module InstanceMethods - # This method is available in the project's `config.rb`. - # It takes a underscore-separated symbol, finds the appropriate - # feature module and includes it. - # - # activate :lorem - # - # @param [Symbol, Module] ext Which extension to activate - # @return [void] - # rubocop:disable BlockNesting - def activate(ext, options={}, &block) - extension = ::Middleman::Extensions.load(ext) - logger.debug "== Activating: #{ext}" + # This method is available in the project's `config.rb`. + # It takes a underscore-separated symbol, finds the appropriate + # feature module and includes it. + # + # activate :lorem + # + # @param [Symbol, Module] ext Which extension to activate + # @return [void] + # rubocop:disable BlockNesting + def activate(ext, options={}, &block) + extension = ::Middleman::Extensions.load(ext) + logger.debug "== Activating: #{ext}" - if extension.supports_multiple_instances? - extensions[ext] ||= {} - key = "instance_#{extensions[ext].keys.length}" - extensions[ext][key] = extension.new(self.class, options, &block) + if extension.supports_multiple_instances? + extensions[ext] ||= {} + key = "instance_#{extensions[ext].keys.length}" + 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 - if extensions[ext] - raise "#{ext} has already been activated and cannot be re-activated." - else - extensions[ext] = extension.new(self.class, options, &block) - end + extensions[ext] = extension.new(self.class, options, &block) + end + end + end + + # Access activated extensions + # + # @return [Hash] + 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 - # Access activated extensions - # - # @return [Hash] - def extensions - @extensions ||= {} + run_hook :initialized + + run_hook :before_configuration + + # 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 - # Load features before starting server - def initialize - super + if build? + run_hook :build_config + 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 - $LOAD_PATH.unshift(root) + run_hook :instance_available - ::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] - begin - require 'middleman-sprockets' - activate(:sprockets) - rescue LoadError + 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 - run_hook :initialized - - run_hook :before_configuration - - # 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}" + if klass.is_a?(::Middleman::Extension) + # Forward Extension helpers to TemplateContext + (klass.class.defined_helpers || []).each do |m| + @template_context_class.send(:include, m) end - if klass.is_a?(::Middleman::Extension) - # 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 + ::Middleman::Extension.activated_extension(klass) end + end end end diff --git a/middleman-core/lib/middleman-core/core_extensions/request.rb b/middleman-core/lib/middleman-core/core_extensions/request.rb index c8155198..3e822715 100644 --- a/middleman-core/lib/middleman-core/core_extensions/request.rb +++ b/middleman-core/lib/middleman-core/core_extensions/request.rb @@ -56,8 +56,6 @@ module Middleman # Set the shared instance # # @private - # @param [Middleman::Application] inst - # @return [void] attr_writer :inst # Return built Rack app @@ -188,7 +186,6 @@ module Middleman # message. # # @param env - # @param [Rack::Request] req # @param [Rack::Response] res def process_request(env, _, res) start_time = Time.now diff --git a/middleman-core/lib/middleman-core/extension.rb b/middleman-core/lib/middleman-core/extension.rb index 80865c3a..f4248657 100644 --- a/middleman-core/lib/middleman-core/extension.rb +++ b/middleman-core/lib/middleman-core/extension.rb @@ -2,64 +2,179 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/class/attribute' 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 + # @!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 + + # @!attribute defined_helpers + # @!scope class + # @api private + # @return [Array] a list of all the helper modules this extension provides. Set these using {#helpers}. 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 << self + # @api private + # @return [Middleman::Configuration::ConfigurationManager] The defined options for this extension. def config @_config ||= ::Middleman::Configuration::ConfigurationManager.new 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) config.define_setting(key, default, description) 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 # of this extension, or a block whose contents will all # 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] 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 ||= [] if block_given? mod = Module.new mod.module_eval(&block) - m = [mod] + modules = [mod] end - self.defined_helpers += m - end - - def activate - new(::Middleman::Application) + self.defined_helpers += modules end + # Reset all {Extension.after_extension_activated} callbacks. + # @api private + # @return [void] def clear_after_extension_callbacks @_extension_activation_callbacks = {} 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) @_extension_activation_callbacks ||= {} @_extension_activation_callbacks[name] ||= [] @_extension_activation_callbacks[name] << block if block_given? 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) 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| block.arity == 1 ? block.call(instance) : block.call 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 + # @!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" + # 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) @_helpers = [] @klass = klass @@ -74,6 +189,39 @@ module Middleman bind_after_build 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] resources A list of all the resources known to the sitemap. + # @return [Array] The transformed list of resources. + + # Assign the app instance. Used internally. + # @api private def app=(app) @app = app @@ -85,8 +233,10 @@ module Middleman 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) @options = self.class.config.dup @options.finalize! diff --git a/middleman-core/lib/middleman-core/extensions.rb b/middleman-core/lib/middleman-core/extensions.rb index f775b99e..47157122 100644 --- a/middleman-core/lib/middleman-core/extensions.rb +++ b/middleman-core/lib/middleman-core/extensions.rb @@ -1,4 +1,10 @@ +require 'middleman-core/extension' + 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 class << self def registered @@ -66,55 +72,4 @@ module Middleman 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 - -require 'middleman-core/extension' diff --git a/middleman-core/lib/middleman-core/load_paths.rb b/middleman-core/lib/middleman-core/load_paths.rb index 68defc69..36c32704 100644 --- a/middleman-core/lib/middleman-core/load_paths.rb +++ b/middleman-core/lib/middleman-core/load_paths.rb @@ -1,5 +1,6 @@ # Core Pathname library used for traversal require 'pathname' +require 'middleman-core/auto_gem_extensions' module Middleman class << self diff --git a/middleman-core/lib/middleman-core/sitemap/store.rb b/middleman-core/lib/middleman-core/sitemap/store.rb index a987843c..816d94d5 100644 --- a/middleman-core/lib/middleman-core/sitemap/store.rb +++ b/middleman-core/lib/middleman-core/sitemap/store.rb @@ -57,21 +57,20 @@ module Middleman app.config_context.class.send :delegate, :sitemap, to: :app end - # Register a klass which can manipulate the main site map list. Best to register - # these in a before_configuration or after_configuration hook. + # Register an object which can transform the sitemap resource list. Best to register + # these in a `before_configuration` or `after_configuration` hook. # # @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] - def register_resource_list_manipulator(name, inst, *) - @resource_list_manipulators << [name, inst] + def register_resource_list_manipulator(name, manipulator, *) + @resource_list_manipulators << [name, manipulator] rebuild_resource_list!(:registered_new) end # Rebuild the list of resources from scratch, using registed manipulators - # rubocop:disable UnusedMethodArgument # @return [void] - def rebuild_resource_list!(reason=nil) + def rebuild_resource_list!(_=nil) @lock.synchronize do @needs_sitemap_rebuild = true end diff --git a/middleman-core/lib/middleman-core/template_context.rb b/middleman-core/lib/middleman-core/template_context.rb index f2d4a4fb..81d405a8 100644 --- a/middleman-core/lib/middleman-core/template_context.rb +++ b/middleman-core/lib/middleman-core/template_context.rb @@ -60,7 +60,6 @@ module Middleman # Sinatra/Padrino compatible render method signature referenced by some view # helpers. Especially partials. # - # @param [String, Symbol] engine # @param [String, Symbol] data # @param [Hash] options # @return [String] diff --git a/middleman-templates/.yardopts b/middleman-templates/.yardopts index 72efde46..fcefc936 100644 --- a/middleman-templates/.yardopts +++ b/middleman-templates/.yardopts @@ -1,4 +1,5 @@ lib/**/*.rb --exclude lib/middleman-templates --no-private ---hide-void-return \ No newline at end of file +--hide-void-return +--markup=markdown \ No newline at end of file