chriseppstein/compass

View on GitHub
core/lib/compass/core/asset_url_resolver.rb

Summary

Maintainability
A
2 hrs
Test Coverage
class Compass::Core::AssetUrlResolver
  include Compass::Core::HTTPUtil

  class AssetNotFound < Sass::SyntaxError
    def initialize(type, path, asset_collections)
      asset_search_paths = asset_collections.map{|ac| ac.send(:"#{type}s_path") }
      super("Could not find #{path} in #{ asset_search_paths.join(", ")}")
    end
  end

  attr_accessor :asset_collections
  attr_accessor :configuration

  def initialize(asset_collections, configuration = nil)
    @configuration = configuration || Compass.configuration
    @asset_collections = asset_collections.dup
    unless @asset_collections.find {|ac| Compass::Configuration::DefaultAssetCollection === ac }
      @asset_collections.unshift(Compass::Configuration::DefaultAssetCollection.new)
    end
    if configuration
      @asset_collections.each {|ac| ac.configuration = configuration }
    end
  end

  # Compute a url for a given asset type (:image or :font)
  def compute_url(type, relative_path, relative_to_css_url = nil, use_cache_buster = true)
    # pass through fully specified urls
    return relative_path if relative_path.start_with?("http://")

    clean_relative_path, query, target = clean_path(relative_path)

    # Find the asset collection that includes this asset
    asset_collection, clean_relative_path, real_path = find_collection(type, clean_relative_path)

    # Didn't find the asset, but it's a full url so just return it.
    return relative_path if asset_collection.nil? && relative_path.start_with?("/")

    # Raise an error for relative paths if we didn't find it on the search path.
    raise AssetNotFound.new(type, relative_path, @asset_collections) unless asset_collection

    # Make a root-relative url (starting with /)
    asset_url = url_join(asset_collection.send(:"http_#{type}s_path"), clean_relative_path)

    # Compute asset cache buster
    busted_path, busted_query = cache_buster(asset_collection, asset_url, real_path) if use_cache_buster
    asset_url = busted_path if busted_path
    query = [query, busted_query].compact.join("&") if busted_query

    # Convert path to a relative url if a css file is specified.
    relative_url = compute_relative_path(relative_to_css_url, asset_url) if relative_to_css_url

    # Compute asset host when not relative and one is provided
    asset_host = if asset_collection.asset_host && relative_url.nil?
                   asset_collection.asset_host.call(asset_url) 
                 end

    # build the full url
    url = url_join(asset_host || "", relative_url || asset_url)
    url << "?" if query
    url << query if query
    url << "#" if target
    url << target if target
    return url
  end

  # Find the real path to a relative asset path
  def find_asset(type, relative_path)
    clean_relative_path, _, _ = clean_path(relative_path)
    _, _, real_path = find_collection(type, clean_relative_path)
    return real_path
  end

  def relative_path(type, absolute_path)
    absolute_path = File.expand_path(absolute_path)
    @asset_collections.find do |ac|
      asset_path = File.expand_path(ac.send(:"#{type}s_path"))
      if absolute_path.start_with?(asset_path+File::SEPARATOR)
        return absolute_path[(asset_path+File::SEPARATOR).size..-1]
      end
    end
    nil
  end

  def glob(type, glob_expression, options = {})
    match_all = options.fetch(:match_all, false)
    resolved_files = []
    @asset_collections.each do |ac|
      _, some_resolved_files = ac.globs?(type, glob_expression)
      resolved_files += some_resolved_files if some_resolved_files
      return resolved_files if resolved_files.any? and !match_all
    end
    resolved_files
  end

  protected

  def clean_path(relative_path)
    # If the image has an target reference, remove it (Common with SVG)
    clean_relative_path, target = relative_path.split("#", 2)

    # If the image has a query, remove it
    clean_relative_path, query = clean_relative_path.split("?", 2)

    # Get rid of silliness in the url
    clean_relative_path = expand_url_path(clean_relative_path)

    [clean_relative_path, query, target]
  end

  def find_collection(type, relative_path)
    asset_collection = nil
    clean_relative_path = nil
    absolute_path = nil
    @asset_collections.each do |ac|
      cp, ap = ac.send(:"includes_#{type}?", relative_path)
      if ap 
        asset_collection = ac
        absolute_path = ap
        clean_relative_path = cp
        break
      end
    end
    [asset_collection, clean_relative_path, absolute_path]
  end

  def absolute_path?(path)
    path[0..0] == "/" || path[0..3] == "http"
  end

  def cache_buster(asset_collection, path, real_path)
    cache_buster = compute_cache_buster(asset_collection, path, real_path)
    return [path, nil] if cache_buster.nil?
    cache_buster = {:query => cache_buster} if cache_buster.is_a?(String)
    [cache_buster[:path] || path, cache_buster[:query]]
  end

  def compute_cache_buster(asset_collection, path, real_path)
    file = nil
    if asset_collection.asset_cache_buster == :none
      nil
    elsif asset_collection.asset_cache_buster
      args = [path]
      if asset_collection.asset_cache_buster.arity > 1
        file = File.new(real_path)
        args << file
      end
      asset_collection.asset_cache_buster.call(*args)
    else
      default_cache_buster(path, real_path)
    end
  ensure
    file.close if file
  end

  def default_cache_buster(path, real_path)
    if File.readable?(real_path)
      File.mtime(real_path).to_i.to_s
    else
      Compass::Util.compass_warn("WARNING: '#{real_path}' cannot be read.")
      nil
    end
  end
end