sinatra-assetpack-flexible-compression 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +2 -0
  3. data/HISTORY.md +131 -0
  4. data/README.md +664 -0
  5. data/Rakefile +31 -0
  6. data/docsrc/style.css +32 -0
  7. data/examples/basic/.gitignore +1 -0
  8. data/examples/basic/Rakefile +7 -0
  9. data/examples/basic/app.rb +39 -0
  10. data/examples/basic/app/css/test.sass +11 -0
  11. data/examples/basic/app/images/icon.png +0 -0
  12. data/examples/basic/app/js/app.js +4 -0
  13. data/examples/basic/app/js/vendor/jquery.js +2 -0
  14. data/examples/basic/app/js/vendor/jquery.plugin.js +2 -0
  15. data/examples/basic/app/js/vendor/underscore.js +2 -0
  16. data/examples/basic/views/index.erb +26 -0
  17. data/examples/classic/app.rb +24 -0
  18. data/examples/classic/css/screen.scss +1 -0
  19. data/examples/compass/.gitignore +1 -0
  20. data/examples/compass/Rakefile +7 -0
  21. data/examples/compass/app.rb +45 -0
  22. data/examples/compass/app/css/main.scss +64 -0
  23. data/examples/compass/app/images/icon-scfd8d7d404.png +0 -0
  24. data/examples/compass/app/images/icon/mail.png +0 -0
  25. data/examples/compass/app/images/icon/refresh.png +0 -0
  26. data/examples/compass/app/images/junk/mail.png +0 -0
  27. data/examples/compass/app/images/junk/refresh.png +0 -0
  28. data/examples/compass/app/js/app.js +3 -0
  29. data/examples/compass/app/js/vendor/jquery.js +2 -0
  30. data/examples/compass/app/js/vendor/jquery.plugin.js +2 -0
  31. data/examples/compass/app/js/vendor/underscore.js +2 -0
  32. data/examples/compass/config.ru +3 -0
  33. data/examples/compass/views/index.erb +15 -0
  34. data/lib/sinatra/assetpack.rb +61 -0
  35. data/lib/sinatra/assetpack/buster_helpers.rb +36 -0
  36. data/lib/sinatra/assetpack/class_methods.rb +82 -0
  37. data/lib/sinatra/assetpack/compressor.rb +54 -0
  38. data/lib/sinatra/assetpack/configurator.rb +21 -0
  39. data/lib/sinatra/assetpack/css.rb +36 -0
  40. data/lib/sinatra/assetpack/engine.rb +20 -0
  41. data/lib/sinatra/assetpack/engines/closure.rb +19 -0
  42. data/lib/sinatra/assetpack/engines/jsmin.rb +10 -0
  43. data/lib/sinatra/assetpack/engines/sass.rb +11 -0
  44. data/lib/sinatra/assetpack/engines/simple.rb +11 -0
  45. data/lib/sinatra/assetpack/engines/sqwish.rb +21 -0
  46. data/lib/sinatra/assetpack/engines/uglify.rb +12 -0
  47. data/lib/sinatra/assetpack/engines/yui.rb +22 -0
  48. data/lib/sinatra/assetpack/hasharray.rb +66 -0
  49. data/lib/sinatra/assetpack/helpers.rb +61 -0
  50. data/lib/sinatra/assetpack/html_helpers.rb +17 -0
  51. data/lib/sinatra/assetpack/image.rb +59 -0
  52. data/lib/sinatra/assetpack/options.rb +326 -0
  53. data/lib/sinatra/assetpack/package.rb +116 -0
  54. data/lib/sinatra/assetpack/rake.rb +29 -0
  55. data/lib/sinatra/assetpack/version.rb +7 -0
  56. data/test/app/.gitignore +1 -0
  57. data/test/app/Rakefile +7 -0
  58. data/test/app/app.rb +62 -0
  59. data/test/app/app/css/behavior.htc +1 -0
  60. data/test/app/app/css/js2c.css +494 -0
  61. data/test/app/app/css/screen.sass +9 -0
  62. data/test/app/app/css/sqwishable.css +7 -0
  63. data/test/app/app/css/style.css +3 -0
  64. data/test/app/app/css/stylus.styl +3 -0
  65. data/test/app/app/css_stylus/stylus.styl +3 -0
  66. data/test/app/app/images/background.jpg +1 -0
  67. data/test/app/app/images/email.png +0 -0
  68. data/test/app/app/js/_ignoreme.js +1 -0
  69. data/test/app/app/js/hello.js +1 -0
  70. data/test/app/app/js/hi.coffee +2 -0
  71. data/test/app/app/js/ugly.js +7 -0
  72. data/test/app/app/js_glob/a/b/c1/hello.js +1 -0
  73. data/test/app/app/js_glob/a/b/c2/hi.js +1 -0
  74. data/test/app/app/js_glob/a/b/c2/hola.js +1 -0
  75. data/test/app/app/views/index.haml +1 -0
  76. data/test/arity_test.rb +26 -0
  77. data/test/build_test.rb +31 -0
  78. data/test/cache_test.rb +9 -0
  79. data/test/compressed_test.rb +30 -0
  80. data/test/glob_test.rb +42 -0
  81. data/test/helpers_test.rb +30 -0
  82. data/test/ignore_test.rb +30 -0
  83. data/test/img_test.rb +31 -0
  84. data/test/local_file_test.rb +21 -0
  85. data/test/mime_type_test.rb +33 -0
  86. data/test/non_existent_test.rb +45 -0
  87. data/test/options_test.rb +21 -0
  88. data/test/order_test.rb +20 -0
  89. data/test/preproc_test.rb +28 -0
  90. data/test/redundant_test.rb +11 -0
  91. data/test/simplecss_test.rb +16 -0
  92. data/test/sqwish_test.rb +31 -0
  93. data/test/stylus_test.rb +23 -0
  94. data/test/template_cache_test.rb +29 -0
  95. data/test/test_helper.rb +46 -0
  96. data/test/tilt_test.rb +11 -0
  97. data/test/uglifier_test.rb +23 -0
  98. data/test/unit_test.rb +109 -0
  99. data/test/yui_test.rb +22 -0
  100. metadata +276 -0
@@ -0,0 +1,17 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module HtmlHelpers
4
+ extend self
5
+
6
+ def e(str)
7
+ re = Rack::Utils.escape_html str
8
+ re = re.gsub("/", '/') # Rack sometimes insists on munging slashes in Ruby 1.8.
9
+ re
10
+ end
11
+
12
+ def kv(hash)
13
+ hash.map { |k, v| " #{e k}='#{e v}'" }.join('')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,59 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # An image.
4
+ #
5
+ # == Common usage
6
+ #
7
+ # i = Image['/app/images/background.png'] # Local file path
8
+ #
9
+ # i.dimensions # Tuple for [ width, height ]
10
+ # i.width
11
+ # i.height
12
+ #
13
+ # i.dimensions? # True if dimensions are available
14
+ # # (e.g., if ImageMagick is installed and working)
15
+ #
16
+ class Image
17
+ # Looks up an image.
18
+ # This makes each image only have one associated instance forever.
19
+ def self.[](fname)
20
+ fname = File.expand_path(fname) || fname
21
+
22
+ @cache ||= Hash.new
23
+ @cache[fname] ||= new fname
24
+ end
25
+
26
+ def initialize(file)
27
+ @file = file
28
+ end
29
+
30
+ def dimensions
31
+ return @dimensions unless @dimensions.nil?
32
+
33
+ _, _, dim = `identify "#{@file}"`.split(' ')
34
+ w, h = dim.split('x')
35
+
36
+ if w.to_i != 0 && h.to_i != 0
37
+ @dimensions = [w.to_i, h.to_i]
38
+ else
39
+ @dimensions = false
40
+ end
41
+
42
+ rescue => e
43
+ @dimensions = false
44
+ end
45
+
46
+ def dimensions?
47
+ !! dimensions
48
+ end
49
+
50
+ def width
51
+ dimensions? && dimensions[0]
52
+ end
53
+
54
+ def height
55
+ dimensions? && dimensions[1]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,326 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # Assets.
4
+ #
5
+ # == Common usage
6
+ #
7
+ # SinatraApp.assets {
8
+ # # dsl stuff here
9
+ # }
10
+ #
11
+ # a = SinatraApp.assets
12
+ #
13
+ # Getting options:
14
+ #
15
+ # a.js_compression
16
+ # a.output_path
17
+ #
18
+ # Served:
19
+ #
20
+ # a.served # { '/js' => '/var/www/project/app/js', ... }
21
+ # # (URL path => local path)
22
+ #
23
+ # Packages:
24
+ #
25
+ # a.packages # { 'app.css' => #<Package>, ... }
26
+ # # (name.type => package instance)
27
+ #
28
+ # Build:
29
+ #
30
+ # a.build! { |path| puts "Building #{path}" }
31
+ #
32
+ # Lookup:
33
+ #
34
+ # a.local_path_for('/images/bg.gif')
35
+ # a.served?('/images/bg.gif')
36
+ #
37
+ # a.glob(['/js/*.js', '/js/vendor/**/*.js'])
38
+ # # Returns a HashArray of (local => remote)
39
+ #
40
+ class Options
41
+ include Configurator
42
+
43
+ def initialize(app, &blk)
44
+ unless app.root?
45
+ raise Error, "Please set :root in your Sinatra app."
46
+ end
47
+
48
+ @app = app
49
+ @js_compression = :jsmin
50
+ @css_compression = :simple
51
+
52
+ begin
53
+ @output_path = app.public
54
+ rescue NoMethodError
55
+ @output_path = app.public_folder
56
+ end
57
+
58
+ @js_compression_options = Hash.new
59
+ @css_compression_options = Hash.new
60
+
61
+ @ignored = Array.new
62
+
63
+ reset!
64
+
65
+ # Defaults!
66
+ serve '/css', :from => 'app/css'
67
+ serve '/js', :from => 'app/js'
68
+ serve '/images', :from => 'app/images'
69
+
70
+ ignore '.*'
71
+ ignore '_*'
72
+
73
+ blk.arity <= 0 ? instance_eval(&blk) : yield(self) if block_given?
74
+ end
75
+
76
+ # =====================================================================
77
+ # DSL methods
78
+
79
+ def serve(path, options={})
80
+ raise Error unless options[:from]
81
+ return unless File.directory?(File.join(app.root, options[:from]))
82
+
83
+ @served[path] = options[:from]
84
+ end
85
+
86
+ # Undo defaults.
87
+ def reset!
88
+ @served = Hash.new
89
+ @packages = Hash.new
90
+ end
91
+
92
+ # Ignores a given path spec.
93
+ def ignore(spec)
94
+ if spec[0] == '/'
95
+ @ignored << "#{spec}"
96
+ @ignored << "#{spec}/**"
97
+ else
98
+ @ignored << "**/#{spec}"
99
+ @ignored << "**/#{spec}/**"
100
+ end
101
+ end
102
+
103
+ # Makes nothing ignored. Use this if you don't want to ignore dotfiles and underscore files.
104
+ def clear_ignores!
105
+ @ignored = Array.new
106
+ end
107
+
108
+ # Checks if a given path is ignored.
109
+ def ignored?(fn)
110
+ @ignored.any? { |spec| File.fnmatch spec, fn }
111
+ end
112
+
113
+ # Adds some JS packages.
114
+ #
115
+ # js :foo, [ '/js/vendor/jquery.*.js', ... ]
116
+ # js :foo, '/js/foo.js', [ '/js/vendor/jquery.*.js', ... ]
117
+ #
118
+ def js(name, *args)
119
+ js_or_css :js, name, *args
120
+ end
121
+
122
+ # Adds some CSS packages.
123
+ #
124
+ # css :app, [ '/css/screen.css', ... ]
125
+ # css :app, '/css/app.css', [ '/css/screen.css', ... ]
126
+ #
127
+ def css(name, *args) #path, files=[])
128
+ js_or_css :css, name, *args
129
+ end
130
+
131
+ def js_or_css(type, name, *args)
132
+ # Account for "css :name, '/path/to/css', [ files ]"
133
+ if args[0].is_a?(String) && args[1].respond_to?(:each)
134
+ path, files = args
135
+
136
+ # Account for "css :name, [ files ]"
137
+ elsif args[0].respond_to?(:each)
138
+ path = "/assets/#{name}.#{type}" # /assets/foobar.css by default
139
+ files = args[0]
140
+
141
+ else
142
+ raise ArgumentError
143
+ end
144
+
145
+ @packages["#{name}.#{type}"] = Package.new(self, name, type, path, files)
146
+ end
147
+
148
+ attr_reader :app # Sinatra::Base instance
149
+ attr_reader :packages # Hash, keys are "foo.js", values are Packages
150
+ attr_reader :served # Hash, paths to be served.
151
+ # Key is URI path, value is local path
152
+
153
+ attrib :js_compression # Symbol, compression method for JS
154
+ attrib :css_compression # Symbol, compression method for CSS
155
+ attrib :output_path # '/public'
156
+
157
+ attrib :js_compression_options # Hash
158
+ attrib :css_compression_options # Hash
159
+
160
+ attrib :prebuild # Bool
161
+
162
+ def js_compression(name=nil, options=nil)
163
+ @js_compression = name unless name.nil?
164
+ @js_compression_options = options if options.is_a?(Hash)
165
+ @js_compression
166
+ end
167
+
168
+ def css_compression(name=nil, options=nil)
169
+ @css_compression = name unless name.nil?
170
+ @css_compression_options = options if options.is_a?(Hash)
171
+ @css_compression
172
+ end
173
+
174
+ # =====================================================================
175
+ # Stuff
176
+
177
+ attr_reader :served
178
+
179
+ def build!(&blk)
180
+ session = Rack::Test::Session.new app
181
+
182
+ get = lambda { |path|
183
+ response = session.get(path)
184
+ out = response.body
185
+ mtime = Time.parse(response.headers['Last-Modified']) if response.headers['Last-Modified']
186
+
187
+ [ out, mtime ]
188
+ }
189
+
190
+ packages.each { |_, pack|
191
+ out, mtime = get[pack.path]
192
+
193
+ write pack.path, out, mtime, &blk
194
+ write pack.production_path, out, mtime, &blk
195
+ }
196
+
197
+ files.each { |path, local|
198
+ out, mtime = get[path]
199
+
200
+ write path, out, mtime, &blk
201
+ write BusterHelpers.add_cache_buster(path, local), out, mtime, &blk
202
+ }
203
+ end
204
+
205
+ # Caches the packages.
206
+ def cache!(&blk)
207
+ return false if app.reload_templates
208
+
209
+ session = Rack::Test::Session.new app
210
+ packages.each { |_, pack|
211
+ yield pack.path if block_given?
212
+ session.get(pack.path)
213
+ }
214
+
215
+ true
216
+ end
217
+
218
+ def served?(path)
219
+ !! local_file_for(path)
220
+ end
221
+
222
+ # Returns the local file for a given URI path.
223
+ # Returns nil if a file is not found.
224
+ def local_file_for(path)
225
+ path = path.squeeze('/')
226
+
227
+ uri, local = served.detect { |uri, local| path[0...uri.size] == uri }
228
+
229
+ if local
230
+ path = path[uri.size..-1]
231
+ path = File.join app.root, local, path
232
+
233
+ path if File.exists?(path)
234
+ end
235
+ end
236
+
237
+ # Returns the local file for a given URI path. (for dynamic files)
238
+ # Returns nil if a file is not found.
239
+ # TODO: consolidate with local_file_for
240
+ def dyn_local_file_for(file, from)
241
+ # Remove extension
242
+ file = $1 if file =~ /^(.*)(\.[^\.]+)$/
243
+
244
+ # Remove cache-buster (/js/app.28389.js => /js/app)
245
+ file = $1 if file =~ /^(.*)\.[0-9]+$/
246
+
247
+ Dir[File.join(app.root, from, "#{file}.*")].first
248
+ end
249
+
250
+ # Writes `public/#{path}` based on contents of `output`.
251
+ def write(path, output, mtime=nil)
252
+ require 'fileutils'
253
+
254
+ path = File.join(@output_path, path)
255
+ yield path if block_given?
256
+
257
+ FileUtils.mkdir_p File.dirname(path)
258
+ File.open(path, 'w') { |f| f.write output }
259
+
260
+ if mtime
261
+ File.utime mtime, mtime, path
262
+ end
263
+ end
264
+
265
+ # Returns the files as a hash.
266
+ def files(match=nil)
267
+ # All
268
+ # A buncha tuples
269
+ tuples = @served.map { |prefix, local_path|
270
+ path = File.expand_path(File.join(@app.root, local_path))
271
+ spec = File.join(path, '**', '*')
272
+
273
+ Dir[spec].map { |f|
274
+ [ to_uri(f, prefix, path), f ] unless File.directory?(f)
275
+ }
276
+ }.flatten.compact
277
+
278
+ Hash[*tuples]
279
+ end
280
+
281
+ # Returns an array of URI paths of those matching given globs.
282
+ #
283
+ # glob('spec')
284
+ # glob(['spec1', 'spec2' ...])
285
+ # glob('spec', preserve: true)
286
+ #
287
+ # If `preserve` is set to true, it will preserve any specs that are not
288
+ # wildcards that don't match anything.
289
+ #
290
+ def glob(match, options={})
291
+
292
+ match = [*match] # Force array-ness
293
+
294
+ paths = match.map { |spec|
295
+ if options[:preserve] && !spec.include?('*')
296
+ spec
297
+ else
298
+ files.keys.select { |f| File.fnmatch?(spec, f) }.sort
299
+ end
300
+ }.flatten
301
+
302
+ paths = paths.uniq
303
+ tuples = paths.map { |key| [key, files[key]] }
304
+
305
+ HashArray[*tuples.flatten]
306
+ end
307
+
308
+ private
309
+ # Returns a URI for a given file
310
+ # path = '/projects/x/app/css'
311
+ # to_uri('/projects/x/app/css/file.sass', '/styles', path) => '/styles/file.css'
312
+ #
313
+ def to_uri(f, prefix, path)
314
+ fn = (prefix + f.gsub(path, '')).squeeze('/')
315
+
316
+ # Switch the extension ('x.sass' => 'x.css')
317
+ file_ext = File.extname(fn).to_s[1..-1]
318
+ out_ext = AssetPack.tilt_formats[file_ext]
319
+
320
+ fn = fn.gsub(/\.#{file_ext}$/, ".#{out_ext}") if file_ext && out_ext
321
+
322
+ fn
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,116 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # A package.
4
+ #
5
+ # == Common usage
6
+ #
7
+ # package = assets.packages['application.css']
8
+ #
9
+ # package.files # List of local files
10
+ # package.paths # List of URI paths
11
+ #
12
+ # package.type # :css or :js
13
+ # package.css?
14
+ # package.js?
15
+ #
16
+ # package.path # '/css/application.css' => where to serve the compressed file
17
+ #
18
+ # package.to_development_html
19
+ # package.to_production_html
20
+ #
21
+ class Package
22
+ include HtmlHelpers
23
+ include BusterHelpers
24
+
25
+ def initialize(assets, name, type, path, filespecs)
26
+ @assets = assets # Options instance
27
+ @name = name # "application"
28
+ @type = type # :js or :css
29
+ @path = path # '/js/app.js' -- where to served the compressed file
30
+ @filespecs = filespecs # [ '/js/*.js' ]
31
+ end
32
+
33
+ attr_reader :type
34
+ attr_reader :path
35
+ attr_reader :filespecs
36
+ attr_reader :name
37
+
38
+ # Returns a list of URIs
39
+ def paths_and_files
40
+ list = @assets.glob(@filespecs, :preserve => true)
41
+ list.reject! { |path, file| @assets.ignored?(path) }
42
+ list
43
+ end
44
+
45
+ def files
46
+ paths_and_files.values
47
+ end
48
+
49
+ def paths
50
+ paths_and_files.keys
51
+ end
52
+
53
+ def mtime
54
+ BusterHelpers.mtime_for(files)
55
+ end
56
+
57
+ # Returns the regex for the route, including cache buster crap.
58
+ def route_regex
59
+ re = @path.gsub(/(.[^.]+)$/) { |ext| "(?:\.[0-9]+)?#{ext}" }
60
+ /^#{re}$/
61
+ end
62
+
63
+ def to_development_html(options={})
64
+ paths_and_files.map { |path, file|
65
+ path = add_cache_buster(path, file) # app.css => app.829378.css
66
+ link_tag(path, options)
67
+ }.join("\n")
68
+ end
69
+
70
+ # The URI path of the minified file (with cache buster)
71
+ def production_path
72
+ add_cache_buster @path, *files
73
+ end
74
+
75
+ def to_production_html(options={})
76
+ link_tag production_path, options
77
+ end
78
+
79
+ def minify
80
+ engine = @assets.send(:"#{@type}_compression")
81
+ options = @assets.send(:"#{@type}_compression_options")
82
+
83
+ Compressor.compress combined, @type, engine, options
84
+ end
85
+
86
+ # The cache hash.
87
+ def hash
88
+ if @assets.app.development?
89
+ "#{name}.#{type}/#{mtime}"
90
+ else
91
+ "#{name}.#{type}"
92
+ end
93
+ end
94
+
95
+ def combined
96
+ session = Rack::Test::Session.new(@assets.app)
97
+ paths.map { |path|
98
+ result = session.get(path)
99
+ result.body if result.status == 200
100
+ }.join("\n")
101
+ end
102
+
103
+ def js?() @type == :js; end
104
+ def css?() @type == :css; end
105
+
106
+ private
107
+ def link_tag(file, options={})
108
+ if js?
109
+ "<script src='#{e file}'#{kv options}></script>"
110
+ elsif css?
111
+ "<link rel='stylesheet' href='#{e file}'#{kv options} />"
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end