diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index 442decf1..0de3e485 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -1,29 +1,14 @@ -# i18n Built-in -require 'i18n' - -# Don't fail on invalid locale, that's not what our current -# users expect. -::I18n.enforce_available_locales = false - # Use ActiveSupport JSON require 'active_support/json' require 'active_support/core_ext/integer/inflections' -# Simple callback library -require 'hooks' - -# Our custom logger -require 'middleman-core/logger' - require 'middleman-core/contracts' - +require 'middleman-core/callback_manager' +require 'middleman-core/logger' require 'middleman-core/sitemap/store' - require 'middleman-core/configuration' - require 'middleman-core/extension_manager' require 'middleman-core/core_extensions' - require 'middleman-core/config_context' require 'middleman-core/file_renderer' require 'middleman-core/template_renderer' @@ -38,6 +23,9 @@ module Middleman include Contracts class << self + extend Forwardable + def_delegator :config, :define_setting + # Global configuration for the whole Middleman project. # @return [ConfigurationManager] def config @@ -56,38 +44,37 @@ module Middleman end end - # Uses callbacks - include Hooks - include Hooks::InstanceHooks + Contract ::Middleman::ConfigContext + attr_reader :config_context - define_hook :initialized - define_hook :after_configuration - define_hook :before_configuration + Contract ::Middleman::Sitemap::Store + attr_reader :sitemap - # Before request hook - define_hook :before + # An anonymous subclass of ::Middleman::TemplateContext + attr_reader :template_context_class - # Ready (all loading and parsing of extensions complete) hook - define_hook :ready + # An instance of the above anonymouse class. + attr_reader :generic_template_context - # Runs before the build is started - define_hook :before_build + Contract ::Middleman::Configuration::ConfigurationManager + attr_reader :config - # Runs after the build is finished - define_hook :after_build + Contract ::Middleman::ExtensionManager + attr_reader :extensions - define_hook :before_shutdown + Contract SetOf[MiddlewareDescriptor] + attr_reader :middleware - define_hook :before_render - define_hook :after_render + Contract SetOf[MapDescriptor] + attr_reader :mappings # Which host preview should start on. # @return [Fixnum] - config.define_setting :host, '0.0.0.0', 'The preview server host' + define_setting :host, '0.0.0.0', 'The preview server host' # Which port preview should start on. # @return [Fixnum] - config.define_setting :port, 4567, 'The preview server port' + define_setting :port, 4567, 'The preview server port' # Whether to serve the preview server over HTTPS. # @return [Boolean] @@ -103,73 +90,73 @@ module Middleman # Name of the source directory # @return [String] - config.define_setting :source, 'source', 'Name of the source directory' + define_setting :source, 'source', 'Name of the source directory' # Middleman mode. Defaults to :server, set to :build by the build process # @return [String] - config.define_setting :mode, ((ENV['MM_ENV'] && ENV['MM_ENV'].to_sym) || :server), 'Middleman mode. Defaults to :server' + define_setting :mode, ((ENV['MM_ENV'] && ENV['MM_ENV'].to_sym) || :server), 'Middleman mode. Defaults to :server' # Middleman environment. Defaults to :development # @return [String] - config.define_setting :environment, :development, 'Middleman environment. Defaults to :development' + define_setting :environment, :development, 'Middleman environment. Defaults to :development' # Which file should be used for directory indexes # @return [String] - config.define_setting :index_file, 'index.html', 'Which file should be used for directory indexes' + define_setting :index_file, 'index.html', 'Which file should be used for directory indexes' # Whether to strip the index file name off links to directory indexes # @return [Boolean] - config.define_setting :strip_index_file, true, 'Whether to strip the index file name off links to directory indexes' + define_setting :strip_index_file, true, 'Whether to strip the index file name off links to directory indexes' # Whether to include a trailing slash when stripping the index file # @return [Boolean] - config.define_setting :trailing_slash, true, 'Whether to include a trailing slash when stripping the index file' + define_setting :trailing_slash, true, 'Whether to include a trailing slash when stripping the index file' # Location of javascripts within source. # @return [String] - config.define_setting :js_dir, 'javascripts', 'Location of javascripts within source' + define_setting :js_dir, 'javascripts', 'Location of javascripts within source' # Location of stylesheets within source. Used by Compass. # @return [String] - config.define_setting :css_dir, 'stylesheets', 'Location of stylesheets within source' + define_setting :css_dir, 'stylesheets', 'Location of stylesheets within source' # Location of images within source. Used by HTML helpers and Compass. # @return [String] - config.define_setting :images_dir, 'images', 'Location of images within source' + define_setting :images_dir, 'images', 'Location of images within source' # Location of fonts within source. Used by Compass. # @return [String] - config.define_setting :fonts_dir, 'fonts', 'Location of fonts within source' + define_setting :fonts_dir, 'fonts', 'Location of fonts within source' # Location of layouts within source. Used by renderers. # @return [String] - config.define_setting :layouts_dir, 'layouts', 'Location of layouts within source' + define_setting :layouts_dir, 'layouts', 'Location of layouts within source' # Where to build output files # @return [String] - config.define_setting :build_dir, 'build', 'Where to build output files' + define_setting :build_dir, 'build', 'Where to build output files' # Default prefix for building paths. Used by HTML helpers and Compass. # @return [String] - config.define_setting :http_prefix, '/', 'Default prefix for building paths' + define_setting :http_prefix, '/', 'Default prefix for building paths' # Default layout name # @return [String, Symbold] - config.define_setting :layout, :_auto_layout, 'Default layout name' + define_setting :layout, :_auto_layout, 'Default layout name' # Default string encoding for templates and output. # @return [String] - config.define_setting :encoding, 'utf-8', 'Default string encoding for templates and output' + define_setting :encoding, 'utf-8', 'Default string encoding for templates and output' # Should Padrino include CRSF tag # @return [Boolean] - config.define_setting :protect_from_csrf, false, 'Should Padrino include CRSF tag' + define_setting :protect_from_csrf, false, 'Should Padrino include CRSF tag' # Set to automatically convert some characters into a directory - config.define_setting :automatic_directory_matcher, nil, 'Set to automatically convert some characters into a directory' + define_setting :automatic_directory_matcher, nil, 'Set to automatically convert some characters into a directory' # Setup callbacks which can exclude paths from the sitemap - config.define_setting :ignored_sitemap_matchers, { + define_setting :ignored_sitemap_matchers, { # Files starting with an underscore, but not a double-underscore partials: proc { |file| ignored = false @@ -190,37 +177,40 @@ module Middleman } }, 'Callbacks that can exclude paths from the sitemap' - config.define_setting :watcher_disable, false, 'If the Listen watcher should not run' - config.define_setting :watcher_force_polling, false, 'If the Listen watcher should run in polling mode' - config.define_setting :watcher_latency, nil, 'The Listen watcher latency' + define_setting :watcher_disable, false, 'If the Listen watcher should not run' + define_setting :watcher_force_polling, false, 'If the Listen watcher should run in polling mode' + define_setting :watcher_latency, nil, 'The Listen watcher latency' - attr_reader :config_context - attr_reader :sitemap - attr_reader :cache - attr_reader :template_context_class - attr_reader :config - attr_reader :generic_template_context - attr_reader :extensions - attr_reader :sources - - Contract SetOf[MiddlewareDescriptor] - attr_reader :middleware - - Contract SetOf[MapDescriptor] - attr_reader :mappings - - # Reference to Logger singleton + # Delegate convenience methods off to their implementations def_delegator :"::Middleman::Logger", :singleton, :logger def_delegator :"::Middleman::Util", :instrument def_delegators :"self.class", :root, :root_path def_delegators :@generic_template_context, :link_to, :image_tag, :asset_path def_delegators :@extensions, :activate + def_delegators :config, :define_setting # Initialize the Middleman project def initialize(&block) # Search the root of the project for required files $LOAD_PATH.unshift(root) unless $LOAD_PATH.include?(root) + @callbacks = ::Middleman::CallbackManager.new + @callbacks.install_methods!(self, [ + :initialized, + :configure, + :before_sitemap, + :before_configuration, + :after_configuration, + :after_configuration_eval, + :ready, + :before_build, + :after_build, + :before_shutdown, + :before, # Before Rack requests + :before_render, + :after_render + ]) + @middleware = Set.new @mappings = Set.new @@ -228,21 +218,22 @@ module Middleman @generic_template_context = @template_context_class.new(self) @config_context = ConfigContext.new(self, @template_context_class) - ::Middleman::FileRenderer.cache.clear - ::Middleman::TemplateRenderer.cache.clear - # Setup the default values from calls to set before initialization @config = ::Middleman::Configuration::ConfigurationManager.new @config.load_settings(self.class.config.all_settings) config[:source] = ENV['MM_SOURCE'] if ENV['MM_SOURCE'] + # TODO, make this less global + ::Middleman::FileRenderer.cache.clear + ::Middleman::TemplateRenderer.cache.clear + @extensions = ::Middleman::ExtensionManager.new(self) # Evaluate a passed block if given config_context.instance_exec(&block) if block_given? - @extensions.auto_activate(:before_sitemap) + execute_callbacks(:before_sitemap) # Initialize the Sitemap @sitemap = ::Middleman::Sitemap::Store.new(self) @@ -254,42 +245,34 @@ module Middleman ::Middleman::Extension.clear_after_extension_callbacks - @extensions.auto_activate(:before_configuration) + after_configuration_eval(&method(:prune_tilt_templates)) - run_hook :initialized - - run_hook :before_configuration - - evaluate_configuration - - # 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 - - # Clean up missing Tilt exts - Tilt.mappings.each do |key, _| - begin - Tilt[".#{key}"] - rescue LoadError, NameError - Tilt.mappings.delete(key) - end - end - - @extensions.activate_all - - run_hook :after_configuration - config_context.execute_callbacks(:after_configuration) - - run_hook :ready - @config_context.execute_callbacks(:ready) + start_lifecycle end - def evaluate_configuration + # Boot the app. + def start_lifecycle + # Before config is parsed, before extensions get to it. + execute_callbacks(:initialized) + + # Before config is parsed. Mostly used for extensions. + execute_callbacks(:before_configuration) + + # Eval config. + evaluate_configuration! + + # Post parsing, pre-extension callback + execute_callbacks(:after_configuration_eval) + + # After extensions have worked after_config + execute_callbacks(:after_configuration) + + # Everything is stable + execute_callbacks(:ready) + end + + # Eval config + def evaluate_configuration! # Check for and evaluate local configuration in `config.rb` config_rb = File.join(root, 'config.rb') if File.exist? config_rb @@ -311,49 +294,67 @@ module Middleman end # Run any `configure` blocks for the current environment. - config_context.execute_callbacks([:configure, config[:environment]]) + execute_callbacks([:configure, config[:environment]]) # Run any `configure` blocks for the current mode. - config_context.execute_callbacks([:configure, config[:mode]]) + execute_callbacks([:configure, config[:mode]]) + end + + # Clean up missing Tilt exts + def prune_tilt_templates + ::Tilt.mappings.each do |key, _| + begin + ::Tilt[".#{key}"] + rescue LoadError, NameError + ::Tilt.mappings.delete(key) + end + end end # Whether we're in server mode # @return [Boolean] If we're in dev mode + Contract Bool def server? config[:mode] == :server end # Whether we're in build mode # @return [Boolean] If we're in dev mode + Contract Bool def build? config[:mode] == :build end # Whether we're in a specific environment # @return [Boolean] + Contract Bool def environment?(key) config[:environment] == key end # Backwards compatible helper. What the current environment is. # @return [Symbol] + Contract Symbol def environment config[:environment] end # Backwards compatible helper. Whether we're in dev mode. # @return [Boolean] + Contract Bool def development? environment?(:development) end # Backwards compatible helper. Whether we're in production mode. # @return [Boolean] + Contract Bool def production? environment?(:production) end # Backwards compatible helper. The full path to the default source dir. + Contract Pathname def source_dir Pathname(File.join(root, config[:source])) end @@ -376,8 +377,9 @@ module Middleman @mappings << MapDescriptor.new(map, block) end + # Let everyone know we're shutting down. def shutdown! - run_hook :before_shutdown + execute_callbacks(:before_shutdown) end # Work around this bug: http://bugs.ruby-lang.org/issues/4521 diff --git a/middleman-core/lib/middleman-core/builder.rb b/middleman-core/lib/middleman-core/builder.rb index b4f789df..da2bea06 100644 --- a/middleman-core/lib/middleman-core/builder.rb +++ b/middleman-core/lib/middleman-core/builder.rb @@ -2,6 +2,7 @@ require 'pathname' require 'fileutils' require 'tempfile' require 'middleman-core/rack' +require 'middleman-core/callback_manager' require 'middleman-core/contracts' module Middleman @@ -36,10 +37,11 @@ module Middleman @glob = opts.fetch(:glob) @cleaning = opts.fetch(:clean) - @_event_callbacks = [] - rack_app = ::Middleman::Rack.new(@app).to_app @rack = ::Rack::MockRequest.new(rack_app) + + @callbacks = ::Middleman::CallbackManager.new + @callbacks.install_methods!(self, [:on_build_event]) end # Run the build phase. @@ -49,7 +51,7 @@ module Middleman @has_error = false @events = {} - @app.run_hook :before_build, self + @app.execute_callbacks(:before_build, [self]) queue_current_paths if @cleaning prerender_css @@ -60,21 +62,11 @@ module Middleman ::Middleman::Profiling.report('build') - # Run hooks - @app.run_hook :after_build, self - @app.config_context.execute_callbacks(:after_build, [self]) + @app.execute_callbacks(:after_build, [self]) !@has_error end - # Attach callbacks for build events. - # @return [Array] All the attached events. - Contract Proc => ArrayOf[Proc] - def on_build_event(&block) - @_event_callbacks << block if block_given? - @_event_callbacks - end - # Pre-request CSS to give Compass a chance to build sprites # @return [Array] List of css resources that were output. Contract ResourceList @@ -253,9 +245,7 @@ module Middleman @events[event_type] ||= [] @events[event_type] << target - @_event_callbacks.each do |callback| - callback.call(event_type, target, extra) - end + execute_callbacks(:on_build_event, [event_type, target, extra]) end end end diff --git a/middleman-core/lib/middleman-core/callback_manager.rb b/middleman-core/lib/middleman-core/callback_manager.rb index 3bedc8b9..5f9ca360 100644 --- a/middleman-core/lib/middleman-core/callback_manager.rb +++ b/middleman-core/lib/middleman-core/callback_manager.rb @@ -11,22 +11,23 @@ module Middleman @callbacks = ::Hamster.hash end - Contract RespondTo[:define_singleton_method], ArrayOf[Symbol], Maybe[Proc] => Any - def install_methods!(install_target, names, &block) + Contract RespondTo[:define_singleton_method], ArrayOf[Symbol] => Any + def install_methods!(install_target, names) manager = self - names.each do |name| - method_name = block_given? ? block.call(name) : name - + names.each do |method_name| install_target.define_singleton_method(method_name) do |*keys, &b| - key_set = keys.unshift(name) - manager.add(key_set.length > 1 ? key_set : key_set.first, &b) + key_set = keys.unshift(method_name) + manager.add(key_set.length > 1 ? key_set : key_set[0], &b) end end - install_target.define_singleton_method(:execute_callbacks) do |keys, *args| - manager.execute(keys, args, self) + install_target.define_singleton_method(:execute_callbacks) do |*args| + keys = args.shift + manager.execute(keys, args[0], self) end + + install_target.define_singleton_method(:callbacks_for, &method(:callbacks_for)) end Contract Or[Symbol, ArrayOf[Symbol]], Proc => Any @@ -34,16 +35,19 @@ module Middleman immutable_keys = keys.is_a?(Symbol) ? keys : ::Hamster::Vector.new(keys) @callbacks = @callbacks.put(immutable_keys) do |v| - v.nil? ? ::Hamster.set(block) : v.add(block) + v.nil? ? ::Hamster::Vector.new([block]) : v.push(block) end end Contract Or[Symbol, ArrayOf[Symbol]], Maybe[ArrayOf[Any]], Maybe[RespondTo[:instance_exec]] => Any def execute(keys, args=[], scope=self) - immutable_keys = keys.is_a?(Symbol) ? keys : ::Hamster::Vector.new(keys) + callbacks_for(keys).each { |b| scope.instance_exec(*args, &b) } + end - callbacks = @callbacks.get(immutable_keys) - callbacks && callbacks.each { |b| scope.instance_exec(*args, &b) } + Contract Or[Symbol, ArrayOf[Symbol]] => ::Hamster::Vector + def callbacks_for(keys) + immutable_keys = keys.is_a?(Symbol) ? keys : ::Hamster::Vector.new(keys) + @callbacks.get(immutable_keys) || ::Hamster.vector end end end diff --git a/middleman-core/lib/middleman-core/config_context.rb b/middleman-core/lib/middleman-core/config_context.rb index 7c644f70..ded3ae15 100644 --- a/middleman-core/lib/middleman-core/config_context.rb +++ b/middleman-core/lib/middleman-core/config_context.rb @@ -15,8 +15,19 @@ module Middleman @app = app @template_context_class = template_context_class + sub_callbacks = [:before_build, :after_build, :configure, :after_configuration, :ready] + @callbacks = ::Middleman::CallbackManager.new - @callbacks.install_methods!(self, [:ready, :after_build, :after_configuration, :configure]) + @callbacks.install_methods!(self, sub_callbacks) + + # Trigger internal callbacks when app level are executed. + self_context = self + + sub_callbacks.each do |key| + app.send(key) do |*args| + self_context.execute_callbacks(key, args) + end + end end def helpers(*helper_modules, &block) diff --git a/middleman-core/lib/middleman-core/core_extensions/i18n.rb b/middleman-core/lib/middleman-core/core_extensions/i18n.rb index c9d74572..2f5f3bb9 100644 --- a/middleman-core/lib/middleman-core/core_extensions/i18n.rb +++ b/middleman-core/lib/middleman-core/core_extensions/i18n.rb @@ -10,6 +10,25 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension # Exposes `langs` to templates expose_to_template :langs + def initialize(*) + super + + require 'i18n' + + # Don't fail on invalid locale, that's not what our current + # users expect. + ::I18n.enforce_available_locales = false + + # 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. + app.after_configuration_eval do + ::I18n.load_path.delete_if { |path| path =~ %r{tmp/aruba} } + ::I18n.reload! + end if ENV['TEST'] + end + def after_configuration # See https://github.com/svenfuchs/i18n/wiki/Fallbacks unless options[:no_fallbacks] diff --git a/middleman-core/lib/middleman-core/extension.rb b/middleman-core/lib/middleman-core/extension.rb index 2c15eef0..3747b7d5 100644 --- a/middleman-core/lib/middleman-core/extension.rb +++ b/middleman-core/lib/middleman-core/extension.rb @@ -380,6 +380,7 @@ module Middleman def bind_after_configuration ext = self + @app.after_configuration do ext.after_configuration if ext.respond_to?(:after_configuration) diff --git a/middleman-core/lib/middleman-core/extension_manager.rb b/middleman-core/lib/middleman-core/extension_manager.rb index 6b1fbe93..916713b0 100644 --- a/middleman-core/lib/middleman-core/extension_manager.rb +++ b/middleman-core/lib/middleman-core/extension_manager.rb @@ -8,6 +8,17 @@ module Middleman def initialize(app) @app = app @activated = {} + + manager = self + { + before_sitemap: :before_sitemap, + initialized: :before_configuration + }.each do |key, value| + cb = proc { manager.auto_activate(value) } + @app.send(key, &cb) + end + + @app.after_configuration_eval(&method(:activate_all)) end def auto_activate(key) diff --git a/middleman-core/lib/middleman-core/file_renderer.rb b/middleman-core/lib/middleman-core/file_renderer.rb index 91b083e1..eb98e657 100644 --- a/middleman-core/lib/middleman-core/file_renderer.rb +++ b/middleman-core/lib/middleman-core/file_renderer.rb @@ -59,14 +59,10 @@ module Middleman options = options.deep_merge(options[:renderer_options]) if options[:renderer_options] template_class = ::Tilt[path] + # Allow hooks to manipulate the template before render - @app.class.callbacks_for_hook(:before_render).each do |callback| - newbody = if callback.respond_to?(:call) - callback.call(body, path, locs, template_class) - elsif callback.respond_to?(:evaluate) - callback.evaluate(self, body, path, locs, template_class) - end - body = newbody if newbody # Allow the callback to return nil to skip it + body = @app.callbacks_for(:before_render).reduce(body) do |sum, callback| + callback.call(sum, path, locs, template_class) || sum end # Read compiled template from disk or cache @@ -80,14 +76,8 @@ module Middleman end # Allow hooks to manipulate the result after render - @app.class.callbacks_for_hook(:after_render).each do |callback| - # Uber::Options::Value doesn't respond to call - newcontent = if callback.respond_to?(:call) - callback.call(content, path, locs, template_class) - elsif callback.respond_to?(:evaluate) - callback.evaluate(self, content, path, locs, template_class) - end - content = newcontent if newcontent # Allow the callback to return nil to skip it + content = @app.callbacks_for(:before_render).reduce(content) do |sum, callback| + callback.call(sum, path, locs, template_class) || sum end output = ::ActiveSupport::SafeBuffer.new '' diff --git a/middleman-core/lib/middleman-core/rack.rb b/middleman-core/lib/middleman-core/rack.rb index 9cc79212..2da9ce21 100644 --- a/middleman-core/lib/middleman-core/rack.rb +++ b/middleman-core/lib/middleman-core/rack.rb @@ -85,7 +85,7 @@ module Middleman full_request_path = File.join(env['SCRIPT_NAME'], request_path) # Path including rack mount # Run before callbacks - @middleman.run_hook :before + @middleman.execute_callbacks(:before) # Get the resource object for this path resource = @middleman.sitemap.find_resource_by_destination_path(request_path.gsub(' ', '%20')) diff --git a/middleman-core/lib/middleman-core/renderers/coffee_script.rb b/middleman-core/lib/middleman-core/renderers/coffee_script.rb index ba39159c..82e1707c 100644 --- a/middleman-core/lib/middleman-core/renderers/coffee_script.rb +++ b/middleman-core/lib/middleman-core/renderers/coffee_script.rb @@ -13,9 +13,7 @@ module Middleman ::Tilt.register 'coffee', DebuggingCoffeeScriptTemplate ::Tilt.prefer(DebuggingCoffeeScriptTemplate) - app.before_configuration do - DebuggingCoffeeScriptTemplate.middleman_app = self - end + DebuggingCoffeeScriptTemplate.middleman_app = app end # A Template for Tilt which outputs debug messages diff --git a/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb b/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb index 58b89776..9ba53899 100644 --- a/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb +++ b/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb @@ -12,7 +12,7 @@ module Middleman def initialize(app, config={}, &block) super - # Array of callbacks which can ass ignored + # Array of callbacks which can assign ignored @ignored_callbacks = Set.new @app.sitemap.define_singleton_method(:ignored?, &method(:ignored?)) diff --git a/middleman-core/lib/middleman-core/sources.rb b/middleman-core/lib/middleman-core/sources.rb index 87b0d957..34d2ab8a 100644 --- a/middleman-core/lib/middleman-core/sources.rb +++ b/middleman-core/lib/middleman-core/sources.rb @@ -345,4 +345,4 @@ module Middleman end # And, require the actual default implementation for a watcher. -require 'middleman-core/sources/source_watcher.rb' +require 'middleman-core/sources/source_watcher' diff --git a/middleman-core/lib/middleman-core/sources/source_watcher.rb b/middleman-core/lib/middleman-core/sources/source_watcher.rb index d2bbbe92..80b3702f 100644 --- a/middleman-core/lib/middleman-core/sources/source_watcher.rb +++ b/middleman-core/lib/middleman-core/sources/source_watcher.rb @@ -1,6 +1,7 @@ # Watcher Library require 'listen' require 'middleman-core/contracts' +require 'middleman-core/contracts' require 'backports/2.0.0/enumerable/lazy' module Middleman @@ -55,7 +56,8 @@ module Middleman @listener = nil - @on_change_callbacks = Set.new + @callbacks = ::Middleman::CallbackManager.new + @callbacks.install_methods!(self, [:on_change]) @waiting_for_existence = !@directory.exist? end @@ -174,16 +176,6 @@ module Middleman listen! end - # Add callback to be run on file change - # - # @param [Proc] matcher A Regexp to match the change path against - # @return [Set] - Contract Proc => SetOf[Proc] - def on_change(&block) - @on_change_callbacks << block - @on_change_callbacks - end - # Work around this bug: http://bugs.ruby-lang.org/issues/4521 # where Ruby will call to_s/inspect while printing exception # messages, which can take a long time (minutes at full CPU) @@ -237,11 +229,11 @@ module Middleman logger.debug "== Deletion (#{f[:types].inspect}): #{f[:relative_path]}" end - run_callbacks( - @on_change_callbacks, + execute_callbacks(:on_change, [ valid_updates, - valid_removes - ) unless valid_updates.empty? && valid_removes.empty? + valid_removes, + self + ]) unless valid_updates.empty? && valid_removes.empty? end def add_file_to_cache(f) @@ -289,17 +281,5 @@ module Middleman ::Middleman::SourceFile.new(Pathname(relative_path), path, @directory, types) end - - # Notify callbacks for a file given an array of callbacks - # - # @param [Pathname] path The file that was changed - # @param [Symbol] callbacks_name The name of the callbacks method - # @return [void] - Contract Set, ArrayOf[IsA['Middleman::SourceFile']], ArrayOf[IsA['Middleman::SourceFile']] => Any - def run_callbacks(callbacks, updated_files, removed_files) - callbacks.each do |callback| - callback.call(updated_files, removed_files, self) - end - end end end diff --git a/middleman-core/middleman-core.gemspec b/middleman-core/middleman-core.gemspec index 7fbc48d7..752108eb 100644 --- a/middleman-core/middleman-core.gemspec +++ b/middleman-core/middleman-core.gemspec @@ -24,7 +24,6 @@ Gem::Specification.new do |s| s.add_dependency('rack', ['>= 1.4.5', '< 2.0']) s.add_dependency('tilt', ['~> 1.4.1']) s.add_dependency('erubis') - s.add_dependency('hooks', ['~> 0.3']) # Helpers s.add_dependency('activesupport', ['~> 4.2.0']) diff --git a/middleman-core/spec/middleman-core/callbacks_spec.rb b/middleman-core/spec/middleman-core/callbacks_spec.rb new file mode 100644 index 00000000..35cad47f --- /dev/null +++ b/middleman-core/spec/middleman-core/callbacks_spec.rb @@ -0,0 +1,132 @@ +# require 'spec_helper' +require 'middleman-core/callback_manager' + +describe ::Middleman::CallbackManager do + it "adds a simple key" do + counters = { + test1: 0, + test2: 0, + test3: 0 + } + + m = ::Middleman::CallbackManager.new + m.add(:test3) { counters[:test3] += 1 } + m.add(:test1) { counters[:test1] += 1 } + m.add(:test2) { counters[:test2] += 1 } + m.add(:test1) { counters[:test1] += 1 } + m.add(:test2) { counters[:test2] += 1 } + m.add(:test1) { counters[:test1] += 1 } + m.add(:test3) { counters[:test3] += 1 } + + m.execute(:test1) + m.execute(:test2) + + expect(counters[:test1]).to eq 3 + expect(counters[:test2]).to eq 2 + expect(counters[:test3]).to eq 0 + end + + it "callbacks run in order" do + result = [] + + m = ::Middleman::CallbackManager.new + m.add(:test) { result.push(1) } + m.add(:test) { result.push(2) } + m.add(:test) { result.push(3) } + + m.execute(:test) + + expect(result.join('')).to eq '123' + end + + it "adds a nested key" do + counters = { + test1: 0, + test1a: 0 + } + + m = ::Middleman::CallbackManager.new + m.add([:test1, :a]) { |n| counters[:test1a] += n } + m.add(:test1) { counters[:test1] += 1 } + + m.execute([:test1, :a], [2]) + m.execute([:test1, :b], [5]) + + expect(counters[:test1]).to eq 0 + expect(counters[:test1a]).to eq 2 + end + + it "works in isolation" do + m1 = ::Middleman::CallbackManager.new + m2 = ::Middleman::CallbackManager.new + + counters = { + test1: 0, + test2: 0 + } + + m1.add(:test1) { |n| counters[:test1] += n } + m2.add(:test1) { |n| counters[:test2] += n } + + m1.execute(:test1, [2]) + m2.execute(:test1, [5]) + m1.execute(:test2, [20]) + m2.execute(:test2, [50]) + + expect(counters[:test1]).to eq 2 + expect(counters[:test2]).to eq 5 + end + + it "installs to arbitrary instances" do + instance = Class.new(Object).new + + m = ::Middleman::CallbackManager.new + m.install_methods!(instance, [:ready]) + + counter = 0 + instance.ready { |n| counter += n } + instance.execute_callbacks(:ready, [2]) + instance.execute_callbacks(:ready2, [10]) + instance.execute_callbacks([:ready], [20]) + instance.execute_callbacks([:ready, :two], [20]) + expect(counter).to eq 2 + end + + it "executes in default scope" do + instance = Class.new(Object).new + m = ::Middleman::CallbackManager.new + m.install_methods!(instance, [:ready]) + + internal_self = nil + instance.ready do + internal_self = self + end + + instance.execute_callbacks(:ready) + + expect(internal_self) === instance + end + + it "executes in custom scope" do + instance = Class.new(Object).new + m = ::Middleman::CallbackManager.new + m.install_methods!(instance, [:ready]) + + external_class = Struct.new(:counter, :scope) do + def when_ready(n) + self[:scope] = self + self[:counter] += n + end + end + + external_instance = external_class.new(0, nil) + + instance.ready(&external_instance.method(:when_ready)) + + instance.execute_callbacks(:ready, [5]) + + expect(external_instance[:scope]).to eq external_instance + expect(external_instance[:counter]).to eq 5 + end + +end \ No newline at end of file