ramaze-asset 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gems +6 -0
- data/.gitignore +14 -0
- data/.rvmrc +1 -0
- data/LICENSE +19 -0
- data/README.md +64 -0
- data/Rakefile +15 -0
- data/example.rb +54 -0
- data/lib/.gitkeep +0 -0
- data/lib/ramaze/.gitkeep +0 -0
- data/lib/ramaze/asset.rb +15 -0
- data/lib/ramaze/asset/css.rb +44 -0
- data/lib/ramaze/asset/environment.rb +482 -0
- data/lib/ramaze/asset/error.rb +5 -0
- data/lib/ramaze/asset/file_group.rb +385 -0
- data/lib/ramaze/asset/javascript.rb +41 -0
- data/lib/ramaze/asset/spec/bacon/color_output.rb +39 -0
- data/lib/ramaze/asset/spec/helper.rb +18 -0
- data/lib/ramaze/asset/version.rb +5 -0
- data/lib/vendor/cssmin.rb +110 -0
- data/lib/vendor/jsmin.rb +280 -0
- data/pkg/.gitkeep +0 -0
- data/ramaze-asset.gemspec +24 -0
- data/spec/.gitkeep +0 -0
- data/spec/fixtures/public/css/github.css +126 -0
- data/spec/fixtures/public/css/reset.css +123 -0
- data/spec/fixtures/public/js/mootools_core.js +5515 -0
- data/spec/fixtures/public/js/mootools_more.js +13455 -0
- data/spec/helper.rb +4 -0
- data/spec/ramaze_asset/css.rb +69 -0
- data/spec/ramaze_asset/environment.rb +148 -0
- data/spec/ramaze_asset/file_group.rb +118 -0
- data/spec/ramaze_asset/javascript.rb +69 -0
- data/task/build.rake +35 -0
- data/task/clean.rake +6 -0
- data/task/test.rake +8 -0
- metadata +148 -0
@@ -0,0 +1,385 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'ramaze/gestalt'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module Ramaze
|
7
|
+
module Asset
|
8
|
+
##
|
9
|
+
# Ramaze::Asset::FileGroup is used to group a set of files of the same type,
|
10
|
+
# such as Javascript files, together. The HTML for these files can be
|
11
|
+
# generated as well as a minified version of all the files.
|
12
|
+
#
|
13
|
+
# ## Creating File Groups
|
14
|
+
#
|
15
|
+
# Ramaze::Asset comes with two file groups that are capable of processing
|
16
|
+
# Javascript and CSS files. If you need to have a file group for
|
17
|
+
# Coffeescript, Less or other files is quite easy to add these yourself.
|
18
|
+
# First you must create a class that extends Ramaze::Asset::FileGroup:
|
19
|
+
#
|
20
|
+
# class Less < Ramaze::Asset::FileGroup
|
21
|
+
#
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# It's important that you define the correct file extensions for your file
|
25
|
+
# group. Without this Ramaze::Asset will not be able to find the files for
|
26
|
+
# you (unless they have an extension specified) and the minified file will
|
27
|
+
# end up not having an extension. Setting an extension can be done by
|
28
|
+
# calling the class method ``extension()``. This method has two parameters,
|
29
|
+
# the first one is the extension of the source file, the second the
|
30
|
+
# extension for the minified file. In case of Less this would result in the
|
31
|
+
# following:
|
32
|
+
#
|
33
|
+
# class Less < Ramaze::Asset::FileGroup
|
34
|
+
# extension '.less', '.css'
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# The next step is to define your own minify() and html_tag() methods. If
|
38
|
+
# you don't define these methods they'll raise an error and most likely
|
39
|
+
# break things.
|
40
|
+
#
|
41
|
+
# class Less < Ramaze::Asset::FileGroup
|
42
|
+
# extension '.less', '.css'
|
43
|
+
#
|
44
|
+
# def minify(input)
|
45
|
+
#
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def html_tag(gestalt, path)
|
49
|
+
#
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# The minify() method should return a string containing the minified data.
|
54
|
+
# The html_tag() method uses an instance of Ramaze::Gestalt to build a
|
55
|
+
# single tag for a given (relative) path.
|
56
|
+
#
|
57
|
+
# A full example of Less looks like the following:
|
58
|
+
#
|
59
|
+
# require 'tempfile'
|
60
|
+
#
|
61
|
+
# class Less < Ramaze::Asset::FileGroup
|
62
|
+
# extension '.less', '.css'
|
63
|
+
#
|
64
|
+
# # +input+ contains the raw Less data. The command line tool only
|
65
|
+
# # accepts files so this data has to be written to a temp file.
|
66
|
+
# def minify(input)
|
67
|
+
# file = Tempfile.new('less')
|
68
|
+
# file.write(input)
|
69
|
+
# file.rewind
|
70
|
+
#
|
71
|
+
# minified = `lessc #{file.path} -x`
|
72
|
+
#
|
73
|
+
# file.close(true)
|
74
|
+
#
|
75
|
+
# return minified
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# def html_tag(gestalt, path)
|
79
|
+
# gestalt.link(
|
80
|
+
# :rel => 'stylesheet',
|
81
|
+
# :href => path,
|
82
|
+
# :type => 'text/css'
|
83
|
+
# )
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# Note that it's important to remember that when dealing with files that
|
88
|
+
# have to be compiled, such as Less and Coffeescript files, setting :minify
|
89
|
+
# to false will not work. Without setting this option to true the minify()
|
90
|
+
# method will never be called and thus the raw Less/Coffeescript file would
|
91
|
+
# be served.
|
92
|
+
#
|
93
|
+
# @author Yorick Peterse
|
94
|
+
# @since 0.1
|
95
|
+
#
|
96
|
+
class FileGroup
|
97
|
+
# Array containing all the files that belong to this group, including
|
98
|
+
# their files extensions.
|
99
|
+
attr_accessor :files
|
100
|
+
|
101
|
+
# Hash containing all the options for the file group.
|
102
|
+
attr_reader :options
|
103
|
+
|
104
|
+
##
|
105
|
+
# Sets the file extensions for the current class. These extensions should
|
106
|
+
# start with a dot. If the minified extension is not specified it will be
|
107
|
+
# generated by adding a ".min" suffix followed by the source extension. If
|
108
|
+
# the source extension is ".css" the minified extension would in this case
|
109
|
+
# be ".min.css".
|
110
|
+
#
|
111
|
+
# @author Yorick Peterse
|
112
|
+
# @since 0.1
|
113
|
+
# @param [#to_s] source_ext The extension of the source file such as
|
114
|
+
# ".css" or ".js".
|
115
|
+
# @param [#to_s] minified_ext The extension to use for the minified file.
|
116
|
+
# Useful when the resulting extension is different than the source
|
117
|
+
# extension (such as with Less or Coffeescript).
|
118
|
+
#
|
119
|
+
def self.extension(source_ext, minified_ext = nil)
|
120
|
+
if minified_ext.nil?
|
121
|
+
minified_ext = '.min' + source_ext
|
122
|
+
end
|
123
|
+
|
124
|
+
if source_ext[0] != '.' or minified_ext[0] != '.'
|
125
|
+
raise(
|
126
|
+
Ramaze::Asset::AssetError,
|
127
|
+
'Extensions should start with a dot'
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
self.instance_variable_set(
|
132
|
+
:@extension,
|
133
|
+
{:source => source_ext, :minified => minified_ext}
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Creates a new instance of the file group and prepares it.
|
139
|
+
#
|
140
|
+
# @author Yorick Peterse
|
141
|
+
# @since 0.1
|
142
|
+
# @param [Array] files An array of files for this group.
|
143
|
+
# @param [Hash] options A hash containing various options to customize
|
144
|
+
# this file group.
|
145
|
+
# @option options :minify When set to true all the files in the group will
|
146
|
+
# be minified.
|
147
|
+
# @option options :name A name to use for the minified file. By default
|
148
|
+
# this is set to a hash of all the file names.
|
149
|
+
# @option options :paths An array of file paths to look for the files.
|
150
|
+
# @option options :cache_path The path to a directory where the minified
|
151
|
+
# files should be saved.
|
152
|
+
#
|
153
|
+
def initialize(files, options = {})
|
154
|
+
@minified = false
|
155
|
+
@files = files
|
156
|
+
@options = {
|
157
|
+
:minify => false,
|
158
|
+
:name => nil,
|
159
|
+
:paths => [],
|
160
|
+
:cache_path => []
|
161
|
+
}.merge(options)
|
162
|
+
|
163
|
+
if @options[:paths].empty?
|
164
|
+
raise(
|
165
|
+
Ramaze::Asset::AssetError,
|
166
|
+
'No public directories were specified'
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
if !File.directory?(@options[:cache_path])
|
171
|
+
raise(
|
172
|
+
Ramaze::Asset::AssetError,
|
173
|
+
"The directory #{@options[:cache_path]} does not exist"
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
if extension.nil?
|
178
|
+
raise(
|
179
|
+
Ramaze::Asset::AssetError,
|
180
|
+
'You need to specify an extension'
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
prepare_files
|
185
|
+
|
186
|
+
# When :minify is set :name should also be set.
|
187
|
+
if @options[:minify] === true and @options[:name].nil?
|
188
|
+
@options[:name] = @files.map { |file| file }.join()
|
189
|
+
@options[:name] = Digest::SHA1.new.hexdigest(@options[:name])
|
190
|
+
end
|
191
|
+
|
192
|
+
if !@options[:name].nil?
|
193
|
+
@options[:name] += extension[:minified]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# Returns the extension of the current file group.
|
199
|
+
#
|
200
|
+
# @author Yorick Peterse
|
201
|
+
# @since 0.1.
|
202
|
+
# @return [String]
|
203
|
+
#
|
204
|
+
def extension
|
205
|
+
return self.class.instance_variable_get(:@extension)
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# When the :minify option is set to true this method will merge all files,
|
210
|
+
# minify them and cache them in the :cache_path directory.
|
211
|
+
#
|
212
|
+
# @author Yorick Peterse
|
213
|
+
# @since 0.1
|
214
|
+
#
|
215
|
+
def build
|
216
|
+
return if @options[:minify] != true
|
217
|
+
|
218
|
+
cache_path = File.join(
|
219
|
+
@options[:cache_path],
|
220
|
+
@options[:name]
|
221
|
+
)
|
222
|
+
|
223
|
+
# Minify the file in a sub process so that memory leaks (or just general
|
224
|
+
# increases of memory usage) don't affect the master process.
|
225
|
+
pid = Process.fork do
|
226
|
+
processed = []
|
227
|
+
file_paths = []
|
228
|
+
minified = ''
|
229
|
+
write = true
|
230
|
+
|
231
|
+
# Try to find the paths to the files.
|
232
|
+
@options[:paths].each do |directory|
|
233
|
+
@files.each do |file|
|
234
|
+
path = File.join(directory, file)
|
235
|
+
|
236
|
+
# Only add the file to the list if it hasn't already been added.
|
237
|
+
if File.exist?(path) and !processed.include?(file)
|
238
|
+
file_paths.push(path)
|
239
|
+
processed.push(file)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
file_paths.each do |file|
|
245
|
+
minified += minify(File.read(file, File.size(file)))
|
246
|
+
end
|
247
|
+
|
248
|
+
# Check if the file already exists. If this is the cache a hash of
|
249
|
+
# both files is generated and compared. If it's different the file has
|
250
|
+
# to be re-created.
|
251
|
+
if File.exist?(cache_path)
|
252
|
+
old_hash = Digest::SHA1.new.hexdigest(minified)
|
253
|
+
new_hash = Digest::SHA1.new.hexdigest(
|
254
|
+
File.read(cache_path, File.size(cache_path))
|
255
|
+
)
|
256
|
+
|
257
|
+
if old_hash === new_hash
|
258
|
+
write = false
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
if write === true
|
263
|
+
File.open(cache_path, 'w') do |handle|
|
264
|
+
handle.write(minified)
|
265
|
+
handle.close
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Don't call any at_exit() hooks, they're not needed in this process.
|
270
|
+
Kernel.exit!
|
271
|
+
end
|
272
|
+
|
273
|
+
Process.waitpid(pid)
|
274
|
+
|
275
|
+
# Make sure the cache file is present
|
276
|
+
if !File.size?(cache_path)
|
277
|
+
raise(
|
278
|
+
Ramaze::Asset::AssetError,
|
279
|
+
"The cache file #{cache_path} could not be created"
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
@minified = true
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# Builds the HTML tags for all the current files.
|
288
|
+
#
|
289
|
+
# @author Yorick Peterse
|
290
|
+
# @since 0.1
|
291
|
+
# @return [String]
|
292
|
+
#
|
293
|
+
def build_html
|
294
|
+
prefix = '/'
|
295
|
+
|
296
|
+
if @options[:minify] === true and @minified === true
|
297
|
+
# Get the relative path to the cache directory from one of the public
|
298
|
+
# directories.
|
299
|
+
@options[:paths].each do |path|
|
300
|
+
path = @options[:cache_path].gsub(path, '')
|
301
|
+
|
302
|
+
if path != @options[:cache_path] and !path.empty?
|
303
|
+
prefix += path
|
304
|
+
break
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
files = [('/' + @options[:name]).squeeze('/')]
|
309
|
+
else
|
310
|
+
files = @files
|
311
|
+
end
|
312
|
+
|
313
|
+
# Let's make sure the URLs are actually pointing to the cached
|
314
|
+
# directory.
|
315
|
+
files.each_with_index do |file, index|
|
316
|
+
files[index] = "#{prefix}/#{file}".squeeze('/')
|
317
|
+
end
|
318
|
+
|
319
|
+
g = Ramaze::Gestalt.new
|
320
|
+
|
321
|
+
files.each { |file| html_tag(g, file) }
|
322
|
+
|
323
|
+
return g.to_s
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# Minifies a single file.
|
328
|
+
#
|
329
|
+
# @author Yorick Peterse
|
330
|
+
# @since 0.1
|
331
|
+
# @param [String] input The string to minify.
|
332
|
+
# @raise NotImplementedError Raised when the sub class didn't implement
|
333
|
+
# this method.
|
334
|
+
#
|
335
|
+
def minify(input)
|
336
|
+
raise(
|
337
|
+
NotImplementedError,
|
338
|
+
'You need to define your own minify() instance method'
|
339
|
+
)
|
340
|
+
end
|
341
|
+
|
342
|
+
##
|
343
|
+
# Builds the HTML tag for a single file using Ramaze::Gestalt.
|
344
|
+
#
|
345
|
+
# @author Yorick Peterse
|
346
|
+
# @since 0.1
|
347
|
+
# @param [Ramaze::Gestalt] gestalt An instance of Ramaze::Gestalt that's
|
348
|
+
# used to build all the tags.
|
349
|
+
# @param [String] path The relative path to the file.
|
350
|
+
# @raise NotImplementedError Raised when the sub class didn't implement
|
351
|
+
# this method.
|
352
|
+
#
|
353
|
+
def html_tag(gestalt, path)
|
354
|
+
raise(
|
355
|
+
NotImplementedError,
|
356
|
+
'You need to define your own build_html instance method'
|
357
|
+
)
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
##
|
363
|
+
# Loops through all the files and adds the required extensions to them and
|
364
|
+
# makes sure they're relative to the root rather than the current working
|
365
|
+
# directory.
|
366
|
+
#
|
367
|
+
# @author Yorick Peterse
|
368
|
+
# @since 0.1
|
369
|
+
#
|
370
|
+
def prepare_files
|
371
|
+
@files.each_with_index do |file, index|
|
372
|
+
file += extension[:source] if File.extname(file) != extension[:source]
|
373
|
+
|
374
|
+
if file[0] != '/'
|
375
|
+
file = '/' + file
|
376
|
+
end
|
377
|
+
|
378
|
+
file = file.squeeze('/')
|
379
|
+
|
380
|
+
@files[index] = file
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end # FileGroup
|
384
|
+
end # Asset
|
385
|
+
end # Ramaze
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require __DIR__('../../vendor/jsmin')
|
2
|
+
|
3
|
+
module Ramaze
|
4
|
+
module Asset
|
5
|
+
##
|
6
|
+
# File group for Javascript files. These Javascript files are minified using
|
7
|
+
# JSMin.
|
8
|
+
#
|
9
|
+
# @author Yorick Peterse
|
10
|
+
# @since 0.1
|
11
|
+
#
|
12
|
+
class Javascript < Ramaze::Asset::FileGroup
|
13
|
+
extension '.js'
|
14
|
+
|
15
|
+
##
|
16
|
+
# Minifies the output and returns the result as a string.
|
17
|
+
#
|
18
|
+
# @author Yorick Peterse
|
19
|
+
# @since 0.1
|
20
|
+
# @param [String] input The input to minify.
|
21
|
+
# @return [String]
|
22
|
+
#
|
23
|
+
def minify(input)
|
24
|
+
return JSMin.minify(input)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Builds a ``<script>`` tag for a single Javascript file.
|
29
|
+
#
|
30
|
+
# @author Yorick Peterse
|
31
|
+
# @since 0.1
|
32
|
+
# @param [Ramaze::Gestalt] gestalt An instance of Ramaze::Gestalt used to
|
33
|
+
# build the tags.
|
34
|
+
# @param [String] path The relative path to the file for the tag.
|
35
|
+
#
|
36
|
+
def html_tag(gestalt, path)
|
37
|
+
gestalt.script(:src => path, :type => 'text/javascript') {}
|
38
|
+
end
|
39
|
+
end # Javascript
|
40
|
+
end # Asset
|
41
|
+
end # Ramaze
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#:nodoc:
|
2
|
+
module Bacon
|
3
|
+
#:nodoc:
|
4
|
+
module ColorOutput
|
5
|
+
#:nodoc:
|
6
|
+
def handle_specification(name)
|
7
|
+
puts spaces + name
|
8
|
+
yield
|
9
|
+
puts if Counter[:context_depth] == 1
|
10
|
+
end
|
11
|
+
|
12
|
+
#:nodoc:
|
13
|
+
def handle_requirement(description)
|
14
|
+
error = yield
|
15
|
+
|
16
|
+
if !error.empty?
|
17
|
+
puts "#{spaces} \e[31m- #{description} [FAILED]\e[0m"
|
18
|
+
else
|
19
|
+
puts "#{spaces} \e[32m- #{description}\e[0m"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#:nodoc:
|
24
|
+
def handle_summary
|
25
|
+
print ErrorLog if Backtraces
|
26
|
+
puts "%d specifications (%d requirements), %d failures, %d errors" %
|
27
|
+
Counter.values_at(:specifications, :requirements, :failed, :errors)
|
28
|
+
end
|
29
|
+
|
30
|
+
#:nodoc:
|
31
|
+
def spaces
|
32
|
+
if Counter[:context_depth] === 0
|
33
|
+
Counter[:context_depth] = 1
|
34
|
+
end
|
35
|
+
|
36
|
+
return ' ' * (Counter[:context_depth] - 1)
|
37
|
+
end
|
38
|
+
end # ColorOutput
|
39
|
+
end # Bacon
|