html-proofer 3.19.4 → 4.0.0.rc1
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.
- 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
|