html-proofer 2.5.2 → 3.0.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.
@@ -1,124 +0,0 @@
1
- class LinkCheckable < ::HTML::Proofer::Checkable
2
-
3
- def href
4
- real_attr @href
5
- end
6
-
7
- def id
8
- real_attr @id
9
- end
10
-
11
- def name
12
- real_attr @name
13
- end
14
-
15
- def missing_href?
16
- href.nil? && name.nil? && id.nil?
17
- end
18
-
19
- def placeholder?
20
- (id || name) && href.nil?
21
- end
22
- end
23
-
24
- class LinkCheck < ::HTML::Proofer::CheckRunner
25
- include HTML::Proofer::Utils
26
-
27
- def run
28
- @html.css('a, link').each do |node|
29
- link = LinkCheckable.new(node, self)
30
- line = node.line
31
-
32
- next if link.ignore?
33
- next if link.href =~ /^javascript:/ # can't put this in ignore? because the URI does not parse
34
- next if link.placeholder?
35
-
36
- # is it even a valid URL?
37
- unless link.valid?
38
- add_issue("#{link.href} is an invalid URL", line)
39
- next
40
- end
41
-
42
- check_schemes(link, line)
43
-
44
- # is there even a href?
45
- if link.missing_href?
46
- add_issue('anchor has no href attribute', line)
47
- next
48
- end
49
-
50
- # intentionally here because we still want valid? & missing_href? to execute
51
- next if link.non_http_remote?
52
-
53
- # does the file even exist?
54
- if link.remote?
55
- add_to_external_urls(link.href, line)
56
- next
57
- elsif !link.internal?
58
- add_issue("internally linking to #{link.href}, which does not exist", line) unless link.exists?
59
- end
60
-
61
- # does the local directory have a trailing slash?
62
- if link.unslashed_directory? link.absolute_path
63
- add_issue("internally linking to a directory #{link.absolute_path} without trailing slash", line)
64
- next
65
- end
66
-
67
- # verify the target hash
68
- handle_hash(link, line) if link.hash
69
- end
70
-
71
- external_urls
72
- end
73
-
74
- def check_schemes(link, line)
75
- case link.scheme
76
- when 'mailto'
77
- handle_mailto(link, line)
78
- when 'tel'
79
- handle_tel(link, line)
80
- when 'http'
81
- add_issue("#{link.href} is not an HTTPS link", line) if @options[:enforce_https]
82
- end
83
- end
84
-
85
- def handle_mailto(link, line)
86
- if link.path.empty?
87
- add_issue("#{link.href} contains no email address", line)
88
- elsif !link.path.include?('@')
89
- add_issue("#{link.href} contains an invalid email address", line)
90
- end
91
- end
92
-
93
- def handle_tel(link, line)
94
- add_issue("#{link.href} contains no phone number", line) if link.path.empty?
95
- end
96
-
97
- def handle_hash(link, line)
98
- if link.internal?
99
- unless hash_check @html, link.hash
100
- add_issue("linking to internal hash ##{link.hash} that does not exist", line)
101
- end
102
- elsif link.external?
103
- external_link_check(link, line)
104
- end
105
- end
106
-
107
- def external_link_check(link, line)
108
- if !link.exists?
109
- add_issue("trying to find hash of #{link.href}, but #{link.absolute_path} does not exist", line)
110
- else
111
- target_html = create_nokogiri link.absolute_path
112
- unless hash_check target_html, link.hash
113
- add_issue("linking to #{link.href}, but #{link.hash} does not exist", line)
114
- end
115
- end
116
- end
117
-
118
- def hash_check(html, href_hash)
119
- html.xpath("//*[case_insensitive_equals(@id, '#{href_hash}')]", \
120
- "//*[case_insensitive_equals(@name, '#{href_hash}')]", \
121
- HTML::Proofer::XpathFunctions.new).length > 0
122
- end
123
-
124
- end
@@ -1,40 +0,0 @@
1
- # encoding: utf-8
2
-
3
- class ScriptCheckable < ::HTML::Proofer::Checkable
4
-
5
- def src
6
- real_attr @src
7
- end
8
-
9
- def missing_src?
10
- !src
11
- end
12
-
13
- def blank?
14
- @text.strip.empty?
15
- end
16
-
17
- end
18
-
19
- class ScriptCheck < ::HTML::Proofer::CheckRunner
20
- def run
21
- @html.css('script').each do |node|
22
- script = ScriptCheckable.new(node, self)
23
- line = node.line
24
-
25
- next if script.ignore?
26
- next unless script.blank?
27
-
28
- # does the script exist?
29
- if script.missing_src?
30
- add_issue('script is empty and has no src attribute', line)
31
- elsif script.remote?
32
- add_to_external_urls(script.src, line)
33
- else
34
- add_issue("internal script #{script.src} does not exist", line) unless script.exists?
35
- end
36
- end
37
-
38
- external_urls
39
- end
40
- end
@@ -1,42 +0,0 @@
1
- require 'yell'
2
- require 'colored'
3
-
4
- module HTML
5
- class Proofer
6
- class Log
7
- include Yell::Loggable
8
-
9
- def initialize(verbose, verbosity = nil)
10
- log_level = if verbosity.nil?
11
- verbose ? :debug : :info
12
- else
13
- verbosity
14
- end
15
-
16
- @logger = Yell.new(:format => false, \
17
- :name => 'HTML::Proofer', \
18
- :level => "gte.#{log_level}") do |l|
19
- l.adapter :stdout, :level => [:debug, :info, :warn]
20
- l.adapter :stderr, :level => [:error, :fatal]
21
- end
22
- end
23
-
24
- def log(level, color, message)
25
- @logger.send level, colorize(color, message)
26
- end
27
-
28
- def colorize(color, message)
29
- if $stdout.isatty && $stderr.isatty
30
- Colored.colorize(message, foreground: color)
31
- else
32
- message
33
- end
34
- end
35
-
36
- # dumb override to play nice with Typhoeus/Ethon
37
- def debug(message = nil)
38
- log(:debug, :yellow, message) unless message.nil?
39
- end
40
- end
41
- end
42
- end
@@ -1,179 +0,0 @@
1
- require 'typhoeus'
2
- require 'uri'
3
- require_relative './utils'
4
-
5
- module HTML
6
- class Proofer
7
- class UrlValidator
8
- include HTML::Proofer::Utils
9
-
10
- attr_accessor :logger, :external_urls, :iterable_external_urls, :hydra
11
-
12
- def initialize(logger, external_urls, options, typhoeus_opts, hydra_opts)
13
- @logger = logger
14
- @external_urls = external_urls
15
- @iterable_external_urls = {}
16
- @failed_tests = []
17
- @options = options
18
- @hydra = Typhoeus::Hydra.new(hydra_opts)
19
- @typhoeus_opts = typhoeus_opts
20
- @external_domain_paths_with_queries = {}
21
- end
22
-
23
- def run
24
- @iterable_external_urls = remove_query_values
25
- external_link_checker(@iterable_external_urls)
26
- @failed_tests
27
- end
28
-
29
- def remove_query_values
30
- return if @external_urls.nil?
31
- iterable_external_urls = @external_urls.dup
32
- @external_urls.keys.each do |url|
33
- uri = begin
34
- Addressable::URI.parse(url)
35
- rescue URI::Error, Addressable::URI::InvalidURIError
36
- @logger.log :error, :red, "#{url} is an invalid URL"
37
- nil
38
- end
39
- next if uri.nil? || uri.query.nil?
40
- iterable_external_urls.delete(url) unless new_url_query_values?(uri)
41
- end
42
- iterable_external_urls
43
- end
44
-
45
- # remember queries we've seen, ignore future ones
46
- def new_url_query_values?(uri)
47
- queries = uri.query_values.keys.join('-')
48
- domain_path = extract_domain_path(uri)
49
- if @external_domain_paths_with_queries[domain_path].nil?
50
- @external_domain_paths_with_queries[domain_path] = [queries]
51
- true
52
- elsif !@external_domain_paths_with_queries[domain_path].include?(queries)
53
- @external_domain_paths_with_queries[domain_path] << queries
54
- true
55
- else
56
- false
57
- end
58
- end
59
-
60
- def extract_domain_path(uri)
61
- uri.host + uri.path
62
- end
63
-
64
- # Proofer runs faster if we pull out all the external URLs and run the checks
65
- # at the end. Otherwise, we're halting the consuming process for every file during
66
- # the check_directory_of_files process.
67
- #
68
- # In addition, sorting the list lets libcurl keep connections to the same hosts alive.
69
- #
70
- # Finally, we'll first make a HEAD request, rather than GETing all the contents.
71
- # If the HEAD fails, we'll fall back to GET, as some servers are not configured
72
- # for HEAD. If we've decided to check for hashes, we must do a GET--HEAD is
73
- # not an option.
74
- def external_link_checker(external_urls)
75
- external_urls = Hash[external_urls.sort]
76
-
77
- count = external_urls.length
78
- check_text = "#{count} " << (count == 1 ? 'external link' : 'external links')
79
- logger.log :info, :blue, "Checking #{check_text}..."
80
-
81
- Ethon.logger = logger # log from Typhoeus/Ethon
82
-
83
- url_processor(external_urls)
84
-
85
- logger.log :debug, :yellow, "Running requests for all #{hydra.queued_requests.size} external URLs..."
86
- hydra.run
87
- end
88
-
89
- def url_processor(external_urls)
90
- external_urls.each_pair do |href, filenames|
91
- href = begin
92
- clean_url(href)
93
- rescue URI::Error, Addressable::URI::InvalidURIError
94
- add_external_issue(filenames, "#{href} is an invalid URL")
95
- next
96
- end
97
-
98
- if hash?(href) && @options[:check_external_hash]
99
- queue_request(:get, href, filenames)
100
- else
101
- queue_request(:head, href, filenames)
102
- end
103
- end
104
- end
105
-
106
- def clean_url(href)
107
- Addressable::URI.parse(href).normalize
108
- end
109
-
110
- def queue_request(method, href, filenames)
111
- request = Typhoeus::Request.new(href, @typhoeus_opts.merge({ :method => method }))
112
- request.on_complete { |response| response_handler(response, filenames) }
113
- hydra.queue request
114
- end
115
-
116
- def response_handler(response, filenames)
117
- effective_url = response.options[:effective_url]
118
- href = response.request.base_url.to_s
119
- method = response.request.options[:method]
120
- response_code = response.code
121
-
122
- debug_msg = "Received a #{response_code} for #{href}"
123
- debug_msg << " in #{filenames.join(' ')}" unless filenames.nil?
124
- logger.log :debug, :yellow, debug_msg
125
-
126
- if response_code.between?(200, 299)
127
- check_hash_in_2xx_response(href, effective_url, response, filenames)
128
- elsif response.timed_out?
129
- handle_timeout(href, filenames, response_code)
130
- elsif method == :head
131
- queue_request(:get, href, filenames)
132
- else
133
- return if @options[:only_4xx] && !response_code.between?(400, 499)
134
- # Received a non-successful http response.
135
- add_external_issue(filenames, "External link #{href} failed: #{response_code} #{response.return_message}", response_code)
136
- end
137
- end
138
-
139
- # Even though the response was a success, we may have been asked to check
140
- # if the hash on the URL exists on the page
141
- def check_hash_in_2xx_response(href, effective_url, response, filenames)
142
- return if @options[:only_4xx]
143
- return unless @options[:check_external_hash]
144
- return unless (hash = hash?(href))
145
-
146
- body_doc = create_nokogiri(response.body)
147
-
148
- # user-content is a special addition by GitHub.
149
- xpath = %(//*[@name="#{hash}"]|//*[@id="#{hash}"])
150
- if URI.parse(href).host.match(/github\.com/i)
151
- xpath << %(|//*[@name="user-content-#{hash}"]|//*[@id="user-content-#{hash}"])
152
- end
153
-
154
- return unless body_doc.xpath(xpath).empty?
155
-
156
- add_external_issue filenames, "External link #{href} failed: #{effective_url} exists, but the hash '#{hash}' does not", response.code
157
- end
158
-
159
- def handle_timeout(href, filenames, response_code)
160
- return if @options[:only_4xx]
161
- add_external_issue filenames, "External link #{href} failed: got a time out", response_code
162
- end
163
-
164
- def add_external_issue(filenames, desc, status = nil)
165
- if filenames.nil?
166
- @failed_tests << CheckRunner::Issue.new('', desc, nil, status)
167
- else
168
- filenames.each { |f| @failed_tests << CheckRunner::Issue.new(f, desc, nil, status) }
169
- end
170
- end
171
-
172
- def hash?(url)
173
- URI.parse(url).fragment
174
- rescue URI::InvalidURIError
175
- nil
176
- end
177
- end
178
- end
179
- end
@@ -1,26 +0,0 @@
1
- require 'nokogiri'
2
-
3
- module HTML
4
- class Proofer
5
- module Utils
6
- def create_nokogiri(path)
7
- if File.exist? path
8
- content = File.open(path).read
9
- else
10
- content = path
11
- end
12
-
13
- Nokogiri::HTML(content)
14
- end
15
- module_function :create_nokogiri
16
-
17
- def swap(href, replacement)
18
- replacement.each do |link, replace|
19
- href = href.gsub(link, replace)
20
- end
21
- href
22
- end
23
- module_function :swap
24
- end
25
- end
26
- end
@@ -1,5 +0,0 @@
1
- module HTML
2
- class Proofer
3
- VERSION = '2.5.2'
4
- end
5
- end
@@ -1,9 +0,0 @@
1
- module HTML
2
- class Proofer
3
- class XpathFunctions
4
- def case_insensitive_equals(node_set, str_to_match)
5
- node_set.find_all { |node| node.to_s.downcase == str_to_match.to_s.downcase }
6
- end
7
- end
8
- end
9
- end
data/lib/html/proofer.rb DELETED
@@ -1,202 +0,0 @@
1
- def require_all(path)
2
- glob = File.join(File.dirname(__FILE__), path, '*.rb')
3
- Dir[glob].each do |f|
4
- require f
5
- end
6
- end
7
-
8
- require_all 'proofer'
9
- require_all 'proofer/check_runner'
10
- require_all 'proofer/checks'
11
- require_relative './proofer/utils'
12
- require_relative './proofer/xpathfunctions'
13
-
14
- require 'parallel'
15
-
16
- begin
17
- require 'awesome_print'
18
- rescue LoadError; end
19
-
20
- module HTML
21
-
22
- class Proofer
23
- include HTML::Proofer::Utils
24
-
25
- attr_reader :options, :typhoeus_opts, :hydra_opts, :parallel_opts, :validation_opts, :external_urls, :iterable_external_urls
26
-
27
- TYPHOEUS_DEFAULTS = {
28
- :followlocation => true,
29
- :headers => {
30
- 'User-Agent' => "Mozilla/5.0 (compatible; HTML Proofer/#{VERSION}; +https://github.com/gjtorikian/html-proofer)"
31
- }
32
- }
33
-
34
- def initialize(src, opts = {})
35
- @src = src
36
-
37
- if opts[:verbose]
38
- warn '`@options[:verbose]` will be removed in a future 3.x.x release: http://git.io/vGHHh'
39
- end
40
- if opts[:href_ignore]
41
- warn '`@options[:href_ignore]` will be renamed in a future 3.x.x release: http://git.io/vGHHy'
42
- end
43
-
44
- @proofer_opts = {
45
- :ext => '.html',
46
- :check_favicon => false,
47
- :href_swap => [],
48
- :href_ignore => [],
49
- :file_ignore => [],
50
- :url_ignore => [],
51
- :check_external_hash => false,
52
- :alt_ignore => [],
53
- :empty_alt_ignore => false,
54
- :enforce_https => false,
55
- :disable_external => false,
56
- :verbose => false,
57
- :only_4xx => false,
58
- :directory_index_file => 'index.html',
59
- :check_html => false,
60
- :error_sort => :path,
61
- :checks_to_ignore => []
62
- }
63
-
64
- @typhoeus_opts = TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
65
- opts.delete(:typhoeus)
66
-
67
- @hydra_opts = opts[:hydra] || {}
68
- opts.delete(:hydra)
69
-
70
- # fall back to parallel defaults
71
- @parallel_opts = opts[:parallel] || {}
72
- opts.delete(:parallel)
73
-
74
- @validation_opts = opts[:validation] || {}
75
- opts.delete(:validation)
76
-
77
- @options = @proofer_opts.merge(opts)
78
-
79
- @failed_tests = []
80
- end
81
-
82
- def logger
83
- @logger ||= HTML::Proofer::Log.new(@options[:verbose], @options[:verbosity])
84
- end
85
-
86
- def run
87
- count = checks.length
88
- check_text = "#{checks} " << (count == 1 ? 'check' : 'checks')
89
- logger.log :info, :blue, "Running #{check_text} on #{@src} on *#{@options[:ext]}... \n\n"
90
-
91
- if @src.is_a?(Array) && !@options[:disable_external]
92
- check_list_of_links
93
- else
94
- check_directory_of_files
95
- end
96
-
97
- if @failed_tests.empty?
98
- logger.log :info, :green, 'HTML-Proofer finished successfully.'
99
- else
100
- print_failed_tests
101
- end
102
- end
103
-
104
- def check_list_of_links
105
- if @options[:href_swap]
106
- @src = @src.map do |url|
107
- swap(url, @options[:href_swap])
108
- end
109
- end
110
- @external_urls = Hash[*@src.map { |s| [s, nil] }.flatten]
111
- validate_urls
112
- end
113
-
114
- # Collects any external URLs found in a directory of files. Also collectes
115
- # every failed test from check_files_for_internal_woes.
116
- # Sends the external URLs to Typhoeus for batch processing.
117
- def check_directory_of_files
118
- @external_urls = {}
119
- results = check_files_for_internal_woes
120
-
121
- results.each do |item|
122
- @external_urls.merge!(item[:external_urls])
123
- @failed_tests.concat(item[:failed_tests])
124
- end
125
-
126
- validate_urls unless @options[:disable_external]
127
-
128
- logger.log :info, :blue, "Ran on #{files.length} files!\n\n"
129
- end
130
-
131
- # Walks over each implemented check and runs them on the files, in parallel.
132
- def check_files_for_internal_woes
133
- Parallel.map(files, @parallel_opts) do |path|
134
- html = create_nokogiri(path)
135
- result = { :external_urls => {}, :failed_tests => [] }
136
-
137
- checks.each do |klass|
138
- logger.log :debug, :yellow, "Checking #{klass.to_s.downcase} on #{path} ..."
139
- check = Object.const_get(klass).new(@src, path, html, @options, @typhoeus_opts, @hydra_opts, @parallel_opts, @validation_opts)
140
- check.run
141
- result[:external_urls].merge!(check.external_urls)
142
- result[:failed_tests].concat(check.issues) if check.issues.length > 0
143
- end
144
- result
145
- end
146
- end
147
-
148
- def validate_urls
149
- url_validator = HTML::Proofer::UrlValidator.new(logger, @external_urls, @options, @typhoeus_opts, @hydra_opts)
150
- @failed_tests.concat(url_validator.run)
151
- @iterable_external_urls = url_validator.iterable_external_urls
152
- end
153
-
154
- def files
155
- if File.directory? @src
156
- pattern = File.join(@src, '**', "*#{@options[:ext]}")
157
- files = Dir.glob(pattern).select { |fn| File.file? fn }
158
- files.reject { |f| ignore_file?(f) }
159
- elsif File.extname(@src) == @options[:ext]
160
- [@src].reject { |f| ignore_file?(f) }
161
- else
162
- []
163
- end
164
- end
165
-
166
- def ignore_file?(file)
167
- options[:file_ignore].each do |pattern|
168
- return true if pattern.is_a?(String) && pattern == file
169
- return true if pattern.is_a?(Regexp) && pattern =~ file
170
- end
171
-
172
- false
173
- end
174
-
175
- def checks
176
- return @checks unless @checks.nil?
177
- @checks = HTML::Proofer::CheckRunner.checks.map(&:name)
178
- @checks.delete('FaviconCheck') unless @options[:check_favicon]
179
- @checks.delete('HtmlCheck') unless @options[:check_html]
180
- @options[:checks_to_ignore].each do |ignored|
181
- @checks.delete(ignored)
182
- end
183
- @checks
184
- end
185
-
186
- def failed_tests
187
- return [] if @failed_tests.empty?
188
- result = []
189
- @failed_tests.each { |f| result << f.to_s }
190
- result
191
- end
192
-
193
- def print_failed_tests
194
- sorted_failures = HTML::Proofer::CheckRunner::SortedIssues.new(@failed_tests, @options[:error_sort], logger)
195
-
196
- sorted_failures.sort_and_report
197
- count = @failed_tests.length
198
- failure_text = "#{count} " << (count == 1 ? 'failure' : 'failures')
199
- fail logger.colorize :red, "HTML-Proofer found #{failure_text}!"
200
- end
201
- end
202
- end