ee_jammit 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NmU3NDMxYmYxYTg0M2QzZDA0MTQ0ZGE1NmNjNTJiMzU2ZjA2ZjljMw==
5
+ data.tar.gz: !binary |-
6
+ YWE3MmU3ZDQyYmI5NmM5MzNkNjU2ZmFjMjU2Y2Q4NTIyNDZkZGQ0Zg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NDYyMTBmY2Y0OWYwYmIzNzUxZWViMDNmYTliZTI0YTRiZjNkNjk5MWE2ZTU2
10
+ MjNlOThhN2E2ZjZlMGQ5MWVmMzBmNmQ3NzE5M2ZlNjI2YzA0NzBmZGI5YTdj
11
+ MWFiNDAyMmNhNzFhOTdkNmQ0MWZjODliMTgyMGU5MTMzZjk1MWI=
12
+ data.tar.gz: !binary |-
13
+ YmE5NzA1M2E5N2ZhODc0MjVmZWU2NTk1ZDgwNDdkZThmMDRlNDdkODM4M2Zh
14
+ NGNjY2E4NTc4YTI5NGRmNWMzYWUwNGE1MmNkMWU4ZDliZmM5YTUxMmIzZTU4
15
+ NTU0YWJlY2RmOWVjMTA5NmU1NDkwMzY0MjE0YTBiNDY0ZjRiNTE=
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009-2011 Jeremy Ashkenas, DocumentCloud
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,24 @@
1
+ ==
2
+ _ _ __ __ __ __ ___ _____
3
+ _ | |/_\ | \/ | \/ |_ _|_ _|
4
+ | || / _ \| |\/| | |\/| || | | |
5
+ \__/_/ \_\_| |_|_| |_|___| |_|
6
+
7
+
8
+ Jammit is an industrial strength asset packaging library for Rails,
9
+ providing both the CSS and JavaScript concatenation and compression
10
+ that you'd expect, as well as ahead-of-time gzipping, built-in JavaScript
11
+ template support, and optional Data-URI / MHTML image embedding.
12
+
13
+ Installation:
14
+ gem install jammit
15
+
16
+ For documentation, usage, and examples, see:
17
+ http://documentcloud.github.com/jammit/
18
+
19
+ To suggest a feature or report a bug:
20
+ http://github.com/documentcloud/jammit/issues/
21
+
22
+ For internal source docs, see:
23
+ http://documentcloud.github.com/jammit/doc/
24
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby -rrubygems
2
+ require 'pathname'
3
+
4
+ APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath)
5
+ require File.join(APP_ROOT, '../lib/jammit/command_line.rb')
6
+
7
+ Jammit::CommandLine.new
@@ -0,0 +1,34 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'ee_jammit'
3
+ s.version = '0.6.6' # Keep version in sync with jammit.rb
4
+ s.date = '2011-11-30'
5
+
6
+ s.homepage = "http://documentcloud.github.com/jammit/"
7
+ s.summary = "Industrial Strength Asset Packaging for Rails"
8
+ s.description = <<-EOS
9
+ Enphase Fork to get the Rails 4 route compatibility
10
+ Jammit is an industrial strength asset packaging library for Rails,
11
+ providing both the CSS and JavaScript concatenation and compression that
12
+ you'd expect, as well as YUI Compressor and Closure Compiler compatibility,
13
+ ahead-of-time gzipping, built-in JavaScript template support, and optional
14
+ Data-URI / MHTML image embedding.
15
+ EOS
16
+
17
+ s.authors = ['Jeremy Ashkenas','Brenda Strech']
18
+ s.email = 'jeremy@documentcloud.org'
19
+ s.rubyforge_project = 'jammit'
20
+
21
+ s.require_paths = ['lib']
22
+ s.executables = ['jammit']
23
+
24
+ s.extra_rdoc_files = ['README']
25
+ s.rdoc_options << '--title' << 'Jammit' <<
26
+ '--exclude' << 'test' <<
27
+ '--main' << 'README' <<
28
+ '--all'
29
+
30
+ s.add_dependency 'cssmin', ['>= 1.0.2']
31
+ s.add_dependency 'jsmin', ['>= 1.0.1']
32
+
33
+ s.files = Dir['lib/**/*', 'bin/*', 'rails/*', 'jammit.gemspec', 'LICENSE', 'README']
34
+ end
@@ -0,0 +1,236 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__))
2
+
3
+ # @Jammit@ is the central namespace for all Jammit classes, and provides access
4
+ # to all of the configuration options.
5
+ module Jammit
6
+
7
+ VERSION = "0.6.6"
8
+
9
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
10
+
11
+ ASSET_ROOT = File.expand_path((defined?(Rails) && Rails.root.to_s.length > 0) ? Rails.root : ENV['RAILS_ROOT'] || ".") unless defined?(ASSET_ROOT)
12
+
13
+ DEFAULT_PUBLIC_ROOT = (defined?(Rails) && Rails.public_path.to_s.length > 0) ? Rails.public_path : File.join(ASSET_ROOT, 'public') unless defined?(PUBLIC_ROOT)
14
+
15
+ DEFAULT_CONFIG_PATH = File.join(ASSET_ROOT, 'config', 'assets.yml')
16
+
17
+ DEFAULT_PACKAGE_PATH = "assets"
18
+
19
+ DEFAULT_JST_SCRIPT = File.join(ROOT, 'lib/jammit/jst.js')
20
+
21
+ DEFAULT_JST_COMPILER = "template"
22
+
23
+ DEFAULT_JST_NAMESPACE = "window.JST"
24
+
25
+ JAVASCRIPT_COMPRESSORS = [:jsmin, :yui, :closure, :uglifier]
26
+
27
+ DEFAULT_JAVASCRIPT_COMPRESSOR = :jsmin
28
+
29
+ CSS_COMPRESSORS = [:cssmin, :yui, :sass]
30
+
31
+ DEFAULT_CSS_COMPRESSOR = :cssmin
32
+
33
+ # Extension matchers for JavaScript and JST, which need to be disambiguated.
34
+ JS_EXTENSION = /\.js\Z/
35
+ DEFAULT_JST_EXTENSION = "jst"
36
+
37
+ # Jammit raises a @PackageNotFound@ exception when a non-existent package is
38
+ # requested by a browser -- rendering a 404.
39
+ class PackageNotFound < NameError; end
40
+
41
+ # Jammit raises a MissingConfiguration exception when you try to load the
42
+ # configuration of an assets.yml file that doesn't exist, or are missing
43
+ # a piece of required configuration.
44
+ class MissingConfiguration < NameError; end
45
+
46
+ # Jammit raises an OutputNotWritable exception if the output directory for
47
+ # cached packages is locked.
48
+ class OutputNotWritable < StandardError; end
49
+
50
+ # Jammit raises a DeprecationError if you try to use an outdated feature.
51
+ class DeprecationError < StandardError; end
52
+
53
+ class << self
54
+ attr_reader :configuration, :template_function, :template_namespace,
55
+ :embed_assets, :package_assets, :compress_assets, :gzip_assets,
56
+ :package_path, :mhtml_enabled, :include_jst_script, :config_path,
57
+ :javascript_compressor, :compressor_options, :css_compressor,
58
+ :css_compressor_options, :template_extension,
59
+ :template_extension_matcher, :allow_debugging,
60
+ :rewrite_relative_paths, :public_root
61
+ attr_accessor :javascript_compressors, :css_compressors
62
+ end
63
+
64
+ # The minimal required configuration.
65
+ @configuration = {}
66
+ @public_root = DEFAULT_PUBLIC_ROOT
67
+ @package_path = DEFAULT_PACKAGE_PATH
68
+
69
+ @javascript_compressors = JAVASCRIPT_COMPRESSORS
70
+ @css_compressors = CSS_COMPRESSORS
71
+
72
+ # Load the complete asset configuration from the specified @config_path@.
73
+ # If we're loading softly, don't let missing configuration error out.
74
+ def self.load_configuration(config_path, soft=false)
75
+ exists = config_path && File.exists?(config_path)
76
+ return false if soft && !exists
77
+ raise MissingConfiguration, "could not find the \"#{config_path}\" configuration file" unless exists
78
+ conf = YAML.load(ERB.new(File.read(config_path)).result)
79
+
80
+ # Optionally overwrite configuration based on the environment.
81
+ rails_env = (defined?(Rails) ? ::Rails.env : ENV['RAILS_ENV'] || "development")
82
+ conf.merge! conf.delete rails_env if conf.has_key? rails_env
83
+
84
+ @config_path = config_path
85
+ @configuration = symbolize_keys(conf)
86
+ @package_path = conf[:package_path] || DEFAULT_PACKAGE_PATH
87
+ @embed_assets = conf[:embed_assets] || conf[:embed_images]
88
+ @compress_assets = !(conf[:compress_assets] == false)
89
+ @rewrite_relative_paths = !(conf[:rewrite_relative_paths] == false)
90
+ @gzip_assets = !(conf[:gzip_assets] == false)
91
+ @allow_debugging = !(conf[:allow_debugging] == false)
92
+ @mhtml_enabled = @embed_assets && @embed_assets != "datauri"
93
+ @compressor_options = symbolize_keys(conf[:compressor_options] || {})
94
+ @css_compressor_options = symbolize_keys(conf[:css_compressor_options] || {})
95
+ set_javascript_compressor(conf[:javascript_compressor])
96
+ set_css_compressor(conf[:css_compressor])
97
+ set_package_assets(conf[:package_assets])
98
+ set_template_function(conf[:template_function])
99
+ set_template_namespace(conf[:template_namespace])
100
+ set_template_extension(conf[:template_extension])
101
+ set_public_root(conf[:public_root]) if conf[:public_root]
102
+ symbolize_keys(conf[:stylesheets]) if conf[:stylesheets]
103
+ symbolize_keys(conf[:javascripts]) if conf[:javascripts]
104
+ check_for_deprecations
105
+ self
106
+ end
107
+
108
+ # Force a reload by resetting the Packager and reloading the configuration.
109
+ # In development, this will be called as a before_filter before every request.
110
+ def self.reload!
111
+ Thread.current[:jammit_packager] = nil
112
+ load_configuration(@config_path)
113
+ end
114
+
115
+ # Keep a global (thread-local) reference to a @Jammit::Packager@, to avoid
116
+ # recomputing asset lists unnecessarily.
117
+ def self.packager
118
+ Thread.current[:jammit_packager] ||= Packager.new
119
+ end
120
+
121
+ # Generate the base filename for a version of a given package.
122
+ def self.filename(package, extension, suffix=nil)
123
+ suffix_part = suffix ? "-#{suffix}" : ''
124
+ "#{package}#{suffix_part}.#{extension}"
125
+ end
126
+
127
+ # Generates the server-absolute URL to an asset package.
128
+ def self.asset_url(package, extension, suffix=nil, mtime=nil)
129
+ timestamp = mtime ? "?#{mtime.to_i}" : ''
130
+ "/#{package_path}/#{filename(package, extension, suffix)}#{timestamp}"
131
+ end
132
+
133
+ # Convenience method for packaging up Jammit, using the default options.
134
+ def self.package!(options={})
135
+ options = {
136
+ :config_path => Jammit::DEFAULT_CONFIG_PATH,
137
+ :output_folder => nil,
138
+ :base_url => nil,
139
+ :public_root => nil,
140
+ :force => false
141
+ }.merge(options)
142
+ load_configuration(options[:config_path])
143
+ set_public_root(options[:public_root]) if options[:public_root]
144
+ packager.force = options[:force]
145
+ packager.package_names = options[:package_names]
146
+ packager.precache_all(options[:output_folder], options[:base_url])
147
+ end
148
+
149
+ private
150
+
151
+ # Allows command-line definition of `PUBLIC_ROOT`, for those using Jammit
152
+ # outside of Rails.
153
+ def self.set_public_root(public_root=nil)
154
+ @public_root = public_root if public_root
155
+ end
156
+
157
+ # Ensure that the JavaScript compressor is a valid choice.
158
+ def self.set_javascript_compressor(value)
159
+ value = value && value.to_sym
160
+ @javascript_compressor = javascript_compressors.include?(value) ? value : DEFAULT_JAVASCRIPT_COMPRESSOR
161
+ end
162
+
163
+ # Ensure that the CSS compressor is a valid choice.
164
+ def self.set_css_compressor(value)
165
+ value = value && value.to_sym
166
+ @css_compressor = css_compressors.include?(value) ? value : DEFAULT_CSS_COMPRESSOR
167
+ end
168
+
169
+ # Turn asset packaging on or off, depending on configuration and environment.
170
+ def self.set_package_assets(value)
171
+ package_env = !defined?(Rails) || (!Rails.env.development? && !Rails.env.test?)
172
+ @package_assets = value == true || value.nil? ? package_env :
173
+ value == 'always' ? true : false
174
+ end
175
+
176
+ # Assign the JST template function, unless explicitly turned off.
177
+ def self.set_template_function(value)
178
+ @template_function = value == true || value.nil? ? DEFAULT_JST_COMPILER :
179
+ value == false ? '' : value
180
+ @include_jst_script = @template_function == DEFAULT_JST_COMPILER
181
+ end
182
+
183
+ # Set the root JS object in which to stash all compiled JST.
184
+ def self.set_template_namespace(value)
185
+ @template_namespace = value == true || value.nil? ? DEFAULT_JST_NAMESPACE : value.to_s
186
+ end
187
+
188
+ # Set the extension for JS templates.
189
+ def self.set_template_extension(value)
190
+ @template_extension = (value == true || value.nil? ? DEFAULT_JST_EXTENSION : value.to_s).gsub(/\A\.?(.*)\Z/, '\1')
191
+ @template_extension_matcher = /\.#{Regexp.escape(@template_extension)}\Z/
192
+ end
193
+
194
+ # The YUI Compressor requires Java > 1.4, and Closure requires Java > 1.6.
195
+ def self.check_java_version
196
+ return true if @checked_java_version
197
+ java = @compressor_options[:java] || 'java'
198
+ @css_compressor_options[:java] ||= java if @compressor_options[:java]
199
+ version = (`#{java} -version 2>&1`)[/\d+\.\d+/]
200
+ disable_compression if !version ||
201
+ (@javascript_compressor == :closure && version < '1.6') ||
202
+ (@javascript_compressor == :yui && version < '1.4')
203
+ @checked_java_version = true
204
+ end
205
+
206
+ # If we don't have a working Java VM, then disable asset compression and
207
+ # complain loudly.
208
+ def self.disable_compression
209
+ @compress_assets = false
210
+ warn("Asset compression disabled -- Java unavailable.")
211
+ end
212
+
213
+ # Jammit 0.5+ no longer supports separate template packages.
214
+ def self.check_for_deprecations
215
+ if @configuration[:templates]
216
+ raise DeprecationError, "Jammit 0.5+ no longer supports separate packages for templates.\nPlease fold your templates into the appropriate 'javascripts' package instead."
217
+ end
218
+ end
219
+
220
+ def self.warn(message)
221
+ message = "Jammit Warning: #{message}"
222
+ $stderr.puts message
223
+ end
224
+
225
+ # Clone of active_support's symbolize_keys, so that we don't have to depend
226
+ # on active_support in any fashion. Converts a hash's keys to all symbols.
227
+ def self.symbolize_keys(hash)
228
+ hash.keys.each do |key|
229
+ hash[(key.to_sym rescue key) || key] = hash.delete(key)
230
+ end
231
+ hash
232
+ end
233
+
234
+ end
235
+
236
+ require 'jammit/dependencies'
@@ -0,0 +1,84 @@
1
+ require 'optparse'
2
+ require File.expand_path(File.dirname(__FILE__) + '/../jammit')
3
+
4
+ module Jammit
5
+
6
+ # The @CommandLine@ is able to compress, pre-package, and pre-gzip all the
7
+ # assets specified in the configuration file, in order to avoid an initial
8
+ # round of slow requests after a fresh deployment.
9
+ class CommandLine
10
+
11
+ BANNER = <<-EOS
12
+
13
+ Usage: jammit OPTIONS
14
+
15
+ Run jammit inside a Rails application to compresses all JS, CSS,
16
+ and JST according to config/assets.yml, saving the packaged
17
+ files and corresponding gzipped versions.
18
+
19
+ If you're using "embed_assets", and you wish to precompile the
20
+ MHTML stylesheet variants, you must specify the "base-url".
21
+
22
+ Options:
23
+ EOS
24
+
25
+ # The @Jammit::CommandLine@ runs from the contents of @ARGV@.
26
+ def initialize
27
+ parse_options
28
+ ensure_configuration_file
29
+ Jammit.package!(@options)
30
+ end
31
+
32
+
33
+ private
34
+
35
+ # Make sure that we have a readable configuration file. The @jammit@
36
+ # command can't run without one.
37
+ def ensure_configuration_file
38
+ config = @options[:config_path]
39
+ return true if File.exists?(config) && File.readable?(config)
40
+ puts "Could not find the asset configuration file \"#{config}\""
41
+ exit(1)
42
+ end
43
+
44
+ # Uses @OptionParser@ to grab the options: *--output*, *--config*, and
45
+ # *--base-url*...
46
+ def parse_options
47
+ @options = {
48
+ :config_path => Jammit::DEFAULT_CONFIG_PATH,
49
+ :output_folder => nil,
50
+ :base_url => nil,
51
+ :force => false
52
+ }
53
+ @option_parser = OptionParser.new do |opts|
54
+ opts.on('-o', '--output PATH', 'output folder for packages (default: "public/assets")') do |output_folder|
55
+ @options[:output_folder] = output_folder
56
+ end
57
+ opts.on('-c', '--config PATH', 'path to assets.yml (default: "config/assets.yml")') do |config_path|
58
+ @options[:config_path] = config_path
59
+ end
60
+ opts.on('-u', '--base-url URL', 'base URL for MHTML (ex: "http://example.com")') do |base_url|
61
+ @options[:base_url] = base_url
62
+ end
63
+ opts.on('-f', '--force', 'force a rebuild of all assets') do |force|
64
+ @options[:force] = force
65
+ end
66
+ opts.on('-p', '--packages LIST', 'list of packages to build (ex: "core,ui", default: all)') do |package_names|
67
+ @options[:package_names] = package_names.split(/,\s*/).map {|n| n.to_sym }
68
+ end
69
+ opts.on('-P', '--public-root PATH', 'path to public assets (default: "public")') do |public_root|
70
+ puts "Option for PUBLIC_ROOT"
71
+ @options[:public_root] = public_root
72
+ end
73
+ opts.on_tail('-v', '--version', 'display Jammit version') do
74
+ puts "Jammit version #{Jammit::VERSION}"
75
+ exit
76
+ end
77
+ end
78
+ @option_parser.banner = BANNER
79
+ @option_parser.parse!(ARGV)
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,266 @@
1
+ module Jammit
2
+
3
+ # Uses the YUI Compressor or Closure Compiler to compress JavaScript.
4
+ # Always uses YUI to compress CSS (Which means that Java must be installed.)
5
+ # Also knows how to create a concatenated JST file.
6
+ # If "embed_assets" is turned on, creates "mhtml" and "datauri" versions of
7
+ # all stylesheets, with all enabled assets inlined into the css.
8
+ class Compressor
9
+
10
+ # Mapping from extension to mime-type of all embeddable assets.
11
+ EMBED_MIME_TYPES = {
12
+ '.png' => 'image/png',
13
+ '.jpg' => 'image/jpeg',
14
+ '.jpeg' => 'image/jpeg',
15
+ '.gif' => 'image/gif',
16
+ '.tif' => 'image/tiff',
17
+ '.tiff' => 'image/tiff',
18
+ '.ttf' => 'application/x-font-ttf',
19
+ '.otf' => 'font/opentype',
20
+ '.woff' => 'application/x-font-woff'
21
+ }
22
+
23
+ # Font extensions for which we allow embedding:
24
+ EMBED_EXTS = EMBED_MIME_TYPES.keys
25
+ EMBED_FONTS = ['.ttf', '.otf', '.woff']
26
+
27
+ # (32k - padding) maximum length for data-uri assets (an IE8 limitation).
28
+ MAX_IMAGE_SIZE = 32700
29
+
30
+ # CSS asset-embedding regexes for URL rewriting.
31
+ EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
32
+ EMBEDDABLE = /[\A\/]embed\//
33
+ EMBED_REPLACER = /url\(__EMBED__(.+?)(\?\d+)?\)/
34
+
35
+ # MHTML file constants.
36
+ MHTML_START = "/*\r\nContent-Type: multipart/related; boundary=\"MHTML_MARK\"\r\n\r\n"
37
+ MHTML_SEPARATOR = "--MHTML_MARK\r\n"
38
+ MHTML_END = "\r\n--MHTML_MARK--\r\n*/\r\n"
39
+
40
+ # JST file constants.
41
+ JST_START = "(function(){"
42
+ JST_END = "})();"
43
+
44
+ JAVASCRIPT_COMPRESSORS = {
45
+ :jsmin => Jammit.javascript_compressors.include?(:jsmin) ? Jammit::JsminCompressor : nil,
46
+ :yui => Jammit.javascript_compressors.include?(:yui) ? YUI::JavaScriptCompressor : nil,
47
+ :closure => Jammit.javascript_compressors.include?(:closure) ? Closure::Compiler : nil,
48
+ :uglifier => Jammit.javascript_compressors.include?(:uglifier) ? Jammit::Uglifier : nil
49
+ }
50
+
51
+ CSS_COMPRESSORS = {
52
+ :cssmin => Jammit.css_compressors.include?(:cssmin) ? Jammit::CssminCompressor : nil,
53
+ :yui => Jammit.css_compressors.include?(:yui) ? YUI::CssCompressor : nil,
54
+ :sass => Jammit.css_compressors.include?(:sass) ? Jammit::SassCompressor : nil
55
+ }
56
+
57
+ JAVASCRIPT_DEFAULT_OPTIONS = {
58
+ :jsmin => {},
59
+ :yui => {:munge => true},
60
+ :closure => {},
61
+ :uglifier => {:copyright => false}
62
+ }
63
+
64
+ # CSS compression can be provided with YUI Compressor or sass. JS
65
+ # compression can be provided with YUI Compressor, Google Closure
66
+ # Compiler or UglifyJS.
67
+ def initialize
68
+ if Jammit.javascript_compressors.include?(:yui) || Jammit.javascript_compressors.include?(:closure) || Jammit.css_compressors.include?(:yui)
69
+ Jammit.check_java_version
70
+ end
71
+
72
+ css_flavor = Jammit.css_compressor || Jammit::DEFAULT_CSS_COMPRESSOR
73
+ @css_compressor = CSS_COMPRESSORS[css_flavor].new(Jammit.css_compressor_options || {})
74
+ js_flavor = Jammit.javascript_compressor || Jammit::DEFAULT_JAVASCRIPT_COMPRESSOR
75
+ @options = JAVASCRIPT_DEFAULT_OPTIONS[js_flavor].merge(Jammit.compressor_options || {})
76
+ @js_compressor = JAVASCRIPT_COMPRESSORS[js_flavor].new(@options)
77
+ end
78
+
79
+ # Concatenate together a list of JavaScript paths, and pass them through the
80
+ # YUI Compressor (with munging enabled). JST can optionally be included.
81
+ def compress_js(paths)
82
+ if (jst_paths = paths.grep(Jammit.template_extension_matcher)).empty?
83
+ js = concatenate(paths)
84
+ else
85
+ js = concatenate(paths - jst_paths) + compile_jst(jst_paths)
86
+ end
87
+ Jammit.compress_assets ? @js_compressor.compress(js) : js
88
+ end
89
+
90
+ # Concatenate and compress a list of CSS stylesheets. When compressing a
91
+ # :datauri or :mhtml variant, post-processes the result to embed
92
+ # referenced assets.
93
+ def compress_css(paths, variant=nil, asset_url=nil)
94
+ @asset_contents = {}
95
+ css = concatenate_and_tag_assets(paths, variant)
96
+ css = @css_compressor.compress(css) if Jammit.compress_assets
97
+ case variant
98
+ when nil then return css
99
+ when :datauri then return with_data_uris(css)
100
+ when :mhtml then return with_mhtml(css, asset_url)
101
+ else raise PackageNotFound, "\"#{variant}\" is not a valid stylesheet variant"
102
+ end
103
+ end
104
+
105
+ # Compiles a single JST file by writing out a javascript that adds
106
+ # template properties to a top-level template namespace object. Adds a
107
+ # JST-compilation function to the top of the package, unless you've
108
+ # specified your own preferred function, or turned it off.
109
+ # JST templates are named with the basename of their file.
110
+ def compile_jst(paths)
111
+ namespace = Jammit.template_namespace
112
+ paths = paths.grep(Jammit.template_extension_matcher).sort
113
+ base_path = find_base_path(paths)
114
+ compiled = paths.map do |path|
115
+ contents = read_binary_file(path)
116
+ contents = contents.gsub(/\r?\n/, "\\n").gsub("'", '\\\\\'')
117
+ name = template_name(path, base_path)
118
+ "#{namespace}['#{name}'] = #{Jammit.template_function}('#{contents}');"
119
+ end
120
+ compiler = Jammit.include_jst_script ? read_binary_file(DEFAULT_JST_SCRIPT) : '';
121
+ setup_namespace = "#{namespace} = #{namespace} || {};"
122
+ [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
123
+ end
124
+
125
+
126
+ private
127
+
128
+ # Given a set of paths, find a common prefix path.
129
+ def find_base_path(paths)
130
+ return nil if paths.length <= 1
131
+ paths.sort!
132
+ first = paths.first.split('/')
133
+ last = paths.last.split('/')
134
+ i = 0
135
+ while first[i] == last[i] && i <= first.length
136
+ i += 1
137
+ end
138
+ res = first.slice(0, i).join('/')
139
+ res.empty? ? nil : res
140
+ end
141
+
142
+ # Determine the name of a JS template. If there's a common base path, use
143
+ # the namespaced prefix. Otherwise, simply use the filename.
144
+ def template_name(path, base_path)
145
+ return File.basename(path, ".#{Jammit.template_extension}") unless base_path
146
+ path.gsub(/\A#{Regexp.escape(base_path)}\/(.*)\.#{Jammit.template_extension}\Z/, '\1')
147
+ end
148
+
149
+ # In order to support embedded assets from relative paths, we need to
150
+ # expand the paths before contatenating the CSS together and losing the
151
+ # location of the original stylesheet path. Validate the assets while we're
152
+ # at it.
153
+ def concatenate_and_tag_assets(paths, variant=nil)
154
+ stylesheets = [paths].flatten.map do |css_path|
155
+ contents = read_binary_file(css_path)
156
+ contents.gsub(EMBED_DETECTOR) do |url|
157
+ ipath, cpath = Pathname.new($1), Pathname.new(File.expand_path(css_path))
158
+ is_url = URI.parse($1).absolute?
159
+ is_url ? url : "url(#{construct_asset_path(ipath, cpath, variant)})"
160
+ end
161
+ end
162
+ stylesheets.join("\n")
163
+ end
164
+
165
+ # Re-write all enabled asset URLs in a stylesheet with their corresponding
166
+ # Data-URI Base-64 encoded asset contents.
167
+ def with_data_uris(css)
168
+ css.gsub(EMBED_REPLACER) do |url|
169
+ "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
170
+ end
171
+ end
172
+
173
+ # Re-write all enabled asset URLs in a stylesheet with the MHTML equivalent.
174
+ # The newlines ("\r\n") in the following method are critical. Without them
175
+ # your MHTML will look identical, but won't work.
176
+ def with_mhtml(css, asset_url)
177
+ paths, index = {}, 0
178
+ css = css.gsub(EMBED_REPLACER) do |url|
179
+ i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
180
+ "url(mhtml:#{asset_url}!#{i})"
181
+ end
182
+ mhtml = paths.sort.map do |path, identifier|
183
+ mime, contents = mime_type(path), encoded_contents(path)
184
+ [MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
185
+ end
186
+ [MHTML_START, mhtml, MHTML_END, css].flatten.join('')
187
+ end
188
+
189
+ # Return a rewritten asset URL for a new stylesheet -- the asset should
190
+ # be tagged for embedding if embeddable, and referenced at the correct level
191
+ # if relative.
192
+ def construct_asset_path(asset_path, css_path, variant)
193
+ public_path = absolute_path(asset_path, css_path)
194
+ return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
195
+ source = asset_path.absolute? || ! Jammit.rewrite_relative_paths ? asset_path.to_s : relative_path(public_path)
196
+ rewrite_asset_path(source, public_path)
197
+ end
198
+
199
+ # Get the site-absolute public path for an asset file path that may or may
200
+ # not be relative, given the path of the stylesheet that contains it.
201
+ def absolute_path(asset_pathname, css_pathname)
202
+ (asset_pathname.absolute? ?
203
+ Pathname.new(File.join(Jammit.public_root, asset_pathname)) :
204
+ css_pathname.dirname + asset_pathname).cleanpath
205
+ end
206
+
207
+ # CSS assets that are referenced by relative paths, and are *not* being
208
+ # embedded, must be rewritten relative to the newly-merged stylesheet path.
209
+ def relative_path(absolute_path)
210
+ File.join('../', absolute_path.sub(Jammit.public_root, ''))
211
+ end
212
+
213
+ # Similar to the AssetTagHelper's method of the same name, this will
214
+ # append the RAILS_ASSET_ID cache-buster to URLs, if it's defined.
215
+ def rewrite_asset_path(path, file_path)
216
+ asset_id = rails_asset_id(file_path)
217
+ (!asset_id || asset_id == '') ? path : "#{path}?#{asset_id}"
218
+ end
219
+
220
+ # Similar to the AssetTagHelper's method of the same name, this will
221
+ # determine the correct asset id for a file.
222
+ def rails_asset_id(path)
223
+ asset_id = ENV["RAILS_ASSET_ID"]
224
+ return asset_id if asset_id
225
+ File.exists?(path) ? File.mtime(path).to_i.to_s : ''
226
+ end
227
+
228
+ # An asset is valid for embedding if it exists, is less than 32K, and is
229
+ # stored somewhere inside of a folder named "embed". IE does not support
230
+ # Data-URIs larger than 32K, and you probably shouldn't be embedding assets
231
+ # that large in any case. Because we need to check the base64 length here,
232
+ # save it so that we don't have to compute it again later.
233
+ def embeddable?(asset_path, variant)
234
+ font = EMBED_FONTS.include?(asset_path.extname)
235
+ return false unless variant
236
+ return false unless asset_path.to_s.match(EMBEDDABLE) && asset_path.exist?
237
+ return false unless EMBED_EXTS.include?(asset_path.extname)
238
+ return false unless font || encoded_contents(asset_path).length < MAX_IMAGE_SIZE
239
+ return false if font && variant == :mhtml
240
+ return true
241
+ end
242
+
243
+ # Return the Base64-encoded contents of an asset on a single line.
244
+ def encoded_contents(asset_path)
245
+ return @asset_contents[asset_path] if @asset_contents[asset_path]
246
+ data = read_binary_file(asset_path)
247
+ @asset_contents[asset_path] = Base64.encode64(data).gsub(/\n/, '')
248
+ end
249
+
250
+ # Grab the mime-type of an asset, by filename.
251
+ def mime_type(asset_path)
252
+ EMBED_MIME_TYPES[File.extname(asset_path)]
253
+ end
254
+
255
+ # Concatenate together a list of asset files.
256
+ def concatenate(paths)
257
+ [paths].flatten.map {|p| read_binary_file(p) }.join("\n")
258
+ end
259
+
260
+ # `File.read`, but in "binary" mode.
261
+ def read_binary_file(path)
262
+ File.open(path, 'rb:UTF-8') {|f| f.read }
263
+ end
264
+ end
265
+
266
+ end