ee_jammit 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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