require 'cssmin'
module AssetHat
# Methods for minifying and optimizing CSS.
module CSS
# A list of supported minification
# engine names.
ENGINES = [:weak, :cssmin]
# Returns the expected path for the minified version of a CSS asset:
#
# AssetHat::CSS.min_filepath('public/stylesheets/bundles/application.css')
# # => 'public/stylesheets/bundles/application.min.css'
def self.min_filepath(filepath)
AssetHat.min_filepath(filepath, 'css')
end
# Accepts a string of CSS, and returns that CSS minified. Options:
#
# [engine] Default is :cssmin
; see
# Engines.cssmin.
# Allowed values are in ENGINES.
def self.minify(input_string, options={})
options.reverse_merge!(:engine => :cssmin)
engine = options[:engine].to_sym
unless ENGINES.include?(engine)
raise %{
Unknown CSS minification engine '#{engine}'.
Allowed: #{ENGINES.map{ |e| "'#{e}'" }.join(', ')}
}.strip.gsub(/\s+/, ' ') and return
end
AssetHat::CSS::Engines.send(engine, input_string).strip
end
# Given a string containing CSS, appends each referenced asset's last
# commit ID to its URL, e.g.,
# background: url(/images/foo.png?ab12cd3)
. This enables
# cache busting: If the user's browser has cached a copy of foo.png from a
# previous deployment, this new URL forces the browser to ignore that
# cache and request the latest version.
def self.add_asset_commit_ids(css)
update_css_urls(css, %w[images htc]) do |src, quote|
# Get absolute path
filepath = File.join(ASSETS_DIR, src)
# Convert to relative path
filepath.sub!(/^#{FileUtils.pwd}#{File::SEPARATOR}/, '')
commit_id = AssetHat.last_commit_id(filepath)
if commit_id.present?
"url(#{quote}#{src}#{src =~ /\?/ ? '&' : '?'}#{commit_id}#{quote})"
else
"url(#{quote}#{src}#{quote})"
end
end
end
# Arguments:
#
# - A string containing CSS;
# - A string containing the app's asset host, e.g.,
# 'http\://cdn%d.example.com'. This value is typically taken from
# config.action_controller.asset_host
in
# the app's config/environments/production.rb
.
#
# An asset host is added to every image URL in the CSS, e.g.,
# background: url(http\://cdn2.example.com/images/foo.png)
;
# if %d
in the asset host, it is replaced with an arbitrary
# number in 0-3, inclusive.
#
# Options:
#
# [ssl] Set to true
to simulate a request via SSL. Defaults
# to false
.
def self.add_asset_hosts(css, asset_host, options={})
return css if asset_host.blank?
options.reverse_merge!(:ssl => false)
update_css_urls(css, %w[images]) do |src, quote|
computed_asset_host = AssetHat.compute_asset_host(
asset_host, src, options.slice(:ssl))
"url(#{quote}#{computed_asset_host}#{src}#{quote})"
end
end
# Swappable CSS minification engines. Each accepts and returns a string.
module Engines
# Barebones CSS minification engine that only strips whitespace from the
# start and end of every line, including linebreaks. For safety, doesn't
# attempt to parse the CSS itself.
def self.weak(input_string)
input = StringIO.new(input_string)
output = StringIO.new
input.each do |line|
# Remove indentation and trailing whitespace (including line breaks)
line.strip!
next if line.blank?
output.write line
end
output.rewind
output.read
end
# CSS minification engine that mostly uses the CSSMin gem, a Ruby port
# of Lecomte's YUI Compressor and Schlueter's PHP cssmin.
#
# Sources:
# - http://github.com/rgrove/cssmin
# - http://rubygems.org/gems/cssmin
def self.cssmin(input_string)
output = CSSMin.minify(input_string)
# Remove rules that have empty declaration blocks
output.gsub!(/\}([^\}]+\{;\}){1,}/, '}')
output
end
end
private
# Strips any balanced quotation marks from `src`. Returns `src` and an
# instance of the quotation mark as two separate strings.
def self.separate_src_and_quotes(src)
quote = src[0, 1]
if %w[' "].include?(quote) && quote == src[-1, 1]
src = src[1, src.length - 2] # Strip quotes
else
quote = nil # No quotes in original CSS
end
[src, quote]
end
def self.update_css_urls(css, dirs, &css_block)
new_css = css.dup
dirs = dirs.join('|')
gsub_block = lambda do |match|
src, quote = separate_src_and_quotes($1)
css_block.call(src, quote)
end
# Match without quotes
new_css.gsub!(/url[\s]*\((\/(#{dirs})\/[^)'"]+)\)/, &gsub_block)
# Match with single/double quotes
new_css.gsub!(/url[\s]*\(((['"])\/(#{dirs})\/[^)]+\2)\)/, &gsub_block)
new_css
end
end
end