85 lines
4.1 KiB
JavaScript
85 lines
4.1 KiB
JavaScript
/**
|
|
* @fileoverview Rule to enforce reference-style links when URL matches a defined identifier.
|
|
* @author TKDev7
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Imports
|
|
//-----------------------------------------------------------------------------
|
|
import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* @import { Image, Link } from "mdast";
|
|
* @import { MarkdownRuleDefinition } from "../types.js";
|
|
* @typedef {"referenceLikeUrl"} NoReferenceLikeUrlsMessageIds
|
|
* @typedef {[]} NoReferenceLikeUrlsOptions
|
|
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoReferenceLikeUrlsOptions, MessageIds: NoReferenceLikeUrlsMessageIds }>} NoReferenceLikeUrlsRuleDefinition
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
/** Pattern to match both inline links: `[text](url)` and images: ``, with optional title */
|
|
const linkOrImagePattern = /\[(?<label>(?:\\.|[^()\\]|\([\s\S]*\))*?)\]\((?<destination>[ \t]*\r?\n?(?<![ \t])[ \t]*(?:<[^>]*>|[^ \t()]+))(?:[ \t]*\r?\n?(?<![ \t])[ \t]*(?:"[^"]*"|'[^']*'|\([^)]*\)))?[ \t]*\r?\n?(?<![ \t])[ \t]*\)$/u;
|
|
//-----------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//-----------------------------------------------------------------------------
|
|
export default /** @satisfies {NoReferenceLikeUrlsRuleDefinition} */ ({
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
recommended: true,
|
|
description: "Disallow URLs that match defined reference identifiers",
|
|
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-reference-like-urls.md",
|
|
},
|
|
fixable: "code",
|
|
messages: {
|
|
referenceLikeUrl: "Unexpected resource {{type}} ('{{prefix}}[text](url)') with URL that matches a definition identifier. Use '[text][id]' syntax instead.",
|
|
},
|
|
},
|
|
create(context) {
|
|
const { sourceCode } = context;
|
|
/** @type {Set<string>} */
|
|
const definitionIdentifiers = new Set();
|
|
/** @type {Array<Image | Link>} */
|
|
const relevantNodes = [];
|
|
return {
|
|
definition(node) {
|
|
definitionIdentifiers.add(node.identifier);
|
|
},
|
|
"image, link"(/** @type {Image | Link} */ node) {
|
|
relevantNodes.push(node);
|
|
},
|
|
"root:exit"() {
|
|
for (const node of relevantNodes) {
|
|
const text = sourceCode.getText(node);
|
|
const match = linkOrImagePattern.exec(text);
|
|
if (match !== null) {
|
|
const { label, destination } = match.groups;
|
|
const { type, title } = node;
|
|
const prefix = type === "image" ? "!" : "";
|
|
const url = normalizeIdentifier(destination).toLowerCase();
|
|
if (definitionIdentifiers.has(url)) {
|
|
context.report({
|
|
loc: node.position,
|
|
messageId: "referenceLikeUrl",
|
|
data: {
|
|
type,
|
|
prefix,
|
|
},
|
|
fix(fixer) {
|
|
// The AST treats both missing and empty titles as null, so it's safe to auto-fix in both cases.
|
|
if (title) {
|
|
return null;
|
|
}
|
|
return fixer.replaceText(node, `${prefix}[${label}][${destination}]`);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|