/**
* Srcdoc injection builder for the HTML viewer.
*
* Product rule: arbitrary HTML must render exactly as it would in a plain
* browser tab. The viewer never writes into the document's namespace — no bare
* CSS custom properties, no classes on the author's root, no `color-scheme`,
* no styling of author elements. Host theme tokens are pushed under the
* viewer-owned `--pn-*` prefix, which the annotation CSS reads.
*
* Documents that WANT to follow the host theme (e.g. Plannotator-generated
* artifacts) opt in with ``,
* which re-enables the bare-token push, the `light` class on their root, and
* `color-scheme` sync — for that document only.
*
* Pure string logic (no DOM) so the rendering-neutrality contract is unit-testable.
*/
import { ANNOTATION_HIGHLIGHT_CSS, BRIDGE_SCRIPT } from "./bridge-script";
export const THEME_TOKENS = [
"--background",
"--foreground",
"--card",
"--card-foreground",
"--primary",
"--primary-foreground",
"--secondary",
"--secondary-foreground",
"--muted",
"--muted-foreground",
"--accent",
"--accent-foreground",
"--destructive",
"--destructive-foreground",
"--success",
"--success-foreground",
"--warning",
"--warning-foreground",
"--border",
"--input",
"--ring",
"--code-bg",
"--focus-highlight",
"--font-sans",
"--font-mono",
"--radius",
] as const;
/** Viewer-owned namespace for properties injected into the document. */
export const PN_TOKEN_PREFIX = "--pn-";
/**
* Version-diff highlights. htmlDiff tags the / it generates with
* this class so author-written / markup is never restyled.
*/
export const DIFF_HIGHLIGHT_CSS =
"ins.plannotator-diff{background:#e6ffec;color:#0a7d33;text-decoration:none;border-radius:2px;box-shadow:0 0 0 1px #abf2bc inset}" +
"del.plannotator-diff{background:#ffebe9;color:#b31d28;text-decoration:line-through;border-radius:2px;box-shadow:0 0 0 1px #ffc1bc inset}";
/**
* True when the document opts in to following the host theme via
* `` (attribute order/quoting agnostic).
*/
export function hasHostThemeOptIn(rawHtml: string): boolean {
const metas = rawHtml.match(/]*>/gi);
if (!metas) return false;
return metas.some(
(tag) =>
/\bname\s*=\s*["']?plannotator-theme["']?/i.test(tag) &&
/\bcontent\s*=\s*["']?host["']?/i.test(tag),
);
}
/**
* Build the theme properties to write into the document. Bare host token names
* (`--muted`, `--background`, …) collide with author variables, so they are
* remapped to `--pn-*`; the originals ride along only for host-theme documents.
*/
export function buildThemeTokenPayload(
tokens: Record,
hostTheme: boolean,
): Record {
const payload: Record = {};
for (const [key, val] of Object.entries(tokens)) {
payload[PN_TOKEN_PREFIX + key.slice(2)] = val;
if (hostTheme) payload[key] = val;
}
return payload;
}
export interface SrcdocInjectionOptions {
/** Host theme tokens, keyed by bare name (as read from the host root). */
tokens: Record;
/** Whether the host is currently in its light theme. */
isLight: boolean;
/** Document opted in to host theming (see {@link hasHostThemeOptIn}). */
hostTheme: boolean;
/** The version-diff view is showing (rawHtml is htmlDiff output). */
diffActive: boolean;
/**
* Load the bridge through a classic ``;
}
if (!BRIDGE_SCRIPT) {
// Only reachable when a host aliased `./bridge-script` to the generated
// `bridge-script.lite` module (which stubs the inline literal) and then
// rendered an HtmlViewer without `bridgeScriptUrl`: an empty inline
// script would be a silently dead surface, so fail loudly instead.
throw new Error(
"@plannotator/ui HtmlViewer: the inline bridge script is stubbed out "
+ "(bridge-script.lite alias) but no bridgeScriptUrl was passed.",
);
}
return ``;
}
/** The `${buildBridgeScriptTag(bridgeScriptUrl)}`;
}
/**
* A document-authored CSP `` tag (e.g. `default-src 'none'` in
* Plannotator's own portable guided-review exports) blocks the inline bridge
* script and disables annotation entirely. The iframe `sandbox` attribute is
* the security boundary for the annotate surface; the page's CSP was written
* for its standalone context, so it is removed before injection.
*
* The package itself never adds a CSP `` to the srcdoc document (the
* injection is one `