@@ -524,14 +524,41 @@ <h1>Source code for python_template_cpp</h1><div class="highlight"><pre>
524524let modal = null ;
525525let modalContent = null ;
526526let previousScrollOffset = [ window . scrollX , window . scrollY ] ;
527+ let _lazyObserver = null ;
528+ let _renderQueue = Promise . resolve ( ) ;
529+
530+ // Apply d3 zoom to each SVG. Idempotent: an SVG already wrapped is skipped,
531+ // so this is safe to call both for the initial batch and for diagrams that
532+ // render lazily once they become visible.
533+ const addZoomToSvgs = ( svgs ) => {
534+ svgs . each ( function ( ) {
535+ if ( this . getAttribute ( 'data-zoom-applied' ) === 'true' ) return ;
536+ var svg = d3 . select ( this ) ;
537+ svg . html ( "<g class='wrapper'>" + svg . html ( ) + "</g>" ) ;
538+ var inner = svg . select ( "g" ) ;
539+ var zoom = d3 . zoom ( ) . on ( "zoom" , function ( event ) {
540+ inner . attr ( "transform" , event . transform ) ;
541+ } ) ;
542+ svg . call ( zoom ) ;
543+ this . setAttribute ( 'data-zoom-applied' , 'true' ) ;
544+ } ) ;
545+ } ;
527546
528547const runMermaid = async ( rerun ) => {
529548 console . log ( "Running mermaid diagrams, rerun =" , rerun ) ;
530549 // clear all existing mermaid charts
531550 let all_mermaids = document . querySelectorAll ( ".mermaid" ) ;
532551
533552 if ( rerun ) {
553+ // Disconnect any previous lazy rendering observer
554+ if ( _lazyObserver ) {
555+ _lazyObserver . disconnect ( ) ;
556+ _lazyObserver = null ;
557+ }
558+
534559 all_mermaids . forEach ( ( el ) => {
560+ el . removeAttribute ( 'data-mermaid-deferred' ) ;
561+ el . removeAttribute ( 'data-mermaid-render-failed' ) ;
535562 if ( ! el . hasAttribute ( "data-original-code" ) ) {
536563 // store original code
537564 // console.log(`Storing original code for first run: `, el.innerHTML);
@@ -548,35 +575,89 @@ <h1>Source code for python_template_cpp</h1><div class="highlight"><pre>
548575 el . setAttribute ( 'data-original-code' , el . innerHTML ) ;
549576 }
550577 } ) ;
551- await mermaid . run ( ) ;
578+
579+ // Separate visible and hidden elements. Mermaid's layout engine measures
580+ // text dimensions via the DOM, which fails for elements hidden by a parent
581+ // (e.g., Reveal.js non-active slides, sphinx-design unopened tabs),
582+ // producing broken SVGs. Render visible elements now, defer hidden ones.
583+ const visible = [ ] ;
584+ const hidden = [ ] ;
585+ all_mermaids . forEach ( ( el ) => {
586+ // offsetParent is null for display:none ancestors.
587+ // getClientRects().length > 0 catches position:fixed elements
588+ // (which also have null offsetParent but are still visible).
589+ if ( el . offsetParent !== null || el . getClientRects ( ) . length > 0 ) {
590+ visible . push ( el ) ;
591+ } else {
592+ hidden . push ( el ) ;
593+ el . setAttribute ( 'data-mermaid-deferred' , 'true' ) ;
594+ }
595+ } ) ;
596+
597+ if ( visible . length > 0 ) {
598+ try {
599+ await mermaid . run ( { nodes : visible } ) ;
600+ } catch ( e ) {
601+ console . error ( "Mermaid rendering failed:" , e ) ;
602+ }
603+ }
604+
605+ // Lazily render hidden elements when they become visible.
606+ // Uses a promise chain (_renderQueue) to serialize rendering,
607+ // since IntersectionObserver may report multiple entries at once
608+ // and mermaid.run() is not safe to call concurrently.
609+ if ( hidden . length > 0 ) {
610+ if ( typeof IntersectionObserver === 'undefined' ) {
611+ console . warn ( "IntersectionObserver not available; hidden mermaid diagrams will not render." ) ;
612+ } else {
613+ _renderQueue = Promise . resolve ( ) ;
614+ _lazyObserver = new IntersectionObserver ( ( entries ) => {
615+ for ( const entry of entries ) {
616+ if ( entry . isIntersecting && entry . target . getAttribute ( "data-processed" ) !== "true" ) {
617+ _lazyObserver . unobserve ( entry . target ) ;
618+ const el = entry . target ;
619+ _renderQueue = _renderQueue . then ( async ( ) => {
620+ try {
621+ await mermaid . run ( { nodes : [ el ] } ) ;
622+ el . removeAttribute ( 'data-mermaid-deferred' ) ;
623+ // Apply zoom to the now-rendered diagram, matching
624+ // the decoration visible diagrams receive.
625+ if ( "True" === "True" ) {
626+ addZoomToSvgs ( d3 . selectAll ( ".mermaid svg" ) ) ;
627+ }
628+ } catch ( e ) {
629+ console . error ( "Mermaid deferred rendering failed:" , e ) ;
630+ el . removeAttribute ( 'data-mermaid-deferred' ) ;
631+ el . setAttribute ( 'data-mermaid-render-failed' , 'true' ) ;
632+ }
633+ } ) ;
634+ }
635+ }
636+ } ) ;
637+ hidden . forEach ( ( el ) => _lazyObserver . observe ( el ) ) ;
638+ }
639+ }
552640 }
553641
554642 all_mermaids = document . querySelectorAll ( ".mermaid" ) ;
555- const mermaids_processed = document . querySelectorAll ( ".mermaid[data-processed='true']" ) ;
643+ // Exclude deferred (hidden) and render-failed elements from completion checks
644+ const mermaids_active = document . querySelectorAll ( ".mermaid:not([data-mermaid-deferred]):not([data-mermaid-render-failed])" ) ;
645+ const mermaids_processed = document . querySelectorAll ( ".mermaid:not([data-mermaid-deferred]):not([data-mermaid-render-failed])[data-processed='true']" ) ;
556646
557647 if ( "True" === "True" ) {
558- const mermaids_to_add_zoom = - 1 === - 1 ? all_mermaids . length : - 1 ;
648+ const mermaids_to_add_zoom = - 1 === - 1 ? mermaids_active . length : - 1 ;
559649 if ( mermaids_to_add_zoom > 0 ) {
560650 var svgs = d3 . selectAll ( ".mermaid svg" ) ;
561- if ( all_mermaids . length !== mermaids_processed . length ) {
562- setTimeout ( ( ) => runMermaid ( false ) , 200 ) ;
563- return ;
564- } else if ( svgs . size ( ) !== mermaids_to_add_zoom ) {
651+ // Wait until every active (visible) diagram has rendered. Deferred
652+ // diagrams are excluded, so a fixed d3_node_count that counts a
653+ // hidden diagram no longer loops forever waiting for its SVG.
654+ if ( mermaids_active . length !== mermaids_processed . length ) {
565655 setTimeout ( ( ) => runMermaid ( false ) , 200 ) ;
566656 return ;
567- } else {
568- svgs . each ( function ( ) {
569- var svg = d3 . select ( this ) ;
570- svg . html ( "<g class='wrapper'>" + svg . html ( ) + "</g>" ) ;
571- var inner = svg . select ( "g" ) ;
572- var zoom = d3 . zoom ( ) . on ( "zoom" , function ( event ) {
573- inner . attr ( "transform" , event . transform ) ;
574- } ) ;
575- svg . call ( zoom ) ;
576- } ) ;
577657 }
658+ addZoomToSvgs ( svgs ) ;
578659 }
579- } else if ( all_mermaids . length !== mermaids_processed . length ) {
660+ } else if ( mermaids_active . length !== mermaids_processed . length ) {
580661 // Wait for mermaid to process all diagrams
581662 setTimeout ( ( ) => runMermaid ( false ) , 200 ) ;
582663 return ;
0 commit comments