Merge pull request #424 from middleman/remove_ruby_autoload

Remove usage of autoload statement in favor of require
This commit is contained in:
Thomas Reynolds 2012-05-08 16:56:53 -07:00
commit b486b5a31d
27 changed files with 2122 additions and 2048 deletions

View file

@ -3,7 +3,12 @@ libdir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
# Top-level Middleman namespace # Top-level Middleman namespace
module Middleman; end module Middleman
# Backwards compatibility namespace
module Features; end
end
require "middleman-core/version" require "middleman-core/version"
require "middleman-core/util" require "middleman-core/util"

View file

@ -9,6 +9,8 @@ require "middleman-core/vendor/hooks-0.2.0/lib/hooks"
require "middleman-core/sitemap" require "middleman-core/sitemap"
require "middleman-core/core_extensions"
# Core Middleman Class # Core Middleman Class
module Middleman module Middleman
class Application class Application
@ -160,14 +162,35 @@ module Middleman
register Middleman::CoreExtensions::I18n register Middleman::CoreExtensions::I18n
# Built-in Extensions # Built-in Extensions
Middleman::Extensions.register(:directory_indexes) {
Middleman::Extensions::DirectoryIndexes } # Provide Apache-style index.html files for directories
Middleman::Extensions.register(:lorem) { Middleman::Extensions.register(:directory_indexes) do
Middleman::Extensions::Lorem } require "middleman-core/extensions/directory_indexes"
Middleman::Extensions.register(:automatic_image_sizes) { Middleman::Extensions::DirectoryIndexes
Middleman::Extensions::AutomaticImageSizes } end
Middleman::Extensions.register(:asset_host) {
Middleman::Extensions::AssetHost } # Lorem provides a handful of helpful prototyping methods to generate
# words, paragraphs, fake images, names and email addresses.
Middleman::Extensions.register(:lorem) do
require "middleman-core/extensions/lorem"
Middleman::Extensions::Lorem
end
# AutomaticImageSizes inspects the images used in your dynamic templates
# and automatically adds width and height attributes to their HTML
# elements.
Middleman::Extensions.register(:automatic_image_sizes) do
require "middleman-core/extensions/automatic_image_sizes"
Middleman::Extensions::AutomaticImageSizes
end
# AssetHost allows you to setup multiple domains to host your static
# assets. Calls to asset paths in dynamic templates will then rotate
# through each of the asset servers to better spread the load.
Middleman::Extensions.register(:asset_host) do
require "middleman-core/extensions/asset_host"
Middleman::Extensions::AssetHost
end
# Initialize the Middleman project # Initialize the Middleman project
def initialize(&block) def initialize(&block)

View file

@ -5,13 +5,6 @@ require "thor/group"
# CLI Module # CLI Module
module Middleman::Cli module Middleman::Cli
module Cli
autoload :Build, "middleman-core/cli/build"
autoload :Init, "middleman-core/cli/init"
autoload :Extension, "middleman-core/cli/extension"
autoload :Server, "middleman-core/cli/server"
end
# The base task from which everything else etends # The base task from which everything else etends
class Base < Thor class Base < Thor

View file

@ -0,0 +1,39 @@
# Rack Request
require "middleman-core/core_extensions/request"
# File Change Notifier
require "middleman-core/core_extensions/file_watcher"
# Add Builder callbacks
require "middleman-core/core_extensions/builder"
# Custom Feature API
require "middleman-core/core_extensions/extensions"
# Asset Path Pipeline
require "middleman-core/core_extensions/assets"
# Data looks at the data/ folder for YAML files and makes them available
# to dynamic requests.
require "middleman-core/core_extensions/data"
# Parse YAML from templates
require "middleman-core/core_extensions/front_matter"
# External helpers looks in the helpers/ folder for helper modules
require "middleman-core/core_extensions/external_helpers"
# DefaultHelpers are the built-in dynamic template helpers.
require "middleman-core/core_extensions/default_helpers"
# Extended version of Padrino's rendering
require "middleman-core/core_extensions/rendering"
# Pass custom options to views
require "middleman-core/core_extensions/routing"
# Catch and show exceptions at the Rack level
require "middleman-core/core_extensions/show_exceptions"
# i18n
require "middleman-core/core_extensions/i18n"

View file

@ -1,38 +1,42 @@
# Base helper to manipulate asset paths module Middleman
module Middleman::CoreExtensions::Assets module CoreExtensions
# Extension registered # Base helper to manipulate asset paths
class << self module Assets
# @private
def registered(app)
# Disable Padrino cache buster
app.set :asset_stamp, false
# Include helpers # Extension registered
app.send :include, InstanceMethod class << self
end def registered(app)
alias :included :registered # Disable Padrino cache buster
end app.set :asset_stamp, false
# Methods to be mixed-in to Middleman::Application # Include helpers
module InstanceMethod app.send :include, InstanceMethod
# Get the URL of an asset given a type/prefix
#
# @param [String] path The path (such as "photo.jpg")
# @param [String] prefix The type prefix (such as "images")
# @return [String] The fully qualified asset url
def asset_url(path, prefix="")
# Don't touch assets which already have a full path
if path.include?("//")
path
else # rewrite paths to use their destination path
path = File.join(prefix, path)
if resource = sitemap.find_resource_by_path(path)
path = resource.destination_path
end end
alias :included :registered
end
File.join(http_prefix, path) # Methods to be mixed-in to Middleman::Application
module InstanceMethod
# Get the URL of an asset given a type/prefix
#
# @param [String] path The path (such as "photo.jpg")
# @param [String] prefix The type prefix (such as "images")
# @return [String] The fully qualified asset url
def asset_url(path, prefix="")
# Don't touch assets which already have a full path
if path.include?("//")
path
else # rewrite paths to use their destination path
path = File.join(prefix, path)
if resource = sitemap.find_resource_by_path(path)
path = resource.destination_path
end
File.join(http_prefix, path)
end
end
end end
end end
end end

View file

@ -1,12 +1,17 @@
# Convenience methods to allow config.rb to talk to the Builder module Middleman
module Middleman::CoreExtensions::Builder module CoreExtensions
# Extension registered # Convenience methods to allow config.rb to talk to the Builder
class << self module Builder
# @private
def registered(app) # Extension registered
app.define_hook :after_build class << self
# @private
def registered(app)
app.define_hook :after_build
end
alias :included :registered
end
end end
alias :included :registered
end end
end end

View file

@ -1,175 +1,180 @@
# Data formats module Middleman
require "yaml" module CoreExtensions
require "active_support/json"
# The data extension parses YAML and JSON files in the data/ directory # The data extension parses YAML and JSON files in the data/ directory
# and makes them available to config.rb, templates and extensions # and makes them available to config.rb, templates and extensions
module Middleman::CoreExtensions::Data module Data
# Extension registered # Extension registered
class << self class << self
# @private # @private
def registered(app) def registered(app)
app.set :data_dir, "data" # Data formats
app.send :include, InstanceMethods require "yaml"
end require "active_support/json"
alias :included :registered
end
# Instance methods app.set :data_dir, "data"
module InstanceMethods app.send :include, InstanceMethods
# Setup data files before anything else so they are available when end
# parsing config.rb alias :included :registered
def initialize
self.files.changed DataStore.matcher do |file|
self.data.touch_file(file) if file.match(%r{^#{self.data_dir}\/})
end end
self.files.deleted DataStore.matcher do |file| # Instance methods
self.data.remove_file(file) if file.match(%r{^#{self.data_dir}\/}) module InstanceMethods
end # Setup data files before anything else so they are available when
# parsing config.rb
def initialize
self.files.changed DataStore.matcher do |file|
self.data.touch_file(file) if file.match(%r{^#{self.data_dir}\/})
end
super self.files.deleted DataStore.matcher do |file|
end self.data.remove_file(file) if file.match(%r{^#{self.data_dir}\/})
end
# The data object super
# end
# @return [DataStore]
def data
@_data ||= DataStore.new(self)
end
end
# The core logic behind the data extension. # The data object
class DataStore #
# @return [DataStore]
# Static methods def data
class << self @_data ||= DataStore.new(self)
# The regex which tells Middleman which files are for data
#
# @return [Regexp]
def matcher
%r{[\w-]+\.(yml|yaml|json)$}
end
end
# Store static data hash
#
# @param [Symbol] name Name of the data, used for namespacing
# @param [Hash] content The content for this data
# @return [void]
def store(name=nil, content=nil)
@_local_sources ||= {}
@_local_sources[name.to_s] = content unless name.nil? || content.nil?
@_local_sources
end
# Store callback-based data
#
# @param [Symbol] name Name of the data, used for namespacing
# @param [Proc] proc The callback which will return data
# @return [void]
def callbacks(name=nil, proc=nil)
@_callback_sources ||= {}
@_callback_sources[name.to_s] = proc unless name.nil? || proc.nil?
@_callback_sources
end
# Setup data store
#
# @param [Middleman::Application] app The current instance of Middleman
def initialize(app)
@app = app
@local_data = {}
end
# Update the internal cache for a given file path
#
# @param [String] file The file to be re-parsed
# @return [void]
def touch_file(file)
file = File.expand_path(file, @app.root)
extension = File.extname(file)
basename = File.basename(file, extension)
if %w(.yaml .yml).include?(extension)
data = YAML.load_file(file)
elsif extension == ".json"
data = ActiveSupport::JSON.decode(File.read(file))
else
return
end
@local_data[basename] = ::Middleman::Util.recursively_enhance(data)
end
# Remove a given file from the internal cache
#
# @param [String] file The file to be cleared
# @return [void]
def remove_file(file)
extension = File.extname(file)
basename = File.basename(file, extension)
@local_data.delete(basename) if @local_data.has_key?(basename)
end
# Get a hash hash from either internal static data or a callback
#
# @param [String, Symbol] path The name of the data namespace
# @return [Hash, nil]
def data_for_path(path)
response = nil
@@local_sources ||= {}
@@callback_sources ||= {}
if self.store.has_key?(path.to_s)
response = self.store[path.to_s]
elsif self.callbacks.has_key?(path.to_s)
response = self.callbacks[path.to_s].call()
end
response
end
# "Magically" find namespaces of data if they exist
#
# @param [String] path The namespace to search for
# @return [Hash, nil]
def method_missing(path)
if @local_data.has_key?(path.to_s)
return @local_data[path.to_s]
else
result = data_for_path(path)
if result
return ::Middleman::Util.recursively_enhance(result)
end end
end end
super # The core logic behind the data extension.
end class DataStore
# Convert all the data into a static hash # Static methods
# class << self
# @return [Hash]
def to_h
data = {}
self.store.each do |k, v| # The regex which tells Middleman which files are for data
data[k] = data_for_path(k) #
# @return [Regexp]
def matcher
%r{[\w-]+\.(yml|yaml|json)$}
end
end
# Store static data hash
#
# @param [Symbol] name Name of the data, used for namespacing
# @param [Hash] content The content for this data
# @return [void]
def store(name=nil, content=nil)
@_local_sources ||= {}
@_local_sources[name.to_s] = content unless name.nil? || content.nil?
@_local_sources
end
# Store callback-based data
#
# @param [Symbol] name Name of the data, used for namespacing
# @param [Proc] proc The callback which will return data
# @return [void]
def callbacks(name=nil, proc=nil)
@_callback_sources ||= {}
@_callback_sources[name.to_s] = proc unless name.nil? || proc.nil?
@_callback_sources
end
# Setup data store
#
# @param [Middleman::Application] app The current instance of Middleman
def initialize(app)
@app = app
@local_data = {}
end
# Update the internal cache for a given file path
#
# @param [String] file The file to be re-parsed
# @return [void]
def touch_file(file)
file = File.expand_path(file, @app.root)
extension = File.extname(file)
basename = File.basename(file, extension)
if %w(.yaml .yml).include?(extension)
data = YAML.load_file(file)
elsif extension == ".json"
data = ActiveSupport::JSON.decode(File.read(file))
else
return
end
@local_data[basename] = ::Middleman::Util.recursively_enhance(data)
end
# Remove a given file from the internal cache
#
# @param [String] file The file to be cleared
# @return [void]
def remove_file(file)
extension = File.extname(file)
basename = File.basename(file, extension)
@local_data.delete(basename) if @local_data.has_key?(basename)
end
# Get a hash hash from either internal static data or a callback
#
# @param [String, Symbol] path The name of the data namespace
# @return [Hash, nil]
def data_for_path(path)
response = nil
@@local_sources ||= {}
@@callback_sources ||= {}
if self.store.has_key?(path.to_s)
response = self.store[path.to_s]
elsif self.callbacks.has_key?(path.to_s)
response = self.callbacks[path.to_s].call()
end
response
end
# "Magically" find namespaces of data if they exist
#
# @param [String] path The namespace to search for
# @return [Hash, nil]
def method_missing(path)
if @local_data.has_key?(path.to_s)
return @local_data[path.to_s]
else
result = data_for_path(path)
if result
return ::Middleman::Util.recursively_enhance(result)
end
end
super
end
# Convert all the data into a static hash
#
# @return [Hash]
def to_h
data = {}
self.store.each do |k, v|
data[k] = data_for_path(k)
end
self.callbacks.each do |k, v|
data[k] = data_for_path(k)
end
(@local_data || {}).each do |k, v|
data[k] = v
end
data
end
end end
self.callbacks.each do |k, v|
data[k] = data_for_path(k)
end
(@local_data || {}).each do |k, v|
data[k] = v
end
data
end end
end end
end end

View file

@ -1,119 +1,123 @@
require "active_support/core_ext/integer/inflections" module Middleman
require 'padrino-helpers' module CoreExtensions
# Built-in helpers
module DefaultHelpers
# Built-in helpers # Extension registered
module Middleman::CoreExtensions::DefaultHelpers class << self
# @private
def registered(app)
require "active_support/core_ext/integer/inflections"
require 'padrino-helpers'
# Extension registered app.helpers ::Padrino::Helpers::OutputHelpers
class << self app.helpers ::Padrino::Helpers::TagHelpers
# @private app.helpers ::Padrino::Helpers::AssetTagHelpers
def registered(app) app.helpers ::Padrino::Helpers::FormHelpers
app.helpers ::Padrino::Helpers::OutputHelpers app.helpers ::Padrino::Helpers::FormatHelpers
app.helpers ::Padrino::Helpers::TagHelpers app.helpers ::Padrino::Helpers::RenderHelpers
app.helpers ::Padrino::Helpers::AssetTagHelpers app.helpers ::Padrino::Helpers::NumberHelpers
app.helpers ::Padrino::Helpers::FormHelpers # app.helpers ::Padrino::Helpers::TranslationHelpers
app.helpers ::Padrino::Helpers::FormatHelpers
app.helpers ::Padrino::Helpers::RenderHelpers
app.helpers ::Padrino::Helpers::NumberHelpers
# app.helpers ::Padrino::Helpers::TranslationHelpers
app.helpers Helpers app.helpers Helpers
end end
alias :included :registered alias :included :registered
end
# The helpers
module Helpers
# Output a stylesheet link tag based on the current path
#
# @param [String] separator How to break up path in parts
# @return [String]
def auto_stylesheet_link_tag(separator="/")
auto_tag(:css, separator) do |path|
stylesheet_link_tag path
end end
end
# Output a javascript tag based on the current path # The helpers
# module Helpers
# @param [String] separator How to break up path in parts # Output a stylesheet link tag based on the current path
# @return [String] #
def auto_javascript_include_tag(separator="/") # @param [String] separator How to break up path in parts
auto_tag(:js, separator) do |path| # @return [String]
javascript_include_tag path def auto_stylesheet_link_tag(separator="/")
end auto_tag(:css, separator) do |path|
end stylesheet_link_tag path
end
end
# Output a stylesheet link tag based on the current path # Output a javascript tag based on the current path
# #
# @param [Symbol] asset_ext The type of asset # @param [String] separator How to break up path in parts
# @param [String] separator How to break up path in parts # @return [String]
# @param [String] asset_dir Where to look for assets def auto_javascript_include_tag(separator="/")
# @return [void] auto_tag(:js, separator) do |path|
def auto_tag(asset_ext, separator="/", asset_dir=nil) javascript_include_tag path
if asset_dir.nil? end
asset_dir = case asset_ext end
when :js then js_dir
when :css then css_dir # Output a stylesheet link tag based on the current path
#
# @param [Symbol] asset_ext The type of asset
# @param [String] separator How to break up path in parts
# @param [String] asset_dir Where to look for assets
# @return [void]
def auto_tag(asset_ext, separator="/", asset_dir=nil)
if asset_dir.nil?
asset_dir = case asset_ext
when :js then js_dir
when :css then css_dir
end
end
# If the basename of the request as no extension, assume we are serving a
# directory and join index_file to the path.
path = full_path(current_path.dup)
path = path.sub(%r{^/}, '')
path = path.gsub(File.extname(path), ".#{asset_ext}")
path = path.gsub("/", separator)
yield path if sitemap.find_resource_by_path(File.join(asset_dir, path))
end
# Generate body css classes based on the current path
#
# @return [String]
def page_classes
path = current_path.dup
path << index_file if path.match(%r{/$})
path = path.gsub(%r{^/}, '')
classes = []
parts = path.split('.')[0].split('/')
parts.each_with_index { |path, i| classes << parts.first(i+1).join('_') }
classes.join(' ')
end
# Get the path of a file of a given type
#
# @param [Symbol] kind The type of file
# @param [String] source The path to the file
# @return [String]
def asset_path(kind, source)
return source if source =~ /^http/
asset_folder = case kind
when :css then css_dir
when :js then js_dir
when :images then images_dir
else kind.to_s
end
source = source.to_s.gsub(/\s/, '')
ignore_extension = (kind == :images) # don't append extension
source << ".#{kind}" unless ignore_extension or source =~ /\.#{kind}/
result_path = source if source =~ %r{^/} # absolute path
result_path ||= asset_url(source, asset_folder)
"#{result_path}"
end
def link_to(*args, &block)
url_arg_index = block_given? ? 0 : 1
if url = args[url_arg_index]
# Only try to work with absolute URLs
if url.start_with? '/'
resource = sitemap.find_resource_by_path(url)
args[url_arg_index] = resource.url if resource
end
end
super(*args, &block)
end end
end end
# If the basename of the request as no extension, assume we are serving a
# directory and join index_file to the path.
path = full_path(current_path.dup)
path = path.sub(%r{^/}, '')
path = path.gsub(File.extname(path), ".#{asset_ext}")
path = path.gsub("/", separator)
yield path if sitemap.find_resource_by_path(File.join(asset_dir, path))
end
# Generate body css classes based on the current path
#
# @return [String]
def page_classes
path = current_path.dup
path << index_file if path.match(%r{/$})
path = path.gsub(%r{^/}, '')
classes = []
parts = path.split('.')[0].split('/')
parts.each_with_index { |path, i| classes << parts.first(i+1).join('_') }
classes.join(' ')
end
# Get the path of a file of a given type
#
# @param [Symbol] kind The type of file
# @param [String] source The path to the file
# @return [String]
def asset_path(kind, source)
return source if source =~ /^http/
asset_folder = case kind
when :css then css_dir
when :js then js_dir
when :images then images_dir
else kind.to_s
end
source = source.to_s.gsub(/\s/, '')
ignore_extension = (kind == :images) # don't append extension
source << ".#{kind}" unless ignore_extension or source =~ /\.#{kind}/
result_path = source if source =~ %r{^/} # absolute path
result_path ||= asset_url(source, asset_folder)
"#{result_path}"
end
def link_to(*args, &block)
url_arg_index = block_given? ? 0 : 1
if url = args[url_arg_index]
# Only try to work with absolute URLs
if url.start_with? '/'
resource = sitemap.find_resource_by_path(url)
args[url_arg_index] = resource.url if resource
end
end
super(*args, &block)
end end
end end
end end

View file

@ -28,123 +28,127 @@
# methods to use in your views. Some modify the output on-the-fly. And some # methods to use in your views. Some modify the output on-the-fly. And some
# apply computationally-intensive changes to your final build files. # apply computationally-intensive changes to your final build files.
# Using for version parsing
require "rubygems"
# Namespace extensions module # Namespace extensions module
module Middleman::CoreExtensions::Extensions module Middleman
module CoreExtensions
module Extensions
# Register extension # Register extension
class << self class << self
# @private # @private
def included(app) def included(app)
# app.set :default_extensions, [] # Using for version parsing
app.define_hook :after_configuration require "rubygems"
app.define_hook :before_configuration
app.define_hook :build_config
app.define_hook :development_config
app.extend ClassMethods # app.set :default_extensions, []
app.send :include, InstanceMethods app.define_hook :after_configuration
app.delegate :configure, :to => :"self.class" app.define_hook :before_configuration
end app.define_hook :build_config
end app.define_hook :development_config
# Class methods app.extend ClassMethods
module ClassMethods app.send :include, InstanceMethods
# Add a callback to run in a specific environment app.delegate :configure, :to => :"self.class"
#
# @param [String, Symbol] env The environment to run in
# @return [void]
def configure(env, &block)
send("#{env}_config", &block)
end
# Alias `extensions` to access registered extensions
#
# @return [Array<Module>]
def extensions
@extensions ||= []
end
# Register a new extension
#
# @param [Module] extension Extension modules to register
# @param [Hash] options Per-extension options hash
# @return [void]
def register(extension, options={}, &block)
@extensions ||= []
@extensions += [extension]
extend extension
if extension.respond_to?(:registered)
if extension.method(:registered).arity === 1
extension.registered(self, &block)
else
extension.registered(self, options, &block)
end end
end end
end
end
# Instance methods # Class methods
module InstanceMethods module ClassMethods
# This method is available in the project's `config.rb`. # Add a callback to run in a specific environment
# It takes a underscore-separated symbol, finds the appropriate #
# feature module and includes it. # @param [String, Symbol] env The environment to run in
# # @return [void]
# activate :lorem def configure(env, &block)
# send("#{env}_config", &block)
# @param [Symbol, Module] ext Which extension to activate end
# @return [void]
def activate(ext, options={}, &block)
# Make :i18n a no-op
return if ext == :i18n
ext_module = if ext.is_a?(Module) # Alias `extensions` to access registered extensions
ext #
else # @return [Array<Module>]
::Middleman::Extensions.load(ext.to_sym) def extensions
@extensions ||= []
end
# Register a new extension
#
# @param [Module] extension Extension modules to register
# @param [Hash] options Per-extension options hash
# @return [void]
def register(extension, options={}, &block)
@extensions ||= []
@extensions += [extension]
extend extension
if extension.respond_to?(:registered)
if extension.method(:registered).arity === 1
extension.registered(self, &block)
else
extension.registered(self, options, &block)
end
end
end
end end
if ext_module.nil? # Instance methods
puts "== Unknown Extension: #{ext}" module InstanceMethods
else # This method is available in the project's `config.rb`.
puts "== Activating: #{ext}" if logging? # It takes a underscore-separated symbol, finds the appropriate
self.class.register(ext_module, options, &block) # feature module and includes it.
end #
end # activate :lorem
#
# @param [Symbol, Module] ext Which extension to activate
# @return [void]
def activate(ext, options={}, &block)
# Make :i18n a no-op
return if ext == :i18n
# Load features before starting server ext_module = if ext.is_a?(Module)
def initialize ext
super else
::Middleman::Extensions.load(ext.to_sym)
end
self.class.inst = self if ext_module.nil?
run_hook :before_configuration puts "== Unknown Extension: #{ext}"
else
puts "== Activating: #{ext}" if logging?
self.class.register(ext_module, options, &block)
end
end
# Search the root of the project for required files # Load features before starting server
$LOAD_PATH.unshift(root) def initialize
super
# Check for and evaluate local configuration self.class.inst = self
local_config = File.join(root, "config.rb") run_hook :before_configuration
if File.exists? local_config
puts "== Reading: Local config" if logging?
instance_eval File.read(local_config), local_config, 1
end
run_hook :build_config if build? # Search the root of the project for required files
run_hook :development_config if development? $LOAD_PATH.unshift(root)
run_hook :after_configuration # Check for and evaluate local configuration
local_config = File.join(root, "config.rb")
if File.exists? local_config
puts "== Reading: Local config" if logging?
instance_eval File.read(local_config), local_config, 1
end
# Add in defaults run_hook :build_config if build?
default_extensions.each do |ext| run_hook :development_config if development?
activate ext
end
if logging? run_hook :after_configuration
self.class.extensions.each do |ext|
puts "== Extension: #{ext}" # Add in defaults
default_extensions.each do |ext|
activate ext
end
if logging?
self.class.extensions.each do |ext|
puts "== Extension: #{ext}"
end
end
end end
end end
end end

View file

@ -1,35 +1,39 @@
# Load helpers in helpers/ # Load helpers in helpers/
module Middleman::CoreExtensions::ExternalHelpers module Middleman
module CoreExtensions
module ExternalHelpers
# Setup extension # Setup extension
class << self class << self
# once registered # once registered
def registered(app) def registered(app)
# Setup a default helpers paths # Setup a default helpers paths
app.set :helpers_dir, "helpers" app.set :helpers_dir, "helpers"
app.set :helpers_filename_glob, "**/*_helper.rb" app.set :helpers_filename_glob, "**/*_helper.rb"
app.set :helpers_filename_to_module_name_proc, Proc.new { |filename| app.set :helpers_filename_to_module_name_proc, Proc.new { |filename|
basename = File.basename(filename, File.extname(filename)) basename = File.basename(filename, File.extname(filename))
basename.camelcase basename.camelcase
} }
# After config # After config
app.after_configuration do app.after_configuration do
helpers_path = File.expand_path(helpers_dir, root) helpers_path = File.expand_path(helpers_dir, root)
next unless File.exists?(helpers_path) next unless File.exists?(helpers_path)
Dir[File.join(helpers_path, helpers_filename_glob)].each do |filename| Dir[File.join(helpers_path, helpers_filename_glob)].each do |filename|
module_name = helpers_filename_to_module_name_proc.call(filename) module_name = helpers_filename_to_module_name_proc.call(filename)
next unless module_name next unless module_name
require filename require filename
next unless Object.const_defined?(module_name.to_sym) next unless Object.const_defined?(module_name.to_sym)
helpers Object.const_get(module_name.to_sym) helpers Object.const_get(module_name.to_sym)
end
end
end end
alias :included :registered
end end
end end
alias :included :registered
end end
end end

View file

@ -1,153 +1,157 @@
require "find"
require "middleman-core/watcher"
require "set"
# API for watching file change events # API for watching file change events
module Middleman::CoreExtensions::FileWatcher module Middleman
module CoreExtensions
module FileWatcher
# Setup extension # Setup extension
class << self class << self
# Once registered # Once registered
def registered(app) def registered(app)
app.extend ClassMethods require "find"
app.send :include, InstanceMethods require "middleman-core/watcher"
require "set"
# Before parsing config, load the data/ directory app.extend ClassMethods
app.before_configuration do app.send :include, InstanceMethods
data_path = File.join(self.root, self.data_dir)
self.files.reload_path(data_path) if File.exists?(data_path) # Before parsing config, load the data/ directory
app.before_configuration do
data_path = File.join(self.root, self.data_dir)
self.files.reload_path(data_path) if File.exists?(data_path)
end
# After config, load everything else
app.ready do
self.files.reload_path(self.root)
end
end
alias :included :registered
end end
# After config, load everything else # Class methods
app.ready do module ClassMethods
self.files.reload_path(self.root)
# Access the file api
# @return [Middleman::CoreExtensions::FileWatcher::API]
def files
@_files_api ||= API.new
end
end end
end
alias :included :registered
end
# Class methods # Instance methods
module ClassMethods module InstanceMethods
# Access the file api # Access the file api
# @return [Middleman::CoreExtensions::FileWatcher::API] # @return [Middleman::CoreExtensions::FileWatcher::API]
def files def files
@_files_api ||= API.new api = self.class.files
end api.instance ||= self
end api
end
# Instance methods
module InstanceMethods
# Access the file api
# @return [Middleman::CoreExtensions::FileWatcher::API]
def files
api = self.class.files
api.instance ||= self
api
end
end
# Core File Change API class
class API
attr_accessor :instance, :known_paths
# Initialize api and internal path cache
def initialize
self.known_paths = Set.new
end
# Add callback to be run on file change
#
# @param [nil,Regexp] matcher A Regexp to match the change path against
# @return [Array<Proc>]
def changed(matcher=nil, &block)
@_changed ||= []
@_changed << [block, matcher] if block_given?
@_changed
end
# Add callback to be run on file deletion
#
# @param [nil,Regexp] matcher A Regexp to match the deleted path against
# @return [Array<Proc>]
def deleted(matcher=nil, &block)
@_deleted ||= []
@_deleted << [block, matcher] if block_given?
@_deleted
end
# Notify callbacks that a file changed
#
# @param [String] path The file that changed
# @return [void]
def did_change(path)
puts "== File Change: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths << path
self.run_callbacks(path, :changed)
end
# Notify callbacks that a file was deleted
#
# @param [String] path The file that was deleted
# @return [void]
def did_delete(path)
puts "== File Deletion: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths.delete(path)
self.run_callbacks(path, :deleted)
end
# Manually trigger update events
#
# @param [String] path The path to reload
# @return [void]
def reload_path(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |path|
next if File.directory?(path)
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
relative_path = path.sub("#{self.instance.root}/", "")
subset.delete(relative_path)
self.did_change(relative_path)
end if File.exists?(path)
subset.each do |removed_path|
self.did_delete(removed_path)
end end
end
# Like reload_path, but only triggers events on new files # Core File Change API class
# class API
# @param [String] path The path to reload attr_accessor :instance, :known_paths
# @return [void]
def find_new_files(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |file| # Initialize api and internal path cache
next if File.directory?(file) def initialize
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) } self.known_paths = Set.new
relative_path = file.sub("#{self.instance.root}/", "") end
self.did_change(relative_path) unless subset.include?(relative_path)
end if File.exists?(path)
end
protected # Add callback to be run on file change
# Notify callbacks for a file given an array of callbacks #
# # @param [nil,Regexp] matcher A Regexp to match the change path against
# @param [String] path The file that was changed # @return [Array<Proc>]
# @param [Symbol] callbacks_name The name of the callbacks method def changed(matcher=nil, &block)
# @return [void] @_changed ||= []
def run_callbacks(path, callbacks_name) @_changed << [block, matcher] if block_given?
return if ::Middleman::Watcher.ignore_list.any? { |r| path.match(r) } @_changed
end
self.send(callbacks_name).each do |callback, matcher| # Add callback to be run on file deletion
next if path.match(%r{^#{self.instance.build_dir}/}) #
next unless matcher.nil? || path.match(matcher) # @param [nil,Regexp] matcher A Regexp to match the deleted path against
self.instance.instance_exec(path, &callback) # @return [Array<Proc>]
def deleted(matcher=nil, &block)
@_deleted ||= []
@_deleted << [block, matcher] if block_given?
@_deleted
end
# Notify callbacks that a file changed
#
# @param [String] path The file that changed
# @return [void]
def did_change(path)
puts "== File Change: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths << path
self.run_callbacks(path, :changed)
end
# Notify callbacks that a file was deleted
#
# @param [String] path The file that was deleted
# @return [void]
def did_delete(path)
puts "== File Deletion: #{path}" if instance.logging? && !::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.known_paths.delete(path)
self.run_callbacks(path, :deleted)
end
# Manually trigger update events
#
# @param [String] path The path to reload
# @return [void]
def reload_path(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |path|
next if File.directory?(path)
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
relative_path = path.sub("#{self.instance.root}/", "")
subset.delete(relative_path)
self.did_change(relative_path)
end if File.exists?(path)
subset.each do |removed_path|
self.did_delete(removed_path)
end
end
# Like reload_path, but only triggers events on new files
#
# @param [String] path The path to reload
# @return [void]
def find_new_files(path)
relative_path = path.sub("#{self.instance.root}/", "")
subset = self.known_paths.select { |p| p.match(%r{^#{relative_path}}) }
Find.find(path) do |file|
next if File.directory?(file)
next if Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
relative_path = file.sub("#{self.instance.root}/", "")
self.did_change(relative_path) unless subset.include?(relative_path)
end if File.exists?(path)
end
protected
# Notify callbacks for a file given an array of callbacks
#
# @param [String] path The file that was changed
# @param [Symbol] callbacks_name The name of the callbacks method
# @return [void]
def run_callbacks(path, callbacks_name)
return if ::Middleman::Watcher.ignore_list.any? { |r| path.match(r) }
self.send(callbacks_name).each do |callback, matcher|
next if path.match(%r{^#{self.instance.build_dir}/})
next unless matcher.nil? || path.match(matcher)
self.instance.instance_exec(path, &callback)
end
end
end end
end end
end end

View file

@ -5,204 +5,208 @@ require "yaml"
require "tilt" require "tilt"
# Frontmatter namespace # Frontmatter namespace
module Middleman::CoreExtensions::FrontMatter module Middleman
module CoreExtensions
module FrontMatter
# Setup extension # Setup extension
class << self class << self
# Once registered # Once registered
def registered(app) def registered(app)
app.set :frontmatter_extensions, %w(.htm .html .php) app.set :frontmatter_extensions, %w(.htm .html .php)
app.extend ClassMethods app.extend ClassMethods
app.send :include, InstanceMethods app.send :include, InstanceMethods
app.delegate :frontmatter_changed, :to => :"self.class" app.delegate :frontmatter_changed, :to => :"self.class"
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods ::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
end
alias :included :registered
end
# Frontmatter class methods
module ClassMethods
# Register callback on frontmatter updates
# @param [Regexp] matcher
# @return [Array<Array<Proc, Regexp>>]
def frontmatter_changed(matcher=nil, &block)
@_frontmatter_changed ||= []
@_frontmatter_changed << [block, matcher] if block_given?
@_frontmatter_changed
end
end
module ResourceInstanceMethods
# This page's frontmatter
# @return [Hash]
def data
app.frontmatter(source_file).first
end
end
# Frontmatter instance methods
module InstanceMethods
# Override init
def initialize
exts = frontmatter_extensions.join("|").gsub(".", "\.")
static_path = source_dir.sub(root, "").sub(/^\//, "").sub(/\/$/, "") + "/"
matcher = %r{#{static_path}.*(#{exts})}
files.changed matcher do |file|
frontmatter_extension.touch_file(file)
end
files.deleted matcher do |file|
frontmatter_extension.remove_file(file)
end
sitemap.provides_metadata matcher do |path|
fmdata = if self.frontmatter_extension.has_data?(path)
self.frontmatter(path)[0]
else
{}
end end
alias :included :registered
data = {}
%w(layout layout_engine).each do |opt|
data[opt.to_sym] = fmdata[opt] if fmdata.has_key?(opt)
end
{ :options => data, :page => fmdata }
end end
# Initialize class # Frontmatter class methods
frontmatter_extension module ClassMethods
super # Register callback on frontmatter updates
end # @param [Regexp] matcher
# @return [Array<Array<Proc, Regexp>>]
# Notify callbacks that the frontmatter changed def frontmatter_changed(matcher=nil, &block)
# @param [String] path @_frontmatter_changed ||= []
# @return [void] @_frontmatter_changed << [block, matcher] if block_given?
def frontmatter_did_change(path) @_frontmatter_changed
frontmatter_changed.each do |callback, matcher|
next if path.match(%r{^#{build_dir}/})
next if !matcher.nil? && !path.match(matcher)
instance_exec(path, &callback)
end
end
# Get the frontmatter object
# @return [Middleman::CoreExtensions::FrontMatter::FrontMatter]
def frontmatter_extension
@_frontmatter_extension ||= FrontMatter.new(self)
end
# Get the frontmatter for a given path
# @param [String] path
# @return [Hash]
def frontmatter(path)
frontmatter_extension.data(path)
end
end
# Core Frontmatter class
class FrontMatter
# Initialize frontmatter with current app
# @param [Middleman::Application] app
def initialize(app)
@app = app
@source = File.expand_path(@app.source, @app.root)
@local_data = {}
# Setup ignore callback
@app.ignore do |path|
if p = @app.sitemap.find_resource_by_path(path)
!p.proxy? && p.data && p.data["ignored"] == true
else
false
end end
end end
end
# Whether the frontmatter knows about a path module ResourceInstanceMethods
# @param [String] path
# @return [Boolean]
def has_data?(path)
@local_data.has_key?(path.to_s)
end
# Update frontmatter if a file changes # This page's frontmatter
# @param [String] file # @return [Hash]
# @return [void] def data
def touch_file(file) app.frontmatter(source_file).first
extension = File.extname(file).sub(/\./, "")
return unless ::Tilt.mappings.has_key?(extension)
file = File.expand_path(file, @app.root)
content = File.read(file)
result = parse_front_matter(content)
if result
data, content = result
data = ::Middleman::Util.recursively_enhance(data).freeze
@local_data[file] = [data, content]
@app.cache.set([:raw_template, file], result[1])
@app.frontmatter_did_change(file)
end
end
# Update frontmatter if a file is delted
# @param [String] file
# @return [void]
def remove_file(file)
file = File.expand_path(file, @app.root)
if @local_data.has_key?(file)
@app.cache.remove(:raw_template, file)
@local_data.delete(file)
end
end
# Get the frontmatter for a given path
# @param [String] path
# @return [Hash]
def data(path)
if @local_data.has_key?(path.to_s)
@local_data[path.to_s]
else
[ ::Middleman::Util.recursively_enhance({}).freeze, nil ]
end
end
private
# Parse frontmatter out of a string
# @param [String] content
# @return [Array<Hash, String>]
def parse_front_matter(content)
yaml_regex = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
if content =~ yaml_regex
content = content[($1.size + $2.size)..-1]
begin
data = YAML.load($1)
rescue => e
puts "YAML Exception: #{e.message}"
return false
end end
else
return false
end end
[data, content] # Frontmatter instance methods
module InstanceMethods
# Override init
def initialize
exts = frontmatter_extensions.join("|").gsub(".", "\.")
static_path = source_dir.sub(root, "").sub(/^\//, "").sub(/\/$/, "") + "/"
matcher = %r{#{static_path}.*(#{exts})}
files.changed matcher do |file|
frontmatter_extension.touch_file(file)
end
files.deleted matcher do |file|
frontmatter_extension.remove_file(file)
end
sitemap.provides_metadata matcher do |path|
fmdata = if self.frontmatter_extension.has_data?(path)
self.frontmatter(path)[0]
else
{}
end
data = {}
%w(layout layout_engine).each do |opt|
data[opt.to_sym] = fmdata[opt] if fmdata.has_key?(opt)
end
{ :options => data, :page => fmdata }
end
# Initialize class
frontmatter_extension
super
end
# Notify callbacks that the frontmatter changed
# @param [String] path
# @return [void]
def frontmatter_did_change(path)
frontmatter_changed.each do |callback, matcher|
next if path.match(%r{^#{build_dir}/})
next if !matcher.nil? && !path.match(matcher)
instance_exec(path, &callback)
end
end
# Get the frontmatter object
# @return [Middleman::CoreExtensions::FrontMatter::FrontMatter]
def frontmatter_extension
@_frontmatter_extension ||= FrontMatter.new(self)
end
# Get the frontmatter for a given path
# @param [String] path
# @return [Hash]
def frontmatter(path)
frontmatter_extension.data(path)
end
end
# Core Frontmatter class
class FrontMatter
# Initialize frontmatter with current app
# @param [Middleman::Application] app
def initialize(app)
@app = app
@source = File.expand_path(@app.source, @app.root)
@local_data = {}
# Setup ignore callback
@app.ignore do |path|
if p = @app.sitemap.find_resource_by_path(path)
!p.proxy? && p.data && p.data["ignored"] == true
else
false
end
end
end
# Whether the frontmatter knows about a path
# @param [String] path
# @return [Boolean]
def has_data?(path)
@local_data.has_key?(path.to_s)
end
# Update frontmatter if a file changes
# @param [String] file
# @return [void]
def touch_file(file)
extension = File.extname(file).sub(/\./, "")
return unless ::Tilt.mappings.has_key?(extension)
file = File.expand_path(file, @app.root)
content = File.read(file)
result = parse_front_matter(content)
if result
data, content = result
data = ::Middleman::Util.recursively_enhance(data).freeze
@local_data[file] = [data, content]
@app.cache.set([:raw_template, file], result[1])
@app.frontmatter_did_change(file)
end
end
# Update frontmatter if a file is delted
# @param [String] file
# @return [void]
def remove_file(file)
file = File.expand_path(file, @app.root)
if @local_data.has_key?(file)
@app.cache.remove(:raw_template, file)
@local_data.delete(file)
end
end
# Get the frontmatter for a given path
# @param [String] path
# @return [Hash]
def data(path)
if @local_data.has_key?(path.to_s)
@local_data[path.to_s]
else
[ ::Middleman::Util.recursively_enhance({}).freeze, nil ]
end
end
private
# Parse frontmatter out of a string
# @param [String] content
# @return [Array<Hash, String>]
def parse_front_matter(content)
yaml_regex = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
if content =~ yaml_regex
content = content[($1.size + $2.size)..-1]
begin
data = YAML.load($1)
rescue => e
puts "YAML Exception: #{e.message}"
return false
end
else
return false
end
[data, content]
end
end
end end
end end
end end

View file

@ -1,141 +1,145 @@
# i18n Namespace # i18n Namespace
module Middleman::CoreExtensions::I18n module Middleman
module CoreExtensions
module I18n
# Setup extension # Setup extension
class << self class << self
# Once registerd # Once registerd
def registered(app) def registered(app)
app.set :locales_dir, "locales" app.set :locales_dir, "locales"
app.send :include, InstanceMethods app.send :include, InstanceMethods
# Needed for helpers as well # Needed for helpers as well
app.after_configuration do app.after_configuration do
# This is for making the tests work - since the tests # This is for making the tests work - since the tests
# don't completely reload middleman, I18n.load_path can get # don't completely reload middleman, I18n.load_path can get
# polluted with paths from other test app directories that don't # polluted with paths from other test app directories that don't
# exist anymore. # exist anymore.
::I18n.load_path.delete_if {|path| path =~ %r{tmp/aruba}} ::I18n.load_path.delete_if {|path| path =~ %r{tmp/aruba}}
::I18n.load_path += Dir[File.join(root, locales_dir, "*.yml")] ::I18n.load_path += Dir[File.join(root, locales_dir, "*.yml")]
::I18n.reload! ::I18n.reload!
end end
end
alias :included :registered
end
class Localizer
def initialize(app)
@app = app
@maps = {}
end
def setup(options)
@options = options
@lang_map = @options[:lang_map] || {}
@path = @options[:path] || "/:locale/"
@templates_dir = @options[:templates_dir] || "localizable"
@mount_at_root = @options.has_key?(:mount_at_root) ? @options[:mount_at_root] : langs.first
if !@app.build?
puts "== Locales: #{langs.join(", ")}"
end
# Don't output localizable files
@app.ignore File.join(@templates_dir, "**")
@app.sitemap.provides_metadata_for_path do |url|
if d = get_localization_data(url)
lang, page_id = d
instance_vars = Proc.new {
::I18n.locale = lang
@lang = lang
@page_id = page_id
}
{ :blocks => [instance_vars] }
else
{}
end end
alias :included :registered
end end
@app.sitemap.register_resource_list_manipulator( class Localizer
:i18n, def initialize(app)
@app.i18n @app = app
) @maps = {}
end end
def langs def setup(options)
@options[:langs] || begin @options = options
Dir[File.join(@app.root, @app.locales_dir, "*.yml")].map { |file|
File.basename(file).gsub(".yml", "")
}.sort.map(&:to_sym)
end
end
def get_localization_data(path) @lang_map = @options[:lang_map] || {}
@_localization_data ||= {} @path = @options[:path] || "/:locale/"
@_localization_data[path] @templates_dir = @options[:templates_dir] || "localizable"
end @mount_at_root = @options.has_key?(:mount_at_root) ? @options[:mount_at_root] : langs.first
# Update the main sitemap resource list if !@app.build?
# @return [void] puts "== Locales: #{langs.join(", ")}"
def manipulate_resource_list(resources)
@_localization_data = {}
new_resources = []
resources.each do |resource|
next unless File.fnmatch(File.join(@templates_dir, "**"), resource.path)
page_id = File.basename(resource.path, File.extname(resource.path))
langs.map do |lang|
::I18n.locale = lang
localized_page_id = ::I18n.t("paths.#{page_id}", :default => page_id)
path = resource.path.sub(@templates_dir, "")
# Build lang path
if @mount_at_root == lang
prefix = "/"
else
replacement = @lang_map.has_key?(lang) ? @lang_map[lang] : lang
prefix = @path.sub(":locale", replacement.to_s)
end end
path = ::Middleman::Util.normalize_path( # Don't output localizable files
File.join(prefix, path.sub(page_id, localized_page_id)) @app.ignore File.join(@templates_dir, "**")
@app.sitemap.provides_metadata_for_path do |url|
if d = get_localization_data(url)
lang, page_id = d
instance_vars = Proc.new {
::I18n.locale = lang
@lang = lang
@page_id = page_id
}
{ :blocks => [instance_vars] }
else
{}
end
end
@app.sitemap.register_resource_list_manipulator(
:i18n,
@app.i18n
) )
end
@_localization_data[path] = [lang, path, localized_page_id] def langs
@options[:langs] || begin
Dir[File.join(@app.root, @app.locales_dir, "*.yml")].map { |file|
File.basename(file).gsub(".yml", "")
}.sort.map(&:to_sym)
end
end
p = ::Middleman::Sitemap::Resource.new( def get_localization_data(path)
@app.sitemap, @_localization_data ||= {}
path @_localization_data[path]
) end
p.proxy_to(resource.path)
new_resources << p # Update the main sitemap resource list
# @return [void]
def manipulate_resource_list(resources)
@_localization_data = {}
new_resources = []
resources.each do |resource|
next unless File.fnmatch(File.join(@templates_dir, "**"), resource.path)
page_id = File.basename(resource.path, File.extname(resource.path))
langs.map do |lang|
::I18n.locale = lang
localized_page_id = ::I18n.t("paths.#{page_id}", :default => page_id)
path = resource.path.sub(@templates_dir, "")
# Build lang path
if @mount_at_root == lang
prefix = "/"
else
replacement = @lang_map.has_key?(lang) ? @lang_map[lang] : lang
prefix = @path.sub(":locale", replacement.to_s)
end
path = ::Middleman::Util.normalize_path(
File.join(prefix, path.sub(page_id, localized_page_id))
)
@_localization_data[path] = [lang, path, localized_page_id]
p = ::Middleman::Sitemap::Resource.new(
@app.sitemap,
path
)
p.proxy_to(resource.path)
new_resources << p
end
end
resources + new_resources
end end
end end
resources + new_resources # Frontmatter class methods
end module InstanceMethods
end
# Frontmatter class methods # Initialize the i18n
module InstanceMethods def i18n
@_i18n ||= Localizer.new(self)
end
# Initialize the i18n # Main i18n API
def i18n def localize(options={})
@_i18n ||= Localizer.new(self) settings.after_configuration do
end i18n.setup(options)
end
# Main i18n API end
def localize(options={})
settings.after_configuration do
i18n.setup(options)
end end
end end
end end

View file

@ -9,357 +9,361 @@ class Tilt::Template
end end
# Rendering extension # Rendering extension
module Middleman::CoreExtensions::Rendering module Middleman
module CoreExtensions
module Rendering
# Setup extension # Setup extension
class << self class << self
# Once registered # Once registered
def registered(app) def registered(app)
# Include methods # Include methods
app.send :include, InstanceMethods app.send :include, InstanceMethods
# Activate custom renderers # Activate custom renderers
app.register Middleman::Renderers::ERb app.register Middleman::Renderers::ERb
end end
alias :included :registered alias :included :registered
end
# Custom error class for handling
class TemplateNotFound < RuntimeError
end
# Rendering instance methods
module InstanceMethods
# Override init to clear cache on file removal
def initialize
# Default extension map
@_template_extensions = {
}
static_path = source_dir.sub(self.root, "").sub(/^\//, "")
render_regex = static_path.empty? ? // : (%r{^#{static_path + "/"}})
self.files.changed render_regex do |file|
path = File.expand_path(file, self.root)
self.cache.remove(:raw_template, path)
end end
super # Custom error class for handling
end class TemplateNotFound < RuntimeError
# Add or overwrite a default template extension
#
# @param [Hash] extension_map
# @return [void]
def template_extensions(extension_map={})
@_template_extensions.merge!(extension_map)
end
# Render a template, with layout, given a path
#
# @param [String] path
# @param [Hash] locs
# @param [Hash] opts
# @return [String]
def render_template(path, locs={}, opts={})
# Detect the remdering engine from the extension
extension = File.extname(path)
engine = extension[1..-1].to_sym
# Store last engine for later (could be inside nested renders)
@current_engine, engine_was = engine, @current_engine
# Use a dup of self as a context so that instance variables set within
# the template don't persist for other templates.
context = self.dup
# Store current locs/opts for later
@current_locs = locs, @current_opts = opts
# Keep rendering template until we've used up all extensions. This handles
# cases like `style.css.sass.erb`
while ::Tilt[path]
content = render_individual_file(path, locs, opts, context)
path = File.basename(path, File.extname(path))
cache.set([:raw_template, path], content)
end end
# Certain output file types don't use layouts # Rendering instance methods
needs_layout = !%w(.js .json .css .txt).include?(extension) module InstanceMethods
# If we need a layout and have a layout, use it # Override init to clear cache on file removal
if needs_layout && layout_path = fetch_layout(engine, opts) def initialize
content = render_individual_file(layout_path, locs, opts, context) { content } # Default extension map
end @_template_extensions = {
# Return result }
content
ensure
# Pop all the saved variables from earlier as we may be returning to a
# previous render (layouts, partials, nested layouts).
@current_engine = engine_was
@content_blocks = nil
@current_locs = nil
@current_opts = nil
end
# Sinatra/Padrino compatible render method signature referenced by some view static_path = source_dir.sub(self.root, "").sub(/^\//, "")
# helpers. Especially partials. render_regex = static_path.empty? ? // : (%r{^#{static_path + "/"}})
#
# @param [String, Symbol] engine
# @param [String, Symbol] data
# @param [Hash] options
# @return [String]
def render(engine, data, options={}, &block)
data = data.to_s
locals = options[:locals] self.files.changed render_regex do |file|
path = File.expand_path(file, self.root)
self.cache.remove(:raw_template, path)
end
found_partial = false super
engine = nil end
# If the path is known to the sitemap # Add or overwrite a default template extension
if resource = sitemap.find_resource_by_path(current_path) #
current_dir = File.dirname(resource.source_file) # @param [Hash] extension_map
engine = File.extname(resource.source_file)[1..-1].to_sym # @return [void]
def template_extensions(extension_map={})
@_template_extensions.merge!(extension_map)
end
# Look for partials relative to the current path # Render a template, with layout, given a path
if current_dir != self.source_dir #
relative_dir = File.join(current_dir.sub("#{self.source_dir}/", ""), data) # @param [String] path
# @param [Hash] locs
# @param [Hash] opts
# @return [String]
def render_template(path, locs={}, opts={})
# Detect the remdering engine from the extension
extension = File.extname(path)
engine = extension[1..-1].to_sym
# Try to use the current engine first # Store last engine for later (could be inside nested renders)
found_partial, found_engine = resolve_template(relative_dir, :preferred_engine => engine, :try_without_underscore => true) @current_engine, engine_was = engine, @current_engine
# Fall back to any engine available # Use a dup of self as a context so that instance variables set within
# the template don't persist for other templates.
context = self.dup
# Store current locs/opts for later
@current_locs = locs, @current_opts = opts
# Keep rendering template until we've used up all extensions. This handles
# cases like `style.css.sass.erb`
while ::Tilt[path]
content = render_individual_file(path, locs, opts, context)
path = File.basename(path, File.extname(path))
cache.set([:raw_template, path], content)
end
# Certain output file types don't use layouts
needs_layout = !%w(.js .json .css .txt).include?(extension)
# If we need a layout and have a layout, use it
if needs_layout && layout_path = fetch_layout(engine, opts)
content = render_individual_file(layout_path, locs, opts, context) { content }
end
# Return result
content
ensure
# Pop all the saved variables from earlier as we may be returning to a
# previous render (layouts, partials, nested layouts).
@current_engine = engine_was
@content_blocks = nil
@current_locs = nil
@current_opts = nil
end
# 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]
def render(engine, data, options={}, &block)
data = data.to_s
locals = options[:locals]
found_partial = false
engine = nil
# If the path is known to the sitemap
if resource = sitemap.find_resource_by_path(current_path)
current_dir = File.dirname(resource.source_file)
engine = File.extname(resource.source_file)[1..-1].to_sym
# Look for partials relative to the current path
if current_dir != self.source_dir
relative_dir = File.join(current_dir.sub("#{self.source_dir}/", ""), data)
# Try to use the current engine first
found_partial, found_engine = resolve_template(relative_dir, :preferred_engine => engine, :try_without_underscore => true)
# Fall back to any engine available
if !found_partial
found_partial, found_engine = resolve_template(relative_dir, :try_without_underscore => true)
end
end
end
# Look in the root for the partial with the current engine
if !found_partial && !engine.nil?
found_partial, found_engine = resolve_template(data, :preferred_engine => engine, :try_without_underscore => true)
end
# Look in the root with any engine
if !found_partial if !found_partial
found_partial, found_engine = resolve_template(relative_dir, :try_without_underscore => true) found_partial, found_engine = resolve_template(data, :try_without_underscore => true)
end
end
end
# Look in the root for the partial with the current engine
if !found_partial && !engine.nil?
found_partial, found_engine = resolve_template(data, :preferred_engine => engine, :try_without_underscore => true)
end
# Look in the root with any engine
if !found_partial
found_partial, found_engine = resolve_template(data, :try_without_underscore => true)
end
# Render the partial if found, otherwide throw exception
if found_partial
render_individual_file(found_partial, locals, options, self, &block)
else
raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}"
end
end
# Render an on-disk file. Used for everything, including layouts.
#
# @param [String, Symbol] path
# @param [Hash] locs
# @param [Hash] opts
# @param [Class] context
# @return [String]
def render_individual_file(path, locs = {}, opts = {}, context = self, &block)
path = path.to_s
# Save current buffere for later
@_out_buf, _buf_was = "", @_out_buf
# Read from disk or cache the contents of the file
body = cache.fetch(:raw_template, path) do
File.read(path)
end
# Merge per-extension options from config
extension = File.extname(path)
options = opts.merge(options_for_ext(extension))
options[:outvar] ||= '@_out_buf'
# Read compiled template from disk or cache
template = cache.fetch(:compiled_template, options, body) do
::Tilt.new(path, 1, options) { body }
end
# Render using Tilt
template.render(context, locs, &block)
ensure
# Reset stored buffer
@_out_buf = _buf_was
end
# Get a hash of configuration options for a given file extension, from
# config.rb
#
# @param [String] ext
# @return [Hash]
def options_for_ext(ext)
# Read options for extension from config/Tilt or cache
cache.fetch(:options_for_ext, ext) do
options = {}
# Find all the engines which handle this extension in tilt. Look for
# config variables of that name and merge it
extension_class = ::Tilt[ext]
::Tilt.mappings.each do |ext, engines|
next unless engines.include? extension_class
engine_options = respond_to?(ext.to_sym) ? send(ext.to_sym) : {}
options.merge!(engine_options)
end
options
end
end
# Find a layout for a given engine
#
# @param [Symbol] engine
# @param [Hash] opts
# @return [String]
def fetch_layout(engine, opts)
# The layout name comes from either the system default or the options
local_layout = opts.has_key?(:layout) ? opts[:layout] : layout
return false unless local_layout
# Look for engine-specific options
engine_options = respond_to?(engine) ? send(engine) : {}
# The engine for the layout can be set in options, engine_options or passed
# into this method
layout_engine = if opts.has_key?(:layout_engine)
opts[:layout_engine]
elsif engine_options.has_key?(:layout_engine)
engine_options[:layout_engine]
else
engine
end
# Automatic mode
if local_layout == :_auto_layout
# Look for :layout of any extension
# If found, use it. If not, continue
locate_layout(:layout, layout_engine) || false
else
# Look for specific layout
# If found, use it. If not, error.
if layout_path = locate_layout(local_layout, layout_engine)
layout_path
else
raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate layout: #{local_layout}"
end
end
end
# Find a layout on-disk, optionally using a specific engine
# @param [String] name
# @param [Symbol] preferred_engine
# @return [String]
def locate_layout(name, preferred_engine=nil)
# Whether we've found the layout
layout_path = false
# If we prefer a specific engine
if !preferred_engine.nil?
# Check root
layout_path, layout_engine = resolve_template(name, :preferred_engine => preferred_engine)
# Check layouts folder
if !layout_path
layout_path, layout_engine = resolve_template(File.join("layouts", name.to_s), :preferred_engine => preferred_engine)
end
end
# Check root, no preference
if !layout_path
layout_path, layout_engine = resolve_template(name)
end
# Check layouts folder, no preference
if !layout_path
layout_path, layout_engine = resolve_template(File.join("layouts", name.to_s))
end
# Return the path
layout_path
end
# Allow layouts to be wrapped in the contents of other layouts
# @param [String, Symbol] layout_name
# @return [void]
def wrap_layout(layout_name, &block)
content = capture(&block) if block_given?
layout_path = locate_layout(layout_name, current_engine)
concat render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content }
end
# The currently rendering engine
# @return [Symbol, nil]
def current_engine
@current_engine ||= nil
end
# Find a template on disk given a output path
# @param [String] request_path
# @param [Hash] options
# @return [Array<String, Symbol>, Boolean]
def resolve_template(request_path, options={})
# Find the path by searching or using the cache
request_path = request_path.to_s
cache.fetch(:resolve_template, request_path, options) do
relative_path = request_path.sub(%r{^/}, "")
on_disk_path = File.expand_path(relative_path, self.source_dir)
# By default, any engine will do
preferred_engine = "*"
# Unless we're specifically looking for a preferred engine
if options.has_key?(:preferred_engine)
extension_class = ::Tilt[options[:preferred_engine]]
matched_exts = []
# Get a list of extensions for a preferred engine
# TODO: Cache this
::Tilt.mappings.each do |ext, engines|
next unless engines.include? extension_class
matched_exts << ext
end end
# Change the glob to only look for the matched extensions # Render the partial if found, otherwide throw exception
if matched_exts.length > 0 if found_partial
preferred_engine = "{" + matched_exts.join(",") + "}" render_individual_file(found_partial, locals, options, self, &block)
else else
return false raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}"
end end
end end
# Look for files that match # Render an on-disk file. Used for everything, including layouts.
path_with_ext = on_disk_path + "." + preferred_engine #
# @param [String, Symbol] path
# @param [Hash] locs
# @param [Hash] opts
# @param [Class] context
# @return [String]
def render_individual_file(path, locs = {}, opts = {}, context = self, &block)
path = path.to_s
found_path = Dir[path_with_ext].find do |path| # Save current buffere for later
::Tilt[path] @_out_buf, _buf_was = "", @_out_buf
# Read from disk or cache the contents of the file
body = cache.fetch(:raw_template, path) do
File.read(path)
end
# Merge per-extension options from config
extension = File.extname(path)
options = opts.merge(options_for_ext(extension))
options[:outvar] ||= '@_out_buf'
# Read compiled template from disk or cache
template = cache.fetch(:compiled_template, options, body) do
::Tilt.new(path, 1, options) { body }
end
# Render using Tilt
template.render(context, locs, &block)
ensure
# Reset stored buffer
@_out_buf = _buf_was
end end
if !found_path && options[:try_without_underscore] && # Get a hash of configuration options for a given file extension, from
path_no_underscore = path_with_ext. # config.rb
sub(relative_path, relative_path.sub(/^_/, ""). #
sub(/\/_/, "/")) # @param [String] ext
found_path = Dir[path_no_underscore].find do |path| # @return [Hash]
::Tilt[path] def options_for_ext(ext)
# Read options for extension from config/Tilt or cache
cache.fetch(:options_for_ext, ext) do
options = {}
# Find all the engines which handle this extension in tilt. Look for
# config variables of that name and merge it
extension_class = ::Tilt[ext]
::Tilt.mappings.each do |ext, engines|
next unless engines.include? extension_class
engine_options = respond_to?(ext.to_sym) ? send(ext.to_sym) : {}
options.merge!(engine_options)
end
options
end end
end end
# If we found one, return it and the found engine # Find a layout for a given engine
if found_path || (File.exists?(on_disk_path) && !File.directory?(on_disk_path)) #
engine = found_path ? File.extname(found_path)[1..-1].to_sym : nil # @param [Symbol] engine
[ found_path || on_disk_path, engine ] # @param [Hash] opts
else # @return [String]
false def fetch_layout(engine, opts)
# The layout name comes from either the system default or the options
local_layout = opts.has_key?(:layout) ? opts[:layout] : layout
return false unless local_layout
# Look for engine-specific options
engine_options = respond_to?(engine) ? send(engine) : {}
# The engine for the layout can be set in options, engine_options or passed
# into this method
layout_engine = if opts.has_key?(:layout_engine)
opts[:layout_engine]
elsif engine_options.has_key?(:layout_engine)
engine_options[:layout_engine]
else
engine
end
# Automatic mode
if local_layout == :_auto_layout
# Look for :layout of any extension
# If found, use it. If not, continue
locate_layout(:layout, layout_engine) || false
else
# Look for specific layout
# If found, use it. If not, error.
if layout_path = locate_layout(local_layout, layout_engine)
layout_path
else
raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate layout: #{local_layout}"
end
end
end
# Find a layout on-disk, optionally using a specific engine
# @param [String] name
# @param [Symbol] preferred_engine
# @return [String]
def locate_layout(name, preferred_engine=nil)
# Whether we've found the layout
layout_path = false
# If we prefer a specific engine
if !preferred_engine.nil?
# Check root
layout_path, layout_engine = resolve_template(name, :preferred_engine => preferred_engine)
# Check layouts folder
if !layout_path
layout_path, layout_engine = resolve_template(File.join("layouts", name.to_s), :preferred_engine => preferred_engine)
end
end
# Check root, no preference
if !layout_path
layout_path, layout_engine = resolve_template(name)
end
# Check layouts folder, no preference
if !layout_path
layout_path, layout_engine = resolve_template(File.join("layouts", name.to_s))
end
# Return the path
layout_path
end
# Allow layouts to be wrapped in the contents of other layouts
# @param [String, Symbol] layout_name
# @return [void]
def wrap_layout(layout_name, &block)
content = capture(&block) if block_given?
layout_path = locate_layout(layout_name, current_engine)
concat render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content }
end
# The currently rendering engine
# @return [Symbol, nil]
def current_engine
@current_engine ||= nil
end
# Find a template on disk given a output path
# @param [String] request_path
# @param [Hash] options
# @return [Array<String, Symbol>, Boolean]
def resolve_template(request_path, options={})
# Find the path by searching or using the cache
request_path = request_path.to_s
cache.fetch(:resolve_template, request_path, options) do
relative_path = request_path.sub(%r{^/}, "")
on_disk_path = File.expand_path(relative_path, self.source_dir)
# By default, any engine will do
preferred_engine = "*"
# Unless we're specifically looking for a preferred engine
if options.has_key?(:preferred_engine)
extension_class = ::Tilt[options[:preferred_engine]]
matched_exts = []
# Get a list of extensions for a preferred engine
# TODO: Cache this
::Tilt.mappings.each do |ext, engines|
next unless engines.include? extension_class
matched_exts << ext
end
# Change the glob to only look for the matched extensions
if matched_exts.length > 0
preferred_engine = "{" + matched_exts.join(",") + "}"
else
return false
end
end
# Look for files that match
path_with_ext = on_disk_path + "." + preferred_engine
found_path = Dir[path_with_ext].find do |path|
::Tilt[path]
end
if !found_path && options[:try_without_underscore] &&
path_no_underscore = path_with_ext.
sub(relative_path, relative_path.sub(/^_/, "").
sub(/\/_/, "/"))
found_path = Dir[path_no_underscore].find do |path|
::Tilt[path]
end
end
# If we found one, return it and the found engine
if found_path || (File.exists?(on_disk_path) && !File.directory?(on_disk_path))
engine = found_path ? File.extname(found_path)[1..-1].to_sym : nil
[ found_path || on_disk_path, engine ]
else
false
end
end
end end
end end
end end

View file

@ -1,6 +1,7 @@
# Base helper to manipulate asset paths
module Middleman module Middleman
module CoreExtensions module CoreExtensions
# Base helper to manipulate asset paths
module Request module Request
# Extension registered # Extension registered

View file

@ -1,87 +1,91 @@
# Routing extension # Routing extension
module Middleman::CoreExtensions::Routing module Middleman
module CoreExtensions
module Routing
# Setup extension # Setup extension
class << self class << self
# Once registered # Once registered
def registered(app) def registered(app)
# Include methods # Include methods
app.send :include, InstanceMethods app.send :include, InstanceMethods
end
alias :included :registered
end
# Routing instance methods
module InstanceMethods
# Takes a block which allows many pages to have the same layout
#
# with_layout :admin do
# page "/admin/"
# page "/admin/login.html"
# end
#
# @param [String, Symbol] layout_name
# @return [void]
def with_layout(layout_name, &block)
old_layout = layout
set :layout, layout_name
instance_exec(&block) if block_given?
ensure
set :layout, old_layout
end
# The page method allows the layout to be set on a specific path
#
# page "/about.html", :layout => false
# page "/", :layout => :homepage_layout
#
# @param [String] url
# @param [Hash] opts
# @return [void]
def page(url, opts={}, &block)
a_block = block_given? ? block : nil
# Default layout
opts[:layout] = layout if opts[:layout].nil?
# If the url is a regexp
if url.is_a?(Regexp) || url.include?("*")
# Use the metadata loop for matching against paths at runtime
sitemap.provides_metadata_for_path url do |url|
{ :options => opts, :blocks => [a_block] }
end end
return alias :included :registered
end end
# Normalized path # Routing instance methods
url = full_path(url) module InstanceMethods
# Setup proxy # Takes a block which allows many pages to have the same layout
if opts.has_key?(:proxy) #
proxy(url, opts[:proxy]) # with_layout :admin do
# page "/admin/"
# page "/admin/login.html"
# end
#
# @param [String, Symbol] layout_name
# @return [void]
def with_layout(layout_name, &block)
old_layout = layout
if opts.has_key?(:ignore) && opts[:ignore] set :layout, layout_name
ignore(opts[:proxy]) instance_exec(&block) if block_given?
opts.delete(:ignore) ensure
set :layout, old_layout
end end
opts.delete(:proxy) # The page method allows the layout to be set on a specific path
else #
if opts.has_key?(:ignore) && opts[:ignore] # page "/about.html", :layout => false
ignore(url) # page "/", :layout => :homepage_layout
opts.delete(:ignore) #
end # @param [String] url
end # @param [Hash] opts
# @return [void]
def page(url, opts={}, &block)
a_block = block_given? ? block : nil
# Setup a metadata matcher for rendering those options # Default layout
sitemap.provides_metadata_for_path url do |url| opts[:layout] = layout if opts[:layout].nil?
{ :options => opts, :blocks => [a_block] }
# If the url is a regexp
if url.is_a?(Regexp) || url.include?("*")
# Use the metadata loop for matching against paths at runtime
sitemap.provides_metadata_for_path url do |url|
{ :options => opts, :blocks => [a_block] }
end
return
end
# Normalized path
url = full_path(url)
# Setup proxy
if opts.has_key?(:proxy)
proxy(url, opts[:proxy])
if opts.has_key?(:ignore) && opts[:ignore]
ignore(opts[:proxy])
opts.delete(:ignore)
end
opts.delete(:proxy)
else
if opts.has_key?(:ignore) && opts[:ignore]
ignore(url)
opts.delete(:ignore)
end
end
# Setup a metadata matcher for rendering those options
sitemap.provides_metadata_for_path url do |url|
{ :options => opts, :blocks => [a_block] }
end
end
end end
end end
end end

View file

@ -1,26 +1,31 @@
# Require lib
require 'rack/showexceptions'
# Support rack/showexceptions during development # Support rack/showexceptions during development
module Middleman::CoreExtensions::ShowExceptions module Middleman
module CoreExtensions
module ShowExceptions
# Setup extension # Setup extension
class << self class << self
# Once registered # Once registered
def registered(app) def registered(app)
# Require lib # When in dev
require 'rack/showexceptions' app.configure :development do
# Include middlemare
# When in dev if show_exceptions
app.configure :development do use ::Middleman::CoreExtensions::ShowExceptions::Middleware
# Include middlemare end
if show_exceptions end
use ::Middleman::CoreExtensions::ShowExceptions::Middleware
end end
end end
# Custom exception class
# TODO: Style this ourselves
class Middleware < ::Rack::ShowExceptions
end
end end
end end
# Custom exception class
# TODO: Style this ourselves
class Middleware < ::Rack::ShowExceptions
end
end end

View file

@ -1,71 +1,6 @@
module Middleman module Middleman
# Backwards compatibility namespace
module Features
end
module CoreExtensions
# Rack Request
autoload :Request, "middleman-core/core_extensions/request"
# File Change Notifier
autoload :FileWatcher, "middleman-core/core_extensions/file_watcher"
# In-memory Sitemap
autoload :Sitemap, "middleman-core/core_extensions/sitemap"
# Add Builder callbacks
autoload :Builder, "middleman-core/core_extensions/builder"
# Custom Feature API
autoload :Extensions, "middleman-core/core_extensions/extensions"
# Asset Path Pipeline
autoload :Assets, "middleman-core/core_extensions/assets"
# Data looks at the data/ folder for YAML files and makes them available
# to dynamic requests.
autoload :Data, "middleman-core/core_extensions/data"
# Parse YAML from templates
autoload :FrontMatter, "middleman-core/core_extensions/front_matter"
# External helpers looks in the helpers/ folder for helper modules
autoload :ExternalHelpers, "middleman-core/core_extensions/external_helpers"
# DefaultHelpers are the built-in dynamic template helpers.
autoload :DefaultHelpers, "middleman-core/core_extensions/default_helpers"
# Extended version of Padrino's rendering
autoload :Rendering, "middleman-core/core_extensions/rendering"
# Pass custom options to views
autoload :Routing, "middleman-core/core_extensions/routing"
# Catch and show exceptions at the Rack level
autoload :ShowExceptions, "middleman-core/core_extensions/show_exceptions"
# i18n
autoload :I18n, "middleman-core/core_extensions/i18n"
end
module Extensions module Extensions
# Provide Apache-style index.html files for directories
autoload :DirectoryIndexes, "middleman-core/extensions/directory_indexes"
# Lorem provides a handful of helpful prototyping methods to generate
# words, paragraphs, fake images, names and email addresses.
autoload :Lorem, "middleman-core/extensions/lorem"
# AutomaticImageSizes inspects the images used in your dynamic templates
# and automatically adds width and height attributes to their HTML
# elements.
autoload :AutomaticImageSizes, "middleman-core/extensions/automatic_image_sizes"
# AssetHost allows you to setup multiple domains to host your static
# assets. Calls to asset paths in dynamic templates will then rotate
# through each of the asset servers to better spread the load.
autoload :AssetHost, "middleman-core/extensions/asset_host"
class << self class << self
def registered def registered

View file

@ -51,7 +51,4 @@ module Middleman::Extensions
end end
end end
end end
# Register the extension
register :directory_indexes, DirectoryIndexes
end end

View file

@ -192,7 +192,4 @@ module Middleman::Extensions
end end
end end
end end
# Register the extension
register :lorem, Lorem
end end

View file

@ -1,78 +1,78 @@
require "middleman-core/sitemap/store"
require "middleman-core/sitemap/resource"
require "middleman-core/sitemap/extensions/on_disk"
require "middleman-core/sitemap/extensions/proxies"
require "middleman-core/sitemap/extensions/ignores"
# Core Sitemap Extensions # Core Sitemap Extensions
module Middleman::Sitemap module Middleman
autoload :Store, "middleman-core/sitemap/store" module Sitemap
autoload :Resource, "middleman-core/sitemap/resource"
module Extensions # Setup Extension
autoload :OnDisk, "middleman-core/sitemap/extensions/on_disk" class << self
autoload :Proxies, "middleman-core/sitemap/extensions/proxies"
autoload :Ignores, "middleman-core/sitemap/extensions/ignores"
autoload :Traversal, "middleman-core/sitemap/extensions/traversal"
end
# Setup Extension # Once registered
class << self def registered(app)
# Once registered app.register Middleman::Sitemap::Extensions::Proxies
def registered(app) app.register Middleman::Sitemap::Extensions::Ignores
app.register Middleman::Sitemap::Extensions::Proxies # Setup callbacks which can exclude paths from the sitemap
app.register Middleman::Sitemap::Extensions::Ignores app.set :ignored_sitemap_matchers, {
# dotfiles and folders in the root
:root_dotfiles => proc { |file, path| file.match(/^\./) },
# Setup callbacks which can exclude paths from the sitemap # Files starting with an dot, but not .htaccess
app.set :ignored_sitemap_matchers, { :source_dotfiles => proc { |file, path|
# dotfiles and folders in the root (file.match(/\/\./) && !file.match(/\/\.htaccess/))
:root_dotfiles => proc { |file, path| file.match(/^\./) }, },
# Files starting with an dot, but not .htaccess # Files starting with an underscore, but not a double-underscore
:source_dotfiles => proc { |file, path| :partials => proc { |file, path| (file.match(/\/_/) && !file.match(/\/__/)) },
(file.match(/\/\./) && !file.match(/\/\.htaccess/))
},
# Files starting with an underscore, but not a double-underscore :layout => proc { |file, path|
:partials => proc { |file, path| (file.match(/\/_/) && !file.match(/\/__/)) }, file.match(/^source\/layout\./) || file.match(/^source\/layouts\//)
},
:layout => proc { |file, path| # Files without any output extension (layouts, partials)
file.match(/^source\/layout\./) || file.match(/^source\/layouts\//) # :extensionless => proc { |file, path| !path.match(/\./) },
}, }
# Files without any output extension (layouts, partials) # Include instance methods
# :extensionless => proc { |file, path| !path.match(/\./) }, app.send :include, InstanceMethods
}
# Include instance methods # Initialize Sitemap
app.send :include, InstanceMethods app.before_configuration do
sitemap
# Initialize Sitemap end
app.before_configuration do
sitemap
end end
end alias :included :registered
alias :included :registered
end
# Sitemap instance methods
module InstanceMethods
# Get the sitemap class instance
# @return [Middleman::Sitemap::Store]
def sitemap
@_sitemap ||= Store.new(self)
end end
# Get the resource object for the current path # Sitemap instance methods
# @return [Middleman::Sitemap::Resource] module InstanceMethods
def current_page
current_resource
end
# Get the resource object for the current path # Get the sitemap class instance
# @return [Middleman::Sitemap::Resource] # @return [Middleman::Sitemap::Store]
def current_resource def sitemap
sitemap.find_resource_by_destination_path(current_path) @_sitemap ||= Store.new(self)
end end
# Get the resource object for the current path
# @return [Middleman::Sitemap::Resource]
def current_page
current_resource
end
# Get the resource object for the current path
# @return [Middleman::Sitemap::Resource]
def current_resource
sitemap.find_resource_by_destination_path(current_path)
end
end
end end
end end

View file

@ -1,84 +1,90 @@
module Middleman::Sitemap::Extensions module Middleman
module Ignores module Sitemap
# Setup extension module Extensions
class << self
# Once registered module Ignores
def registered(app)
# Include methods
app.send :include, InstanceMethods
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods # Setup extension
end class << self
alias :included :registered # Once registered
end def registered(app)
# Include methods
app.send :include, InstanceMethods
# Helpers methods for Resources ::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
module ResourceInstanceMethods
# Whether the Resource is ignored
# @return [Boolean]
def ignored?
@app.ignore_manager.ignored?(path) ||
(!proxy? &&
@app.ignore_manager.ignored?(source_file.sub("#{@app.source_dir}/", ""))
)
end
end
# Ignore-related instance methods
module InstanceMethods
def ignore_manager
@_ignore_manager ||= IgnoreManager.new(self)
end
# Ignore a path or add an ignore callback
# @param [String, Regexp] path Path glob expression, or path regex
# @return [void]
def ignore(path=nil, &block)
ignore_manager.ignore(path, &block)
end
end
# Class to handle managing ignores
class IgnoreManager
def initialize(app)
@app = app
# Array of callbacks which can ass ignored
@ignored_callbacks = []
end
# Ignore a path or add an ignore callback
# @param [String, Regexp] path Path glob expression, or path regex
# @return [void]
def ignore(path=nil, &block)
original_callback_size = @ignored_callbacks.size
if path.is_a? Regexp
@ignored_callbacks << Proc.new {|p| p =~ path }
elsif path.is_a? String
path_clean = ::Middleman::Util.normalize_path(path)
if path_clean.include?("*") # It's a glob
@ignored_callbacks << Proc.new {|p| File.fnmatch(path_clean, p) }
else
# Add a specific-path ignore unless that path is already covered
@ignored_callbacks << Proc.new {|p| p == path_clean } unless ignored?(path_clean)
end end
elsif block_given?
@ignored_callbacks << block
end
end
# Whether a path is ignored alias :included :registered
# @param [String] path end
# @return [Boolean]
def ignored?(path) # Helpers methods for Resources
path_clean = ::Middleman::Util.normalize_path(path) module ResourceInstanceMethods
@ignored_callbacks.any? { |b| b.call(path_clean) }
# Whether the Resource is ignored
# @return [Boolean]
def ignored?
@app.ignore_manager.ignored?(path) ||
(!proxy? &&
@app.ignore_manager.ignored?(source_file.sub("#{@app.source_dir}/", ""))
)
end
end
# Ignore-related instance methods
module InstanceMethods
def ignore_manager
@_ignore_manager ||= IgnoreManager.new(self)
end
# Ignore a path or add an ignore callback
# @param [String, Regexp] path Path glob expression, or path regex
# @return [void]
def ignore(path=nil, &block)
ignore_manager.ignore(path, &block)
end
end
# Class to handle managing ignores
class IgnoreManager
def initialize(app)
@app = app
# Array of callbacks which can ass ignored
@ignored_callbacks = []
end
# Ignore a path or add an ignore callback
# @param [String, Regexp] path Path glob expression, or path regex
# @return [void]
def ignore(path=nil, &block)
original_callback_size = @ignored_callbacks.size
if path.is_a? Regexp
@ignored_callbacks << Proc.new {|p| p =~ path }
elsif path.is_a? String
path_clean = ::Middleman::Util.normalize_path(path)
if path_clean.include?("*") # It's a glob
@ignored_callbacks << Proc.new {|p| File.fnmatch(path_clean, p) }
else
# Add a specific-path ignore unless that path is already covered
@ignored_callbacks << Proc.new {|p| p == path_clean } unless ignored?(path_clean)
end
elsif block_given?
@ignored_callbacks << block
end
end
# Whether a path is ignored
# @param [String] path
# @return [Boolean]
def ignored?(path)
path_clean = ::Middleman::Util.normalize_path(path)
@ignored_callbacks.any? { |b| b.call(path_clean) }
end
end
end end
end end
end end

View file

@ -1,76 +1,83 @@
require 'set' require 'set'
module Middleman::Sitemap::Extensions module Middleman
class OnDisk
attr_accessor :sitemap module Sitemap
attr_accessor :waiting_for_ready
def initialize(sitemap) module Extensions
@sitemap = sitemap
@app = @sitemap.app
@file_paths_on_disk = Set.new class OnDisk
scoped_self = self attr_accessor :sitemap
@waiting_for_ready = true attr_accessor :waiting_for_ready
# Register file change callback def initialize(sitemap)
@app.files.changed do |file| @sitemap = sitemap
scoped_self.touch_file(file, !scoped_self.waiting_for_ready) @app = @sitemap.app
end
# Register file delete callback @file_paths_on_disk = Set.new
@app.files.deleted do |file|
scoped_self.remove_file(file, !scoped_self.waiting_for_ready)
end
@app.ready do scoped_self = self
scoped_self.waiting_for_ready = false @waiting_for_ready = true
scoped_self.sitemap.rebuild_resource_list!(:on_disk_ready)
end
end
# Update or add an on-disk file path # Register file change callback
# @param [String] file @app.files.changed do |file|
# @return [Boolean] scoped_self.touch_file(file, !scoped_self.waiting_for_ready)
def touch_file(file, rebuild=true) end
return false if file == @app.source_dir || File.directory?(file)
path = @sitemap.file_to_path(file) # Register file delete callback
return false unless path @app.files.deleted do |file|
scoped_self.remove_file(file, !scoped_self.waiting_for_ready)
end
ignored = @app.ignored_sitemap_matchers.any? do |name, callback| @app.ready do
callback.call(file, path) scoped_self.waiting_for_ready = false
end scoped_self.sitemap.rebuild_resource_list!(:on_disk_ready)
end
end
@file_paths_on_disk << file unless ignored # Update or add an on-disk file path
# @param [String] file
# @return [Boolean]
def touch_file(file, rebuild=true)
return false if file == @app.source_dir || File.directory?(file)
# Rebuild the sitemap any time a file is touched path = @sitemap.file_to_path(file)
# in case one of the other manipulators return false unless path
# (like asset_hash) cares about the contents of this file,
# whether or not it belongs in the sitemap (like a partial)
@sitemap.rebuild_resource_list!(:touched_file) if rebuild
end
# Remove a file from the store ignored = @app.ignored_sitemap_matchers.any? do |name, callback|
# @param [String] file callback.call(file, path)
# @return [void] end
def remove_file(file, rebuild=true)
if @file_paths_on_disk.delete?(file)
@sitemap.rebuild_resource_list!(:removed_file) if rebuild
end
end
# Update the main sitemap resource list @file_paths_on_disk << file unless ignored
# @return [void]
def manipulate_resource_list(resources) # Rebuild the sitemap any time a file is touched
resources + @file_paths_on_disk.map do |file| # in case one of the other manipulators
::Middleman::Sitemap::Resource.new( # (like asset_hash) cares about the contents of this file,
@sitemap, # whether or not it belongs in the sitemap (like a partial)
@sitemap.file_to_path(file), @sitemap.rebuild_resource_list!(:touched_file) if rebuild
File.expand_path(file, @app.root) end
)
# Remove a file from the store
# @param [String] file
# @return [void]
def remove_file(file, rebuild=true)
if @file_paths_on_disk.delete?(file)
@sitemap.rebuild_resource_list!(:removed_file) if rebuild
end
end
# Update the main sitemap resource list
# @return [void]
def manipulate_resource_list(resources)
resources + @file_paths_on_disk.map do |file|
::Middleman::Sitemap::Resource.new(
@sitemap,
@sitemap.file_to_path(file),
File.expand_path(file, @app.root)
)
end
end
end end
end end
end end

View file

@ -1,99 +1,104 @@
module Middleman::Sitemap::Extensions module Middleman
module Proxies module Sitemap
# Setup extension module Extensions
class << self
# Once registered module Proxies
def registered(app)
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
# Include methods # Setup extension
app.send :include, InstanceMethods class << self
end
alias :included :registered # Once registered
end def registered(app)
::Middleman::Sitemap::Resource.send :include, ResourceInstanceMethods
module ResourceInstanceMethods # Include methods
# Whether this page is a proxy app.send :include, InstanceMethods
# @return [Boolean] end
def proxy?
!!@proxied_to
end
# Set this page to proxy to a target path alias :included :registered
# @param [String] target
# @return [void]
def proxy_to(target)
@proxied_to = target
end
# The path of the page this page is proxied to, or nil if it's not proxied.
# @return [String]
def proxied_to
@proxied_to
end
# Whether this page has a template file
# @return [Boolean]
def template?
if proxy?
store.find_resource_by_path(proxied_to).template?
else
super
end end
end
def get_source_file module ResourceInstanceMethods
if proxy? # Whether this page is a proxy
proxy_resource = store.find_resource_by_path(proxied_to) # @return [Boolean]
raise "Path #{path} proxies to unknown file #{proxied_to}" unless proxy_resource def proxy?
proxy_resource.source_file !!@proxied_to
end
# Set this page to proxy to a target path
# @param [String] target
# @return [void]
def proxy_to(target)
@proxied_to = target
end
# The path of the page this page is proxied to, or nil if it's not proxied.
# @return [String]
def proxied_to
@proxied_to
end
# Whether this page has a template file
# @return [Boolean]
def template?
if proxy?
store.find_resource_by_path(proxied_to).template?
else
super
end
end
def get_source_file
if proxy?
proxy_resource = store.find_resource_by_path(proxied_to)
raise "Path #{path} proxies to unknown file #{proxied_to}" unless proxy_resource
proxy_resource.source_file
end
end
end end
end
end
module InstanceMethods module InstanceMethods
def proxy_manager def proxy_manager
@_proxy_manager ||= ProxyManager.new(self) @_proxy_manager ||= ProxyManager.new(self)
end end
def proxy(*args) def proxy(*args)
proxy_manager.proxy(*args) proxy_manager.proxy(*args)
end end
end end
class ProxyManager class ProxyManager
def initialize(app) def initialize(app)
@app = app @app = app
@proxy_paths = {} @proxy_paths = {}
end end
# Setup a proxy from a path to a target # Setup a proxy from a path to a target
# @param [String] path # @param [String] path
# @param [String] target # @param [String] target
# @return [void] # @return [void]
def proxy(path, target) def proxy(path, target)
@proxy_paths[::Middleman::Util.normalize_path(path)] = ::Middleman::Util.normalize_path(target) @proxy_paths[::Middleman::Util.normalize_path(path)] = ::Middleman::Util.normalize_path(target)
@app.sitemap.rebuild_resource_list!(:added_proxy) @app.sitemap.rebuild_resource_list!(:added_proxy)
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return [void]
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @proxy_paths.map do |key, value| resources + @proxy_paths.map do |key, value|
p = ::Middleman::Sitemap::Resource.new( p = ::Middleman::Sitemap::Resource.new(
@app.sitemap, @app.sitemap,
key key
) )
p.proxy_to(value) p.proxy_to(value)
p p
end
end
end end
end end
end end
end end
end end

View file

@ -1,77 +1,84 @@
module Middleman::Sitemap::Extensions module Middleman
module Traversal
# This resource's parent resource
# @return [Middleman::Sitemap::Resource, nil]
def parent
parts = path.split("/")
parts.pop if path.include?(app.index_file)
return nil if parts.length < 1 module Sitemap
parts.pop module Extensions
parts << app.index_file
parent_path = "/" + parts.join("/") module Traversal
# This resource's parent resource
# @return [Middleman::Sitemap::Resource, nil]
def parent
parts = path.split("/")
parts.pop if path.include?(app.index_file)
store.find_resource_by_destination_path(parent_path) return nil if parts.length < 1
end
# This resource's child resources parts.pop
# @return [Array<Middleman::Sitemap::Resource>] parts << app.index_file
def children
return [] unless directory_index?
if eponymous_directory? parent_path = "/" + parts.join("/")
base_path = eponymous_directory_path
prefix = %r|^#{base_path.sub("/", "\\/")}|
else
base_path = path.sub("#{app.index_file}", "")
prefix = %r|^#{base_path.sub("/", "\\/")}|
end
store.resources.select do |sub_resource| store.find_resource_by_destination_path(parent_path)
if sub_resource.path == self.path || sub_resource.path !~ prefix end
false
else # This resource's child resources
inner_path = sub_resource.path.sub(prefix, "") # @return [Array<Middleman::Sitemap::Resource>]
parts = inner_path.split("/") def children
if parts.length == 1 return [] unless directory_index?
true
elsif parts.length == 2 if eponymous_directory?
parts.last == app.index_file base_path = eponymous_directory_path
prefix = %r|^#{base_path.sub("/", "\\/")}|
else else
false base_path = path.sub("#{app.index_file}", "")
prefix = %r|^#{base_path.sub("/", "\\/")}|
end end
store.resources.select do |sub_resource|
if sub_resource.path == self.path || sub_resource.path !~ prefix
false
else
inner_path = sub_resource.path.sub(prefix, "")
parts = inner_path.split("/")
if parts.length == 1
true
elsif parts.length == 2
parts.last == app.index_file
else
false
end
end
end
end
# This resource's sibling resources
# @return [Array<Middleman::Sitemap::Resource>]
def siblings
return [] unless parent
parent.children.reject { |p| p == self }
end
# Whether this resource either a directory index, or has the same name as an existing directory in the source
# @return [Boolean]
def directory_index?
path.include?(app.index_file) || path =~ /\/$/ || eponymous_directory?
end
# Whether the resource has the same name as a directory in the source
# (e.g., if the resource is named 'gallery.html' and a path exists named 'gallery/', this would return true)
# @return [Boolean]
def eponymous_directory?
full_path = File.join(app.source_dir, eponymous_directory_path)
!!(File.exists?(full_path) && File.directory?(full_path))
end
# The path for this resource if it were a directory, and not a file
# (e.g., for 'gallery.html' this would return 'gallery/')
# @return [String]
def eponymous_directory_path
path.sub(ext, '/').sub(/\/$/, "") + "/"
end end
end end
end end
# This resource's sibling resources
# @return [Array<Middleman::Sitemap::Resource>]
def siblings
return [] unless parent
parent.children.reject { |p| p == self }
end
# Whether this resource either a directory index, or has the same name as an existing directory in the source
# @return [Boolean]
def directory_index?
path.include?(app.index_file) || path =~ /\/$/ || eponymous_directory?
end
# Whether the resource has the same name as a directory in the source
# (e.g., if the resource is named 'gallery.html' and a path exists named 'gallery/', this would return true)
# @return [Boolean]
def eponymous_directory?
full_path = File.join(app.source_dir, eponymous_directory_path)
!!(File.exists?(full_path) && File.directory?(full_path))
end
# The path for this resource if it were a directory, and not a file
# (e.g., for 'gallery.html' this would return 'gallery/')
# @return [String]
def eponymous_directory_path
path.sub(ext, '/').sub(/\/$/, "") + "/"
end
end end
end end

View file

@ -1,148 +1,153 @@
# Sitemap namespace require "middleman-core/sitemap/extensions/traversal"
module Middleman::Sitemap
# Sitemap Resource class module Middleman
class Resource
include Middleman::Sitemap::Extensions::Traversal
# @return [Middleman::Application] # Sitemap namespace
attr_reader :app module Sitemap
# @return [Middleman::Sitemap::Store] # Sitemap Resource class
attr_reader :store class Resource
include Middleman::Sitemap::Extensions::Traversal
# The source path of this resource (relative to the source directory, # @return [Middleman::Application]
# without template extensions) attr_reader :app
# @return [String]
attr_reader :path
# Set the on-disk source file for this resource # @return [Middleman::Sitemap::Store]
# @return [String] attr_reader :store
# attr_reader :source_file
def source_file # The source path of this resource (relative to the source directory,
@source_file || get_source_file # without template extensions)
end # @return [String]
attr_reader :path
# Initialize resource with parent store and URL # Set the on-disk source file for this resource
# @param [Middleman::Sitemap::Store] store # @return [String]
# @param [String] path # attr_reader :source_file
# @param [String] source_file
def initialize(store, path, source_file=nil)
@store = store
@app = @store.app
@path = path
@source_file = source_file
@destination_paths = [@path] def source_file
@source_file || get_source_file
@local_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
end
# Whether this resource has a template file
# @return [Boolean]
def template?
return false if source_file.nil?
!::Tilt[source_file].nil?
end
# Get the metadata for both the current source_file and the current path
# @return [Hash]
def metadata
result = store.metadata_for_file(source_file).dup
path_meta = store.metadata_for_path(path).dup
if path_meta.has_key?(:blocks)
result[:blocks] << path_meta[:blocks]
path_meta.delete(:blocks)
end
result.deep_merge!(path_meta)
local_meta = @local_metadata.dup
if local_meta.has_key?(:blocks)
result[:blocks] << local_meta[:blocks]
local_meta.delete(:blocks)
end
result.deep_merge!(local_meta)
result
end
# Merge in new metadata specific to this resource.
# @param [Hash] metadata A metadata block like provides_metadata_for_path takes
def add_metadata(metadata={}, &block)
if metadata.has_key?(:blocks)
@local_metadata[:blocks] << metadata[:blocks]
metadata.delete(:blocks)
end
@local_metadata.deep_merge(metadata)
@local_metadata[:blocks] << block if block_given?
end
# Get the output/preview URL for this resource
# @return [String]
def destination_path
@destination_paths.last
end
# Set the output/preview URL for this resource
# @param [String] path
# @return [void]
def destination_path=(path)
@destination_paths << path
end
# The template instance
# @return [Middleman::Sitemap::Template]
def template
@_template ||= ::Middleman::Sitemap::Template.new(self)
end
# Extension of the path (i.e. '.js')
# @return [String]
def ext
File.extname(path)
end
# Mime type of the path
# @return [String]
def mime_type
app.mime_type ext
end
# Render this resource
# @return [String]
def render(opts={}, locs={}, &block)
return File.open(source_file).read unless template?
start_time = Time.now
puts "== Render Start: #{source_file}" if app.logging?
md = metadata.dup
opts = md[:options].deep_merge(opts)
locs = md[:locals].deep_merge(locs)
# Forward remaining data to helpers
if md.has_key?(:page)
app.data.store("page", md[:page])
end end
md[:blocks].flatten.compact.each do |block| # Initialize resource with parent store and URL
app.instance_eval(&block) # @param [Middleman::Sitemap::Store] store
# @param [String] path
# @param [String] source_file
def initialize(store, path, source_file=nil)
@store = store
@app = @store.app
@path = path
@source_file = source_file
@destination_paths = [@path]
@local_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
end end
app.instance_eval(&block) if block_given? # Whether this resource has a template file
result = app.render_template(source_file, locs, opts) # @return [Boolean]
def template?
return false if source_file.nil?
!::Tilt[source_file].nil?
end
puts "== Render End: #{source_file} (#{(Time.now - start_time).round(2)}s)" if app.logging? # Get the metadata for both the current source_file and the current path
result # @return [Hash]
end def metadata
result = store.metadata_for_file(source_file).dup
# A path without the directory index - so foo/index.html becomes path_meta = store.metadata_for_path(path).dup
# just foo. Best for linking. if path_meta.has_key?(:blocks)
# @return [String] result[:blocks] << path_meta[:blocks]
def url path_meta.delete(:blocks)
'/' + destination_path.sub(/#{Regexp.escape(app.index_file)}$/, '') end
result.deep_merge!(path_meta)
local_meta = @local_metadata.dup
if local_meta.has_key?(:blocks)
result[:blocks] << local_meta[:blocks]
local_meta.delete(:blocks)
end
result.deep_merge!(local_meta)
result
end
# Merge in new metadata specific to this resource.
# @param [Hash] metadata A metadata block like provides_metadata_for_path takes
def add_metadata(metadata={}, &block)
if metadata.has_key?(:blocks)
@local_metadata[:blocks] << metadata[:blocks]
metadata.delete(:blocks)
end
@local_metadata.deep_merge(metadata)
@local_metadata[:blocks] << block if block_given?
end
# Get the output/preview URL for this resource
# @return [String]
def destination_path
@destination_paths.last
end
# Set the output/preview URL for this resource
# @param [String] path
# @return [void]
def destination_path=(path)
@destination_paths << path
end
# The template instance
# @return [Middleman::Sitemap::Template]
def template
@_template ||= ::Middleman::Sitemap::Template.new(self)
end
# Extension of the path (i.e. '.js')
# @return [String]
def ext
File.extname(path)
end
# Mime type of the path
# @return [String]
def mime_type
app.mime_type ext
end
# Render this resource
# @return [String]
def render(opts={}, locs={}, &block)
return File.open(source_file).read unless template?
start_time = Time.now
puts "== Render Start: #{source_file}" if app.logging?
md = metadata.dup
opts = md[:options].deep_merge(opts)
locs = md[:locals].deep_merge(locs)
# Forward remaining data to helpers
if md.has_key?(:page)
app.data.store("page", md[:page])
end
md[:blocks].flatten.compact.each do |block|
app.instance_eval(&block)
end
app.instance_eval(&block) if block_given?
result = app.render_template(source_file, locs, opts)
puts "== Render End: #{source_file} (#{(Time.now - start_time).round(2)}s)" if app.logging?
result
end
# A path without the directory index - so foo/index.html becomes
# just foo. Best for linking.
# @return [String]
def url
'/' + destination_path.sub(/#{Regexp.escape(app.index_file)}$/, '')
end
end end
end end
end end

View file

@ -1,208 +1,211 @@
# Used for merging results of metadata callbacks # Used for merging results of metadata callbacks
require "active_support/core_ext/hash/deep_merge" require "active_support/core_ext/hash/deep_merge"
# Sitemap namespace module Middleman
module Middleman::Sitemap
# The Store class # Sitemap namespace
# module Sitemap
# The Store manages a collection of Resource objects, which represent
# individual items in the sitemap. Resources are indexed by "source path",
# which is the path relative to the source directory, minus any template
# extensions. All "path" parameters used in this class are source paths.
class Store
# @return [Middleman::Application] # The Store class
attr_accessor :app #
# The Store manages a collection of Resource objects, which represent
# individual items in the sitemap. Resources are indexed by "source path",
# which is the path relative to the source directory, minus any template
# extensions. All "path" parameters used in this class are source paths.
class Store
# Initialize with parent app # @return [Middleman::Application]
# @param [Middleman::Application] app attr_accessor :app
def initialize(app)
@app = app
@resources = []
@_cached_metadata = {}
@_lookup_cache = { :path => {}, :destination_path => {} }
@resource_list_manipulators = []
# Register classes which can manipulate the main site map list
register_resource_list_manipulator(:on_disk, Middleman::Sitemap::Extensions::OnDisk.new(self), false)
# Proxies
register_resource_list_manipulator(:proxies, @app.proxy_manager, false)
end
# Register a klass which can manipulate the main site map list
# @param [Symbol] name Name of the manipulator for debugging
# @param [Class, Module] inst Abstract namespace which can update the resource list
# @param [Boolean] immediately_rebuild Whether the resource list should be immediately recalculated
# @return [void]
def register_resource_list_manipulator(name, inst, immediately_rebuild=true)
@resource_list_manipulators << [name, inst]
rebuild_resource_list!(:registered_new) if immediately_rebuild
end
# Rebuild the list of resources from scratch, using registed manipulators
# @return [void]
def rebuild_resource_list!(reason=nil)
@resources = @resource_list_manipulators.inject([]) do |result, (_, inst)|
inst.manipulate_resource_list(result)
end
# Reset lookup cache
@_lookup_cache = { :path => {}, :destination_path => {} }
@resources.each do |resource|
@_lookup_cache[:path][resource.path] = resource
@_lookup_cache[:destination_path][resource.destination_path] = resource
end
end
# Find a resource given its original path
# @param [String] request_path The original path of a resource.
# @return [Middleman::Sitemap::Resource]
def find_resource_by_path(request_path)
request_path = ::Middleman::Util.normalize_path(request_path)
@_lookup_cache[:path][request_path]
end
# Find a resource given its destination path
# @param [String] request_path The destination (output) path of a resource.
# @return [Middleman::Sitemap::Resource]
def find_resource_by_destination_path(request_path)
request_path = ::Middleman::Util.normalize_path(request_path)
@_lookup_cache[:destination_path][request_path]
end
# Get the array of all resources
# @param [Boolean] include_ignored Whether to include ignored resources
# @return [Array<Middleman::Sitemap::Resource>]
def resources(include_ignored=false)
if include_ignored
@resources
else
@resources.reject(&:ignored?)
end
end
# Register a handler to provide metadata on a file path
# @param [Regexp] matcher
# @return [Array<Array<Proc, Regexp>>]
def provides_metadata(matcher=nil, &block)
@_provides_metadata ||= []
@_provides_metadata << [block, matcher] if block_given?
@_provides_metadata
end
# Get the metadata for a specific file
# @param [String] source_file
# @return [Hash]
def metadata_for_file(source_file)
blank_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
provides_metadata.inject(blank_metadata) do |result, (callback, matcher)|
next result if !matcher.nil? && !source_file.match(matcher)
metadata = callback.call(source_file)
if metadata.has_key?(:blocks)
result[:blocks] << metadata[:blocks]
metadata.delete(:blocks)
end
result.deep_merge(metadata)
end
end
# Register a handler to provide metadata on a url path
# @param [Regexp] matcher
# @param [Symbol] origin an indicator of where this metadata came from - only one
# block per [matcher, origin] pair may exist.
# @return [Array<Array<Proc, Regexp>>]
def provides_metadata_for_path(matcher=nil, origin=nil, &block)
@_provides_metadata_for_path ||= []
if block_given?
if origin
existing_provider = @_provides_metadata_for_path.find {|b,m,o| o == origin && m == matcher}
end
if existing_provider
existing_provider[0] = block
else
@_provides_metadata_for_path << [block, matcher, origin]
end
# Initialize with parent app
# @param [Middleman::Application] app
def initialize(app)
@app = app
@resources = []
@_cached_metadata = {} @_cached_metadata = {}
@_lookup_cache = { :path => {}, :destination_path => {} }
@resource_list_manipulators = []
# Register classes which can manipulate the main site map list
register_resource_list_manipulator(:on_disk, Middleman::Sitemap::Extensions::OnDisk.new(self), false)
# Proxies
register_resource_list_manipulator(:proxies, @app.proxy_manager, false)
end end
@_provides_metadata_for_path
end
# Get the metadata for a specific URL # Register a klass which can manipulate the main site map list
# @param [String] request_path # @param [Symbol] name Name of the manipulator for debugging
# @return [Hash] # @param [Class, Module] inst Abstract namespace which can update the resource list
def metadata_for_path(request_path) # @param [Boolean] immediately_rebuild Whether the resource list should be immediately recalculated
return @_cached_metadata[request_path] if @_cached_metadata[request_path] # @return [void]
def register_resource_list_manipulator(name, inst, immediately_rebuild=true)
@resource_list_manipulators << [name, inst]
rebuild_resource_list!(:registered_new) if immediately_rebuild
end
blank_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] } # Rebuild the list of resources from scratch, using registed manipulators
# @return [void]
@_cached_metadata[request_path] = provides_metadata_for_path.inject(blank_metadata) do |result, (callback, matcher)| def rebuild_resource_list!(reason=nil)
case matcher @resources = @resource_list_manipulators.inject([]) do |result, (_, inst)|
when Regexp inst.manipulate_resource_list(result)
next result unless request_path.match(matcher)
when String
next result unless File.fnmatch("/" + matcher.sub(%r{^/}, ''), "/#{request_path}")
end end
metadata = callback.call(request_path) # Reset lookup cache
@_lookup_cache = { :path => {}, :destination_path => {} }
if metadata.has_key?(:blocks) @resources.each do |resource|
result[:blocks] << metadata[:blocks] @_lookup_cache[:path][resource.path] = resource
metadata.delete(:blocks) @_lookup_cache[:destination_path][resource.destination_path] = resource
end end
result.deep_merge(metadata)
end end
end
# Get the URL path for an on-disk file # Find a resource given its original path
# @param [String] file # @param [String] request_path The original path of a resource.
# @return [String] # @return [Middleman::Sitemap::Resource]
def file_to_path(file) def find_resource_by_path(request_path)
file = File.expand_path(file, @app.root) request_path = ::Middleman::Util.normalize_path(request_path)
@_lookup_cache[:path][request_path]
end
prefix = @app.source_dir.sub(/\/$/, "") + "/" # Find a resource given its destination path
return false unless file.include?(prefix) # @param [String] request_path The destination (output) path of a resource.
# @return [Middleman::Sitemap::Resource]
def find_resource_by_destination_path(request_path)
request_path = ::Middleman::Util.normalize_path(request_path)
@_lookup_cache[:destination_path][request_path]
end
path = file.sub(prefix, "") # Get the array of all resources
extensionless_path(path) # @param [Boolean] include_ignored Whether to include ignored resources
end # @return [Array<Middleman::Sitemap::Resource>]
def resources(include_ignored=false)
# Get a path without templating extensions if include_ignored
# @param [String] file @resources
# @return [String]
def extensionless_path(file)
path = file.dup
end_of_the_line = false
while !end_of_the_line
if !::Tilt[path].nil?
path = path.sub(File.extname(path), "")
else else
end_of_the_line = true @resources.reject(&:ignored?)
end end
end end
# If there is no extension, look for one # Register a handler to provide metadata on a file path
if File.extname(path).empty? # @param [Regexp] matcher
input_ext = File.extname(file) # @return [Array<Array<Proc, Regexp>>]
def provides_metadata(matcher=nil, &block)
@_provides_metadata ||= []
@_provides_metadata << [block, matcher] if block_given?
@_provides_metadata
end
if !input_ext.empty? # Get the metadata for a specific file
input_ext = input_ext.split(".").last.to_sym # @param [String] source_file
if @app.template_extensions.has_key?(input_ext) # @return [Hash]
path << ".#{@app.template_extensions[input_ext]}" def metadata_for_file(source_file)
blank_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
provides_metadata.inject(blank_metadata) do |result, (callback, matcher)|
next result if !matcher.nil? && !source_file.match(matcher)
metadata = callback.call(source_file)
if metadata.has_key?(:blocks)
result[:blocks] << metadata[:blocks]
metadata.delete(:blocks)
end
result.deep_merge(metadata)
end
end
# Register a handler to provide metadata on a url path
# @param [Regexp] matcher
# @param [Symbol] origin an indicator of where this metadata came from - only one
# block per [matcher, origin] pair may exist.
# @return [Array<Array<Proc, Regexp>>]
def provides_metadata_for_path(matcher=nil, origin=nil, &block)
@_provides_metadata_for_path ||= []
if block_given?
if origin
existing_provider = @_provides_metadata_for_path.find {|b,m,o| o == origin && m == matcher}
end
if existing_provider
existing_provider[0] = block
else
@_provides_metadata_for_path << [block, matcher, origin]
end
@_cached_metadata = {}
end
@_provides_metadata_for_path
end
# Get the metadata for a specific URL
# @param [String] request_path
# @return [Hash]
def metadata_for_path(request_path)
return @_cached_metadata[request_path] if @_cached_metadata[request_path]
blank_metadata = { :options => {}, :locals => {}, :page => {}, :blocks => [] }
@_cached_metadata[request_path] = provides_metadata_for_path.inject(blank_metadata) do |result, (callback, matcher)|
case matcher
when Regexp
next result unless request_path.match(matcher)
when String
next result unless File.fnmatch("/" + matcher.sub(%r{^/}, ''), "/#{request_path}")
end
metadata = callback.call(request_path)
if metadata.has_key?(:blocks)
result[:blocks] << metadata[:blocks]
metadata.delete(:blocks)
end
result.deep_merge(metadata)
end
end
# Get the URL path for an on-disk file
# @param [String] file
# @return [String]
def file_to_path(file)
file = File.expand_path(file, @app.root)
prefix = @app.source_dir.sub(/\/$/, "") + "/"
return false unless file.include?(prefix)
path = file.sub(prefix, "")
extensionless_path(path)
end
# Get a path without templating extensions
# @param [String] file
# @return [String]
def extensionless_path(file)
path = file.dup
end_of_the_line = false
while !end_of_the_line
if !::Tilt[path].nil?
path = path.sub(File.extname(path), "")
else
end_of_the_line = true
end end
end end
end
path # If there is no extension, look for one
if File.extname(path).empty?
input_ext = File.extname(file)
if !input_ext.empty?
input_ext = input_ext.split(".").last.to_sym
if @app.template_extensions.has_key?(input_ext)
path << ".#{@app.template_extensions[input_ext]}"
end
end
end
path
end
end end
end end
end end