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.
@@ -13,7 +13,7 @@ module HTMLProofer
13
13
  check_external_hash: true,
14
14
  check_internal_hash: true,
15
15
  checks: DEFAULT_TESTS,
16
- directory_index_file: "index.html",
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? ? [] : list.split(",")
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, _list|
191
- @options[long_opt_symbol] = parse_json_option("hydra", arg, symbolize_names: false)
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, _list|
195
- @options[long_opt_symbol] = parse_json_option("cache", arg, symbolize_names: false)
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: ["Automatically add specified extension to files for internal links, ",
274
- "to allow extensionless URLs (as supported by most servers) (default: `.html`).",],
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
- extensions: ["A comma-separated list of Strings indicating the file extensions you",
277
- "would like to check (default: `.html`)",],
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: ["If `true`, does not flag `a` tags missing `href`. In HTML5, this is technically ",
281
- "allowed, but could also be human error. (default: `false`)",],
282
- checks: ["A comma-separated list of Strings indicating which checks you",
283
- "want to run (default: `[\"Links\", \"Images\", \"Scripts\"]",],
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\'s not marked as `https` (default: `true`)."],
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: ["If `true`, ignores images with empty/missing ",
292
- "alt tags (in other words, `<img alt>` and `<img alt=\"\">`",
293
- "are valid; set this to `false` to flag those) (default: `true`).",],
294
- ignore_empty_mailto: ["If `true`, allows `mailto:` `href`s which don't",
295
- "contain an email address (default: `false`)'.",],
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: ["A comma-separated list of Strings or RegExps containing URLs that are",
300
- "safe to ignore. This affects all HTML attributes, such as `alt` tags on images.",],
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: ["JSON-formatted config that maps element names to the",
305
- "preferred attribute to check (default: `{}`).",],
306
- swap_urls: ["A comma-separated list containing key-value pairs of `RegExp => String`.",
307
- "It transforms URLs that match `RegExp` into `String` via `gsub`.",
308
- "The escape sequences `\\:` should be used to produce literal `:`s.",],
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: ["Sets the logging level. One of `:debug`, `:info`, ",
315
- "`:warn`, `:error`, or `:fatal`. (default: `:info`)",],
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
@@ -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 nil unless meta_tag?
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 nil if !img_tag? && !script_tag? && !source_tag?
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 nil if !img_tag? && !source_tag?
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 nil if !a_tag? && !link_tag?
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 nil if blank?(srcset)
99
+ return if blank?(srcset)
87
100
 
88
- srcset.split(",").map(&:strip)
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 nil if blank?(srcsets)
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
- return true unless blank?(attrs)
136
+ true unless blank?(attrs)
122
137
  end
123
138
 
124
139
  private def swap_attributes!
@@ -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
@@ -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(format: false, \
15
- name: "HTMLProofer", \
16
- level: "gte.#{log_level}") do |l|
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 (STDOUT_LEVELS.include?(level) && $stdout.isatty) || \
43
- (STDERR_LEVELS.include?(level) && $stderr.isatty)
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
 
@@ -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, :checked_paths, :current_check
10
- attr_accessor :current_filename, :current_source, :reporter
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
- @checked_paths = {}
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(:info,
46
- "Running #{check_text} (#{format_checks_list(checks)}) in #{@source} on *#{@options[:extensions].join(", ")} files ...\n\n")
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
- return true if pattern.is_a?(String) && pattern == file
170
- return true if pattern.is_a?(Regexp) && pattern =~ file
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.select { |f| f.is_a?(Failure) }
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(/HTMLProofer::Check::/, "")
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(href, url,
92
- response, filenames)
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.sans_hash} exists, but the hash '#{hash}' does not"
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(m[:filename], "Links > External", description, line: m[:line], status: status)
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
- @runner.current_source = metadata[:source]
35
- @runner.current_filename = metadata[:filename]
36
-
37
- target_file_path = url.absolute_path
38
- unless file_exists?(target_file_path)
39
- @failed_checks << Failure.new(@runner.current_filename, "Links > Internal",
40
- "internally linking to #{url}, which does not exist", line: metadata[:line], status: nil, content: nil)
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(@runner.current_filename, "Links > Internal",
59
- "internally linking to #{url}; the file exists, but the hash '#{url.hash}' does not", line: metadata[:line], status: nil, content: nil)
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(metadata[:filename], "Links > Internal",
79
- "internally linking to #{url}; the file exists, but the hash '#{href_hash}' does not", line: metadata[:line], status: nil, content: nil)
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
- # prevents searching files we didn't ask about
108
- return false unless url.known_extension?
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
- return true if fragment_ids.include?("top")
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
- escaped_frag_id = "'#{frag_id.split("'").join("', \"'\", '")}', ''"
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, concat(#{escaped_frag_id}))]",
130
- "//*[case_sensitive_equals(@name, concat(#{escaped_frag_id}))]",
150
+ "//*[case_sensitive_equals(@id, #{xpath_arg})]",
151
+ "//*[case_sensitive_equals(@name, #{xpath_arg})]",
131
152
  ]
132
153
  end
133
154
  xpaths << XpathFunctions.new
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTMLProofer
4
- VERSION = "5.0.0"
4
+ VERSION = "5.2.2"
5
5
  end
@@ -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.== str_to_match.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
@@ -15,8 +15,11 @@ require "html_proofer/version"
15
15
  require "fileutils"
16
16
 
17
17
  if ENV.fetch("DEBUG", false)
18
- require "awesome_print"
19
18
  require "debug"
19
+ begin
20
+ require "amazing_print"
21
+ rescue LoadError
22
+ end
20
23
  end
21
24
 
22
25
  module HTMLProofer