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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/bin/htmlproofer +30 -57
  3. data/lib/html-proofer.rb +1 -54
  4. data/lib/html_proofer/attribute/url.rb +231 -0
  5. data/lib/html_proofer/attribute.rb +15 -0
  6. data/lib/html_proofer/cache.rb +234 -0
  7. data/lib/html_proofer/check/favicon.rb +35 -0
  8. data/lib/html_proofer/check/images.rb +62 -0
  9. data/lib/html_proofer/check/links.rb +118 -0
  10. data/lib/html_proofer/check/open_graph.rb +34 -0
  11. data/lib/html_proofer/check/scripts.rb +38 -0
  12. data/lib/html_proofer/check.rb +91 -0
  13. data/lib/{html-proofer → html_proofer}/configuration.rb +30 -31
  14. data/lib/html_proofer/element.rb +122 -0
  15. data/lib/html_proofer/failure.rb +17 -0
  16. data/lib/{html-proofer → html_proofer}/log.rb +0 -0
  17. data/lib/html_proofer/reporter/cli.rb +29 -0
  18. data/lib/html_proofer/reporter.rb +23 -0
  19. data/lib/html_proofer/runner.rb +245 -0
  20. data/lib/html_proofer/url_validator/external.rb +189 -0
  21. data/lib/html_proofer/url_validator/internal.rb +86 -0
  22. data/lib/html_proofer/url_validator.rb +16 -0
  23. data/lib/{html-proofer → html_proofer}/utils.rb +5 -8
  24. data/lib/{html-proofer → html_proofer}/version.rb +1 -1
  25. data/lib/html_proofer/xpath_functions.rb +10 -0
  26. data/lib/html_proofer.rb +56 -0
  27. metadata +46 -27
  28. data/lib/html-proofer/cache.rb +0 -194
  29. data/lib/html-proofer/check/favicon.rb +0 -29
  30. data/lib/html-proofer/check/html.rb +0 -37
  31. data/lib/html-proofer/check/images.rb +0 -48
  32. data/lib/html-proofer/check/links.rb +0 -182
  33. data/lib/html-proofer/check/opengraph.rb +0 -46
  34. data/lib/html-proofer/check/scripts.rb +0 -42
  35. data/lib/html-proofer/check.rb +0 -75
  36. data/lib/html-proofer/element.rb +0 -265
  37. data/lib/html-proofer/issue.rb +0 -65
  38. data/lib/html-proofer/middleware.rb +0 -82
  39. data/lib/html-proofer/runner.rb +0 -249
  40. 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
- require_relative 'version'
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
- allow_hash_href: false,
10
- alt_ignore: [],
11
- assume_extension: false,
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
- url_ignore: [],
33
- url_swap: {}
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 = {}.freeze
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