@@ -31,6 +31,7 @@ import {
3131 MANUAL_EDIT_DISCOVERY_SELECTOR ,
3232 MANUAL_EDIT_SOURCE_PATH_ATTR ,
3333} from '../edit-mode/bridge' ;
34+ import { isApprovedFontStylesheetHref } from './deck-thumbnail-parser' ;
3435
3536export type SrcdocOptions = {
3637 deck ?: boolean ;
@@ -48,6 +49,9 @@ export type SrcdocOptions = {
4849 /** Install the live-preview error and white-screen reporting bridge. Keep
4950 * this disabled for exports, captures, thumbnails, and historical previews. */
5051 previewObservability ?: boolean ;
52+ /** Let trusted font-CDN stylesheets load without blocking the live preview's
53+ * first paint. Keep disabled for exports and other capture surfaces. */
54+ deferFontStylesheets ?: boolean ;
5155 /**
5256 * Force every CSS animation/transition to complete instantly so the
5357 * document settles at its final visual state and stops repainting. Meant
@@ -395,7 +399,10 @@ export function buildSrcdoc(
395399 const withOdIds = annotateMissingOdIds ( withSafeTitle ) ;
396400 const withSourcePaths = options . editBridge ? annotateManualEditSourcePaths ( withOdIds ) : withOdIds ;
397401 const withBase = options . baseHref ? injectBaseHref ( withSourcePaths , options . baseHref ) : withSourcePaths ;
398- const withShim = injectSandboxShim ( withBase ) ;
402+ const withDeferredFonts = options . deferFontStylesheets
403+ ? deferTrustedFontStylesheets ( withBase )
404+ : withBase ;
405+ const withShim = injectSandboxShim ( withDeferredFonts ) ;
399406 const blockLoadTimeScriptRedirect = htmlHasLoadTimeLocationNavigation ( withBase ) ;
400407 // Always on: a redirect loop can freeze ANY previewed artifact, and the guard
401408 // is inert on documents that never self-redirect. Injected right after the
@@ -1301,6 +1308,62 @@ function injectManualEditBridge(doc: string): string {
13011308 return injectBeforeBodyEnd ( withStyle , buildManualEditBridge ( false ) ) ;
13021309}
13031310
1311+ const DEFERRED_FONT_STYLESHEET_ATTR = 'data-od-deferred-font-stylesheet' ;
1312+
1313+ function deferTrustedFontStylesheets ( doc : string ) : string {
1314+ if ( typeof DOMParser === 'undefined' ) return doc ;
1315+ let parsed : Document ;
1316+ try {
1317+ parsed = new DOMParser ( ) . parseFromString ( doc , 'text/html' ) ;
1318+ } catch {
1319+ return doc ;
1320+ }
1321+
1322+ let deferred = false ;
1323+ parsed . querySelectorAll < HTMLLinkElement > ( 'link[rel~="stylesheet"][href]' ) . forEach ( ( link ) => {
1324+ const href = link . getAttribute ( 'href' ) ?? '' ;
1325+ if ( ! isApprovedFontStylesheetHref ( href ) ) return ;
1326+ const authoredMedia = link . getAttribute ( 'media' ) ?. trim ( ) ?? '' ;
1327+ if ( authoredMedia . toLowerCase ( ) === 'print' ) return ;
1328+ link . setAttribute ( DEFERRED_FONT_STYLESHEET_ATTR , authoredMedia ) ;
1329+ link . setAttribute ( 'media' , 'print' ) ;
1330+ deferred = true ;
1331+ } ) ;
1332+ if ( ! deferred ) return doc ;
1333+
1334+ const script = `<script data-od-font-stylesheet-loader>(function(){
1335+ var attr = '${ DEFERRED_FONT_STYLESHEET_ATTR } ';
1336+ var selector = 'link[' + attr + ']';
1337+ function activate(link){
1338+ if (!link || !link.hasAttribute(attr)) return;
1339+ var media = link.getAttribute(attr) || 'all';
1340+ link.setAttribute('media', media);
1341+ link.removeAttribute(attr);
1342+ }
1343+ function watch(link){
1344+ if (!link || link.__odFontStylesheetWatched) return;
1345+ link.__odFontStylesheetWatched = true;
1346+ link.addEventListener('load', function(){ activate(link); }, { once: true });
1347+ try { if (link.sheet) activate(link); } catch (_) {}
1348+ }
1349+ function scan(root){
1350+ if (!root) return;
1351+ if (root.matches && root.matches(selector)) watch(root);
1352+ var links = root.querySelectorAll ? root.querySelectorAll(selector) : [];
1353+ for (var i = 0; i < links.length; i += 1) watch(links[i]);
1354+ }
1355+ var observer = typeof MutationObserver === 'function' ? new MutationObserver(function(records){
1356+ for (var i = 0; i < records.length; i += 1) {
1357+ var added = records[i].addedNodes || [];
1358+ for (var j = 0; j < added.length; j += 1) scan(added[j]);
1359+ }
1360+ }) : null;
1361+ if (observer && document.documentElement) observer.observe(document.documentElement, { childList: true, subtree: true });
1362+ scan(document);
1363+ })();</script>` ;
1364+ return injectAfterHeadOpen ( serializeHtmlDocument ( parsed ) , script ) ;
1365+ }
1366+
13041367function injectAfterHeadOpen ( doc : string , payload : string ) : string {
13051368 if ( / < h e a d [ ^ > ] * > / i. test ( doc ) ) return doc . replace ( / < h e a d [ ^ > ] * > / i, ( m ) => `${ m } ${ payload } ` ) ;
13061369 return payload + doc ;
0 commit comments