headhunter 0.0.1 → 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,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: []