ramaze-asset 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Ramaze
2
+ module Asset
3
+ class AssetError < StandardError; end
4
+ end # Asset
5
+ end # Ramaze
@@ -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