html-proofer 2.3.0 → 3.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 +85 -0
- data/lib/html-proofer/cache.rb +153 -0
- data/lib/html-proofer/check/favicon.rb +15 -0
- data/lib/html-proofer/check/html.rb +21 -0
- data/lib/html-proofer/check/images.rb +47 -0
- data/lib/html-proofer/check/links.rb +120 -0
- data/lib/html-proofer/check/scripts.rb +28 -0
- data/lib/html-proofer/check.rb +63 -0
- data/lib/html-proofer/configuration.rb +56 -0
- data/lib/html-proofer/element.rb +165 -0
- data/lib/{html/proofer/check_runner → html-proofer}/issue.rb +8 -10
- data/lib/html-proofer/log.rb +49 -0
- data/lib/html-proofer/runner.rb +160 -0
- data/lib/html-proofer/url_validator.rb +218 -0
- data/lib/html-proofer/utils.rb +40 -0
- data/lib/html-proofer/version.rb +3 -0
- data/lib/html-proofer.rb +47 -0
- metadata +64 -289
- data/.gitignore +0 -26
- data/.travis.yml +0 -12
- data/Gemfile +0 -3
- data/LICENSE.txt +0 -22
- data/README.md +0 -265
- data/Rakefile +0 -21
- data/bin/htmlproof +0 -71
- data/html-proofer.gemspec +0 -31
- data/lib/html/proofer/check_runner.rb +0 -62
- data/lib/html/proofer/checkable.rb +0 -164
- data/lib/html/proofer/checks/favicon.rb +0 -21
- data/lib/html/proofer/checks/html.rb +0 -40
- data/lib/html/proofer/checks/images.rb +0 -57
- data/lib/html/proofer/checks/links.rb +0 -107
- data/lib/html/proofer/checks/scripts.rb +0 -39
- data/lib/html/proofer/log.rb +0 -38
- data/lib/html/proofer/url_validator.rb +0 -135
- data/lib/html/proofer/utils.rb +0 -24
- data/lib/html/proofer/version.rb +0 -6
- data/lib/html/proofer.rb +0 -189
- data/script/bootstrap +0 -5
- data/spec/html/proofer/checkable_spec.rb +0 -47
- data/spec/html/proofer/command_spec.rb +0 -88
- data/spec/html/proofer/favicon_spec.rb +0 -47
- data/spec/html/proofer/fixtures/favicon/favicon_absent.html +0 -4
- data/spec/html/proofer/fixtures/favicon/favicon_absent_apple.html +0 -7
- data/spec/html/proofer/fixtures/favicon/favicon_broken.html +0 -7
- data/spec/html/proofer/fixtures/favicon/favicon_broken_but_ignored.html +0 -7
- data/spec/html/proofer/fixtures/favicon/favicon_present.html +0 -7
- data/spec/html/proofer/fixtures/favicon/favicon_present_shortcut.html +0 -7
- data/spec/html/proofer/fixtures/html/div_inside_head.html +0 -6
- data/spec/html/proofer/fixtures/html/html5_tags.html +0 -9
- data/spec/html/proofer/fixtures/html/invalid_tag.html +0 -3
- data/spec/html/proofer/fixtures/html/missing_closing_quotes.html +0 -5
- data/spec/html/proofer/fixtures/html/opening_and_ending_tag_mismatch.html +0 -7
- data/spec/html/proofer/fixtures/html/svg_elements.html +0 -729
- data/spec/html/proofer/fixtures/html/unescaped_ampersand_in_attribute.html +0 -4
- data/spec/html/proofer/fixtures/html/unmatched_end_tag.html +0 -5
- data/spec/html/proofer/fixtures/images/emptyImageAltText.html +0 -10
- data/spec/html/proofer/fixtures/images/existingImageExternal.html +0 -9
- data/spec/html/proofer/fixtures/images/gpl.png +0 -0
- data/spec/html/proofer/fixtures/images/ignorableAltViaOptions.html +0 -13
- data/spec/html/proofer/fixtures/images/ignorableImages.html +0 -10
- data/spec/html/proofer/fixtures/images/ignoreAltButNotLink.html +0 -13
- data/spec/html/proofer/fixtures/images/image_missing_protocol_invalid.html +0 -9
- data/spec/html/proofer/fixtures/images/image_missing_protocol_valid.html +0 -9
- data/spec/html/proofer/fixtures/images/missingImageAlt.html +0 -9
- data/spec/html/proofer/fixtures/images/missingImageAltText.html +0 -9
- data/spec/html/proofer/fixtures/images/missingImageExternal.html +0 -9
- data/spec/html/proofer/fixtures/images/missingImageInternal.html +0 -9
- data/spec/html/proofer/fixtures/images/missingImageSrc.html +0 -9
- data/spec/html/proofer/fixtures/images/relativeToSelf.html +0 -2
- data/spec/html/proofer/fixtures/images/rootRelativeImages.html +0 -2
- data/spec/html/proofer/fixtures/images/srcSetCheck.html +0 -7
- data/spec/html/proofer/fixtures/images/srcSetIgnorable.html +0 -13
- data/spec/html/proofer/fixtures/images/srcSetMissingAlt.html +0 -7
- data/spec/html/proofer/fixtures/images/srcSetMissingImage.html +0 -7
- data/spec/html/proofer/fixtures/images/terribleImageName.html +0 -9
- data/spec/html/proofer/fixtures/images/workingDataURIImage.html +0 -3
- data/spec/html/proofer/fixtures/links/_site/folder.html/index.html +0 -1
- data/spec/html/proofer/fixtures/links/anchors_in_pre.html +0 -12
- data/spec/html/proofer/fixtures/links/attributeWithDash.html +0 -8
- data/spec/html/proofer/fixtures/links/blank_mailto_link.html +0 -9
- data/spec/html/proofer/fixtures/links/blank_tel_link.html +0 -9
- data/spec/html/proofer/fixtures/links/brokenHashExternal.html +0 -11
- data/spec/html/proofer/fixtures/links/brokenHashInternal.html +0 -14
- data/spec/html/proofer/fixtures/links/brokenHashOnTheWeb.html +0 -2
- data/spec/html/proofer/fixtures/links/brokenInternalLink.html +0 -0
- data/spec/html/proofer/fixtures/links/brokenLinkExternal.html +0 -11
- data/spec/html/proofer/fixtures/links/brokenLinkInternal.html +0 -10
- data/spec/html/proofer/fixtures/links/brokenLinkWithNumber.html +0 -8
- data/spec/html/proofer/fixtures/links/brokenUnixLinks.html +0 -5
- data/spec/html/proofer/fixtures/links/broken_hash_with_query.html +0 -9
- data/spec/html/proofer/fixtures/links/checkSSLLinks.html +0 -1
- data/spec/html/proofer/fixtures/links/ensure_typhoeus_options.html +0 -1
- data/spec/html/proofer/fixtures/links/erstiebegr%C3%BC%C3%9Fung.html +0 -0
- data/spec/html/proofer/fixtures/links/erstiebegr/303/274/303/237ung.html +0 -0
- data/spec/html/proofer/fixtures/links/escape_pipes.html +0 -9
- data/spec/html/proofer/fixtures/links/file.foo +0 -11
- data/spec/html/proofer/fixtures/links/folder/anchorLink.html +0 -0
- data/spec/html/proofer/fixtures/links/folder/assets/barrel.png +0 -0
- data/spec/html/proofer/fixtures/links/folder/index.html +0 -0
- data/spec/html/proofer/fixtures/links/folder/multiples/catalog/file.html +0 -8
- data/spec/html/proofer/fixtures/links/folder/multiples/javadoc/file.html +0 -8
- data/spec/html/proofer/fixtures/links/folder/relativeImage.html +0 -2
- data/spec/html/proofer/fixtures/links/folder-php/index.php +0 -2
- data/spec/html/proofer/fixtures/links/githubHash.html +0 -1
- data/spec/html/proofer/fixtures/links/gpl.png +0 -0
- data/spec/html/proofer/fixtures/links/hashReferringToSelf.html +0 -44
- data/spec/html/proofer/fixtures/links/head_link_href.html +0 -7
- data/spec/html/proofer/fixtures/links/head_link_href_absent.html +0 -7
- data/spec/html/proofer/fixtures/links/head_link_href_empty.html +0 -7
- data/spec/html/proofer/fixtures/links/ignorableLinks.html +0 -13
- data/spec/html/proofer/fixtures/links/ignorableLinksViaOptions.html +0 -13
- data/spec/html/proofer/fixtures/links/invalid_mailto_link.html +0 -9
- data/spec/html/proofer/fixtures/links/javascript_link.html +0 -9
- data/spec/html/proofer/fixtures/links/linkToFolder.html +0 -9
- data/spec/html/proofer/fixtures/links/linkTranslatedViaHrefSwap.html +0 -9
- data/spec/html/proofer/fixtures/links/linkWithHttps.html +0 -9
- data/spec/html/proofer/fixtures/links/linkWithRedirect.html +0 -12
- data/spec/html/proofer/fixtures/links/link_directory_without_slash.html +0 -1
- data/spec/html/proofer/fixtures/links/link_missing_protocol_invalid.html +0 -9
- data/spec/html/proofer/fixtures/links/link_missing_protocol_valid.html +0 -9
- data/spec/html/proofer/fixtures/links/link_pointing_to_directory.html +0 -1
- data/spec/html/proofer/fixtures/links/links_in_pre.html +0 -13
- data/spec/html/proofer/fixtures/links/mailto_link.html +0 -9
- data/spec/html/proofer/fixtures/links/missingLinkHref.html +0 -9
- data/spec/html/proofer/fixtures/links/multipleProblems.html +0 -11
- data/spec/html/proofer/fixtures/links/nodupe.html +0 -1
- data/spec/html/proofer/fixtures/links/non_standard_characters.html +0 -10
- data/spec/html/proofer/fixtures/links/notarealhash.html +0 -0
- data/spec/html/proofer/fixtures/links/other_protocols.html +0 -4
- data/spec/html/proofer/fixtures/links/placeholder_with_empty_id.html +0 -1
- data/spec/html/proofer/fixtures/links/placeholder_with_id.html +0 -1
- data/spec/html/proofer/fixtures/links/placeholder_with_name.html +0 -1
- data/spec/html/proofer/fixtures/links/redirected_error.html +0 -1
- data/spec/html/proofer/fixtures/links/relativeLinks.html +0 -4
- data/spec/html/proofer/fixtures/links/rootLink/folder/index.html +0 -1
- data/spec/html/proofer/fixtures/links/rootLink/index.html +0 -1
- data/spec/html/proofer/fixtures/links/rootLink/rootLink.html +0 -9
- data/spec/html/proofer/fixtures/links/tel_link.html +0 -9
- data/spec/html/proofer/fixtures/links/urlencoded-href.html +0 -2
- data/spec/html/proofer/fixtures/links/utf8Link.html +0 -2
- data/spec/html/proofer/fixtures/resources/Screen Shot 2012-08-09 at 7.51.18 AM.png +0 -0
- data/spec/html/proofer/fixtures/resources/books/assets/getting-good-with-git.jpg +0 -0
- data/spec/html/proofer/fixtures/resources/books/assets/pro-git.jpg +0 -0
- data/spec/html/proofer/fixtures/resources/books/assets/version-control-with-git.jpg +0 -0
- data/spec/html/proofer/fixtures/resources/books/nestedRelativeImages.html +0 -5
- data/spec/html/proofer/fixtures/resources/gpl.png +0 -0
- data/spec/html/proofer/fixtures/scripts/ignorableLinksViaOptions.html +0 -4
- data/spec/html/proofer/fixtures/scripts/script.js +0 -1
- data/spec/html/proofer/fixtures/scripts/script_broken_external.html +0 -9
- data/spec/html/proofer/fixtures/scripts/script_content.html +0 -9
- data/spec/html/proofer/fixtures/scripts/script_content_absent.html +0 -9
- data/spec/html/proofer/fixtures/scripts/script_in_pre.html +0 -5
- data/spec/html/proofer/fixtures/scripts/script_missing_internal.html +0 -9
- data/spec/html/proofer/fixtures/scripts/script_valid_internal.html +0 -9
- data/spec/html/proofer/fixtures/sorting/issue/broken_image_one.html +0 -1
- data/spec/html/proofer/fixtures/sorting/issue/broken_image_two.html +0 -4
- data/spec/html/proofer/fixtures/sorting/path/multiple_issues.html +0 -11
- data/spec/html/proofer/fixtures/sorting/path/single_issue.html +0 -1
- data/spec/html/proofer/fixtures/sorting/status/a_404.html +0 -1
- data/spec/html/proofer/fixtures/sorting/status/broken_link.html +0 -3
- data/spec/html/proofer/fixtures/sorting/status/missing_redirect.html +0 -1
- data/spec/html/proofer/fixtures/utils/lang-jp.html +0 -1
- data/spec/html/proofer/html_spec.rb +0 -57
- data/spec/html/proofer/images_spec.rb +0 -147
- data/spec/html/proofer/links_spec.rb +0 -364
- data/spec/html/proofer/scripts_spec.rb +0 -47
- data/spec/html/proofer/utils_spec.rb +0 -14
- data/spec/html/proofer_spec.rb +0 -122
- data/spec/spec_helper.rb +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ce6c96e75683a8da4de60754d83aef138221d02
|
|
4
|
+
data.tar.gz: b341f1ee5b7237e58cba8e775dfaa152eaeeb0a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 000ff0816076d1e8b738526f380e88309fdc04d93fa203a43d62960696064062b7942647a5f79be0f8e7239ca69e67f08f669866352cabf67152a6f95beb643b
|
|
7
|
+
data.tar.gz: 1863ee52843f2d5071170aaed79ce2ae9f6902ffb1a99d93021b746eb9d3d792d460d4caff2f703907291915b0bc8861af8967cd4f5a394e0a6604aa23e0dec0
|
data/bin/htmlproofer
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
STDOUT.sync = true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w( .. lib ))
|
|
5
|
+
|
|
6
|
+
require 'html-proofer'
|
|
7
|
+
require 'mercenary'
|
|
8
|
+
|
|
9
|
+
Mercenary.program(:htmlproofer) do |p|
|
|
10
|
+
p.version HTMLProofer::VERSION
|
|
11
|
+
p.description %(Test your rendered HTML files to make sure they're accurate.)
|
|
12
|
+
p.syntax 'htmlproofer PATH [options]'
|
|
13
|
+
|
|
14
|
+
p.description 'Runs the HTML-Proofer suite on the files in PATH. For more details, see the README.'
|
|
15
|
+
|
|
16
|
+
p.option 'allow_hash_href', '--allow-hash-href', 'If `true`, ignores the `href` `#`'
|
|
17
|
+
p.option 'as_links', '--as-links', 'Assumes that `PATH` is a comma-separated array of links to check.'
|
|
18
|
+
p.option 'alt_ignore', '--alt-ignore image1,[image2,...]', Array, 'A comma-separated list of Strings or RegExps containing `img`s whose missing `alt` tags are safe to ignore'
|
|
19
|
+
p.option 'assume_extension', '--assume-extension', 'Automatically add extension (e.g. `.html`) to file paths, to allow extensionless URLs (as supported by Jekyll 3 and GitHub Pages) (default: `false`).'
|
|
20
|
+
p.option 'checks_to_ignore', '--checks-to-ignore check1,[check2,...]', Array, ' An array of Strings indicating which checks you\'d like to not perform.'
|
|
21
|
+
p.option 'check_external_hash', '--check-external-hash', 'Checks whether external hashes exist (even if the website exists). This slows the checker down (default: `false`).'
|
|
22
|
+
p.option 'check_favicon', '--check-favicon', 'Enables the favicon checker (default: `false`).'
|
|
23
|
+
p.option 'check_html', '--check-html', 'Enables HTML validation errors from Nokogiri (default: `false`).'
|
|
24
|
+
p.option 'directory_index_file', '--directory-index-file', String, 'Sets the file to look for when a link refers to a directory. (default: `index.html`)'
|
|
25
|
+
p.option 'disable_external', '--disable-external', 'If `true`, does not run the external link checker, which can take a lot of time (default: `false`)'
|
|
26
|
+
p.option 'empty_alt_ignore', '--empty-alt-ignore', 'If `true`, ignores images with empty alt tags'
|
|
27
|
+
p.option 'error_sort', '--error-sort SORT', 'Defines the sort order for error output. Can be `:path`, `:desc`, or `:status` (default: `path`).'
|
|
28
|
+
p.option 'enforce_https', '--enforce-https', 'Fails a link if it\'s not marked as `https` (default: `false`).'
|
|
29
|
+
p.option 'extension', '--extension EXT', String, 'The extension of your HTML files including the dot. (default: `.html`)'
|
|
30
|
+
p.option 'external_only', '--external_only', 'Only checks problems with external references'
|
|
31
|
+
p.option 'file_ignore', '--file-ignore file1,[file2,...]', Array, 'A comma-separated list of Strings or RegExps containing file paths that are safe to ignore'
|
|
32
|
+
p.option 'http_status_ignore', '--http-status-ignore 123,[xxx, ...]', Array, 'A comma-separated list of numbers representing status codes to ignore.'
|
|
33
|
+
p.option 'report_invalid_tags', '--report-invalid-tags', 'Ignore `check_html` errors associated with unknown markup (default: `false`)'
|
|
34
|
+
p.option 'report_script_embeds', '--report-script-embeds', 'Ignore `check_html` errors associated with `script`s (default: `false`)'
|
|
35
|
+
p.option 'log_level', '--log-level <level>', String, 'Sets the logging level, as determined by Yell'
|
|
36
|
+
p.option 'only_4xx', '--only-4xx', 'Only reports errors for links that fall within the 4xx status code range'
|
|
37
|
+
p.option 'timeframe', '--timeframe <time>', String, 'A string representing the caching timeframe.'
|
|
38
|
+
p.option 'url_ignore', '--url-ignore link1,[link2,...]', Array, 'A comma-separated list of Strings or RegExps containing URLs that are safe to ignore. It affects all HTML attributes. Note that non-HTTP(S) URIs are always ignored'
|
|
39
|
+
p.option 'url_swap', '--url-swap re:string,[re:string,...]', Array, 'A comma-separated list containing key-value pairs of `RegExp => String`. It transforms URLs that match `RegExp` into `String` via `gsub`.'
|
|
40
|
+
|
|
41
|
+
p.action do |args, opts|
|
|
42
|
+
args = ['.'] if args.empty?
|
|
43
|
+
path = args.first
|
|
44
|
+
|
|
45
|
+
options = {}
|
|
46
|
+
|
|
47
|
+
# prepare everything to go to proofer
|
|
48
|
+
p.options.select { |o| !opts[o.config_key].nil? }.each do |option|
|
|
49
|
+
if option.return_type.to_s == 'Array' # TODO: is_a? doesn't work here?
|
|
50
|
+
opts[option.config_key] = opts[option.config_key].map { |i| HTMLProofer::Configuration.to_regex?(i) }
|
|
51
|
+
end
|
|
52
|
+
options[option.config_key.to_sym] = opts[option.config_key]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# some minor manipulation of a special option
|
|
56
|
+
unless opts['url_swap'].nil?
|
|
57
|
+
options[:url_swap] = {}
|
|
58
|
+
opts['url_swap'].each do |s|
|
|
59
|
+
pair = s.split(':', 2)
|
|
60
|
+
options[:url_swap][Regexp.new(pair[0])] = pair[1]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
options[:error_sort] = opts['error-sort'].to_sym unless opts['error-sort'].nil?
|
|
65
|
+
options[:log_level] = opts['log_level'].to_sym unless opts['log_level'].nil?
|
|
66
|
+
|
|
67
|
+
# FIXME: this is gross
|
|
68
|
+
options[:validation] = {}
|
|
69
|
+
options[:validation][:report_script_embeds] = opts['report_script_embeds']
|
|
70
|
+
options[:validation][:report_invalid_tags] = opts['report_invalid_tags']
|
|
71
|
+
|
|
72
|
+
options[:cache] = {}
|
|
73
|
+
options[:cache][:timeframe] = opts['timeframe'] unless opts['timeframe'].nil?
|
|
74
|
+
|
|
75
|
+
paths = path.split(',')
|
|
76
|
+
if opts['as_links']
|
|
77
|
+
links = path.delete(' ').split(',')
|
|
78
|
+
HTMLProofer.check_links(links, options).run
|
|
79
|
+
elsif File.directory?(paths.first)
|
|
80
|
+
HTMLProofer.check_directories(paths, options).run
|
|
81
|
+
else
|
|
82
|
+
HTMLProofer.check_file(path, options).run
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require_relative 'utils'
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/core_ext/string'
|
|
5
|
+
require 'active_support/core_ext/date'
|
|
6
|
+
require 'active_support/core_ext/numeric/time'
|
|
7
|
+
|
|
8
|
+
module HTMLProofer
|
|
9
|
+
class Cache
|
|
10
|
+
include HTMLProofer::Utils
|
|
11
|
+
|
|
12
|
+
CACHE_LOG = File.join(STORAGE_DIR, 'cache.log')
|
|
13
|
+
|
|
14
|
+
attr_reader :exists, :cache_log
|
|
15
|
+
|
|
16
|
+
def initialize(logger, options)
|
|
17
|
+
@logger = logger
|
|
18
|
+
@cache_log = {}
|
|
19
|
+
|
|
20
|
+
if options.nil? || options.empty?
|
|
21
|
+
define_singleton_method('use_cache?') { false }
|
|
22
|
+
else
|
|
23
|
+
define_singleton_method('use_cache?') { true }
|
|
24
|
+
@parsed_timeframe = parsed_timeframe(options[:timeframe])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@cache_time = Time.now
|
|
28
|
+
|
|
29
|
+
if File.exist?(CACHE_LOG)
|
|
30
|
+
contents = File.read(CACHE_LOG)
|
|
31
|
+
@cache_log = contents.empty? ? {} : JSON.parse(contents)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def within_timeframe?(time)
|
|
36
|
+
(@parsed_timeframe..@cache_time).cover?(time)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def urls
|
|
40
|
+
@cache_log['urls'] || []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def size
|
|
44
|
+
@cache_log.length
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parsed_timeframe(timeframe)
|
|
48
|
+
time, date = timeframe.match(/(\d+)(\D)/).captures
|
|
49
|
+
time = time.to_f
|
|
50
|
+
case date
|
|
51
|
+
when 'M'
|
|
52
|
+
time.months.ago
|
|
53
|
+
when 'w'
|
|
54
|
+
time.weeks.ago
|
|
55
|
+
when 'd'
|
|
56
|
+
time.days.ago
|
|
57
|
+
when 'h'
|
|
58
|
+
time.hours.ago
|
|
59
|
+
else
|
|
60
|
+
fail ArgumentError, "#{date} is not a valid timeframe!"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add(url, filenames, status, msg = '')
|
|
65
|
+
data = {
|
|
66
|
+
:time => @cache_time,
|
|
67
|
+
:filenames => filenames,
|
|
68
|
+
:status => status,
|
|
69
|
+
:message => msg
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@cache_log[clean_url(url)] = data
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def detect_url_changes(found)
|
|
76
|
+
existing_urls = @cache_log.keys.map { |url| clean_url(url) }
|
|
77
|
+
found_urls = found.keys.map { |url| clean_url(url) }
|
|
78
|
+
|
|
79
|
+
# prepare to add new URLs detected
|
|
80
|
+
additions = found.reject do |url, _|
|
|
81
|
+
url = clean_url(url)
|
|
82
|
+
if existing_urls.include?(url)
|
|
83
|
+
true
|
|
84
|
+
else
|
|
85
|
+
@logger.log :debug, "Adding #{url} to cache check"
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
new_link_count = additions.length
|
|
91
|
+
new_link_text = pluralize(new_link_count, 'link', 'links')
|
|
92
|
+
@logger.log :info, "Adding #{new_link_text} to the cache..."
|
|
93
|
+
|
|
94
|
+
# remove from cache URLs that no longer exist
|
|
95
|
+
del = 0
|
|
96
|
+
@cache_log.delete_if do |url, _|
|
|
97
|
+
url = clean_url(url)
|
|
98
|
+
if !found_urls.include?(url)
|
|
99
|
+
@logger.log :debug, "Removing #{url} from cache check"
|
|
100
|
+
del += 1
|
|
101
|
+
true
|
|
102
|
+
else
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
del_link_text = pluralize(del, 'link', 'links')
|
|
108
|
+
@logger.log :info, "Removing #{del_link_text} from the cache..."
|
|
109
|
+
|
|
110
|
+
additions
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write
|
|
114
|
+
File.write(CACHE_LOG, @cache_log.to_json)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def load?
|
|
118
|
+
@load.nil?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def retrieve_urls(external_urls)
|
|
122
|
+
urls_to_check = detect_url_changes(external_urls)
|
|
123
|
+
@cache_log.each_pair do |url, cache|
|
|
124
|
+
if within_timeframe?(cache['time'])
|
|
125
|
+
next if cache['message'].empty? # these were successes to skip
|
|
126
|
+
urls_to_check[url] = cache['filenames'] # these are failures to retry
|
|
127
|
+
else
|
|
128
|
+
urls_to_check[url] = cache['filenames'] # pass or fail, recheck expired links
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
urls_to_check
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# FIXME: there seems to be some discrepenacy where Typhoeus occasionally adds
|
|
135
|
+
# a trailing slash to URL strings, which causes issues with the cache
|
|
136
|
+
def slashless_url(url)
|
|
137
|
+
url.chomp('/')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# FIXME: it seems that Typhoeus actually acts on escaped URLs,
|
|
141
|
+
# but there's no way to get at that information, and the cache
|
|
142
|
+
# stores unescaped URLs. Because of this, some links, such as
|
|
143
|
+
# github.com/search/issues?q=is:open+is:issue+fig are not matched
|
|
144
|
+
# as github.com/search/issues?q=is%3Aopen+is%3Aissue+fig
|
|
145
|
+
def unescape_url(url)
|
|
146
|
+
Addressable::URI.unescape(url)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def clean_url(url)
|
|
150
|
+
slashless_url(unescape_url(url))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class FaviconCheck < ::HTMLProofer::Check
|
|
2
|
+
def run
|
|
3
|
+
found = false
|
|
4
|
+
@html.xpath('//link[not(ancestor::pre or ancestor::code)]').each do |node|
|
|
5
|
+
favicon = create_element(node)
|
|
6
|
+
next if favicon.ignore?
|
|
7
|
+
found = true if favicon.rel.split(' ').last.eql? 'icon'
|
|
8
|
+
break if found
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
return if found
|
|
12
|
+
|
|
13
|
+
add_issue 'no favicon specified'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class HtmlCheck < ::HTMLProofer::Check
|
|
2
|
+
SCRIPT_EMBEDS_MSG = /Element script embeds close tag/
|
|
3
|
+
INVALID_TAG_MSG = /Tag ([\w\-:]+) invalid/
|
|
4
|
+
INVALID_PREFIX = /Namespace prefix/
|
|
5
|
+
|
|
6
|
+
def run
|
|
7
|
+
@html.errors.each do |error|
|
|
8
|
+
message = error.message
|
|
9
|
+
line = error.line
|
|
10
|
+
|
|
11
|
+
if message =~ INVALID_TAG_MSG || message =~ INVALID_PREFIX
|
|
12
|
+
next unless options[:validation][:report_invalid_tags]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# tags embedded in scripts are used in templating languages: http://git.io/vOovv
|
|
16
|
+
next if !options[:validation][:report_script_embeds] && message =~ SCRIPT_EMBEDS_MSG
|
|
17
|
+
|
|
18
|
+
add_issue(message, line: line)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class ImageCheck < ::HTMLProofer::Check
|
|
2
|
+
SCREEN_SHOT_REGEX = /Screen(?: |%20)Shot(?: |%20)\d+-\d+-\d+(?: |%20)at(?: |%20)\d+.\d+.\d+/
|
|
3
|
+
|
|
4
|
+
def empty_alt_tag?
|
|
5
|
+
@img.alt.strip.empty?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def terrible_filename?
|
|
9
|
+
@img.url =~ SCREEN_SHOT_REGEX
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def missing_src?
|
|
13
|
+
blank?(@img.url)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
@html.css('img').each do |node|
|
|
18
|
+
@img = create_element(node)
|
|
19
|
+
line = node.line
|
|
20
|
+
|
|
21
|
+
next if @img.ignore?
|
|
22
|
+
|
|
23
|
+
# screenshot filenames should return because of terrible names
|
|
24
|
+
if terrible_filename?
|
|
25
|
+
add_issue("image has a terrible filename (#{@img.url})", line: line)
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# does the image exist?
|
|
30
|
+
if missing_src?
|
|
31
|
+
add_issue('image has no src or srcset attribute', line: line)
|
|
32
|
+
else
|
|
33
|
+
if @img.remote?
|
|
34
|
+
add_to_external_urls(@img.url, line)
|
|
35
|
+
elsif !@img.exists?
|
|
36
|
+
add_issue("internal image #{@img.url} does not exist", line: line)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if @img.alt.nil? || (empty_alt_tag? && !@img.ignore_empty_alt?)
|
|
41
|
+
add_issue("image #{@img.url} does not have an alt attribute", line: line)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
external_urls
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
class LinkCheck < ::HTMLProofer::Check
|
|
2
|
+
include HTMLProofer::Utils
|
|
3
|
+
|
|
4
|
+
def missing_href?
|
|
5
|
+
blank?(@link.href) && blank?(@link.name) && blank?(@link.id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def placeholder?
|
|
9
|
+
(!blank?(@link.id) || !blank?(@link.name)) && @link.href.nil?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
@html.css('a, link').each do |node|
|
|
14
|
+
@link = create_element(node)
|
|
15
|
+
line = @node.line
|
|
16
|
+
|
|
17
|
+
next if @link.ignore?
|
|
18
|
+
|
|
19
|
+
next if placeholder?
|
|
20
|
+
next if @link.allow_hash_href? && @link.href == '#'
|
|
21
|
+
|
|
22
|
+
# is it even a valid URL?
|
|
23
|
+
unless @link.valid?
|
|
24
|
+
add_issue("#{@link.href} is an invalid URL", line: line)
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
check_schemes(@link, line)
|
|
29
|
+
|
|
30
|
+
# is there even an href?
|
|
31
|
+
if missing_href?
|
|
32
|
+
# HTML5 allows dropping the href: http://git.io/vBX0z
|
|
33
|
+
next if @html.internal_subset.name == 'html' && @html.internal_subset.external_id.nil?
|
|
34
|
+
add_issue('anchor has no href attribute', line: line)
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# intentionally here because we still want valid? & missing_href? to execute
|
|
39
|
+
next if @link.non_http_remote?
|
|
40
|
+
# does the file even exist?
|
|
41
|
+
if @link.remote?
|
|
42
|
+
add_to_external_urls(@link.href, line)
|
|
43
|
+
next
|
|
44
|
+
elsif !@link.internal? && !@link.exists?
|
|
45
|
+
add_issue("internally linking to #{@link.href}, which does not exist", line: line)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# does the local directory have a trailing slash?
|
|
49
|
+
if @link.unslashed_directory? @link.absolute_path
|
|
50
|
+
add_issue("internally linking to a directory #{@link.absolute_path} without trailing slash", line: line)
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# verify the target hash
|
|
55
|
+
handle_hash(@link, line) if @link.hash
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
external_urls
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_schemes(link, line)
|
|
62
|
+
case link.scheme
|
|
63
|
+
when 'mailto'
|
|
64
|
+
handle_mailto(link, line)
|
|
65
|
+
when 'tel'
|
|
66
|
+
handle_tel(link, line)
|
|
67
|
+
when 'http'
|
|
68
|
+
return unless @options[:enforce_https]
|
|
69
|
+
add_issue("#{link.href} is not an HTTPS link", line: line)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_mailto(link, line)
|
|
74
|
+
if link.path.empty?
|
|
75
|
+
add_issue("#{link.href} contains no email address", line: line)
|
|
76
|
+
elsif !link.path.include?('@')
|
|
77
|
+
add_issue("#{link.href} contains an invalid email address", line: line)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def handle_tel(link, line)
|
|
82
|
+
add_issue("#{link.href} contains no phone number", line: line) if link.path.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def handle_hash(link, line)
|
|
86
|
+
if link.internal?
|
|
87
|
+
unless hash_check @html, link.hash
|
|
88
|
+
add_issue("linking to internal hash ##{link.hash} that does not exist", line: line)
|
|
89
|
+
end
|
|
90
|
+
elsif link.external?
|
|
91
|
+
external_link_check(link, line)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def external_link_check(link, line)
|
|
96
|
+
if !link.exists?
|
|
97
|
+
add_issue("trying to find hash of #{link.href}, but #{link.absolute_path} does not exist", line: line)
|
|
98
|
+
else
|
|
99
|
+
target_html = create_nokogiri link.absolute_path
|
|
100
|
+
unless hash_check target_html, link.hash
|
|
101
|
+
add_issue("linking to #{link.href}, but #{link.hash} does not exist", line: line)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def hash_check(html, href_hash)
|
|
107
|
+
decoded_href_hash = URI.decode(href_hash)
|
|
108
|
+
html.xpath("//*[case_insensitive_equals(@id, '#{href_hash}')]", \
|
|
109
|
+
"//*[case_insensitive_equals(@name, '#{href_hash}')]", \
|
|
110
|
+
"//*[case_insensitive_equals(@id, '#{decoded_href_hash}')]", \
|
|
111
|
+
"//*[case_insensitive_equals(@name, '#{decoded_href_hash}')]", \
|
|
112
|
+
XpathFunctions.new).length > 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class XpathFunctions
|
|
116
|
+
def case_insensitive_equals(node_set, str_to_match)
|
|
117
|
+
node_set.find_all {|node| node.to_s.downcase == str_to_match.to_s.downcase }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class ScriptCheck < ::HTMLProofer::Check
|
|
2
|
+
attr_reader :src
|
|
3
|
+
|
|
4
|
+
def missing_src?
|
|
5
|
+
!@script.src
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def run
|
|
9
|
+
@html.css('script').each do |node|
|
|
10
|
+
@script = create_element(node)
|
|
11
|
+
line = node.line
|
|
12
|
+
|
|
13
|
+
next if @script.ignore?
|
|
14
|
+
next unless node.text.strip.empty?
|
|
15
|
+
|
|
16
|
+
# does the script exist?
|
|
17
|
+
if missing_src?
|
|
18
|
+
add_issue('script is empty and has no src attribute', line: line)
|
|
19
|
+
elsif @script.remote?
|
|
20
|
+
add_to_external_urls(@script.src, line)
|
|
21
|
+
elsif !@script.exists?
|
|
22
|
+
add_issue("internal script #{@script.src} does not exist", line: line)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
external_urls
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module HTMLProofer
|
|
2
|
+
# Mostly handles issue management and collecting of external URLs.
|
|
3
|
+
class Check
|
|
4
|
+
attr_reader :node, :element, :src, :path, :options, :issues, :external_urls
|
|
5
|
+
|
|
6
|
+
def initialize(src, path, html, options)
|
|
7
|
+
@src = src
|
|
8
|
+
@path = path
|
|
9
|
+
@html = remove_ignored(html)
|
|
10
|
+
@options = options
|
|
11
|
+
@issues = []
|
|
12
|
+
@external_urls = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_element(node)
|
|
16
|
+
@node = node
|
|
17
|
+
Element.new(node, self)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
fail NotImplementedError, 'HTMLProofer::Check subclasses must implement #run'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_issue(desc, line: nil, status: -1)
|
|
25
|
+
@issues << Issue.new(@path, desc, line: line, status: status)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_to_external_urls(url, _)
|
|
29
|
+
return if @external_urls[url]
|
|
30
|
+
add_path_for_url(url)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_path_for_url(url)
|
|
34
|
+
if @external_urls[url]
|
|
35
|
+
@external_urls[url] << @path
|
|
36
|
+
else
|
|
37
|
+
@external_urls[url] = [@path]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.subchecks
|
|
42
|
+
classes = []
|
|
43
|
+
|
|
44
|
+
ObjectSpace.each_object(Class) do |c|
|
|
45
|
+
next unless c.superclass == self
|
|
46
|
+
classes << c
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
classes
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def blank?(attr)
|
|
53
|
+
attr.nil? || attr.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def remove_ignored(html)
|
|
59
|
+
html.css('code, pre, tt').each(&:unlink)
|
|
60
|
+
html
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module HTMLProofer
|
|
2
|
+
module Configuration
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
|
|
5
|
+
PROOFER_DEFAULTS = {
|
|
6
|
+
:allow_hash_href => false,
|
|
7
|
+
:alt_ignore => [],
|
|
8
|
+
:assume_extension => false,
|
|
9
|
+
:check_external_hash => false,
|
|
10
|
+
:check_favicon => false,
|
|
11
|
+
:check_html => false,
|
|
12
|
+
:checks_to_ignore => [],
|
|
13
|
+
:directory_index_file => 'index.html',
|
|
14
|
+
:disable_external => false,
|
|
15
|
+
:empty_alt_ignore => false,
|
|
16
|
+
:enforce_https => false,
|
|
17
|
+
:error_sort => :path,
|
|
18
|
+
:extension => '.html',
|
|
19
|
+
:external_only => false,
|
|
20
|
+
:file_ignore => [],
|
|
21
|
+
:http_status_ignore => [],
|
|
22
|
+
:log_level => :info,
|
|
23
|
+
:only_4xx => false,
|
|
24
|
+
:url_ignore => [],
|
|
25
|
+
:url_swap => []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
TYPHOEUS_DEFAULTS = {
|
|
29
|
+
:followlocation => true,
|
|
30
|
+
:headers => {
|
|
31
|
+
'User-Agent' => "Mozilla/5.0 (compatible; HTML Proofer/#{HTMLProofer::VERSION}; +https://github.com/gjtorikian/html-proofer)"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
HYDRA_DEFAULTS = {
|
|
36
|
+
:max_concurrency => 50
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
PARALLEL_DEFAULTS = {}
|
|
40
|
+
|
|
41
|
+
VALIDATION_DEFAULTS = {
|
|
42
|
+
:report_script_embeds => false,
|
|
43
|
+
:report_invalid_tags => false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CACHE_DEFAULTS = {}
|
|
47
|
+
|
|
48
|
+
def self.to_regex?(item)
|
|
49
|
+
if item.start_with?('/') && item.end_with?('/')
|
|
50
|
+
Regexp.new item[1...-1]
|
|
51
|
+
else
|
|
52
|
+
item
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|