sinatra-assetpack-flexible-compression 0.0.1

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.
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