html-proofer 3.19.4 → 4.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.
- checksums.yaml +4 -4
- data/bin/htmlproofer +31 -57
- data/lib/html-proofer.rb +1 -54
- data/lib/html_proofer/attribute/url.rb +237 -0
- data/lib/html_proofer/attribute.rb +15 -0
- data/lib/html_proofer/cache.rb +279 -0
- data/lib/html_proofer/check/favicon.rb +40 -0
- data/lib/html_proofer/check/images.rb +93 -0
- data/lib/html_proofer/check/links.rb +129 -0
- data/lib/html_proofer/check/open_graph.rb +39 -0
- data/lib/html_proofer/check/scripts.rb +46 -0
- data/lib/html_proofer/check.rb +92 -0
- data/lib/html_proofer/configuration.rb +88 -0
- data/lib/html_proofer/element.rb +122 -0
- data/lib/html_proofer/failure.rb +17 -0
- data/lib/{html-proofer → html_proofer}/log.rb +19 -19
- data/lib/html_proofer/reporter/cli.rb +33 -0
- data/lib/html_proofer/reporter.rb +23 -0
- data/lib/html_proofer/runner.rb +246 -0
- data/lib/html_proofer/url_validator/external.rb +194 -0
- data/lib/html_proofer/url_validator/internal.rb +96 -0
- data/lib/html_proofer/url_validator.rb +16 -0
- data/lib/{html-proofer → html_proofer}/utils.rb +9 -12
- data/lib/{html-proofer → html_proofer}/version.rb +1 -1
- data/lib/html_proofer/xpath_functions.rb +10 -0
- data/lib/html_proofer.rb +57 -0
- metadata +42 -22
- 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/configuration.rb +0 -88
- 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,92 @@
|
|
|
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_filename, short_name, description, line: line, status: status,
|
|
29
|
+
content: content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.subchecks(runner_options)
|
|
33
|
+
# grab all known checks
|
|
34
|
+
checks = ObjectSpace.each_object(Class).select do |klass|
|
|
35
|
+
klass < self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# remove any checks not explicitly included
|
|
39
|
+
checks.each_with_object([]) do |check, arr|
|
|
40
|
+
next unless runner_options[:checks].include?(check.short_name)
|
|
41
|
+
|
|
42
|
+
arr << check
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def short_name
|
|
47
|
+
self.class.name.split("::").last
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.short_name
|
|
51
|
+
name.split("::").last
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_to_internal_urls(url, line)
|
|
55
|
+
url_string = url.raw_attribute
|
|
56
|
+
|
|
57
|
+
@internal_urls[url_string] = [] if @internal_urls[url_string].nil?
|
|
58
|
+
|
|
59
|
+
metadata = {
|
|
60
|
+
source: @runner.current_source,
|
|
61
|
+
filename: @runner.current_filename,
|
|
62
|
+
line: line,
|
|
63
|
+
base_url: base_url,
|
|
64
|
+
found: false,
|
|
65
|
+
}
|
|
66
|
+
@internal_urls[url_string] << metadata
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add_to_external_urls(url, line)
|
|
70
|
+
url_string = url.to_s
|
|
71
|
+
|
|
72
|
+
@external_urls[url_string] = [] if @external_urls[url_string].nil?
|
|
73
|
+
|
|
74
|
+
@external_urls[url_string] << { filename: @runner.current_filename, line: line }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def base_url
|
|
78
|
+
return @base_url if defined?(@base_url)
|
|
79
|
+
|
|
80
|
+
return (@base_url = "") if (base = @html.at_css("base")).nil?
|
|
81
|
+
|
|
82
|
+
@base_url = base["href"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def remove_ignored(html)
|
|
86
|
+
return if html.nil?
|
|
87
|
+
|
|
88
|
+
html.css("code, pre, tt").each(&:unlink)
|
|
89
|
+
html
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTMLProofer
|
|
4
|
+
module Configuration
|
|
5
|
+
DEFAULT_TESTS = ["Links", "Images", "Scripts"].freeze
|
|
6
|
+
|
|
7
|
+
PROOFER_DEFAULTS = {
|
|
8
|
+
allow_hash_href: true,
|
|
9
|
+
allow_missing_href: false,
|
|
10
|
+
assume_extension: ".html",
|
|
11
|
+
check_external_hash: true,
|
|
12
|
+
checks: DEFAULT_TESTS,
|
|
13
|
+
directory_index_file: "index.html",
|
|
14
|
+
disable_external: false,
|
|
15
|
+
ignore_empty_alt: true,
|
|
16
|
+
ignore_empty_mailto: false,
|
|
17
|
+
ignore_files: [],
|
|
18
|
+
ignore_missing_alt: false,
|
|
19
|
+
ignore_status_codes: [],
|
|
20
|
+
ignore_urls: [],
|
|
21
|
+
enforce_https: true,
|
|
22
|
+
extensions: [".html"],
|
|
23
|
+
log_level: :info,
|
|
24
|
+
only_4xx: false,
|
|
25
|
+
swap_attributes: {},
|
|
26
|
+
swap_urls: {},
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
TYPHOEUS_DEFAULTS = {
|
|
30
|
+
followlocation: true,
|
|
31
|
+
headers: {
|
|
32
|
+
"User-Agent" => "Mozilla/5.0 (compatible; HTML Proofer/#{HTMLProofer::VERSION}; +https://github.com/gjtorikian/html-proofer)",
|
|
33
|
+
"Accept" => "application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5",
|
|
34
|
+
},
|
|
35
|
+
connecttimeout: 10,
|
|
36
|
+
timeout: 30,
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
HYDRA_DEFAULTS = {
|
|
40
|
+
max_concurrency: 50,
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
PARALLEL_DEFAULTS = {
|
|
44
|
+
enable: true,
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
CACHE_DEFAULTS = {}.freeze
|
|
48
|
+
|
|
49
|
+
def self.generate_defaults(opts)
|
|
50
|
+
options = PROOFER_DEFAULTS.merge(opts)
|
|
51
|
+
|
|
52
|
+
options[:typhoeus] = HTMLProofer::Configuration::TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
|
|
53
|
+
options[:hydra] = HTMLProofer::Configuration::HYDRA_DEFAULTS.merge(opts[:hydra] || {})
|
|
54
|
+
|
|
55
|
+
options[:parallel] = HTMLProofer::Configuration::PARALLEL_DEFAULTS.merge(opts[:parallel] || {})
|
|
56
|
+
options[:cache] = HTMLProofer::Configuration::CACHE_DEFAULTS.merge(opts[:cache] || {})
|
|
57
|
+
|
|
58
|
+
options.delete(:src)
|
|
59
|
+
|
|
60
|
+
options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.to_regex?(item)
|
|
64
|
+
if item.start_with?("/") && item.end_with?("/")
|
|
65
|
+
Regexp.new(item[1...-1])
|
|
66
|
+
else
|
|
67
|
+
item
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.parse_json_option(option_name, config, symbolize_names: true)
|
|
72
|
+
raise ArgumentError, "Must provide an option name in string format." unless option_name.is_a?(String)
|
|
73
|
+
raise ArgumentError, "Must provide an option name in string format." if option_name.strip.empty?
|
|
74
|
+
|
|
75
|
+
return {} if config.nil?
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "Must provide a JSON configuration in string format." unless config.is_a?(String)
|
|
78
|
+
|
|
79
|
+
return {} if config.strip.empty?
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
JSON.parse(config, { symbolize_names: symbolize_names })
|
|
83
|
+
rescue StandardError
|
|
84
|
+
raise ArgumentError, "Option '#{option_name} did not contain valid JSON."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -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
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "yell"
|
|
4
|
+
require "rainbow"
|
|
5
5
|
|
|
6
6
|
module HTMLProofer
|
|
7
7
|
class Log
|
|
8
8
|
include Yell::Loggable
|
|
9
9
|
|
|
10
|
-
STDOUT_LEVELS =
|
|
11
|
-
STDERR_LEVELS =
|
|
10
|
+
STDOUT_LEVELS = [:debug, :info, :warn].freeze
|
|
11
|
+
STDERR_LEVELS = [:error, :fatal].freeze
|
|
12
12
|
|
|
13
13
|
def initialize(log_level)
|
|
14
14
|
@logger = Yell.new(format: false, \
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
l.adapter
|
|
18
|
-
l.adapter
|
|
15
|
+
name: "HTMLProofer", \
|
|
16
|
+
level: "gte.#{log_level}") do |l|
|
|
17
|
+
l.adapter(:stdout, level: "lte.warn")
|
|
18
|
+
l.adapter(:stderr, level: "gte.error")
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -24,23 +24,23 @@ module HTMLProofer
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def log_with_color(level, message)
|
|
27
|
-
@logger.send
|
|
27
|
+
@logger.send(level, colorize(level, message))
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def colorize(level, message)
|
|
31
31
|
color = case level
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
when :debug
|
|
33
|
+
:cyan
|
|
34
|
+
when :info
|
|
35
|
+
:blue
|
|
36
|
+
when :warn
|
|
37
|
+
:yellow
|
|
38
|
+
when :error, :fatal
|
|
39
|
+
:red
|
|
40
|
+
end
|
|
41
41
|
|
|
42
42
|
if (STDOUT_LEVELS.include?(level) && $stdout.isatty) || \
|
|
43
|
-
|
|
43
|
+
(STDERR_LEVELS.include?(level) && $stderr.isatty)
|
|
44
44
|
Rainbow(message).send(color)
|
|
45
45
|
else
|
|
46
46
|
message
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTMLProofer
|
|
4
|
+
class Reporter
|
|
5
|
+
class Cli < HTMLProofer::Reporter
|
|
6
|
+
def report
|
|
7
|
+
msg = failures.each_with_object([]) do |(check_name, failures), arr|
|
|
8
|
+
str = ["For the #{check_name} check, the following failures were found:\n"]
|
|
9
|
+
|
|
10
|
+
failures.each do |failure|
|
|
11
|
+
path_str = blank?(failure.path) ? "" : "At #{failure.path}"
|
|
12
|
+
|
|
13
|
+
line_str = failure.line.nil? ? "" : ":#{failure.line}"
|
|
14
|
+
|
|
15
|
+
path_and_line = "#{path_str}#{line_str}"
|
|
16
|
+
path_and_line = blank?(path_and_line) ? "" : "* #{path_and_line}:\n\n"
|
|
17
|
+
|
|
18
|
+
status_str = failure.status.nil? ? "" : " (status code #{failure.status})"
|
|
19
|
+
|
|
20
|
+
indent = blank?(path_and_line) ? "* " : " "
|
|
21
|
+
str << <<~MSG
|
|
22
|
+
#{path_and_line}#{indent}#{failure.description}#{status_str}
|
|
23
|
+
MSG
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
arr << str.join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@logger.log(:error, msg.join("\n"))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
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
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTMLProofer
|
|
4
|
+
class Runner
|
|
5
|
+
include HTMLProofer::Utils
|
|
6
|
+
|
|
7
|
+
attr_reader :options, :cache, :logger, :internal_urls, :external_urls, :checked_paths, :current_check
|
|
8
|
+
attr_accessor :current_filename, :current_source, :reporter
|
|
9
|
+
|
|
10
|
+
URL_TYPES = [:external, :internal].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(src, opts = {})
|
|
13
|
+
@options = HTMLProofer::Configuration.generate_defaults(opts)
|
|
14
|
+
|
|
15
|
+
@type = @options.delete(:type)
|
|
16
|
+
@source = src
|
|
17
|
+
|
|
18
|
+
@logger = HTMLProofer::Log.new(@options[:log_level])
|
|
19
|
+
@cache = Cache.new(self, @options[:cache])
|
|
20
|
+
|
|
21
|
+
@external_urls = {}
|
|
22
|
+
@internal_urls = {}
|
|
23
|
+
@failures = []
|
|
24
|
+
|
|
25
|
+
@before_request = []
|
|
26
|
+
|
|
27
|
+
@checked_paths = {}
|
|
28
|
+
|
|
29
|
+
@current_check = nil
|
|
30
|
+
@current_source = nil
|
|
31
|
+
@current_filename = nil
|
|
32
|
+
|
|
33
|
+
@reporter = Reporter::Cli.new(logger: @logger)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
check_text = pluralize(checks.length, "check", "checks")
|
|
38
|
+
|
|
39
|
+
if @type == :links
|
|
40
|
+
@logger.log(:info, "Running #{check_text} (#{format_checks_list(checks)}) on #{@source} ... \n\n")
|
|
41
|
+
check_list_of_links unless @options[:disable_external]
|
|
42
|
+
else
|
|
43
|
+
@logger.log(:info,
|
|
44
|
+
"Running #{check_text} (#{format_checks_list(checks)}) in #{@source} on *#{@options[:extensions].join(", ")} files...\n\n")
|
|
45
|
+
|
|
46
|
+
check_files
|
|
47
|
+
@logger.log(:info, "Ran on #{pluralize(files.length, "file", "files")}!\n\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@cache.write
|
|
51
|
+
|
|
52
|
+
@reporter.failures = @failures
|
|
53
|
+
|
|
54
|
+
if @failures.empty?
|
|
55
|
+
@logger.log(:info, "HTML-Proofer finished successfully.")
|
|
56
|
+
else
|
|
57
|
+
@failures.uniq!
|
|
58
|
+
report_failed_checks
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_list_of_links
|
|
63
|
+
@external_urls = @source.uniq.each_with_object({}) do |link, hash|
|
|
64
|
+
url = Attribute::Url.new(self, link, base_url: nil).to_s
|
|
65
|
+
|
|
66
|
+
hash[url] = []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
validate_external_urls
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Walks over each implemented check and runs them on the files, in parallel.
|
|
73
|
+
# Sends the collected external URLs to Typhoeus for batch processing.
|
|
74
|
+
def check_files
|
|
75
|
+
process_files.each do |result|
|
|
76
|
+
URL_TYPES.each do |url_type|
|
|
77
|
+
type = :"#{url_type}_urls"
|
|
78
|
+
ivar_name = "@#{type}"
|
|
79
|
+
ivar = instance_variable_get(ivar_name)
|
|
80
|
+
|
|
81
|
+
if ivar.empty?
|
|
82
|
+
instance_variable_set(ivar_name, result[type])
|
|
83
|
+
else
|
|
84
|
+
result[type].each do |url, metadata|
|
|
85
|
+
ivar[url] = [] if ivar[url].nil?
|
|
86
|
+
ivar[url].concat(metadata)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
@failures.concat(result[:failures])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
validate_external_urls unless @options[:disable_external]
|
|
94
|
+
|
|
95
|
+
validate_internal_urls
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Walks over each implemented check and runs them on the files, in parallel.
|
|
99
|
+
def process_files
|
|
100
|
+
if @options[:parallel][:enable]
|
|
101
|
+
Parallel.map(files, @options[:parallel]) { |file| load_file(file[:path], file[:source]) }
|
|
102
|
+
else
|
|
103
|
+
files.map do |file|
|
|
104
|
+
load_file(file[:path], file[:source])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_file(path, source)
|
|
110
|
+
@html = create_nokogiri(path)
|
|
111
|
+
check_parsed(path, source)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Collects any external URLs found in a directory of files. Also collectes
|
|
115
|
+
# every failed test from process_files.
|
|
116
|
+
def check_parsed(path, source)
|
|
117
|
+
result = { internal_urls: {}, external_urls: {}, failures: [] }
|
|
118
|
+
|
|
119
|
+
checks.each do |klass|
|
|
120
|
+
@current_source = source
|
|
121
|
+
@current_filename = path
|
|
122
|
+
|
|
123
|
+
check = Object.const_get(klass).new(self, @html)
|
|
124
|
+
@logger.log(:debug, "Running #{check.short_name} in #{path}")
|
|
125
|
+
|
|
126
|
+
@current_check = check
|
|
127
|
+
|
|
128
|
+
check.run
|
|
129
|
+
|
|
130
|
+
result[:external_urls].merge!(check.external_urls) { |_key, old, current| old.concat(current) }
|
|
131
|
+
result[:internal_urls].merge!(check.internal_urls) { |_key, old, current| old.concat(current) }
|
|
132
|
+
result[:failures].concat(check.failures)
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate_external_urls
|
|
138
|
+
external_url_validator = HTMLProofer::UrlValidator::External.new(self, @external_urls)
|
|
139
|
+
external_url_validator.before_request = @before_request
|
|
140
|
+
@failures.concat(external_url_validator.validate)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_internal_urls
|
|
144
|
+
internal_link_validator = HTMLProofer::UrlValidator::Internal.new(self, @internal_urls)
|
|
145
|
+
@failures.concat(internal_link_validator.validate)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def files
|
|
149
|
+
@files ||= if @type == :directory
|
|
150
|
+
@source.map do |src|
|
|
151
|
+
pattern = File.join(src, "**", "*{#{@options[:extensions].join(",")}}")
|
|
152
|
+
Dir.glob(pattern).select do |f|
|
|
153
|
+
File.file?(f) && !ignore_file?(f)
|
|
154
|
+
end.map { |f| { source: src, path: f } }
|
|
155
|
+
end.flatten
|
|
156
|
+
elsif @type == :file && @options[:extensions].include?(File.extname(@source))
|
|
157
|
+
[@source].reject { |f| ignore_file?(f) }.map { |f| { source: f, path: f } }
|
|
158
|
+
else
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ignore_file?(file)
|
|
164
|
+
@options[:ignore_files].each do |pattern|
|
|
165
|
+
return true if pattern.is_a?(String) && pattern == file
|
|
166
|
+
return true if pattern.is_a?(Regexp) && pattern =~ file
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def check_sri?
|
|
173
|
+
@options[:check_sri]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def enforce_https?
|
|
177
|
+
@options[:enforce_https]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def checks
|
|
181
|
+
return @checks if defined?(@checks) && !@checks.nil?
|
|
182
|
+
|
|
183
|
+
return (@checks = ["LinkCheck"]) if @type == :links
|
|
184
|
+
|
|
185
|
+
@checks = HTMLProofer::Check.subchecks(@options).map(&:name)
|
|
186
|
+
|
|
187
|
+
@checks
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def failed_checks
|
|
191
|
+
@reporter.failures.flatten.select { |f| f.is_a?(Failure) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def report_failed_checks
|
|
195
|
+
@reporter.report
|
|
196
|
+
|
|
197
|
+
failure_text = pluralize(@failures.length, "failure", "failures")
|
|
198
|
+
@logger.log(:fatal, "\nHTML-Proofer found #{failure_text}!")
|
|
199
|
+
exit(1)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Set before_request callback.
|
|
203
|
+
#
|
|
204
|
+
# @example Set before_request.
|
|
205
|
+
# request.before_request { |request| p "yay" }
|
|
206
|
+
#
|
|
207
|
+
# @param [ Block ] block The block to execute.
|
|
208
|
+
#
|
|
209
|
+
# @yield [ Typhoeus::Request ]
|
|
210
|
+
#
|
|
211
|
+
# @return [ Array<Block> ] All before_request blocks.
|
|
212
|
+
def before_request(&block)
|
|
213
|
+
@before_request ||= []
|
|
214
|
+
@before_request << block if block
|
|
215
|
+
@before_request
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def load_internal_cache
|
|
219
|
+
load_cache(:internal)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def load_external_cache
|
|
223
|
+
load_cache(:external)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private def load_cache(type)
|
|
227
|
+
ivar = instance_variable_get("@#{type}_urls")
|
|
228
|
+
|
|
229
|
+
existing_urls_count = @cache.size(type)
|
|
230
|
+
cache_text = pluralize(existing_urls_count, "#{type} link", "#{type} links")
|
|
231
|
+
@logger.log(:debug, "Found #{cache_text} in the cache")
|
|
232
|
+
|
|
233
|
+
urls_to_check = @cache.retrieve_urls(ivar, type)
|
|
234
|
+
urls_detected = pluralize(urls_to_check.count, "#{type} link", "#{type} links")
|
|
235
|
+
@logger.log(:info, "Checking #{urls_detected}")
|
|
236
|
+
|
|
237
|
+
urls_to_check
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private def format_checks_list(checks)
|
|
241
|
+
checks.map do |check|
|
|
242
|
+
check.sub(/HTMLProofer::Check::/, "")
|
|
243
|
+
end.join(", ")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|