/* * languages.js — what the render window can preview, one entry per language. * * Each entry turns a fence's contents into the of the sandboxed frame: * * toBody(value) -> { html, userOffset } * * `userOffset` is how many lines of that body come before the user's own code. * The frame reports runtime errors by line number and those numbers are * document-relative, so without this an error in a JSX component would be * reported at some line deep inside the inlined Preact build. The caller adds * the lines of document shell above the body and hands the total to the * bootstrap, which subtracts it before reporting. * * A `toBody` may throw: JSX that doesn't parse has no preview to show. The * caller catches and shows the message in place of the frame. * * The backend keeps a matching registry (PREVIEW_LANGS in synapse/tools.py) * that says how each language is *validated* rather than rendered. Neither * depends on the other at runtime; tests/test_tools.py asserts the key sets * stay equal. */ import { transform } from "./jsx-transform.js"; import { PREACT_RUNTIME } from "./runtime.js"; const countNewlines = (text) => (text.match(/\n/g) || []).length; /** Markup languages: the fence is already a document body. */ const markup = (value) => ({ html: value, userOffset: 0 }); /** * Pick what to mount. An explicit default export wins, then a component named * App, then the last capitalized declaration - models tend to define helpers * first and the thing they were asked for last. */ function mountTarget({ defaultExport, components }) { if (defaultExport) return defaultExport; if (components.includes("App")) return "App"; if (components.length) return components[components.length - 1]; throw new Error( "No component found to render. Name one `App`, or `export default` it.", ); } /** * One line of stubs for every binding an import would have provided. * * Imports are dropped — there is no module loader in the sandbox — so a name * that came from a package is simply missing, and the first use of it reports * "useInView is not defined" at a line far from the import that explains it. * Each stub throws with the module name instead, and `||` means anything the * runtime already provides (useState and friends) keeps its real implementation. */ function importStubs(imports) { const names = new Map(); for (const { names: bound, module } of imports || []) { for (const name of bound) if (!names.has(name)) names.set(name, module); } if (!names.size) return ""; const lines = [...names].map(([name, module]) => { const why = JSON.stringify( `${name} came from ${module ? `"${module}"` : "an import"}, which the preview ` + "cannot load — it has no module loader and no network. Inline what you need, " + "or use the built-in hooks, which are already in scope.", ); return `window[${JSON.stringify(name)}] = window[${JSON.stringify(name)}] ` + `|| function () { throw new Error(${why}); };`; }); return `\n`; } function jsxBody(value) { const result = transform(value); const target = mountTarget(result); const head = '
\n' + `\n` + importStubs(result.imports) + "", userOffset: countNewlines(head), }; } export const PREVIEW_LANGS = { html: { toBody: markup }, svg: { toBody: markup }, jsx: { toBody: jsxBody }, tsx: { toBody: jsxBody }, }; export const RENDERABLE_LANGS = new Set(Object.keys(PREVIEW_LANGS));