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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/bin/htmlproofer +85 -0
  3. data/lib/html-proofer/cache.rb +153 -0
  4. data/lib/html-proofer/check/favicon.rb +15 -0
  5. data/lib/html-proofer/check/html.rb +21 -0
  6. data/lib/html-proofer/check/images.rb +47 -0
  7. data/lib/html-proofer/check/links.rb +120 -0
  8. data/lib/html-proofer/check/scripts.rb +28 -0
  9. data/lib/html-proofer/check.rb +63 -0
  10. data/lib/html-proofer/configuration.rb +56 -0
  11. data/lib/html-proofer/element.rb +165 -0
  12. data/lib/{html/proofer/check_runner → html-proofer}/issue.rb +8 -10
  13. data/lib/html-proofer/log.rb +49 -0
  14. data/lib/html-proofer/runner.rb +160 -0
  15. data/lib/html-proofer/url_validator.rb +218 -0
  16. data/lib/html-proofer/utils.rb +40 -0
  17. data/lib/html-proofer/version.rb +3 -0
  18. data/lib/html-proofer.rb +47 -0
  19. metadata +64 -289
  20. data/.gitignore +0 -26
  21. data/.travis.yml +0 -12
  22. data/Gemfile +0 -3
  23. data/LICENSE.txt +0 -22
  24. data/README.md +0 -265
  25. data/Rakefile +0 -21
  26. data/bin/htmlproof +0 -71
  27. data/html-proofer.gemspec +0 -31
  28. data/lib/html/proofer/check_runner.rb +0 -62
  29. data/lib/html/proofer/checkable.rb +0 -164
  30. data/lib/html/proofer/checks/favicon.rb +0 -21
  31. data/lib/html/proofer/checks/html.rb +0 -40
  32. data/lib/html/proofer/checks/images.rb +0 -57
  33. data/lib/html/proofer/checks/links.rb +0 -107
  34. data/lib/html/proofer/checks/scripts.rb +0 -39
  35. data/lib/html/proofer/log.rb +0 -38
  36. data/lib/html/proofer/url_validator.rb +0 -135
  37. data/lib/html/proofer/utils.rb +0 -24
  38. data/lib/html/proofer/version.rb +0 -6
  39. data/lib/html/proofer.rb +0 -189
  40. data/script/bootstrap +0 -5
  41. data/spec/html/proofer/checkable_spec.rb +0 -47
  42. data/spec/html/proofer/command_spec.rb +0 -88
  43. data/spec/html/proofer/favicon_spec.rb +0 -47
  44. data/spec/html/proofer/fixtures/favicon/favicon_absent.html +0 -4
  45. data/spec/html/proofer/fixtures/favicon/favicon_absent_apple.html +0 -7
  46. data/spec/html/proofer/fixtures/favicon/favicon_broken.html +0 -7
  47. data/spec/html/proofer/fixtures/favicon/favicon_broken_but_ignored.html +0 -7
  48. data/spec/html/proofer/fixtures/favicon/favicon_present.html +0 -7
  49. data/spec/html/proofer/fixtures/favicon/favicon_present_shortcut.html +0 -7
  50. data/spec/html/proofer/fixtures/html/div_inside_head.html +0 -6
  51. data/spec/html/proofer/fixtures/html/html5_tags.html +0 -9
  52. data/spec/html/proofer/fixtures/html/invalid_tag.html +0 -3
  53. data/spec/html/proofer/fixtures/html/missing_closing_quotes.html +0 -5
  54. data/spec/html/proofer/fixtures/html/opening_and_ending_tag_mismatch.html +0 -7
  55. data/spec/html/proofer/fixtures/html/svg_elements.html +0 -729
  56. data/spec/html/proofer/fixtures/html/unescaped_ampersand_in_attribute.html +0 -4
  57. data/spec/html/proofer/fixtures/html/unmatched_end_tag.html +0 -5
  58. data/spec/html/proofer/fixtures/images/emptyImageAltText.html +0 -10
  59. data/spec/html/proofer/fixtures/images/existingImageExternal.html +0 -9
  60. data/spec/html/proofer/fixtures/images/gpl.png +0 -0
  61. data/spec/html/proofer/fixtures/images/ignorableAltViaOptions.html +0 -13
  62. data/spec/html/proofer/fixtures/images/ignorableImages.html +0 -10
  63. data/spec/html/proofer/fixtures/images/ignoreAltButNotLink.html +0 -13
  64. data/spec/html/proofer/fixtures/images/image_missing_protocol_invalid.html +0 -9
  65. data/spec/html/proofer/fixtures/images/image_missing_protocol_valid.html +0 -9
  66. data/spec/html/proofer/fixtures/images/missingImageAlt.html +0 -9
  67. data/spec/html/proofer/fixtures/images/missingImageAltText.html +0 -9
  68. data/spec/html/proofer/fixtures/images/missingImageExternal.html +0 -9
  69. data/spec/html/proofer/fixtures/images/missingImageInternal.html +0 -9
  70. data/spec/html/proofer/fixtures/images/missingImageSrc.html +0 -9
  71. data/spec/html/proofer/fixtures/images/relativeToSelf.html +0 -2
  72. data/spec/html/proofer/fixtures/images/rootRelativeImages.html +0 -2
  73. data/spec/html/proofer/fixtures/images/srcSetCheck.html +0 -7
  74. data/spec/html/proofer/fixtures/images/srcSetIgnorable.html +0 -13
  75. data/spec/html/proofer/fixtures/images/srcSetMissingAlt.html +0 -7
  76. data/spec/html/proofer/fixtures/images/srcSetMissingImage.html +0 -7
  77. data/spec/html/proofer/fixtures/images/terribleImageName.html +0 -9
  78. data/spec/html/proofer/fixtures/images/workingDataURIImage.html +0 -3
  79. data/spec/html/proofer/fixtures/links/_site/folder.html/index.html +0 -1
  80. data/spec/html/proofer/fixtures/links/anchors_in_pre.html +0 -12
  81. data/spec/html/proofer/fixtures/links/attributeWithDash.html +0 -8
  82. data/spec/html/proofer/fixtures/links/blank_mailto_link.html +0 -9
  83. data/spec/html/proofer/fixtures/links/blank_tel_link.html +0 -9
  84. data/spec/html/proofer/fixtures/links/brokenHashExternal.html +0 -11
  85. data/spec/html/proofer/fixtures/links/brokenHashInternal.html +0 -14
  86. data/spec/html/proofer/fixtures/links/brokenHashOnTheWeb.html +0 -2
  87. data/spec/html/proofer/fixtures/links/brokenInternalLink.html +0 -0
  88. data/spec/html/proofer/fixtures/links/brokenLinkExternal.html +0 -11
  89. data/spec/html/proofer/fixtures/links/brokenLinkInternal.html +0 -10
  90. data/spec/html/proofer/fixtures/links/brokenLinkWithNumber.html +0 -8
  91. data/spec/html/proofer/fixtures/links/brokenUnixLinks.html +0 -5
  92. data/spec/html/proofer/fixtures/links/broken_hash_with_query.html +0 -9
  93. data/spec/html/proofer/fixtures/links/checkSSLLinks.html +0 -1
  94. data/spec/html/proofer/fixtures/links/ensure_typhoeus_options.html +0 -1
  95. data/spec/html/proofer/fixtures/links/erstiebegr%C3%BC%C3%9Fung.html +0 -0
  96. data/spec/html/proofer/fixtures/links/erstiebegr/303/274/303/237ung.html +0 -0
  97. data/spec/html/proofer/fixtures/links/escape_pipes.html +0 -9
  98. data/spec/html/proofer/fixtures/links/file.foo +0 -11
  99. data/spec/html/proofer/fixtures/links/folder/anchorLink.html +0 -0
  100. data/spec/html/proofer/fixtures/links/folder/assets/barrel.png +0 -0
  101. data/spec/html/proofer/fixtures/links/folder/index.html +0 -0
  102. data/spec/html/proofer/fixtures/links/folder/multiples/catalog/file.html +0 -8
  103. data/spec/html/proofer/fixtures/links/folder/multiples/javadoc/file.html +0 -8
  104. data/spec/html/proofer/fixtures/links/folder/relativeImage.html +0 -2
  105. data/spec/html/proofer/fixtures/links/folder-php/index.php +0 -2
  106. data/spec/html/proofer/fixtures/links/githubHash.html +0 -1
  107. data/spec/html/proofer/fixtures/links/gpl.png +0 -0
  108. data/spec/html/proofer/fixtures/links/hashReferringToSelf.html +0 -44
  109. data/spec/html/proofer/fixtures/links/head_link_href.html +0 -7
  110. data/spec/html/proofer/fixtures/links/head_link_href_absent.html +0 -7
  111. data/spec/html/proofer/fixtures/links/head_link_href_empty.html +0 -7
  112. data/spec/html/proofer/fixtures/links/ignorableLinks.html +0 -13
  113. data/spec/html/proofer/fixtures/links/ignorableLinksViaOptions.html +0 -13
  114. data/spec/html/proofer/fixtures/links/invalid_mailto_link.html +0 -9
  115. data/spec/html/proofer/fixtures/links/javascript_link.html +0 -9
  116. data/spec/html/proofer/fixtures/links/linkToFolder.html +0 -9
  117. data/spec/html/proofer/fixtures/links/linkTranslatedViaHrefSwap.html +0 -9
  118. data/spec/html/proofer/fixtures/links/linkWithHttps.html +0 -9
  119. data/spec/html/proofer/fixtures/links/linkWithRedirect.html +0 -12
  120. data/spec/html/proofer/fixtures/links/link_directory_without_slash.html +0 -1
  121. data/spec/html/proofer/fixtures/links/link_missing_protocol_invalid.html +0 -9
  122. data/spec/html/proofer/fixtures/links/link_missing_protocol_valid.html +0 -9
  123. data/spec/html/proofer/fixtures/links/link_pointing_to_directory.html +0 -1
  124. data/spec/html/proofer/fixtures/links/links_in_pre.html +0 -13
  125. data/spec/html/proofer/fixtures/links/mailto_link.html +0 -9
  126. data/spec/html/proofer/fixtures/links/missingLinkHref.html +0 -9
  127. data/spec/html/proofer/fixtures/links/multipleProblems.html +0 -11
  128. data/spec/html/proofer/fixtures/links/nodupe.html +0 -1
  129. data/spec/html/proofer/fixtures/links/non_standard_characters.html +0 -10
  130. data/spec/html/proofer/fixtures/links/notarealhash.html +0 -0
  131. data/spec/html/proofer/fixtures/links/other_protocols.html +0 -4
  132. data/spec/html/proofer/fixtures/links/placeholder_with_empty_id.html +0 -1
  133. data/spec/html/proofer/fixtures/links/placeholder_with_id.html +0 -1
  134. data/spec/html/proofer/fixtures/links/placeholder_with_name.html +0 -1
  135. data/spec/html/proofer/fixtures/links/redirected_error.html +0 -1
  136. data/spec/html/proofer/fixtures/links/relativeLinks.html +0 -4
  137. data/spec/html/proofer/fixtures/links/rootLink/folder/index.html +0 -1
  138. data/spec/html/proofer/fixtures/links/rootLink/index.html +0 -1
  139. data/spec/html/proofer/fixtures/links/rootLink/rootLink.html +0 -9
  140. data/spec/html/proofer/fixtures/links/tel_link.html +0 -9
  141. data/spec/html/proofer/fixtures/links/urlencoded-href.html +0 -2
  142. data/spec/html/proofer/fixtures/links/utf8Link.html +0 -2
  143. data/spec/html/proofer/fixtures/resources/Screen Shot 2012-08-09 at 7.51.18 AM.png +0 -0
  144. data/spec/html/proofer/fixtures/resources/books/assets/getting-good-with-git.jpg +0 -0
  145. data/spec/html/proofer/fixtures/resources/books/assets/pro-git.jpg +0 -0
  146. data/spec/html/proofer/fixtures/resources/books/assets/version-control-with-git.jpg +0 -0
  147. data/spec/html/proofer/fixtures/resources/books/nestedRelativeImages.html +0 -5
  148. data/spec/html/proofer/fixtures/resources/gpl.png +0 -0
  149. data/spec/html/proofer/fixtures/scripts/ignorableLinksViaOptions.html +0 -4
  150. data/spec/html/proofer/fixtures/scripts/script.js +0 -1
  151. data/spec/html/proofer/fixtures/scripts/script_broken_external.html +0 -9
  152. data/spec/html/proofer/fixtures/scripts/script_content.html +0 -9
  153. data/spec/html/proofer/fixtures/scripts/script_content_absent.html +0 -9
  154. data/spec/html/proofer/fixtures/scripts/script_in_pre.html +0 -5
  155. data/spec/html/proofer/fixtures/scripts/script_missing_internal.html +0 -9
  156. data/spec/html/proofer/fixtures/scripts/script_valid_internal.html +0 -9
  157. data/spec/html/proofer/fixtures/sorting/issue/broken_image_one.html +0 -1
  158. data/spec/html/proofer/fixtures/sorting/issue/broken_image_two.html +0 -4
  159. data/spec/html/proofer/fixtures/sorting/path/multiple_issues.html +0 -11
  160. data/spec/html/proofer/fixtures/sorting/path/single_issue.html +0 -1
  161. data/spec/html/proofer/fixtures/sorting/status/a_404.html +0 -1
  162. data/spec/html/proofer/fixtures/sorting/status/broken_link.html +0 -3
  163. data/spec/html/proofer/fixtures/sorting/status/missing_redirect.html +0 -1
  164. data/spec/html/proofer/fixtures/utils/lang-jp.html +0 -1
  165. data/spec/html/proofer/html_spec.rb +0 -57
  166. data/spec/html/proofer/images_spec.rb +0 -147
  167. data/spec/html/proofer/links_spec.rb +0 -364
  168. data/spec/html/proofer/scripts_spec.rb +0 -47
  169. data/spec/html/proofer/utils_spec.rb +0 -14
  170. data/spec/html/proofer_spec.rb +0 -122
  171. data/spec/spec_helper.rb +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e5d2a78c7071f5534b4fb0d22c34337d9b92a6c
4
- data.tar.gz: 4e89db1b52488116724d07608297d3abfb6581bb
3
+ metadata.gz: 1ce6c96e75683a8da4de60754d83aef138221d02
4
+ data.tar.gz: b341f1ee5b7237e58cba8e775dfaa152eaeeb0a5
5
5
  SHA512:
6
- metadata.gz: 84e99216d622692010d127c80f4cf4a128b5391535169418a9715d99f1d0ccdfcbe3edf976ee769ce2d5021b1989c6e232d00de72ae8574f82eb29b925a0e269
7
- data.tar.gz: 57e675836bb4a138dcfd84a0751772bbd99d05d3605dc8c1f922197683fe92a65563f0618393720a5ed3f44101025977f94f2dbf7cba96161def098148ad98ce
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