lib/jammit/compressor.rb in jammit-0.3.3 vs lib/jammit/compressor.rb in jammit-0.4.0
- old
+ new
@@ -1,36 +1,45 @@
module Jammit
# Uses the YUI Compressor or Closure Compiler to compress JavaScript.
# Always uses YUI to compress CSS (Which means that Java must be installed.)
# Also knows how to create a concatenated JST file.
- # If "embed_images" is turned on, creates "mhtml" and "datauri" versions of
- # all stylesheets, with all enabled images inlined into the css.
+ # If "embed_assets" is turned on, creates "mhtml" and "datauri" versions of
+ # all stylesheets, with all enabled assets inlined into the css.
class Compressor
- # Mapping from extension to mime-type of all embeddable images.
- IMAGE_MIME_TYPES = {
+ # Mapping from extension to mime-type of all embeddable assets.
+ EMBED_MIME_TYPES = {
'.png' => 'image/png',
'.jpg' => 'image/jpeg',
'.jpeg' => 'image/jpeg',
'.gif' => 'image/gif',
'.tif' => 'image/tiff',
- '.tiff' => 'image/tiff'
+ '.tiff' => 'image/tiff',
+ '.ttf' => 'font/truetype',
+ '.otf' => 'font/opentype'
}
+ # Font extensions for which we allow embedding:
+ EMBED_EXTS = EMBED_MIME_TYPES.keys
+ EMBED_FONTS = ['.ttf', '.otf']
- IMAGE_DETECTOR = /url\(['"]?([^\s)]+\.(png|jpg|jpeg|gif|tif|tiff))['"]?\)/
- IMAGE_EMBED = /[\A\/]embed\//
- IMAGE_REPLACER = /url\(__EMBED__([^\s)]+)\)/
+ # Maximum size for embeddable images (an IE8 limitation).
+ MAX_IMAGE_SIZE = 32.kilobytes
+ # CSS asset-embedding regexes for URL rewriting.
+ EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
+ EMBEDDABLE = /[\A\/]embed\//
+ EMBED_REPLACER = /url\(__EMBED__([^\s)]+)(\?\d+)?\)/
+
# MHTML file constants.
MHTML_START = "/*\r\nContent-Type: multipart/related; boundary=\"JAMMIT_MHTML_SEPARATOR\"\r\n\r\n"
MHTML_SEPARATOR = "--JAMMIT_MHTML_SEPARATOR\r\n"
MHTML_END = "*/\r\n"
# JST file constants.
- JST_START = "(function(){window.JST = window.JST || {};"
+ JST_START = "(function(){"
JST_END = "})();"
COMPRESSORS = {
:yui => YUI::JavaScriptCompressor,
:closure => Closure::Compiler
@@ -43,132 +52,144 @@
# Creating a compressor initializes the internal YUI Compressor from
# the "yui-compressor" gem, or the internal Closure Compiler from the
# "closure-compiler" gem.
def initialize
- @css_compressor = YUI::CssCompressor.new(Jammit.css_compressor_options)
- flavor = Jammit.javascript_compressor
- @options = DEFAULT_OPTIONS[flavor].merge(Jammit.compressor_options)
+ @css_compressor = YUI::CssCompressor.new(Jammit.css_compressor_options || {})
+ flavor = Jammit.javascript_compressor || Jammit::DEFAULT_COMPRESSOR
+ @options = DEFAULT_OPTIONS[flavor].merge(Jammit.compressor_options || {})
@js_compressor = COMPRESSORS[flavor].new(@options)
end
# Concatenate together a list of JavaScript paths, and pass them through the
- # YUI Compressor (with munging enabled).
+ # YUI Compressor (with munging enabled). JST can optionally be included.
def compress_js(paths)
- js = concatenate(paths)
+ if (jst_paths = paths.grep(JST_EXT)).empty?
+ js = concatenate(paths)
+ else
+ js = concatenate(paths - jst_paths) + compile_jst(jst_paths)
+ end
Jammit.compress_assets ? @js_compressor.compress(js) : js
end
# Concatenate and compress a list of CSS stylesheets. When compressing a
# :datauri or :mhtml variant, post-processes the result to embed
- # referenced images.
+ # referenced assets.
def compress_css(paths, variant=nil, asset_url=nil)
- css = concatenate_and_tag_images(paths, variant)
+ css = concatenate_and_tag_assets(paths, variant)
css = @css_compressor.compress(css) if Jammit.compress_assets
case variant
when nil then return css
when :datauri then return with_data_uris(css)
when :mhtml then return with_mhtml(css, asset_url)
else raise PackageNotFound, "\"#{variant}\" is not a valid stylesheet variant"
end
end
# Compiles a single JST file by writing out a javascript that adds
- # template properties to a top-level "window.JST" object. Adds a
+ # template properties to a top-level template namespace object. Adds a
# JST-compilation function to the top of the package, unless you've
# specified your own preferred function, or turned it off.
# JST templates are named with the basename of their file.
def compile_jst(paths)
- compiled = paths.map do |path|
+ namespace = Jammit.template_namespace
+ compiled = paths.grep(JST_EXT).map do |path|
template_name = File.basename(path, File.extname(path))
contents = File.read(path).gsub(/\n/, '').gsub("'", '\\\\\'')
- "window.JST.#{template_name} = #{Jammit.template_function}('#{contents}');"
+ "#{namespace}.#{template_name} = #{Jammit.template_function}('#{contents}');"
end
compiler = Jammit.include_jst_script ? File.read(DEFAULT_JST_SCRIPT) : '';
- [JST_START, compiler, compiled, JST_END].flatten.join("\n")
+ setup_namespace = "#{namespace} = #{namespace} || {};"
+ [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
end
private
- # In order to support embedded images from relative paths, we need to
+ # In order to support embedded assets from relative paths, we need to
# expand the paths before contatenating the CSS together and losing the
- # location of the original stylesheet path. Validate the images while we're
+ # location of the original stylesheet path. Validate the assets while we're
# at it.
- def concatenate_and_tag_images(paths, variant=nil)
+ def concatenate_and_tag_assets(paths, variant=nil)
stylesheets = [paths].flatten.map do |css_path|
- File.read(css_path).gsub(IMAGE_DETECTOR) do |url|
+ File.read(css_path).gsub(EMBED_DETECTOR) do |url|
ipath, cpath = Pathname.new($1), Pathname.new(File.expand_path(css_path))
is_url = URI.parse($1).absolute?
- is_url ? url : "url(#{rewrite_image_path(ipath, cpath, !!variant)})"
+ is_url ? url : "url(#{rewrite_asset_path(ipath, cpath, variant)})"
end
end
stylesheets.join("\n")
end
- # Re-write all enabled image URLs in a stylesheet with their corresponding
- # Data-URI Base-64 encoded image contents.
+ # Re-write all enabled asset URLs in a stylesheet with their corresponding
+ # Data-URI Base-64 encoded asset contents.
def with_data_uris(css)
- css.gsub(IMAGE_REPLACER) do |url|
- "url(\"data:#{mime_type($1)};base64,#{encoded_contents($1)}\")"
+ css.gsub(EMBED_REPLACER) do |url|
+ "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
end
end
- # Re-write all enabled image URLs in a stylesheet with the MHTML equivalent.
+ # Re-write all enabled asset URLs in a stylesheet with the MHTML equivalent.
# The newlines ("\r\n") in the following method are critical. Without them
# your MHTML will look identical, but won't work.
def with_mhtml(css, asset_url)
paths, index = {}, 0
- css = css.gsub(IMAGE_REPLACER) do |url|
+ css = css.gsub(EMBED_REPLACER) do |url|
i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
"url(mhtml:#{asset_url}!#{i})"
end
mhtml = paths.map do |path, identifier|
mime, contents = mime_type(path), encoded_contents(path)
[MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
end
[MHTML_START, mhtml, MHTML_END, css].flatten.join('')
end
- # Return a rewritten image URL for a new stylesheet -- the image should
+ # Return a rewritten asset URL for a new stylesheet -- the asset should
# be tagged for embedding if embeddable, and referenced at the correct level
# if relative.
- def rewrite_image_path(image_path, css_path, embed=false)
- public_path = absolute_path(image_path, css_path)
- return "__EMBED__#{public_path}" if embed && embeddable?(public_path)
- image_path.absolute? ? image_path.to_s : relative_path(public_path)
+ def rewrite_asset_path(asset_path, css_path, variant)
+ public_path = absolute_path(asset_path, css_path)
+ return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
+ asset_path.absolute? ? asset_path.to_s : relative_path(public_path)
end
- # Get the site-absolute public path for an image file path that may or may
+ # Get the site-absolute public path for an asset file path that may or may
# not be relative, given the path of the stylesheet that contains it.
- def absolute_path(image_pathname, css_pathname)
- (image_pathname.absolute? ?
- Pathname.new(File.join(PUBLIC_ROOT, image_pathname)) :
- css_pathname.dirname + image_pathname).cleanpath
+ def absolute_path(asset_pathname, css_pathname)
+ (asset_pathname.absolute? ?
+ Pathname.new(File.join(PUBLIC_ROOT, asset_pathname)) :
+ css_pathname.dirname + asset_pathname).cleanpath
end
- # CSS images that are referenced by relative paths, and are *not* being
+ # CSS assets that are referenced by relative paths, and are *not* being
# embedded, must be rewritten relative to the newly-merged stylesheet path.
def relative_path(absolute_path)
File.join('../', absolute_path.sub(PUBLIC_ROOT, ''))
end
- # An image is valid for embedding if it exists, is less than 32K, and is
+ # An asset is valid for embedding if it exists, is less than 32K, and is
# stored somewhere inside of a folder named "embed".
# IE does not support Data-URIs larger than 32K, and you probably shouldn't
- # be embedding images that large in any case.
- def embeddable?(image_path)
- image_path.to_s.match(IMAGE_EMBED) && image_path.exist? && image_path.size < 32.kilobytes
+ # be embedding assets that large in any case.
+ def embeddable?(asset_path, variant)
+ font = EMBED_FONTS.include?(asset_path.extname)
+ return false unless variant
+ return false unless asset_path.to_s.match(EMBEDDABLE) && asset_path.exist?
+ return false unless EMBED_EXTS.include?(asset_path.extname)
+ return false unless font || asset_path.size < MAX_IMAGE_SIZE
+ return false if font && variant == :mhtml
+ true
end
- # Return the Base64-encoded contents of an image on a single line.
- def encoded_contents(image_path)
- Base64.encode64(File.read(image_path)).gsub(/\n/, '')
+ # Return the Base64-encoded contents of an asset on a single line.
+ def encoded_contents(asset_path)
+ Base64.encode64(File.read(asset_path)).gsub(/\n/, '')
end
- # Grab the mime-type of an image, by filename.
- def mime_type(image_path)
- IMAGE_MIME_TYPES[File.extname(image_path)]
+ # Grab the mime-type of an asset, by filename.
+ def mime_type(asset_path)
+ EMBED_MIME_TYPES[File.extname(asset_path)]
end
# Concatenate together a list of asset files.
def concatenate(paths)
[paths].flatten.map {|p| File.read(p) }.join("\n")