Reimplement MD042/no-empty-links using micromark tokens.

This commit is contained in:
David Anson 2024-06-29 16:45:40 -07:00
parent 7377b75383
commit 964e4c80ab
6 changed files with 219 additions and 97 deletions

View file

@ -2,8 +2,9 @@
"use strict";
const { addErrorContext, escapeForRegExp, filterTokens } =
require("../helpers");
const { addErrorContext } = require("../helpers");
const { getDescendantsByType, filterByTypes } = require("../helpers/micromark.cjs");
const { referenceLinkImageData } = require("./cache");
// eslint-disable-next-line jsdoc/valid-types
/** @type import("./markdownlint").Rule */
@ -11,43 +12,55 @@ module.exports = {
"names": [ "MD042", "no-empty-links" ],
"description": "No empty links",
"tags": [ "links" ],
"parser": "markdownit",
"parser": "micromark",
"function": function MD042(params, onError) {
filterTokens(params, "inline", function forToken(token) {
let inLink = false;
let linkText = "";
let emptyLink = false;
for (const child of token.children) {
if (child.type === "link_open") {
inLink = true;
linkText = "";
for (const attr of child.attrs) {
if (attr[0] === "href" && (!attr[1] || (attr[1] === "#"))) {
emptyLink = true;
}
}
} else if (child.type === "link_close") {
inLink = false;
if (emptyLink) {
let context = `[${linkText}]`;
let range = null;
const match = child.line.match(
new RegExp(`${escapeForRegExp(context)}\\((?:|#|<>)\\)`)
);
if (match) {
context = match[0];
// @ts-ignore
range = [ match.index + 1, match[0].length ];
}
addErrorContext(
onError, child.lineNumber, context, null, null, range
);
emptyLink = false;
}
} else if (inLink) {
linkText += child.content;
}
const { definitions } = referenceLinkImageData();
const isReferenceDefinitionHash = (token) => {
const definition = definitions.get(token.text.trim());
return (definition && (definition[1] === "#"));
};
const links = filterByTypes(
params.parsers.micromark.tokens,
[ "link" ]
);
for (const link of links) {
const labelText = getDescendantsByType(link, [ "label", "labelText" ]);
const reference = getDescendantsByType(link, [ "reference" ]);
const resource = getDescendantsByType(link, [ "resource" ]);
const referenceString = getDescendantsByType(reference, [ "referenceString" ]);
const resourceDestination = getDescendantsByType(resource, [ "resourceDestination" ]);
const resourceDestinationString = [
...getDescendantsByType(resourceDestination, [ "resourceDestinationRaw", "resourceDestinationString" ]),
...getDescendantsByType(resourceDestination, [ "resourceDestinationLiteral", "resourceDestinationString" ])
];
const hasLabelText = labelText.length > 0;
const hasReference = reference.length > 0;
const hasResource = resource.length > 0;
const hasReferenceString = referenceString.length > 0;
const hasResourceDestinationString = resourceDestinationString.length > 0;
let error = false;
if (
hasLabelText &&
((!hasReference && !hasResource) || (hasReference && !hasReferenceString))
) {
error = isReferenceDefinitionHash(labelText[0]);
} else if (hasReferenceString && !hasResourceDestinationString) {
error = isReferenceDefinitionHash(referenceString[0]);
} else if (!hasReferenceString && hasResourceDestinationString) {
error = (resourceDestinationString[0].text.trim() === "#");
} else if (!hasReferenceString && !hasResourceDestinationString) {
error = true;
}
});
if (error) {
addErrorContext(
onError,
link.startLine,
link.text,
undefined,
undefined,
[ link.startColumn, link.endColumn - link.startColumn ]
);
}
}
}
};