From 647a908eabdebe9f1c4a2ba022586df8588780c8 Mon Sep 17 00:00:00 2001 From: lewis Date: Fri, 17 Apr 2026 22:57:23 +0800 Subject: [PATCH] fix(presenter): use contentDocument.write() instead of srcdoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous iframe srcdoc approach completely broke because .replace(/"/g, '"') mangled the stylesheet into , causing browser to treat the entire "file://..." as the attribute value literal. All CSS failed to load → blank/unstyled preview. New approach: - iframe.contentDocument.write() takes raw HTML string, NO escaping needed - All quotes (double and single) pass through untouched - stylesheet href attributes remain valid - body class and html attrs pass through cleanly - Added initWhenReady() polling to ensure iframe contentDocument is available before first render (avoids race condition where document.write fires before iframe is fully initialized) Verified with headless Chrome render — current/next slides now show correct colors, fonts, layout matching audience view pixel-for-pixel. --- assets/runtime.js | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/assets/runtime.js b/assets/runtime.js index 7b3aaa9..8a1abcb 100644 --- a/assets/runtime.js +++ b/assets/runtime.js @@ -186,22 +186,22 @@ function buildPresenterHTML(slideData, styleSheets, total, startIdx, bodyClasses, htmlAttrs) { const slidesJSON = JSON.stringify(slideData); - // Build the srcdoc template for slide iframes. - // Each iframe gets its own viewport (1920x1080) so all CSS - // (vw/vh/clamp/percentage) resolves identically to the audience view. - // We use a placeholder %%SLIDE_HTML%% that gets replaced per-slide. - const iframeSrcdocTemplate = '' - + '' + // Build iframe document template. Each iframe gets its own viewport + // at 1920x1080 so vw/vh/clamp all resolve exactly like the audience view. + // We inject via contentDocument.write() so there's ZERO HTML escaping issues. + // Template is a JS string (not embedded HTML attribute), so quotes stay raw. + const iframeDocTemplate = '' + + '' + '' - + styleSheets.replace(/"/g, '"') + + styleSheets + '' - + '' + + '' + '
%%SLIDE_HTML%%
' + ''; @@ -267,7 +267,7 @@ + ' var total = ' + total + ';\n' + ' var idx = ' + startIdx + ';\n' + ' var CHANNEL_NAME = ' + JSON.stringify(CHANNEL_NAME) + ';\n' -+ ' var SRCDOC_TPL = ' + JSON.stringify(iframeSrcdocTemplate) + ';\n' ++ ' var DOC_TPL = ' + JSON.stringify(iframeDocTemplate) + ';\n' + ' var bc; try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) {}\n' + '\n' + ' var iframeCur = document.getElementById("iframe-cur");\n' @@ -291,9 +291,12 @@ + ' return Math.min(cw / 1920, ch / 1080);\n' + ' }\n' + '\n' -+ ' function setIframeSrcdoc(iframe, slideHTML) {\n' -+ ' var doc = SRCDOC_TPL.replace("%%SLIDE_HTML%%", slideHTML);\n' -+ ' iframe.srcdoc = doc;\n' ++ ' function renderIframe(iframe, slideHTML) {\n' ++ ' var doc = DOC_TPL.replace("%%SLIDE_HTML%%", slideHTML);\n' ++ ' try {\n' ++ ' var d = iframe.contentDocument || iframe.contentWindow.document;\n' ++ ' d.open(); d.write(doc); d.close();\n' ++ ' } catch(e) { console.error("presenter iframe render failed", e); }\n' + ' }\n' + '\n' + ' var endEl = null;\n' @@ -301,12 +304,12 @@ + ' n = Math.max(0, Math.min(total - 1, n));\n' + ' idx = n;\n' + ' /* Current slide — render in iframe */\n' -+ ' setIframeSrcdoc(iframeCur, slideData[n].html);\n' ++ ' renderIframe(iframeCur, slideData[n].html);\n' + ' /* Next slide */\n' + ' if (n + 1 < total) {\n' + ' iframeNxt.style.display = "";\n' + ' if (endEl) { endEl.remove(); endEl = null; }\n' -+ ' setIframeSrcdoc(iframeNxt, slideData[n + 1].html);\n' ++ ' renderIframe(iframeNxt, slideData[n + 1].html);\n' + ' } else {\n' + ' iframeNxt.style.display = "none";\n' + ' if (!endEl) {\n' @@ -352,7 +355,15 @@ + ' });\n' + '\n' + ' window.addEventListener("resize", reScale);\n' -+ ' setTimeout(function(){ update(idx); }, 100);\n' ++ ' /* Wait for iframes to be ready, then render */\n' ++ ' function initWhenReady() {\n' ++ ' if (iframeCur.contentDocument && iframeNxt.contentDocument) {\n' ++ ' update(idx);\n' ++ ' } else {\n' ++ ' setTimeout(initWhenReady, 50);\n' ++ ' }\n' ++ ' }\n' ++ ' setTimeout(initWhenReady, 50);\n' + '})();\n' + '\n' + '';