html-proofer 5.0.0 → 5.2.2
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/lib/html_proofer/attribute/url.rb +58 -38
- data/lib/html_proofer/cache.rb +16 -9
- data/lib/html_proofer/check/favicon.rb +9 -5
- data/lib/html_proofer/check/images.rb +36 -22
- data/lib/html_proofer/check/links.rb +38 -23
- data/lib/html_proofer/check/open_graph.rb +12 -8
- data/lib/html_proofer/check/scripts.rb +22 -12
- data/lib/html_proofer/check.rb +17 -9
- data/lib/html_proofer/configuration.rb +72 -31
- data/lib/html_proofer/element.rb +24 -9
- data/lib/html_proofer/failure.rb +3 -2
- data/lib/html_proofer/log.rb +7 -5
- data/lib/html_proofer/reporter.rb +2 -2
- data/lib/html_proofer/runner.rb +18 -10
- data/lib/html_proofer/url_validator/external.rb +44 -5
- data/lib/html_proofer/url_validator/internal.rb +48 -27
- data/lib/html_proofer/version.rb +1 -1
- data/lib/html_proofer/xpath_functions.rb +1 -1
- data/lib/html_proofer.rb +4 -1
- metadata +27 -142
|
@@ -13,7 +13,7 @@ module HTMLProofer
|
|
|
13
13
|
check_external_hash: true,
|
|
14
14
|
check_internal_hash: true,
|
|
15
15
|
checks: DEFAULT_TESTS,
|
|
16
|
-
|
|
16
|
+
directory_index_files: ["index.html"],
|
|
17
17
|
disable_external: false,
|
|
18
18
|
ignore_empty_alt: true,
|
|
19
19
|
ignore_empty_mailto: false,
|
|
@@ -47,6 +47,12 @@ module HTMLProofer
|
|
|
47
47
|
|
|
48
48
|
class << self
|
|
49
49
|
def generate_defaults(opts)
|
|
50
|
+
# If `:directory_index_file` (singular) is given, convert it into an
|
|
51
|
+
# array for `:directory_index_files` (plural) instead.
|
|
52
|
+
if opts.key?(:directory_index_file)
|
|
53
|
+
opts[:directory_index_files] = [opts.delete(:directory_index_file)]
|
|
54
|
+
end
|
|
55
|
+
|
|
50
56
|
options = PROOFER_DEFAULTS.merge(opts)
|
|
51
57
|
|
|
52
58
|
options[:typhoeus] = HTMLProofer::Configuration::TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
|
|
@@ -89,6 +95,10 @@ module HTMLProofer
|
|
|
89
95
|
@options[long_opt_symbol] = arg
|
|
90
96
|
end
|
|
91
97
|
|
|
98
|
+
set_option(opts, "--directory-index-files [FILENAME1,FILENAME2,...]") do |long_opt_symbol, list|
|
|
99
|
+
@options[long_opt_symbol] = list.nil? ? [] : list.split(",")
|
|
100
|
+
end
|
|
101
|
+
|
|
92
102
|
set_option(opts, "--extensions [EXT1,EXT2,...]") do |long_opt_symbol, list|
|
|
93
103
|
@options[long_opt_symbol] = list.nil? ? [] : list.split(",")
|
|
94
104
|
end
|
|
@@ -134,7 +144,17 @@ module HTMLProofer
|
|
|
134
144
|
|
|
135
145
|
section(opts, "Ignore Configuration") do
|
|
136
146
|
set_option(opts, "--ignore-files [FILE1,FILE2,...]") do |long_opt_symbol, list|
|
|
137
|
-
@options[long_opt_symbol] = list.nil?
|
|
147
|
+
@options[long_opt_symbol] = if list.nil?
|
|
148
|
+
[]
|
|
149
|
+
else
|
|
150
|
+
list.split(",").map.each do |l|
|
|
151
|
+
if l.start_with?("/") && l.end_with?("/")
|
|
152
|
+
Regexp.new(l[1...-1])
|
|
153
|
+
else
|
|
154
|
+
l
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
138
158
|
end
|
|
139
159
|
|
|
140
160
|
set_option(opts, "--[no-]ignore-empty-alt") do |long_opt_symbol, arg|
|
|
@@ -187,12 +207,12 @@ module HTMLProofer
|
|
|
187
207
|
@options[long_opt_symbol] = parse_json_option("typhoeus", arg, symbolize_names: false)
|
|
188
208
|
end
|
|
189
209
|
|
|
190
|
-
set_option(opts, "--hydra <CONFIG>") do |long_opt_symbol,
|
|
191
|
-
@options[long_opt_symbol] = parse_json_option("hydra", arg, symbolize_names:
|
|
210
|
+
set_option(opts, "--hydra <CONFIG>") do |long_opt_symbol, arg|
|
|
211
|
+
@options[long_opt_symbol] = parse_json_option("hydra", arg, symbolize_names: true)
|
|
192
212
|
end
|
|
193
213
|
|
|
194
|
-
set_option(opts, "--cache <CONFIG>") do |long_opt_symbol,
|
|
195
|
-
@options[long_opt_symbol] = parse_json_option("cache", arg, symbolize_names:
|
|
214
|
+
set_option(opts, "--cache <CONFIG>") do |long_opt_symbol, arg|
|
|
215
|
+
@options[long_opt_symbol] = parse_json_option("cache", arg, symbolize_names: true)
|
|
196
216
|
end
|
|
197
217
|
end
|
|
198
218
|
|
|
@@ -223,8 +243,8 @@ module HTMLProofer
|
|
|
223
243
|
arg.split(",").each_with_object({}) do |s, hsh|
|
|
224
244
|
split = s.split(/(?<!\\):/, 2)
|
|
225
245
|
|
|
226
|
-
re = split[0].gsub(
|
|
227
|
-
string = split[1].gsub(
|
|
246
|
+
re = split[0].gsub("\\:", ":")
|
|
247
|
+
string = split[1].gsub("\\:", ":")
|
|
228
248
|
hsh[Regexp.new(re)] = string
|
|
229
249
|
end
|
|
230
250
|
end
|
|
@@ -270,49 +290,70 @@ module HTMLProofer
|
|
|
270
290
|
module ConfigurationHelp
|
|
271
291
|
TEXT = {
|
|
272
292
|
as_links: ["Assumes that `PATH` is a comma-separated array of links to check."],
|
|
273
|
-
assume_extension: [
|
|
274
|
-
|
|
293
|
+
assume_extension: [
|
|
294
|
+
"Automatically add specified extension to files for internal links, ",
|
|
295
|
+
"to allow extensionless URLs (as supported by most servers) (default: `.html`).",
|
|
296
|
+
],
|
|
275
297
|
directory_index_file: ["Sets the file to look for when a link refers to a directory. (default: `index.html`)."],
|
|
276
|
-
|
|
277
|
-
|
|
298
|
+
directory_index_files: ["Sets the files to look for when a link refers to a directory. (default: `[\"index.html\"]`)."],
|
|
299
|
+
extensions: [
|
|
300
|
+
"A comma-separated list of Strings indicating the file extensions you",
|
|
301
|
+
"would like to check (default: `.html`)",
|
|
302
|
+
],
|
|
278
303
|
|
|
279
304
|
allow_hash_href: ['"If `true`, assumes `href="#"` anchors are valid (default: `true`)"'],
|
|
280
|
-
allow_missing_href: [
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
allow_missing_href: [
|
|
306
|
+
"If `true`, does not flag `a` tags missing `href`. In HTML5, this is technically ",
|
|
307
|
+
"allowed, but could also be human error. (default: `false`)",
|
|
308
|
+
],
|
|
309
|
+
checks: [
|
|
310
|
+
"A comma-separated list of Strings indicating which checks you",
|
|
311
|
+
"want to run (default: `[\"Links\", \"Images\", \"Scripts\"]",
|
|
312
|
+
],
|
|
284
313
|
check_external_hash: ["Checks whether external hashes exist (even if the webpage exists) (default: `true`)."],
|
|
285
314
|
check_internal_hash: ["Checks whether internal hashes exist (even if the webpage exists) (default: `true`)."],
|
|
286
315
|
check_sri: ["Check that `<link>` and `<script>` external resources use SRI (default: `false`)."],
|
|
287
316
|
disable_external: ["If `true`, does not run the external link checker (default: `false`)."],
|
|
288
|
-
enforce_https: ["Fails a link if it
|
|
317
|
+
enforce_https: ["Fails a link if it's not marked as `https` (default: `true`)."],
|
|
289
318
|
root_dir: ["The absolute path to the directory serving your html-files."],
|
|
290
319
|
|
|
291
|
-
ignore_empty_alt: [
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
ignore_empty_alt: [
|
|
321
|
+
"If `true`, ignores images with empty/missing ",
|
|
322
|
+
"alt tags (in other words, `<img alt>` and `<img alt=\"\">`",
|
|
323
|
+
"are valid; set this to `false` to flag those) (default: `true`).",
|
|
324
|
+
],
|
|
325
|
+
ignore_empty_mailto: [
|
|
326
|
+
"If `true`, allows `mailto:` `href`s which don't",
|
|
327
|
+
"contain an email address (default: `false`)'.",
|
|
328
|
+
],
|
|
296
329
|
ignore_missing_alt: ["If `true`, ignores images with missing alt tags (default: `false`)."],
|
|
297
330
|
ignore_status_codes: ["A comma-separated list of numbers representing status codes to ignore."],
|
|
298
331
|
ignore_files: ["A comma-separated list of Strings or RegExps containing file paths that are safe to ignore"],
|
|
299
|
-
ignore_urls: [
|
|
300
|
-
|
|
332
|
+
ignore_urls: [
|
|
333
|
+
"A comma-separated list of Strings or RegExps containing URLs that are",
|
|
334
|
+
"safe to ignore. This affects all HTML attributes, such as `alt` tags on images.",
|
|
335
|
+
],
|
|
301
336
|
only_status_codes: ["A comma-separated list of numbers representing the only status codes to report on."],
|
|
302
337
|
only_4xx: ["Only reports errors for links that fall within the 4xx status code range."],
|
|
303
338
|
|
|
304
|
-
swap_attributes: [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
339
|
+
swap_attributes: [
|
|
340
|
+
"JSON-formatted config that maps element names to the",
|
|
341
|
+
"preferred attribute to check (default: `{}`).",
|
|
342
|
+
],
|
|
343
|
+
swap_urls: [
|
|
344
|
+
"A comma-separated list containing key-value pairs of `RegExp => String`.",
|
|
345
|
+
"It transforms URLs that match `RegExp` into `String` via `gsub`.",
|
|
346
|
+
"The escape sequences `\\:` should be used to produce literal `:`s.",
|
|
347
|
+
],
|
|
309
348
|
|
|
310
349
|
typhoeus: ["JSON-formatted string of Typhoeus config; if set, overrides the html-proofer defaults."],
|
|
311
350
|
hydra: ["JSON-formatted string of Hydra config; if set, overrides the html-proofer defaults."],
|
|
312
351
|
cache: ["JSON-formatted string of cache config; if set, overrides the html-proofer defaults."],
|
|
313
352
|
|
|
314
|
-
log_level: [
|
|
315
|
-
|
|
353
|
+
log_level: [
|
|
354
|
+
"Sets the logging level. One of `:debug`, `:info`, ",
|
|
355
|
+
"`:warn`, `:error`, or `:fatal`. (default: `:info`)",
|
|
356
|
+
],
|
|
316
357
|
|
|
317
358
|
version: ["Prints the version of html-proofer."],
|
|
318
359
|
}.freeze
|
data/lib/html_proofer/element.rb
CHANGED
|
@@ -16,7 +16,7 @@ module HTMLProofer
|
|
|
16
16
|
swap_attributes!
|
|
17
17
|
|
|
18
18
|
@base_url = base_url
|
|
19
|
-
@url = Attribute::Url.new(runner, link_attribute, base_url: base_url)
|
|
19
|
+
@url = Attribute::Url.new(runner, link_attribute, base_url: base_url, source: @runner.current_source, filename: @runner.current_filename)
|
|
20
20
|
|
|
21
21
|
@line = node.line
|
|
22
22
|
@content = node.content
|
|
@@ -27,7 +27,7 @@ module HTMLProofer
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def meta_content
|
|
30
|
-
return
|
|
30
|
+
return unless meta_tag?
|
|
31
31
|
|
|
32
32
|
@node["content"]
|
|
33
33
|
end
|
|
@@ -37,7 +37,7 @@ module HTMLProofer
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def src
|
|
40
|
-
return
|
|
40
|
+
return if !img_tag? && !script_tag? && !source_tag?
|
|
41
41
|
|
|
42
42
|
@node["src"]
|
|
43
43
|
end
|
|
@@ -51,7 +51,7 @@ module HTMLProofer
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def srcset
|
|
54
|
-
return
|
|
54
|
+
return if !img_tag? && !source_tag?
|
|
55
55
|
|
|
56
56
|
@node["srcset"]
|
|
57
57
|
end
|
|
@@ -61,7 +61,7 @@ module HTMLProofer
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def href
|
|
64
|
-
return
|
|
64
|
+
return if !a_tag? && !link_tag?
|
|
65
65
|
|
|
66
66
|
@node["href"]
|
|
67
67
|
end
|
|
@@ -82,10 +82,25 @@ module HTMLProofer
|
|
|
82
82
|
!blank?(srcset) && srcset.split(",").size > 1
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# From https://github.com/sindresorhus/srcset/blob/f7c48acd7facf18e94dec47e6b96e84e0f0e69dc/index.js#LL1-L16C71
|
|
86
|
+
# This regex represents a loose rule of an “image candidate string”; see https://html.spec.whatwg.org/multipage/images.html#srcset-attribute
|
|
87
|
+
# An “image candidate string” roughly consists of the following:
|
|
88
|
+
# 1. Zero or more whitespace characters.
|
|
89
|
+
# 2. A non-empty URL that does not start or end with `,`.
|
|
90
|
+
# 3. Zero or more whitespace characters.
|
|
91
|
+
# 4. An optional “descriptor” that starts with a whitespace character.
|
|
92
|
+
# 5. Zero or more whitespace characters.
|
|
93
|
+
# 6. Each image candidate string is separated by a `,`.
|
|
94
|
+
# We intentionally implement a loose rule here so that we can perform more aggressive error handling and reporting in the below code.
|
|
95
|
+
|
|
96
|
+
IMAGE_CANDIDATE_REGEX = /\s*([^,]\S*[^,](?:\s+[^,]+)?)\s*(?:,|$)/
|
|
97
|
+
|
|
85
98
|
def srcsets
|
|
86
|
-
return
|
|
99
|
+
return if blank?(srcset)
|
|
87
100
|
|
|
88
|
-
srcset.split(
|
|
101
|
+
srcset.split(IMAGE_CANDIDATE_REGEX).select.with_index do |_part, idx|
|
|
102
|
+
idx.odd?
|
|
103
|
+
end.map(&:strip)
|
|
89
104
|
end
|
|
90
105
|
|
|
91
106
|
def multiple_sizes?
|
|
@@ -97,7 +112,7 @@ module HTMLProofer
|
|
|
97
112
|
end
|
|
98
113
|
|
|
99
114
|
def srcsets_wo_sizes
|
|
100
|
-
return
|
|
115
|
+
return if blank?(srcsets)
|
|
101
116
|
|
|
102
117
|
srcsets.map do |srcset|
|
|
103
118
|
srcset.split(" ").first
|
|
@@ -118,7 +133,7 @@ module HTMLProofer
|
|
|
118
133
|
|
|
119
134
|
attrs = @runner.options[:swap_attributes][@node.name]
|
|
120
135
|
|
|
121
|
-
|
|
136
|
+
true unless blank?(attrs)
|
|
122
137
|
end
|
|
123
138
|
|
|
124
139
|
private def swap_attributes!
|
data/lib/html_proofer/failure.rb
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module HTMLProofer
|
|
4
4
|
class Failure
|
|
5
|
-
attr_reader :path, :check_name, :description, :status, :line, :content
|
|
5
|
+
attr_reader :path, :check_name, :description, :status, :line, :content, :element
|
|
6
6
|
|
|
7
|
-
def initialize(path, check_name, description, line: nil, status: nil, content: nil)
|
|
7
|
+
def initialize(path, check_name, description, line: nil, status: nil, content: nil, element: nil)
|
|
8
8
|
@path = path
|
|
9
9
|
@check_name = check_name
|
|
10
10
|
@description = description
|
|
@@ -12,6 +12,7 @@ module HTMLProofer
|
|
|
12
12
|
@line = line
|
|
13
13
|
@status = status
|
|
14
14
|
@content = content
|
|
15
|
+
@element = element
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
data/lib/html_proofer/log.rb
CHANGED
|
@@ -11,9 +11,11 @@ module HTMLProofer
|
|
|
11
11
|
STDERR_LEVELS = [:error, :fatal].freeze
|
|
12
12
|
|
|
13
13
|
def initialize(log_level)
|
|
14
|
-
@logger = Yell.new(
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
@logger = Yell.new(
|
|
15
|
+
format: false,
|
|
16
|
+
name: "HTMLProofer",
|
|
17
|
+
level: "gte.#{log_level}",
|
|
18
|
+
) do |l|
|
|
17
19
|
l.adapter(:stdout, level: "lte.warn")
|
|
18
20
|
l.adapter(:stderr, level: "gte.error")
|
|
19
21
|
end
|
|
@@ -39,8 +41,8 @@ module HTMLProofer
|
|
|
39
41
|
:red
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
if
|
|
43
|
-
|
|
44
|
+
if STDOUT_LEVELS.include?(level) ||
|
|
45
|
+
STDERR_LEVELS.include?(level)
|
|
44
46
|
Rainbow(message).send(color)
|
|
45
47
|
else
|
|
46
48
|
message
|
|
@@ -11,8 +11,8 @@ module HTMLProofer
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def failures=(failures)
|
|
14
|
-
@failures = failures.group_by(&:check_name)
|
|
15
|
-
.transform_values { |issues| issues.sort_by { |issue| [issue.path, issue.line] } }
|
|
14
|
+
@failures = failures.group_by(&:check_name)
|
|
15
|
+
.transform_values { |issues| issues.sort_by { |issue| [issue.path, issue.line] } }
|
|
16
16
|
.sort
|
|
17
17
|
end
|
|
18
18
|
|
data/lib/html_proofer/runner.rb
CHANGED
|
@@ -6,8 +6,8 @@ module HTMLProofer
|
|
|
6
6
|
class Runner
|
|
7
7
|
include HTMLProofer::Utils
|
|
8
8
|
|
|
9
|
-
attr_reader :options, :cache, :logger, :internal_urls, :external_urls, :
|
|
10
|
-
attr_accessor :
|
|
9
|
+
attr_reader :options, :cache, :logger, :internal_urls, :external_urls, :resolved_paths, :current_check, :current_filename, :current_source
|
|
10
|
+
attr_accessor :reporter
|
|
11
11
|
|
|
12
12
|
URL_TYPES = [:external, :internal].freeze
|
|
13
13
|
|
|
@@ -26,7 +26,7 @@ module HTMLProofer
|
|
|
26
26
|
|
|
27
27
|
@before_request = []
|
|
28
28
|
|
|
29
|
-
@
|
|
29
|
+
@resolved_paths = {}
|
|
30
30
|
|
|
31
31
|
@current_check = nil
|
|
32
32
|
@current_source = nil
|
|
@@ -42,8 +42,10 @@ module HTMLProofer
|
|
|
42
42
|
@logger.log(:info, "Running #{check_text} (#{format_checks_list(checks)}) on #{@source} ...\n\n")
|
|
43
43
|
check_list_of_links unless @options[:disable_external]
|
|
44
44
|
else
|
|
45
|
-
@logger.log(
|
|
46
|
-
|
|
45
|
+
@logger.log(
|
|
46
|
+
:info,
|
|
47
|
+
"Running #{check_text} (#{format_checks_list(checks)}) in #{@source} on *#{@options[:extensions].join(", ")} files ...\n\n",
|
|
48
|
+
)
|
|
47
49
|
|
|
48
50
|
check_files
|
|
49
51
|
@logger.log(:info, "Ran on #{pluralize(files.length, "file", "files")}!\n\n")
|
|
@@ -166,8 +168,14 @@ module HTMLProofer
|
|
|
166
168
|
|
|
167
169
|
def ignore_file?(file)
|
|
168
170
|
@options[:ignore_files].each do |pattern|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
if pattern.is_a?(String) && pattern == file
|
|
172
|
+
@logger.log(:debug, "Ignoring #{file} because it matches #{pattern}")
|
|
173
|
+
return true
|
|
174
|
+
end
|
|
175
|
+
next unless pattern.is_a?(Regexp) && pattern.match(file)
|
|
176
|
+
|
|
177
|
+
@logger.log(:debug, "Ignoring #{file} because it matches regexp #{pattern}")
|
|
178
|
+
return true
|
|
171
179
|
end
|
|
172
180
|
|
|
173
181
|
false
|
|
@@ -192,7 +200,7 @@ module HTMLProofer
|
|
|
192
200
|
end
|
|
193
201
|
|
|
194
202
|
def failed_checks
|
|
195
|
-
@reporter.failures.flatten.
|
|
203
|
+
@reporter.failures.flatten.grep(Failure)
|
|
196
204
|
end
|
|
197
205
|
|
|
198
206
|
def report_failed_checks
|
|
@@ -228,7 +236,7 @@ module HTMLProofer
|
|
|
228
236
|
end
|
|
229
237
|
|
|
230
238
|
private def load_cache(type)
|
|
231
|
-
ivar = instance_variable_get("@#{type}_urls")
|
|
239
|
+
ivar = instance_variable_get(:"@#{type}_urls")
|
|
232
240
|
|
|
233
241
|
existing_urls_count = @cache.size(type)
|
|
234
242
|
cache_text = pluralize(existing_urls_count, "#{type} link", "#{type} links")
|
|
@@ -241,7 +249,7 @@ module HTMLProofer
|
|
|
241
249
|
|
|
242
250
|
private def format_checks_list(checks)
|
|
243
251
|
checks.map do |check|
|
|
244
|
-
check.sub(
|
|
252
|
+
check.sub("HTMLProofer::Check::", "")
|
|
245
253
|
end.sort.join(", ")
|
|
246
254
|
end
|
|
247
255
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "typhoeus"
|
|
4
|
-
require "uri"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
# require "uri"
|
|
6
|
+
require "pdf-reader"
|
|
5
7
|
|
|
6
8
|
module HTMLProofer
|
|
7
9
|
class UrlValidator
|
|
@@ -88,8 +90,12 @@ module HTMLProofer
|
|
|
88
90
|
return if @runner.options[:ignore_status_codes].include?(response_code)
|
|
89
91
|
|
|
90
92
|
if response_code.between?(200, 299)
|
|
91
|
-
@cache.add_external(href, filenames, response_code, "OK", true) unless check_hash_in_2xx_response(
|
|
92
|
-
|
|
93
|
+
@cache.add_external(href, filenames, response_code, "OK", true) unless check_hash_in_2xx_response(
|
|
94
|
+
href,
|
|
95
|
+
url,
|
|
96
|
+
response,
|
|
97
|
+
filenames,
|
|
98
|
+
)
|
|
93
99
|
elsif response.timed_out?
|
|
94
100
|
handle_timeout(href, filenames, response_code)
|
|
95
101
|
elsif response_code.zero?
|
|
@@ -115,6 +121,31 @@ module HTMLProofer
|
|
|
115
121
|
return false unless url.hash?
|
|
116
122
|
|
|
117
123
|
hash = url.hash
|
|
124
|
+
# `response.headers` covers both live responses (which expose the headers
|
|
125
|
+
# via :response_headers) and stubbed ones (which set :headers directly);
|
|
126
|
+
# reading options[:headers] saw nothing but stubs.
|
|
127
|
+
headers = response.headers || {}
|
|
128
|
+
content_type = headers.find { |k, _| k.casecmp("content-type").zero? }
|
|
129
|
+
|
|
130
|
+
# attempt to verify PDF hash ref; see #787 for more details
|
|
131
|
+
# FIXME: this is re-reading the PDF response
|
|
132
|
+
if content_type && content_type[1].include?("pdf")
|
|
133
|
+
io = URI.parse(url.to_s).open
|
|
134
|
+
reader = PDF::Reader.new(io)
|
|
135
|
+
|
|
136
|
+
pages = reader.pages
|
|
137
|
+
if hash =~ /\Apage=(\d+)\z/
|
|
138
|
+
page = Regexp.last_match[1].to_i
|
|
139
|
+
|
|
140
|
+
unless pages[page - 1]
|
|
141
|
+
msg = "External link #{href} failed: #{url.without_hash} exists, but the hash '#{hash}' does not"
|
|
142
|
+
add_failure(filenames, msg, response.code)
|
|
143
|
+
@cache.add_external(href, filenames, response.code, msg, false)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
118
149
|
|
|
119
150
|
body_doc = create_nokogiri(response.body)
|
|
120
151
|
|
|
@@ -130,7 +161,7 @@ module HTMLProofer
|
|
|
130
161
|
|
|
131
162
|
return unless body_doc.xpath(xpath.join("|")).empty?
|
|
132
163
|
|
|
133
|
-
msg = "External link #{href} failed: #{url.
|
|
164
|
+
msg = "External link #{href} failed: #{url.without_hash} exists, but the hash '#{hash}' does not"
|
|
134
165
|
add_failure(filenames, msg, response.code)
|
|
135
166
|
@cache.add_external(href, filenames, response.code, msg, false)
|
|
136
167
|
true
|
|
@@ -167,7 +198,15 @@ module HTMLProofer
|
|
|
167
198
|
@failed_checks << Failure.new("", "Links > External", description, status: status)
|
|
168
199
|
else
|
|
169
200
|
metadata.each do |m|
|
|
170
|
-
@failed_checks << Failure.new(
|
|
201
|
+
@failed_checks << Failure.new(
|
|
202
|
+
m[:filename],
|
|
203
|
+
"Links > External",
|
|
204
|
+
description,
|
|
205
|
+
line: m[:line],
|
|
206
|
+
status: status,
|
|
207
|
+
content: m[:element]&.content,
|
|
208
|
+
element: m[:element],
|
|
209
|
+
)
|
|
171
210
|
end
|
|
172
211
|
end
|
|
173
212
|
end
|
|
@@ -29,15 +29,18 @@ module HTMLProofer
|
|
|
29
29
|
matched_count_to_log = pluralize(matched_files.count, "reference", "references")
|
|
30
30
|
@logger.log(:debug, "(#{i + 1} / #{links.count}) Internal link #{link}: Checking #{matched_count_to_log}")
|
|
31
31
|
matched_files.each do |metadata|
|
|
32
|
-
url = HTMLProofer::Attribute::Url.new(@runner, link, base_url: metadata[:base_url])
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
url = HTMLProofer::Attribute::Url.new(@runner, link, base_url: metadata[:base_url], source: metadata[:source], filename: metadata[:filename])
|
|
33
|
+
|
|
34
|
+
unless url.exists?
|
|
35
|
+
@failed_checks << Failure.new(
|
|
36
|
+
metadata[:filename],
|
|
37
|
+
"Links > Internal",
|
|
38
|
+
"internally linking to #{url}, which does not exist",
|
|
39
|
+
line: metadata[:line],
|
|
40
|
+
status: nil,
|
|
41
|
+
content: metadata[:element]&.content,
|
|
42
|
+
element: metadata[:element],
|
|
43
|
+
)
|
|
41
44
|
to_add << [url, metadata, false]
|
|
42
45
|
next
|
|
43
46
|
end
|
|
@@ -45,6 +48,7 @@ module HTMLProofer
|
|
|
45
48
|
hash_exists = hash_exists_for_url?(url)
|
|
46
49
|
if hash_exists.nil?
|
|
47
50
|
# the hash needs to be checked in the target file, we collect the url and metadata
|
|
51
|
+
target_file_path = url.resolved_path
|
|
48
52
|
unless file_paths_hashes_to_check.key?(target_file_path)
|
|
49
53
|
file_paths_hashes_to_check[target_file_path] = {}
|
|
50
54
|
end
|
|
@@ -55,8 +59,15 @@ module HTMLProofer
|
|
|
55
59
|
next
|
|
56
60
|
end
|
|
57
61
|
unless hash_exists
|
|
58
|
-
@failed_checks << Failure.new(
|
|
59
|
-
|
|
62
|
+
@failed_checks << Failure.new(
|
|
63
|
+
metadata[:filename],
|
|
64
|
+
"Links > Internal",
|
|
65
|
+
"internally linking to #{url}; the file exists, but the hash '#{url.hash}' does not",
|
|
66
|
+
line: metadata[:line],
|
|
67
|
+
status: nil,
|
|
68
|
+
content: metadata[:element]&.content,
|
|
69
|
+
element: metadata[:element],
|
|
70
|
+
)
|
|
60
71
|
to_add << [url, metadata, false]
|
|
61
72
|
next
|
|
62
73
|
end
|
|
@@ -75,8 +86,15 @@ module HTMLProofer
|
|
|
75
86
|
exists = hash_exists_in_html?(href_hash, html)
|
|
76
87
|
url_metadata.each do |(url, metadata)|
|
|
77
88
|
unless exists
|
|
78
|
-
@failed_checks << Failure.new(
|
|
79
|
-
|
|
89
|
+
@failed_checks << Failure.new(
|
|
90
|
+
metadata[:filename],
|
|
91
|
+
"Links > Internal",
|
|
92
|
+
"internally linking to #{url}; the file exists, but the hash '#{href_hash}' does not",
|
|
93
|
+
line: metadata[:line],
|
|
94
|
+
status: nil,
|
|
95
|
+
content: metadata[:element]&.content,
|
|
96
|
+
element: metadata[:element],
|
|
97
|
+
)
|
|
80
98
|
end
|
|
81
99
|
to_add << [url, metadata, exists]
|
|
82
100
|
end
|
|
@@ -91,12 +109,6 @@ module HTMLProofer
|
|
|
91
109
|
@failed_checks
|
|
92
110
|
end
|
|
93
111
|
|
|
94
|
-
private def file_exists?(absolute_path)
|
|
95
|
-
return @runner.checked_paths[absolute_path] if @runner.checked_paths.key?(absolute_path)
|
|
96
|
-
|
|
97
|
-
@runner.checked_paths[absolute_path] = File.exist?(absolute_path)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
112
|
# verify the hash w/o just based on the URL, w/o looking at the target file
|
|
101
113
|
# => returns nil if the has could not be verified
|
|
102
114
|
private def hash_exists_for_url?(url)
|
|
@@ -104,16 +116,17 @@ module HTMLProofer
|
|
|
104
116
|
return true if blank?(href_hash)
|
|
105
117
|
return true unless @runner.options[:check_internal_hash]
|
|
106
118
|
|
|
107
|
-
#
|
|
108
|
-
|
|
119
|
+
# For hash links, we need to defer to file-based checking
|
|
120
|
+
# (even if URL doesn't have extension, it may resolve to index.html)
|
|
109
121
|
return false unless url.has_hash?
|
|
110
122
|
|
|
123
|
+
# If URL has no known extension, defer to file-based checking
|
|
124
|
+
return false unless url.known_extension?
|
|
125
|
+
|
|
111
126
|
decoded_href_hash = Addressable::URI.unescape(href_hash)
|
|
112
127
|
fragment_ids = [href_hash, decoded_href_hash]
|
|
113
128
|
# https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
nil
|
|
129
|
+
true if fragment_ids.include?("top")
|
|
117
130
|
end
|
|
118
131
|
|
|
119
132
|
private def hash_exists_in_html?(href_hash, html)
|
|
@@ -124,10 +137,18 @@ module HTMLProofer
|
|
|
124
137
|
|
|
125
138
|
private def find_fragments(fragment_ids, html)
|
|
126
139
|
xpaths = fragment_ids.uniq.flat_map do |frag_id|
|
|
127
|
-
|
|
140
|
+
# Build XPath string argument, handling single quotes
|
|
141
|
+
if frag_id.include?("'")
|
|
142
|
+
# Use concat() to handle single quotes: concat('part1', "'", 'part2')
|
|
143
|
+
escaped_frag_id = frag_id.split("'").map { |part| "'#{part}'" }.join(", \"'\", ")
|
|
144
|
+
xpath_arg = "concat(#{escaped_frag_id})"
|
|
145
|
+
else
|
|
146
|
+
# No single quotes, just use quoted string
|
|
147
|
+
xpath_arg = "'#{frag_id}'"
|
|
148
|
+
end
|
|
128
149
|
[
|
|
129
|
-
"//*[case_sensitive_equals(@id,
|
|
130
|
-
"//*[case_sensitive_equals(@name,
|
|
150
|
+
"//*[case_sensitive_equals(@id, #{xpath_arg})]",
|
|
151
|
+
"//*[case_sensitive_equals(@name, #{xpath_arg})]",
|
|
131
152
|
]
|
|
132
153
|
end
|
|
133
154
|
xpaths << XpathFunctions.new
|
data/lib/html_proofer/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ module HTMLProofer
|
|
|
4
4
|
# https://stackoverflow.com/a/8812293
|
|
5
5
|
class XpathFunctions
|
|
6
6
|
def case_sensitive_equals(node_set, str_to_match)
|
|
7
|
-
node_set.find_all { |node| node.to_s
|
|
7
|
+
node_set.find_all { |node| node.to_s == str_to_match.to_s }
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
data/lib/html_proofer.rb
CHANGED