11document . addEventListener ( 'click' , function ( event ) {
22 var anchor = event . target . closest ( 'a[href^="#"]' ) ;
33 if ( anchor ) {
4+ // Skip ref-links; they are replaced by inline expansions
5+ if ( anchor . classList . contains ( 'ref-link' ) ) {
6+ event . preventDefault ( ) ;
7+ return ;
8+ }
9+ // Don't interfere with Bootstrap tabs or collapse toggles
10+ if ( anchor . getAttribute ( 'data-bs-toggle' ) ) return ;
411 event . preventDefault ( ) ;
512 history . pushState ( { } , '' , anchor . href ) ;
613 }
@@ -71,4 +78,342 @@ function anchorLink(linkTarget) {
7178 } , 500 ) ;
7279 }
7380 } , 1000 ) ;
74- }
81+ }
82+
83+
84+ // ═══════════════════════════════════════════════════════════
85+ // Fix duplicate IDs produced by link_to_reused_ref
86+ // ═══════════════════════════════════════════════════════════
87+ //
88+ // The schema doc generator reuses the same IDs when inlining
89+ // a $ref definition at multiple schema paths. Duplicate IDs
90+ // break Bootstrap tabs/collapses because getElementById
91+ // always returns the first match. This pass finds duplicates
92+ // and rewrites subsequent occurrences so every ID is unique.
93+ // ═══════════════════════════════════════════════════════════
94+
95+ ( function ( ) {
96+ function fixDuplicateIds ( ) {
97+ var seen = { } ; // id -> true for first occurrence
98+ var dupCount = 0 ;
99+
100+ // Pass 1: rename duplicate IDs. First occurrence keeps
101+ // its id; subsequent occurrences get a unique suffix.
102+ var allWithId = document . querySelectorAll ( '[id]' ) ;
103+ allWithId . forEach ( function ( el ) {
104+ var id = el . id ;
105+ if ( ! id ) return ;
106+ if ( seen [ id ] ) {
107+ dupCount ++ ;
108+ el . setAttribute ( 'data-orig-id' , id ) ;
109+ el . id = id + '__d' + dupCount ;
110+ } else {
111+ seen [ id ] = true ;
112+ }
113+ } ) ;
114+
115+ if ( dupCount === 0 ) return ;
116+
117+ // Build lookup: origId -> [el, el, ...] for fast scoping
118+ var renamed = { } ;
119+ document . querySelectorAll ( '[data-orig-id]' ) . forEach ( function ( el ) {
120+ var origId = el . getAttribute ( 'data-orig-id' ) ;
121+ if ( ! renamed [ origId ] ) renamed [ origId ] = [ ] ;
122+ renamed [ origId ] . push ( el ) ;
123+ } ) ;
124+
125+ // Build full candidate list: origId -> [el, ...] including
126+ // both the original (first-occurrence) element and all renamed
127+ // duplicates so that scoping works for every occurrence.
128+ var allTargets = { } ;
129+ Object . keys ( renamed ) . forEach ( function ( origId ) {
130+ var orig = document . getElementById ( origId ) ;
131+ allTargets [ origId ] = orig ? [ orig ] . concat ( renamed [ origId ] ) : renamed [ origId ] ;
132+ } ) ;
133+
134+ // Find the target element (original or renamed) that shares
135+ // the closest common ancestor with the referrer.
136+ function findLocalTarget ( referrer , origId ) {
137+ var candidates = allTargets [ origId ] ;
138+ if ( ! candidates ) return origId ;
139+ var scope = referrer . parentElement ;
140+ while ( scope ) {
141+ for ( var i = 0 ; i < candidates . length ; i ++ ) {
142+ if ( scope . contains ( candidates [ i ] ) ) return candidates [ i ] . id ;
143+ }
144+ scope = scope . parentElement ;
145+ }
146+ return origId ;
147+ }
148+
149+ // Pass 2: fix references that point to renamed IDs.
150+ function fixHashAttr ( el , attr ) {
151+ var val = el . getAttribute ( attr ) ;
152+ if ( ! val || val . charAt ( 0 ) !== '#' ) return ;
153+ var refId = val . substring ( 1 ) ;
154+ if ( ! renamed [ refId ] ) return ;
155+ var localId = findLocalTarget ( el , refId ) ;
156+ if ( localId !== refId ) el . setAttribute ( attr , '#' + localId ) ;
157+ }
158+
159+ function fixPlainAttr ( el , attr ) {
160+ var val = el . getAttribute ( attr ) ;
161+ if ( ! val || ! renamed [ val ] ) return ;
162+ var localId = findLocalTarget ( el , val ) ;
163+ if ( localId !== val ) el . setAttribute ( attr , localId ) ;
164+ }
165+
166+ document . querySelectorAll ( 'a[href^="#"]' ) . forEach ( function ( el ) {
167+ fixHashAttr ( el , 'href' ) ;
168+ } ) ;
169+ document . querySelectorAll ( '[data-bs-target^="#"]' ) . forEach ( function ( el ) {
170+ fixHashAttr ( el , 'data-bs-target' ) ;
171+ } ) ;
172+ document . querySelectorAll ( '[data-bs-parent^="#"]' ) . forEach ( function ( el ) {
173+ fixHashAttr ( el , 'data-bs-parent' ) ;
174+ } ) ;
175+ document . querySelectorAll ( '[aria-controls]' ) . forEach ( function ( el ) {
176+ fixPlainAttr ( el , 'aria-controls' ) ;
177+ } ) ;
178+ document . querySelectorAll ( '[aria-labelledby]' ) . forEach ( function ( el ) {
179+ fixPlainAttr ( el , 'aria-labelledby' ) ;
180+ } ) ;
181+ document . querySelectorAll ( '[onclick]' ) . forEach ( function ( el ) {
182+ var onclick = el . getAttribute ( 'onclick' ) ;
183+ if ( ! onclick ) return ;
184+ var changed = false ;
185+ var updated = onclick . replace (
186+ / a n c h o r L i n k \( ' ( [ ^ ' ] + ) ' \) / g,
187+ function ( match , id ) {
188+ if ( ! renamed [ id ] ) return match ;
189+ var localId = findLocalTarget ( el , id ) ;
190+ if ( localId !== id ) { changed = true ; return "anchorLink('" + localId + "')" ; }
191+ return match ;
192+ }
193+ ) . replace (
194+ / s e t A n c h o r \( ' # ( [ ^ ' ] + ) ' \) / g,
195+ function ( match , id ) {
196+ if ( ! renamed [ id ] ) return match ;
197+ var localId = findLocalTarget ( el , id ) ;
198+ if ( localId !== id ) { changed = true ; return "setAnchor('#" + localId + "')" ; }
199+ return match ;
200+ }
201+ ) ;
202+ if ( changed ) el . setAttribute ( 'onclick' , updated ) ;
203+ } ) ;
204+ }
205+
206+ if ( document . readyState === 'loading' ) {
207+ document . addEventListener ( 'DOMContentLoaded' , fixDuplicateIds ) ;
208+ } else {
209+ fixDuplicateIds ( ) ;
210+ }
211+ } ) ( ) ;
212+
213+
214+ // ═══════════════════════════════════════════════════════════
215+ // Automatic inline expansion for reused definitions
216+ // ═══════════════════════════════════════════════════════════
217+ //
218+ // When link_to_reused_ref is enabled, repeated definitions
219+ // render as "Same definition as X" links pointing to the
220+ // original. This enhancement hides those links and clones
221+ // the original definition inline automatically when the
222+ // parent property row is expanded. No user click required.
223+ // The full HTML stays in the DOM for SEO crawlability.
224+ // ═══════════════════════════════════════════════════════════
225+
226+ ( function ( ) {
227+ var expandCounter = 0 ;
228+
229+ /**
230+ * Rewrite IDs inside a cloned subtree so they don't collide
231+ * with the originals. Also updates internal href="#...",
232+ * data-bs-target, data-bs-parent, and aria attributes.
233+ */
234+ function deduplicateIds ( container , suffix ) {
235+ var elements = container . querySelectorAll ( '[id]' ) ;
236+ var idMap = { } ;
237+ elements . forEach ( function ( el ) {
238+ var oldId = el . id ;
239+ var newId = oldId + suffix ;
240+ idMap [ oldId ] = newId ;
241+ el . id = newId ;
242+ } ) ;
243+
244+ container . querySelectorAll ( '[href]' ) . forEach ( function ( el ) {
245+ var href = el . getAttribute ( 'href' ) ;
246+ if ( href && href . charAt ( 0 ) === '#' ) {
247+ var refId = href . substring ( 1 ) ;
248+ if ( idMap [ refId ] ) {
249+ el . setAttribute ( 'href' , '#' + idMap [ refId ] ) ;
250+ }
251+ }
252+ } ) ;
253+ container . querySelectorAll ( '[data-bs-target]' ) . forEach ( function ( el ) {
254+ var val = el . getAttribute ( 'data-bs-target' ) ;
255+ if ( val && val . charAt ( 0 ) === '#' ) {
256+ var refId = val . substring ( 1 ) ;
257+ if ( idMap [ refId ] ) {
258+ el . setAttribute ( 'data-bs-target' , '#' + idMap [ refId ] ) ;
259+ }
260+ }
261+ } ) ;
262+ container . querySelectorAll ( '[data-bs-parent]' ) . forEach ( function ( el ) {
263+ var val = el . getAttribute ( 'data-bs-parent' ) ;
264+ if ( val && val . charAt ( 0 ) === '#' ) {
265+ var refId = val . substring ( 1 ) ;
266+ if ( idMap [ refId ] ) {
267+ el . setAttribute ( 'data-bs-parent' , '#' + idMap [ refId ] ) ;
268+ }
269+ }
270+ } ) ;
271+ container . querySelectorAll ( '[aria-controls]' ) . forEach ( function ( el ) {
272+ var val = el . getAttribute ( 'aria-controls' ) ;
273+ if ( val && idMap [ val ] ) {
274+ el . setAttribute ( 'aria-controls' , idMap [ val ] ) ;
275+ }
276+ } ) ;
277+ container . querySelectorAll ( '[aria-labelledby]' ) . forEach ( function ( el ) {
278+ var val = el . getAttribute ( 'aria-labelledby' ) ;
279+ if ( val && idMap [ val ] ) {
280+ el . setAttribute ( 'aria-labelledby' , idMap [ val ] ) ;
281+ }
282+ } ) ;
283+
284+ container . querySelectorAll ( '[onclick]' ) . forEach ( function ( el ) {
285+ var onclick = el . getAttribute ( 'onclick' ) ;
286+ if ( onclick ) {
287+ var updated = onclick . replace (
288+ / a n c h o r L i n k \( ' ( [ ^ ' ] + ) ' \) / g,
289+ function ( match , id ) {
290+ return idMap [ id ] ? "anchorLink('" + idMap [ id ] + "')" : match ;
291+ }
292+ ) . replace (
293+ / s e t A n c h o r \( ' # ( [ ^ ' ] + ) ' \) / g,
294+ function ( match , id ) {
295+ return idMap [ id ] ? "setAnchor('#" + idMap [ id ] + "')" : match ;
296+ }
297+ ) ;
298+ el . setAttribute ( 'onclick' , updated ) ;
299+ }
300+ } ) ;
301+ }
302+
303+ /**
304+ * Check whether a node is "leading metadata" that already
305+ * appears in the ref-link's container: the type badge
306+ * (span.badge.value-type), a <br>, a description span,
307+ * or whitespace text nodes between them.
308+ */
309+ function isLeadingMeta ( node ) {
310+ if ( node . nodeType === 3 ) {
311+ // Text node: skip if whitespace-only
312+ return node . textContent . trim ( ) === '' ;
313+ }
314+ if ( node . nodeType !== 1 ) return false ;
315+ var el = node ;
316+ // Type badge, e.g. <span class="badge ... value-type">
317+ if ( el . tagName === 'SPAN' && el . classList . contains ( 'value-type' ) ) return true ;
318+ // <br> element right after the type badge
319+ if ( el . tagName === 'BR' ) return true ;
320+ // Description span
321+ if ( el . tagName === 'SPAN' && el . classList . contains ( 'description' ) ) return true ;
322+ return false ;
323+ }
324+
325+ /**
326+ * Clone a source definition into the container that holds
327+ * the ref-link. The ref-link itself is hidden via CSS.
328+ * Leading type badge, <br>, and description are skipped
329+ * because the container already shows them.
330+ */
331+ function expandRefLink ( link ) {
332+ // Skip if already expanded
333+ if ( link . getAttribute ( 'data-ref-expanded' ) === 'true' ) return ;
334+ link . setAttribute ( 'data-ref-expanded' , 'true' ) ;
335+
336+ var targetId = link . getAttribute ( 'href' ) . substring ( 1 ) ;
337+ var source = document . getElementById ( targetId ) ;
338+ if ( ! source ) return ;
339+
340+ expandCounter ++ ;
341+ var suffix = '__exp' + expandCounter ;
342+
343+ var content = document . createElement ( 'div' ) ;
344+ content . className = 'ref-expand-content' ;
345+
346+ // Clone child nodes, skipping leading metadata that
347+ // duplicates what the container already displays.
348+ var nodes = source . childNodes ;
349+ var pastLeading = false ;
350+ for ( var i = 0 ; i < nodes . length ; i ++ ) {
351+ if ( ! pastLeading && isLeadingMeta ( nodes [ i ] ) ) continue ;
352+ pastLeading = true ;
353+ content . appendChild ( nodes [ i ] . cloneNode ( true ) ) ;
354+ }
355+
356+ deduplicateIds ( content , suffix ) ;
357+
358+ // Insert the cloned content after the ref-link
359+ link . parentNode . insertBefore ( content , link . nextSibling ) ;
360+ }
361+
362+ /**
363+ * Check whether a ref-link is directly visible within the
364+ * panel that was just shown. Returns false if the link sits
365+ * inside a nested collapse that is still hidden.
366+ */
367+ function isVisibleInPanel ( link , panel ) {
368+ var el = link . parentElement ;
369+ while ( el && el !== panel ) {
370+ if ( el . classList . contains ( 'collapse' ) && ! el . classList . contains ( 'show' ) ) {
371+ return false ;
372+ }
373+ el = el . parentElement ;
374+ }
375+ return true ;
376+ }
377+
378+ /**
379+ * When a collapse panel is shown, expand only the ref-links
380+ * that are directly visible (not buried in nested collapses).
381+ */
382+ function onCollapseShown ( e ) {
383+ var panel = e . target ;
384+ var refLinks = panel . querySelectorAll ( '.ref-link' ) ;
385+ refLinks . forEach ( function ( link ) {
386+ if ( isVisibleInPanel ( link , panel ) ) {
387+ expandRefLink ( link ) ;
388+ }
389+ } ) ;
390+ }
391+
392+ /**
393+ * Initialize: hide ref-link text, listen for collapse events.
394+ */
395+ function initRefLinks ( ) {
396+ var refLinks = document . querySelectorAll ( '.ref-link' ) ;
397+ refLinks . forEach ( function ( link ) {
398+ // Remove the original onclick
399+ link . removeAttribute ( 'onclick' ) ;
400+
401+ // Expand ref-links that are already visible on load
402+ // (not inside any collapsed panel)
403+ var parentCollapse = link . closest ( '.collapse' ) ;
404+ if ( ! parentCollapse || parentCollapse . classList . contains ( 'show' ) ) {
405+ expandRefLink ( link ) ;
406+ }
407+ } ) ;
408+
409+ // Listen for Bootstrap collapse show events
410+ document . addEventListener ( 'shown.bs.collapse' , onCollapseShown ) ;
411+ }
412+
413+ // Run on DOM ready
414+ if ( document . readyState === 'loading' ) {
415+ document . addEventListener ( 'DOMContentLoaded' , initRefLinks ) ;
416+ } else {
417+ initRefLinks ( ) ;
418+ }
419+ } ) ( ) ;
0 commit comments