8
8
//-----------------------------------------------------------------------------
9
9
10
10
/**
11
- * @import { Heading } from "mdast";
12
11
* @import { MarkdownRuleDefinition } from "../types.js";
13
12
* @typedef {"duplicateHeading" } NoDuplicateHeadingsMessageIds
14
13
* @typedef {[{ checkSiblingsOnly?: boolean }] } NoDuplicateHeadingsOptions
15
14
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoDuplicateHeadingsOptions, MessageIds: NoDuplicateHeadingsMessageIds }> } NoDuplicateHeadingsRuleDefinition
16
15
*/
17
16
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
-
42
17
//-----------------------------------------------------------------------------
43
18
// Rule Definition
44
19
//-----------------------------------------------------------------------------
@@ -74,7 +49,6 @@ export default {
74
49
75
50
create ( context ) {
76
51
const [ { checkSiblingsOnly } ] = context . options ;
77
- const { sourceCode } = context ;
78
52
79
53
/** @type {Map<number, Set<string>> } */
80
54
const headingsByLevel = checkSiblingsOnly
@@ -89,46 +63,15 @@ export default {
89
63
: new Map ( [ [ 1 , new Set ( ) ] ] ) ;
90
64
let lastLevel = 1 ;
91
65
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 ;
128
70
129
71
return {
130
72
heading ( node ) {
131
- const headingText = getHeadingText ( node ) ;
73
+ headingChildrenSequence = "" ;
74
+ headingText = "" ;
132
75
133
76
if ( checkSiblingsOnly ) {
134
77
const currentLevel = node . depth ;
@@ -146,8 +89,22 @@ export default {
146
89
lastLevel = currentLevel ;
147
90
currentLevelHeadings = headingsByLevel . get ( currentLevel ) ;
148
91
}
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
+ } ,
149
105
150
- if ( currentLevelHeadings . has ( headingText ) ) {
106
+ "heading:exit" ( node ) {
107
+ if ( currentLevelHeadings . has ( headingChildrenSequence ) ) {
151
108
context . report ( {
152
109
loc : node . position ,
153
110
messageId : "duplicateHeading" ,
@@ -156,7 +113,7 @@ export default {
156
113
} ,
157
114
} ) ;
158
115
} else {
159
- currentLevelHeadings . add ( headingText ) ;
116
+ currentLevelHeadings . add ( headingChildrenSequence ) ;
160
117
}
161
118
} ,
162
119
} ;
0 commit comments