lib/sprockets/resolve.rb



require 'set'
require 'sprockets/http_utils'
require 'sprockets/path_dependency_utils'
require 'sprockets/uri_utils'

module Sprockets
  module Resolve
    include HTTPUtils, PathDependencyUtils, URIUtils

    # Public: Find Asset URI for given a logical path by searching the
    # environment's load paths.
    #
    #     resolve("application.js")
    #     # => "file:///path/to/app/javascripts/application.js?type=application/javascript"
    #
    # An accept content type can be given if the logical path doesn't have a
    # format extension.
    #
    #     resolve("application", accept: "application/javascript")
    #     # => "file:///path/to/app/javascripts/application.coffee?type=application/javascript"
    #
    # The String Asset URI is returned or nil if no results are found.
    def resolve(path, load_paths: config[:paths], accept: nil, pipeline: nil, base_path: nil)
      paths = load_paths

      if valid_asset_uri?(path)
        uri, deps = resolve_asset_uri(path)
      elsif absolute_path?(path)
        filename, type, deps = resolve_absolute_path(paths, path, accept)
      elsif relative_path?(path)
        filename, type, path_pipeline, deps, index_alias = resolve_relative_path(paths, path, base_path, accept)
      else
        filename, type, path_pipeline, deps, index_alias = resolve_logical_path(paths, path, accept)
      end

      if filename
        uri = build_asset_uri(filename, type: type, pipeline: pipeline || path_pipeline, index_alias: index_alias)
      end

      return uri, deps
    end

    # Public: Same as resolve() but raises a FileNotFound exception instead of
    # nil if no assets are found.
    def resolve!(path, **kargs)
      uri, deps = resolve(path, **kargs)

      unless uri
        message = "couldn't find file '#{path}'"

        if relative_path?(path) && kargs[:base_path]
          load_path, _ = paths_split(config[:paths], kargs[:base_path])
          message << " under '#{load_path}'"
        end

        message << " with type '#{kargs[:accept]}'" if kargs[:accept]

        raise FileNotFound, message
      end

      return uri, deps
    end

    protected

      # Internal: Finds an asset given a URI
      #
      # uri - String. Contains file:// scheme, absolute path to
      #       file.
      #       e.g. "file:///Users/schneems/sprockets/test/fixtures/default/gallery.js?type=application/javascript"
      #
      # Returns Array. Contains a String uri and Set of dependencies
      def resolve_asset_uri(uri)
        filename, _ = URIUtils.parse_asset_uri(uri)
        return uri, Set.new( [URIUtils.build_file_digest_uri(filename)] )
      end

      # Internal: Finds a file in a set of given paths
      #
      # paths    - Array of Strings.
      # filename - String containing absolute path to a file including extension.
      #            e.g. "/Users/schneems/sprockets/test/fixtures/asset/application.js"
      # accept   - String. A Quality value incoded set of
      #            mime types that we are looking for. Can be nil.
      #            e.g. "application/javascript" or "text/css, */*"
      #
      # Returns Array. Filename, type, path_pipeline, deps, index_alias
      def resolve_absolute_path(paths, filename, accept)
        deps = Set.new
        filename = File.expand_path(filename)

        # Ensure path is under load paths
        return nil, nil, deps unless PathUtils.paths_split(paths, filename)

        _, mime_type = PathUtils.match_path_extname(filename, config[:mime_exts])
        type = resolve_transform_type(mime_type, accept)
        return nil, nil, deps if accept && !type

        return nil, nil, deps unless file?(filename)

        deps << URIUtils.build_file_digest_uri(filename)
        return filename, type, deps
      end

      # Internal: Finds a relative file in a set of given paths
      #
      # paths   - Array of Strings.
      # path    - String. A relative filename with or without extension
      #           e.g. "./jquery" or "../foo.js"
      # dirname - String. Base path where we start looking for the given file.
      # accept  - String. A Quality value incoded set of
      #           mime types that we are looking for. Can be nil.
      #           e.g. "application/javascript" or "text/css, */*"
      #
      # Returns Array. Filename, type, path_pipeline, deps, index_alias
      def resolve_relative_path(paths, path, dirname, accept)
        filename = File.expand_path(path, dirname)
        load_path, _ = PathUtils.paths_split(paths, dirname)
        if load_path && logical_path = PathUtils.split_subpath(load_path, filename)
          resolve_logical_path([load_path], logical_path, accept)
        else
          return nil, nil, nil, Set.new
        end
      end

      # Internal: Finds a file in a set of given paths
      #
      # paths        - Array of Strings.
      # logical_path - String. A filename with extension
      #                e.g. "coffee/foo.js" or "foo.js"
      # accept       - String. A Quality value incoded set of
      #                mime types that we are looking for. Can be nil.
      #                e.g. "application/javascript" or "text/css, */*"
      #
      # Finds a file on the given paths.
      #
      # Returns Array. Filename, type, path_pipeline, deps, index_alias
      def resolve_logical_path(paths, logical_path, accept)
        extname, mime_type = PathUtils.match_path_extname(logical_path, config[:mime_exts])
        logical_name = logical_path.chomp(extname)

        extname, pipeline = PathUtils.match_path_extname(logical_name, config[:pipeline_exts])
        logical_name = logical_name.chomp(extname)

        parsed_accept = parse_accept_options(mime_type, accept)
        transformed_accepts = expand_transform_accepts(parsed_accept)

        filename, mime_type, deps, index_alias = resolve_under_paths(paths, logical_name, transformed_accepts)

        if filename
          deps << build_file_digest_uri(filename)
          type = resolve_transform_type(mime_type, parsed_accept)
          return filename, type, pipeline, deps, index_alias
        else
          return nil, nil, nil, deps
        end
      end

      # Internal: Finds a file in a set of given paths
      #
      # paths        - Array of Strings.
      # logical_name - String. A filename without extension
      #                e.g. "application" or "coffee/foo"
      # accepts      - Array of array containing mime/version pairs
      #                e.g. [["application/javascript", 1.0]]
      #
      # Finds a file with the same name as `logical_name` or "index" inside
      # of the `logical_name` directory that matches a valid mime-type/version from
      # `accepts`.
      #
      # Returns Array. Filename, type, dependencies, and index_alias
      def resolve_under_paths(paths, logical_name, accepts)
        deps = Set.new
        return nil, nil, deps if accepts.empty?

        # TODO: Allow new path resolves to be registered
        @resolvers ||= [
          method(:resolve_main_under_path),
          method(:resolve_alts_under_path),
          method(:resolve_index_under_path)
        ]
        mime_exts = config[:mime_exts]

        paths.each do |load_path|
          candidates = []
          @resolvers.each do |fn|
            result = fn.call(load_path, logical_name, mime_exts)
            candidates.concat(result[0])
            deps.merge(result[1])
          end

          candidate = HTTPUtils.find_best_q_match(accepts, candidates) do |c, matcher|
            match_mime_type?(c[:type] || "application/octet-stream", matcher)
          end
          return candidate[:filename], candidate[:type], deps, candidate[:index_alias] if candidate
        end

        return nil, nil, deps
      end

      # Internal: Finds candidate files on a given path
      #
      # load_path    - String. An absolute path to a directory
      # logical_name - String. A filename without extension
      #                e.g. "application" or "coffee/foo"
      # mime_exts    - Hash of file extensions and their mime types
      #                e.g. {".xml.builder"=>"application/xml+builder"}
      #
      # Finds files that match a given `logical_name` with an acceptable
      # mime type that is included in `mime_exts` on the `load_path`.
      #
      # Returns Array. First element is an Array of hashes or empty, second is a String
      def resolve_main_under_path(load_path, logical_name, mime_exts)
        dirname    = File.dirname(File.join(load_path, logical_name))
        candidates = self.find_matching_path_for_extensions(dirname, File.basename(logical_name), mime_exts)
        candidates.map! do |c|
          { filename: c[0], type: c[1] }
        end
        return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
      end


      # Internal: Finds candidate index files in a given path
      #
      # load_path    - String. An absolute path to a directory
      # logical_name - String. A filename without extension
      #                e.g. "application" or "coffee/foo"
      # mime_exts    - Hash of file extensions and their mime types
      #                e.g. {".xml.builder"=>"application/xml+builder"}
      #
      # Looking in the given `load_path` this method will find all files under the `logical_name` directory
      # that are named `index` and have a matching mime type in `mime_exts`.
      #
      # Returns Array. First element is an Array of hashes or empty, second is a String
      def resolve_index_under_path(load_path, logical_name, mime_exts)
        dirname = File.join(load_path, logical_name)

        if self.directory?(dirname)
          candidates = self.find_matching_path_for_extensions(dirname, "index".freeze, mime_exts)
        else
          candidates = []
        end

        candidates.map! do |c|
          { filename: c[0],
            type: c[1],
            index_alias: compress_from_root(c[0].sub(/\/index(\.[^\/]+)$/, '\1')) }
        end

        return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
      end

      def resolve_alts_under_path(load_path, logical_name, mime_exts)
        filenames, deps = self.resolve_alternates(load_path, logical_name)
        filenames.map! do |fn|
          _, mime_type = PathUtils.match_path_extname(fn, mime_exts)
          { filename: fn, type: mime_type }
        end
        return filenames, deps
      end

      # Internal: Converts mimetype into accept Array
      #
      # - mime_type     - String, optional. e.g. "text/html"
      # - explicit_type - String, optional. e.g. "application/javascript"
      #
      # When called with an explicit_type and a mime_type, only a mime_type
      # that matches the given explicit_type will be accepted.
      #
      # Returns Array of Array
      #
      #     [["application/javascript", 1.0]]
      #     [["*/*", 1.0]]
      #     []
      def parse_accept_options(mime_type, explicit_type)
        if mime_type
          return [[mime_type, 1.0]] if explicit_type.nil?
          return [[mime_type, 1.0]] if HTTPUtils.parse_q_values(explicit_type).any? { |accept, _| HTTPUtils.match_mime_type?(mime_type, accept) }
          return []
        end

        accepts = HTTPUtils.parse_q_values(explicit_type)
        accepts << ['*/*'.freeze, 1.0] if accepts.empty?
        return accepts
      end

      def resolve_alternates(load_path, logical_name)
        return [], Set.new
      end
  end
end