ghazel-jammit 0.4.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 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
+
data/bin/jammit ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "#{File.dirname(__FILE__)}/../lib/jammit/command_line.rb"
4
+
5
+ Jammit::CommandLine.new
data/jammit.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'ghazel-jammit'
3
+ s.version = '0.4.3.1' # Keep version in sync with jammit.rb
4
+ s.date = '2009-1-29'
5
+
6
+ s.homepage = "http://documentcloud.github.com/jammit/"
7
+ s.summary = "Industrial Strength Asset Packaging for Rails"
8
+ s.description = <<-EOS
9
+ Jammit is an industrial strength asset packaging library for Rails,
10
+ providing both the CSS and JavaScript concatenation and compression that
11
+ you'd expect, as well as YUI Compressor and Closure Compiler compatibility,
12
+ ahead-of-time gzipping, built-in JavaScript template support, and optional
13
+ Data-URI / MHTML image embedding.
14
+ EOS
15
+
16
+ s.authors = ['Jeremy Ashkenas']
17
+ s.email = 'jeremy@documentcloud.org'
18
+ s.rubyforge_project = 'jammit'
19
+
20
+ s.require_paths = ['lib']
21
+ s.executables = ['jammit']
22
+
23
+ s.has_rdoc = true
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 'rails', ['>= 2.0.0']
31
+ s.add_dependency 'yui-compressor', ['>= 0.9.1']
32
+ s.add_dependency 'closure-compiler', ['>= 0.1.0']
33
+
34
+ s.files = Dir['lib/**/*', 'bin/*', 'jammit.gemspec', 'LICENSE', 'README']
35
+ end
@@ -0,0 +1,79 @@
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.load_configuration(@options[:config_path])
30
+ Jammit.packager.force = @options[:force]
31
+ Jammit.packager.precache_all(@options[:output_folder], @options[:base_url])
32
+ end
33
+
34
+
35
+ private
36
+
37
+ # Make sure that we have a readable configuration file. The @jammit@
38
+ # command can't run without one.
39
+ def ensure_configuration_file
40
+ config = @options[:config_path]
41
+ return true if File.exists?(config) && File.readable?(config)
42
+ puts "Could not find the asset configuration file \"#{config}\""
43
+ exit(1)
44
+ end
45
+
46
+ # Uses @OptionParser@ to grab the options: *--output*, *--config*, and
47
+ # *--base-url*...
48
+ def parse_options
49
+ @options = {
50
+ :config_path => Jammit::DEFAULT_CONFIG_PATH,
51
+ :output_folder => nil,
52
+ :base_url => nil,
53
+ :force => false
54
+ }
55
+ @option_parser = OptionParser.new do |opts|
56
+ opts.on('-o', '--output PATH', 'output folder for packages (default: "public/assets")') do |output_folder|
57
+ @options[:output_folder] = output_folder
58
+ end
59
+ opts.on('-c', '--config PATH', 'path to assets.yml (default: "config/assets.yml")') do |config_path|
60
+ @options[:config_path] = config_path
61
+ end
62
+ opts.on('-u', '--base-url URL', 'base URL for MHTML (ex: "http://example.com")') do |base_url|
63
+ @options[:base_url] = base_url
64
+ end
65
+ opts.on('-f', '--force', 'force a rebuild of all assets') do |force|
66
+ @options[:force] = force
67
+ end
68
+ opts.on_tail('-v', '--version', 'display Jammit version') do
69
+ puts "Jammit version #{Jammit::VERSION}"
70
+ exit
71
+ end
72
+ end
73
+ @option_parser.banner = BANNER
74
+ @option_parser.parse!(ARGV)
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,202 @@
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
+ include ActionView::Helpers::AssetTagHelper
11
+
12
+ # Mapping from extension to mime-type of all embeddable assets.
13
+ EMBED_MIME_TYPES = {
14
+ '.png' => 'image/png',
15
+ '.jpg' => 'image/jpeg',
16
+ '.jpeg' => 'image/jpeg',
17
+ '.gif' => 'image/gif',
18
+ '.tif' => 'image/tiff',
19
+ '.tiff' => 'image/tiff',
20
+ '.ttf' => 'font/truetype',
21
+ '.otf' => 'font/opentype'
22
+ }
23
+
24
+ # Font extensions for which we allow embedding:
25
+ EMBED_EXTS = EMBED_MIME_TYPES.keys
26
+ EMBED_FONTS = ['.ttf', '.otf']
27
+
28
+ # Maximum size for embeddable images (an IE8 limitation).
29
+ MAX_IMAGE_SIZE = 32.kilobytes
30
+
31
+ # CSS asset-embedding regexes for URL rewriting.
32
+ EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
33
+ EMBEDDABLE = /[\A\/]embed\//
34
+ EMBED_REPLACER = /url\(__EMBED__([^\s)]+)(\?\d+)?\)/
35
+
36
+ # MHTML file constants.
37
+ MHTML_START = "/*\r\nContent-Type: multipart/related; boundary=\"JAMMIT_MHTML_SEPARATOR\"\r\n\r\n"
38
+ MHTML_SEPARATOR = "--JAMMIT_MHTML_SEPARATOR\r\n"
39
+ MHTML_END = "*/\r\n"
40
+
41
+ # JST file constants.
42
+ JST_START = "(function(){"
43
+ JST_END = "})();"
44
+
45
+ COMPRESSORS = {
46
+ :yui => YUI::JavaScriptCompressor,
47
+ :closure => Closure::Compiler
48
+ }
49
+
50
+ DEFAULT_OPTIONS = {
51
+ :yui => {:munge => true},
52
+ :closure => {}
53
+ }
54
+
55
+ # Creating a compressor initializes the internal YUI Compressor from
56
+ # the "yui-compressor" gem, or the internal Closure Compiler from the
57
+ # "closure-compiler" gem.
58
+ def initialize
59
+ @css_compressor = YUI::CssCompressor.new(Jammit.css_compressor_options || {})
60
+ flavor = Jammit.javascript_compressor || Jammit::DEFAULT_COMPRESSOR
61
+ @options = DEFAULT_OPTIONS[flavor].merge(Jammit.compressor_options || {})
62
+ @js_compressor = COMPRESSORS[flavor].new(@options)
63
+ end
64
+
65
+ # Concatenate together a list of JavaScript paths, and pass them through the
66
+ # YUI Compressor (with munging enabled).
67
+ def compress_js(paths)
68
+ js = concatenate(paths)
69
+ Jammit.compress_assets ? @js_compressor.compress(js) : js
70
+ end
71
+
72
+ # Concatenate and compress a list of CSS stylesheets. When compressing a
73
+ # :datauri or :mhtml variant, post-processes the result to embed
74
+ # referenced assets.
75
+ def compress_css(paths, variant=nil, asset_url=nil)
76
+ css = concatenate_and_tag_assets(paths, variant)
77
+ css = @css_compressor.compress(css) if Jammit.compress_assets
78
+ case variant
79
+ when nil then return css
80
+ when :datauri then return with_data_uris(css)
81
+ when :mhtml then return with_mhtml(css, asset_url)
82
+ else raise PackageNotFound, "\"#{variant}\" is not a valid stylesheet variant"
83
+ end
84
+ end
85
+
86
+ # Compiles a single JST file by writing out a javascript that adds
87
+ # template properties to a top-level template namespace object. Adds a
88
+ # JST-compilation function to the top of the package, unless you've
89
+ # specified your own preferred function, or turned it off.
90
+ # JST templates are named with the basename of their file.
91
+ def compile_jst(paths)
92
+ namespace = Jammit.template_namespace
93
+ compiled = paths.map do |path|
94
+ template_name = File.basename(path, File.extname(path))
95
+ contents = File.read(path).gsub(/\n/, '').gsub("'", '\\\\\'')
96
+ "#{namespace}.#{template_name} = #{Jammit.template_function}('#{contents}');"
97
+ end
98
+ compiler = Jammit.include_jst_script ? File.read(DEFAULT_JST_SCRIPT) : '';
99
+ setup_namespace = "#{namespace} = #{namespace} || {};"
100
+ [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
101
+ end
102
+
103
+
104
+ private
105
+
106
+ # In order to support embedded assets from relative paths, we need to
107
+ # expand the paths before contatenating the CSS together and losing the
108
+ # location of the original stylesheet path. Validate the assets while we're
109
+ # at it.
110
+ def concatenate_and_tag_assets(paths, variant=nil)
111
+ stylesheets = [paths].flatten.map do |css_path|
112
+ File.read(css_path).gsub(EMBED_DETECTOR) do |url|
113
+ ipath, cpath = Pathname.new($1), Pathname.new(File.expand_path(css_path))
114
+ is_url = URI.parse($1).absolute?
115
+ is_url ? url : "url(#{construct_asset_path(ipath, cpath, variant)})"
116
+ end
117
+ end
118
+ stylesheets.join("\n")
119
+ end
120
+
121
+ # Re-write all enabled asset URLs in a stylesheet with their corresponding
122
+ # Data-URI Base-64 encoded asset contents.
123
+ def with_data_uris(css)
124
+ css.gsub(EMBED_REPLACER) do |url|
125
+ "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
126
+ end
127
+ end
128
+
129
+ # Re-write all enabled asset URLs in a stylesheet with the MHTML equivalent.
130
+ # The newlines ("\r\n") in the following method are critical. Without them
131
+ # your MHTML will look identical, but won't work.
132
+ def with_mhtml(css, asset_url)
133
+ paths, index = {}, 0
134
+ css = css.gsub(EMBED_REPLACER) do |url|
135
+ i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
136
+ "url(mhtml:#{asset_url}!#{i})"
137
+ end
138
+ paths = paths.sort
139
+ mhtml = paths.map do |path, identifier|
140
+ mime, contents = mime_type(path), encoded_contents(path)
141
+ [MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
142
+ end
143
+ [MHTML_START, mhtml, MHTML_END, css].flatten.join('')
144
+ end
145
+
146
+ # Return a rewritten asset URL for a new stylesheet -- the asset should
147
+ # be tagged for embedding if embeddable, and referenced at the correct level
148
+ # if relative.
149
+ def construct_asset_path(asset_path, css_path, variant)
150
+ public_path = absolute_path(asset_path, css_path)
151
+ return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
152
+ source = asset_path.absolute? ? asset_path.to_s : relative_path(public_path)
153
+ rewrite_asset_path(source)
154
+ end
155
+
156
+ # Get the site-absolute public path for an asset file path that may or may
157
+ # not be relative, given the path of the stylesheet that contains it.
158
+ def absolute_path(asset_pathname, css_pathname)
159
+ (asset_pathname.absolute? ?
160
+ Pathname.new(File.join(PUBLIC_ROOT, asset_pathname)) :
161
+ css_pathname.dirname + asset_pathname).cleanpath
162
+ end
163
+
164
+ # CSS assets that are referenced by relative paths, and are *not* being
165
+ # embedded, must be rewritten relative to the newly-merged stylesheet path.
166
+ def relative_path(absolute_path)
167
+ File.join('../', absolute_path.sub(PUBLIC_ROOT, ''))
168
+ end
169
+
170
+ # An asset is valid for embedding if it exists, is less than 32K, and is
171
+ # stored somewhere inside of a folder named "embed".
172
+ # IE does not support Data-URIs larger than 32K, and you probably shouldn't
173
+ # be embedding assets that large in any case.
174
+ def embeddable?(asset_path, variant)
175
+ font = EMBED_FONTS.include?(asset_path.extname)
176
+ return false unless variant
177
+ return false unless asset_path.to_s.match(EMBEDDABLE) && asset_path.exist?
178
+ return false unless EMBED_EXTS.include?(asset_path.extname)
179
+ return false unless font || asset_path.size < MAX_IMAGE_SIZE
180
+ return false if font && variant == :mhtml
181
+ true
182
+ end
183
+
184
+ # Return the Base64-encoded contents of an asset on a single line.
185
+ def encoded_contents(asset_path)
186
+ data = File.open(asset_path, 'rb'){|f| f.read }
187
+ Base64.encode64(data).gsub(/\n/, '')
188
+ end
189
+
190
+ # Grab the mime-type of an asset, by filename.
191
+ def mime_type(asset_path)
192
+ EMBED_MIME_TYPES[File.extname(asset_path)]
193
+ end
194
+
195
+ # Concatenate together a list of asset files.
196
+ def concatenate(paths)
197
+ [paths].flatten.map {|p| File.read(p) }.join("\n")
198
+ end
199
+
200
+ end
201
+
202
+ end
@@ -0,0 +1,91 @@
1
+ module Jammit
2
+
3
+ # The JammitController is added to your Rails application when the Gem is
4
+ # loaded. It takes responsibility for /assets, and dynamically packages any
5
+ # missing or uncached asset packages.
6
+ class Controller < ActionController::Base
7
+
8
+ VALID_FORMATS = [:css, :js, :jst]
9
+
10
+ SUFFIX_STRIPPER = /-(datauri|mhtml)\Z/
11
+
12
+ NOT_FOUND_PATH = "#{PUBLIC_ROOT}/404.html"
13
+
14
+ # The "package" action receives all requests for asset packages that haven't
15
+ # yet been cached. The package will be built, cached, and gzipped.
16
+ def package
17
+ parse_request
18
+ case @extension
19
+ when :js then render :js => (@contents = Jammit.packager.pack_javascripts(@package))
20
+ when :css then render :text => generate_stylesheets, :content_type => 'text/css'
21
+ when :jst then render :js => (@contents = Jammit.packager.pack_templates(@package))
22
+ end
23
+ cache_package if perform_caching
24
+ rescue Jammit::PackageNotFound
25
+ package_not_found
26
+ end
27
+
28
+
29
+ private
30
+
31
+ # Tells the Jammit::Packager to cache and gzip an asset package. We can't
32
+ # just use the built-in "cache_page" because we need to ensure that
33
+ # the timestamp that ends up in the MHTML is also on the cached file.
34
+ def cache_package
35
+ dir = File.join(page_cache_directory, Jammit.package_path)
36
+ Jammit.packager.cache(@package, @extension, @contents, dir, @variant, @mtime)
37
+ end
38
+
39
+ # Generate the complete, timestamped, MHTML url -- if we're rendering a
40
+ # dynamic MHTML package, we'll need to put one URL in the response, and a
41
+ # different one into the cached package.
42
+ def prefix_url(path)
43
+ host = request.port == 80 ? request.host : request.host_with_port
44
+ "#{request.protocol}#{host}#{path}"
45
+ end
46
+
47
+ # If we're generating MHTML/CSS, return a stylesheet with the absolute
48
+ # request URL to the client, and cache a version with the timestamped cache
49
+ # URL swapped in.
50
+ def generate_stylesheets
51
+ return @contents = Jammit.packager.pack_stylesheets(@package, @variant) unless @variant == :mhtml
52
+ @mtime = Time.now
53
+ request_url = prefix_url(request.request_uri)
54
+ cached_url = prefix_url(Jammit.asset_url(@package, @extension, @variant, @mtime))
55
+ css = Jammit.packager.pack_stylesheets(@package, @variant, request_url)
56
+ @contents = css.gsub(request_url, cached_url) if perform_caching
57
+ css
58
+ end
59
+
60
+ # Extracts the package name, extension (:css, :js, :jst), and variant
61
+ # (:datauri, :mhtml) from the incoming URL.
62
+ def parse_request
63
+ pack = params[:package]
64
+ @extension = params[:extension].to_sym
65
+ raise PackageNotFound unless VALID_FORMATS.include?(@extension)
66
+ if Jammit.embed_assets
67
+ suffix_match = pack.match(SUFFIX_STRIPPER)
68
+ @variant = Jammit.embed_assets && suffix_match && suffix_match[1].to_sym
69
+ pack.sub!(SUFFIX_STRIPPER, '')
70
+ end
71
+ @package = pack.to_sym
72
+ end
73
+
74
+ # Render the 404 page, if one exists, for any packages that don't.
75
+ def package_not_found
76
+ return render(:file => NOT_FOUND_PATH, :status => 404) if File.exists?(NOT_FOUND_PATH)
77
+ render :text => "<h1>404: \"#{@package}\" asset package not found.</h1>", :status => 404
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ # Make the Jammit::Controller available to Rails as a top-level controller.
85
+ ::JammitController = Jammit::Controller
86
+
87
+ if Rails.env.development?
88
+ ActionController::Base.class_eval do
89
+ append_before_filter { Jammit.reload! }
90
+ end
91
+ end
@@ -0,0 +1,26 @@
1
+ # Standard Library Dependencies:
2
+ require 'uri'
3
+ require 'erb'
4
+ require 'zlib'
5
+ require 'base64'
6
+ require 'pathname'
7
+ require 'fileutils'
8
+
9
+ # Gem Dependencies:
10
+ require 'rubygems'
11
+ require 'yui/compressor'
12
+ require 'closure-compiler'
13
+ require 'active_support'
14
+
15
+ # Load initial configuration before the rest of Jammit.
16
+ Jammit.load_configuration(Jammit::DEFAULT_CONFIG_PATH) if defined?(Rails)
17
+
18
+ # Jammit Core:
19
+ require 'jammit/compressor'
20
+ require 'jammit/packager'
21
+
22
+ # Jammit Rails Integration:
23
+ if defined?(Rails)
24
+ require 'jammit/controller' # Rails will auto-load 'jammit/helper' for us.
25
+ require 'jammit/routes'
26
+ end
@@ -0,0 +1,77 @@
1
+ module Jammit
2
+
3
+ # The Jammit::Helper module, which is made available to every view, provides
4
+ # helpers for writing out HTML tags for asset packages. In development you
5
+ # get the ordered list of source files -- in any other environment, a link
6
+ # to the cached packages.
7
+ module Helper
8
+
9
+ DATA_URI_START = "<!--[if (!IE)|(gte IE 8)]><!-->"
10
+ DATA_URI_END = "<!--<![endif]-->"
11
+ MHTML_START = "<!--[if lte IE 7]>"
12
+ MHTML_END = "<![endif]-->"
13
+
14
+ # If embed_assets is turned on, writes out links to the Data-URI and MHTML
15
+ # versions of the stylesheet package, otherwise the package is regular
16
+ # compressed CSS, and in development the stylesheet URLs are passed verbatim.
17
+ def include_stylesheets(*packages)
18
+ options = packages.extract_options!
19
+ return individual_stylesheets(packages, options) unless Jammit.package_assets
20
+ disabled = (options.delete(:embed_assets) == false) || (options.delete(:embed_images) == false)
21
+ return packaged_stylesheets(packages, options) if disabled || !Jammit.embed_assets
22
+ return embedded_image_stylesheets(packages, options)
23
+ end
24
+
25
+ # Writes out the URL to the bundled and compressed javascript package,
26
+ # except in development, where it references the individual scripts.
27
+ def include_javascripts(*packages)
28
+ tags = packages.map do |pack|
29
+ Jammit.package_assets ? Jammit.asset_url(pack, :js) : Jammit.packager.individual_urls(pack.to_sym, :js)
30
+ end
31
+ javascript_include_tag(tags.flatten)
32
+ end
33
+
34
+ # Writes out the URL to the concatenated and compiled JST file -- we always
35
+ # have to pre-process it, even in development.
36
+ def include_templates(*packages)
37
+ javascript_include_tag(packages.map {|pack| Jammit.asset_url(pack, :jst) })
38
+ end
39
+
40
+
41
+ private
42
+
43
+ # HTML tags, in order, for all of the individual stylesheets.
44
+ def individual_stylesheets(packages, options)
45
+ tags_with_options(packages, options) {|p| Jammit.packager.individual_urls(p.to_sym, :css) }
46
+ end
47
+
48
+ # HTML tags for the stylesheet packages.
49
+ def packaged_stylesheets(packages, options)
50
+ tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css) }
51
+ end
52
+
53
+ # HTML tags for the 'datauri', and 'mhtml' versions of the packaged
54
+ # stylesheets, using conditional comments to load the correct variant.
55
+ def embedded_image_stylesheets(packages, options)
56
+ datauri_tags = tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css, :datauri) }
57
+ ie_tags = Jammit.mhtml_enabled ?
58
+ tags_with_options(packages, options) {|p| Jammit.asset_url(p, :css, :mhtml) } :
59
+ packaged_stylesheets(packages, options)
60
+ [DATA_URI_START, datauri_tags, DATA_URI_END, MHTML_START, ie_tags, MHTML_END].join("\n")
61
+ end
62
+
63
+ # Generate the stylesheet tags for a batch of packages, with options, by
64
+ # yielding each package to a block.
65
+ def tags_with_options(packages, options)
66
+ packages = packages.dup
67
+ packages.map! {|package| yield package }.flatten!
68
+ packages.push(options) unless options.empty?
69
+ stylesheet_link_tag(*packages)
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ # Include the Jammit asset helpers in all views, a-la ApplicationHelper.
77
+ ::ActionView::Base.send(:include, Jammit::Helper)
data/lib/jammit/jst.js ADDED
@@ -0,0 +1 @@
1
+ var template = function(str){var fn = new Function('obj', 'var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push(\''+str.replace(/[\r\t\n]/g, " ").replace(/'(?=[^%]*%>)/g,"\t").split("'").join("\\'").split("\t").join("'").replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');"); return fn;};
@@ -0,0 +1,147 @@
1
+ module Jammit
2
+
3
+ # The Jammit::Packager resolves the configuration file into lists of real
4
+ # assets that get merged into individual asset packages. Given the compiled
5
+ # contents of an asset package, the Packager knows how to cache that package
6
+ # with the correct timestamps.
7
+ class Packager
8
+
9
+ # In Rails, the difference between a path and an asset URL is "public".
10
+ PATH_TO_URL = /\A#{Regexp.escape(ASSET_ROOT)}(\/?public)?/
11
+
12
+ # Set force to false to allow packages to only be rebuilt when their source
13
+ # files have changed since the last time their package was built.
14
+ attr_accessor :force
15
+
16
+ # Creating a new Packager will rebuild the list of assets from the
17
+ # Jammit.configuration. When assets.yml is being changed on the fly,
18
+ # create a new Packager.
19
+ def initialize
20
+ @compressor = Compressor.new
21
+ @force = false
22
+ @config = {
23
+ :css => (Jammit.configuration[:stylesheets] || {}).symbolize_keys,
24
+ :js => (Jammit.configuration[:javascripts] || {}).symbolize_keys,
25
+ :jst => (Jammit.configuration[:templates] || {}).symbolize_keys
26
+ }
27
+ @packages = {
28
+ :css => create_packages(@config[:css]),
29
+ :js => create_packages(@config[:js]),
30
+ :jst => create_packages(@config[:jst])
31
+ }
32
+ end
33
+
34
+ # Ask the packager to precache all defined assets, along with their gzip'd
35
+ # versions. In order to prebuild the MHTML stylesheets, we need to know the
36
+ # base_url, because IE only supports MHTML with absolute references.
37
+ # Unless forced, will only rebuild assets whose source files have been
38
+ # changed since their last package build.
39
+ def precache_all(output_dir=nil, base_url=nil)
40
+ output_dir ||= File.join(PUBLIC_ROOT, Jammit.package_path)
41
+ cacheable(:js, output_dir).each {|p| cache(p, 'js', pack_javascripts(p), output_dir) }
42
+ cacheable(:jst, output_dir).each {|p| cache(p, 'jst', pack_templates(p), output_dir) }
43
+ cacheable(:css, output_dir).each do |p|
44
+ cache(p, 'css', pack_stylesheets(p), output_dir)
45
+ if Jammit.embed_assets
46
+ cache(p, 'css', pack_stylesheets(p, :datauri), output_dir, :datauri)
47
+ if Jammit.mhtml_enabled && base_url
48
+ mtime = Time.now
49
+ asset_url = "#{base_url}#{Jammit.asset_url(p, :css, :mhtml, mtime)}"
50
+ cache(p, 'css', pack_stylesheets(p, :mhtml, asset_url), output_dir, :mhtml, mtime)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Caches a single prebuilt asset package and gzips it at the highest
57
+ # compression level. Ensures that the modification time of both both
58
+ # variants is identical, for web server caching modules, as well as MHTML.
59
+ def cache(package, extension, contents, output_dir, suffix=nil, mtime=Time.now)
60
+ FileUtils.mkdir_p(output_dir) unless File.exists?(output_dir)
61
+ raise OutputNotWritable, "Jammit doesn't have permission to write to \"#{output_dir}\"" unless File.writable?(output_dir)
62
+ files = []
63
+ files << file_name = File.join(output_dir, Jammit.filename(package, extension, suffix))
64
+ File.open(file_name, 'wb+') {|f| f.write(contents) }
65
+ if Jammit.gzip_assets
66
+ files << zip_name = "#{file_name}.gz"
67
+ Zlib::GzipWriter.open(zip_name, Zlib::BEST_COMPRESSION) {|f| f.write(contents) }
68
+ end
69
+ File.utime(mtime, mtime, *files)
70
+ end
71
+
72
+ # Get the list of individual assets for a package.
73
+ def individual_urls(package, extension)
74
+ package_for(package, extension)[:urls]
75
+ end
76
+
77
+ # Return the compressed contents of a stylesheet package.
78
+ def pack_stylesheets(package, variant=nil, asset_url=nil)
79
+ @compressor.compress_css(package_for(package, :css)[:paths], variant, asset_url)
80
+ end
81
+
82
+ # Return the compressed contents of a javascript package.
83
+ def pack_javascripts(package)
84
+ @compressor.compress_js(package_for(package, :js)[:paths])
85
+ end
86
+
87
+ # Return the compiled contents of a JST package.
88
+ def pack_templates(package)
89
+ @compressor.compile_jst(package_for(package, :jst)[:paths])
90
+ end
91
+
92
+
93
+ private
94
+
95
+ # Look up a package asset list by name, raising an exception if the
96
+ # package has gone missing.
97
+ def package_for(package, extension)
98
+ pack = @packages[extension] && @packages[extension][package]
99
+ pack || not_found(package, extension)
100
+ end
101
+
102
+ # Absolute globs are absolute -- relative globs are relative to ASSET_ROOT.
103
+ # Print a warning if no files were found that match the glob.
104
+ def glob_files(glob)
105
+ absolute = Pathname.new(glob).absolute?
106
+ paths = Dir[absolute ? glob : File.join(ASSET_ROOT, glob)].sort
107
+ Jammit.warn("No assets match '#{glob}'") if paths.empty?
108
+ paths
109
+ end
110
+
111
+ # Return a list of all of the packages that should be cached. If "force" is
112
+ # true, this is all of them -- otherwise only the packages whose source
113
+ # files have changed since the last package build.
114
+ def cacheable(extension, output_dir)
115
+ names = @packages[extension].keys
116
+ return names if @force
117
+ return names.select do |name|
118
+ pack = package_for(name, extension)
119
+ cached = File.join(output_dir, Jammit.filename(name, extension))
120
+ since = File.exists?(cached) && File.mtime(cached)
121
+ !since || pack[:paths].any? {|src| File.mtime(src) > since }
122
+ end
123
+ end
124
+
125
+ # Compiles the list of assets that goes into each package. Runs an ordered
126
+ # list of Dir.globs, taking the merged unique result.
127
+ def create_packages(config)
128
+ packages = {}
129
+ return packages if !config
130
+ config.each do |name, globs|
131
+ globs ||= []
132
+ packages[name] = {}
133
+ paths = globs.map {|glob| glob_files(glob) }.flatten.uniq
134
+ packages[name][:paths] = paths
135
+ packages[name][:urls] = paths.map {|path| path.sub(PATH_TO_URL, '') }
136
+ end
137
+ packages
138
+ end
139
+
140
+ # Raise a PackageNotFound exception for missing packages...
141
+ def not_found(package, extension)
142
+ raise PackageNotFound, "assets.yml does not contain a \"#{package}\" #{extension.to_s.upcase} package"
143
+ end
144
+
145
+ end
146
+
147
+ end
@@ -0,0 +1,16 @@
1
+ module Jammit
2
+
3
+ module Routes
4
+
5
+ # Jammit uses a single route in order to slow down Rails' routing speed
6
+ # by the absolute minimum. In your config/routes.rb file, call:
7
+ # Jammit::Routes.draw(map)
8
+ # Passing in the routing "map" object.
9
+ def self.draw(map)
10
+ map.jammit "/#{Jammit.package_path}/:package.:extension",
11
+ :controller => 'jammit', :action => 'package'
12
+ end
13
+
14
+ end
15
+
16
+ end
data/lib/jammit.rb ADDED
@@ -0,0 +1,153 @@
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.4.3.1"
8
+
9
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
10
+
11
+ ASSET_ROOT = File.expand_path(defined?(Rails) ? Rails.root : ".") unless defined?(ASSET_ROOT)
12
+
13
+ PUBLIC_ROOT = File.join(ASSET_ROOT, 'public')
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
+ AVAILABLE_COMPRESSORS = [:yui, :closure]
26
+
27
+ DEFAULT_COMPRESSOR = :yui
28
+
29
+ # Jammit raises a @PackageNotFound@ exception when a non-existent package is
30
+ # requested by a browser -- rendering a 404.
31
+ class PackageNotFound < NameError; end
32
+
33
+ # Jammit raises a ConfigurationNotFound exception when you try to load the
34
+ # configuration of an assets.yml file that doesn't exist.
35
+ class ConfigurationNotFound < NameError; end
36
+
37
+ # Jammit raises an OutputNotWritable exception if the output directory for
38
+ # cached packages is locked.
39
+ class OutputNotWritable < StandardError; end
40
+
41
+ class << self
42
+ attr_reader :configuration, :template_function, :template_namespace,
43
+ :embed_assets, :package_assets, :compress_assets, :gzip_assets,
44
+ :package_path, :mhtml_enabled, :include_jst_script,
45
+ :javascript_compressor, :compressor_options, :css_compressor_options
46
+ end
47
+
48
+ # The minimal required configuration.
49
+ @configuration = {}
50
+ @package_path = DEFAULT_PACKAGE_PATH
51
+
52
+ # Load the complete asset configuration from the specified @config_path@.
53
+ def self.load_configuration(config_path)
54
+ exists = config_path && File.exists?(config_path)
55
+ raise ConfigurationNotFound, "could not find the \"#{config_path}\" configuration file" unless exists
56
+ conf = YAML.load(ERB.new(File.read(config_path)).result)
57
+ @config_path = config_path
58
+ @configuration = conf = conf.symbolize_keys
59
+ @package_path = conf[:package_path] || DEFAULT_PACKAGE_PATH
60
+ @embed_assets = conf[:embed_assets] || conf[:embed_images]
61
+ @compress_assets = !(conf[:compress_assets] == false)
62
+ @gzip_assets = !(conf[:gzip_assets] == false)
63
+ @mhtml_enabled = @embed_assets && @embed_assets != "datauri"
64
+ @compressor_options = (conf[:compressor_options] || {}).symbolize_keys
65
+ @css_compressor_options = (conf[:css_compressor_options] || {}).symbolize_keys
66
+ set_javascript_compressor(conf[:javascript_compressor])
67
+ set_package_assets(conf[:package_assets])
68
+ set_template_function(conf[:template_function])
69
+ set_template_namespace(conf[:template_namespace])
70
+ check_java_version
71
+ self
72
+ end
73
+
74
+ # Force a reload by resetting the Packager and reloading the configuration.
75
+ # In development, this will be called as a before_filter before every request.
76
+ def self.reload!
77
+ Thread.current[:jammit_packager] = nil
78
+ load_configuration(@config_path)
79
+ end
80
+
81
+ # Keep a global (thread-local) reference to a @Jammit::Packager@, to avoid
82
+ # recomputing asset lists unnecessarily.
83
+ def self.packager
84
+ Thread.current[:jammit_packager] ||= Packager.new
85
+ end
86
+
87
+ # Generate the base filename for a version of a given package.
88
+ def self.filename(package, extension, suffix=nil)
89
+ suffix_part = suffix ? "-#{suffix}" : ''
90
+ "#{package}#{suffix_part}.#{extension}"
91
+ end
92
+
93
+ # Generates the server-absolute URL to an asset package.
94
+ def self.asset_url(package, extension, suffix=nil, mtime=nil)
95
+ timestamp = mtime ? "?#{mtime.to_i}" : ''
96
+ "/#{package_path}/#{filename(package, extension, suffix)}#{timestamp}"
97
+ end
98
+
99
+
100
+ private
101
+
102
+ # Ensure that the JavaScript compressor is a valid choice.
103
+ def self.set_javascript_compressor(value)
104
+ value = value && value.to_sym
105
+ @javascript_compressor = AVAILABLE_COMPRESSORS.include?(value) ? value : DEFAULT_COMPRESSOR
106
+ end
107
+
108
+ # Turn asset packaging on or off, depending on configuration and environment.
109
+ def self.set_package_assets(value)
110
+ package_env = !defined?(Rails) || !Rails.env.development?
111
+ @package_assets = value == true || value.nil? ? package_env :
112
+ value == 'always' ? true : false
113
+ end
114
+
115
+ # Assign the JST template function, unless explicitly turned off.
116
+ def self.set_template_function(value)
117
+ @template_function = value == true || value.nil? ? DEFAULT_JST_COMPILER :
118
+ value == false ? '' : value
119
+ @include_jst_script = @template_function == DEFAULT_JST_COMPILER
120
+ end
121
+
122
+ # Set the root JS object in which to stash all compiled JST.
123
+ def self.set_template_namespace(value)
124
+ @template_namespace = value == true || value.nil? ? DEFAULT_JST_NAMESPACE : value.to_s
125
+ end
126
+
127
+ # The YUI Compressor requires Java > 1.4, and Closure requires Java > 1.6.
128
+ def self.check_java_version
129
+ java = @compressor_options[:java] || 'java'
130
+ @css_compressor_options[:java] ||= java if @compressor_options[:java]
131
+ version = (`#{java} -version 2>&1`)[/\d+\.\d+/]
132
+ disable_compression if !version ||
133
+ (@javascript_compressor == :closure && version < '1.6') ||
134
+ (@javascript_compressor == :yui && version < '1.4')
135
+ end
136
+
137
+ # If we don't have a working Java VM, then disable asset compression and
138
+ # complain loudly.
139
+ def self.disable_compression
140
+ @compress_assets = false
141
+ warn("Asset compression disabled -- Java unavailable.")
142
+ end
143
+
144
+ def self.warn(message)
145
+ message = "Jammit Warning: #{message}"
146
+ @logger ||= (defined?(Rails) && Rails.logger ? Rails.logger :
147
+ defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : nil)
148
+ @logger ? @logger.warn(message) : STDERR.puts(message)
149
+ end
150
+
151
+ end
152
+
153
+ require 'jammit/dependencies'
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ghazel-jammit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Ashkenas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-29 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: yui-compressor
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: closure-compiler
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.1.0
44
+ version:
45
+ description: " Jammit is an industrial strength asset packaging library for Rails,\n providing both the CSS and JavaScript concatenation and compression that\n you'd expect, as well as YUI Compressor and Closure Compiler compatibility,\n ahead-of-time gzipping, built-in JavaScript template support, and optional\n Data-URI / MHTML image embedding.\n"
46
+ email: jeremy@documentcloud.org
47
+ executables:
48
+ - jammit
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README
53
+ files:
54
+ - lib/jammit/command_line.rb
55
+ - lib/jammit/compressor.rb
56
+ - lib/jammit/controller.rb
57
+ - lib/jammit/dependencies.rb
58
+ - lib/jammit/helper.rb
59
+ - lib/jammit/jst.js
60
+ - lib/jammit/packager.rb
61
+ - lib/jammit/routes.rb
62
+ - lib/jammit.rb
63
+ - bin/jammit
64
+ - jammit.gemspec
65
+ - LICENSE
66
+ - README
67
+ has_rdoc: true
68
+ homepage: http://documentcloud.github.com/jammit/
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options:
73
+ - --title
74
+ - Jammit
75
+ - --exclude
76
+ - test
77
+ - --main
78
+ - README
79
+ - --all
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ requirements: []
95
+
96
+ rubyforge_project: jammit
97
+ rubygems_version: 1.3.5
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Industrial Strength Asset Packaging for Rails
101
+ test_files: []
102
+