ramaze-asset 0.2

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