88//-----------------------------------------------------------------------------
99
1010/**
11- * @import { Heading } from "mdast";
1211 * @import { MarkdownRuleDefinition } from "../types.js";
1312 * @typedef {"duplicateHeading" } NoDuplicateHeadingsMessageIds
1413 * @typedef {[{ checkSiblingsOnly?: boolean }] } NoDuplicateHeadingsOptions
1514 * @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateHeadingsOptions, MessageIds: NoDuplicateHeadingsMessageIds }> } NoDuplicateHeadingsRuleDefinition
1615 */
1716
18- //-----------------------------------------------------------------------------
19- // Helpers
20- //-----------------------------------------------------------------------------
21-
22- /**
23- * This pattern does not match backslash-escaped `#` characters
24- * @example
25- * ```markdown
26- * <!-- OK -->
27- * ### foo ###
28- * ## foo ###
29- * # foo #
30- *
31- * <!-- NOT OK -->
32- * ### foo \###
33- * ## foo #\##
34- * # foo \#
35- * ```
36- *
37- * @see https://spec.commonmark.org/0.31.2/#example-76
38- */
39- const trailingAtxHeadingHashPattern = / (?< ! [ \t ] ) [ \t ] + # + [ \t ] * $ / u;
40- const leadingAtxHeadingHashPattern = / ^ # { 1 , 6 } [ \t ] + / u;
41-
4217//-----------------------------------------------------------------------------
4318// Rule Definition
4419//-----------------------------------------------------------------------------
@@ -74,7 +49,6 @@ export default {
7449
7550 create ( context ) {
7651 const [ { checkSiblingsOnly } ] = context . options ;
77- const { sourceCode } = context ;
7852
7953 /** @type {Map<number, Set<string>> } */
8054 const headingsByLevel = checkSiblingsOnly
@@ -89,46 +63,15 @@ export default {
8963 : new Map ( [ [ 1 , new Set ( ) ] ] ) ;
9064 let lastLevel = 1 ;
9165 let currentLevelHeadings = headingsByLevel . get ( lastLevel ) ;
92-
93- /**
94- * Gets the text of a heading node
95- * @param {Heading } node The heading node
96- * @returns {string } The heading text
97- */
98- function getHeadingText ( node ) {
99- /*
100- * There are two types of headings in markdown:
101- * - ATX headings, which consist of 1-6 # characters followed by content
102- * and optionally ending with any number of # characters
103- * - Setext headings, which are underlined with = or -
104- * Setext headings are identified by being on two lines instead of one,
105- * with the second line containing only = or - characters. In order to
106- * get the correct heading text, we need to determine which type of
107- * heading we're dealing with.
108- */
109- const isSetext =
110- node . position . start . line !== node . position . end . line ;
111-
112- if ( isSetext ) {
113- // get only the text from the first line
114- return sourceCode . lines [ node . position . start . line - 1 ] . trim ( ) ;
115- }
116-
117- // For ATX headings, get the text between the # characters
118- const text = sourceCode . getText ( node ) ;
119-
120- /*
121- * Please avoid using `String.prototype.trim()` here,
122- * as it would remove intentional non-breaking space (NBSP) characters.
123- */
124- return text
125- . replace ( leadingAtxHeadingHashPattern , "" ) // Remove leading # characters
126- . replace ( trailingAtxHeadingHashPattern , "" ) ; // Remove trailing # characters
127- }
66+ /** @type {string } */
67+ let headingChildrenSequence ;
68+ /** @type {string } */
69+ let headingText ;
12870
12971 return {
13072 heading ( node ) {
131- const headingText = getHeadingText ( node ) ;
73+ headingChildrenSequence = "" ;
74+ headingText = "" ;
13275
13376 if ( checkSiblingsOnly ) {
13477 const currentLevel = node . depth ;
@@ -146,8 +89,22 @@ export default {
14689 lastLevel = currentLevel ;
14790 currentLevelHeadings = headingsByLevel . get ( currentLevel ) ;
14891 }
92+ } ,
93+
94+ "heading *" ( { type, value } ) {
95+ if ( value ) {
96+ headingChildrenSequence += `[${ type } ,${ value } ]` ; // We use a custom sequence representation to keep track of heading children.
97+
98+ if ( type !== "html" ) {
99+ headingText += value ;
100+ }
101+ } else {
102+ headingChildrenSequence += `[${ type } ]` ;
103+ }
104+ } ,
149105
150- if ( currentLevelHeadings . has ( headingText ) ) {
106+ "heading:exit" ( node ) {
107+ if ( currentLevelHeadings . has ( headingChildrenSequence ) ) {
151108 context . report ( {
152109 loc : node . position ,
153110 messageId : "duplicateHeading" ,
@@ -156,7 +113,7 @@ export default {
156113 } ,
157114 } ) ;
158115 } else {
159- currentLevelHeadings . add ( headingText ) ;
116+ currentLevelHeadings . add ( headingChildrenSequence ) ;
160117 }
161118 } ,
162119 } ;
0 commit comments