ram 0.0.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.
@@ -0,0 +1,19 @@
1
+ == Ram
2
+
3
+ Ram is an asset packaging library, written in Ruby. It is entirely taken from the Jammit gem, but stripped of any Rails dependencies or helpers. It was primarily written for packaging nanoc assets, though I suppose it could be used for many types of web development projects.
4
+
5
+ Installation:
6
+
7
+ gem install ram
8
+
9
+ Documentation, usage, and examples:
10
+
11
+ hmmm
12
+
13
+ To suggest a feature or report a bug:
14
+
15
+ hmmm
16
+
17
+ For source documentation:
18
+
19
+ one more
data/bin/ram ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby -rrubygems
2
+
3
+ require "#{File.dirname(__FILE__)}/../lib/ram/command_line.rb"
4
+
5
+ Ram::CommandLine.new
@@ -0,0 +1,216 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__))
2
+
3
+ # @Ram@ is the central namespace for all Ram classes, and provides access
4
+ # to all of the configuration options.
5
+ module Ram
6
+
7
+ VERSION = "0.6.3"
8
+
9
+ ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
10
+
11
+ ASSET_ROOT = '.' unless defined?(ASSET_ROOT)
12
+
13
+ DEFAULT_PUBLIC_ROOT = 'output' 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/ram/jst.js')
20
+
21
+ DEFAULT_JST_COMPILER = "template"
22
+
23
+ DEFAULT_JST_NAMESPACE = "window.JST"
24
+
25
+ COMPRESSORS = [:yui, :closure, :uglifier]
26
+
27
+ DEFAULT_COMPRESSOR = :yui
28
+
29
+ # Extension matchers for JavaScript and JST, which need to be disambiguated.
30
+ JS_EXTENSION = /\.js\Z/
31
+ DEFAULT_JST_EXTENSION = "jst"
32
+
33
+ # Ram raises a @PackageNotFound@ exception when a non-existent package is
34
+ # requested by a browser -- rendering a 404.
35
+ class PackageNotFound < NameError; end
36
+
37
+ # Ram raises a MissingConfiguration exception when you try to load the
38
+ # configuration of an assets.yml file that doesn't exist, or are missing
39
+ # a piece of required configuration.
40
+ class MissingConfiguration < NameError; end
41
+
42
+ # Ram raises an OutputNotWritable exception if the output directory for
43
+ # cached packages is locked.
44
+ class OutputNotWritable < StandardError; end
45
+
46
+ # Ram raises a DeprecationError if you try to use an outdated feature.
47
+ class DeprecationError < StandardError; end
48
+
49
+ class << self
50
+ attr_reader :configuration, :template_function, :template_namespace,
51
+ :embed_assets, :package_assets, :compress_assets, :gzip_assets,
52
+ :package_path, :mhtml_enabled, :include_jst_script, :config_path,
53
+ :javascript_compressor, :compressor_options, :css_compressor_options,
54
+ :template_extension, :template_extension_matcher, :allow_debugging,
55
+ :public_root
56
+ attr_accessor :compressors
57
+ end
58
+
59
+ # The minimal required configuration.
60
+ @configuration = {}
61
+ @public_root = DEFAULT_PUBLIC_ROOT
62
+ @package_path = DEFAULT_PACKAGE_PATH
63
+ @compressors = COMPRESSORS
64
+
65
+ # Load the complete asset configuration from the specified @config_path@.
66
+ # If we're loading softly, don't let missing configuration error out.
67
+ def self.load_configuration(config_path, soft=false)
68
+ exists = config_path && File.exists?(config_path)
69
+ return false if soft && !exists
70
+ raise MissingConfiguration, "could not find the \"#{config_path}\" configuration file" unless exists
71
+ conf = YAML.load(ERB.new(File.read(config_path)).result)
72
+
73
+ @config_path = config_path
74
+ @configuration = symbolize_keys(conf)
75
+ @package_path = conf[:package_path] || DEFAULT_PACKAGE_PATH
76
+ @embed_assets = conf[:embed_assets] || conf[:embed_images]
77
+ @compress_assets = !(conf[:compress_assets] == false)
78
+ @gzip_assets = !(conf[:gzip_assets] == false)
79
+ @allow_debugging = !(conf[:allow_debugging] == false)
80
+ @mhtml_enabled = @embed_assets && @embed_assets != "datauri"
81
+ @compressor_options = symbolize_keys(conf[:compressor_options] || {})
82
+ @css_compressor_options = symbolize_keys(conf[:css_compressor_options] || {})
83
+ set_javascript_compressor(conf[:javascript_compressor])
84
+ set_package_assets(conf[:package_assets])
85
+ set_template_function(conf[:template_function])
86
+ set_template_namespace(conf[:template_namespace])
87
+ set_template_extension(conf[:template_extension])
88
+ set_public_root(conf[:public_root]) if conf[:public_root]
89
+ symbolize_keys(conf[:stylesheets]) if conf[:stylesheets]
90
+ symbolize_keys(conf[:javascripts]) if conf[:javascripts]
91
+ check_for_deprecations
92
+ self
93
+ end
94
+
95
+ # Force a reload by resetting the Packager and reloading the configuration.
96
+ # In development, this will be called as a before_filter before every request.
97
+ def self.reload!
98
+ Thread.current[:ram_packager] = nil
99
+ load_configuration(@config_path)
100
+ end
101
+
102
+ # Keep a global (thread-local) reference to a @Ram::Packager@, to avoid
103
+ # recomputing asset lists unnecessarily.
104
+ def self.packager
105
+ Thread.current[:ram_packager] ||= Packager.new
106
+ end
107
+
108
+ # Generate the base filename for a version of a given package.
109
+ def self.filename(package, extension, suffix=nil)
110
+ suffix_part = suffix ? "-#{suffix}" : ''
111
+ "#{package}#{suffix_part}.#{extension}"
112
+ end
113
+
114
+ # Generates the server-absolute URL to an asset package.
115
+ def self.asset_url(package, extension, suffix=nil, mtime=nil)
116
+ timestamp = mtime ? "?#{mtime.to_i}" : ''
117
+ "/#{package_path}/#{filename(package, extension, suffix)}#{timestamp}"
118
+ end
119
+
120
+ # Convenience method for packaging up Ram, using the default options.
121
+ def self.package!(options={})
122
+ options = {
123
+ :config_path => Ram::DEFAULT_CONFIG_PATH,
124
+ :output_folder => nil,
125
+ :base_url => nil,
126
+ :public_root => nil,
127
+ :force => false
128
+ }.merge(options)
129
+ load_configuration(options[:config_path])
130
+ set_public_root(options[:public_root]) if options[:public_root]
131
+ packager.force = options[:force]
132
+ packager.package_names = options[:package_names]
133
+ packager.precache_all(options[:output_folder], options[:base_url])
134
+ end
135
+
136
+ private
137
+
138
+ # Allows command-line definition of `PUBLIC_ROOT`
139
+ def self.set_public_root(public_root=nil)
140
+ @public_root = public_root if public_root
141
+ end
142
+
143
+ # Ensure that the JavaScript compressor is a valid choice.
144
+ def self.set_javascript_compressor(value)
145
+ value = value && value.to_sym
146
+ @javascript_compressor = compressors.include?(value) ? value : DEFAULT_COMPRESSOR
147
+ end
148
+
149
+ # Turn asset packaging on or off, depending on configuration and environment.
150
+ def self.set_package_assets(value)
151
+ package_env = 'production'
152
+ @package_assets = value == true || value.nil? ? package_env :
153
+ value == 'always' ? true : false
154
+ end
155
+
156
+ # Assign the JST template function, unless explicitly turned off.
157
+ def self.set_template_function(value)
158
+ @template_function = value == true || value.nil? ? DEFAULT_JST_COMPILER :
159
+ value == false ? '' : value
160
+ @include_jst_script = @template_function == DEFAULT_JST_COMPILER
161
+ end
162
+
163
+ # Set the root JS object in which to stash all compiled JST.
164
+ def self.set_template_namespace(value)
165
+ @template_namespace = value == true || value.nil? ? DEFAULT_JST_NAMESPACE : value.to_s
166
+ end
167
+
168
+ # Set the extension for JS templates.
169
+ def self.set_template_extension(value)
170
+ @template_extension = (value == true || value.nil? ? DEFAULT_JST_EXTENSION : value.to_s).gsub(/\A\.?(.*)\Z/, '\1')
171
+ @template_extension_matcher = /\.#{Regexp.escape(@template_extension)}\Z/
172
+ end
173
+
174
+ # The YUI Compressor requires Java > 1.4, and Closure requires Java > 1.6.
175
+ def self.check_java_version
176
+ return true if @checked_java_version
177
+ java = @compressor_options[:java] || 'java'
178
+ @css_compressor_options[:java] ||= java if @compressor_options[:java]
179
+ version = (`#{java} -version 2>&1`)[/\d+\.\d+/]
180
+ disable_compression if !version ||
181
+ (@javascript_compressor == :closure && version < '1.6') ||
182
+ (@javascript_compressor == :yui && version < '1.4')
183
+ @checked_java_version = true
184
+ end
185
+
186
+ # If we don't have a working Java VM, then disable asset compression and
187
+ # complain loudly.
188
+ def self.disable_compression
189
+ @compress_assets = false
190
+ warn("Asset compression disabled -- Java unavailable.")
191
+ end
192
+
193
+ # Ram 0.5+ no longer supports separate template packages.
194
+ def self.check_for_deprecations
195
+ if @configuration[:templates]
196
+ raise DeprecationError, "Ram 0.5+ no longer supports separate packages for templates.\nPlease fold your templates into the appropriate 'javascripts' package instead."
197
+ end
198
+ end
199
+
200
+ def self.warn(message)
201
+ message = "Ram Warning: #{message}"
202
+ $stderr.puts message
203
+ end
204
+
205
+ # Clone of active_support's symbolize_keys, so that we don't have to depend
206
+ # on active_support in any fashion. Converts a hash's keys to all symbols.
207
+ def self.symbolize_keys(hash)
208
+ hash.keys.each do |key|
209
+ hash[(key.to_sym rescue key) || key] = hash.delete(key)
210
+ end
211
+ hash
212
+ end
213
+
214
+ end
215
+
216
+ require 'ram/dependencies'
@@ -0,0 +1,84 @@
1
+ require 'optparse'
2
+ require File.expand_path(File.dirname(__FILE__) + '/../ram')
3
+
4
+ module Ram
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: ram OPTIONS
14
+
15
+ Run ram 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 @Ram::CommandLine@ runs from the contents of @ARGV@.
26
+ def initialize
27
+ parse_options
28
+ ensure_configuration_file
29
+ Ram.package!(@options)
30
+ end
31
+
32
+
33
+ private
34
+
35
+ # Make sure that we have a readable configuration file. The @ram@
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 => Ram::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 Ram version') do
74
+ puts "Ram version #{Ram::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,253 @@
1
+ module Ram
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' => 'font/truetype',
19
+ '.otf' => 'font/opentype',
20
+ '.woff' => '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
+ COMPRESSORS = {
45
+ :yui => YUI::JavaScriptCompressor,
46
+ :closure => Ram.compressors.include?(:closure) ? Closure::Compiler : nil,
47
+ :uglifier => Ram.compressors.include?(:uglifier) ? Ram::Uglifier : nil
48
+ }
49
+
50
+ DEFAULT_OPTIONS = {
51
+ :yui => {:munge => true},
52
+ :closure => {},
53
+ :uglifier => {:copyright => false}
54
+ }
55
+
56
+ # The css compressor is always the YUI Compressor. JS compression can be
57
+ # provided with YUI Compressor, Google Closure Compiler or UglifyJS.
58
+ def initialize
59
+ Ram.check_java_version
60
+ @css_compressor = YUI::CssCompressor.new(Ram.css_compressor_options || {})
61
+ flavor = Ram.javascript_compressor || Ram::DEFAULT_COMPRESSOR
62
+ @options = DEFAULT_OPTIONS[flavor].merge(Ram.compressor_options || {})
63
+ @js_compressor = COMPRESSORS[flavor].new(@options)
64
+ end
65
+
66
+ # Concatenate together a list of JavaScript paths, and pass them through the
67
+ # YUI Compressor (with munging enabled). JST can optionally be included.
68
+ def compress_js(paths)
69
+ if (jst_paths = paths.grep(Ram.template_extension_matcher)).empty?
70
+ js = concatenate(paths)
71
+ else
72
+ js = concatenate(paths - jst_paths) + compile_jst(jst_paths)
73
+ end
74
+ Ram.compress_assets ? @js_compressor.compress(js) : js
75
+ end
76
+
77
+ # Concatenate and compress a list of CSS stylesheets. When compressing a
78
+ # :datauri or :mhtml variant, post-processes the result to embed
79
+ # referenced assets.
80
+ def compress_css(paths, variant=nil, asset_url=nil)
81
+ @asset_contents = {}
82
+ css = concatenate_and_tag_assets(paths, variant)
83
+ css = @css_compressor.compress(css) if Ram.compress_assets
84
+ case variant
85
+ when nil then return css
86
+ when :datauri then return with_data_uris(css)
87
+ when :mhtml then return with_mhtml(css, asset_url)
88
+ else raise PackageNotFound, "\"#{variant}\" is not a valid stylesheet variant"
89
+ end
90
+ end
91
+
92
+ # Compiles a single JST file by writing out a javascript that adds
93
+ # template properties to a top-level template namespace object. Adds a
94
+ # JST-compilation function to the top of the package, unless you've
95
+ # specified your own preferred function, or turned it off.
96
+ # JST templates are named with the basename of their file.
97
+ def compile_jst(paths)
98
+ namespace = Ram.template_namespace
99
+ paths = paths.grep(Ram.template_extension_matcher).sort
100
+ base_path = find_base_path(paths)
101
+ compiled = paths.map do |path|
102
+ contents = read_binary_file(path)
103
+ contents = contents.gsub(/\r?\n/, "\\n").gsub("'", '\\\\\'')
104
+ name = template_name(path, base_path)
105
+ "#{namespace}['#{name}'] = #{Ram.template_function}('#{contents}');"
106
+ end
107
+ compiler = Ram.include_jst_script ? read_binary_file(DEFAULT_JST_SCRIPT) : '';
108
+ setup_namespace = "#{namespace} = #{namespace} || {};"
109
+ [JST_START, setup_namespace, compiler, compiled, JST_END].flatten.join("\n")
110
+ end
111
+
112
+
113
+ private
114
+
115
+ # Given a set of paths, find a common prefix path.
116
+ def find_base_path(paths)
117
+ return nil if paths.length <= 1
118
+ paths.sort!
119
+ first = paths.first.split('/')
120
+ last = paths.last.split('/')
121
+ i = 0
122
+ while first[i] == last[i] && i <= first.length
123
+ i += 1
124
+ end
125
+ res = first.slice(0, i).join('/')
126
+ res.empty? ? nil : res
127
+ end
128
+
129
+ # Determine the name of a JS template. If there's a common base path, use
130
+ # the namespaced prefix. Otherwise, simply use the filename.
131
+ def template_name(path, base_path)
132
+ return File.basename(path, ".#{Ram.template_extension}") unless base_path
133
+ path.gsub(/\A#{Regexp.escape(base_path)}\/(.*)\.#{Ram.template_extension}\Z/, '\1')
134
+ end
135
+
136
+ # In order to support embedded assets from relative paths, we need to
137
+ # expand the paths before contatenating the CSS together and losing the
138
+ # location of the original stylesheet path. Validate the assets while we're
139
+ # at it.
140
+ def concatenate_and_tag_assets(paths, variant=nil)
141
+ stylesheets = [paths].flatten.map do |css_path|
142
+ contents = read_binary_file(css_path)
143
+ contents.gsub(EMBED_DETECTOR) do |url|
144
+ ipath, cpath = Pathname.new($1), Pathname.new(File.expand_path(css_path))
145
+ is_url = URI.parse($1).absolute?
146
+ is_url ? url : "url(#{construct_asset_path(ipath, cpath, variant)})"
147
+ end
148
+ end
149
+ stylesheets.join("\n")
150
+ end
151
+
152
+ # Re-write all enabled asset URLs in a stylesheet with their corresponding
153
+ # Data-URI Base-64 encoded asset contents.
154
+ def with_data_uris(css)
155
+ css.gsub(EMBED_REPLACER) do |url|
156
+ "url(\"data:#{mime_type($1)};charset=utf-8;base64,#{encoded_contents($1)}\")"
157
+ end
158
+ end
159
+
160
+ # Re-write all enabled asset URLs in a stylesheet with the MHTML equivalent.
161
+ # The newlines ("\r\n") in the following method are critical. Without them
162
+ # your MHTML will look identical, but won't work.
163
+ def with_mhtml(css, asset_url)
164
+ paths, index = {}, 0
165
+ css = css.gsub(EMBED_REPLACER) do |url|
166
+ i = paths[$1] ||= "#{index += 1}-#{File.basename($1)}"
167
+ "url(mhtml:#{asset_url}!#{i})"
168
+ end
169
+ mhtml = paths.sort.map do |path, identifier|
170
+ mime, contents = mime_type(path), encoded_contents(path)
171
+ [MHTML_SEPARATOR, "Content-Location: #{identifier}\r\n", "Content-Type: #{mime}\r\n", "Content-Transfer-Encoding: base64\r\n\r\n", contents, "\r\n"]
172
+ end
173
+ [MHTML_START, mhtml, MHTML_END, css].flatten.join('')
174
+ end
175
+
176
+ # Return a rewritten asset URL for a new stylesheet -- the asset should
177
+ # be tagged for embedding if embeddable, and referenced at the correct level
178
+ # if relative.
179
+ def construct_asset_path(asset_path, css_path, variant)
180
+ public_path = absolute_path(asset_path, css_path)
181
+ return "__EMBED__#{public_path}" if embeddable?(public_path, variant)
182
+ source = asset_path.absolute? ? asset_path.to_s : relative_path(public_path)
183
+ rewrite_asset_path(source, public_path)
184
+ end
185
+
186
+ # Get the site-absolute public path for an asset file path that may or may
187
+ # not be relative, given the path of the stylesheet that contains it.
188
+ def absolute_path(asset_pathname, css_pathname)
189
+ (asset_pathname.absolute? ?
190
+ Pathname.new(File.join(Ram.public_root, asset_pathname)) :
191
+ css_pathname.dirname + asset_pathname).cleanpath
192
+ end
193
+
194
+ # CSS assets that are referenced by relative paths, and are *not* being
195
+ # embedded, must be rewritten relative to the newly-merged stylesheet path.
196
+ def relative_path(absolute_path)
197
+ File.join('../', absolute_path.sub(Ram.public_root, ''))
198
+ end
199
+
200
+ # Similar to the AssetTagHelper's method of the same name, this will
201
+ # append the ASSET_ID cache-buster to URLs, if it's defined.
202
+ def rewrite_asset_path(path, file_path)
203
+ asset_id = get_asset_id(file_path)
204
+ (!asset_id || asset_id == '') ? path : "#{path}?#{asset_id}"
205
+ end
206
+
207
+ # Similar to the AssetTagHelper's method of the same name, this will
208
+ # determine the correct asset id for a file.
209
+ def get_asset_id(path)
210
+ asset_id = ENV["ASSET_ID"]
211
+ return asset_id if asset_id
212
+ File.exists?(path) ? File.mtime(path).to_i.to_s : ''
213
+ end
214
+
215
+ # An asset is valid for embedding if it exists, is less than 32K, and is
216
+ # stored somewhere inside of a folder named "embed". IE does not support
217
+ # Data-URIs larger than 32K, and you probably shouldn't be embedding assets
218
+ # that large in any case. Because we need to check the base64 length here,
219
+ # save it so that we don't have to compute it again later.
220
+ def embeddable?(asset_path, variant)
221
+ font = EMBED_FONTS.include?(asset_path.extname)
222
+ return false unless variant
223
+ return false unless asset_path.to_s.match(EMBEDDABLE) && asset_path.exist?
224
+ return false unless EMBED_EXTS.include?(asset_path.extname)
225
+ return false unless font || encoded_contents(asset_path).length < MAX_IMAGE_SIZE
226
+ return false if font && variant == :mhtml
227
+ return true
228
+ end
229
+
230
+ # Return the Base64-encoded contents of an asset on a single line.
231
+ def encoded_contents(asset_path)
232
+ return @asset_contents[asset_path] if @asset_contents[asset_path]
233
+ data = read_binary_file(asset_path)
234
+ @asset_contents[asset_path] = Base64.encode64(data).gsub(/\n/, '')
235
+ end
236
+
237
+ # Grab the mime-type of an asset, by filename.
238
+ def mime_type(asset_path)
239
+ EMBED_MIME_TYPES[File.extname(asset_path)]
240
+ end
241
+
242
+ # Concatenate together a list of asset files.
243
+ def concatenate(paths)
244
+ [paths].flatten.map {|p| read_binary_file(p) }.join("\n")
245
+ end
246
+
247
+ # `File.read`, but in "binary" mode.
248
+ def read_binary_file(path)
249
+ File.open(path, 'rb:UTF-8') {|f| f.read }
250
+ end
251
+ end
252
+
253
+ end
@@ -0,0 +1,35 @@
1
+ # Standard Library Dependencies:
2
+ require 'uri'
3
+ require 'erb'
4
+ require 'zlib'
5
+ require 'yaml'
6
+ require 'base64'
7
+ require 'pathname'
8
+ require 'fileutils'
9
+
10
+ # Include YUI as the default
11
+ require 'yui/compressor'
12
+
13
+ # Try Closure.
14
+ begin
15
+ require 'closure-compiler'
16
+ rescue LoadError
17
+ Ram.compressors.delete :closure
18
+ end
19
+
20
+ # Try Uglifier.
21
+ begin
22
+ require 'uglifier'
23
+ rescue LoadError
24
+ Ram.compressors.delete :uglifier
25
+ end
26
+
27
+ # Load initial configuration before the rest of Ram.
28
+ Ram.load_configuration(Ram::DEFAULT_CONFIG_PATH, true)
29
+
30
+ # Ram Core:
31
+ require 'ram/uglifier' if Ram.compressors.include? :uglifier
32
+ require 'ram/compressor'
33
+ require 'ram/packager'
34
+
35
+ # TODO: See if the helper might be useful in a nanoc environment...
@@ -0,0 +1,87 @@
1
+ # module Ram
2
+ #
3
+ # # The Ram::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)]><!-->" unless defined?(DATA_URI_START)
10
+ # DATA_URI_END = "<!--<![endif]-->" unless defined?(DATA_URI_END)
11
+ # MHTML_START = "<!--[if lte IE 7]>" unless defined?(MHTML_START)
12
+ # MHTML_END = "<![endif]-->" unless defined?(MHTML_END)
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 html_safe(individual_stylesheets(packages, options)) unless should_package?
20
+ # disabled = (options.delete(:embed_assets) == false) || (options.delete(:embed_images) == false)
21
+ # return html_safe(packaged_stylesheets(packages, options)) if disabled || !Ram.embed_assets
22
+ # return html_safe(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
+ # html_safe packages.map {|pack|
29
+ # should_package? ? Ram.asset_url(pack, :js) : Ram.packager.individual_urls(pack.to_sym, :js)
30
+ # }.flatten.map {|pack|
31
+ # javascript_include_tag pack
32
+ # }.join("\n")
33
+ # end
34
+ #
35
+ # # Writes out the URL to the concatenated and compiled JST file -- we always
36
+ # # have to pre-process it, even in development.
37
+ # def include_templates(*packages)
38
+ # raise DeprecationError, "Ram 0.5+ no longer supports separate packages for templates.\nYou can include your JST alongside your JS, and use include_javascripts."
39
+ # end
40
+ #
41
+ #
42
+ # private
43
+ #
44
+ # def should_package?
45
+ # Ram.package_assets && !(Ram.allow_debugging && params[:debug_assets])
46
+ # end
47
+ #
48
+ # def html_safe(string)
49
+ # string.respond_to?(:html_safe) ? string.html_safe : string
50
+ # end
51
+ #
52
+ # # HTML tags, in order, for all of the individual stylesheets.
53
+ # def individual_stylesheets(packages, options)
54
+ # tags_with_options(packages, options) {|p| Ram.packager.individual_urls(p.to_sym, :css) }
55
+ # end
56
+ #
57
+ # # HTML tags for the stylesheet packages.
58
+ # def packaged_stylesheets(packages, options)
59
+ # tags_with_options(packages, options) {|p| Ram.asset_url(p, :css) }
60
+ # end
61
+ #
62
+ # # HTML tags for the 'datauri', and 'mhtml' versions of the packaged
63
+ # # stylesheets, using conditional comments to load the correct variant.
64
+ # def embedded_image_stylesheets(packages, options)
65
+ # datauri_tags = tags_with_options(packages, options) {|p| Ram.asset_url(p, :css, :datauri) }
66
+ # ie_tags = Ram.mhtml_enabled ?
67
+ # tags_with_options(packages, options) {|p| Ram.asset_url(p, :css, :mhtml) } :
68
+ # packaged_stylesheets(packages, options)
69
+ # [DATA_URI_START, datauri_tags, DATA_URI_END, MHTML_START, ie_tags, MHTML_END].join("\n")
70
+ # end
71
+ #
72
+ # # Generate the stylesheet tags for a batch of packages, with options, by
73
+ # # yielding each package to a block.
74
+ # def tags_with_options(packages, options)
75
+ # packages.dup.map {|package|
76
+ # yield package
77
+ # }.flatten.map {|package|
78
+ # stylesheet_link_tag package, options
79
+ # }.join("\n")
80
+ # end
81
+ #
82
+ # end
83
+ #
84
+ # end
85
+ #
86
+ # # Include the Ram asset helpers in all views, a-la ApplicationHelper.
87
+ # ::ActionView::Base.send(:include, Ram::Helper)
@@ -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(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/<%=([\s\S]+?)%>/g,function(match,code){return "',"+code.replace(/\\'/g, "'")+",'";}).replace(/<%([\s\S]+?)%>/g,function(match,code){return "');"+code.replace(/\\'/g, "'").replace(/[\r\n\t]/g,' ')+"__p.push('";}).replace(/\r/g,'\\r').replace(/\n/g,'\\n').replace(/\t/g,'\\t')+"');}return __p.join('');");return fn;};
@@ -0,0 +1,173 @@
1
+ module Ram
2
+
3
+ # The Ram::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
+
10
+ # Set force to false to allow packages to only be rebuilt when their source
11
+ # files have changed since the last time their package was built.
12
+ attr_accessor :force, :package_names
13
+
14
+ # Creating a new Packager will rebuild the list of assets from the
15
+ # Ram.configuration. When assets.yml is being changed on the fly,
16
+ # create a new Packager.
17
+ def initialize
18
+ # The difference between a path and an asset URL is "public".
19
+ @path_diff = Ram.public_root.sub(ASSET_ROOT, '')
20
+ @path_to_url = /\A#{Regexp.escape(ASSET_ROOT)}(\/?#{Regexp.escape(@path_diff)})?/
21
+
22
+ @compressor = Compressor.new
23
+ @force = false
24
+ @package_names = nil
25
+ @config = {
26
+ :css => (Ram.configuration[:stylesheets] || {}),
27
+ :js => (Ram.configuration[:javascripts] || {})
28
+ }
29
+ @packages = {
30
+ :css => create_packages(@config[:css]),
31
+ :js => create_packages(@config[:js])
32
+ }
33
+ end
34
+
35
+ # Ask the packager to precache all defined assets, along with their gzip'd
36
+ # versions. In order to prebuild the MHTML stylesheets, we need to know the
37
+ # base_url, because IE only supports MHTML with absolute references.
38
+ # Unless forced, will only rebuild assets whose source files have been
39
+ # changed since their last package build.
40
+ def precache_all(output_dir=nil, base_url=nil)
41
+ output_dir ||= File.join(Ram.public_root, Ram.package_path)
42
+ cacheable(:js, output_dir).each {|p| cache(p, 'js', pack_javascripts(p), output_dir) }
43
+ cacheable(:css, output_dir).each do |p|
44
+ cache(p, 'css', pack_stylesheets(p), output_dir)
45
+ if Ram.embed_assets
46
+ cache(p, 'css', pack_stylesheets(p, :datauri), output_dir, :datauri)
47
+ if Ram.mhtml_enabled
48
+ raise MissingConfiguration, "A --base-url option is required in order to generate MHTML." unless base_url
49
+ mtime = latest_mtime package_for(p, :css)[:paths]
50
+ asset_url = "#{base_url}#{Ram.asset_url(p, :css, :mhtml, mtime)}"
51
+ cache(p, 'css', pack_stylesheets(p, :mhtml, asset_url), output_dir, :mhtml, mtime)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # Caches a single prebuilt asset package and gzips it at the highest
58
+ # compression level. Ensures that the modification time of both both
59
+ # variants is identical, for web server caching modules, as well as MHTML.
60
+ def cache(package, extension, contents, output_dir, suffix=nil, mtime=nil)
61
+ FileUtils.mkdir_p(output_dir) unless File.exists?(output_dir)
62
+ raise OutputNotWritable, "Ram doesn't have permission to write to \"#{output_dir}\"" unless File.writable?(output_dir)
63
+ mtime ||= latest_mtime package_for(package, extension.to_sym)[:paths]
64
+ files = []
65
+ files << file_name = File.join(output_dir, Ram.filename(package, extension, suffix))
66
+ File.open(file_name, 'wb+') {|f| f.write(contents) }
67
+ if Ram.gzip_assets
68
+ files << zip_name = "#{file_name}.gz"
69
+ Zlib::GzipWriter.open(zip_name, Zlib::BEST_COMPRESSION) {|f| f.write(contents) }
70
+ end
71
+ File.utime(mtime, mtime, *files)
72
+ end
73
+
74
+ # Get the list of individual assets for a package.
75
+ def individual_urls(package, extension)
76
+ package_for(package, extension)[:urls]
77
+ end
78
+
79
+ # Return the compressed contents of a stylesheet package.
80
+ def pack_stylesheets(package, variant=nil, asset_url=nil)
81
+ @compressor.compress_css(package_for(package, :css)[:paths], variant, asset_url)
82
+ end
83
+
84
+ # Return the compressed contents of a javascript package.
85
+ def pack_javascripts(package)
86
+ @compressor.compress_js(package_for(package, :js)[:paths])
87
+ end
88
+
89
+ # Return the compiled contents of a JST package.
90
+ def pack_templates(package)
91
+ @compressor.compile_jst(package_for(package, :js)[:paths])
92
+ end
93
+
94
+ private
95
+
96
+ # Look up a package asset list by name, raising an exception if the
97
+ # package has gone missing.
98
+ def package_for(package, extension)
99
+ pack = @packages[extension] && @packages[extension][package]
100
+ pack || not_found(package, extension)
101
+ end
102
+
103
+ # Absolute globs are absolute -- relative globs are relative to ASSET_ROOT.
104
+ # Print a warning if no files were found that match the glob.
105
+ def glob_files(glob)
106
+ absolute = Pathname.new(glob).absolute?
107
+ paths = Dir[absolute ? glob : File.join(ASSET_ROOT, glob)].sort
108
+ Ram.warn("No assets match '#{glob}'") if paths.empty?
109
+ paths
110
+ end
111
+
112
+ # Get the latest mtime of a list of files (plus the config path).
113
+ def latest_mtime(paths)
114
+ paths += [Ram.config_path]
115
+ paths.map {|p| File.mtime(p) }.max || Time.now
116
+ end
117
+
118
+ # Return a list of all of the packages that should be cached. If "force" is
119
+ # true, this is all of them -- otherwise only the packages that are missing
120
+ # or whose source files have changed since the last package build.
121
+ def cacheable(extension, output_dir)
122
+ names = @packages[extension].keys
123
+ names = names.select {|n| @package_names.include? n } if @package_names
124
+ config_mtime = File.mtime(Ram.config_path)
125
+ return names if @force
126
+ return names.select do |name|
127
+ pack = package_for(name, extension)
128
+ cached = [Ram.filename(name, extension)]
129
+ cached.push Ram.filename(name, extension, :datauri) if Ram.embed_assets
130
+ cached.push Ram.filename(name, extension, :mhtml) if Ram.mhtml_enabled
131
+ cached.map! {|file| File.join(output_dir, file) }
132
+ if cached.any? {|file| !File.exists?(file) }
133
+ true
134
+ else
135
+ since = cached.map {|file| File.mtime(file) }.min
136
+ config_mtime > since || pack[:paths].any? {|src| File.mtime(src) > since }
137
+ end
138
+ end
139
+ end
140
+
141
+ # Compiles the list of assets that goes into each package. Runs an
142
+ # ordered list of Dir.globs, taking the merged unique result.
143
+ # If there are JST files in this package we need to add an extra
144
+ # path for when package_assets is off (e.g. in a dev environment).
145
+ # This package (e.g. /assets/package-name.jst) will never exist as
146
+ # an actual file but will be dynamically generated by Ram on
147
+ # every request.
148
+ def create_packages(config)
149
+ packages = {}
150
+ return packages if !config
151
+ config.each do |name, globs|
152
+ globs ||= []
153
+ packages[name] = {}
154
+ paths = globs.flatten.uniq.map {|glob| glob_files(glob) }.flatten.uniq
155
+ packages[name][:paths] = paths
156
+ if !paths.grep(Ram.template_extension_matcher).empty?
157
+ packages[name][:urls] = paths.grep(JS_EXTENSION).map {|path| path.sub(@path_to_url, '') }
158
+ packages[name][:urls] += [Ram.asset_url(name, Ram.template_extension)]
159
+ else
160
+ packages[name][:urls] = paths.map {|path| path.sub(@path_to_url, '') }
161
+ end
162
+ end
163
+ packages
164
+ end
165
+
166
+ # Raise a PackageNotFound exception for missing packages...
167
+ def not_found(package, extension)
168
+ raise PackageNotFound, "assets.yml does not contain a \"#{package}\" #{extension.to_s.upcase} package"
169
+ end
170
+
171
+ end
172
+
173
+ end
@@ -0,0 +1,3 @@
1
+ class Ram::Uglifier < ::Uglifier
2
+ alias :compress :compile
3
+ end
@@ -0,0 +1,33 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'ram'
3
+ s.version = '0.0.1' # Keep version in sync with ram.rb
4
+ s.date = '2011-10-25'
5
+
6
+ s.homepage = "http://fleetventures.com/"
7
+ s.summary = "Asset Packaging"
8
+ s.description = <<-EOS
9
+ Ram is an asset packaging library for any type of web project. It was
10
+ originalyl adapted from the excellent Jammit gem, but without any Rails
11
+ dependencies or tie ins. This provides both the CSS and JavaScript
12
+ concatenation and compression that you'd expect, as well as YUI Compressor
13
+ and Closure Compiler compatibility, ahead-of-time gzipping, built-in
14
+ JavaScript template support, and optional Data-URI / MHTML image embedding.
15
+ EOS
16
+
17
+ s.authors = ['David Richards']
18
+ s.email = 'david@fleetventures.com'
19
+ s.rubyforge_project = 'ram'
20
+
21
+ s.require_paths = ['lib']
22
+ s.executables = ['ram']
23
+
24
+ s.extra_rdoc_files = ['README.md']
25
+ s.rdoc_options << '--title' << 'Ram' <<
26
+ '--exclude' << 'test' <<
27
+ '--main' << 'README.md' <<
28
+ '--all'
29
+
30
+ s.add_dependency 'yui-compressor', ['>= 0.9.3']
31
+
32
+ s.files = Dir['lib/**/*', 'bin/*', 'ram.gemspec', 'LICENSE', 'README.md']
33
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ram
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Richards
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-25 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: yui-compressor
16
+ requirement: &2153929620 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2153929620
25
+ description: ! " Ram is an asset packaging library for any type of web project.
26
+ \ It was \n originalyl adapted from the excellent Jammit gem, but without any
27
+ Rails\n dependencies or tie ins. This provides both the CSS and JavaScript
28
+ \n concatenation and compression that you'd expect, as well as YUI Compressor
29
+ \n and Closure Compiler compatibility, ahead-of-time gzipping, built-in \n JavaScript
30
+ template support, and optional Data-URI / MHTML image embedding.\n"
31
+ email: david@fleetventures.com
32
+ executables:
33
+ - ram
34
+ extensions: []
35
+ extra_rdoc_files:
36
+ - README.md
37
+ files:
38
+ - lib/ram/command_line.rb
39
+ - lib/ram/compressor.rb
40
+ - lib/ram/dependencies.rb
41
+ - lib/ram/helper.rb
42
+ - lib/ram/jst.js
43
+ - lib/ram/packager.rb
44
+ - lib/ram/uglifier.rb
45
+ - lib/ram.rb
46
+ - bin/ram
47
+ - ram.gemspec
48
+ - LICENSE
49
+ - README.md
50
+ homepage: http://fleetventures.com/
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --title
55
+ - Ram
56
+ - --exclude
57
+ - test
58
+ - --main
59
+ - README.md
60
+ - --all
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project: ram
77
+ rubygems_version: 1.8.10
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: Asset Packaging
81
+ test_files: []
82
+ has_rdoc: