import { useEffect, useRef, useState } from "react";
// Which languages get a live sandboxed preview (RenderBlock) instead of a plain
// syntax block (CodeBlock), and how each becomes a document body, lives in
// ./preview/languages.js. A language like `js` is deliberately absent —
// auto-executing bare script isn't this feature's job (see RenderBlock's doc
// comment for the sandboxing model).
import { PREVIEW_LANGS, RENDERABLE_LANGS } from "./preview/languages.js";
// Parse content into an array of {type, value, lang, streaming} blocks.
// Handles:
// - ```lang\ncode``` (well-formed)
// - ```code``` (no language, no newline — model formatting bug)
// - ```lang\ncode (unclosed — streaming in progress)
function parseBlocks(content) {
const blocks = [];
let i = 0;
while (i < content.length) {
const fenceStart = content.indexOf("```", i);
if (fenceStart === -1) {
if (i < content.length) blocks.push({ type: "text", value: content.slice(i) });
break;
}
if (fenceStart > i) blocks.push({ type: "text", value: content.slice(i, fenceStart) });
let j = fenceStart + 3;
let lang = "";
// Language specifier is valid only when word-chars are followed by a newline.
// If there's no newline (e.g. ```pythonprint(...)) treat everything as code.
const langMatch = content.slice(j).match(/^(\w+)(\r?\n)/);
if (langMatch) {
lang = langMatch[1];
j += langMatch[0].length;
} else if (/^\r?\n/.test(content.slice(j))) {
j += content[j] === "\r" ? 2 : 1;
}
// else: no newline — leave j as-is, lang stays ""
const closeIdx = content.indexOf("```", j);
if (closeIdx === -1) {
blocks.push({ type: "code", lang, value: content.slice(j), streaming: true });
i = content.length;
} else {
blocks.push({ type: "code", lang, value: content.slice(j, closeIdx).replace(/\n$/, ""), streaming: false });
i = closeIdx + 3;
}
}
return blocks;
}
export function Markdown({ content }) {
if (!content) return null;
const blocks = parseBlocks(content);
return (
);
}
// Content-Security-Policy for the rendered preview. Together with the iframe's
// `sandbox` attribute below, this is the entire trust boundary for model-
// authored HTML/SVG, so it stays conservative rather than convenient:
// - script-src/style-src 'unsafe-inline' inline ` below is safe unescaped because this module is emitted as an
// external .js asset - it is never inlined into index.html, where the HTML
// parser would end the surrounding script tag early.
//
// postMessage is the one channel an opaque-origin sandboxed frame still has to
// the parent, and this is the entire protocol over it: one message shape,
// outbound only, carrying a content height and an error string. Nothing flows
// the other way. The parent treats both fields as untrusted data - the height
// is clamped and the message is rendered as text, never as markup - because
// they were produced by the same code the sandbox exists to contain.
//
// Without this the frame is silent: a preview whose script throws just renders
// blank, which is why the server-side validator in synapse/tools.py has to
// guess at runtime failures it can't observe.
const _PREVIEW_BOOTSTRAP = ``;
// Substituted with the real line offset once the document is assembled and its
// shell can be measured. Sits on one line so replacing it can't shift any.
const _OFFSET_TOKEN = "__PREVIEW_LINE_OFFSET__";
/**
* Build the sandboxed document for a fence. Returns {doc, error}: a language
* whose source doesn't parse (JSX, today) has no document to show, and the
* caller renders the message instead of a frame.
*
* The shell - charset, CSP, bootstrap - is identical for every language; only
* the body differs, so only that part goes through the registry. Nothing about
* the sandboxing is per-language and shouldn't be: SVG can carry ` +
_PREVIEW_BOOTSTRAP +
"";
// Lines of shell above the user's own code: the document head, plus whatever
// the language puts in the body ahead of it (the Preact build, for JSX).
const offset = (head.match(/\n/g) || []).length + body.userOffset;
return {
doc: (head + body.html + "