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.
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