Class: Mdlint::Linter::Rules::LinkCheck
- Inherits:
-
Mdlint::Linter::Rule
- Object
- Mdlint::Linter::Rule
- Mdlint::Linter::Rules::LinkCheck
- Defined in:
- lib/mdlint/linter/rules/link_check.rb,
sig/internal.rbs
Instance Attribute Summary
Attributes inherited from Mdlint::Linter::Rule
Instance Method Summary collapse
- #add_missing_target(path, token) ⇒ Object
- #check(tokens, source) ⇒ Object
- #check_external_target(target, token) ⇒ Object
- #check_fragment(fragment, token, image, headings) ⇒ Object
- #check_target(target, token, image, source) ⇒ Object
- #heading_slugs(tokens) ⇒ Object
- #non_markdown_file?(path) ⇒ Boolean
- #slugify(value) ⇒ String
Methods inherited from Mdlint::Linter::Rule
#add_violation, #fix, inherited, #initialize
Constructor Details
This class inherits a constructor from Mdlint::Linter::Rule
Instance Method Details
#add_missing_target(path, token) ⇒ Object
83 84 85 86 87 88 89 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 83 def add_missing_target(path, token) add_violation( message: "Link target does not exist: #{path}", line: (token.map&.first || 0) + 1, fixable: false ) end |
#check(tokens, source) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 15 def check(tokens, source) return @violations unless @options[:check_links] || @options[:check_external_links] @headings = heading_slugs(tokens) tokens.each do |token| next unless token.type == :inline token.children.each do |child| next unless child.type == :link_open || child.type == :image target = child.attrs[:href] || child.attrs[:src] image = child.is_a?(Token) && child.type == :image check_target(target, token, image, source) end end @violations end |
#check_external_target(target, token) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 91 def check_external_target(target, token) uri = URI.parse(target) host = uri.host.to_s request_path = uri.path.to_s request_path = "/" if request_path.empty? request_path += "?#{uri.query}" if uri.query response = Net::HTTP.start( host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 3, read_timeout: 3 ) do |http| result = http.head(request_path) result.is_a?(Net::HTTPMethodNotAllowed) ? http.get(request_path) : result end return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection) add_violation( message: "External link returned HTTP #{response.code}: #{target}", line: (token.map&.first || 0) + 1, fixable: false ) rescue URI::InvalidURIError, SocketError, SystemCallError, Timeout::Error, IOError => error add_violation( message: "External link could not be reached: #{target} (#{error.class})", line: (token.map&.first || 0) + 1, fixable: false ) end |
#check_fragment(fragment, token, image, headings) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 71 def check_fragment(fragment, token, image, headings) return if image || fragment.nil? || fragment.empty? decoded = URI::DEFAULT_PARSER.unescape(fragment).downcase return if headings.include?(decoded) add_violation( message: "Link anchor does not exist: ##{fragment}", line: (token.map&.first || 0) + 1, fixable: false ) end |
#check_target(target, token, image, source) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 36 def check_target(target, token, image, source) return if target.nil? || target.empty? if target.match?(%r{\Ahttps?://}i) check_external_target(target, token) if @options[:check_external_links] return end return if target.match?(%r{\A(?:ftp|mailto):}i) path, fragment = target.split("#", 2) base = @options[:filename] && File.dirname(@options[:filename]) return unless base || path.empty? if path.empty? check_fragment(fragment, token, image, @headings) return end resolved_path = File.(path, base) unless File.file?(resolved_path) add_missing_target(path, token) return end return if image || fragment.nil? || fragment.empty? return if non_markdown_file?(resolved_path) target_headings = begin heading_slugs(Parser.parse(File.read(resolved_path), @options)) rescue StandardError [] end check_fragment(fragment, token, image, target_headings) end |
#heading_slugs(tokens) ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 126 def heading_slugs(tokens) counts = Hash.new(0) slugs = [] tokens.each_with_index do |token, index| next unless token.type == :heading_open inline = tokens[(index + 1)..]&.find { |candidate| candidate.type == :inline } base = slugify(inline&.content.to_s) next if base.empty? suffix = counts[base] counts[base] += 1 slugs << (suffix.zero? ? base : "#{base}-#{suffix}") end slugs end |
#non_markdown_file?(path) ⇒ Boolean
122 123 124 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 122 def non_markdown_file?(path) !%w[.md .markdown .mdown .mkdn].include?(File.extname(path).downcase) end |
#slugify(value) ⇒ String
143 144 145 146 147 |
# File 'lib/mdlint/linter/rules/link_check.rb', line 143 def slugify(value) value = value.gsub(/[`*_~\[\]()<>]/, "") value.downcase.gsub(/[^\p{Alnum}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\s-]/, "") .strip.gsub(/\s+/, "-") end |