2016-01-31 23:13:52 +01:00
|
|
|
# Core Pathname library used for traversal
|
|
|
|
require 'pathname'
|
|
|
|
require 'uri'
|
2016-04-22 01:06:26 +02:00
|
|
|
require 'memoist'
|
|
|
|
require 'addressable'
|
|
|
|
require 'tilt'
|
2016-01-31 23:13:52 +01:00
|
|
|
|
|
|
|
require 'middleman-core/contracts'
|
|
|
|
|
|
|
|
# rubocop:disable ModuleLength
|
|
|
|
module Middleman
|
|
|
|
module Util
|
2016-04-22 01:06:26 +02:00
|
|
|
extend Memoist
|
2016-01-31 23:13:52 +01:00
|
|
|
include Contracts
|
|
|
|
|
|
|
|
module_function
|
|
|
|
|
2016-04-22 01:06:26 +02:00
|
|
|
Contract String => ::Addressable::URI
|
|
|
|
def parse_uri(uri)
|
|
|
|
::Addressable::URI.parse(uri)
|
|
|
|
end
|
|
|
|
memoize :parse_uri
|
|
|
|
|
|
|
|
Contract String => Any
|
|
|
|
def tilt_class(path)
|
|
|
|
::Tilt[path]
|
|
|
|
end
|
|
|
|
memoize :tilt_class
|
|
|
|
|
2016-01-31 23:13:52 +01:00
|
|
|
# Normalize a path to not include a leading slash
|
|
|
|
# @param [String] path
|
|
|
|
# @return [String]
|
|
|
|
Contract String => String
|
|
|
|
def normalize_path(path)
|
|
|
|
# The tr call works around a bug in Ruby's Unicode handling
|
|
|
|
::URI.decode(path).sub(%r{^/}, '').tr('', '')
|
|
|
|
end
|
2016-04-22 01:06:26 +02:00
|
|
|
memoize :normalize_path
|
2016-01-31 23:13:52 +01:00
|
|
|
|
|
|
|
# This is a separate method from normalize_path in case we
|
|
|
|
# change how we normalize paths
|
|
|
|
Contract String => String
|
|
|
|
def strip_leading_slash(path)
|
|
|
|
path.sub(%r{^/}, '')
|
|
|
|
end
|
2016-04-22 01:06:26 +02:00
|
|
|
memoize :strip_leading_slash
|
2016-01-31 23:13:52 +01:00
|
|
|
|
2016-04-23 00:52:42 +02:00
|
|
|
IGNORE_DESCRIPTOR = Or[Regexp, RespondTo[:call], String]
|
|
|
|
Contract IGNORE_DESCRIPTOR, String => Bool
|
|
|
|
def should_ignore?(validator, value)
|
|
|
|
if validator.is_a? Regexp
|
|
|
|
# Treat as Regexp
|
|
|
|
!!(value =~ validator)
|
|
|
|
elsif validator.respond_to? :call
|
|
|
|
# Treat as proc
|
|
|
|
validator.call(value)
|
|
|
|
elsif validator.is_a? String
|
|
|
|
# Treat as glob
|
|
|
|
File.fnmatch(value, validator)
|
|
|
|
else
|
|
|
|
# If some unknown thing, don't ignore
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
memoize :should_ignore?
|
|
|
|
|
2016-01-31 23:13:52 +01:00
|
|
|
# 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 ::Middleman::Application, Symbol, Or[String, Symbol], Hash => 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
|
|
|
|
Contract ::Middleman::Application, String, String, Hash => String
|
|
|
|
def asset_url(app, path, prefix='', options={})
|
|
|
|
# Don't touch assets which already have a full path
|
|
|
|
return path if path.include?('//') || path.start_with?('data:')
|
|
|
|
|
|
|
|
if options[:relative] && !options[:current_resource]
|
|
|
|
raise ArgumentError, '#asset_url must be run in a context with current_resource if relative: true'
|
|
|
|
end
|
|
|
|
|
|
|
|
uri = URI(path)
|
|
|
|
path = uri.path
|
|
|
|
|
2016-03-19 03:37:12 +01:00
|
|
|
# Ensure the url we pass into find_resource_by_destination_path is not a
|
|
|
|
# relative path, since it only takes absolute url paths.
|
|
|
|
dest_path = url_for(app, path, options.merge(relative: false))
|
|
|
|
|
2016-05-22 21:31:53 +02:00
|
|
|
result = if resource = app.sitemap.find_resource_by_path(dest_path)
|
|
|
|
resource.url
|
|
|
|
elsif resource = app.sitemap.find_resource_by_destination_path(dest_path)
|
2016-01-31 23:13:52 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
final_result = ::URI.encode(relative_path_from_resource(options[:current_resource], result, options[:relative]))
|
|
|
|
|
|
|
|
result_uri = URI(final_result)
|
|
|
|
result_uri.query = uri.query
|
|
|
|
result_uri.fragment = uri.fragment
|
|
|
|
result_uri.to_s
|
|
|
|
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.
|
2016-02-22 20:17:21 +01:00
|
|
|
Contract ::Middleman::Application, Or[String, Symbol, ::Middleman::Sitemap::Resource], Hash => String
|
2016-01-31 23:13:52 +01:00
|
|
|
def url_for(app, path_or_resource, options={})
|
2016-02-22 20:17:21 +01:00
|
|
|
if path_or_resource.is_a?(String) || path_or_resource.is_a?(Symbol)
|
|
|
|
r = app.sitemap.find_resource_by_page_id(path_or_resource)
|
|
|
|
path_or_resource = r if r
|
|
|
|
end
|
|
|
|
|
2016-01-31 23:13:52 +01:00
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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 && !uri.host
|
|
|
|
# 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)
|
|
|
|
if resource
|
|
|
|
resource_url = resource.url
|
|
|
|
else
|
|
|
|
# Try to find a resource relative to destination paths
|
|
|
|
url_path = Pathname(uri.path)
|
|
|
|
current_source_dir = Pathname('/' + this_resource.destination_path).dirname
|
|
|
|
url_path = current_source_dir.join(url_path) if url_path.relative?
|
|
|
|
resource = app.sitemap.find_resource_by_destination_path(url_path.to_s)
|
|
|
|
resource_url = resource.url if resource
|
|
|
|
end
|
|
|
|
elsif options[:find_resource] && uri.path && !uri.host
|
|
|
|
resource = app.sitemap.find_resource_by_path(uri.path)
|
|
|
|
resource_url = resource.url if resource
|
|
|
|
end
|
|
|
|
|
|
|
|
if resource
|
|
|
|
uri.path = if this_resource
|
|
|
|
::URI.encode(relative_path_from_resource(this_resource, resource_url, effective_relative))
|
|
|
|
else
|
|
|
|
resource_url
|
|
|
|
end
|
|
|
|
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, ::Middleman::Application => String
|
|
|
|
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
|
|
|
|
|
|
|
|
# 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 ::Middleman::Sitemap::Resource, String, Bool => 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
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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 PATH_MATCHER, String => Bool
|
|
|
|
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)
|
|
|
|
!!(path =~ matcher)
|
|
|
|
when matcher.respond_to?(:call)
|
|
|
|
matcher.call(path)
|
|
|
|
else
|
|
|
|
::File.fnmatch(matcher.to_s, path)
|
|
|
|
end
|
|
|
|
end
|
2016-04-22 01:06:26 +02:00
|
|
|
memoize :path_match
|
2016-01-31 23:13:52 +01:00
|
|
|
end
|
|
|
|
end
|