sqweeze 0.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,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
+