forked from documentcloud/jammit
-
Notifications
You must be signed in to change notification settings - Fork 4
/
compressor.rb
253 lines (222 loc) · 10.2 KB
/
compressor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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_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 assets.
EMBED_MIME_TYPES = {
'.png' => 'image/png',
'.jpg' => 'image/jpeg',
'.jpeg' => 'image/jpeg',
'.gif' => 'image/gif',
'.tif' => 'image/tiff',
'.tiff' => 'image/tiff',
'.ttf' => 'font/truetype',
'.otf' => 'font/opentype',
'.woff' => 'font/woff'
}
# Font extensions for which we allow embedding:
EMBED_EXTS = EMBED_MIME_TYPES.keys
EMBED_FONTS = ['.ttf', '.otf', '.woff']
# (32k - padding) maximum length for data-uri assets (an IE8 limitation).
MAX_IMAGE_SIZE = 32700
# CSS asset-embedding regexes for URL rewriting.
EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
EMBEDDABLE = /[\A\/]embed\//
EMBED_REPLACER = /url\(__EMBED__(.+?)(\?\d+)?\)/
# MHTML file constants.
MHTML_START = "/*\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 file constants.
JST_START = "(function(){"
JST_END = "})();"
COMPRESSORS = {
:yui => YUI::JavaScriptCompressor,
:closure => Jammit.compressors.include?(:closure) ? Closure::Compiler : nil,
:uglifier => Jammit.compressors.include?(:uglifier) ? Jammit::Uglifier : nil
}
DEFAULT_OPTIONS = {
:yui => {:munge => true},
:closure => {},
:uglifier => {:copyright => false}
}
# The css compressor is always the YUI Compressor. JS compression can be
# provided with YUI Compressor, Google Closure Compiler or UglifyJS.
def initialize
Jammit.check_java_version
@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). JST can optionally be included.
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
# Concatenate and compress a list of CSS stylesheets. When compressing a
# :datauri or :mhtml variant, post-processes the result to embed
# referenced assets.
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
# 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.
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
private
# Given a set of paths, find a common prefix path.
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
# Determine the name of a JS template. If there's a common base path, use
# the namespaced prefix. Otherwise, simply use the filename.
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
# 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.
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
# 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(EMBED_REPLACER) do |url|
"url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
end
end
# 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(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
# 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 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? ? asset_path.to_s : relative_path(public_path)
rewrite_asset_path(source, public_path)
end
# 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(asset_pathname, css_pathname)
(asset_pathname.absolute? ?
Pathname.new(File.join(PUBLIC_ROOT, asset_pathname)) :
css_pathname.dirname + asset_pathname).cleanpath
end
# 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
# 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.
def rewrite_asset_path(path, file_path)
asset_id = rails_asset_id(file_path)
(!asset_id || asset_id == '') ? path : "#{path}?#{asset_id}"
end
# Similar to the AssetTagHelper's method of the same name, this will
# determine the correct asset id for a file.
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
# 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.
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
# Return the Base64-encoded contents of an asset on a single line.
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
# 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| read_binary_file(p) }.join("\n")
end
# `File.read`, but in "binary" mode.
def read_binary_file(path)
File.open(path, 'rb') {|f| f.read }
end
end
end