import { adopt, define, sheet } from "@nibling/web/internals"; import type { Files } from "../files.ts"; import type { Unsubscribe } from "../shell/workbench.ts"; const previewSheet = sheet(` :host { display: flex; container: nib-preview / size; } iframe { flex: 1; min-width: 0; min-height: 0; border: 0; background: var(--nib-color-document); } iframe:focus-visible { outline: var(--nib-size-focus-ring) solid var(--nib-color-focus-ring); outline-offset: calc(-1 * var(--nib-size-focus-ring)); } `); /** * Renders an HTML file in an opaque-origin iframe. The real preview is guest HTTP on the sandbox's * own origin (D3); `sandbox` without `allow-same-origin` is the in-page stand-in. An opaque origin * has nothing to fetch relative URLs from, so local stylesheets and scripts are inlined. */ export class PreviewElement extends HTMLElement { readonly #iframe: HTMLIFrameElement; constructor() { super(); const root = this.attachShadow({ mode: "open", delegatesFocus: true }); adopt(root, previewSheet); this.#iframe = document.createElement("iframe"); root.append(this.#iframe); } connect(files: Files, path: string): Unsubscribe { this.#iframe.title = `Preview of ${path}`; this.#iframe.sandbox.add("allow-scripts"); let deps = new Set(); const draw = () => { const bundle = inline(files, path); deps = bundle.deps; this.#iframe.srcdoc = bundle.html; }; draw(); return files.watch((changed) => { if (changed === path || deps.has(changed)) draw(); }); } } /** Inlines the stylesheets and scripts an HTML file links from `files`, and lists the paths it read. */ function inline(files: Files, path: string): { html: string; deps: Set } { const deps = new Set(); const doc = new DOMParser().parseFromString(files.read(path) ?? "", "text/html"); const local = (ref: string | null) => { if (!ref) return; const url = new URL(ref, `file:///${path}`); if (url.protocol !== "file:") return; const target = decodeURIComponent(url.pathname.slice(1)); const text = files.read(target); if (text === undefined) return; deps.add(target); return text; }; for (const link of doc.querySelectorAll('link[rel~="stylesheet"][href]')) { const text = local(link.getAttribute("href")); if (text === undefined) continue; const style = doc.createElement("style"); style.textContent = text; link.replaceWith(style); } for (const script of doc.querySelectorAll("script[src]")) { const text = local(script.getAttribute("src")); if (text === undefined) continue; script.removeAttribute("src"); script.textContent = text; } return { html: `\n${doc.documentElement.outerHTML}`, deps }; } declare global { interface HTMLElementTagNameMap { "nib-preview": PreviewElement; } } define("nib-preview", PreviewElement);