Update previous commit for MD051/link-fragments to rename, refactor, add support for HTML anchors, and validate against

markdown-link-check (fixes #253).
This commit is contained in:
David Anson 2022-04-10 05:37:57 +00:00
parent 33ee1cd85e
commit db5d9f6dbb
21 changed files with 355 additions and 181 deletions

View file

@ -2,45 +2,74 @@
"use strict";
const { addError, forEachHeading, filterTokens } = require("../helpers");
const { addError, escapeForRegExp, filterTokens, forEachLine, forEachHeading,
htmlElementRe, overlapsAnyRange } = require("../helpers");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
// Regular expression for identifying HTML anchor names
const identifierRe = /(?:id|name)\s*=\s*['"]?([^'"\s>]+)/iu;
/**
* Converts a Markdown heading into an HTML fragment
* according to the rules used by GitHub.
* Converts a Markdown heading into an HTML fragment according to the rules
* used by GitHub.
*
* @param {string} string The string to convert.
* @returns {string} The converted string.
* @param {Object} inline Inline token for heading.
* @returns {string} Fragment string for heading.
*/
function convertHeadingToHTMLFragment(string) {
return "#" + string
function convertHeadingToHTMLFragment(inline) {
const inlineText = inline.children.map((token) => token.content).join("");
return "#" + inlineText
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^-_a-z0-9]/g, "");
}
module.exports = {
"names": [ "MD051", "valid-link-fragments" ],
"names": [ "MD051", "link-fragments" ],
"description": "Link fragments should be valid",
"tags": [ "links" ],
"function": function MD051(params, onError) {
const validLinkFragments = [];
forEachHeading(params, (_heading, content) => {
validLinkFragments.push(convertHeadingToHTMLFragment(content));
const fragments = new Set();
forEachHeading(params, (heading, content, inline) => {
fragments.add(convertHeadingToHTMLFragment(inline));
});
filterTokens(params, "inline", (token) => {
token.children.forEach((child) => {
const { lineNumber, type, attrs } = child;
if (type === "link_open") {
const href = attrs.find((attr) => attr[0] === "href");
if (href !== undefined &&
href[1].startsWith("#") &&
!validLinkFragments.includes(href[1])
) {
const detail = "Link Fragment is invalid";
addError(onError, lineNumber, detail, href[1]);
const exclusions = codeBlockAndSpanRanges();
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
let match = null;
// eslint-disable-next-line no-unmodified-loop-condition
while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
const [ tag, , element ] = match;
if (
(element.toLowerCase() === "a") &&
!overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length)
) {
const idMatch = identifierRe.exec(tag);
if (idMatch) {
fragments.add(`#${idMatch[1]}`);
}
}
});
}
});
filterTokens(params, "inline", (token) => {
for (const child of token.children) {
const { attrs, lineNumber, line, type } = child;
if (type === "link_open") {
const href = attrs.find((attr) => attr[0] === "href");
const id = href && href[1];
if (id && (id.length > 1) && (id[0] === "#") && !fragments.has(id)) {
let context = id;
let range = null;
const match = line.match(
new RegExp(`\\[.*?\\]\\(${escapeForRegExp(context)}\\)`)
);
if (match) {
context = match[0];
range = [ match.index + 1, match[0].length ];
}
addError(onError, lineNumber, null, context, range);
}
}
}
});
}
};