first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
@@ -0,0 +1,147 @@
/**
* @fileoverview Rule to prevent missing label references in Markdown.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { illegalShorthandTailPattern } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { Position } from "unist";
* @import { Text } from "mdast";
* @import { MarkdownRuleDefinition } from "../types.js";
* @import { MarkdownSourceCode } from "../language/markdown-source-code.js";
* @typedef {"notFound"} NoMissingLabelRefsMessageIds
* @typedef {[{ allowLabels?: string[] }]} NoMissingLabelRefsOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingLabelRefsOptions, MessageIds: NoMissingLabelRefsMessageIds }>} NoMissingLabelRefsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Finds missing references in a node.
* @param {Text} node The node to check.
* @param {MarkdownSourceCode} sourceCode The Markdown source code object.
* @returns {Array<{label:string,position:Position}>} The missing references.
*/
function findMissingReferences(node, sourceCode) {
/** @type {Array<{label:string,position:Position}>} */
const missing = [];
const nodeText = sourceCode.getText(node);
/**
* Matches substrings like `"[foo]"`, `"[]"`, `"[foo][bar]"`, `"[foo][]"`, `"[][bar]"`, or `"[][]"`.
* `left` is the content between the first brackets. It can be empty.
* `right` is the content between the second brackets. It can be empty, and it can be undefined.
*/
const labelPattern = /(?<=(?<!\\)(?:\\{2})*)\[(?<left>(?:\\.|[^[\]\\])*)\](?:\[(?<right>(?:\\.|[^\]\\])*)\])?/dgu;
/** @type {RegExpExecArray | null} */
let match;
/*
* This loop searches the text inside the node for sequences that
* look like label references and reports an error for each one found.
*/
while ((match = labelPattern.exec(nodeText))) {
// skip illegal shorthand tail -- handled by no-invalid-label-refs
if (illegalShorthandTailPattern.test(match[0])) {
continue;
}
const { left, right } = match.groups;
// `[][]` or `[]`
if (!left && !right) {
continue;
}
let label, labelIndices;
if (right) {
label = right;
labelIndices = match.indices.groups.right;
}
else {
label = left;
labelIndices = match.indices.groups.left;
}
const startOffset = labelIndices[0] + node.position.start.offset;
const endOffset = labelIndices[1] + node.position.start.offset;
missing.push({
label: label.trim(),
position: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset),
},
});
}
return missing;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMissingLabelRefsRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow missing label references",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-label-refs.md",
},
schema: [
{
type: "object",
properties: {
allowLabels: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
allowLabels: [],
},
],
messages: {
notFound: "Label reference '{{label}}' not found.",
},
},
create(context) {
const { sourceCode } = context;
const allowLabels = new Set(context.options[0].allowLabels);
/** @type {Array<{label:string,position:Position}>} */
let allMissingReferences = [];
return {
"root:exit"() {
for (const missingReference of allMissingReferences) {
context.report({
loc: missingReference.position,
messageId: "notFound",
data: {
label: missingReference.label,
},
});
}
},
text(node) {
const missingReferences = findMissingReferences(node, sourceCode);
for (const missingReference of missingReferences) {
if (!allowLabels.has(missingReference.label)) {
allMissingReferences.push(missingReference);
}
}
},
definition(node) {
/*
* Sometimes a poorly-formatted link will end up a text node instead of a link node
* even though the label definition exists. Here, we remove any missing references
* that have a matching label definition.
*/
allMissingReferences = allMissingReferences.filter(missingReference => missingReference.label !== node.identifier);
},
};
},
});