/** * @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>} */ 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); } }, }; }, });