UNPKG

remark-lint-no-duplicate-headings-in-section

Version:

remark-lint rule to warn on duplicate headings in a section

186 lines (174 loc) 4.57 kB
/** * remark-lint rule to warn when the same text is used in multiple headings * in a section. * * ## What is this? * * This package checks that headings are unique in sections. * * ## When should I use this? * * You can use this package to check that headings are unique. * * ## API * * ### `unified().use(remarkLintNoDuplicateHeadingsInSection)` * * Warn when the same text is used in multiple headings in a section. * * ###### Parameters * * There are no options. * * ###### Returns * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * * ## Recommendation * * It’s likely a mistake that the same heading text is used in the same * section. * * [api-remark-lint-no-duplicate-headings-in-section]: #unifieduseremarklintnoduplicateheadingsinsection * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * * @module no-duplicate-headings-in-section * @author Titus Wormer * @copyright Titus Wormer * @license MIT * @example * {"name": "ok.md"} * * # Planets * * ## Venus * * ### Discovery * * ## Mars * * ### Discovery * * ### Phobos * * #### Discovery * * @example * {"label": "input", "name": "not-ok.md"} * * # Planets * * ## Mars * * ### Discovery * * ### Discovery * * @example * {"label": "output", "name": "not-ok.md"} * * 7:1-7:14: Unexpected heading with equivalent text in section, expected unique headings * * @example * {"label": "input", "name": "tolerant-heading-increment.md"} * * # Planets * * #### Discovery * * ###### Phobos * * #### Discovery * * ###### Deimos * * @example * {"label": "output", "name": "tolerant-heading-increment.md"} * * 7:1-7:15: Unexpected heading with equivalent text in section, expected unique headings * * @example * {"label": "input", "mdx": true, "name": "mdx.mdx"} * * MDX is supported <em>too</em>. * * <h1>Planets</h1> * <h2>Mars</h2> * <h3>Discovery</h3> * <h3>Discovery</h3> * * @example * {"label": "output", "mdx": true, "name": "mdx.mdx"} * * 6:1-6:19: Unexpected heading with equivalent text in section, expected unique headings */ /** * @import {Heading, Nodes, Root} from 'mdast' * @import {} from 'mdast-util-mdx' */ import {ok as assert} from 'devlop' import {toString} from 'mdast-util-to-string' import {lintRule} from 'unified-lint-rule' import {visitParents} from 'unist-util-visit-parents' import {VFileMessage} from 'vfile-message' const jsxNameRe = /^h([1-6])$/ const remarkLintNoDuplicateHeadingsInSection = lintRule( { origin: 'remark-lint:no-duplicate-headings-in-section', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-duplicate-headings-in-section#readme' }, /** * @param {Root} tree * Tree. * @returns {undefined} * Nothing. */ function (tree, file) { /** @type {Array<Map<string, Array<Nodes>>>} */ const stack = [] visitParents(tree, function (node, parents) { /** @type {Heading['depth'] | undefined} */ let rank if (node.type === 'heading') { rank = node.depth } else if ( (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && node.name ) { const match = node.name.match(jsxNameRe) rank = match ? /** @type {Heading['depth']} */ (Number(match[1])) : undefined } if (rank) { const ancestors = [...parents, node] const value = toString(node).toLowerCase() const index = rank - 1 const map = stack[index] || (stack[index] = new Map()) const duplicateAncestors = map.get(value) if (node.position && duplicateAncestors) { const duplicate = duplicateAncestors.at(-1) assert(duplicate) // Always defined. file.message( 'Unexpected heading with equivalent text in section, expected unique headings', { ancestors, cause: new VFileMessage('Equivalent heading text defined here', { ancestors: duplicateAncestors, place: duplicate.position, source: 'remark-lint', ruleId: 'no-duplicate-headings-in-section' }), place: node.position } ) } map.set(value, ancestors) // Drop things after it. stack.length = rank } }) } ) export default remarkLintNoDuplicateHeadingsInSection