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