import { scopeCssToComposition, wrapScopedCompositionScript } from "../compiler/compositionScoping"; import { readDeclaredDefaults } from "./getVariables"; type LoadExternalCompositionsParams = { injectedStyles: HTMLStyleElement[]; injectedScripts: HTMLScriptElement[]; parseDimensionPx: (value: string | null) => string | null; onDiagnostic?: (payload: { code: string; details: Record; }) => void; }; type PendingScript = | { kind: "inline"; content: string; type: string; scopeCompositionId: string | null; } | { kind: "external"; src: string; type: string; }; const EXTERNAL_SCRIPT_LOAD_TIMEOUT_MS = 8000; const BARE_RELATIVE_PATH_RE = /^(?![a-zA-Z][a-zA-Z\d+\-.]*:)(?!\/\/)(?!\/)(?!\.\.?\/).+/; function uniqueCompositionId(baseId: string, index: number): string { return `${baseId}__hf${index}`; } const waitForExternalScriptLoad = ( scriptEl: HTMLScriptElement, ): Promise<{ status: "load" | "error" | "timeout"; elapsedMs: number }> => new Promise((resolve) => { let settled = false; const startedAt = Date.now(); let timeoutId: number | null = null; const settle = (status: "load" | "error" | "timeout") => { if (settled) return; settled = true; if (timeoutId != null) { window.clearTimeout(timeoutId); } resolve({ status, elapsedMs: Math.max(0, Date.now() - startedAt), }); }; scriptEl.addEventListener("load", () => settle("load"), { once: true }); scriptEl.addEventListener("error", () => settle("error"), { once: true }); timeoutId = window.setTimeout(() => settle("timeout"), EXTERNAL_SCRIPT_LOAD_TIMEOUT_MS); }); function resetCompositionHost(host: Element) { while (host.firstChild) { host.removeChild(host.firstChild); } host.textContent = ""; } const FLATTENED_INNER_ROOT_STRIP_ATTRS = [ "data-composition-id", "data-composition-file", "data-start", "data-duration", "data-end", "data-track-index", "data-track", "data-composition-src", "data-hf-authored-duration", "data-hf-authored-end", ]; function prepareFlattenedInnerRoot(innerRoot: HTMLElement): HTMLElement { const prepared = document.importNode(innerRoot, true) as HTMLElement; const authoredRootId = prepared.getAttribute("id")?.trim(); for (const attrName of FLATTENED_INNER_ROOT_STRIP_ATTRS) { prepared.removeAttribute(attrName); } if (authoredRootId) { prepared.removeAttribute("id"); prepared.setAttribute("data-hf-authored-id", authoredRootId); } prepared.setAttribute("data-hf-inner-root", "true"); const w = prepared.getAttribute("data-width"); const h = prepared.getAttribute("data-height"); prepared.style.width = w ? `${w}px` : "100%"; prepared.style.height = h ? `${h}px` : "100%"; return prepared; } function resolveScriptSourceUrl(scriptSrc: string, compositionUrl: URL | null): string { const trimmedSrc = scriptSrc.trim(); if (!trimmedSrc) return scriptSrc; try { if (BARE_RELATIVE_PATH_RE.test(trimmedSrc)) { // Composition payloads may use root-relative semantics without a leading slash. return new URL(trimmedSrc, document.baseURI).toString(); } if (compositionUrl) { return new URL(trimmedSrc, compositionUrl).toString(); } return new URL(trimmedSrc, document.baseURI).toString(); } catch { return scriptSrc; } } function parseHostVariableValues(host: Element): Record { const raw = host.getAttribute("data-variable-values"); if (!raw) return {}; let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return {}; } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; return parsed as Record; } type HostCompositionIdentity = { authoredCompositionId: string | null; runtimeCompositionId: string | null; }; function getHostCompositionIdentity(host: Element): HostCompositionIdentity { const currentCompositionId = (host.getAttribute("data-composition-id") || "").trim() || null; const authoredCompositionId = (host.getAttribute("data-hf-original-composition-id") || currentCompositionId || "").trim() || null; return { authoredCompositionId, runtimeCompositionId: currentCompositionId, }; } function countAuthoredCompositionIds(hosts: Element[]): Map { const hostCountsByCompositionId = new Map(); for (const host of hosts) { const compId = getHostCompositionIdentity(host).authoredCompositionId || ""; if (!compId) continue; hostCountsByCompositionId.set(compId, (hostCountsByCompositionId.get(compId) || 0) + 1); } return hostCountsByCompositionId; } function hasMatchingInlineTemplate(host: Element): boolean { const authoredCompositionId = getHostCompositionIdentity(host).authoredCompositionId; if (!authoredCompositionId) return false; return !!document.querySelector(`template#${CSS.escape(authoredCompositionId)}-template`); } function isMountedInlineCompositionHost(host: Element): boolean { return !!host.querySelector('[data-hf-inner-root="true"]'); } function shouldAssignRuntimeCompositionId(host: Element): boolean { if (host.hasAttribute("data-composition-src")) return true; if (!hasMatchingInlineTemplate(host)) return false; if (host.children.length === 0) return true; if (host.hasAttribute("data-hf-original-composition-id")) return true; return isMountedInlineCompositionHost(host); } function getTrackedCompositionHosts(): Element[] { const hosts = Array.from( document.querySelectorAll("[data-composition-src], [data-composition-id]"), ); return hosts.filter((host) => { if (host.hasAttribute("data-composition-src")) return true; return hasMatchingInlineTemplate(host); }); } function cleanupDetachedScopedVariables() { const byComp = window.__hfVariablesByComp; if (!byComp) return; const activeRuntimeCompositionIds = new Set( getTrackedCompositionHosts() .map((host) => getHostCompositionIdentity(host).runtimeCompositionId) .filter((compositionId): compositionId is string => !!compositionId), ); for (const runtimeCompositionId of Object.keys(byComp)) { if (!activeRuntimeCompositionIds.has(runtimeCompositionId)) { delete byComp[runtimeCompositionId]; } } } function assignRuntimeCompositionIds( hosts: Element[], hostCountsByCompositionId: Map = countAuthoredCompositionIds(hosts), ): Map { const hostInstanceByCompositionId = new Map(); const hostIdentityByElement = new Map(); for (const host of hosts) { const { authoredCompositionId, runtimeCompositionId: previousRuntimeCompositionId } = getHostCompositionIdentity(host); const shouldAssign = shouldAssignRuntimeCompositionId(host); if (!authoredCompositionId) { hostIdentityByElement.set(host, { authoredCompositionId: null, runtimeCompositionId: previousRuntimeCompositionId, }); continue; } const duplicateInstance = (hostCountsByCompositionId.get(authoredCompositionId) || 0) > 1; let runtimeCompositionId = previousRuntimeCompositionId || authoredCompositionId; if (shouldAssign) { const instanceIndex = duplicateInstance ? (hostInstanceByCompositionId.get(authoredCompositionId) || 0) + 1 : 0; if (duplicateInstance) { hostInstanceByCompositionId.set(authoredCompositionId, instanceIndex); } runtimeCompositionId = duplicateInstance ? uniqueCompositionId(authoredCompositionId, instanceIndex) : authoredCompositionId; if (duplicateInstance) { host.setAttribute("data-hf-original-composition-id", authoredCompositionId); } else { host.removeAttribute("data-hf-original-composition-id"); } host.setAttribute("data-composition-id", runtimeCompositionId); if ( previousRuntimeCompositionId && previousRuntimeCompositionId !== runtimeCompositionId && window.__hfVariablesByComp ) { delete window.__hfVariablesByComp[previousRuntimeCompositionId]; } } hostIdentityByElement.set(host, { authoredCompositionId, runtimeCompositionId, }); } return hostIdentityByElement; } async function mountCompositionContent(params: { host: Element; authoredCompositionId: string | null; runtimeCompositionId: string | null; hostCompositionSrc: string; sourceNode: ParentNode; hasTemplate: boolean; fallbackBodyInnerHtml: string; compositionUrl: URL | null; injectedStyles: HTMLStyleElement[]; injectedScripts: HTMLScriptElement[]; parseDimensionPx: (value: string | null) => string | null; /** Extra