sqweeze 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Manifest
2
+ README.rdoc
3
+ Rakefile
4
+ bin/sqweeze
5
+ lib/compilers/assetLinker.rb
6
+ lib/compilers/cssDomCompiler.rb
7
+ lib/compilers/jsDomCompiler.rb
8
+ lib/compressor.rb
9
+ lib/compressors/cssCompressor.rb
10
+ lib/compressors/gifCompressor.rb
11
+ lib/compressors/jpegCompressor.rb
12
+ lib/compressors/jsCompressor.rb
13
+ lib/compressors/pngCompressor.rb
14
+ lib/confManager.rb
15
+ lib/domCompiler.rb
16
+ lib/sqweezeUtils.rb
17
+ spec/confManager_spec.rb
18
+ spec/domCompiler_spec.rb
19
+ spec/sqw_spec_helper.rb
20
+ sqweeze.gemspec
@@ -0,0 +1,28 @@
1
+ == Sqweeze
2
+ A command line web asset optimisation tool.
3
+
4
+ === Features
5
+
6
+ _Sqweeze_ cuts the unnecessary bytes out of your web assets, helping you delivering your web project more fast and efficiently.
7
+ With a little help from a number of well known open source third party tools, _Sqweeze_ does the following:
8
+
9
+ * Lossless compresses .PNG, .JPEG and .GIF image assets.
10
+ * Compresses and concatenates into single files multiple Stylesheets and Javascript files.
11
+ * Embeds the compressed assets referenced into the stylesheet, using using {Data-uris}[https://developer.mozilla.org/En/The_data_URL_scheme] (for proper browsers and IE8) and {MHTML}[http://en.wikipedia.org/wiki/MHTML] (for IE6-7).
12
+ * Finally, it provides some basic functionality to linking the compressed Javascript and CSS files back to the document.
13
+
14
+ === Installing it
15
+
16
+ Sqweeze relies on a few command-line image compression tools, namely Pngcrush, Jpegtrain, and Gifsicle, which have to be installed first.
17
+ On an Ubuntu box, you can install these by running a single command line
18
+
19
+ sudo apt-get install pngcrush gifsicle libjpeg-progs
20
+
21
+ (Mac OSX users can install the first two as Macports packages. The third (part of libjpeg) instead, has to be
22
+ {compiled manually}[http://www.phpied.com/installing-jpegtran-mac-unix-linux]. However, Jpegtrain will not be needed as long as you do not have Jpeg files into your project.)
23
+
24
+ Once installed the requirements above, you can install sqweeze, and its ruby dependencies, through ruby gems
25
+
26
+ gem install sqweeze
27
+
28
+
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('sqweeze', '0.0.1') do |p|
6
+ p.description = "A command line web-asset optimisation tool"
7
+ p.url = "http://github.com/premasagar/sqwidget-builder"
8
+ p.author = "Andrea Fiore"
9
+ p.email = "and@inventati.org"
10
+ p.spec_pattern=['spec']
11
+ p.ignore_pattern = ["doc/", 'spec/test_dir_sqweezed',"webapp"]
12
+ p.runtime_dependencies = ['hpricot >= 0.8.2','closure-compiler >=0.2.2','yui-compressor >= 0.9.1']
13
+
14
+ end
15
+
@@ -0,0 +1,116 @@
1
+ #! /usr/bin/env ruby
2
+ # rubygems
3
+ %w[rubygems yui/compressor closure-compiler hpricot].each{|lib| require lib}
4
+ # ruby standard library
5
+ %w[logger singleton yaml base64 open-uri pathname optparse].each{|lib| require lib}
6
+
7
+ # add lib sub-directories to load path
8
+ SQWEEZE_DIR=File.expand_path( File.dirname(File.dirname(__FILE__)))
9
+ $LOAD_PATH.unshift(SQWEEZE_DIR) unless $LOAD_PATH.include?(SQWEEZE_DIR)
10
+ ['lib','lib/compressor', 'lib/compilers'].each do |sub_dir|
11
+ $LOAD_PATH.unshift("#{SQWEEZE_DIR}/#{sub_dir}")
12
+ end
13
+
14
+ %w[sqweezeUtils confManager compressor domCompiler
15
+ compressors/cssCompressor compressors/jsCompressor compressors/gifCompressor compressors/pngCompressor compressors/jpegCompressor
16
+ compilers/cssDomCompiler compilers/jsDomCompiler compilers/assetLinker
17
+ ].each{|lib| require lib}
18
+
19
+
20
+
21
+
22
+ options = {}
23
+ op=OptionParser.new do |opts|
24
+
25
+ opts.banner = "Usage: sqweeze [options] SOURCE_DIR [TARGET_DIR]"
26
+ opts.program_name= 'sqweeze'
27
+ opts.on("-s", "--optimisation-strategy=ALL_IN_ONE",
28
+ "JavaScript/CSS optimisation strategy to be adopted ") do |s|
29
+ raise OptionParser::InvalidArgument unless ['all_inline','all_in_one'].include?(s)
30
+ options[:optimisation_strategy] = s.downcase.to_sym
31
+ end
32
+
33
+ opts.separator ""
34
+ opts.separator "File Selection:"
35
+ opts.on('-i','--include-files=PATTERN', "Include a file/directory pattern (e.g. images/**/*.png ) ") do |inc|
36
+ options[:include_files] = inc
37
+ end
38
+ opts.on('-e','--exclude-files=PATTERN', "Exclude a file/directory pattern (e.g. doc/**/*.* )") do |exc|
39
+ options[:exclude_files] = exc
40
+ end
41
+ opts.on('-d','--dom-documents=PATTERN', "Dom document/s to which CSS/JS should be linked (e.g. **/index.html )") do |dom|
42
+ options[:dom_documents] = dom
43
+ end
44
+
45
+ opts.separator ""
46
+ opts.separator "Image Compression:"
47
+ opts.on("--[no-]png", "do/do-not use the PNG Compressor ") do |png|
48
+ options[:compress_png] = png
49
+ end
50
+ opts.on("--[no-]jpeg", "do/do-not use the Jpeg Compressor ") do |jpeg|
51
+ options[:compress_jpeg] = jpeg
52
+ end
53
+ opts.on("--[no-]gif", "do/do-not use the GIF Compressor ") do |gif|
54
+ options[:compress_gif] = gif
55
+ end
56
+ opts.separator ""
57
+ opts.separator "Stylesheets:"
58
+
59
+ opts.on("--[no-]css", "do/do-not use the CSS Compressor ") do |css|
60
+ options[:compress_css] = css
61
+ end
62
+ opts.on("-m","--mhtml-root=[ABSOLUTE_URL]", 'An absolute url pointing at the project stylesheets directory on the web') do |mr|
63
+ # raise OptionParser::InvalidArgument unless URI(mr).absolute?
64
+ options[:mhtml_root] = mr
65
+ end
66
+ opts.separator ""
67
+ opts.separator "JavaScript:"
68
+
69
+ opts.on("--[no-]js", "do/do-not use the JS Compressor ") do |js|
70
+ options[:compress_js] = js
71
+ end
72
+ opts.on('-a','--append_scripts_to=HEAD', "Append script elements to the HEAD or the BODY of the document") do |elm|
73
+ options[:append_scripts_to] = elm
74
+ end
75
+ opts.on("-j","--default-js-compressor=YUI", "Sets the default JavaScript compressor (can be either YUI or Closure) ") do |j|
76
+ raise OptionParser::InvalidArgument unless ['yui','closure'].include?(j.downcase)
77
+ options[:default_js_compressor] = j.downcase.to_sym
78
+ end
79
+
80
+ end
81
+
82
+ begin
83
+ op.parse!
84
+ Proc.new { puts "Missing SOURCE_DIR argument"; exit(1) }.call unless ARGV[0]
85
+ Proc.new { puts "source directory #{ARGV[0]} does not exist, please provide a valid path"
86
+ exit(1) }.call unless File.exists?(ARGV[0])
87
+
88
+
89
+ $log=Logger.new( STDERR )
90
+ cm=ConfManager.instance
91
+ cm.prepare(ARGV[0], ARGV[1], options)
92
+
93
+ # Compress images
94
+ { :compress_png => PngCompressor.new,
95
+ :compress_gif => GifCompressor.new,
96
+ :compress_jpeg => JpegCompressor.new
97
+ }.each do |opt, cmp|
98
+ cmp.compress unless cm.get_conf(opt) == false
99
+ end
100
+
101
+ case cm.get_conf(:optimisation_strategy)
102
+ when :all_inline
103
+ puts "not implemented yet.."
104
+ exit
105
+
106
+
107
+ when :all_in_one
108
+ { :compress_js => JsCompressor.new,
109
+ :compress_css => CssCompressor.new
110
+ }.each do |opt, cmp|
111
+ cmp.compress unless cm.get_conf(opt) == false
112
+ end
113
+ # link assets to main HTML file
114
+ AssetLinker.new.compile unless cm.get_conf(:dom_documents).empty?
115
+ end
116
+ end
@@ -0,0 +1,33 @@
1
+ class AssetLinker < DOMCompiler
2
+
3
+ def compile
4
+ iterate_over("link[@rel='stylesheet'], script[@src]") do |elm,doc|
5
+ # Skip if the script has an absolute link
6
+ next if elm.get_attribute('src') =~ /^http:\/\//
7
+ # Otherwise delete.
8
+ elm.parent.children.delete(elm)
9
+ end
10
+ dom_documents.each do |path|
11
+ # Append concatenated JavaScript file.
12
+ doc = Hpricot(open(path))
13
+ doc.search(@cm.get_conf(:append_scripts_to).to_s.downcase).append( "<script type='text/javascript' src='javascripts.min.js'></script>")
14
+
15
+ stylesheets_html=<<EOF
16
+ <!--[if lte IE 8]>
17
+ <link href="stylesheets.min.mhtml.css" rel="stylesheet" />
18
+ <![endif]-->
19
+ <!--[if IE 8]>
20
+ <link href="stylesheets.min.datauri.css" rel="stylesheet" />
21
+ <![endif]-->
22
+ <!--[if !IE]>
23
+ <link href="stylesheets.min.datauri.css" rel="stylesheet" />
24
+ <![endif]-->
25
+ EOF
26
+
27
+ # Append ie conditional tags
28
+ doc.search('head').append(stylesheets_html)
29
+ write_file(doc.innerHTML,path)
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,23 @@
1
+ class CssDomCompiler < DOMCompiler
2
+
3
+ def compile
4
+ iterate_over('link[@rel="stylesheet"]') do |elm, doc|
5
+ next unless elm.has_attribute?('href') and not elm.get_attribute('href').nil?
6
+
7
+ fbody=get_resource(elm.get_attribute('href'))
8
+
9
+ # this is a bit convoluted, but it seems to be the right way of doing it
10
+ elm.parent.children.delete(elm)
11
+
12
+ style_html = "<style type=\"text/css\">\n" + css_cdata(fbody,:compress) + '</style>'
13
+ doc.search('head').append(style_html)
14
+
15
+ end
16
+ end
17
+
18
+
19
+ def compress(fbody)
20
+ YUI::CssCompressor.new.compress(fbody)
21
+ end
22
+
23
+ end
@@ -0,0 +1,24 @@
1
+ class JsDomCompiler < DOMCompiler
2
+
3
+ def compile
4
+ iterate_over('script[@src]') do |element, doc |
5
+
6
+ res_body=get_resource(element.get_attribute('src'))
7
+ element.remove_attribute('src')
8
+ # do the YUI/CLOSURE thing here
9
+ element.innerHTML=js_cdata(res_body,:compress)
10
+ end
11
+ end
12
+
13
+
14
+ def compress(fbody)
15
+ unless @cm.default_js_compressor == :closure
16
+ compressor, method = YUI::JavaScriptCompressor.new( :munge => true), :compress
17
+ else
18
+ compressor, method = Closure::Compiler.new, :compiler
19
+ end
20
+ compressor.send(method,fbody)
21
+ end
22
+
23
+
24
+ end
@@ -0,0 +1,128 @@
1
+ # Pseudo-abstract class inherited by all the other classes performing asset-compression operations
2
+
3
+ class Compressor
4
+ include SqweezeUtils
5
+
6
+ # Sets the file extensions that a compressor accepts
7
+ #
8
+ # *Note*: As this class cannot be instanciated directly, this method is only used by subclasses.
9
+ def initialize(input_file_extensions)
10
+
11
+ # Behave as a pseudo abstract class
12
+ raise "Cannot instantiate #{self.class} directly" if self.class == Compressor
13
+
14
+ @cm=ConfManager.instance
15
+ @input_file_extensions=input_file_extensions
16
+
17
+
18
+ # Get the project file list from the configuration manager
19
+ @input_files=@cm.files
20
+
21
+ # The commands used by this compressor,
22
+ # listed in order of priority
23
+
24
+ @commands={}
25
+
26
+ # Set the default shell command to use for this compressor.
27
+ # issue: what's the point of calling this default command and not just default..
28
+ @default_command=nil
29
+
30
+ # whether or not concatenating the input files (it is the case of css and javascripts)
31
+ @concatenate_input=false
32
+
33
+ # Store the overall byte weight of the assets, before compressions
34
+ @byteweight_before = collect_filepaths.inject(0){|sum,f| sum+File.size(f)}
35
+
36
+ #set later, after compression
37
+ @byteweight_after = 0
38
+ end
39
+
40
+
41
+ attr_reader :input_file_extensions, #something here
42
+ :byteweight_before,
43
+ :byteweight_after
44
+
45
+
46
+ # Sets the system command to be invoked by the compressor.
47
+ #
48
+ # This raises a runtime error if the third party command cannot be found in the file system.
49
+ def set_command(libname,command)
50
+
51
+ raise "missing library #{libname}" unless @cm.get_conf(:bin_paths).keys.include?(libname)
52
+ @default_command=libname if @commands.empty?
53
+ @commands[libname]=command
54
+ end
55
+
56
+ # Turns a string or an array of strings containing several file extension names into regular expressions.
57
+
58
+ def filextension2regexpstr(ext=@input_file_extensions)
59
+ if @input_file_extensions.is_a?(Array) and @input_file_extensions.size > 1
60
+ "(#{@input_file_extensions.collect{|ext|"\.#{ext}"}.join('|')})$"
61
+ else
62
+ "\.#{@input_file_extensions}$"
63
+ end
64
+ end
65
+
66
+
67
+ # Collects the paths of files having the compressor extension name.
68
+ def collect_filepaths
69
+ exp_str = filextension2regexpstr
70
+ @cm.files.select{|path| path =~ Regexp.new(exp_str,true) }
71
+ end
72
+
73
+ # Applies a system command to a list of file paths.
74
+ #
75
+ # This method is overridden by both CssCompressor and JsCompressor, as these two classes do not rely on
76
+ # command line executable but use ruby bindings to YUI and Google Closure.
77
+
78
+ def process(inputpath,cmd=nil)
79
+ output_path =@cm.get_target_path(inputpath)
80
+
81
+ cl= cmd.gsub('%executable%', @cm.get_conf(:bin_paths)[@default_command]).
82
+ gsub('%input%',inputpath).
83
+ gsub('%output%', output_path)
84
+ system(cl)
85
+ $log.debug("run command #{cl}, pid=#{$?.pid} exitstatus=#{$?.exitstatus}")
86
+ output_path
87
+ end
88
+
89
+
90
+ # Iterates over the list of file paths matching the compressor extension/s and applies the process method to each of them.
91
+
92
+ def compress
93
+ files=@input_files=collect_filepaths
94
+
95
+ # This will consist of only an entry (the concatenated file body)
96
+ # when compressing javascript or css files
97
+
98
+ files=[concatenate_files] if @concatenate_input
99
+ cmd= (@commands.empty?)? nil : @commands[ @default_command ]
100
+
101
+ files.each do |path|
102
+ output_path=process(path,cmd)
103
+ size_check(path,output_path) unless %w(.css .js).include?( File.extname(output_path))
104
+ end
105
+ end
106
+
107
+ # Makes sure that the byteweight of a compressed file is not larger that the original one.
108
+
109
+ def size_check(input_path,output_path)
110
+ before_size, after_size = byteweight(input_path), byteweight(output_path)
111
+ # if the compressed file is bigger than the original one, copy the original in the target dir
112
+ if after_size < before_size
113
+ $log.info("compressed #{input_path} to the #{ compression_percentage(before_size,after_size)}% of its original size (from: #{before_size} to #{after_size} bytes ) ")
114
+ @byteweight_after+=after_size
115
+ else
116
+ $log.debug("Oppss!!! output(#{after_size}) >= input(#{before_size}), using the original file #{input_path} ")
117
+ FileUtils.cp(input_path,output_path)
118
+ @byteweight_after+=before_size
119
+ end
120
+ end
121
+
122
+ # Concatenates several files into a single string (used in both CSS and JS compression).
123
+
124
+ def concatenate_files
125
+ @input_files.collect{|f|File.open(f,'r').read}.join("\n")
126
+ end
127
+
128
+ end
@@ -0,0 +1,103 @@
1
+ # Compresses stylesheets using the YUI compressor, creating three new files in the target directory:
2
+ # [<code>stylesheets.min.css</code>]
3
+ # compressed and concatenated stylesheets without embedded assets (suitable for all browsers).
4
+ #
5
+ # [<code>stylesheets.datauir.css</code>]
6
+ # compressed and concatenated stylesheets with assets embedded as data-uris (suitable for both Webkit and Geko based browser, and ie8).
7
+ #
8
+ # [<code>stylesheets.mhtml.css</code>]
9
+ # compressed and concatenated stylesheets with assets embedded as data-uris (suitable for IE6 and IE7).
10
+ #
11
+ # *Note*: some of the methods used by this class are copied from Jash kenas's excellent {Jammit}[http://github.com/documentcloud/jammit/], an asset optimisation tool for Rails.
12
+
13
+ class CssCompressor < Compressor
14
+
15
+ # 32k maximum size for embeddable images (an IE8 limitation).
16
+ MAX_IMAGE_SIZE = 32768
17
+
18
+ # CSS asset-embedding regexes for URL rewriting.
19
+ EMBED_DETECTOR = /url\(['"]?([^\s)]+\.[a-z]+)(\?\d+)?['"]?\)/
20
+
21
+ # MHTML file constants.
22
+ MHTML_START = "/*\nContent-Type: multipart/related; boundary=\"SQWEEZED_ASSET\"\n"
23
+ MHTML_SEPARATOR= "--SQWEEZED_ASSET"
24
+ MHTML_END = "*/\n"
25
+
26
+ def initialize
27
+ super('css')
28
+
29
+ @concatenate_input=true
30
+ @concatenated_file=nil
31
+ # This hash is populated while generating the data uris
32
+ # in order to be reused later in the MHTML file
33
+ @assets={}
34
+ # TODO fonts..
35
+ end
36
+
37
+ def process(input_str,cmd=nil)
38
+ fout= (@concatenate_input)? "#{@cm.target_dir}/all.min.css" : @cm.get_target_path(inputpath)
39
+ compressed_output=YUI::CssCompressor.new.compress(input_str)
40
+
41
+ unless compressed_output.chomp.empty?
42
+ write_file(compressed_output,"#{@cm.target_dir}/stylesheets.min.css")
43
+ embed_datauris( compressed_output )
44
+ embed_mhtml( compressed_output ) if @cm.get_conf(:mhtml_root)
45
+ end
46
+ # this return is pointless
47
+ fout
48
+ end
49
+
50
+
51
+
52
+ private
53
+ def mhtml_location(path)
54
+ p=Pathname.new(path)
55
+ p.relative_path_from( Pathname.new(@cm.target_dir))
56
+ end
57
+
58
+ def embed_datauris(compressed_css)
59
+ out=compressed_css.gsub(EMBED_DETECTOR) do |url|
60
+ compressed_asset_path=remap_filepath($1)
61
+ mime_t=mime_type(compressed_asset_path)
62
+ if compressed_asset_path and File.exists?(compressed_asset_path) and File.size(compressed_asset_path) < MAX_IMAGE_SIZE and mime_t
63
+ base64_asset=encoded_contents( compressed_asset_path )
64
+ $log.debug("file:#{compressed_asset_path}; mime-type: #{mime_type($1)}#")
65
+ # label the image
66
+ @assets[ compressed_asset_path ] = base64_asset
67
+ "url(\"data:#{mime_t};charset=utf-8;base64,#{base64_asset}\")"
68
+ else
69
+ "url(#{$1})"
70
+ end
71
+ end
72
+
73
+
74
+ write_file(out,"#{@cm.target_dir}/stylesheets.min.datauri.css")
75
+ end
76
+
77
+
78
+ def embed_mhtml(compressed_css)
79
+ mhtml= MHTML_START
80
+
81
+ @assets.each do |mhtml_loc,base64|
82
+ mhtml <<"
83
+ #{MHTML_SEPARATOR}
84
+ Content-location: #{ mhtml_loc }
85
+ Content-Transfer-Encoding:base64
86
+
87
+ #{base64}\n"
88
+ end
89
+ mhtml << "*/\n"
90
+ mhtml << compressed_css.gsub(EMBED_DETECTOR) do |css|
91
+ compressed_asset_path=remap_filepath($1)
92
+ if compressed_asset_path
93
+ "url(mhtml:#{@cm.get_conf(:mhtml_root).gsub(/\/$/,'')}/stylesheets.min.mhtml.css!#{mhtml_location( compressed_asset_path)})"
94
+ else
95
+ "url(#{$1})"
96
+ end
97
+ end
98
+
99
+ write_file(mhtml,"#{@cm.target_dir}/stylesheets.min.mhtml.css")
100
+ end
101
+
102
+ end
103
+
@@ -0,0 +1,13 @@
1
+ class GifCompressor < Compressor
2
+
3
+ def initialize
4
+ super('gif')
5
+
6
+ set_command(
7
+ :gifsicle, '%executable% --optimize < %input% > %output%'
8
+ )
9
+ end
10
+
11
+ end
12
+
13
+
@@ -0,0 +1,6 @@
1
+ class JpegCompressor < Compressor
2
+ def initialize
3
+ super(['jpeg','jpg'])
4
+ set_command(:jpegtran,'%executable% -copy none -optimize -outfile %output% %input%')
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ class JsCompressor < Compressor
2
+
3
+ def initialize
4
+ super('js')
5
+ @concatenate_input=true
6
+ end
7
+
8
+
9
+ def process(input_str,cmd=nil)
10
+ unless @cm.get_conf(:default_js_compressor) == :closure
11
+ compressor, method = YUI::JavaScriptCompressor.new( :munge => true), :compress
12
+ else
13
+ compressor, method = Closure::Compiler.new, :compiler
14
+ end
15
+
16
+ fout= (@concatenate_input)? "#{@cm.target_dir}/javascripts.min.js" : @cm.get_target_path(inputpath)
17
+
18
+ File.open(fout,'w') do |f|
19
+ f.write(compressor.send( method, input_str))
20
+ end
21
+
22
+ fout
23
+ end
24
+
25
+ end
26
+
27
+
@@ -0,0 +1,11 @@
1
+ class PngCompressor < Compressor
2
+
3
+ def initialize
4
+ super('png')
5
+ set_command(:pngcrush,'%executable% -q -rem text %input% %output%')
6
+
7
+ end
8
+
9
+ end
10
+
11
+
@@ -0,0 +1,191 @@
1
+ # Main component for handling Sqweeze's file and command-line configuration parameters
2
+
3
+
4
+ class ConfManager
5
+ include Singleton
6
+ # Class constructor.
7
+ #
8
+ # As ConfManager implements the singleton pattern, it is actually instanciated only once.
9
+ # The same instance might be retrieved from different places by calling the method:
10
+ #
11
+ # <code>ConfManager.instance()</code>
12
+
13
+ def initialize
14
+ @conf_filename='.sqweeze.yml'
15
+ @source_dir=nil
16
+ @target_dir=nil
17
+ @files=[]
18
+
19
+ @conf={
20
+ :bin_paths => {},
21
+ :dom_documents => [],
22
+ :include_files => [],
23
+ :exclude_files => [],
24
+ :compress_png => true,
25
+ :compress_jpeg => true,
26
+ :compress_gif => true,
27
+ :compress_js => true,
28
+ :compress_css => true,
29
+ :append_scripts_to => :head,
30
+ :default_js_compressor => :yui,
31
+ :optimisation_strategy => :all_in_one
32
+ }
33
+ end
34
+ attr_reader :target_dir,:source_dir,
35
+ :files,:conf_filename,:conf
36
+
37
+
38
+ # Setter method used to populate the <code>@conf</code> attribute with key value pairs.
39
+ #
40
+ # *Note*: string keys are automatically converted to symbols.
41
+
42
+
43
+ def set_conf(key,value);@conf[key.to_sym]=value;end
44
+
45
+ # Getter method for retrieving configuration values.
46
+
47
+ def get_conf(key);@conf[key.to_sym];end
48
+
49
+ # Sets the source directory, the target directory, and parse configuration files.
50
+ #
51
+ # [source] the source directory
52
+ #
53
+ # [target] the target directory
54
+ #
55
+ # [override_conf] a Hash which may be used to override file-based configuration.
56
+
57
+ def prepare(source,target=nil,override_conf={})
58
+
59
+ @source_dir=source
60
+ @target_dir=unless target
61
+ "#{File.basename(source)}_sqweezed"
62
+ else
63
+ target
64
+ end
65
+
66
+ write_globalconf
67
+ # Parses the global configuration file in $HOME.
68
+ parse_conf
69
+ # Parses the local configuration file in source directory.
70
+ local_conf=mkpath(@conf_filename)
71
+ parse_conf(local_conf) if File.exists?(local_conf)
72
+
73
+ # CLI/Overrides of the values already set in config files.
74
+ override_conf.each{|k,v| set_conf(k, v)} unless override_conf.empty?
75
+
76
+ # Creates the list of the files in the project directory.
77
+
78
+ list_files
79
+ copy_source
80
+ end
81
+
82
+ # Copies the source into the target directory.
83
+ def copy_source
84
+ FileUtils.cp_r(@source_dir,@target_dir)
85
+ # avoid nesting into each other multiple copies of the same directory
86
+ nested_dir=[@target_dir,File.basename(@source_dir)].join('/')
87
+ FileUtils.rm_r(nested_dir) if File.directory?(nested_dir)
88
+ end
89
+
90
+ # Remaps a filepath from the source to the target directory.
91
+ #
92
+ # TODO:check if this works with relative paths (i.e. <code>../imgs/bar.png</code>)
93
+
94
+ def get_target_path(infile_path)
95
+ infile_path.gsub( Regexp.new("^#{@source_dir}"), @target_dir)
96
+ end
97
+
98
+
99
+ # Generates a file pattern suitable to be expanded by ruby's {Dir[]}[http://ruby-doc.org/core/classes/Dir.html] method.
100
+
101
+ def mkpath(pattern,dir=@source_dir)
102
+ dir.to_a.push(pattern).join('/')
103
+ end
104
+
105
+ # Defines a global <code>.sqweeze.yml</code> file and places it in the user's home directory.
106
+ #
107
+ # The golobal config files sets the default path of the image compression binaries
108
+ # (see @conf[:bin_paths]).
109
+
110
+ def write_globalconf
111
+ unless File.exists?("#{ENV['HOME']}/#{@conf_filename}")
112
+ File.open("#{ENV['HOME']}/#{@conf_filename}",'w') do |f|
113
+ f.write("bin_paths:
114
+ - pngcrush: /usr/bin/pngcrush
115
+ - jpegtran: /usr/bin/jpegtran
116
+ - gifsicle: /usr/bin/gifsicle
117
+ ")
118
+ f.close
119
+ end
120
+ end
121
+ end
122
+
123
+ # Parses configuration files and sets user-defined file inclusion/exlusion patterns.
124
+
125
+ def parse_conf(configfile="#{ENV['HOME']}/#{@conf_filename}")
126
+ $log.debug("Parsing configuration file: #{configfile}")
127
+ conf=YAML::load_file( configfile )
128
+
129
+ bin_paths={}
130
+ # do not select commands if their path cannot be found on disk
131
+ conf['bin_paths'].each do |h|
132
+ if File.exists?(h.values.first)
133
+ bin_paths[h.keys.first.to_sym] = h.values.first
134
+ else
135
+ $log.warn("Command #{h.keys.first} not found in #{h.values.first}")
136
+ end
137
+ end
138
+ set_conf(:bin_paths,bin_paths) unless bin_paths.empty?
139
+
140
+ # Expand the dom document pattern excluding the files located in the source directory.
141
+ if conf['dom_documents']
142
+
143
+ domdoc_file_patterns = Dir[conf['dom_documents']].collect{|f| f.gsub(Regexp.new("^#{@source_dir}"),@target_dir)}
144
+ set_conf(:dom_documents, domdoc_file_patterns)
145
+ end
146
+
147
+ # Sets the favourite js compressor ( can either be :yui or :closure, defaults on YUI)
148
+ compressor = ( conf['default_js_compressor'] == :closure) ? :closure : :yui
149
+ set_conf(:default_js_compressor, compressor)
150
+
151
+ # others..
152
+ %w(include exclude).each {|k| set_conf(k, conf[k].to_a)}
153
+ %w(optimisation_strategy append_scripts_to).each{|k| set_conf(k,conf[k]) if conf[k]}
154
+ end
155
+
156
+ # Explodes the inclusion/exclusion patterns provided by the user into a list, and populates the @files attribute.
157
+
158
+ def list_files
159
+ # unless the user defined file inclusion list is empty, select only the files specified by the user.
160
+ # Otherwise consider all the files in the project directory as potentially compressible.
161
+
162
+ @files = unless get_conf(:include).empty?
163
+ files=get_files( get_conf(:include));$log.debug("Including #{files.size} file/s from user list")
164
+ files
165
+ else
166
+ get_files
167
+ end
168
+
169
+ # always exclude files explicitly blacklisted by the use
170
+ exclude_files=get_files(get_conf(:exclude))
171
+
172
+
173
+ $log.debug("Excluding #{exclude_files.size} file/s from user list")
174
+ @files -= exclude_files
175
+ $log.info("#{@files.size} file/s found")
176
+ end
177
+
178
+
179
+ # Get all the files matching an array of patterns
180
+ #
181
+ # (Any file expansion patterns accepted ruby's Dir[] method can be used).
182
+
183
+ def get_files(pathlist=['**/*'])
184
+ pathlist.collect{|pattern|
185
+
186
+ Dir[ mkpath(pattern) ].
187
+ reject{|f|File.directory?(f) or not File.exists?(f)}
188
+ }.flatten
189
+ end
190
+
191
+ end
@@ -0,0 +1,51 @@
1
+ # Pseudo-abstract inherited by all classes doing dom manipulations
2
+ class DOMCompiler
3
+ include SqweezeUtils
4
+ attr_reader :dom_documents
5
+
6
+ def initialize
7
+ # @dom_extnames=['.html','.svg']
8
+ @cm=ConfManager.instance
9
+ #if @cm.link_assets_to and not @cm.link_assets_to.empty?
10
+ @dom_documents=@cm.get_conf(:dom_documents).to_a
11
+ end
12
+ # Retrieves a resource, being this either a URL or a file.
13
+ def get_resource(path_or_url)
14
+ f=open(remap_filepath(path_or_url))
15
+ (f.is_a? File)? f.read : f
16
+ end
17
+ # Iterates over a DOM element and allows to apply a custom block over it.
18
+
19
+ def iterate_over(selector)
20
+ @dom_documents.each do |path|
21
+ doc=Hpricot(open(path))
22
+ if doc
23
+ doc.search(selector).each do |element|
24
+
25
+ #$log.debug..
26
+ yield(element, doc)
27
+ # save document
28
+ #write_file(doc.innerHTML, [@cm.target_dir, File.basename(path) ].join('/') )
29
+ write_file(doc.innerHTML, path )
30
+ end
31
+ else
32
+ $log.error("DOMCompiler cannot parse #{path}")
33
+ end
34
+ end
35
+ end
36
+
37
+ # Wraps a string into CDATA escaped in order to be embedded into a <code><script></code> element.
38
+ # [text] a text string
39
+ # [callback] an instance method used to process the string (normally compile).
40
+ def js_cdata(text,callback)
41
+ ["\n<!--//--><![CDATA[//><!--", self.send(callback,text), "//--><!]]>\n"].join("\n")
42
+ end
43
+
44
+ # Wraps a string into CDATA escaped in order to be embedded into a <code><style></code> element.
45
+ # [text] a text string
46
+ # [callback] an instance method used to process the string (normally compile).
47
+ def css_cdata(text,callback)
48
+ ["/* <![CDATA[ */", self.send(callback,text), "/* ]]> */\n"].join("\n")
49
+ end
50
+
51
+ end
@@ -0,0 +1,86 @@
1
+ module SqweezeUtils
2
+
3
+ # Mapping from extension to mime-type of all embeddable assets.
4
+ EMBED_MIME_TYPES = {
5
+ '.png' => 'image/png',
6
+ '.jpg' => 'image/jpeg',
7
+ '.jpeg' => 'image/jpeg',
8
+ '.gif' => 'image/gif',
9
+ '.tif' => 'image/tiff',
10
+ '.tiff' => 'image/tiff',
11
+ '.ttf' => 'font/truetype',
12
+ '.otf' => 'font/opentype'
13
+ }
14
+
15
+ def write_file(fbody,fpath)
16
+ File.open(fpath,'w') do |f|
17
+ f.write(fbody)
18
+ f.close
19
+ end
20
+ end
21
+
22
+ # Grab the mime-type of an asset, by filename.
23
+ def mime_type(asset_path)
24
+ EMBED_MIME_TYPES[File.extname(asset_path)] if asset_path
25
+ end
26
+
27
+ # Remaps a file in the source directory to its compressed
28
+ # version in the target directory
29
+ #
30
+ # if the resource is an absolute URL fine,
31
+ # Otherwise, if the resource is a relative url in the source dir, try
32
+ # to map it to its compressed version in the target directory
33
+
34
+ def remap_filepath(path)
35
+ path=Pathname.new(path)
36
+ parent_dir, path_basename = path.parent, path.basename
37
+ is_absolute = URI.parse(path).absolute?
38
+ unless is_absolute
39
+ find_file_in_targetdir( [parent_dir, path_basename].join('/'))
40
+ else
41
+ path.to_s
42
+ end
43
+ end
44
+
45
+ # Return the Base64-encoded contents of an asset on a single line.
46
+ def encoded_contents(asset_path)
47
+ data = open(asset_path, 'rb'){|f| f.read }
48
+ Base64.encode64(data).gsub(/\n/, '')
49
+ end
50
+
51
+
52
+ # Gets the byte weight of input strames, wether these are file paths or just strings
53
+
54
+ def byteweight(path_or_string)
55
+ path_or_string="" if path_or_string.nil?
56
+
57
+ if File.exists?(path_or_string)
58
+ File.size(path_or_string)
59
+ else
60
+ bweight=0
61
+ path_or_string.each_byte{|b|bweight+=1}
62
+ bweight
63
+ end
64
+ end
65
+
66
+ def compression_percentage(before_bweight,after_bweight)
67
+ sprintf('%.2f',after_bweight/before_bweight.to_f * 100)
68
+ end
69
+
70
+
71
+ # Find a file in the target directory
72
+ # endpath is a string containing parent directory and file name
73
+ # (e.g imgs/separator.png)
74
+
75
+ # TODO: rewrite this! (it sucks..)
76
+
77
+ def find_file_in_targetdir(endpath)
78
+ pattern= [@cm.target_dir,"**/*#{File.extname(endpath)}"].join('/')
79
+ #puts "searcing for files in #{pattern} ending with #{endpath}"
80
+ Dir[pattern].find{|path| path =~ Regexp.new("#{endpath}$")}
81
+ end
82
+
83
+
84
+
85
+
86
+ end
@@ -0,0 +1,61 @@
1
+ require 'sqw_spec_helper'
2
+
3
+
4
+ describe ConfManager do
5
+ context "Starting up" do
6
+
7
+ before(:each) do
8
+ @cm = ConfManager.instance
9
+ end
10
+
11
+ after(:each) do
12
+ @cm=nil
13
+ # clean up config files
14
+ %w( ENV['home'] test_dir ).each do |f|
15
+ f << "/.sqweeze.yml"
16
+ FileUtils.rm(f) if File.exists?(f)
17
+ end
18
+ end
19
+
20
+ it "Should create a .sqweeze.yml file in the home directory" do
21
+ @cm.prepare('test_dir')
22
+ File.exists?("#{ENV['HOME']}/.sqweeze.yml").should be_true
23
+ end
24
+
25
+ it "Should select the list of files provided by the user if the *include:* section of the .sqweeze.yml is not empty" do
26
+ extra_lines= ["include:","- js/**/*.js",'- color/**/*.png']
27
+ write_configfile(extra_lines[0..1].join("\n"))
28
+ @cm.prepare('test_dir')
29
+ @cm.should have(3).files
30
+
31
+ write_configfile(extra_lines.join("\n"))
32
+ @cm.prepare('test_dir')
33
+ @cm.should have(5).files
34
+ end
35
+
36
+ it "Should select all files in the project if the include: section of .sqweeze.yml is left empty" do
37
+ project_file_count= Dir['test_dir/**/*'].reject{|f| File.directory?(f)}.size
38
+ @cm.prepare('test_dir')
39
+ @cm.should have(project_file_count).files
40
+ end
41
+
42
+ it "Should always exclude the files specified by the user in the .sqweeze.yml exclude section " do
43
+ write_configfile(["exclude:","- js/**/*.js"].join("\n"))
44
+ project_file_count= Dir['test_dir/**/*'].reject{|f| File.directory?(f)}.size
45
+ @cm.prepare('test_dir')
46
+ @cm.should have(46).files
47
+ end
48
+
49
+ it "Should search *dom_documents* in the the target directory " do
50
+ write_configfile("dom_documents: test_dir/*/page.tpl.php")
51
+ @cm.prepare('test_dir')
52
+ @cm.get_conf(:dom_documents).should include('test_dir_sqweezed/tpl/page.tpl.php')
53
+ end
54
+
55
+
56
+
57
+
58
+
59
+
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ require '../lib/confManager'
2
+ require '../lib/DomCompiler'
3
+ require '../lib/jsDomCompiler'
4
+ require '../lib/cssDomCompiler'
5
+ require 'sqw_spec_helper'
6
+
7
+ describe DOMCompiler do
8
+ before(:each) do
9
+ wget_webpage('http://www.dreamhost.com/domains.html','dom-testdir')
10
+ cm = ConfManager.instance
11
+ cm.prepare('dom-testdir','dom-testdir-build')
12
+ @js_c=JSDOMCompiler.new
13
+ end
14
+
15
+ after(:each) do
16
+ FileUtils.rm_r('dom-testdir')
17
+ FileUtils.rm_r('dom-testdir-build')
18
+ end
19
+
20
+ it "(all subclasses) should look for *dom_documents* in target directory" do
21
+ @js_c.should have(1).dom_documents
22
+ @js_c.dom_documents.should include('dom-testdir-build/index.html')
23
+ end
24
+
25
+ it "Should fill each script element having a src attribute with the compressed content of the javascript file therin referenciated" do
26
+ @js_c.compile
27
+
28
+ Hpricot( open('dom-testdir-build/index.html')).search('script').each do |element|
29
+ element.innerHTML.should_not be_empty
30
+ element.get_attribute('src').should be_nil
31
+ end
32
+ end
33
+
34
+ it "Should fill each link tag having a href attribute with the compressed content of the css file therein referenced" do
35
+ @css_c=CSSDOMCompiler.new
36
+ @css_c.compile()
37
+ end
38
+
39
+ end
40
+
41
+ #
@@ -0,0 +1,24 @@
1
+ SQW_CONF_FILE='test_dir/.sqweeze.yml'
2
+ SQW_FBODY=<<EOF
3
+ bin_paths:
4
+ - pngcrush: /usr/bin/pngcrush
5
+ - jpegtran: /usr/bin/jpegtran
6
+ - gifsicle: /usr/bin/gifsicle
7
+ #- cssembed: /home/and/installed/cssembed.jar
8
+ EOF
9
+
10
+ def write_configfile(extra_lines='')
11
+ fbody = SQW_FBODY + extra_lines
12
+ f=File.open(SQW_CONF_FILE,'w')
13
+ f.write( fbody)
14
+ f.close
15
+ end
16
+
17
+ def delete_configfile
18
+ FileUtils.rm_r(SQW_CONF_FILE) if File.exists?(SQW_CONF_FILE)
19
+ end
20
+
21
+ def wget_webpage(url,dir_prefix)
22
+ domain=URI.parse(url).host
23
+ system("wget --no-parent --timestamping --convert-links --page-requisites --no-directories --no-host-directories -erobots=off --quiet --directory-prefix=#{dir_prefix} #{url}")
24
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{sqweeze}
5
+ s.version = "0.0.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andrea Fiore"]
9
+ s.date = %q{2010-05-01}
10
+ s.default_executable = %q{sqweeze}
11
+ s.description = %q{A command line web-asset optimisation tool}
12
+ s.email = %q{and@inventati.org}
13
+ s.executables = ["sqweeze"]
14
+ s.extra_rdoc_files = ["README.rdoc", "bin/sqweeze", "lib/compilers/assetLinker.rb", "lib/compilers/cssDomCompiler.rb", "lib/compilers/jsDomCompiler.rb", "lib/compressor.rb", "lib/compressors/cssCompressor.rb", "lib/compressors/gifCompressor.rb", "lib/compressors/jpegCompressor.rb", "lib/compressors/jsCompressor.rb", "lib/compressors/pngCompressor.rb", "lib/confManager.rb", "lib/domCompiler.rb", "lib/sqweezeUtils.rb"]
15
+ s.files = ["Manifest", "README.rdoc", "Rakefile", "bin/sqweeze", "lib/compilers/assetLinker.rb", "lib/compilers/cssDomCompiler.rb", "lib/compilers/jsDomCompiler.rb", "lib/compressor.rb", "lib/compressors/cssCompressor.rb", "lib/compressors/gifCompressor.rb", "lib/compressors/jpegCompressor.rb", "lib/compressors/jsCompressor.rb", "lib/compressors/pngCompressor.rb", "lib/confManager.rb", "lib/domCompiler.rb", "lib/sqweezeUtils.rb", "spec/confManager_spec.rb", "spec/domCompiler_spec.rb", "spec/sqw_spec_helper.rb", "sqweeze.gemspec"]
16
+ s.homepage = %q{http://github.com/premasagar/sqwidget-builder}
17
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Sqweeze", "--main", "README.rdoc"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = %q{sqweeze}
20
+ s.rubygems_version = %q{1.3.5}
21
+ s.summary = %q{A command line web-asset optimisation tool}
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 3
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<hpricot>, [">= 0", "= 0.8.2"])
29
+ s.add_runtime_dependency(%q<closure-compiler>, [">= 0.2.2"])
30
+ s.add_runtime_dependency(%q<yui-compressor>, [">= 0", "= 0.9.1"])
31
+ else
32
+ s.add_dependency(%q<hpricot>, [">= 0", "= 0.8.2"])
33
+ s.add_dependency(%q<closure-compiler>, [">= 0.2.2"])
34
+ s.add_dependency(%q<yui-compressor>, [">= 0", "= 0.9.1"])
35
+ end
36
+ else
37
+ s.add_dependency(%q<hpricot>, [">= 0", "= 0.8.2"])
38
+ s.add_dependency(%q<closure-compiler>, [">= 0.2.2"])
39
+ s.add_dependency(%q<yui-compressor>, [">= 0", "= 0.9.1"])
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqweeze
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 2
9
+ version: 0.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Andrea Fiore
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-01 00:00:00 +01:00
18
+ default_executable: sqweeze
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: hpricot
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ - - "="
31
+ - !ruby/object:Gem::Version
32
+ segments:
33
+ - 0
34
+ - 8
35
+ - 2
36
+ version: 0.8.2
37
+ type: :runtime
38
+ version_requirements: *id001
39
+ - !ruby/object:Gem::Dependency
40
+ name: closure-compiler
41
+ prerelease: false
42
+ requirement: &id002 !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ segments:
47
+ - 0
48
+ - 2
49
+ - 2
50
+ version: 0.2.2
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: yui-compressor
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ - - "="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ - 9
68
+ - 1
69
+ version: 0.9.1
70
+ type: :runtime
71
+ version_requirements: *id003
72
+ description: A command line web-asset optimisation tool
73
+ email: and@inventati.org
74
+ executables:
75
+ - sqweeze
76
+ extensions: []
77
+
78
+ extra_rdoc_files:
79
+ - README.rdoc
80
+ - bin/sqweeze
81
+ - lib/compilers/assetLinker.rb
82
+ - lib/compilers/cssDomCompiler.rb
83
+ - lib/compilers/jsDomCompiler.rb
84
+ - lib/compressor.rb
85
+ - lib/compressors/cssCompressor.rb
86
+ - lib/compressors/gifCompressor.rb
87
+ - lib/compressors/jpegCompressor.rb
88
+ - lib/compressors/jsCompressor.rb
89
+ - lib/compressors/pngCompressor.rb
90
+ - lib/confManager.rb
91
+ - lib/domCompiler.rb
92
+ - lib/sqweezeUtils.rb
93
+ files:
94
+ - Manifest
95
+ - README.rdoc
96
+ - Rakefile
97
+ - bin/sqweeze
98
+ - lib/compilers/assetLinker.rb
99
+ - lib/compilers/cssDomCompiler.rb
100
+ - lib/compilers/jsDomCompiler.rb
101
+ - lib/compressor.rb
102
+ - lib/compressors/cssCompressor.rb
103
+ - lib/compressors/gifCompressor.rb
104
+ - lib/compressors/jpegCompressor.rb
105
+ - lib/compressors/jsCompressor.rb
106
+ - lib/compressors/pngCompressor.rb
107
+ - lib/confManager.rb
108
+ - lib/domCompiler.rb
109
+ - lib/sqweezeUtils.rb
110
+ - spec/confManager_spec.rb
111
+ - spec/domCompiler_spec.rb
112
+ - spec/sqw_spec_helper.rb
113
+ - sqweeze.gemspec
114
+ has_rdoc: true
115
+ homepage: http://github.com/premasagar/sqwidget-builder
116
+ licenses: []
117
+
118
+ post_install_message:
119
+ rdoc_options:
120
+ - --line-numbers
121
+ - --inline-source
122
+ - --title
123
+ - Sqweeze
124
+ - --main
125
+ - README.rdoc
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ segments:
140
+ - 1
141
+ - 2
142
+ version: "1.2"
143
+ requirements: []
144
+
145
+ rubyforge_project: sqweeze
146
+ rubygems_version: 1.3.6
147
+ signing_key:
148
+ specification_version: 3
149
+ summary: A command line web-asset optimisation tool
150
+ test_files: []
151
+