aanand-deadweight 0.0.3 → 0.1.0

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.
@@ -2,6 +2,10 @@
2
2
 
3
3
  Deadweight is RCov for CSS, kind of. Given a set of stylesheets and a set of URLs, it determines which selectors are actually used and reports which can be "safely" deleted.
4
4
 
5
+ === Screencast!
6
+
7
+ Ryan Bates has worked his magic once again. Head over here for an excellent introduction to deadweight: http://railscasts.com/episodes/180-finding-unused-css
8
+
5
9
  === A Simple Example
6
10
 
7
11
  # lib/tasks/deadweight.rake
@@ -16,7 +20,19 @@ Deadweight is RCov for CSS, kind of. Given a set of stylesheets and a set of URL
16
20
  puts dw.run
17
21
  end
18
22
 
19
- This will output all unused selectors, one per line.
23
+ This will output all unused rules, one per line.
24
+
25
+ Alternately, you can run it from the command-line:
26
+
27
+ $ deadweight -s styles.css -s ie.css index.html about.html
28
+
29
+ You can pipe in CSS rules from STDIN:
30
+
31
+ $ cat styles.css | deadweight index.html
32
+
33
+ And you can use it as an HTTP proxy:
34
+
35
+ $ deadweight -l deadweight.log -s styles.css -w http://github.com/ -P
20
36
 
21
37
  === How You Install It
22
38
 
@@ -28,6 +44,7 @@ This will output all unused selectors, one per line.
28
44
  - By default, it looks at http://localhost:3000.
29
45
  - It's completely dumb about any classes, IDs or tags that are only added by your Javascript layer, but you can filter them out by setting +ignore_selectors+.
30
46
  - You can optionally tell it to use Mechanize, and set up more complicated targets for scraping by specifying them as Procs.
47
+ - There is experimental support for Lyndon (http://github.com/defunkt/lyndon) with -L
31
48
 
32
49
  === A More Complex Example, In Light of All That
33
50
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.1.0
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'deadweight/cli'
3
+
4
+ Deadweight::CLI.execute(STDOUT, STDIN, STDERR, ARGV.dup)
5
+
@@ -1,13 +1,18 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
1
4
  # -*- encoding: utf-8 -*-
2
5
 
3
6
  Gem::Specification.new do |s|
4
7
  s.name = %q{deadweight}
5
- s.version = "0.0.3"
8
+ s.version = "0.1.0"
6
9
 
7
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
11
  s.authors = ["Aanand Prasad"]
9
- s.date = %q{2009-08-18}
12
+ s.date = %q{2009-09-24}
13
+ s.default_executable = %q{deadweight}
10
14
  s.email = %q{aanand.prasad@gmail.com}
15
+ s.executables = ["deadweight"]
11
16
  s.extra_rdoc_files = [
12
17
  "LICENSE",
13
18
  "README.rdoc"
@@ -19,8 +24,11 @@ Gem::Specification.new do |s|
19
24
  "README.rdoc",
20
25
  "Rakefile",
21
26
  "VERSION",
27
+ "bin/deadweight",
22
28
  "deadweight.gemspec",
23
29
  "lib/deadweight.rb",
30
+ "lib/deadweight/cli.rb",
31
+ "test/cli_test.rb",
24
32
  "test/deadweight_test.rb",
25
33
  "test/fixtures/index.html",
26
34
  "test/fixtures/index2.html",
@@ -33,7 +41,8 @@ Gem::Specification.new do |s|
33
41
  s.rubygems_version = %q{1.3.4}
34
42
  s.summary = %q{RCov for CSS}
35
43
  s.test_files = [
36
- "test/deadweight_test.rb",
44
+ "test/cli_test.rb",
45
+ "test/deadweight_test.rb",
37
46
  "test/test_helper.rb"
38
47
  ]
39
48
 
@@ -4,17 +4,34 @@ require 'open-uri'
4
4
  require 'logger'
5
5
 
6
6
  class Deadweight
7
- attr_accessor :root, :stylesheets, :pages, :ignore_selectors, :mechanize, :log_file
7
+ attr_accessor :root, :stylesheets, :rules, :pages, :ignore_selectors, :mechanize, :log_file
8
+ attr_reader :unused_selectors
8
9
 
9
10
  def initialize
10
11
  @root = 'http://localhost:3000'
11
12
  @stylesheets = []
12
13
  @pages = []
14
+ @rules = ""
13
15
  @ignore_selectors = []
14
16
  @mechanize = false
15
17
  @log_file = STDERR
16
18
  end
17
19
 
20
+ def analyze(html)
21
+ doc = Hpricot(html)
22
+
23
+ found_selectors = []
24
+
25
+ @unused_selectors.collect do |selector, declarations|
26
+ # We test against the selector stripped of any pseudo classes,
27
+ # but we report on the selector with its pseudo classes.
28
+ unless doc.search(strip(selector)).empty?
29
+ log.info(" #{selector}")
30
+ selector
31
+ end
32
+ end
33
+ end
34
+
18
35
  # Find all unused CSS selectors and return them as an array.
19
36
  def run
20
37
  css = CssParser::Parser.new
@@ -23,28 +40,31 @@ class Deadweight
23
40
  css.add_block!(fetch(path))
24
41
  end
25
42
 
26
- unused_selectors = []
43
+ css.add_block!(rules)
44
+
45
+ @unused_selectors = {}
27
46
  total_selectors = 0
28
47
 
29
48
  css.each_selector do |selector, declarations, specificity|
30
- unless unused_selectors.include?(selector)
49
+ unless @unused_selectors[selector]
31
50
  total_selectors += 1
32
- unused_selectors << selector unless selector =~ ignore_selectors
51
+ @unused_selectors[selector] = declarations unless selector =~ ignore_selectors
33
52
  end
34
53
  end
35
54
 
36
55
  # Remove selectors with pseudo classes that already have an equivalent
37
56
  # without the pseudo class. Keep the ones that don't, we need to test
38
57
  # them.
39
- unused_selectors.reject! do |selector|
40
- has_pseudo_classes(selector) && unused_selectors.include?(strip(selector))
58
+ @unused_selectors.keys.each do |selector|
59
+ if has_pseudo_classes(selector) && @unused_selectors.include?(strip(selector))
60
+ @unused_selectors.delete(selector)
61
+ end
41
62
  end
42
63
 
43
64
  pages.each do |page|
44
- case page
45
- when String
46
- html = fetch(page)
47
- else
65
+ if page.respond_to?(:read)
66
+ html = page.read
67
+ elsif page.respond_to?(:call)
48
68
  result = instance_eval(&page)
49
69
 
50
70
  html = case result
@@ -53,27 +73,22 @@ class Deadweight
53
73
  else
54
74
  @agent.page.body
55
75
  end
76
+ else
77
+ html = fetch(page)
56
78
  end
57
79
 
58
- doc = Hpricot(html)
80
+ process!(html)
81
+ end
59
82
 
60
- found_selectors = []
83
+ log.info "found #{@unused_selectors.size} unused selectors out of #{total_selectors} total"
61
84
 
62
- unused_selectors.each do |selector|
63
- # We test against the selector stripped of any pseudo classes,
64
- # but we report on the selector with its pseudo classes.
65
- unless doc.search(strip(selector)).empty?
66
- log.info(" #{selector}")
67
- found_selectors << selector
68
- end
69
- end
85
+ @unused_selectors
86
+ end
70
87
 
71
- unused_selectors -= found_selectors
88
+ def process!(html)
89
+ analyze(html).each do |selector|
90
+ @unused_selectors.delete(selector)
72
91
  end
73
-
74
- log.info "found #{unused_selectors.size} unused selectors out of #{total_selectors} total"
75
-
76
- unused_selectors
77
92
  end
78
93
 
79
94
  # Returns the Mechanize instance, if +mechanize+ is set to +true+.
@@ -0,0 +1,187 @@
1
+ require 'optparse'
2
+ require 'deadweight'
3
+
4
+ class Deadweight
5
+ class CLI
6
+ attr_reader :stdout, :stdin, :stderr
7
+ attr_reader :arguments, :options
8
+ attr_reader :output
9
+
10
+ def self.execute(stdout, stdin, stderr, arguments = [])
11
+ @options = {
12
+ :log_file => stderr,
13
+ :output => stdout,
14
+ :proxy_port => 8002
15
+ }
16
+
17
+ self.parse_options(arguments)
18
+ self.new(stdout, stdin, stderr, arguments, @options).execute!
19
+ end
20
+
21
+ def self.option_parser
22
+ @option_parser ||= OptionParser.new do |opts|
23
+ opts.banner = "Usage: #{$0} [options] <url> [<url> ...]"
24
+
25
+ @options[:stylesheets] = []
26
+
27
+ opts.on("-L", "--lyndon", "Pre-process HTML with Lyndon") do
28
+ @options[:lyndon] = true
29
+ end
30
+
31
+ opts.on("-l", "--log FILE", "Where to write log messages") do |v|
32
+ @options[:log_file] = v
33
+ end
34
+
35
+ opts.on("-P", "--proxy", "Run in proxy mode") do
36
+ @options[:proxy] = true
37
+ end
38
+
39
+ opts.on("-p", "--proxy-port", "Port to run the proxy on") do |v|
40
+ @options[:proxy] = true
41
+ @options[:proxy_port] = v.to_i
42
+ end
43
+
44
+ opts.on("-O", "--output FILE",
45
+ "Where to output orphaned CSS rules") do |v|
46
+ @options[:output] = File.new(v, "w")
47
+ end
48
+
49
+ opts.on("-s", "--stylesheet FILE",
50
+ "Apply the specified stylesheet to the target") do |v|
51
+ @options[:stylesheets] << v
52
+ end
53
+
54
+ opts.on("-w", "--whitelist URL-PREFIX",
55
+ "Specifies a prefix for URLs to process") do |v|
56
+ @options[:whitelist] ||= []
57
+ @options[:whitelist] << v
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.parse_options(arguments = [])
63
+ self.option_parser.parse!(arguments)
64
+ @options
65
+ end
66
+
67
+ def initialize(stdout, stdin, stderr, arguments = [], options = {})
68
+ @stdout = stdout
69
+ @stdin = stdin
70
+ @stderr = stderr
71
+ @arguments = arguments
72
+ @options = options
73
+ @output = options[:output]
74
+ end
75
+
76
+ def execute!
77
+ if options[:proxy]
78
+ proxy
79
+ elsif arguments.empty?
80
+ stdout.puts self.class.option_parser.help
81
+ else
82
+ process
83
+ end
84
+ end
85
+
86
+ def process
87
+ # TODO pass stylesheets + pages as args
88
+ dw = Deadweight.new
89
+
90
+ # TODO this should be the default
91
+ dw.root = ""
92
+
93
+ dw.log_file = options[:log_file]
94
+
95
+ dw.stylesheets = options[:stylesheets]
96
+
97
+ dw.rules = stdin.read if stdin.stat.size > 0
98
+
99
+ if options[:lyndon]
100
+ arguments.each do |file|
101
+ dw.pages << IO.popen("cat #{file} | lyndon 2> /dev/null")
102
+ end
103
+ else
104
+ dw.pages = arguments
105
+ end
106
+
107
+ unused_rules = dw.run
108
+ unused_rules.each do |k,v|
109
+ output.puts "#{k} { #{v} }"
110
+ end
111
+ end
112
+
113
+ def proxy
114
+ dw = Deadweight.new
115
+
116
+ # TODO note the boilerplate shared with #process
117
+ dw.root = ""
118
+ dw.log_file = options[:log_file]
119
+ dw.stylesheets = options[:stylesheets]
120
+ dw.rules = stdin.read if stdin.stat.size > 0
121
+
122
+ # initialize selectors
123
+ dw.run
124
+
125
+ stdout.puts "#{dw.unused_selectors.length} rules loaded."
126
+
127
+ require 'webrick/httpproxy'
128
+
129
+ @proxy = WEBrick::HTTPProxyServer.new \
130
+ :AccessLog => [
131
+ [options[:log_file], WEBrick::AccessLog::COMMON_LOG_FORMAT],
132
+ [options[:log_file], WEBrick::AccessLog::REFERER_LOG_FORMAT]
133
+ ],
134
+ :Logger => WEBrick::Log.new(options[:log_file]),
135
+ :Port => options[:proxy_port],
136
+ :ProxyContentHandler => lambda { |request, response|
137
+
138
+ parse_this = false
139
+
140
+ if options[:whitelist]
141
+ options[:whitelist].each do |x|
142
+ sliced_request_uri = response.request_uri.to_s[0..x.length - 1]
143
+ if sliced_request_uri.downcase == x.downcase
144
+ parse_this = true
145
+ break
146
+ end
147
+ end
148
+ else
149
+ parse_this = true
150
+ end
151
+
152
+ if parse_this && response.header["content-type"] =~ /text\/html/
153
+ # TODO this slows things down significantly; better would be to
154
+ # remove the Accept-Encoding header during the request phase
155
+ body = if response.header["content-encoding"] == "gzip"
156
+ Zlib::GzipReader.new(StringIO.new(response.body)).read
157
+ elsif response.header["content-encoding"] == "deflate"
158
+ Zlib::Inflate.inflate(response.body)
159
+ else
160
+ response.body
161
+ end
162
+ dw.process!(body)
163
+
164
+ stdout.puts "After reviewing <#{response.request_uri}>, there were #{dw.unused_selectors.length} rules left"
165
+ # stdout.puts "After reviewing <#{response.request_uri}>, these were left:"
166
+ # dw.unused_selectors.each do |k,v|
167
+ # stdout.puts "#{k} { #{v} }"
168
+ # end
169
+ end
170
+ }
171
+
172
+ trap('INT') do
173
+ @proxy.shutdown
174
+
175
+ # dump the remaining CSS rules if output is set
176
+ unless options[:output] == STDOUT
177
+ dw.unused_selectors.each do |k,v|
178
+ output.puts "#{k} { #{v} }"
179
+ end
180
+ end
181
+ end
182
+
183
+ @proxy.start
184
+ end
185
+ end
186
+ end
187
+
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ class CliTest < Test::Unit::TestCase
4
+ COMMAND = "ruby -rubygems -Ilib bin/deadweight -s test/fixtures/style.css test/fixtures/index.html 2>/dev/null"
5
+
6
+ should "output unused selectors on STDOUT" do
7
+ @result = `#{COMMAND}`.split("\n")
8
+
9
+ assert_equal 1, @result.grep(/^#foo \.bar \.baz \{/).length
10
+ assert_equal 0, @result.grep(/^#foo \{/).length
11
+ assert_equal 0, @result.grep(/^#foo .bar \{/).length
12
+ end
13
+
14
+ should "accept CSS rules on STDIN" do
15
+ @result = `echo ".something { display: block; }" | #{COMMAND}`.split("\n")
16
+
17
+ assert_equal 1, @result.grep(/^\.something \{/).length
18
+ end
19
+ end
20
+
@@ -41,4 +41,20 @@ class DeadweightTest < Test::Unit::TestCase
41
41
 
42
42
  assert @dw.run.empty?
43
43
  end
44
+
45
+ should "accept IO objects as targets" do
46
+ @dw.pages << File.new(File.dirname(__FILE__) + '/fixtures/index2.html')
47
+
48
+ assert @dw.run.empty?
49
+ end
50
+
51
+ should "allow individual CSS rules to be appended" do
52
+ @dw.rules = ".something { display: block; }"
53
+
54
+ assert @dw.run.include?(".something")
55
+ end
56
+
57
+ should 'provide the results of its last run with #unused_selectors' do
58
+ assert_equal @result, @dw.unused_selectors
59
+ end
44
60
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aanand-deadweight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aanand Prasad
@@ -9,8 +9,8 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-18 00:00:00 -07:00
13
- default_executable:
12
+ date: 2009-09-24 00:00:00 -07:00
13
+ default_executable: deadweight
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: css_parser
@@ -34,8 +34,8 @@ dependencies:
34
34
  version:
35
35
  description:
36
36
  email: aanand.prasad@gmail.com
37
- executables: []
38
-
37
+ executables:
38
+ - deadweight
39
39
  extensions: []
40
40
 
41
41
  extra_rdoc_files:
@@ -48,8 +48,11 @@ files:
48
48
  - README.rdoc
49
49
  - Rakefile
50
50
  - VERSION
51
+ - bin/deadweight
51
52
  - deadweight.gemspec
52
53
  - lib/deadweight.rb
54
+ - lib/deadweight/cli.rb
55
+ - test/cli_test.rb
53
56
  - test/deadweight_test.rb
54
57
  - test/fixtures/index.html
55
58
  - test/fixtures/index2.html
@@ -83,5 +86,6 @@ signing_key:
83
86
  specification_version: 3
84
87
  summary: RCov for CSS
85
88
  test_files:
89
+ - test/cli_test.rb
86
90
  - test/deadweight_test.rb
87
91
  - test/test_helper.rb