import { z } from "zod"; import type { BlockMdxConfig } from "../types.js"; /** * Pure (React-free) part of the standard HTML / Tailwind block: its data schema * and MDX round-trip config. Shared by the React spec (`html.tsx`) and any * server-side / agent registry (e.g. the plan app's `plan-block-registry.ts`), * so importing it into a server module never pulls React in. * * This is the registry form of the plan `custom-html` block. The schema MUST * stay data-compatible with the `custom-html` branch of the plan * `planBlockSchema` (bounded `html`, optional bounded `css`, optional trimmed * `caption`, all rejecting full-document / script markup) so a registered block * still validates through the app's generic `update-block` re-validation. The * MDX `tag` (`HtmlBlock`) + flat `html`/`css`/`caption` attribute shape MUST * match the legacy `` encoding * (`plan-mdx.ts` `serializeBlock`/`parseBlock`) so stored `.mdx` round-trips * byte-compatibly. */ export interface HtmlBlockData { /** Bounded HTML fragment (no html/head/body/script/style document markup). */ html: string; /** Optional bounded CSS, scoped into the sandboxed iframe. */ css?: string; /** Optional short caption rendered under the rendered fragment. */ caption?: string; } /** * Rejects full HTML documents, executable/structural tags, and JS/data URLs. * Kept byte-identical to the plan `noFullHtmlDocument` refine so the registry * schema accepts exactly what `planBlockSchema` accepts (and nothing it would * reject). Defense in depth — the block also renders inside a sandboxed iframe. */ const unsafeHtmlPattern = /(?:/]|\b(?:javascript|data:text\/html)\s*:|\bsrcdoc\s*=|\bon[a-z][\w:-]*\s*=)/i; const noFullHtmlDocument = (value: string) => !unsafeHtmlPattern.test(value); export const htmlSchema = z .object({ html: z.string().max(100_000).refine(noFullHtmlDocument, { message: "Custom HTML blocks must be bounded fragments without html/head/body/script/style tags.", }), css: z .string() .max(50_000) .refine(noFullHtmlDocument, { message: "Custom CSS blocks must not include document or script tags.", }) .optional(), caption: z.string().trim().max(400).optional(), }) .strict() as unknown as z.ZodType; /** * MDX config: `html`, `css`, and `caption` are flat attributes (no children) — * exactly the legacy `` self-closing form. * Insertion order of `toAttrs` is the on-disk attribute order, so it stays * `html` → `css` → `caption` to match `plan-mdx.ts:serializeBlock`. `fromAttrs` * mirrors the legacy parse defaults (`html ?? ""`, `css`/`caption` undefined * when absent). */ export const htmlMdx: BlockMdxConfig = { tag: "HtmlBlock", toAttrs: (data) => ({ html: data.html, css: data.css, caption: data.caption, }), fromAttrs: (attrs) => ({ html: attrs.string("html") ?? "", css: attrs.string("css"), caption: attrs.string("caption"), }), };