Refactor new MD054/link-image-style rule, add tests, update documentation (fixes #40, fixes #399).

This commit is contained in:
David Anson 2023-10-25 20:05:19 -07:00
parent 460836445c
commit 10f095c4fd
32 changed files with 3149 additions and 333 deletions

View file

@ -2,92 +2,23 @@
"use strict";
const { addErrorContext } = require("../helpers");
const { addErrorContext, nextLinesRe } = require("../helpers");
const { filterByTypes, filterByPredicate, getTokenTextByType } =
require("../helpers/micromark.cjs");
const { referenceLinkImageData } = require("./cache");
const isInlineLink = ({ children }) => children.some(
({ type }) => type === "resource"
);
const isAutolink = ({ type }) => type === "autolink";
const getNestedTokenTextByType = (tokens, type) => getTokenTextByType(
filterByTypes(tokens, [ type ]),
type
);
const escapeParentheses = (unescaped) => unescaped
.replaceAll("(", "\\(")
.replaceAll(")", "\\)");
const escapeSquares = (unescaped) => unescaped
.replaceAll("[", "\\[")
.replaceAll("]", "\\]");
const escapeAngles = (unescaped) => unescaped
.replaceAll("<", "\\<")
.replaceAll(">", "\\>");
const unescapeParentheses = (escaped) => escaped
.replaceAll("\\(", "(")
.replaceAll("\\)", ")");
const unescapeAngles = (escaped) => escaped
.replaceAll("\\<", "<")
.replaceAll("\\>", ">");
const referenceLinkDestination = (link, tokens) => {
const reference = getNestedTokenTextByType([ link ], "reference");
const id = reference && reference !== "[]" ?
reference.replace(/^\[/, "").replace(/\]$/, "") :
getNestedTokenTextByType([ link ], "labelText");
const definition = filterByPredicate(
filterByTypes(tokens, [ "definition" ]),
(d) => getNestedTokenTextByType([ d ], "definitionLabelString") === id
);
return getNestedTokenTextByType(definition, "definitionDestination");
};
const inlineLinkDestination = (link) => {
const text = getNestedTokenTextByType([ link ], "resourceDestination");
return text && unescapeParentheses(text);
};
const autolinkDestination = (link) => {
const text = getNestedTokenTextByType([ link ], "autolinkProtocol");
return text && unescapeAngles(text);
};
const autolinkFixInfo = (tokens, link) => {
if (isAutolink(link)) {
return null;
const backslashEscapeRe = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g;
const removeBackslashEscapes = (text) => text.replace(backslashEscapeRe, "$1");
const autolinkDisallowedRe = /[ <>]/;
const autolinkAble = (destination) => {
try {
// eslint-disable-next-line no-new
new URL(destination);
} catch {
// Not an absolute URL
return false;
}
const destination = isInlineLink(link) ?
inlineLinkDestination(link) :
referenceLinkDestination(link, tokens);
return {
"editColumn": link.startColumn,
"insertText": `<${escapeAngles(destination)}>`,
"deleteCount": link.endColumn - link.startColumn
};
};
const inlineFixInfo = (tokens, link) => {
if (isInlineLink(link)) {
return null;
}
const destination = isAutolink(link) ?
autolinkDestination(link) :
referenceLinkDestination(link, tokens);
return {
"editColumn": link.startColumn,
"insertText":
`[${escapeSquares(destination)}](${escapeParentheses(destination)})`,
"deleteCount": link.endColumn - link.startColumn
};
return !autolinkDisallowedRe.test(destination);
};
module.exports = {
@ -95,34 +26,77 @@ module.exports = {
"description": "Link and image style",
"tags": [ "images", "links" ],
"function": ({ parsers, config }, onError) => {
const style = String(config.style || "mixed");
const autolink = (config.autolink === undefined) || !!config.autolink;
const inline = (config.inline === undefined) || !!config.inline;
const reference = (config.reference === undefined) || !!config.reference;
if (autolink && inline && reference) {
// Everything allowed, nothing to check
return;
}
const { definitions } = referenceLinkImageData();
const links = filterByTypes(
parsers.micromark.tokens,
[ "autolink", "link", "image" ]
[ "autolink", "image", "link" ]
);
for (const link of links) {
const inlineLink = isInlineLink(link);
const autolink = isAutolink(link);
const range = [ link.startColumn, link.endColumn - link.startColumn ];
let fixInfo = null;
if (style === "autolink_only") {
fixInfo = autolinkFixInfo(parsers.micromark.tokens, link);
} else if (style === "inline_only") {
fixInfo = inlineFixInfo(parsers.micromark.tokens, link);
let label = null;
let destination = null;
const {
children, endColumn, endLine, startColumn, startLine, text, type
} = link;
const image = (type === "image");
let isError = false;
if (type === "autolink") {
// link kind is an autolink
destination = getTokenTextByType(children, "autolinkProtocol");
label = destination;
isError = !autolink;
} else {
// link type is "image" or "link"
const descendents = filterByPredicate(children);
label = getTokenTextByType(descendents, "labelText");
destination =
getTokenTextByType(descendents, "resourceDestinationString");
if (destination) {
// link kind is an inline link
isError = !inline;
} else {
// link kind is a reference link
const referenceLabel =
getTokenTextByType(descendents, "referenceString") || label;
const definition = definitions.get(referenceLabel);
destination = definition && definition[1];
isError = !reference && destination;
}
}
if (
fixInfo ||
(style === "reference_only" && (inlineLink || autolink)) ||
(style === "inline_or_reference" && autolink) ||
(style === "inline_or_autolink" && !(inlineLink || autolink)) ||
(style === "reference_or_autolink" && inlineLink)
) {
if (isError) {
let range = null;
let fixInfo = null;
if (startLine === endLine) {
range = [ startColumn, endColumn - startColumn ];
let insertText = null;
if (inline && label) {
// Most useful form
const prefix = (image ? "!" : "");
const escapedLabel = label.replace(/[[\]]/g, "\\$&");
const escapedDestination = destination.replace(/[()]/g, "\\$&");
insertText = `${prefix}[${escapedLabel}](${escapedDestination})`;
} else if (autolink && !image && autolinkAble(destination)) {
// Simplest form
insertText = `<${removeBackslashEscapes(destination)}>`;
}
if (insertText) {
fixInfo = {
"editColumn": range[0],
insertText,
"deleteCount": range[1]
};
}
}
addErrorContext(
onError,
link.startLine,
link.text,
startLine,
text.replace(nextLinesRe, ""),
null,
null,
range,