/** * Page fragment collection and rendering * * Collects raw markup / script contributions from trusted plugins via * the page:fragments hook. Sandboxed plugins are never invoked. */ import type { PageFragmentContribution, PagePlacement } from "../plugins/types.js"; import { escapeHtmlAttr } from "./metadata.js"; /** Escape sequences that would break out of a script tag */ const SCRIPT_CLOSE_RE = /<\//g; // ── Dedupe and filter ─────────────────────────────────────────── /** * Filter contributions to a specific placement and deduplicate. * - Contributions with the same `key + placement` are deduped (first wins). * - External scripts with the same `src + placement` are deduped. */ export function resolveFragments( contributions: PageFragmentContribution[], placement: PagePlacement, ): PageFragmentContribution[] { const filtered = contributions.filter((c) => c.placement === placement); const seen = new Set(); const result: PageFragmentContribution[] = []; for (const c of filtered) { // Key-based dedupe if (c.key) { const dedupeKey = `key:${c.key}`; if (seen.has(dedupeKey)) continue; seen.add(dedupeKey); } else if (c.kind === "external-script") { const dedupeKey = `src:${c.src}`; if (seen.has(dedupeKey)) continue; seen.add(dedupeKey); } result.push(c); } return result; } // ── HTML rendering ────────────────────────────────────────────── const EVENT_HANDLER_RE = /^on/i; function renderAttributes(attrs: Record): string { return Object.entries(attrs) .filter(([k]) => !EVENT_HANDLER_RE.test(k)) .map(([k, v]) => ` ${escapeHtmlAttr(k)}="${escapeHtmlAttr(v)}"`) .join(""); } /** Render a single fragment contribution to HTML */ function renderFragment(c: PageFragmentContribution): string { switch (c.kind) { case "external-script": { let tag = ``; return tag; } case "html": return c.html; } } /** Render a list of fragment contributions to an HTML string */ export function renderFragments( contributions: PageFragmentContribution[], placement: PagePlacement, ): string { const resolved = resolveFragments(contributions, placement); return resolved.map(renderFragment).join("\n"); }