first commit
This commit is contained in:
+147
@@ -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);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user