# -*- coding: utf-8 -*-
require "pathname"
require "juicer/cache_buster"
module Juicer
module Asset
#
# Assets are files used by CSS and JavaScript files. The Asset class provides
# tools for manipulating asset paths, such as rebasing, adding cache busters,
# and cycling asset hosts.
#
# Asset::Path objects are most commonly created by Juicer::Asset::PathResolver#resolve
# which resolves include paths to file names. It is possible, however, to use
# the asset class directly:
#
# Dir.pwd
# #=> "/home/christian/projects/mysite/design/css"
#
# asset = Juicer::Asset::Path.new "../images/logo.png"
# asset.path
# #=> "../images/logo.png"
#
# asset.rebase("~/projects/mysite/design").path
# #=> "images/logo.png"
#
# asset.filename
# #=> "/home/christian/projects/mysite/design/images/logo.png"
#
# asset.path(:cache_buster_type => :soft)
# #=> "../images/logo.png?jcb=1234567890"
#
# asset.path(:cache_buster_type => :soft, :cache_buster => nil)
# #=> "../images/logo.png?1234567890"
#
# asset.path(:cache_buster => "bustIT")
# #=> "../images/logo.png?bustIT=1234567890"
#
# asset = Juicer::Asset::Path.new "../images/logo.png", :document_root
# #=> "/home/christian/projects/mysite"
#
# asset.absolute_path(:cache_buster_type => :hard)
# #=> "/images/logo-jcb1234567890.png"
#
# asset.absolute_path(:host => "http://localhost")
# #=> "http://localhost/images/logo.png"
#
# asset.absolute_path(:host => "http://localhost", :cache_buster_type => :hard)
# #=> "http://localhost/images/logo-jcb1234567890.png"
#
#
# Author:: Christian Johansen (christian@cjohansen.no)
# Copyright:: Copyright (c) 2009 Christian Johansen
# License:: BSD
#
class Path
# Base directory to resolve relative path from, see Juicer::Asset::Path#initialize
attr_reader :base
# Hosts served from :document_root, see Juicer::Asset::Path#initialize
attr_reader :hosts
# Directory served as root through a web server, see Juicer::Asset::Path#initialize
attr_reader :document_root
@@scheme_pattern = %r{^([a-zA-Z]{3,5}:)?//}
#
# Initialize asset at path. Accepts an optional hash of options:
#
# [:base]
# Base context from which asset is required. Given a path of
# ../images/logo.png and a :base of /project/design/css,
# the asset file will be assumed to live in /project/design/images/logo.png
# Defaults to the current directory.
# [:hosts]
# Array of host names that are served from :document_root. May also
# include scheme/protocol. If not, http is assumed.
# [:document_root]
# The root directory for absolute URLs (ie, the server's document root). This
# option is needed when resolving absolute URLs that include a hostname as well
# as when generating absolute paths.
#
def initialize(path, options = {})
@path = path
@filename = nil
@absolute_path = nil
@relative_path = nil
@path_has_host = @path =~ @@scheme_pattern
@path_is_absolute = @path_has_host || @path =~ /^\//
# Options
@base = options[:base] || Dir.pwd
@document_root = options[:document_root]
@hosts = Juicer::Asset::Path.hosts_with_scheme(options[:hosts])
end
#
# Returns absolute path calculated using the #document_root.
# Optionally accepts a hash of options:
#
# [:host] Return fully qualified URL with this host name. May include
# scheme/protocol. Default scheme is http.
# [:cache_buster] The parameter name for the cache buster.
# [:cache_buster_type] The kind of cache buster to add, :soft
# or :hard.
#
# A cache buster will be added if either (or both) of the :cache_buster
# or :cache_buster_type options are provided. The default cache buster
# type is :soft.
#
# Raises an ArgumentException if no document_root has been set.
#
def absolute_path(options = {})
if !@absolute_path
# Pre-conditions
raise ArgumentError.new("No document root set") if @document_root.nil?
@absolute_path = filename.sub(%r{^#@document_root}, '').sub(/^\/?/, '/')
@absolute_path = "#{Juicer::Asset::Path.host_with_scheme(options[:host])}#@absolute_path"
end
path_with_cache_buster(@absolute_path, options)
end
#
# Return path relative to #base
#
# Accepts an optional hash of options for cache busters:
#
# [:cache_buster] The parameter name for the cache buster.
# [:cache_buster_type] The kind of cache buster to add, :soft
# or :hard.
#
# A cache buster will be added if either (or both) of the :cache_buster
# or :cache_buster_type options are provided. The default cache buster
# type is :soft.
#
def relative_path(options = {})
@relative_path ||= Pathname.new(filename).relative_path_from(Pathname.new(base)).to_s
path_with_cache_buster(@relative_path, options)
end
#
# Returns the original path.
#
# Accepts an optional hash of options for cache busters:
#
# [:cache_buster] The parameter name for the cache buster.
# [:cache_buster_type] The kind of cache buster to add, :soft
# or :hard.
#
# A cache buster will be added if either (or both) of the :cache_buster
# or :cache_buster_type options are provided. The default cache buster
# type is :soft.
#
def path(options = {})
path_with_cache_buster(@path, options)
end
#
# Return filename on disk. Requires the #document_root to be set if
# original path was an absolute one.
#
# If asset path includes scheme/protocol and host, it can only be resolved if
# a match is found in #hosts. Otherwise, an exeception is raised.
#
def filename
return @filename if @filename
# Pre-conditions
raise ArgumentError.new("No document root set") if @path_is_absolute && @document_root.nil?
raise ArgumentError.new("No hosts served from document root") if @path_has_host && @hosts.empty?
path = strip_host(@path)
raise ArgumentError.new("No matching host found for #{@path}") if path =~ @@scheme_pattern
dir = @path_is_absolute ? document_root : base
@filename = File.expand_path(File.join(dir, path))
end
#
# Rebase path and return a new Asset::Path object.
#
# asset = Juicer::Asset::Path.new "../images/logo.png", :base => "/var/www/public/stylesheets"
# asset2 = asset.rebase("/var/www/public")
# asset2.relative_path #=> "images/logo.png"
#
def rebase(base_path)
path = Pathname.new(filename).relative_path_from(Pathname.new(base_path)).to_s
Juicer::Asset::Path.new(path,
:base => base_path,
:hosts => hosts,
:document_root => document_root)
end
#
# Returns basename of filename on disk
#
def basename
File.basename(filename)
end
#
# Returns basename of filename on disk
#
def dirname
File.dirname(filename)
end
#
# Returns true if file exists on disk
#
def exists?
File.exists?(filename)
end
#
# Accepts a single host, or an array of hosts and returns an array of hosts
# that include scheme/protocol, and don't have trailing slash.
#
def self.hosts_with_scheme(hosts)
hosts.nil? ? [] : [hosts].flatten.collect { |host| self.host_with_scheme(host) }
end
#
# Assures that a host has scheme/protocol and no trailing slash
#
def self.host_with_scheme(host)
return host if host.nil?
(host !~ @@scheme_pattern ? "http://#{host}" : host).sub(/\/$/, '')
end
def <=>(other)
filename <=> other.filename
end
private
#
# Adds cache buster to paths if :cache_buster_type and :cache_buster indicates
# they should be added.
#
def path_with_cache_buster(path, options = {})
return path if !options.key?(:cache_buster) && options[:cache_buster_type].nil?
type = options[:cache_buster_type] || :soft
buster_options = {:revision_type => options[:cache_buster_format]}
buster_options[:parameter] = options[:cache_buster] if options.key?(:cache_buster)
buster_path = Juicer::CacheBuster.send(type, filename, buster_options)
path.sub(File.basename(path), File.basename(buster_path))
end
#
# Strip known hosts from path
#
def strip_host(path)
hosts.each do |host|
return path if path !~ @@scheme_pattern
path = path.sub(%r{^#{host}}, '')
end
return path
end
end
end
end