# For instrumenting require 'active_support/notifications' # Using Thor's indifferent hash access require 'thor' # Core Pathname library used for traversal require 'pathname' # Template and Mime detection require 'tilt' require 'rack/mime' module Middleman module Util class << self # 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, s| 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 matcher A matcher string/regexp/proc/etc # @param 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) path.match(matcher) when matcher.respond_to?(:match) matcher.match(path) 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] 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 # 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) if !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 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. # Copied from Thor # # hash = Middleman::Util::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true # # hash[:foo] #=> 'bar' # hash['foo'] #=> 'bar' # hash.foo? #=> true # class HashWithIndifferentAccess < ::Hash #:nodoc: def initialize(hash={}) super() hash.each do |key, value| self[convert_key(key)] = value end end def [](key) super(convert_key(key)) end def []=(key, value) super(convert_key(key), value) end def delete(key) super(convert_key(key)) end def values_at(*indices) indices.collect { |key| self[convert_key(key)] } end def merge(other) dup.merge!(other) end def merge!(other) other.each do |key, value| self[convert_key(key)] = value end self end # Convert to a Hash with String keys. def to_hash Hash.new(default).merge!(self) end protected def convert_key(key) key.is_a?(Symbol) ? key.to_s : key end # Magic predicates. For instance: # # options.force? # => !!options['force'] # options.shebang # => "/usr/lib/local/ruby" # options.test_framework?(:rspec) # => options[:test_framework] == :rspec # def method_missing(method, *args, &block) method = method.to_s if method =~ /^(\w+)\?$/ if args.empty? !!self[$1] else self[$1] == args.first end else self[method] end end end end end