118 lines
4.7 KiB
JavaScript
118 lines
4.7 KiB
JavaScript
/**
|
|
* @fileoverview Rule to prevent non-complaint link references.
|
|
* @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 {"invalidLabelRef"} NoInvalidLabelRefsMessageIds
|
|
* @typedef {[]} NoInvalidLabelRefsOptions
|
|
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoInvalidLabelRefsOptions, MessageIds: NoInvalidLabelRefsMessageIds }>} NoInvalidLabelRefsRuleDefinition
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
/** matches i.e., `[foo][bar]` */
|
|
const labelPattern = /\]\[([^\]]+)\]/u;
|
|
/**
|
|
* 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 findInvalidLabelReferences(node, sourceCode) {
|
|
const nodeText = sourceCode.getText(node);
|
|
const docText = sourceCode.text;
|
|
const invalid = [];
|
|
let startIndex = 0;
|
|
/*
|
|
* This loop works by searching the string inside the node for the next
|
|
* label reference. If it finds one, it checks to see if there is any
|
|
* white space between the [ and ]. If there is, it reports an error.
|
|
* It then moves the start index to the end of the label reference and
|
|
* continues searching the text until the end of the text is found.
|
|
*/
|
|
while (startIndex < nodeText.length) {
|
|
const value = nodeText.slice(startIndex);
|
|
const match = value.match(labelPattern);
|
|
if (!match) {
|
|
break;
|
|
}
|
|
if (!illegalShorthandTailPattern.test(match[0])) {
|
|
startIndex += match.index + match[0].length;
|
|
continue;
|
|
}
|
|
/*
|
|
* Adjust `labelPattern` match index to the full source code.
|
|
*/
|
|
const startOffset = startIndex + match.index + node.position.start.offset;
|
|
const endOffset = startOffset + match[0].length;
|
|
/*
|
|
* Search the entire document text to find the preceding open bracket.
|
|
*/
|
|
const lastOpenBracketIndex = docText.lastIndexOf("[", startOffset);
|
|
if (lastOpenBracketIndex === -1) {
|
|
startIndex += match.index + match[0].length;
|
|
continue;
|
|
}
|
|
/*
|
|
* Note: `label` can contain leading and trailing newlines, so we need to
|
|
* take that into account when calculating the line and column offsets.
|
|
*/
|
|
const label = docText
|
|
.slice(lastOpenBracketIndex, endOffset)
|
|
.match(/!?\[([^\]]+)\]/u)[1];
|
|
invalid.push({
|
|
label: label.trim(),
|
|
position: {
|
|
start: sourceCode.getLocFromIndex(startOffset + 1),
|
|
end: sourceCode.getLocFromIndex(endOffset),
|
|
},
|
|
});
|
|
startIndex += match.index + match[0].length;
|
|
}
|
|
return invalid;
|
|
}
|
|
//-----------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//-----------------------------------------------------------------------------
|
|
export default /** @satisfies {NoInvalidLabelRefsRuleDefinition} */ ({
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
recommended: true,
|
|
description: "Disallow invalid label references",
|
|
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-invalid-label-refs.md",
|
|
},
|
|
messages: {
|
|
invalidLabelRef: "Label reference '{{label}}' is invalid due to white space between [ and ].",
|
|
},
|
|
},
|
|
create(context) {
|
|
const { sourceCode } = context;
|
|
return {
|
|
text(node) {
|
|
const invalidReferences = findInvalidLabelReferences(node, sourceCode);
|
|
for (const invalidReference of invalidReferences) {
|
|
context.report({
|
|
loc: invalidReference.position,
|
|
messageId: "invalidLabelRef",
|
|
data: {
|
|
label: invalidReference.label,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|