102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
/**
|
|
* @fileoverview Rule to prevent duplicate headings in Markdown.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
/**
|
|
* @import { MarkdownRuleDefinition } from "../types.js";
|
|
* @typedef {"duplicateHeading"} NoDuplicateHeadingsMessageIds
|
|
* @typedef {[{ checkSiblingsOnly?: boolean }]} NoDuplicateHeadingsOptions
|
|
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateHeadingsOptions, MessageIds: NoDuplicateHeadingsMessageIds }>} NoDuplicateHeadingsRuleDefinition
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//-----------------------------------------------------------------------------
|
|
export default /** @satisfies {NoDuplicateHeadingsRuleDefinition} */ ({
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "Disallow duplicate headings in the same document",
|
|
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-headings.md",
|
|
},
|
|
messages: {
|
|
duplicateHeading: 'Duplicate heading "{{text}}" found.',
|
|
},
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
checkSiblingsOnly: {
|
|
type: "boolean",
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
defaultOptions: [{ checkSiblingsOnly: false }],
|
|
},
|
|
create(context) {
|
|
const [{ checkSiblingsOnly }] = context.options;
|
|
/** @type {Map<number, Set<string>>} */
|
|
const headingsByLevel = checkSiblingsOnly
|
|
? new Map([
|
|
[1, new Set()],
|
|
[2, new Set()],
|
|
[3, new Set()],
|
|
[4, new Set()],
|
|
[5, new Set()],
|
|
[6, new Set()],
|
|
])
|
|
: new Map([[1, new Set()]]);
|
|
let lastLevel = 1;
|
|
let currentLevelHeadings = headingsByLevel.get(lastLevel);
|
|
/** @type {string} */
|
|
let headingChildrenSequence;
|
|
/** @type {string} */
|
|
let headingText;
|
|
return {
|
|
heading(node) {
|
|
headingChildrenSequence = "";
|
|
headingText = "";
|
|
if (checkSiblingsOnly) {
|
|
const currentLevel = node.depth;
|
|
if (currentLevel < lastLevel) {
|
|
for (let level = lastLevel; level > currentLevel; level--) {
|
|
headingsByLevel.get(level).clear();
|
|
}
|
|
}
|
|
lastLevel = currentLevel;
|
|
currentLevelHeadings = headingsByLevel.get(currentLevel);
|
|
}
|
|
},
|
|
"heading *"({ type, value }) {
|
|
if (value) {
|
|
headingChildrenSequence += `[${type},${value}]`; // We use a custom sequence representation to keep track of heading children.
|
|
if (type !== "html") {
|
|
headingText += value;
|
|
}
|
|
}
|
|
else {
|
|
headingChildrenSequence += `[${type}]`;
|
|
}
|
|
},
|
|
"heading:exit"(node) {
|
|
if (currentLevelHeadings.has(headingChildrenSequence)) {
|
|
context.report({
|
|
loc: node.position,
|
|
messageId: "duplicateHeading",
|
|
data: {
|
|
text: headingText,
|
|
},
|
|
});
|
|
}
|
|
else {
|
|
currentLevelHeadings.add(headingChildrenSequence);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|