2023-10-24 21:07:46 -07:00
|
|
|
// @ts-check
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2023-10-25 20:05:19 -07:00
|
|
|
const { addErrorContext, nextLinesRe } = require("../helpers");
|
2024-08-24 22:05:16 -07:00
|
|
|
const { filterByPredicate, getTokenTextByType } = require("../helpers/micromark.cjs");
|
|
|
|
const { getReferenceLinkImageData, filterByTypesCached } = require("./cache");
|
2023-10-25 20:05:19 -07:00
|
|
|
|
|
|
|
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;
|
2023-10-24 21:07:46 -07:00
|
|
|
}
|
2023-10-25 20:05:19 -07:00
|
|
|
return !autolinkDisallowedRe.test(destination);
|
2023-10-24 21:07:46 -07:00
|
|
|
};
|
|
|
|
|
2024-02-27 20:42:09 -08:00
|
|
|
// eslint-disable-next-line jsdoc/valid-types
|
|
|
|
/** @type import("./markdownlint").Rule */
|
2023-10-24 21:07:46 -07:00
|
|
|
module.exports = {
|
|
|
|
"names": [ "MD054", "link-image-style" ],
|
|
|
|
"description": "Link and image style",
|
|
|
|
"tags": [ "images", "links" ],
|
2024-03-09 16:17:50 -08:00
|
|
|
"parser": "micromark",
|
2023-11-03 20:09:06 -07:00
|
|
|
"function": (params, onError) => {
|
2024-02-28 21:01:23 -08:00
|
|
|
const config = params.config;
|
2023-10-25 20:05:19 -07:00
|
|
|
const autolink = (config.autolink === undefined) || !!config.autolink;
|
|
|
|
const inline = (config.inline === undefined) || !!config.inline;
|
2023-11-11 22:12:50 -08:00
|
|
|
const full = (config.full === undefined) || !!config.full;
|
|
|
|
const collapsed = (config.collapsed === undefined) || !!config.collapsed;
|
|
|
|
const shortcut = (config.shortcut === undefined) || !!config.shortcut;
|
2023-11-12 22:42:02 -08:00
|
|
|
const urlInline = (config.url_inline === undefined) || !!config.url_inline;
|
|
|
|
if (autolink && inline && full && collapsed && shortcut && urlInline) {
|
2023-10-25 20:05:19 -07:00
|
|
|
// Everything allowed, nothing to check
|
|
|
|
return;
|
|
|
|
}
|
2024-08-24 22:05:16 -07:00
|
|
|
const { definitions } = getReferenceLinkImageData();
|
|
|
|
const links = filterByTypesCached([ "autolink", "image", "link" ]);
|
2023-10-24 21:07:46 -07:00
|
|
|
for (const link of links) {
|
2023-10-25 20:05:19 -07:00
|
|
|
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
|
2023-11-12 22:42:02 -08:00
|
|
|
const title = getTokenTextByType(descendents, "resourceTitleString");
|
2023-11-14 19:56:23 -08:00
|
|
|
isError = !inline || (
|
|
|
|
!urlInline &&
|
|
|
|
autolink &&
|
|
|
|
!image &&
|
|
|
|
!title &&
|
|
|
|
(label === destination) &&
|
|
|
|
autolinkAble(destination)
|
|
|
|
);
|
2023-10-25 20:05:19 -07:00
|
|
|
} else {
|
2023-11-11 22:12:50 -08:00
|
|
|
// link kind is a full/collapsed/shortcut reference link
|
|
|
|
const isShortcut = !children.some((t) => t.type === "reference");
|
|
|
|
const referenceString = getTokenTextByType(descendents, "referenceString");
|
|
|
|
const isCollapsed = (referenceString === null);
|
|
|
|
const definition = definitions.get(referenceString || label);
|
2023-10-25 20:05:19 -07:00
|
|
|
destination = definition && definition[1];
|
2023-11-11 22:12:50 -08:00
|
|
|
isError = destination &&
|
|
|
|
(isShortcut ? !shortcut : (isCollapsed ? !collapsed : !full));
|
2023-10-25 20:05:19 -07:00
|
|
|
}
|
2023-10-24 21:07:46 -07:00
|
|
|
}
|
2023-10-25 20:05:19 -07:00
|
|
|
if (isError) {
|
2024-06-21 21:03:30 -07:00
|
|
|
// eslint-disable-next-line no-undef-init
|
|
|
|
let range = undefined;
|
2023-10-25 20:05:19 -07:00
|
|
|
let fixInfo = null;
|
|
|
|
if (startLine === endLine) {
|
|
|
|
range = [ startColumn, endColumn - startColumn ];
|
|
|
|
let insertText = null;
|
2023-11-12 22:42:02 -08:00
|
|
|
const canInline = (inline && label);
|
|
|
|
const canAutolink = (autolink && !image && autolinkAble(destination));
|
|
|
|
if (canInline && (urlInline || !canAutolink)) {
|
2023-10-25 20:05:19 -07:00
|
|
|
// Most useful form
|
|
|
|
const prefix = (image ? "!" : "");
|
2023-11-12 22:42:02 -08:00
|
|
|
// @ts-ignore
|
2023-10-25 20:05:19 -07:00
|
|
|
const escapedLabel = label.replace(/[[\]]/g, "\\$&");
|
|
|
|
const escapedDestination = destination.replace(/[()]/g, "\\$&");
|
|
|
|
insertText = `${prefix}[${escapedLabel}](${escapedDestination})`;
|
2023-11-12 22:42:02 -08:00
|
|
|
} else if (canAutolink) {
|
2023-10-25 20:05:19 -07:00
|
|
|
// Simplest form
|
|
|
|
insertText = `<${removeBackslashEscapes(destination)}>`;
|
|
|
|
}
|
|
|
|
if (insertText) {
|
|
|
|
fixInfo = {
|
|
|
|
"editColumn": range[0],
|
|
|
|
insertText,
|
|
|
|
"deleteCount": range[1]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2023-10-24 21:07:46 -07:00
|
|
|
addErrorContext(
|
|
|
|
onError,
|
2023-10-25 20:05:19 -07:00
|
|
|
startLine,
|
|
|
|
text.replace(nextLinesRe, ""),
|
2024-06-21 21:03:30 -07:00
|
|
|
undefined,
|
|
|
|
undefined,
|
2023-10-24 21:07:46 -07:00
|
|
|
range,
|
|
|
|
fixInfo
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|