@@ -37,13 +37,10 @@ const createChangeElement = entry => {
37
37
// Process lifecycle changes (added, deprecated, etc.)
38
38
...Object . entries ( LIFECYCLE_LABELS )
39
39
. filter ( ( [ field ] ) => entry [ field ] )
40
- . map ( ( [ field , label ] ) => {
41
- const versions = enforceArray ( entry [ field ] ) ;
42
- return {
43
- versions,
44
- label : `${ label } : ${ versions . join ( ', ' ) } ` ,
45
- } ;
46
- } ) ,
40
+ . map ( ( [ field , label ] ) => ( {
41
+ versions : enforceArray ( entry [ field ] ) ,
42
+ label : `${ label } : ${ enforceArray ( entry [ field ] ) . join ( ', ' ) } ` ,
43
+ } ) ) ,
47
44
48
45
// Process explicit changes if they exist
49
46
...( entry . changes ?. map ( change => ( {
@@ -53,7 +50,7 @@ const createChangeElement = entry => {
53
50
} ) ) || [ ] ) ,
54
51
] ;
55
52
56
- if ( changeEntries . length === 0 ) {
53
+ if ( ! changeEntries . length ) {
57
54
return null ;
58
55
}
59
56
@@ -69,20 +66,17 @@ const createChangeElement = entry => {
69
66
* @param {string|undefined } sourceLink - The source link path
70
67
* @returns {import('hastscript').Element|null } The source link element or null if no source link
71
68
*/
72
- const createSourceLink = sourceLink => {
73
- if ( ! sourceLink ) {
74
- return null ;
75
- }
76
-
77
- return createElement ( 'span' , [
78
- INTERNATIONALIZABLE . sourceCode ,
79
- createElement (
80
- 'a' ,
81
- { href : `${ DOC_NODE_BLOB_BASE_URL } ${ sourceLink } ` } ,
82
- sourceLink
83
- ) ,
84
- ] ) ;
85
- } ;
69
+ const createSourceLink = sourceLink =>
70
+ sourceLink
71
+ ? createElement ( 'span' , [
72
+ INTERNATIONALIZABLE . sourceCode ,
73
+ createElement (
74
+ 'a' ,
75
+ { href : `${ DOC_NODE_BLOB_BASE_URL } ${ sourceLink } ` } ,
76
+ sourceLink
77
+ ) ,
78
+ ] )
79
+ : null ;
86
80
87
81
/**
88
82
* Creates a heading element with appropriate styling and metadata
@@ -102,30 +96,25 @@ const createHeadingElement = (content, changeElement) => {
102
96
{ trimWhitespace : true }
103
97
) . children ;
104
98
105
- const headingChildren = [
106
- createElement ( 'div' , [
107
- createElement ( `h${ depth } ` , [
108
- createElement ( `a#${ slug } ` , { href : `#${ slug } ` } , headingContent ) ,
109
- ] ) ,
99
+ const headingWrapper = createElement ( 'div' , [
100
+ createElement ( `h${ depth } ` , [
101
+ createElement ( `a#${ slug } ` , { href : `#${ slug } ` } , headingContent ) ,
110
102
] ) ,
111
- ] ;
103
+ ] ) ;
112
104
113
105
// Add type icon if available
114
106
if ( type && type !== 'misc' ) {
115
- headingChildren [ 0 ] . children . unshift (
116
- createJSXElement ( JSX_IMPORTS . DataTag . name , {
117
- kind : type ,
118
- size : 'sm' ,
119
- } )
107
+ headingWrapper . children . unshift (
108
+ createJSXElement ( JSX_IMPORTS . DataTag . name , { kind : type , size : 'sm' } )
120
109
) ;
121
110
}
122
111
123
112
// Add change history if available
124
113
if ( changeElement ) {
125
- headingChildren [ 0 ] . children . push ( changeElement ) ;
114
+ headingWrapper . children . push ( changeElement ) ;
126
115
}
127
116
128
- return createElement ( 'div' , headingChildren ) ;
117
+ return headingWrapper ;
129
118
} ;
130
119
131
120
/**
@@ -159,10 +148,11 @@ const transformStabilityNode = (node, index, parent) => {
159
148
* @returns {[typeof SKIP] } Visitor instruction to skip the node
160
149
*/
161
150
const transformHeadingNode = ( entry , node , index , parent ) => {
162
- const changeElement = createChangeElement ( entry ) ;
163
-
164
151
// Replace node with new heading and anchor
165
- parent . children [ index ] = createHeadingElement ( node , changeElement ) ;
152
+ parent . children [ index ] = createHeadingElement (
153
+ node ,
154
+ createChangeElement ( entry )
155
+ ) ;
166
156
167
157
// Add source link if available
168
158
const sourceLink = createSourceLink ( entry . source_link ) ;
@@ -172,11 +162,7 @@ const transformHeadingNode = (entry, node, index, parent) => {
172
162
173
163
// Add method signatures for appropriate types
174
164
if ( TYPES_WITH_METHOD_SIGNATURES . includes ( node . data . type ) ) {
175
- parent . children . splice (
176
- index + ( sourceLink ? 2 : 1 ) ,
177
- 1 ,
178
- ...createSignatureElements ( entry )
179
- ) ;
165
+ createSignatureElements ( parent , node . data . entries || [ entry ] ) ;
180
166
}
181
167
182
168
return [ SKIP ] ;
@@ -222,6 +208,62 @@ const createDocumentLayout = (entries, sideBarProps, metaBarProps) => {
222
208
] ) ;
223
209
} ;
224
210
211
+ /**
212
+ * Identifies and removes duplicate headings across metadata entries while tracking them
213
+ * @param {Array<ApiDocMetadataEntry> } metadataEntries - API documentation metadata entries
214
+ * @returns {Array<ApiDocMetadataEntry> } Processed entries with duplicates removed
215
+ */
216
+ const removeDuplicates = metadataEntries => {
217
+ // Group entries by their heading's full name
218
+ const entriesByName = { } ;
219
+
220
+ // First pass: identify headings with method signatures
221
+ metadataEntries . forEach ( entry => {
222
+ visit ( entry . content , createQueries . UNIST . isHeading , node => {
223
+ if ( TYPES_WITH_METHOD_SIGNATURES . includes ( node . data . type ) ) {
224
+ const fullName = getFullName ( node . data ) ;
225
+ ( entriesByName [ fullName ] ??= [ ] ) . push ( { entry, node } ) ;
226
+ }
227
+ } ) ;
228
+ } ) ;
229
+
230
+ // Second pass: remove duplicates, keeping only the last occurrence
231
+ for ( const matches of Object . values ( entriesByName ) ) {
232
+ if ( matches . length > 1 ) {
233
+ // Get the last match which will be kept
234
+ const lastMatch = matches [ matches . length - 1 ] ;
235
+
236
+ // Add all entries to the last entry's node data
237
+ lastMatch . node . data . entries = matches . map ( match => match . entry ) ;
238
+
239
+ // Remove all but the last duplicate from their parent nodes
240
+ matches . slice ( 0 , - 1 ) . forEach ( match => {
241
+ const { entry, node } = match ;
242
+
243
+ // Find the parent of the node to remove
244
+ visit ( entry . content , parent => {
245
+ // Check if this parent contains our node
246
+ const index = ( parent . children || [ ] ) . indexOf ( node ) ;
247
+ if ( index !== - 1 ) {
248
+ // Remove the node from its parent's children
249
+ parent . children . splice ( index , 1 ) ;
250
+ return true ; // Stop traversal once found and removed
251
+ }
252
+ return false ;
253
+ } ) ;
254
+ } ) ;
255
+ }
256
+ }
257
+
258
+ // Filter out any entries that are now empty after removal
259
+ return metadataEntries . filter (
260
+ entry =>
261
+ entry . content &&
262
+ entry . content . children &&
263
+ entry . content . children . length > 0
264
+ ) ;
265
+ } ;
266
+
225
267
/**
226
268
* @typedef {import('estree').Node & { data: ApiDocMetadataEntry } } JSXContent
227
269
*
@@ -233,9 +275,11 @@ const createDocumentLayout = (entries, sideBarProps, metaBarProps) => {
233
275
* @returns {JSXContent } The processed MDX content
234
276
*/
235
277
const buildContent = ( metadataEntries , head , sideBarProps , remark ) => {
236
- const metaBarProps = buildMetaBarProps ( head , metadataEntries ) ;
278
+ const processedEntries = removeDuplicates ( metadataEntries ) ;
279
+ const metaBarProps = buildMetaBarProps ( head , processedEntries ) ;
280
+
237
281
const root = createDocumentLayout (
238
- metadataEntries ,
282
+ processedEntries ,
239
283
sideBarProps ,
240
284
metaBarProps
241
285
) ;
0 commit comments