@@ -37,13 +37,10 @@ const createChangeElement = entry => {
3737 // Process lifecycle changes (added, deprecated, etc.)
3838 ...Object . entries ( LIFECYCLE_LABELS )
3939 . 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+ } ) ) ,
4744
4845 // Process explicit changes if they exist
4946 ...( entry . changes ?. map ( change => ( {
@@ -53,7 +50,7 @@ const createChangeElement = entry => {
5350 } ) ) || [ ] ) ,
5451 ] ;
5552
56- if ( changeEntries . length === 0 ) {
53+ if ( ! changeEntries . length ) {
5754 return null ;
5855 }
5956
@@ -69,20 +66,17 @@ const createChangeElement = entry => {
6966 * @param {string|undefined } sourceLink - The source link path
7067 * @returns {import('hastscript').Element|null } The source link element or null if no source link
7168 */
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 ;
8680
8781/**
8882 * Creates a heading element with appropriate styling and metadata
@@ -102,30 +96,25 @@ const createHeadingElement = (content, changeElement) => {
10296 { trimWhitespace : true }
10397 ) . children ;
10498
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 ) ,
110102 ] ) ,
111- ] ;
103+ ] ) ;
112104
113105 // Add type icon if available
114106 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' } )
120109 ) ;
121110 }
122111
123112 // Add change history if available
124113 if ( changeElement ) {
125- headingChildren [ 0 ] . children . push ( changeElement ) ;
114+ headingWrapper . children . push ( changeElement ) ;
126115 }
127116
128- return createElement ( 'div' , headingChildren ) ;
117+ return headingWrapper ;
129118} ;
130119
131120/**
@@ -159,10 +148,11 @@ const transformStabilityNode = (node, index, parent) => {
159148 * @returns {[typeof SKIP] } Visitor instruction to skip the node
160149 */
161150const transformHeadingNode = ( entry , node , index , parent ) => {
162- const changeElement = createChangeElement ( entry ) ;
163-
164151 // 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+ ) ;
166156
167157 // Add source link if available
168158 const sourceLink = createSourceLink ( entry . source_link ) ;
@@ -172,11 +162,7 @@ const transformHeadingNode = (entry, node, index, parent) => {
172162
173163 // Add method signatures for appropriate types
174164 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 ] ) ;
180166 }
181167
182168 return [ SKIP ] ;
@@ -222,6 +208,62 @@ const createDocumentLayout = (entries, sideBarProps, metaBarProps) => {
222208 ] ) ;
223209} ;
224210
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+
225267/**
226268 * @typedef {import('estree').Node & { data: ApiDocMetadataEntry } } JSXContent
227269 *
@@ -233,9 +275,11 @@ const createDocumentLayout = (entries, sideBarProps, metaBarProps) => {
233275 * @returns {JSXContent } The processed MDX content
234276 */
235277const buildContent = ( metadataEntries , head , sideBarProps , remark ) => {
236- const metaBarProps = buildMetaBarProps ( head , metadataEntries ) ;
278+ const processedEntries = removeDuplicates ( metadataEntries ) ;
279+ const metaBarProps = buildMetaBarProps ( head , processedEntries ) ;
280+
237281 const root = createDocumentLayout (
238- metadataEntries ,
282+ processedEntries ,
239283 sideBarProps ,
240284 metaBarProps
241285 ) ;
0 commit comments