Experiment with Contracts

This commit is contained in:
Thomas Reynolds 2014-07-02 19:04:34 -07:00
parent 928eb82d65
commit 0185d37473
30 changed files with 639 additions and 376 deletions

View file

@ -10,6 +10,7 @@ gem 'fivemat', '~> 1.3.1'
gem 'aruba', '~> 0.6.0' gem 'aruba', '~> 0.6.0'
gem 'rspec', '~> 3.0' gem 'rspec', '~> 3.0'
gem 'simplecov' gem 'simplecov'
gem 'contracts', require: false
# Optional middleman dependencies, included for tests # Optional middleman dependencies, included for tests
gem 'sinatra', require: false gem 'sinatra', require: false

View file

@ -0,0 +1,95 @@
if ENV['TEST'] || ENV['CONTRACTS'] == 'true'
require 'contracts'
class IsA
def self.[](val)
@lookup ||= {}
@lookup[val] ||= new(val)
end
def initialize(val)
@val = val
end
def valid?(val)
val.is_a? @val.constantize
end
end
ResourceList = Contracts::ArrayOf[IsA['Middleman::Sitemap::Resource']]
else
module Contracts
def self.included(base)
base.extend self
end
# rubocop:disable MethodName
def Contract(*)
end
class Callable
def self.[](*)
end
end
class Bool
end
class Num
end
class Pos
end
class Neg
end
class Any
end
class None
end
class Or < Callable
end
class Xor < Callable
end
class And < Callable
end
class Not < Callable
end
class RespondTo < Callable
end
class Send < Callable
end
class Exactly < Callable
end
class ArrayOf < Callable
end
class ResourceList < Callable
end
class Args < Callable
end
class HashOf < Callable
end
class Bool
end
class Maybe < Callable
end
class IsA < Callable
end
end
end

View file

@ -1,3 +1,5 @@
require 'middleman-core/extensions'
# File Change Notifier # File Change Notifier
Middleman::Extensions.register :file_watcher, auto_activate: :before_sitemap do Middleman::Extensions.register :file_watcher, auto_activate: :before_sitemap do
require 'middleman-core/core_extensions/file_watcher' require 'middleman-core/core_extensions/file_watcher'

View file

@ -1,5 +1,6 @@
require 'yaml' require 'yaml'
require 'active_support/json' require 'active_support/json'
require 'middleman-core/contracts'
module Middleman module Middleman
module CoreExtensions module CoreExtensions
@ -33,11 +34,24 @@ module Middleman
# The core logic behind the data extension. # The core logic behind the data extension.
class DataStore class DataStore
include Contracts
# Setup data store
#
# @param [Middleman::Application] app The current instance of Middleman
def initialize(app)
@app = app
@local_data = {}
@local_sources = {}
@callback_sources = {}
end
# Store static data hash # Store static data hash
# #
# @param [Symbol] name Name of the data, used for namespacing # @param [Symbol] name Name of the data, used for namespacing
# @param [Hash] content The content for this data # @param [Hash] content The content for this data
# @return [Hash] # @return [Hash]
Contract Symbol, Hash => Hash
def store(name=nil, content=nil) def store(name=nil, content=nil)
@local_sources[name.to_s] = content unless name.nil? || content.nil? @local_sources[name.to_s] = content unless name.nil? || content.nil?
@local_sources @local_sources
@ -48,21 +62,12 @@ module Middleman
# @param [Symbol] name Name of the data, used for namespacing # @param [Symbol] name Name of the data, used for namespacing
# @param [Proc] proc The callback which will return data # @param [Proc] proc The callback which will return data
# @return [Hash] # @return [Hash]
Contract Symbol, Proc => Hash
def callbacks(name=nil, proc=nil) def callbacks(name=nil, proc=nil)
@callback_sources[name.to_s] = proc unless name.nil? || proc.nil? @callback_sources[name.to_s] = proc unless name.nil? || proc.nil?
@callback_sources @callback_sources
end end
# Setup data store
#
# @param [Middleman::Application] app The current instance of Middleman
def initialize(app)
@app = app
@local_data = {}
@local_sources = {}
@callback_sources = {}
end
# Update the internal cache for a given file path # Update the internal cache for a given file path
# #
# @param [String] file The file to be re-parsed # @param [String] file The file to be re-parsed
@ -91,7 +96,7 @@ module Middleman
data_branch = data_branch[dir] data_branch = data_branch[dir]
end end
data_branch[basename] = ::Middleman::Util.recursively_enhance(data) data_branch[basename] = data && ::Middleman::Util.recursively_enhance(data)
end end
# Remove a given file from the internal cache # Remove a given file from the internal cache
@ -120,6 +125,7 @@ module Middleman
# #
# @param [String, Symbol] path The name of the data namespace # @param [String, Symbol] path The name of the data namespace
# @return [Hash, nil] # @return [Hash, nil]
Contract Or[String, Symbol] => Maybe[Hash]
def data_for_path(path) def data_for_path(path)
response = nil response = nil
@ -170,6 +176,7 @@ module Middleman
# Convert all the data into a static hash # Convert all the data into a static hash
# #
# @return [Hash] # @return [Hash]
Contract None => Hash
def to_h def to_h
data = {} data = {}

View file

@ -1,5 +1,6 @@
require 'pathname' require 'pathname'
require 'set' require 'set'
require 'middleman-core/contracts'
module Middleman module Middleman
module CoreExtensions module CoreExtensions
@ -42,6 +43,7 @@ module Middleman
# Core File Change API class # Core File Change API class
class API class API
extend Forwardable extend Forwardable
include Contracts
attr_reader :app attr_reader :app
attr_reader :known_paths attr_reader :known_paths
@ -61,6 +63,7 @@ module Middleman
# #
# @param [nil,Regexp] matcher A Regexp to match the change path against # @param [nil,Regexp] matcher A Regexp to match the change path against
# @return [Array<Proc>] # @return [Array<Proc>]
Contract Or[Regexp, Proc] => ArrayOf[ArrayOf[Or[Proc, Regexp, nil]]]
def changed(matcher=nil, &block) def changed(matcher=nil, &block)
@_changed << [block, matcher] if block_given? @_changed << [block, matcher] if block_given?
@_changed @_changed
@ -70,6 +73,7 @@ module Middleman
# #
# @param [nil,Regexp] matcher A Regexp to match the deleted path against # @param [nil,Regexp] matcher A Regexp to match the deleted path against
# @return [Array<Proc>] # @return [Array<Proc>]
Contract Or[Regexp, Proc] => ArrayOf[ArrayOf[Or[Proc, Regexp, nil]]]
def deleted(matcher=nil, &block) def deleted(matcher=nil, &block)
@_deleted << [block, matcher] if block_given? @_deleted << [block, matcher] if block_given?
@_deleted @_deleted
@ -130,6 +134,7 @@ module Middleman
reload_path(path, true) reload_path(path, true)
end end
Contract String => Bool
def exists?(path) def exists?(path)
p = Pathname(path) p = Pathname(path)
p = p.relative_path_from(Pathname(@app.root)) unless p.relative? p = p.relative_path_from(Pathname(@app.root)) unless p.relative?
@ -139,6 +144,7 @@ module Middleman
# Whether this path is ignored # Whether this path is ignored
# @param [Pathname] path # @param [Pathname] path
# @return [Boolean] # @return [Boolean]
Contract Or[String, Pathname] => Bool
def ignored?(path) def ignored?(path)
path = path.to_s path = path.to_s
app.config[:file_watcher_ignore].any? { |r| path =~ r } app.config[:file_watcher_ignore].any? { |r| path =~ r }

View file

@ -31,7 +31,8 @@ module Middleman::CoreExtensions
file_watcher.deleted(&method(:clear_data)) file_watcher.deleted(&method(:clear_data))
end end
# Modify each resource to add data & options from frontmatter. # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources.each do |resource| resources.each do |resource|
next if resource.source_file.blank? next if resource.source_file.blank?
@ -60,10 +61,12 @@ module Middleman::CoreExtensions
# Get the template data from a path # Get the template data from a path
# @param [String] path # @param [String] path
# @return [String] # @return [String]
Contract String => String
def template_data_for_file(path) def template_data_for_file(path)
data(path).last data(path).last
end end
Contract String => [Hash, Maybe[String]]
def data(path) def data(path)
p = normalize_path(path) p = normalize_path(path)
@cache[p] ||= frontmatter_and_content(p) @cache[p] ||= frontmatter_and_content(p)
@ -83,6 +86,7 @@ module Middleman::CoreExtensions
# Get the frontmatter and plain content from a file # Get the frontmatter and plain content from a file
# @param [String] path # @param [String] path
# @return [Array<Middleman::Util::HashWithIndifferentAccess, String>] # @return [Array<Middleman::Util::HashWithIndifferentAccess, String>]
Contract String => [Hash, Maybe[String]]
def frontmatter_and_content(path) def frontmatter_and_content(path)
full_path = if Pathname(path).relative? full_path = if Pathname(path).relative?
File.join(app.source_dir, path) File.join(app.source_dir, path)
@ -117,6 +121,7 @@ module Middleman::CoreExtensions
# Parse YAML frontmatter out of a string # Parse YAML frontmatter out of a string
# @param [String] content # @param [String] content
# @return [Array<Hash, String>] # @return [Array<Hash, String>]
Contract String, String => Maybe[[Hash, String]]
def parse_yaml_front_matter(content, full_path) def parse_yaml_front_matter(content, full_path)
yaml_regex = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m yaml_regex = /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
if content =~ yaml_regex if content =~ yaml_regex
@ -127,10 +132,10 @@ module Middleman::CoreExtensions
data = data.symbolize_keys data = data.symbolize_keys
rescue *YAML_ERRORS => e rescue *YAML_ERRORS => e
app.logger.error "YAML Exception parsing #{full_path}: #{e.message}" app.logger.error "YAML Exception parsing #{full_path}: #{e.message}"
return false return nil
end end
else else
return false return nil
end end
[data, content] [data, content]
@ -138,6 +143,10 @@ module Middleman::CoreExtensions
[{}, content] [{}, content]
end end
# Parse JSON frontmatter out of a string
# @param [String] content
# @return [Array<Hash, String>]
Contract String, String => Maybe[[Hash, String]]
def parse_json_front_matter(content, full_path) def parse_json_front_matter(content, full_path)
json_regex = /\A(;;;\s*\n.*?\n?)^(;;;\s*$\n?)/m json_regex = /\A(;;;\s*\n.*?\n?)^(;;;\s*$\n?)/m
@ -149,11 +158,11 @@ module Middleman::CoreExtensions
data = ActiveSupport::JSON.decode(json).symbolize_keys data = ActiveSupport::JSON.decode(json).symbolize_keys
rescue => e rescue => e
app.logger.error "JSON Exception parsing #{full_path}: #{e.message}" app.logger.error "JSON Exception parsing #{full_path}: #{e.message}"
return false return nil
end end
else else
return false return nil
end end
[data, content] [data, content]

View file

@ -43,12 +43,14 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
end end
end end
Contract None => ArrayOf[Symbol]
def langs def langs
@langs ||= known_languages @langs ||= known_languages
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
new_resources = [] new_resources = []
@ -87,6 +89,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
::I18n.reload! ::I18n.reload!
end end
Contract String => Regexp
def convert_glob_to_regex(glob) def convert_glob_to_regex(glob)
# File.fnmatch doesn't support brackets: {rb,yml,yaml} # File.fnmatch doesn't support brackets: {rb,yml,yaml}
regex = glob.sub(/\./, '\.').sub(File.join('**', '*'), '.*').sub(/\//, '\/').sub('{rb,yml,yaml}', '(rb|ya?ml)') regex = glob.sub(/\./, '\.').sub(File.join('**', '*'), '.*').sub(/\//, '\/').sub('{rb,yml,yaml}', '(rb|ya?ml)')
@ -103,6 +106,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
::I18n.fallbacks = ::I18n::Locale::Fallbacks.new if ::I18n.respond_to?(:fallbacks) ::I18n.fallbacks = ::I18n::Locale::Fallbacks.new if ::I18n.respond_to?(:fallbacks)
end end
Contract None => ArrayOf[Symbol]
def known_languages def known_languages
if options[:langs] if options[:langs]
Array(options[:langs]).map(&:to_sym) Array(options[:langs]).map(&:to_sym)
@ -120,6 +124,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
# Parse locale extension filename # Parse locale extension filename
# @return [lang, path, basename] # @return [lang, path, basename]
# will return +nil+ if no locale extension # will return +nil+ if no locale extension
Contract String => Maybe[[Symbol, String, String]]
def parse_locale_extension(path) def parse_locale_extension(path)
path_bits = path.split('.') path_bits = path.split('.')
return nil if path_bits.size < 3 return nil if path_bits.size < 3
@ -132,6 +137,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
[lang, path, basename] [lang, path, basename]
end end
Contract String, String, String, Symbol => IsA['Middleman::Sitemap::Resource']
def build_resource(path, source_path, page_id, lang) def build_resource(path, source_path, page_id, lang)
old_locale = ::I18n.locale old_locale = ::I18n.locale
::I18n.locale = lang ::I18n.locale = lang

View file

@ -16,6 +16,8 @@ module Middleman
app.add_to_config_context :page, &method(:page) app.add_to_config_context :page, &method(:page)
end end
# @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources.each do |resource| resources.each do |resource|
@page_configs.each do |matcher, metadata| @page_configs.each do |matcher, metadata|

View file

@ -1,5 +1,6 @@
require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute'
require 'middleman-core/configuration' require 'middleman-core/configuration'
require 'middleman-core/contracts'
module Middleman module Middleman
# Middleman's Extension API provides the ability to add functionality to Middleman # Middleman's Extension API provides the ability to add functionality to Middleman
@ -64,6 +65,7 @@ module Middleman
# @see http://middlemanapp.com/advanced/custom/ Middleman Custom Extensions Documentation # @see http://middlemanapp.com/advanced/custom/ Middleman Custom Extensions Documentation
class Extension class Extension
extend Forwardable extend Forwardable
include Contracts
# @!attribute supports_multiple_instances # @!attribute supports_multiple_instances
# @!scope class # @!scope class

View file

@ -26,6 +26,7 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
proc: method(:rewrite_url) proc: method(:rewrite_url)
end end
Contract String, Or[String, Pathname], Any => Maybe[String]
def rewrite_url(asset_path, dirpath, _request_path) def rewrite_url(asset_path, dirpath, _request_path)
relative_path = Pathname.new(asset_path).relative? relative_path = Pathname.new(asset_path).relative?
@ -43,7 +44,8 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
@rack_client ||= begin @rack_client ||= begin
rack_app = ::Middleman::Rack.new(app).to_app rack_app = ::Middleman::Rack.new(app).to_app
@ -64,6 +66,7 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
end.each(&method(:manipulate_single_resource)) end.each(&method(:manipulate_single_resource))
end end
Contract IsA['Middleman::Sitemap::Resource'] => Maybe[IsA['Middleman::Sitemap::Resource']]
def manipulate_single_resource(resource) def manipulate_single_resource(resource)
return unless options.exts.include?(resource.ext) return unless options.exts.include?(resource.ext)
return if ignored_resource?(resource) return if ignored_resource?(resource)
@ -79,8 +82,10 @@ class Middleman::Extensions::AssetHash < ::Middleman::Extension
digest = Digest::SHA1.hexdigest(response.body)[0..7] digest = Digest::SHA1.hexdigest(response.body)[0..7]
resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" } resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
resource
end end
Contract IsA['Middleman::Sitemap::Resource'] => Bool
def ignored_resource?(resource) def ignored_resource?(resource)
@ignore.any? { |ignore| Middleman::Util.path_match(ignore, resource.destination_path) } @ignore.any? { |ignore| Middleman::Util.path_match(ignore, resource.destination_path) }
end end

View file

@ -16,6 +16,7 @@ class Middleman::Extensions::AssetHost < ::Middleman::Extension
proc: method(:rewrite_url) proc: method(:rewrite_url)
end end
Contract String, Or[String, Pathname], Any => String
def rewrite_url(asset_path, dirpath, _request_path) def rewrite_url(asset_path, dirpath, _request_path)
relative_path = Pathname.new(asset_path).relative? relative_path = Pathname.new(asset_path).relative?

View file

@ -20,6 +20,7 @@ class Middleman::Extensions::CacheBuster < ::Middleman::Extension
proc: method(:rewrite_url) proc: method(:rewrite_url)
end end
Contract String, Or[String, Pathname], Any => String
def rewrite_url(asset_path, _dirpath, _request_path) def rewrite_url(asset_path, _dirpath, _request_path)
asset_path + '?' + Time.now.strftime('%s') asset_path + '?' + Time.now.strftime('%s')
end end

View file

@ -5,7 +5,8 @@ class Middleman::Extensions::DirectoryIndexes < ::Middleman::Extension
self.resource_list_manipulator_priority = 100 self.resource_list_manipulator_priority = 100
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
index_file = app.config[:index_file] index_file = app.config[:index_file]
new_index_path = "/#{index_file}" new_index_path = "/#{index_file}"

View file

@ -70,6 +70,7 @@ class Middleman::Extensions::Gzip < ::Middleman::Extension
I18n.locale = old_locale I18n.locale = old_locale
end end
Contract String => [Maybe[String], Maybe[Num], Maybe[Num]]
def gzip_file(path) def gzip_file(path)
input_file = File.open(path, 'rb').read input_file = File.open(path, 'rb').read
output_filename = path + '.gz' output_filename = path + '.gz'
@ -104,6 +105,7 @@ class Middleman::Extensions::Gzip < ::Middleman::Extension
# Whether a path should be gzipped # Whether a path should be gzipped
# @param [Pathname] path A destination path # @param [Pathname] path A destination path
# @return [Boolean] # @return [Boolean]
Contract Pathname => Bool
def should_gzip?(path) def should_gzip?(path)
path = path.sub app.config[:build_dir] + '/', '' path = path.sub app.config[:build_dir] + '/', ''
options.exts.include?(path.extname) && options.ignore.none? { |ignore| Middleman::Util.path_match(ignore, path.to_s) } options.exts.include?(path.extname) && options.ignore.none? { |ignore| Middleman::Util.path_match(ignore, path.to_s) }

View file

@ -1,3 +1,5 @@
require 'middleman-core/contracts'
# Minify CSS Extension # Minify CSS Extension
class Middleman::Extensions::MinifyCss < ::Middleman::Extension class Middleman::Extensions::MinifyCss < ::Middleman::Extension
option :inline, false, 'Whether to minify CSS inline within HTML files' option :inline, false, 'Whether to minify CSS inline within HTML files'
@ -24,6 +26,7 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
# Rack middleware to look for CSS and compress it # Rack middleware to look for CSS and compress it
class Rack class Rack
include Contracts
INLINE_CSS_REGEX = /(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m INLINE_CSS_REGEX = /(<style[^>]*>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m
# Init # Init
@ -65,10 +68,12 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
private private
Contract String => Bool
def inline_html_content?(path) def inline_html_content?(path)
(path.end_with?('.html') || path.end_with?('.php')) && @inline (path.end_with?('.html') || path.end_with?('.php')) && @inline
end end
Contract String => Bool
def standalone_css_content?(path) def standalone_css_content?(path)
path.end_with?('.css') && @ignore.none? { |ignore| Middleman::Util.path_match(ignore, path) } path.end_with?('.css') && @ignore.none? { |ignore| Middleman::Util.path_match(ignore, path) }
end end

View file

@ -1,3 +1,5 @@
require 'middleman-core/contracts'
# Minify Javascript Extension # Minify Javascript Extension
class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
option :inline, false, 'Whether to minify JS inline within HTML files' option :inline, false, 'Whether to minify JS inline within HTML files'
@ -16,6 +18,8 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
# Rack middleware to look for JS and compress it # Rack middleware to look for JS and compress it
class Rack class Rack
include Contracts
# Init # Init
# @param [Class] app # @param [Class] app
# @param [Hash] options # @param [Hash] options
@ -61,6 +65,7 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
private private
Contract String => String
def minify_inline_content(uncompressed_source) def minify_inline_content(uncompressed_source)
uncompressed_source.gsub(/(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m) do |match| uncompressed_source.gsub(/(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m) do |match|
first = $1 first = $1

View file

@ -20,6 +20,7 @@ class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
proc: method(:rewrite_url) proc: method(:rewrite_url)
end end
Contract String, Or[String, Pathname], Any => Maybe[String]
def rewrite_url(asset_path, dirpath, request_path) def rewrite_url(asset_path, dirpath, request_path)
relative_path = Pathname.new(asset_path).relative? relative_path = Pathname.new(asset_path).relative?

View file

@ -1,5 +1,7 @@
require 'tilt' require 'tilt'
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/module/delegation'
require 'middleman-core/contracts'
::Tilt.mappings.delete('html') # WTF, Tilt? ::Tilt.mappings.delete('html') # WTF, Tilt?
::Tilt.mappings.delete('csv') ::Tilt.mappings.delete('csv')
@ -7,6 +9,7 @@ require 'active_support/core_ext/string/output_safety'
module Middleman module Middleman
class FileRenderer class FileRenderer
extend Forwardable extend Forwardable
include Contracts
def self.cache def self.cache
@_cache ||= ::Tilt::Cache.new @_cache ||= ::Tilt::Cache.new
@ -25,6 +28,7 @@ module Middleman
# @param [Hash] opts # @param [Hash] opts
# @param [Class] context # @param [Class] context
# @return [String] # @return [String]
Contract Hash, Hash, Any, Proc => String
def render(locs={}, opts={}, context, &block) def render(locs={}, opts={}, context, &block)
path = @path.dup path = @path.dup
@ -96,6 +100,7 @@ module Middleman
# Get the template data from a path # Get the template data from a path
# @param [String] path # @param [String] path
# @return [String] # @return [String]
Contract String => String
def template_data_for_file def template_data_for_file
if @app.extensions[:front_matter] if @app.extensions[:front_matter]
@app.extensions[:front_matter].template_data_for_file(@path) @app.extensions[:front_matter].template_data_for_file(@path)
@ -111,6 +116,7 @@ module Middleman
# #
# @param [String] ext # @param [String] ext
# @return [Hash] # @return [Hash]
Contract String => Hash
def options_for_ext(ext) def options_for_ext(ext)
# Read options for extension from config/Tilt or cache # Read options for extension from config/Tilt or cache
cache.fetch(:options_for_ext, ext) do cache.fetch(:options_for_ext, ext) do

View file

@ -1,10 +1,13 @@
require 'middleman-core/util' require 'middleman-core/util'
require 'middleman-core/contracts'
require 'rack' require 'rack'
require 'rack/response' require 'rack/response'
module Middleman module Middleman
module Middleware module Middleware
class InlineURLRewriter class InlineURLRewriter
include Contracts
def initialize(app, options={}) def initialize(app, options={})
@rack_app = app @rack_app = app
@middleman_app = options[:middleman_app] @middleman_app = options[:middleman_app]
@ -63,10 +66,11 @@ module Middleman
[status, headers, response] [status, headers, response]
end end
Contract Or[Regexp, RespondTo[:call], String] => Bool
def should_ignore?(validator, value) def should_ignore?(validator, value)
if validator.is_a? Regexp if validator.is_a? Regexp
# Treat as Regexp # Treat as Regexp
value.match(validator) !value.match(validator).nil?
elsif validator.respond_to? :call elsif validator.respond_to? :call
# Treat as proc # Treat as proc
validator.call(value) validator.call(value)

View file

@ -10,6 +10,8 @@ module Middleman
::Liquid::Template.file_system = ::Liquid::LocalFileSystem.new(app.source_dir) ::Liquid::Template.file_system = ::Liquid::LocalFileSystem.new(app.source_dir)
end end
# @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
return resources unless app.extensions[:data] return resources unless app.extensions[:data]

View file

@ -18,6 +18,7 @@ module Middleman
# Ignore a path or add an ignore callback # Ignore a path or add an ignore callback
# @param [String, Regexp] path Path glob expression, or path regex # @param [String, Regexp] path Path glob expression, or path regex
# @return [void] # @return [void]
Contract Maybe[Or[String, Regexp]], Proc => Any
def create_ignore(path=nil, &block) def create_ignore(path=nil, &block)
if path.is_a? Regexp if path.is_a? Regexp
@ignored_callbacks << proc { |p| p =~ path } @ignored_callbacks << proc { |p| p =~ path }
@ -40,6 +41,7 @@ module Middleman
# Whether a path is ignored # Whether a path is ignored
# @param [String] path # @param [String] path
# @return [Boolean] # @return [Boolean]
Contract String => Bool
def ignored?(path) def ignored?(path)
path_clean = ::Middleman::Util.normalize_path(path) path_clean = ::Middleman::Util.normalize_path(path)
@ignored_callbacks.any? { |b| b.call(path_clean) } @ignored_callbacks.any? { |b| b.call(path_clean) }

View file

@ -1,4 +1,5 @@
require 'set' require 'set'
require 'middleman-core/contracts'
module Middleman module Middleman
module Sitemap module Sitemap
@ -21,6 +22,7 @@ module Middleman
end end
end end
Contract None => Any
def before_configuration def before_configuration
file_watcher.changed(&method(:touch_file)) file_watcher.changed(&method(:touch_file))
file_watcher.deleted(&method(:remove_file)) file_watcher.deleted(&method(:remove_file))
@ -28,12 +30,16 @@ module Middleman
# Update or add an on-disk file path # Update or add an on-disk file path
# @param [String] file # @param [String] file
# @return [Boolean] # @return [void]
Contract String => Any
def touch_file(file) def touch_file(file)
return false if File.directory?(file) return false if File.directory?(file)
path = @app.sitemap.file_to_path(file) begin
return false unless path @app.sitemap.file_to_path(file)
rescue
return
end
ignored = @app.config[:ignored_sitemap_matchers].any? do |_, callback| ignored = @app.config[:ignored_sitemap_matchers].any? do |_, callback|
if callback.arity == 1 if callback.arity == 1
@ -59,6 +65,7 @@ module Middleman
# Remove a file from the store # Remove a file from the store
# @param [String] file # @param [String] file
# @return [void] # @return [void]
Contract String => Any
def remove_file(file) def remove_file(file)
return unless @file_paths_on_disk.delete?(file) return unless @file_paths_on_disk.delete?(file)
@ -70,7 +77,8 @@ module Middleman
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @file_paths_on_disk.map do |file| resources + @file_paths_on_disk.map do |file|
::Middleman::Sitemap::Resource.new( ::Middleman::Sitemap::Resource.new(

View file

@ -24,6 +24,7 @@ module Middleman
# @option opts [Hash] locals Local variables for the template. These will be available when the template renders. # @option opts [Hash] locals Local variables for the template. These will be available when the template renders.
# @option opts [Hash] data Extra metadata to add to the page. This is the same as frontmatter, though frontmatter will take precedence over metadata defined here. Available via {Resource#data}. # @option opts [Hash] data Extra metadata to add to the page. This is the same as frontmatter, though frontmatter will take precedence over metadata defined here. Available via {Resource#data}.
# @return [void] # @return [void]
Contract String, String, Maybe[Hash] => Any
def create_proxy(path, target, opts={}) def create_proxy(path, target, opts={})
options = opts.dup options = opts.dup
@ -41,7 +42,8 @@ module Middleman
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @proxy_configs.map do |config| resources + @proxy_configs.map do |config|
p = ProxyResource.new( p = ProxyResource.new(
@ -108,6 +110,7 @@ module Middleman
# The resource for the page this page is proxied to. Throws an exception # The resource for the page this page is proxied to. Throws an exception
# if there is no resource. # if there is no resource.
# @return [Sitemap::Resource] # @return [Sitemap::Resource]
Contract None => IsA['Middleman::Sitemap::Resource']
def target_resource def target_resource
resource = @store.find_resource_by_path(@target) resource = @store.find_resource_by_path(@target)
@ -122,10 +125,12 @@ module Middleman
resource resource
end end
Contract None => String
def source_file def source_file
target_resource.source_file target_resource.source_file
end end
Contract None => Maybe[String]
def content_type def content_type
mime_type = super mime_type = super
return mime_type if mime_type return mime_type if mime_type

View file

@ -1,4 +1,5 @@
require 'middleman-core/sitemap/resource' require 'middleman-core/sitemap/resource'
require 'middleman-core/contracts'
module Middleman module Middleman
module Sitemap module Sitemap
@ -17,6 +18,7 @@ module Middleman
# Setup a redirect from a path to a target # Setup a redirect from a path to a target
# @param [String] path # @param [String] path
# @param [Hash] opts The :to value gives a target path # @param [Hash] opts The :to value gives a target path
Contract String, ({ to: Or[String, IsA['Middleman::Sitemap::Resource']] }), Proc => Any
def create_redirect(path, opts={}, &block) def create_redirect(path, opts={}, &block)
opts[:template] = block if block_given? opts[:template] = block if block_given?
@ -26,7 +28,8 @@ module Middleman
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @redirects.map do |path, opts| resources + @redirects.map do |path, opts|
r = RedirectResource.new( r = RedirectResource.new(
@ -41,6 +44,7 @@ module Middleman
end end
class RedirectResource < ::Middleman::Sitemap::Resource class RedirectResource < ::Middleman::Sitemap::Resource
Contract None => Maybe[Proc]
attr_accessor :output attr_accessor :output
def initialize(store, path, target) def initialize(store, path, target)
@ -49,10 +53,12 @@ module Middleman
super(store, path) super(store, path)
end end
Contract None => Bool
def template? def template?
true true
end end
Contract Args[Any] => String
def render(*) def render(*)
url = ::Middleman::Util.url_for(@store.app, @request_path, url = ::Middleman::Util.url_for(@store.app, @request_path,
relative: false, relative: false,
@ -76,6 +82,7 @@ module Middleman
end end
end end
Contract None => Bool
def ignored? def ignored?
false false
end end

View file

@ -18,6 +18,7 @@ module Middleman
# @param [String] path # @param [String] path
# @param [Hash] opts The :path value gives a request path if it # @param [Hash] opts The :path value gives a request path if it
# differs from the output path # differs from the output path
Contract String, Or[({ path: String }), Proc] => Any
def create_endpoint(path, opts={}, &block) def create_endpoint(path, opts={}, &block)
endpoint = { endpoint = {
request_path: path request_path: path
@ -35,7 +36,8 @@ module Middleman
end end
# Update the main sitemap resource list # Update the main sitemap resource list
# @return [void] # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @endpoints.map do |path, config| resources + @endpoints.map do |path, config|
r = EndpointResource.new( r = EndpointResource.new(
@ -50,6 +52,7 @@ module Middleman
end end
class EndpointResource < ::Middleman::Sitemap::Resource class EndpointResource < ::Middleman::Sitemap::Resource
Contract None => Maybe[Proc]
attr_accessor :output attr_accessor :output
def initialize(store, path, request_path) def initialize(store, path, request_path)
@ -57,16 +60,20 @@ module Middleman
@request_path = ::Middleman::Util.normalize_path(request_path) @request_path = ::Middleman::Util.normalize_path(request_path)
end end
Contract None => String
attr_reader :request_path attr_reader :request_path
Contract None => Bool
def template? def template?
true true
end end
Contract Args[Any] => String
def render(*) def render(*)
return output.call if output return output.call if output
end end
Contract None => Bool
def ignored? def ignored?
false false
end end

View file

@ -2,12 +2,14 @@ require 'rack/mime'
require 'middleman-core/sitemap/extensions/traversal' require 'middleman-core/sitemap/extensions/traversal'
require 'middleman-core/file_renderer' require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer' require 'middleman-core/template_renderer'
require 'middleman-core/contracts'
module Middleman module Middleman
# Sitemap namespace # Sitemap namespace
module Sitemap module Sitemap
# Sitemap Resource class # Sitemap Resource class
class Resource class Resource
include Contracts
include Middleman::Sitemap::Extensions::Traversal include Middleman::Sitemap::Extensions::Traversal
# The source path of this resource (relative to the source directory, # The source path of this resource (relative to the source directory,
@ -28,6 +30,10 @@ module Middleman
# @return [String] # @return [String]
alias_method :request_path, :destination_path alias_method :request_path, :destination_path
# The metadata for this resource
# @return [Hash]
attr_reader :metadata
# Initialize resource with parent store and URL # Initialize resource with parent store and URL
# @param [Middleman::Sitemap::Store] store # @param [Middleman::Sitemap::Store] store
# @param [String] path # @param [String] path
@ -48,6 +54,7 @@ module Middleman
# Whether this resource has a template file # Whether this resource has a template file
# @return [Boolean] # @return [Boolean]
Contract None => Bool
def template? def template?
return false if source_file.nil? return false if source_file.nil?
!::Tilt[source_file].nil? !::Tilt[source_file].nil?
@ -59,16 +66,14 @@ module Middleman
# Locals are local variables for rendering this resource's template # Locals are local variables for rendering this resource's template
# Page are data that is exposed through this resource's data member. # Page are data that is exposed through this resource's data member.
# Note: It is named 'page' for backwards compatibility with older MM. # Note: It is named 'page' for backwards compatibility with older MM.
Contract Hash => Hash
def add_metadata(meta={}) def add_metadata(meta={})
@metadata.deep_merge!(meta) @metadata.deep_merge!(meta)
end end
# The metadata for this resource
# @return [Hash]
attr_reader :metadata
# Data about this resource, populated from frontmatter or extensions. # Data about this resource, populated from frontmatter or extensions.
# @return [HashWithIndifferentAccess] # @return [HashWithIndifferentAccess]
Contract None => IsA['Middleman::Util::HashWithIndifferentAccess']
def data def data
# TODO: Should this really be a HashWithIndifferentAccess? # TODO: Should this really be a HashWithIndifferentAccess?
::Middleman::Util.recursively_enhance(metadata[:page]).freeze ::Middleman::Util.recursively_enhance(metadata[:page]).freeze
@ -77,30 +82,34 @@ module Middleman
# Options about how this resource is rendered, such as its :layout, # Options about how this resource is rendered, such as its :layout,
# :renderer_options, and whether or not to use :directory_indexes. # :renderer_options, and whether or not to use :directory_indexes.
# @return [Hash] # @return [Hash]
Contract None => Hash
def options def options
metadata[:options] metadata[:options]
end end
# Local variable mappings that are used when rendering the template for this resource. # Local variable mappings that are used when rendering the template for this resource.
# @return [Hash] # @return [Hash]
Contract None => Hash
def locals def locals
metadata[:locals] metadata[:locals]
end end
# Extension of the path (i.e. '.js') # Extension of the path (i.e. '.js')
# @return [String] # @return [String]
Contract None => String
def ext def ext
File.extname(path) File.extname(path)
end end
# Render this resource # Render this resource
# @return [String] # @return [String]
Contract Hash, Hash => String
def render(opts={}, locs={}) def render(opts={}, locs={})
return ::Middleman::FileRenderer.new(@app, source_file).template_data_for_file unless template? return ::Middleman::FileRenderer.new(@app, source_file).template_data_for_file unless template?
relative_source = Pathname(source_file).relative_path_from(Pathname(@app.root)) relative_source = Pathname(source_file).relative_path_from(Pathname(@app.root))
@app.instrument 'render.resource', path: relative_source, destination_path: destination_path do ::Middleman::Util.instrument 'render.resource', path: relative_source, destination_path: destination_path do
md = metadata md = metadata
opts = md[:options].deep_merge(opts) opts = md[:options].deep_merge(opts)
locs = md[:locals].deep_merge(locs) locs = md[:locals].deep_merge(locs)
@ -119,6 +128,7 @@ module Middleman
# A path without the directory index - so foo/index.html becomes # A path without the directory index - so foo/index.html becomes
# just foo. Best for linking. # just foo. Best for linking.
# @return [String] # @return [String]
Contract None => String
def url def url
url_path = destination_path url_path = destination_path
if @app.config[:strip_index_file] if @app.config[:strip_index_file]
@ -131,19 +141,22 @@ module Middleman
# Whether the source file is binary. # Whether the source file is binary.
# #
# @return [Boolean] # @return [Boolean]
Contract None => Bool
def binary? def binary?
source_file && ::Middleman::Util.binary?(source_file) !source_file.nil? && ::Middleman::Util.binary?(source_file)
end end
# Ignore a resource directly, without going through the whole # Ignore a resource directly, without going through the whole
# ignore filter stuff. # ignore filter stuff.
# @return [void] # @return [void]
Contract None => Any
def ignore! def ignore!
@ignored = true @ignored = true
end end
# Whether the Resource is ignored # Whether the Resource is ignored
# @return [Boolean] # @return [Boolean]
Contract None => Bool
def ignored? def ignored?
return true if @ignored return true if @ignored
# Ignore based on the source path (without template extensions) # Ignore based on the source path (without template extensions)
@ -154,6 +167,7 @@ module Middleman
# The preferred MIME content type for this resource based on extension or metadata # The preferred MIME content type for this resource based on extension or metadata
# @return [String] MIME type for this resource # @return [String] MIME type for this resource
Contract None => Maybe[String]
def content_type def content_type
options[:content_type] || ::Rack::Mime.mime_type(ext, nil) options[:content_type] || ::Rack::Mime.mime_type(ext, nil)
end end

View file

@ -32,6 +32,8 @@ Middleman::Extensions.register :sitemap_redirects, auto_activate: :before_config
Middleman::Sitemap::Extensions::Redirects Middleman::Sitemap::Extensions::Redirects
end end
require 'middleman-core/contracts'
module Middleman module Middleman
# Sitemap namespace # Sitemap namespace
module Sitemap module Sitemap
@ -42,6 +44,8 @@ module Middleman
# which is the path relative to the source directory, minus any template # which is the path relative to the source directory, minus any template
# extensions. All "path" parameters used in this class are source paths. # extensions. All "path" parameters used in this class are source paths.
class Store class Store
include Contracts
# @return [Middleman::Application] # @return [Middleman::Application]
attr_reader :app attr_reader :app
@ -67,6 +71,7 @@ module Middleman
# @param [#manipulate_resource_list] manipulator Resource list manipulator # @param [#manipulate_resource_list] manipulator Resource list manipulator
# @param [Numeric] priority Sets the order of this resource list manipulator relative to the rest. By default this is 50, and manipulators run in the order they are registered, but if a priority is provided then this will run ahead of or behind other manipulators. # @param [Numeric] priority Sets the order of this resource list manipulator relative to the rest. By default this is 50, and manipulators run in the order they are registered, but if a priority is provided then this will run ahead of or behind other manipulators.
# @return [void] # @return [void]
Contract Symbol, RespondTo['manipulate_resource_list'], Maybe[Num] => Any
def register_resource_list_manipulator(name, manipulator, priority=50) def register_resource_list_manipulator(name, manipulator, priority=50)
# The third argument used to be a boolean - handle those who still pass one # The third argument used to be a boolean - handle those who still pass one
priority = 50 unless priority.is_a? Numeric priority = 50 unless priority.is_a? Numeric
@ -92,6 +97,7 @@ module Middleman
# Find a resource given its original path # Find a resource given its original path
# @param [String] request_path The original path of a resource. # @param [String] request_path The original path of a resource.
# @return [Middleman::Sitemap::Resource] # @return [Middleman::Sitemap::Resource]
Contract String => Maybe[IsA['Middleman::Sitemap::Resource']]
def find_resource_by_path(request_path) def find_resource_by_path(request_path)
@lock.synchronize do @lock.synchronize do
request_path = ::Middleman::Util.normalize_path(request_path) request_path = ::Middleman::Util.normalize_path(request_path)
@ -103,6 +109,7 @@ module Middleman
# Find a resource given its destination path # Find a resource given its destination path
# @param [String] request_path The destination (output) path of a resource. # @param [String] request_path The destination (output) path of a resource.
# @return [Middleman::Sitemap::Resource] # @return [Middleman::Sitemap::Resource]
Contract String => Maybe[IsA['Middleman::Sitemap::Resource']]
def find_resource_by_destination_path(request_path) def find_resource_by_destination_path(request_path)
@lock.synchronize do @lock.synchronize do
request_path = ::Middleman::Util.normalize_path(request_path) request_path = ::Middleman::Util.normalize_path(request_path)
@ -114,6 +121,7 @@ module Middleman
# Get the array of all resources # Get the array of all resources
# @param [Boolean] include_ignored Whether to include ignored resources # @param [Boolean] include_ignored Whether to include ignored resources
# @return [Array<Middleman::Sitemap::Resource>] # @return [Array<Middleman::Sitemap::Resource>]
Contract Bool => ResourceList
def resources(include_ignored=false) def resources(include_ignored=false)
@lock.synchronize do @lock.synchronize do
ensure_resource_list_updated! ensure_resource_list_updated!
@ -134,11 +142,12 @@ module Middleman
# Get the URL path for an on-disk file # Get the URL path for an on-disk file
# @param [String] file # @param [String] file
# @return [String] # @return [String]
Contract String => String
def file_to_path(file) def file_to_path(file)
file = File.join(@app.root, file) file = File.join(@app.root, file)
prefix = @app.source_dir.sub(/\/$/, '') + '/' prefix = @app.source_dir.sub(/\/$/, '') + '/'
return false unless file.start_with?(prefix) raise "'#{file}' not inside project folder '#{prefix}" unless file.start_with?(prefix)
path = file.sub(prefix, '') path = file.sub(prefix, '')
@ -153,6 +162,7 @@ module Middleman
# Get a path without templating extensions # Get a path without templating extensions
# @param [String] file # @param [String] file
# @return [String] # @return [String]
Contract String => String
def extensionless_path(file) def extensionless_path(file)
path = file.dup path = file.dup
remove_templating_extensions(path) remove_templating_extensions(path)
@ -197,6 +207,7 @@ module Middleman
# Removes the templating extensions, while keeping the others # Removes the templating extensions, while keeping the others
# @param [String] path # @param [String] path
# @return [String] # @return [String]
Contract String => String
def remove_templating_extensions(path) def remove_templating_extensions(path)
# Strip templating extensions as long as Tilt knows them # Strip templating extensions as long as Tilt knows them
path = path.sub(File.extname(path), '') while ::Tilt[path] path = path.sub(File.extname(path), '') while ::Tilt[path]
@ -206,6 +217,7 @@ module Middleman
# Remove the locale token from the end of the path # Remove the locale token from the end of the path
# @param [String] path # @param [String] path
# @return [String] # @return [String]
Contract String => String
def strip_away_locale(path) def strip_away_locale(path)
if @app.extensions[:i18n] if @app.extensions[:i18n]
path_bits = path.split('.') path_bits = path.split('.')

View file

@ -1,6 +1,7 @@
require 'pathname' require 'pathname'
require 'middleman-core/file_renderer' require 'middleman-core/file_renderer'
require 'middleman-core/template_renderer' require 'middleman-core/template_renderer'
require 'middleman-core/contracts'
module Middleman module Middleman
# The TemplateContext Class # The TemplateContext Class
@ -12,6 +13,7 @@ module Middleman
# the request, passed from template, to layouts and partials. # the request, passed from template, to layouts and partials.
class TemplateContext class TemplateContext
extend Forwardable extend Forwardable
include Contracts
# Allow templates to directly access the current app instance. # Allow templates to directly access the current app instance.
# @return [Middleman::Application] # @return [Middleman::Application]
@ -94,6 +96,7 @@ module Middleman
# @param [String, Symbol] name The partial to render. # @param [String, Symbol] name The partial to render.
# @param [Hash] options # @param [Hash] options
# @return [String] # @return [String]
Contract Any, Or[Symbol, String], Hash => String
def render(_, name, options={}, &block) def render(_, name, options={}, &block)
name = name.to_s name = name.to_s
@ -114,6 +117,7 @@ module Middleman
# @api private # @api private
# @param [String] partial_path # @param [String] partial_path
# @return [String] # @return [String]
Contract String => Maybe[String]
def locate_partial(partial_path) def locate_partial(partial_path)
return unless resource = sitemap.find_resource_by_path(current_path) return unless resource = sitemap.find_resource_by_path(current_path)
@ -141,6 +145,7 @@ module Middleman
# @param [Hash] opts Template options. # @param [Hash] opts Template options.
# @param [Proc] block A block will be evaluated to return internal contents. # @param [Proc] block A block will be evaluated to return internal contents.
# @return [String] The resulting content string. # @return [String] The resulting content string.
Contract String, Hash, Hash, Proc => String
def render_file(path, locs, opts, &block) def render_file(path, locs, opts, &block)
file_renderer = ::Middleman::FileRenderer.new(@app, path) file_renderer = ::Middleman::FileRenderer.new(@app, path)
file_renderer.render(locs, opts, self, &block) file_renderer.render(locs, opts, self, &block)

View file

@ -2,10 +2,12 @@ require 'tilt'
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'middleman-core/template_context' require 'middleman-core/template_context'
require 'middleman-core/file_renderer' require 'middleman-core/file_renderer'
require 'middleman-core/contracts'
module Middleman module Middleman
class TemplateRenderer class TemplateRenderer
extend Forwardable extend Forwardable
include Contracts
def self.cache def self.cache
@_cache ||= ::Tilt::Cache.new @_cache ||= ::Tilt::Cache.new
@ -26,6 +28,7 @@ module Middleman
# @param [Hash] locs # @param [Hash] locs
# @param [Hash] opts # @param [Hash] opts
# @return [String] # @return [String]
Contract Hash, Hash => String
def render(locs={}, opts={}) def render(locs={}, opts={})
path = @path.dup path = @path.dup
extension = File.extname(path) extension = File.extname(path)
@ -78,7 +81,8 @@ module Middleman
# #
# @param [Symbol] engine # @param [Symbol] engine
# @param [Hash] opts # @param [Hash] opts
# @return [String] # @return [String, Boolean]
Contract Symbol, Hash => Or[String, Bool]
def fetch_layout(engine, opts) def fetch_layout(engine, opts)
# The layout name comes from either the system default or the options # The layout name comes from either the system default or the options
local_layout = opts.key?(:layout) ? opts[:layout] : @app.config[:layout] local_layout = opts.key?(:layout) ? opts[:layout] : @app.config[:layout]
@ -117,6 +121,7 @@ module Middleman
# @param [String] name # @param [String] name
# @param [Symbol] preferred_engine # @param [Symbol] preferred_engine
# @return [String] # @return [String]
Contract Or[String, Symbol], Symbol => Maybe[String]
def locate_layout(name, preferred_engine=nil) def locate_layout(name, preferred_engine=nil)
self.class.locate_layout(@app, name, preferred_engine) self.class.locate_layout(@app, name, preferred_engine)
end end
@ -125,6 +130,7 @@ module Middleman
# @param [String] name # @param [String] name
# @param [Symbol] preferred_engine # @param [Symbol] preferred_engine
# @return [String] # @return [String]
Contract IsA['Middleman::Application'], Or[String, Symbol], Symbol => Maybe[String]
def self.locate_layout(app, name, preferred_engine=nil) def self.locate_layout(app, name, preferred_engine=nil)
resolve_opts = {} resolve_opts = {}
resolve_opts[:preferred_engine] = preferred_engine unless preferred_engine.nil? resolve_opts[:preferred_engine] = preferred_engine unless preferred_engine.nil?
@ -143,6 +149,7 @@ module Middleman
# @param [String] request_path # @param [String] request_path
# @param [Hash] options # @param [Hash] options
# @return [Array<String, Symbol>, Boolean] # @return [Array<String, Symbol>, Boolean]
Contract String, Hash => ArrayOf[Or[String, Symbol]]
def resolve_template(request_path, options={}) def resolve_template(request_path, options={})
self.class.resolve_template(@app, request_path, options) self.class.resolve_template(@app, request_path, options)
end end
@ -151,6 +158,7 @@ module Middleman
# @param [String] request_path # @param [String] request_path
# @option options [Boolean] :preferred_engine If set, try this engine first, then fall back to any engine. # @option options [Boolean] :preferred_engine If set, try this engine first, then fall back to any engine.
# @return [String, Boolean] Either the path to the template, or false # @return [String, Boolean] Either the path to the template, or false
Contract IsA['Middleman::Application'], Or[Symbol, String], Hash => Maybe[String]
def self.resolve_template(app, request_path, options={}) def self.resolve_template(app, request_path, options={})
# Find the path by searching or using the cache # Find the path by searching or using the cache
request_path = request_path.to_s request_path = request_path.to_s
@ -194,7 +202,7 @@ module Middleman
elsif File.exist?(on_disk_path) elsif File.exist?(on_disk_path)
on_disk_path on_disk_path
else else
false nil
end end
end end
end end

View file

@ -11,348 +11,12 @@ require 'pathname'
require 'tilt' require 'tilt'
require 'rack/mime' require 'rack/mime'
# DbC
require 'middleman-core/contracts'
module Middleman module Middleman
module Util module Util
class << self include Contracts
# Whether the source file is binary.
#
# @param [String] filename The file to check.
# @return [Boolean]
def binary?(filename)
ext = File.extname(filename)
# We hardcode detecting of gzipped SVG files
return true if ext == '.svgz'
return false if Tilt.registered?(ext.sub('.', ''))
dot_ext = (ext.to_s[0] == '.') ? ext.dup : ".#{ext}"
if mime = ::Rack::Mime.mime_type(dot_ext, nil)
!nonbinary_mime?(mime)
else
file_contents_include_binary_bytes?(filename)
end
end
# Facade for ActiveSupport/Notification
def instrument(name, payload={}, &block)
suffixed_name = (name =~ /\.middleman$/) ? name.dup : "#{name}.middleman"
::ActiveSupport::Notifications.instrument(suffixed_name, payload, &block)
end
# Recursively convert a normal Hash into a HashWithIndifferentAccess
#
# @private
# @param [Hash] data Normal hash
# @return [Middleman::Util::HashWithIndifferentAccess]
def recursively_enhance(data)
if data.is_a? Hash
data = ::Middleman::Util::HashWithIndifferentAccess.new(data)
data.each do |key, val|
data[key] = recursively_enhance(val)
end
data
elsif data.is_a? Array
data.each_with_index do |val, i|
data[i] = recursively_enhance(val)
end
data
else
data
end
end
# Normalize a path to not include a leading slash
# @param [String] path
# @return [String]
def normalize_path(path)
# The tr call works around a bug in Ruby's Unicode handling
path.sub(%r{^/}, '').tr('', '')
end
# This is a separate method from normalize_path in case we
# change how we normalize paths
def strip_leading_slash(path)
path.sub(%r{^/}, '')
end
# Extract the text of a Rack response as a string.
# Useful for extensions implemented as Rack middleware.
# @param response The response from #call
# @return [String] The whole response as a string.
def extract_response_text(response)
# The rack spec states all response bodies must respond to each
result = ''
response.each do |part, _|
result << part
end
result
end
# Takes a matcher, which can be a literal string
# or a string containing glob expressions, or a
# regexp, or a proc, or anything else that responds
# to #match or #call, and returns whether or not the
# given path matches that matcher.
#
# @param [String, #match, #call] matcher A matcher String, RegExp, Proc, etc.
# @param [String] path A path as a string
# @return [Boolean] Whether the path matches the matcher
def path_match(matcher, path)
case
when matcher.is_a?(String)
if matcher.include? '*'
File.fnmatch(matcher, path)
else
path == matcher
end
when matcher.respond_to?(:match)
!matcher.match(path).nil?
when matcher.respond_to?(:call)
matcher.call(path)
else
File.fnmatch(matcher.to_s, path)
end
end
# Get a recusive list of files inside a path.
# Works with symlinks.
#
# @param path Some path string or Pathname
# @param ignore A proc/block that returns true if a given path should be ignored - if a path
# is ignored, nothing below it will be searched either.
# @return [Array<Pathname>] An array of Pathnames for each file (no directories)
def all_files_under(path, &ignore)
path = Pathname(path)
return [] if ignore && ignore.call(path)
if path.directory?
path.children.flat_map do |child|
all_files_under(child, &ignore)
end.compact
elsif path.file?
[path]
else
[]
end
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
# @param [Hash] options Data to pass through.
# @return [String]
def asset_path(app, kind, source, options={})
return source if source.to_s.include?('//') || source.to_s.start_with?('data:')
asset_folder = case kind
when :css
app.config[:css_dir]
when :js
app.config[:js_dir]
when :images
app.config[:images_dir]
when :fonts
app.config[:fonts_dir]
else
kind.to_s
end
source = source.to_s.tr(' ', '')
ignore_extension = (kind == :images || kind == :fonts) # don't append extension
source << ".#{kind}" unless ignore_extension || source.end_with?(".#{kind}")
asset_folder = '' if source.start_with?('/') # absolute path
asset_url(app, source, asset_folder, options)
end
# 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")
# @param [Hash] options Data to pass through.
# @return [String] The fully qualified asset url
def asset_url(app, path, prefix='', _options={})
# Don't touch assets which already have a full path
if path.include?('//') || path.start_with?('data:')
path
else # rewrite paths to use their destination path
if resource = app.sitemap.find_resource_by_destination_path(url_for(app, path))
resource.url
else
path = File.join(prefix, path)
if resource = app.sitemap.find_resource_by_path(path)
resource.url
else
File.join(app.config[:http_prefix], path)
end
end
end
end
# Given a source path (referenced either absolutely or relatively)
# or a Resource, this will produce the nice URL configured for that
# path, respecting :relative_links, directory indexes, etc.
def url_for(app, path_or_resource, options={})
# Handle Resources and other things which define their own url method
url = if path_or_resource.respond_to?(:url)
path_or_resource.url
else
path_or_resource.dup
end.gsub(' ', '%20')
# Try to parse URL
begin
uri = URI(url)
rescue URI::InvalidURIError
# Nothing we can do with it, it's not really a URI
return url
end
relative = options[:relative]
raise "Can't use the relative option with an external URL" if relative && uri.host
# Allow people to turn on relative paths for all links with
# set :relative_links, true
# but still override on a case by case basis with the :relative parameter.
effective_relative = relative || false
effective_relative = true if relative.nil? && app.config[:relative_links]
# Try to find a sitemap resource corresponding to the desired path
this_resource = options[:current_resource]
if path_or_resource.is_a?(::Middleman::Sitemap::Resource)
resource = path_or_resource
resource_url = url
elsif this_resource && uri.path
# Handle relative urls
url_path = Pathname(uri.path)
current_source_dir = Pathname('/' + this_resource.path).dirname
url_path = current_source_dir.join(url_path) if url_path.relative?
resource = app.sitemap.find_resource_by_path(url_path.to_s)
resource_url = resource.url if resource
elsif options[:find_resource] && uri.path
resource = app.sitemap.find_resource_by_path(uri.path)
resource_url = resource.url if resource
end
if resource
uri.path = relative_path_from_resource(this_resource, resource_url, effective_relative)
else
# If they explicitly asked for relative links but we can't find a resource...
raise "No resource exists at #{url}" if relative
end
# Support a :query option that can be a string or hash
if query = options[:query]
uri.query = query.respond_to?(:to_param) ? query.to_param : query.to_s
end
# Support a :fragment or :anchor option just like Padrino
fragment = options[:anchor] || options[:fragment]
uri.fragment = fragment.to_s if fragment
# Finally make the URL back into a string
uri.to_s
end
# Expand a path to include the index file if it's a directory
#
# @param [String] path Request path/
# @param [Middleman::Application] app The requesting app.
# @return [String] Path with index file if necessary.
def full_path(path, app)
resource = app.sitemap.find_resource_by_destination_path(path)
unless resource
# Try it with /index.html at the end
indexed_path = File.join(path.sub(%r{/$}, ''), app.config[:index_file])
resource = app.sitemap.find_resource_by_destination_path(indexed_path)
end
if resource
'/' + resource.destination_path
else
'/' + normalize_path(path)
end
end
def rewrite_paths(body, _path, exts, &_block)
body.dup.gsub(/([=\'\"\(]\s*)([^\s\'\"\)]+(#{Regexp.union(exts)}))/) do |match|
opening_character = $1
asset_path = $2
if result = yield(asset_path)
"#{opening_character}#{result}"
else
match
end
end
end
private
# Is mime type known to be non-binary?
#
# @param [String] mime The mimetype to check.
# @return [Boolean]
def nonbinary_mime?(mime)
case
when mime.start_with?('text/')
true
when mime.include?('xml')
true
when mime.include?('json')
true
when mime.include?('javascript')
true
else
false
end
end
# Read a few bytes from the file and see if they are binary.
#
# @param [String] filename The file to check.
# @return [Boolean]
def file_contents_include_binary_bytes?(filename)
binary_bytes = [0, 1, 2, 3, 4, 5, 6, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30, 31]
s = File.read(filename, 4096) || ''
s.each_byte do |c|
return true if binary_bytes.include?(c)
end
false
end
# Get a relative path to a resource.
#
# @param [Middleman::Sitemap::Resource] curr_resource The resource.
# @param [String] resource_url The target url.
# @param [Boolean] relative If the path should be relative.
# @return [String]
def relative_path_from_resource(curr_resource, resource_url, relative)
# Switch to the relative path between resource and the given resource
# if we've been asked to.
if relative && curr_resource
# Output urls relative to the destination path, not the source path
current_dir = Pathname('/' + curr_resource.destination_path).dirname
relative_path = Pathname(resource_url).relative_path_from(current_dir).to_s
# Put back the trailing slash to avoid unnecessary Apache redirects
if resource_url.end_with?('/') && !relative_path.end_with?('/')
relative_path << '/'
end
relative_path
else
resource_url
end
end
end
# A hash with indifferent access and magic predicates. # A hash with indifferent access and magic predicates.
# Copied from Thor # Copied from Thor
@ -428,5 +92,373 @@ module Middleman
end end
end end
end end
# Whether the source file is binary.
#
# @param [String] filename The file to check.
# @return [Boolean]
Contract String => Bool
def self.binary?(filename)
ext = File.extname(filename)
# We hardcode detecting of gzipped SVG files
return true if ext == '.svgz'
return false if Tilt.registered?(ext.sub('.', ''))
dot_ext = (ext.to_s[0] == '.') ? ext.dup : ".#{ext}"
if mime = ::Rack::Mime.mime_type(dot_ext, nil)
!nonbinary_mime?(mime)
else
file_contents_include_binary_bytes?(filename)
end
end
# Takes a matcher, which can be a literal string
# or a string containing glob expressions, or a
# regexp, or a proc, or anything else that responds
# to #match or #call, and returns whether or not the
# given path matches that matcher.
#
# @param [String, #match, #call] matcher A matcher String, RegExp, Proc, etc.
# @param [String] path A path as a string
# @return [Boolean] Whether the path matches the matcher
Contract Or[String, RespondTo[:match], RespondTo[:call], RespondTo[:to_s]], String => Bool
def self.path_match(matcher, path)
case
when matcher.is_a?(String)
if matcher.include? '*'
File.fnmatch(matcher, path)
else
path == matcher
end
when matcher.respond_to?(:match)
!matcher.match(path).nil?
when matcher.respond_to?(:call)
matcher.call(path)
else
File.fnmatch(matcher.to_s, path)
end
end
# Recursively convert a normal Hash into a HashWithIndifferentAccess
#
# @private
# @param [Hash] data Normal hash
# @return [Middleman::Util::HashWithIndifferentAccess]
Contract Or[Hash, Array] => Or[HashWithIndifferentAccess, Array]
def self.recursively_enhance(data)
if data.is_a? Hash
enhanced = ::Middleman::Util::HashWithIndifferentAccess.new(data)
enhanced.each do |key, val|
enhanced[key] = if val.is_a?(Hash) || val.is_a?(Array)
recursively_enhance(val)
else
val
end
end
enhanced
elsif data.is_a? Array
enhanced = data.dup
enhanced.each_with_index do |val, i|
enhanced[i] = if val.is_a?(Hash) || val.is_a?(Array)
recursively_enhance(val)
else
val
end
end
enhanced
end
end
# Normalize a path to not include a leading slash
# @param [String] path
# @return [String]
Contract String => String
def self.normalize_path(path)
# The tr call works around a bug in Ruby's Unicode handling
path.sub(%r{^/}, '').tr('', '')
end
# This is a separate method from normalize_path in case we
# change how we normalize paths
Contract String => String
def self.strip_leading_slash(path)
path.sub(%r{^/}, '')
end
# Facade for ActiveSupport/Notification
def self.instrument(name, payload={}, &block)
suffixed_name = (name =~ /\.middleman$/) ? name.dup : "#{name}.middleman"
::ActiveSupport::Notifications.instrument(suffixed_name, payload, &block)
end
# Extract the text of a Rack response as a string.
# Useful for extensions implemented as Rack middleware.
# @param response The response from #call
# @return [String] The whole response as a string.
Contract IsA['Rack::BodyProxy'] => String
def self.extract_response_text(response)
# The rack spec states all response bodies must respond to each
result = ''
response.each do |part, _|
result << part
end
result
end
# Get a recusive list of files inside a path.
# Works with symlinks.
#
# @param path Some path string or Pathname
# @param ignore A proc/block that returns true if a given path should be ignored - if a path
# is ignored, nothing below it will be searched either.
# @return [Array<Pathname>] An array of Pathnames for each file (no directories)
Contract Or[String, Pathname], Proc => ArrayOf[Pathname]
def self.all_files_under(path, &ignore)
path = Pathname(path)
return [] if ignore && ignore.call(path)
if path.directory?
path.children.flat_map do |child|
all_files_under(child, &ignore)
end.compact
elsif path.file?
[path]
else
[]
end
end
# Get the path of a file of a given type
#
# @param [Middleman::Application] app The app.
# @param [Symbol] kind The type of file
# @param [String, Symbol] source The path to the file
# @param [Hash] options Data to pass through.
# @return [String]
Contract IsA['Middleman::Application'], Symbol, Or[String, Symbol], Hash => String
def self.asset_path(app, kind, source, options={})
return source if source.to_s.include?('//') || source.to_s.start_with?('data:')
asset_folder = case kind
when :css
app.config[:css_dir]
when :js
app.config[:js_dir]
when :images
app.config[:images_dir]
when :fonts
app.config[:fonts_dir]
else
kind.to_s
end
source = source.to_s.tr(' ', '')
ignore_extension = (kind == :images || kind == :fonts) # don't append extension
source << ".#{kind}" unless ignore_extension || source.end_with?(".#{kind}")
asset_folder = '' if source.start_with?('/') # absolute path
asset_url(app, source, asset_folder, options)
end
# 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")
# @param [Hash] options Data to pass through.
# @return [String] The fully qualified asset url
Contract IsA['Middleman::Application'], String, String, Hash => String
def self.asset_url(app, path, prefix='', _options={})
# Don't touch assets which already have a full path
if path.include?('//') || path.start_with?('data:')
path
else # rewrite paths to use their destination path
if resource = app.sitemap.find_resource_by_destination_path(url_for(app, path))
resource.url
else
path = File.join(prefix, path)
if resource = app.sitemap.find_resource_by_path(path)
resource.url
else
File.join(app.config[:http_prefix], path)
end
end
end
end
# Given a source path (referenced either absolutely or relatively)
# or a Resource, this will produce the nice URL configured for that
# path, respecting :relative_links, directory indexes, etc.
Contract IsA['Middleman::Application'], Or[String, IsA['Middleman::Sitemap::Resource']], Hash => String
def self.url_for(app, path_or_resource, options={})
# Handle Resources and other things which define their own url method
url = if path_or_resource.respond_to?(:url)
path_or_resource.url
else
path_or_resource.dup
end.gsub(' ', '%20')
# Try to parse URL
begin
uri = URI(url)
rescue URI::InvalidURIError
# Nothing we can do with it, it's not really a URI
return url
end
relative = options[:relative]
raise "Can't use the relative option with an external URL" if relative && uri.host
# Allow people to turn on relative paths for all links with
# set :relative_links, true
# but still override on a case by case basis with the :relative parameter.
effective_relative = relative || false
effective_relative = true if relative.nil? && app.config[:relative_links]
# Try to find a sitemap resource corresponding to the desired path
this_resource = options[:current_resource]
if path_or_resource.is_a?(::Middleman::Sitemap::Resource)
resource = path_or_resource
resource_url = url
elsif this_resource && uri.path
# Handle relative urls
url_path = Pathname(uri.path)
current_source_dir = Pathname('/' + this_resource.path).dirname
url_path = current_source_dir.join(url_path) if url_path.relative?
resource = app.sitemap.find_resource_by_path(url_path.to_s)
resource_url = resource.url if resource
elsif options[:find_resource] && uri.path
resource = app.sitemap.find_resource_by_path(uri.path)
resource_url = resource.url if resource
end
if resource
uri.path = if this_resource
relative_path_from_resource(this_resource, resource_url, effective_relative)
else
resource_url
end
else
# If they explicitly asked for relative links but we can't find a resource...
raise "No resource exists at #{url}" if relative
end
# Support a :query option that can be a string or hash
if query = options[:query]
uri.query = query.respond_to?(:to_param) ? query.to_param : query.to_s
end
# Support a :fragment or :anchor option just like Padrino
fragment = options[:anchor] || options[:fragment]
uri.fragment = fragment.to_s if fragment
# Finally make the URL back into a string
uri.to_s
end
# Expand a path to include the index file if it's a directory
#
# @param [String] path Request path/
# @param [Middleman::Application] app The requesting app.
# @return [String] Path with index file if necessary.
Contract String, IsA['Middleman::Application'] => String
def self.full_path(path, app)
resource = app.sitemap.find_resource_by_destination_path(path)
unless resource
# Try it with /index.html at the end
indexed_path = File.join(path.sub(%r{/$}, ''), app.config[:index_file])
resource = app.sitemap.find_resource_by_destination_path(indexed_path)
end
if resource
'/' + resource.destination_path
else
'/' + normalize_path(path)
end
end
Contract String, String, ArrayOf[String], Proc => String
def self.rewrite_paths(body, _path, exts, &_block)
body.dup.gsub(/([=\'\"\(]\s*)([^\s\'\"\)]+(#{Regexp.union(exts)}))/) do |match|
opening_character = $1
asset_path = $2
if result = yield(asset_path)
"#{opening_character}#{result}"
else
match
end
end
end
# Is mime type known to be non-binary?
#
# @param [String] mime The mimetype to check.
# @return [Boolean]
Contract String => Bool
def self.nonbinary_mime?(mime)
case
when mime.start_with?('text/')
true
when mime.include?('xml')
true
when mime.include?('json')
true
when mime.include?('javascript')
true
else
false
end
end
# Read a few bytes from the file and see if they are binary.
#
# @param [String] filename The file to check.
# @return [Boolean]
Contract String => Bool
def self.file_contents_include_binary_bytes?(filename)
binary_bytes = [0, 1, 2, 3, 4, 5, 6, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30, 31]
s = File.read(filename, 4096) || ''
s.each_byte do |c|
return true if binary_bytes.include?(c)
end
false
end
# Get a relative path to a resource.
#
# @param [Middleman::Sitemap::Resource] curr_resource The resource.
# @param [String] resource_url The target url.
# @param [Boolean] relative If the path should be relative.
# @return [String]
Contract IsA['Middleman::Sitemap::Resource'], String, Bool => String
def self.relative_path_from_resource(curr_resource, resource_url, relative)
# Switch to the relative path between resource and the given resource
# if we've been asked to.
if relative
# Output urls relative to the destination path, not the source path
current_dir = Pathname('/' + curr_resource.destination_path).dirname
relative_path = Pathname(resource_url).relative_path_from(current_dir).to_s
# Put back the trailing slash to avoid unnecessary Apache redirects
if resource_url.end_with?('/') && !relative_path.end_with?('/')
relative_path << '/'
end
relative_path
else
resource_url
end
end
end end
end end