html-proofer 3.19.4 → 4.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/htmlproofer +30 -57
- data/lib/html-proofer.rb +1 -54
- data/lib/html_proofer/attribute/url.rb +231 -0
- data/lib/html_proofer/attribute.rb +15 -0
- data/lib/html_proofer/cache.rb +234 -0
- data/lib/html_proofer/check/favicon.rb +35 -0
- data/lib/html_proofer/check/images.rb +62 -0
- data/lib/html_proofer/check/links.rb +118 -0
- data/lib/html_proofer/check/open_graph.rb +34 -0
- data/lib/html_proofer/check/scripts.rb +38 -0
- data/lib/html_proofer/check.rb +91 -0
- data/lib/{html-proofer → html_proofer}/configuration.rb +30 -31
- data/lib/html_proofer/element.rb +122 -0
- data/lib/html_proofer/failure.rb +17 -0
- data/lib/{html-proofer → html_proofer}/log.rb +0 -0
- data/lib/html_proofer/reporter/cli.rb +29 -0
- data/lib/html_proofer/reporter.rb +23 -0
- data/lib/html_proofer/runner.rb +245 -0
- data/lib/html_proofer/url_validator/external.rb +189 -0
- data/lib/html_proofer/url_validator/internal.rb +86 -0
- data/lib/html_proofer/url_validator.rb +16 -0
- data/lib/{html-proofer → html_proofer}/utils.rb +5 -8
- data/lib/{html-proofer → html_proofer}/version.rb +1 -1
- data/lib/html_proofer/xpath_functions.rb +10 -0
- data/lib/html_proofer.rb +56 -0
- metadata +46 -27
- data/lib/html-proofer/cache.rb +0 -194
- data/lib/html-proofer/check/favicon.rb +0 -29
- data/lib/html-proofer/check/html.rb +0 -37
- data/lib/html-proofer/check/images.rb +0 -48
- data/lib/html-proofer/check/links.rb +0 -182
- data/lib/html-proofer/check/opengraph.rb +0 -46
- data/lib/html-proofer/check/scripts.rb +0 -42
- data/lib/html-proofer/check.rb +0 -75
- data/lib/html-proofer/element.rb +0 -265
- data/lib/html-proofer/issue.rb +0 -65
- data/lib/html-proofer/middleware.rb +0 -82
- data/lib/html-proofer/runner.rb +0 -249
- data/lib/html-proofer/url_validator.rb +0 -237
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Check::Favicon < HTMLProofer::Check
|
4
|
+
def run
|
5
|
+
found = false
|
6
|
+
@html.css('link').each do |node|
|
7
|
+
@favicon = create_element(node)
|
8
|
+
|
9
|
+
next if @favicon.ignore?
|
10
|
+
|
11
|
+
break if (found = @favicon.node['rel'].split.last.eql? 'icon')
|
12
|
+
end
|
13
|
+
|
14
|
+
return if immediate_redirect?
|
15
|
+
|
16
|
+
if found
|
17
|
+
if @favicon.url.remote?
|
18
|
+
add_to_external_urls(@favicon.url, @favicon.line)
|
19
|
+
elsif !@favicon.url.exists?
|
20
|
+
add_failure("internal favicon #{@favicon.url.raw_attribute} does not exist", line: @favicon.line, content: @favicon.content)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
add_failure('no favicon provided')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# allow any instant-redirect meta tag
|
30
|
+
def immediate_redirect?
|
31
|
+
@html.xpath("//meta[@http-equiv='refresh']").attribute('content').value.start_with? '0;'
|
32
|
+
rescue StandardError
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Check::Images < HTMLProofer::Check
|
4
|
+
SCREEN_SHOT_REGEX = /Screen(?: |%20)Shot(?: |%20)\d+-\d+-\d+(?: |%20)at(?: |%20)\d+.\d+.\d+/.freeze
|
5
|
+
|
6
|
+
def run
|
7
|
+
@html.css('img').each do |node|
|
8
|
+
@img = create_element(node)
|
9
|
+
|
10
|
+
next if @img.ignore?
|
11
|
+
|
12
|
+
# screenshot filenames should return because of terrible names
|
13
|
+
add_failure("image has a terrible filename (#{@img.url.raw_attribute})", line: @img.line, content: @img.content) if terrible_filename?
|
14
|
+
|
15
|
+
# does the image exist?
|
16
|
+
if missing_src?
|
17
|
+
add_failure('image has no src or srcset attribute', line: @img.line, content: @img.content)
|
18
|
+
elsif @img.url.remote?
|
19
|
+
add_to_external_urls(@img.url, @img.line)
|
20
|
+
elsif !@img.url.exists? && !@img.multiple_srcsets?
|
21
|
+
add_failure("internal image #{@img.url.raw_attribute} does not exist", line: @img.line, content: @img.content)
|
22
|
+
elsif @img.multiple_srcsets?
|
23
|
+
srcsets = @img.srcset.split(',').map(&:strip)
|
24
|
+
srcsets.each do |srcset|
|
25
|
+
srcset_url = HTMLProofer::Attribute::Url.new(@runner, srcset, base_url: @img.base_url)
|
26
|
+
|
27
|
+
if srcset_url.remote?
|
28
|
+
add_to_external_urls(srcset_url.url, @img.line)
|
29
|
+
elsif !srcset_url.exists?
|
30
|
+
add_failure("internal image #{srcset} does not exist", line: @img.line, content: @img.content)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
add_failure("image #{@img.url.raw_attribute} does not have an alt attribute", line: @img.line, content: @img.content) if empty_alt_tag? && !ignore_missing_alt? && !ignore_alt?
|
36
|
+
|
37
|
+
add_failure("image #{@img.url.raw_attribute} uses the http scheme", line: @img.line, content: @img.content) if @runner.enforce_https? && @img.url.http?
|
38
|
+
end
|
39
|
+
|
40
|
+
external_urls
|
41
|
+
end
|
42
|
+
|
43
|
+
def ignore_missing_alt?
|
44
|
+
@runner.options[:ignore_missing_alt]
|
45
|
+
end
|
46
|
+
|
47
|
+
def ignore_alt?
|
48
|
+
@img.url.ignore? || @img.aria_hidden?
|
49
|
+
end
|
50
|
+
|
51
|
+
def empty_alt_tag?
|
52
|
+
@img.node['alt'].nil? || @img.node['alt'].strip.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def terrible_filename?
|
56
|
+
@img.url.to_s =~ SCREEN_SHOT_REGEX
|
57
|
+
end
|
58
|
+
|
59
|
+
def missing_src?
|
60
|
+
blank?(@img.url.to_s)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Check::Links < HTMLProofer::Check
|
4
|
+
def run
|
5
|
+
@html.css('a, link, source').each do |node|
|
6
|
+
@link = create_element(node)
|
7
|
+
|
8
|
+
next if @link.ignore?
|
9
|
+
|
10
|
+
if !allow_hash_href? && @link.node['href'] == '#'
|
11
|
+
add_failure('linking to internal hash #, which points to nowhere', line: @link.line, content: @link.content)
|
12
|
+
next
|
13
|
+
end
|
14
|
+
|
15
|
+
# is there even an href?
|
16
|
+
if blank?(@link.url.raw_attribute)
|
17
|
+
next if allow_missing_href?
|
18
|
+
|
19
|
+
add_failure("'#{@link.node.name}' tag is missing a reference", line: @link.line, content: @link.content)
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
# is it even a valid URL?
|
24
|
+
unless @link.url.valid?
|
25
|
+
add_failure("#{@link.href} is an invalid URL", line: @link.line, content: @link.content)
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
check_schemes
|
30
|
+
|
31
|
+
# intentionally down here because we still want valid? & missing_href? to execute
|
32
|
+
next if @link.url.non_http_remote?
|
33
|
+
|
34
|
+
if !@link.url.internal? && @link.url.remote?
|
35
|
+
check_sri if @runner.check_sri? && @link.link_tag?
|
36
|
+
|
37
|
+
# we need to skip these for now; although the domain main be valid,
|
38
|
+
# curl/Typheous inaccurately return 404s for some links. cc https://git.io/vyCFx
|
39
|
+
next if @link.node['rel'] == 'dns-prefetch'
|
40
|
+
|
41
|
+
unless @link.url.path?
|
42
|
+
add_failure("#{@link.url.raw_attribute} is an invalid URL", line: @link.line, content: @link.content)
|
43
|
+
next
|
44
|
+
end
|
45
|
+
|
46
|
+
add_to_external_urls(@link.url, @link.line)
|
47
|
+
elsif @link.url.internal?
|
48
|
+
# does the local directory have a trailing slash?
|
49
|
+
add_failure("internally linking to a directory #{@link.url.raw_attribute} without trailing slash", line: @link.line, content: @link.content) if @link.url.unslashed_directory?(@link.url.absolute_path)
|
50
|
+
|
51
|
+
add_to_internal_urls(@link.url, @link.line)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
external_urls
|
56
|
+
end
|
57
|
+
|
58
|
+
def allow_missing_href?
|
59
|
+
@runner.options[:allow_missing_href]
|
60
|
+
end
|
61
|
+
|
62
|
+
def allow_hash_href?
|
63
|
+
@runner.options[:allow_hash_href]
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_schemes
|
67
|
+
case @link.url.scheme
|
68
|
+
when 'mailto'
|
69
|
+
handle_mailto
|
70
|
+
when 'tel'
|
71
|
+
handle_tel
|
72
|
+
when 'http'
|
73
|
+
return unless @runner.options[:enforce_https]
|
74
|
+
|
75
|
+
add_failure("#{@link.url.raw_attribute} is not an HTTPS link", line: @link.line, content: @link.content)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_mailto
|
80
|
+
if @link.url.path.empty?
|
81
|
+
add_failure("#{@link.url.raw_attribute} contains no email address", line: @link.line, content: @link.content) unless ignore_empty_mailto?
|
82
|
+
elsif !/#{URI::MailTo::EMAIL_REGEXP}/o.match?(@link.url.path)
|
83
|
+
add_failure("#{@link.url.raw_attribute} contains an invalid email address", line: @link.line, content: @link.content)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_tel
|
88
|
+
add_failure("#{@link.url.raw_attribute} contains no phone number", line: @link.line, content: @link.content) if @link.url.path.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
def ignore_empty_mailto?
|
92
|
+
@runner.options[:ignore_empty_mailto]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Whitelist for affected elements from Subresource Integrity specification
|
96
|
+
# https://w3c.github.io/webappsec-subresource-integrity/#link-element-for-stylesheets
|
97
|
+
SRI_REL_TYPES = %(stylesheet)
|
98
|
+
|
99
|
+
def check_sri
|
100
|
+
return unless SRI_REL_TYPES.include?(@link.node['rel'])
|
101
|
+
|
102
|
+
if blank?(@link.node['integrity']) && blank?(@link.node['crossorigin'])
|
103
|
+
add_failure("SRI and CORS not provided in: #{@link.url.raw_attribute}", line: @link.line, content: @link.content)
|
104
|
+
elsif blank?(@link.node['integrity'])
|
105
|
+
add_failure("Integrity is missing in: #{@link.url.raw_attribute}", line: @link.line, content: @link.content)
|
106
|
+
elsif blank?(@link.node['crossorigin'])
|
107
|
+
add_failure("CORS not provided for external resource in: #{@link.link.url.raw_attribute}", line: @link.line, content: @link.content)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private def source_tag?
|
112
|
+
@link.node.name == 'source'
|
113
|
+
end
|
114
|
+
|
115
|
+
private def anchor_tag?
|
116
|
+
@link.node.name == 'a'
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Check::OpenGraph < HTMLProofer::Check
|
4
|
+
def run
|
5
|
+
@html.css('meta[property="og:url"], meta[property="og:image"]').each do |node|
|
6
|
+
@open_graph = create_element(node)
|
7
|
+
|
8
|
+
next if @open_graph.ignore?
|
9
|
+
|
10
|
+
# does the open_graph exist?
|
11
|
+
if missing_content?
|
12
|
+
add_failure('open graph has no content attribute', line: @open_graph.line, content: @open_graph.content)
|
13
|
+
elsif empty_content?
|
14
|
+
add_failure('open graph content attribute is empty', line: @open_graph.line, content: @open_graph.content)
|
15
|
+
elsif !@open_graph.url.valid?
|
16
|
+
add_failure("#{@open_graph.src} is an invalid URL", line: @open_graph.line)
|
17
|
+
elsif @open_graph.url.remote?
|
18
|
+
add_to_external_urls(@open_graph.url, @open_graph.line)
|
19
|
+
else
|
20
|
+
add_failure("internal open graph #{@open_graph.url.raw_attribute} does not exist", line: @open_graph.line, content: @open_graph.content) unless @open_graph.url.exists?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
external_urls
|
25
|
+
end
|
26
|
+
|
27
|
+
private def missing_content?
|
28
|
+
@open_graph.node['content'].nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
private def empty_content?
|
32
|
+
@open_graph.node['content'].empty?
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Check::Scripts < HTMLProofer::Check
|
4
|
+
def run
|
5
|
+
@html.css('script').each do |node|
|
6
|
+
@script = create_element(node)
|
7
|
+
|
8
|
+
next if @script.ignore?
|
9
|
+
next unless @script.content.strip.empty?
|
10
|
+
|
11
|
+
# does the script exist?
|
12
|
+
if missing_src?
|
13
|
+
add_failure('script is empty and has no src attribute', line: @script.line, content: @script.content)
|
14
|
+
elsif @script.url.remote?
|
15
|
+
add_to_external_urls(@script.src, @script.line)
|
16
|
+
check_sri if @runner.check_sri?
|
17
|
+
elsif !@script.url.exists?
|
18
|
+
add_failure("internal script reference #{@script.src} does not exist", line: @script.line, content: @script.content)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
external_urls
|
23
|
+
end
|
24
|
+
|
25
|
+
def missing_src?
|
26
|
+
@script.node['src'].nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_sri
|
30
|
+
if blank?(@script.node['integrity']) && blank?(@script.node['crossorigin'])
|
31
|
+
add_failure("SRI and CORS not provided in: #{@script.url.raw_attribute}", line: @script.line, content: @script.content)
|
32
|
+
elsif blank?(@script.node['integrity'])
|
33
|
+
add_failure("Integrity is missing in: #{@script.url.raw_attribute}", line: @script.line, content: @script.content)
|
34
|
+
elsif blank?(@script.node['crossorigin'])
|
35
|
+
add_failure("CORS not provided for external resource in: #{@script.url.raw_attribute}", line: @script.line, content: @script.content)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTMLProofer
|
4
|
+
# Mostly handles issue management and collecting of external URLs.
|
5
|
+
class Check
|
6
|
+
include HTMLProofer::Utils
|
7
|
+
|
8
|
+
attr_reader :failures, :options, :internal_urls, :external_urls
|
9
|
+
|
10
|
+
def initialize(runner, html)
|
11
|
+
@runner = runner
|
12
|
+
@html = remove_ignored(html)
|
13
|
+
|
14
|
+
@external_urls = {}
|
15
|
+
@internal_urls = {}
|
16
|
+
@failures = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_element(node)
|
20
|
+
Element.new(@runner, node, base_url: base_url)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
raise NotImplementedError, 'HTMLProofer::Check subclasses must implement #run'
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_failure(description, line: nil, status: nil, content: nil)
|
28
|
+
@failures << Failure.new(@runner.current_path, short_name, description, line: line, status: status, content: content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.subchecks(runner_options)
|
32
|
+
# grab all known checks
|
33
|
+
checks = ObjectSpace.each_object(Class).select do |klass|
|
34
|
+
klass < self
|
35
|
+
end
|
36
|
+
|
37
|
+
# remove any checks not explicitly included
|
38
|
+
checks.each_with_object([]) do |check, arr|
|
39
|
+
next unless runner_options[:checks].include?(check.short_name)
|
40
|
+
|
41
|
+
arr << check
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def short_name
|
46
|
+
self.class.name.split('::').last
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.short_name
|
50
|
+
name.split('::').last
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_to_internal_urls(url, line)
|
54
|
+
url_string = url.raw_attribute
|
55
|
+
|
56
|
+
@internal_urls[url_string] = [] if @internal_urls[url_string].nil?
|
57
|
+
|
58
|
+
metadata = {
|
59
|
+
source: @runner.current_source,
|
60
|
+
current_path: @runner.current_path,
|
61
|
+
line: line,
|
62
|
+
base_url: base_url,
|
63
|
+
found: nil
|
64
|
+
}
|
65
|
+
@internal_urls[url_string] << metadata
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_to_external_urls(url, line)
|
69
|
+
url_string = url.to_s
|
70
|
+
|
71
|
+
@external_urls[url_string] = [] if @external_urls[url_string].nil?
|
72
|
+
|
73
|
+
@external_urls[url_string] << { filename: @runner.current_path, line: line }
|
74
|
+
end
|
75
|
+
|
76
|
+
private def base_url
|
77
|
+
return @base_url if defined?(@base_url)
|
78
|
+
|
79
|
+
return (@base_url = '') if (base = @html.at_css('base')).nil?
|
80
|
+
|
81
|
+
@base_url = base['href']
|
82
|
+
end
|
83
|
+
|
84
|
+
private def remove_ignored(html)
|
85
|
+
return if html.nil?
|
86
|
+
|
87
|
+
html.css('code, pre, tt').each(&:unlink)
|
88
|
+
html
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -2,35 +2,27 @@
|
|
2
2
|
|
3
3
|
module HTMLProofer
|
4
4
|
module Configuration
|
5
|
-
|
5
|
+
DEFAULT_TESTS = %w[Links Images Scripts].freeze
|
6
6
|
|
7
7
|
PROOFER_DEFAULTS = {
|
8
|
+
allow_hash_href: true,
|
8
9
|
allow_missing_href: false,
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
check_external_hash: false,
|
13
|
-
check_favicon: false,
|
14
|
-
check_html: false,
|
15
|
-
check_img_http: false,
|
16
|
-
check_opengraph: false,
|
17
|
-
checks_to_ignore: [],
|
18
|
-
check_sri: false,
|
10
|
+
assume_extension: '.html',
|
11
|
+
check_external_hash: true,
|
12
|
+
checks: DEFAULT_TESTS,
|
19
13
|
directory_index_file: 'index.html',
|
20
14
|
disable_external: false,
|
21
|
-
empty_alt_ignore: false,
|
22
|
-
enforce_https: false,
|
23
|
-
error_sort: :path,
|
24
|
-
extension: '.html',
|
25
|
-
external_only: false,
|
26
|
-
file_ignore: [],
|
27
|
-
http_status_ignore: [],
|
28
|
-
internal_domains: [],
|
29
|
-
log_level: :info,
|
30
15
|
ignore_empty_mailto: false,
|
16
|
+
ignore_files: [],
|
17
|
+
ignore_missing_alt: false,
|
18
|
+
ignore_status_codes: [],
|
19
|
+
ignore_urls: [],
|
20
|
+
enforce_https: true,
|
21
|
+
extensions: ['.html'],
|
22
|
+
log_level: :info,
|
31
23
|
only_4xx: false,
|
32
|
-
|
33
|
-
|
24
|
+
swap_attributes: {},
|
25
|
+
swap_urls: {}
|
34
26
|
}.freeze
|
35
27
|
|
36
28
|
TYPHOEUS_DEFAULTS = {
|
@@ -47,19 +39,26 @@ module HTMLProofer
|
|
47
39
|
max_concurrency: 50
|
48
40
|
}.freeze
|
49
41
|
|
50
|
-
PARALLEL_DEFAULTS = {
|
51
|
-
|
52
|
-
VALIDATION_DEFAULTS = {
|
53
|
-
report_script_embeds: false,
|
54
|
-
report_missing_names: false,
|
55
|
-
report_invalid_tags: false,
|
56
|
-
report_missing_doctype: false,
|
57
|
-
report_eof_tags: false,
|
58
|
-
report_mismatched_tags: false
|
42
|
+
PARALLEL_DEFAULTS = {
|
43
|
+
enable: true
|
59
44
|
}.freeze
|
60
45
|
|
61
46
|
CACHE_DEFAULTS = {}.freeze
|
62
47
|
|
48
|
+
def self.generate_defaults(opts)
|
49
|
+
options = PROOFER_DEFAULTS.merge(opts)
|
50
|
+
|
51
|
+
options[:typhoeus] = HTMLProofer::Configuration::TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
|
52
|
+
options[:hydra] = HTMLProofer::Configuration::HYDRA_DEFAULTS.merge(opts[:hydra] || {})
|
53
|
+
|
54
|
+
options[:parallel] = HTMLProofer::Configuration::PARALLEL_DEFAULTS.merge(opts[:parallel] || {})
|
55
|
+
options[:cache] = HTMLProofer::Configuration::CACHE_DEFAULTS.merge(opts[:cache] || {})
|
56
|
+
|
57
|
+
options.delete(:src)
|
58
|
+
|
59
|
+
options
|
60
|
+
end
|
61
|
+
|
63
62
|
def self.to_regex?(item)
|
64
63
|
if item.start_with?('/') && item.end_with?('/')
|
65
64
|
Regexp.new item[1...-1]
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
module HTMLProofer
|
6
|
+
# Represents the element currently being processed
|
7
|
+
class Element
|
8
|
+
include HTMLProofer::Utils
|
9
|
+
|
10
|
+
attr_reader :node, :url, :base_url, :line, :content
|
11
|
+
|
12
|
+
def initialize(runner, node, base_url: nil)
|
13
|
+
@runner = runner
|
14
|
+
@node = node
|
15
|
+
|
16
|
+
@base_url = base_url
|
17
|
+
@url = Attribute::Url.new(runner, link_attribute, base_url: base_url)
|
18
|
+
|
19
|
+
@line = node.line
|
20
|
+
@content = node.content
|
21
|
+
end
|
22
|
+
|
23
|
+
def link_attribute
|
24
|
+
meta_content || src || srcset || href
|
25
|
+
end
|
26
|
+
|
27
|
+
def meta_content
|
28
|
+
return nil unless meta_tag?
|
29
|
+
return swap_attributes('content') if attribute_swapped?
|
30
|
+
|
31
|
+
@node['content']
|
32
|
+
end
|
33
|
+
|
34
|
+
def meta_tag?
|
35
|
+
@node.name == 'meta'
|
36
|
+
end
|
37
|
+
|
38
|
+
def src
|
39
|
+
return nil if !img_tag? && !script_tag? && !source_tag?
|
40
|
+
return swap_attributes('src') if attribute_swapped?
|
41
|
+
|
42
|
+
@node['src']
|
43
|
+
end
|
44
|
+
|
45
|
+
def img_tag?
|
46
|
+
@node.name == 'img'
|
47
|
+
end
|
48
|
+
|
49
|
+
def script_tag?
|
50
|
+
@node.name == 'script'
|
51
|
+
end
|
52
|
+
|
53
|
+
def srcset
|
54
|
+
return nil if !img_tag? && !source_tag?
|
55
|
+
return swap_attributes('srcset') if attribute_swapped?
|
56
|
+
|
57
|
+
@node['srcset']
|
58
|
+
end
|
59
|
+
|
60
|
+
def source_tag?
|
61
|
+
@node.name == 'source'
|
62
|
+
end
|
63
|
+
|
64
|
+
def href
|
65
|
+
return nil if !a_tag? && !link_tag?
|
66
|
+
return swap_attributes('href') if attribute_swapped?
|
67
|
+
|
68
|
+
@node['href']
|
69
|
+
end
|
70
|
+
|
71
|
+
def a_tag?
|
72
|
+
@node.name == 'a'
|
73
|
+
end
|
74
|
+
|
75
|
+
def link_tag?
|
76
|
+
@node.name == 'link'
|
77
|
+
end
|
78
|
+
|
79
|
+
def aria_hidden?
|
80
|
+
@node.attributes['aria-hidden']&.value == 'true'
|
81
|
+
end
|
82
|
+
|
83
|
+
def multiple_srcsets?
|
84
|
+
!blank?(srcset) && srcset.split(',').size > 1
|
85
|
+
end
|
86
|
+
|
87
|
+
def ignore?
|
88
|
+
return true if @node.attributes['data-proofer-ignore']
|
89
|
+
return true if ancestors_ignorable?
|
90
|
+
|
91
|
+
return true if url&.ignore?
|
92
|
+
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
private def attribute_swapped?
|
97
|
+
return false if blank?(@runner.options[:swap_attributes])
|
98
|
+
|
99
|
+
attrs = @runner.options[:swap_attributes][@node.name]
|
100
|
+
|
101
|
+
return true unless blank?(attrs)
|
102
|
+
end
|
103
|
+
|
104
|
+
private def swap_attributes(old_attr)
|
105
|
+
attrs = @runner.options[:swap_attributes][@node.name]
|
106
|
+
|
107
|
+
new_attr = attrs.find do |(o, _)|
|
108
|
+
o == old_attr
|
109
|
+
end&.last
|
110
|
+
|
111
|
+
return nil if blank?(new_attr)
|
112
|
+
|
113
|
+
@node[new_attr]
|
114
|
+
end
|
115
|
+
|
116
|
+
private def ancestors_ignorable?
|
117
|
+
ancestors_attributes = @node.ancestors.map { |a| a.respond_to?(:attributes) && a.attributes }
|
118
|
+
ancestors_attributes.pop # remove document at the end
|
119
|
+
ancestors_attributes.any? { |a| !a['data-proofer-ignore'].nil? }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTMLProofer
|
4
|
+
class Failure
|
5
|
+
attr_reader :path, :check_name, :description, :status, :line, :content
|
6
|
+
|
7
|
+
def initialize(path, check_name, description, line: nil, status: nil, content: nil)
|
8
|
+
@path = path
|
9
|
+
@check_name = check_name
|
10
|
+
@description = description
|
11
|
+
|
12
|
+
@line = line
|
13
|
+
@status = status
|
14
|
+
@content = content
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HTMLProofer::Reporter::Cli < HTMLProofer::Reporter
|
4
|
+
def report
|
5
|
+
msg = failures.each_with_object([]) do |(check_name, failures), arr|
|
6
|
+
str = ["For the #{check_name} check, the following failures were found:\n"]
|
7
|
+
|
8
|
+
failures.each do |failure|
|
9
|
+
path_str = blank?(failure.path) ? '' : "In #{failure.path}"
|
10
|
+
|
11
|
+
line_str = failure.line.nil? ? '' : " (line #{failure.line})"
|
12
|
+
|
13
|
+
path_and_line = [path_str, line_str].join
|
14
|
+
path_and_line = blank?(path_and_line) ? '' : "* #{path_and_line}:\n\n"
|
15
|
+
|
16
|
+
status_str = failure.status.nil? ? '' : " (status code #{failure.status})"
|
17
|
+
|
18
|
+
indent = blank?(path_and_line) ? '* ' : ' '
|
19
|
+
str << <<~MSG
|
20
|
+
#{path_and_line}#{indent}#{failure.description}#{status_str}
|
21
|
+
MSG
|
22
|
+
end
|
23
|
+
|
24
|
+
arr << str.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
@logger.log(:error, msg.join("\n"))
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTMLProofer
|
4
|
+
class Reporter
|
5
|
+
include HTMLProofer::Utils
|
6
|
+
|
7
|
+
attr_reader :failures
|
8
|
+
|
9
|
+
def initialize(logger: nil)
|
10
|
+
@logger = logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def failures=(failures)
|
14
|
+
@failures = failures.group_by(&:check_name) \
|
15
|
+
.transform_values { |issues| issues.sort_by { |issue| [issue.path, issue.line] } } \
|
16
|
+
.sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def report
|
20
|
+
raise NotImplementedError, 'HTMLProofer::Reporter subclasses must implement #report'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|