headhunter 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 79ba313e9b12c21aa35c4fc74bfe40f86801c9fd
4
+ data.tar.gz: afc84375e5fb68c8fe8af5d4e5af6f30826c0d51
5
+ SHA512:
6
+ metadata.gz: d6c891576972b9fba784d4985f874308a639c8f27973b4813c9542eb08eaca6ef8e5a5a68ac7218e9fb72d5760d36da761ecc67bb5a4f8b24322f86a22314ea2
7
+ data.tar.gz: 1c8dc6fed4c64758bb0dc3b07756fcd19b8b76ceb53a912cf7c64e09e4abe03b88397eaedc0c732e8795d81c93906c935166deb6d9d7b7f048488db0566ef800
@@ -0,0 +1,55 @@
1
+ require 'headhunter/css_hunter'
2
+ require 'headhunter/css_validator'
3
+ require 'headhunter/html_validator'
4
+ require 'headhunter/rails'
5
+ # require 'rack/utils'
6
+
7
+ class Headhunter
8
+ attr_accessor :results
9
+
10
+ def initialize(root)
11
+ @root = root
12
+
13
+ precompile_assets!
14
+
15
+ @html_validator = HtmlValidator.new
16
+ @css_validator = CssValidator.new(stylesheets)
17
+ @css_hunter = CssHunter.new(stylesheets)
18
+
19
+ @css_validator.process!
20
+ end
21
+
22
+ def process!(url, html)
23
+ @html_validator.process!(url, html)
24
+ @css_hunter.process!(url, html)
25
+ end
26
+
27
+ def clean_up!
28
+ remove_assets!
29
+ end
30
+
31
+ def report
32
+ @html_validator.prepare_results_html
33
+
34
+ @html_validator.report
35
+ @css_validator.report
36
+ @css_hunter.report
37
+ end
38
+
39
+ private
40
+
41
+ def precompile_assets!
42
+ # Remove existing assets! This seems to be necessary to make sure that they don't exist twice, see http://stackoverflow.com/questions/20938891
43
+ system 'rake assets:clobber HEADHUNTER=false &> /dev/null'
44
+
45
+ system 'rake assets:precompile HEADHUNTER=false &> /dev/null'
46
+ end
47
+
48
+ def remove_assets!
49
+ system 'rake assets:clobber HEADHUNTER=false &> /dev/null'
50
+ end
51
+
52
+ def stylesheets
53
+ Dir.chdir(@root) { Dir.glob('public/assets/*.css') }
54
+ end
55
+ end
@@ -0,0 +1,100 @@
1
+ require 'css_parser'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+
5
+ class Headhunter
6
+ class CssHunter
7
+ def initialize(stylesheets)
8
+ @stylesheets = stylesheets
9
+ @parsed_rules = {}
10
+ @unused_selectors = []
11
+ @used_selectors = []
12
+
13
+ load_css!
14
+ end
15
+
16
+ def process!(url, html)
17
+ analyze(html).each do |selector|
18
+ @unused_selectors.delete(selector)
19
+ end
20
+ end
21
+
22
+ def report
23
+ log.puts "Found #{@used_selectors.size + @unused_selectors.size} CSS selectors.".yellow
24
+ log.puts "#{@used_selectors.size} selectors are in use.".green if @used_selectors.size > 0
25
+ log.puts "#{@unused_selectors.size} selectors are not in use: #{@unused_selectors.sort.join(', ').red}".red if @unused_selectors.size > 0
26
+ log.puts
27
+ end
28
+
29
+ private
30
+
31
+ def analyze(html)
32
+ doc = Nokogiri::HTML(html)
33
+
34
+ @unused_selectors.collect do |selector, declarations|
35
+ # We test against the selector stripped of any pseudo classes,
36
+ # but we report on the selector with its pseudo classes.
37
+ stripped_selector = strip(selector)
38
+
39
+ next if stripped_selector.empty?
40
+
41
+ if doc.search(stripped_selector).any?
42
+ @used_selectors << selector
43
+ selector
44
+ end
45
+ end
46
+ end
47
+
48
+ def load_css!
49
+ @stylesheets.each do |stylesheet|
50
+ new_selector_count = add_css!(fetch(stylesheet))
51
+ end
52
+ end
53
+
54
+ def fetch(path)
55
+ log.puts(path)
56
+
57
+ loc = path
58
+
59
+ begin
60
+ open(loc).read
61
+ rescue Errno::ENOENT
62
+ raise FetchError.new("#{loc} was not found")
63
+ rescue OpenURI::HTTPError => e
64
+ raise FetchError.new("retrieving #{loc} raised an HTTP error: #{e.message}")
65
+ end
66
+ end
67
+
68
+ def add_css!(css)
69
+ parser = CssParser::Parser.new
70
+ parser.add_block!(css)
71
+
72
+ selector_count = 0
73
+
74
+ parser.each_selector do |selector, declarations, specificity|
75
+ next if @unused_selectors.include?(selector)
76
+ next if selector =~ @ignore_selectors
77
+ next if has_pseudo_classes(selector) and @unused_selectors.include?(strip(selector))
78
+
79
+ @unused_selectors << selector
80
+ @parsed_rules[selector] = declarations
81
+
82
+ selector_count += 1
83
+ end
84
+
85
+ selector_count
86
+ end
87
+
88
+ def has_pseudo_classes(selector)
89
+ selector =~ /::?[\w\-]+/
90
+ end
91
+
92
+ def strip(selector)
93
+ selector = selector.gsub(/^@.*/, '') # @-webkit-keyframes ...
94
+ selector = selector.gsub(/:.*/, '') # input#x:nth-child(2):not(#z.o[type='file'])
95
+ selector
96
+ end
97
+ end
98
+
99
+ class FetchError < StandardError; end
100
+ end
@@ -0,0 +1,131 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+
4
+ class Headhunter
5
+ class CssValidator
6
+ def initialize(stylesheets)
7
+ @profile = 'css3' # TODO: Option for profile css1 and css21
8
+ @stylesheets = stylesheets
9
+ @messages_per_stylesheet = {}
10
+ end
11
+
12
+ def process!
13
+ @stylesheets.each do |stylesheet|
14
+ css = fetch(stylesheet)
15
+ css = ' ' if css.empty? # The validator returns a 500 error if it receives an empty string
16
+
17
+ response = get_validation_response({text: css, profile: @profile, vextwarning: 'true'})
18
+ unless response_indicates_valid?(response)
19
+ process_errors(stylesheet, css, response)
20
+ end
21
+ end
22
+ end
23
+
24
+ def report
25
+ log.puts "Validated #{@stylesheets.size} stylesheets.".yellow
26
+ log.puts "#{x_stylesheets_be(@stylesheets.size - @messages_per_stylesheet.size)} valid.".green if @messages_per_stylesheet.size < @stylesheets.size
27
+ log.puts "#{x_stylesheets_be(@messages_per_stylesheet.size)} invalid.".red if @messages_per_stylesheet.size > 0
28
+
29
+ @messages_per_stylesheet.each_pair do |stylesheet, messages|
30
+ log.puts " #{extract_filename(stylesheet)}:".red
31
+
32
+ messages.each { |message| log.puts " - #{message}".red }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Converts a path like public/assets/application-d205d6f344d8623ca0323cb6f6bd7ca1.css to application.css
39
+ def extract_filename(path)
40
+ matches = path.match /^public\/assets\/(.*)-([a-z0-9]*)(\.css)/
41
+ matches[1] + matches[3]
42
+ end
43
+
44
+ def x_stylesheets_be(size)
45
+ if size <= 1
46
+ "#{size} stylesheet is"
47
+ else
48
+ "#{size} stylesheets are"
49
+ end
50
+ end
51
+
52
+ def process_errors(file, css, response)
53
+ @messages_per_stylesheet[file] = []
54
+
55
+ REXML::Document.new(response.body).root.each_element('//m:error') do |e|
56
+ @messages_per_stylesheet[file] << "#{error_line_prefix}: line #{e.elements['m:line'].text}: #{e.elements['m:message'].get_text.value.strip}\n"
57
+ end
58
+ end
59
+
60
+ def fetch(path) # TODO: Move to Headhunter!
61
+ log.puts(path)
62
+
63
+ loc = path
64
+
65
+ begin
66
+ open(loc).read
67
+ rescue Errno::ENOENT
68
+ raise FetchError.new("#{loc} was not found")
69
+ rescue OpenURI::HTTPError => e
70
+ raise FetchError.new("retrieving #{loc} raised an HTTP error: #{e.message}")
71
+ end
72
+ end
73
+
74
+ def get_validation_response(query_params)
75
+ query_params.merge!({:output => 'soap12'})
76
+ get_validator_response(query_params)
77
+ end
78
+
79
+ def response_indicates_valid?(response)
80
+ response['x-w3c-validator-status'] == 'Valid'
81
+ end
82
+
83
+ def get_validator_response(query_params = {})
84
+ response = call_validator(query_params)
85
+
86
+ raise "HTTP error: #{response.code}" unless response.is_a? Net::HTTPSuccess
87
+ return response
88
+ end
89
+
90
+ def call_validator(query_params)
91
+ boundary = Digest::MD5.hexdigest(Time.now.to_s)
92
+ data = encode_multipart_params(boundary, query_params)
93
+ return http_start(validator_host).post2(validator_path, data, "Content-type" => "multipart/form-data; boundary=#{boundary}" )
94
+ end
95
+
96
+ def encode_multipart_params(boundary, params = {})
97
+ ret = ''
98
+ params.each do |k,v|
99
+ unless v.empty?
100
+ ret << "\r\n--#{boundary}\r\n"
101
+ ret << "Content-Disposition: form-data; name=\"#{k.to_s}\"\r\n\r\n"
102
+ ret << v
103
+ end
104
+ end
105
+ ret << "\r\n--#{boundary}--\r\n"
106
+ ret
107
+ end
108
+
109
+ def http_start(host)
110
+ if ENV['http_proxy']
111
+ uri = URI.parse(ENV['http_proxy'])
112
+ proxy_user, proxy_pass = uri.userinfo.split(/:/) if uri.userinfo
113
+ Net::HTTP.start(host, nil, uri.host, uri.port, proxy_user, proxy_pass)
114
+ else
115
+ Net::HTTP.start(host)
116
+ end
117
+ end
118
+
119
+ def validator_host
120
+ 'jigsaw.w3.org'
121
+ end
122
+
123
+ def validator_path
124
+ '/css-validator/validator'
125
+ end
126
+
127
+ def error_line_prefix
128
+ 'Invalid css'
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,58 @@
1
+ require 'html_validation'
2
+
3
+ class Headhunter
4
+ class HtmlValidator
5
+ def initialize
6
+ @valid_results = []
7
+ @invalid_results = []
8
+ end
9
+
10
+ def process!(url, html)
11
+ html_validation = PageValidations::HTMLValidation.new.validation(html, random_name)
12
+ (html_validation.valid? ? @valid_results : @invalid_results) << html_validation
13
+ end
14
+
15
+ def prepare_results_html
16
+ html = File.read File.dirname(File.expand_path(__FILE__)) + '/templates/results.html'
17
+ html.gsub! '{{VALID_RESULTS}}', prepare_results_for(@valid_results)
18
+ html.gsub! '{{INVALID_RESULTS}}', prepare_results_for(@invalid_results)
19
+ File.open('.validation/results.html', 'w') { |file| file.write(html) }
20
+ end
21
+
22
+ def prepare_results_for(results)
23
+ results.map do |result|
24
+ exceptions_html = ::Rack::Utils.escape_html(File.read(".validation/#{result.resource}.exceptions.txt"))
25
+
26
+ full_result_html = File.read File.dirname(File.expand_path(__FILE__)) + '/templates/result.html'
27
+ full_result_html.gsub! '{{RESOURCE}}', result.resource
28
+ full_result_html.gsub! '{{EXCEPTIONS}}', exceptions_html
29
+ full_result_html.gsub! '{{HTML_CONTEXT}}', 'context'
30
+ full_result_html.gsub! '{{LINK}}', "#{result.resource}.html.txt"
31
+
32
+ full_result_html
33
+ end.join
34
+ end
35
+
36
+ def report
37
+ log.puts "Validated #{@valid_results.size + @invalid_results.size} HTML pages.".yellow
38
+ log.puts "#{x_pages_be(@valid_results.size)} valid.".green if @valid_results.size > 0
39
+ log.puts "#{x_pages_be(@invalid_results.size)} invalid.".red if @invalid_results.size > 0
40
+ log.puts 'Open .validation/results.html to view full results.'
41
+ log.puts
42
+ end
43
+
44
+ private
45
+
46
+ def x_pages_be(size)
47
+ if size <= 1
48
+ "#{size} page is"
49
+ else
50
+ "#{size} pages are"
51
+ end
52
+ end
53
+
54
+ def random_name
55
+ (0...8).map { (65 + rand(26)).chr }.join
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ class Headhunter
2
+ module Rack
3
+ class CapturingMiddleware
4
+ def initialize(app, headhunter)
5
+ @app = app
6
+ @hh = headhunter
7
+ end
8
+
9
+ def call(env)
10
+ response = @app.call(env)
11
+ process(response)
12
+ response
13
+ end
14
+
15
+ def process(rack_response)
16
+ status, headers, response = rack_response
17
+
18
+ if html = extract_html_from(response)
19
+ @hh.process!('unknown', html)
20
+ end
21
+ end
22
+
23
+ def extract_html_from(response)
24
+ response[0]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ require 'headhunter'
2
+ require 'headhunter/rack/capturing_middleware'
3
+
4
+ if ENV['HEADHUNTER'] == 'true'
5
+ class Headhunter
6
+ module Rails
7
+ class Railtie < ::Rails::Railtie
8
+ initializer "headhunter.hijack" do |app|
9
+ head_hunter = Headhunter.new(::Rails.root)
10
+
11
+ at_exit do
12
+ head_hunter.report
13
+ head_hunter.clean_up!
14
+ end
15
+
16
+ app.middleware.insert(0, Headhunter::Rack::CapturingMiddleware, head_hunter)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ <tr>
2
+ <td>{{RESOURCE}}</td>
3
+ <td>{{EXCEPTIONS}}</td>
4
+ <td>{{HTML_CONTEXT}}</td>
5
+ <td>
6
+ <a href="{{LINK}}">Full source</a>
7
+ </td>
8
+ </tr>
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Headhunter Results</title>
5
+ </head>
6
+ <body>
7
+ <h1>Headhunter Results</h1>
8
+
9
+ <h2>Invalid results</h2>
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <td>Resource</td>
14
+ <td>Exceptions</td>
15
+ <td>HTML context</td>
16
+ <td>Full source</td>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {{INVALID_RESULTS}}
21
+ </tbody>
22
+ </table>
23
+
24
+ <h2>Valid results</h2>
25
+ <table>
26
+ <thead>
27
+ <tr>
28
+ <td>Resource</td>
29
+ <td>Exceptions</td>
30
+ <td>HTML context</td>
31
+ <td>Full source</td>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ {{VALID_RESULTS}}
36
+ </tbody>
37
+ </table>
38
+ </body>
39
+ </html>
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: headhunter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.0.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Joshua Muheim
@@ -14,23 +13,20 @@ dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: nokogiri
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - '>='
28
25
  - !ruby/object:Gem::Version
29
26
  version: '0'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: css_parser
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ~>
36
32
  - !ruby/object:Gem::Version
@@ -38,7 +34,6 @@ dependencies:
38
34
  type: :runtime
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ~>
44
39
  - !ruby/object:Gem::Version
@@ -46,65 +41,126 @@ dependencies:
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: html_validation
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
- - - ! '>='
45
+ - - '>='
52
46
  - !ruby/object:Gem::Version
53
47
  version: '0'
54
48
  type: :runtime
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
- - - ! '>='
52
+ - - '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
62
55
  - !ruby/object:Gem::Dependency
63
56
  name: colorize
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
- - - ! '>='
59
+ - - '>='
68
60
  - !ruby/object:Gem::Version
69
61
  version: '0'
70
62
  type: :runtime
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
- - - ! '>='
66
+ - - '>='
76
67
  - !ruby/object:Gem::Version
77
68
  version: '0'
78
- description:
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: fuubar
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '2.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '2.0'
125
+ description: Headhunter is an HTML and CSS validation tool that injects itself into
126
+ your Rails feature tests and automagically checks all your generated HTML and CSS
127
+ for validity. In addition, it also looks out for unused (and therefore superfluous)
128
+ CSS selectors.
79
129
  email: josh@muheimwebdesign.ch
80
130
  executables: []
81
131
  extensions: []
82
132
  extra_rdoc_files: []
83
- files: []
133
+ files:
134
+ - lib/headhunter/css_hunter.rb
135
+ - lib/headhunter/css_validator.rb
136
+ - lib/headhunter/html_validator.rb
137
+ - lib/headhunter/rack/capturing_middleware.rb
138
+ - lib/headhunter/rails.rb
139
+ - lib/headhunter/templates/result.html
140
+ - lib/headhunter/templates/results.html
141
+ - lib/headhunter.rb
84
142
  homepage: http://github.com/jmuheim/headhunter
85
143
  licenses:
86
144
  - MIT
145
+ metadata: {}
87
146
  post_install_message:
88
147
  rdoc_options: []
89
148
  require_paths:
90
149
  - lib
91
150
  required_ruby_version: !ruby/object:Gem::Requirement
92
- none: false
93
151
  requirements:
94
- - - ! '>='
152
+ - - '>='
95
153
  - !ruby/object:Gem::Version
96
154
  version: '0'
97
155
  required_rubygems_version: !ruby/object:Gem::Requirement
98
- none: false
99
156
  requirements:
100
- - - ! '>='
157
+ - - '>='
101
158
  - !ruby/object:Gem::Version
102
159
  version: '0'
103
160
  requirements: []
104
161
  rubyforge_project:
105
- rubygems_version: 1.8.25
162
+ rubygems_version: 2.1.11
106
163
  signing_key:
107
- specification_version: 3
108
- summary: An automatic HTML validator hooks into your request/acceptance/feature test
109
- suite and validates your HTML after every request
164
+ specification_version: 4
165
+ summary: Zero config HTML & CSS validation tool for Rails apps
110
166
  test_files: []