Class: Jammit::Compressor

Inherits:
Object
  • Object
show all
Defined in:
lib/jammit/compressor.rb

Overview

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_assets” is turned on, creates “mhtml” and “datauri” versions of all stylesheets, with all enabled assets inlined into the css.

Constant Summary

EMBED_MIME_TYPES =

Mapping from extension to mime-type of all embeddable assets.

{
  '.png'  => 'image/png',
  '.jpg'  => 'image/jpeg',
  '.jpeg' => 'image/jpeg',
  '.gif'  => 'image/gif',
  '.tif'  => 'image/tiff',
  '.tiff' => 'image/tiff',
  '.ttf'  => 'application/x-font-ttf',
  '.otf'  => 'font/opentype',
  '.woff' => 'application/x-font-woff'
}
EMBED_EXTS =

Font extensions for which we allow embedding:

EMBED_MIME_TYPES.keys
EMBED_FONTS =
['.ttf', '.otf', '.woff']
MAX_IMAGE_SIZE =

(32k – padding) maximum length for data-uri assets (an IE8 limitation).

32700
EMBED_DETECTOR =

CSS asset-embedding regexes for URL rewriting.

/url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
EMBEDDABLE =
/[\A\/]embed\//
EMBED_REPLACER =
/url\(__EMBED__(.+?)(\?\d+)?\)/
MHTML_START =

MHTML file constants.

"/*\r\nContent-Type: multipart/related; boundary=\"MHTML_MARK\"\r\n\r\n"
MHTML_SEPARATOR =
"--MHTML_MARK\r\n"
MHTML_END =
"\r\n--MHTML_MARK--\r\n*/\r\n"
JST_START =

JST file constants.

"(function(){"
JST_END =
"})();"
JAVASCRIPT_COMPRESSORS =
{
  :jsmin    => Jammit.javascript_compressors.include?(:jsmin)  ? Jammit::JsminCompressor : nil,
  :yui      => Jammit.javascript_compressors.include?(:yui)  ? YUI::JavaScriptCompressor : nil,
  :closure  => Jammit.javascript_compressors.include?(:closure)  ? Closure::Compiler : nil,
  :uglifier => Jammit.javascript_compressors.include?(:uglifier) ? Jammit::Uglifier  : nil
}
CSS_COMPRESSORS =
{
 :cssmin   => Jammit.css_compressors.include?(:cssmin) ? Jammit::CssminCompressor : nil,
 :yui      => Jammit.css_compressors.include?(:yui) ? YUI::CssCompressor : nil,
 :sass     => Jammit.css_compressors.include?(:sass) ? Jammit::SassCompressor : nil
}
JAVASCRIPT_DEFAULT_OPTIONS =
{
  :jsmin    => {},
  :yui      => {:munge => true},
  :closure  => {},
  :uglifier => {:copyright => false}
}

Instance Method Summary (collapse)

Constructor Details

- (Compressor) initialize

CSS compression can be provided with YUI Compressor or sass. JS compression can be provided with YUI Compressor, Google Closure Compiler or UglifyJS.



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/jammit/compressor.rb', line 67

def initialize
  if Jammit.javascript_compressors.include?(:yui) || Jammit.javascript_compressors.include?(:closure) || Jammit.css_compressors.include?(:yui)
    Jammit.check_java_version
  end

  css_flavor      = Jammit.css_compressor || Jammit::DEFAULT_CSS_COMPRESSOR
  @css_compressor = CSS_COMPRESSORS[css_flavor].new(Jammit.css_compressor_options || {})
  js_flavor       = Jammit.javascript_compressor || Jammit::DEFAULT_JAVASCRIPT_COMPRESSOR
  @options        = JAVASCRIPT_DEFAULT_OPTIONS[js_flavor].merge(Jammit.compressor_options || {})
  @js_compressor  = JAVASCRIPT_COMPRESSORS[js_flavor].new(@options)
end

Instance Method Details

- (Object) absolute_path(asset_pathname, css_pathname) (private)

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.



201
202
203
204
205
# File 'lib/jammit/compressor.rb', line 201

def absolute_path(asset_pathname, css_pathname)
  (asset_pathname.absolute? ?
    Pathname.new(File.join(Jammit.public_root, asset_pathname)) :
    css_pathname.dirname + asset_pathname).cleanpath
end

- (Object) compile_jst(paths)

Compiles a single JST file by writing out a javascript that adds 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.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/jammit/compressor.rb', line 110

def compile_jst(paths)
  namespace   = Jammit.template_namespace
  paths       = paths.grep(Jammit.template_extension_matcher).sort
  base_path   = find_base_path(paths)
  compiled    = paths.map do |path|
    contents  = read_binary_file(path)
    contents  = contents.gsub(/\r?\n/, "\\n").gsub("'", '\\\\\'')
    name      = template_name(path, base_path)
    "#{namespace}['#{name}'] = #{Jammit.template_function}('#{contents}');"
  end
  compiler = Jammit.include_jst_script ? read_binary_file(DEFAULT_JST_SCRIPT) : '';
  setup_namespace = "#{namespace} = #{namespace} || {};"
  [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
end

- (Object) compress_css(paths, variant = nil, asset_url = nil)

Concatenate and compress a list of CSS stylesheets. When compressing a :datauri or :mhtml variant, post-processes the result to embed referenced assets.



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/jammit/compressor.rb', line 93

def compress_css(paths, variant=nil, asset_url=nil)
  @asset_contents = {}
  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

- (Object) compress_js(paths)

Concatenate together a list of JavaScript paths, and pass them through the YUI Compressor (with munging enabled). JST can optionally be included.



81
82
83
84
85
86
87
88
# File 'lib/jammit/compressor.rb', line 81

def compress_js(paths)
  if (jst_paths = paths.grep(Jammit.template_extension_matcher)).empty?
    js = concatenate(paths)
  else
    js = concatenate(paths - jst_paths) + compile_jst(jst_paths)
  end
  Jammit.compress_assets ? @js_compressor.compress(js) : js
end

- (Object) concatenate(paths) (private)

Concatenate together a list of asset files.



256
257
258
# File 'lib/jammit/compressor.rb', line 256

def concatenate(paths)
  [paths].flatten.map {|p| read_binary_file(p) }.join("\n")
end

- (Object) concatenate_and_tag_assets(paths, variant = nil) (private)

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 assets while we’re at it.



153
154
155
156
157
158
159
160
161
162
163
# File 'lib/jammit/compressor.rb', line 153

def concatenate_and_tag_assets(paths, variant=nil)
  stylesheets = [paths].flatten.map do |css_path|
    contents = read_binary_file(css_path)
    contents.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(#{construct_asset_path(ipath, cpath, variant)})"
    end
  end
  stylesheets.join("\n")
end

- (Object) construct_asset_path(asset_path, css_path, variant) (private)

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.



192
193
194
195
196
197
# File 'lib/jammit/compressor.rb', line 192

def construct_asset_path(asset_path, css_path, variant)
  public_path = absolute_path(asset_path, css_path)
  return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
  source = asset_path.absolute? || ! Jammit.rewrite_relative_paths ? asset_path.to_s : relative_path(public_path)
  rewrite_asset_path(source, public_path)
end

- (Boolean) embeddable?(asset_path, variant) (private)

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 assets that large in any case. Because we need to check the base64 length here, save it so that we don’t have to compute it again later.

Returns:

  • (Boolean)


233
234
235
236
237
238
239
240
241
# File 'lib/jammit/compressor.rb', line 233

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 || encoded_contents(asset_path).length < MAX_IMAGE_SIZE
  return false if font && variant == :mhtml
  return true
end

- (Object) encoded_contents(asset_path) (private)

Return the Base64-encoded contents of an asset on a single line.



244
245
246
247
248
# File 'lib/jammit/compressor.rb', line 244

def encoded_contents(asset_path)
  return @asset_contents[asset_path] if @asset_contents[asset_path]
  data = read_binary_file(asset_path)
  @asset_contents[asset_path] = Base64.encode64(data).gsub(/\n/, '')
end

- (Object) find_base_path(paths) (private)

Given a set of paths, find a common prefix path.



129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/jammit/compressor.rb', line 129

def find_base_path(paths)
  return nil if paths.length <= 1
  paths.sort!
  first = paths.first.split('/')
  last  = paths.last.split('/')
  i = 0
  while first[i] == last[i] && i <= first.length
    i += 1
  end
  res = first.slice(0, i).join('/')
  res.empty? ? nil : res
end

- (Object) mime_type(asset_path) (private)

Grab the mime-type of an asset, by filename.



251
252
253
# File 'lib/jammit/compressor.rb', line 251

def mime_type(asset_path)
  EMBED_MIME_TYPES[File.extname(asset_path)]
end

- (Object) rails_asset_id(path) (private)

Similar to the AssetTagHelper’s method of the same name, this will determine the correct asset id for a file.



222
223
224
225
226
# File 'lib/jammit/compressor.rb', line 222

def rails_asset_id(path)
  asset_id = ENV["RAILS_ASSET_ID"]
  return asset_id if asset_id
  File.exists?(path) ? File.mtime(path).to_i.to_s : ''
end

- (Object) read_binary_file(path) (private)

`File.read`, but in “binary” mode.



261
262
263
# File 'lib/jammit/compressor.rb', line 261

def read_binary_file(path)
  File.open(path, 'rb:UTF-8') {|f| f.read }
end

- (Object) relative_path(absolute_path) (private)

CSS assets that are referenced by relative paths, and are not being embedded, must be rewritten relative to the newly-merged stylesheet path.



209
210
211
# File 'lib/jammit/compressor.rb', line 209

def relative_path(absolute_path)
  File.join('../', absolute_path.sub(Jammit.public_root, ''))
end

- (Object) rewrite_asset_path(path, file_path) (private)

Similar to the AssetTagHelper’s method of the same name, this will append the RAILS_ASSET_ID cache-buster to URLs, if it’s defined.



215
216
217
218
# File 'lib/jammit/compressor.rb', line 215

def rewrite_asset_path(path, file_path)
  asset_id = rails_asset_id(file_path)
  (!asset_id || asset_id == '') ? path : "#{path}?#{asset_id}"
end

- (Object) template_name(path, base_path) (private)

Determine the name of a JS template. If there’s a common base path, use the namespaced prefix. Otherwise, simply use the filename.



144
145
146
147
# File 'lib/jammit/compressor.rb', line 144

def template_name(path, base_path)
  return File.basename(path, ".#{Jammit.template_extension}") unless base_path
  path.gsub(/\A#{Regexp.escape(base_path)}\/(.*)\.#{Jammit.template_extension}\Z/, '\1')
end

- (Object) with_data_uris(css) (private)

Re-write all enabled asset URLs in a stylesheet with their corresponding Data-URI Base-64 encoded asset contents.



167
168
169
170
171
# File 'lib/jammit/compressor.rb', line 167

def with_data_uris(css)
  css.gsub(EMBED_REPLACER) do |url|
    "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
  end
end

- (Object) with_mhtml(css, asset_url) (private)

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.



176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/jammit/compressor.rb', line 176

def with_mhtml(css, asset_url)
  paths, index = {}, 0
  css = css.gsub(EMBED_REPLACER) do |url|
    i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
    "url(mhtml:#{asset_url}!#{i})"
  end
  mhtml = paths.sort.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