diff --git a/.agents/skills/dg-create-roam-render-html-code/SKILL.md b/.agents/skills/dg-create-roam-render-html-code/SKILL.md new file mode 100644 index 000000000..0cfb235c3 --- /dev/null +++ b/.agents/skills/dg-create-roam-render-html-code/SKILL.md @@ -0,0 +1,58 @@ +--- +name: dg-create-roam-render-html-code +description: Wrap a completed HTML artifact in a Roam-compatible React iframe renderer and save it as a local code file. Use when the user asks to turn HTML output into Roam render HTML code. +--- + +# DG Create Roam Render HTML Code + +Take the completed HTML from the current request or from the HTML file the user identifies. Create one local file containing that HTML inside the following JavaScript wrapper. Default to a `.js` file in the current working directory named `-roam-render.js`; honor any filename, extension, or output path the user supplies. + +Derive concise, descriptive names from the HTML's subject: + +- Use an uppercase `UPPER_SNAKE_CASE` constant ending in `_HTML`. +- Use a lower camel case function name that describes the rendered artifact. +- Use a short, human-readable iframe title. + +```js +const ARTIFACT_HTML = "__HTML_SOURCE__"; + +function usefulNamedFunction(props) { + const React = window.React; + if (!React) return null; + + return React.createElement("iframe", { + title: "Useful named title", + srcDoc: ARTIFACT_HTML, + sandbox: "allow-scripts", + loading: "eager", + style: { + width: "100%", + height: "calc(100vh - 140px)", + minHeight: "720px", + display: "block", + border: "1px solid #d8d2c5", + borderRadius: "10px", + background: "#f4f0e6", + }, + }); +} +``` + +Replace the entire `"__HTML_SOURCE__"` string literal with a JSON-serialized JavaScript string containing the complete HTML. Do not summarize, redesign, minify, omit, trim, or normalize line endings in the HTML. Preserve the wrapper's styles unless the user explicitly requests changes. + +Generate the literal mechanically rather than escaping template delimiters by hand. Given the original `html` string and the wrapper above as `wrapper`, use: + +```js +const htmlLiteral = JSON.stringify(html) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029"); +const renderer = wrapper.replace('"__HTML_SOURCE__"', () => htmlLiteral); +``` + +The replacement callback preserves replacement-like text such as `$&` in the HTML. JSON serialization handles quotes, backslashes (including odd runs before backticks or `${` and a trailing backslash), control characters, and line endings without executing embedded expressions. Save `renderer` as the standalone `.js` file; do not wrap it in an HTML script tag. + +Keep `sandbox: "allow-scripts"` for interactive artifacts. Omit `allow-same-origin` so embedded scripts cannot access Roam's parent DOM, authenticated storage, or APIs. For static HTML, use `sandbox: ""`. Add other sandbox permissions only for a specifically required capability; do not remove the sandbox or add `allow-same-origin` to restore parent access. Features requiring Roam APIs need a separately designed integration, not this iframe wrapper. + +Before delivering a renderer, check its JavaScript syntax and verify that its evaluated `srcDoc` exactly equals the original HTML. Include backticks, `${`, quotes, dollar replacement patterns, CRLF, Unicode, and odd/even backslash runs before template delimiters and at end of input in regression checks. Run `node --test scripts/renderer.test.mjs` from this skill directory when changing this guidance. + +After writing the file, report its path. Do not paste the complete generated file into the response unless the user asks. diff --git a/.agents/skills/dg-create-roam-render-html-code/agents/openai.yaml b/.agents/skills/dg-create-roam-render-html-code/agents/openai.yaml new file mode 100644 index 000000000..bc047d990 --- /dev/null +++ b/.agents/skills/dg-create-roam-render-html-code/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "DG Create Roam Render HTML Code" + short_description: "Wrap HTML in a Roam iframe renderer" + default_prompt: "Use $dg-create-roam-render-html-code to save this HTML as Roam render code." diff --git a/.agents/skills/dg-create-roam-render-html-code/scripts/renderer.test.mjs b/.agents/skills/dg-create-roam-render-html-code/scripts/renderer.test.mjs new file mode 100644 index 000000000..c003c80f2 --- /dev/null +++ b/.agents/skills/dg-create-roam-render-html-code/scripts/renderer.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import vm from "node:vm"; + +const skill = readFileSync(new URL("../SKILL.md", import.meta.url), "utf8"); +const blocks = [...skill.matchAll(/```js\r?\n([\s\S]*?)```/g)].map( + (match) => match[1], +); +const [wrapper, serializer] = blocks; + +const render = (html) => { + const source = vm.runInNewContext(`${serializer}\nrenderer`, { + html, + wrapper, + }); + const script = new vm.Script(`${source}\nusefulNamedFunction({})`); + return script.runInNewContext({ + window: { React: { createElement: (tag, props) => ({ tag, props }) } }, + }); +}; + +test("documented serialization preserves HTML exactly without evaluating it", () => { + const inputs = [ + "", + '

Hello

\r\n', + "` ${throwIfEvaluated()} $& $$ $' $`", + "Unicode: 🦉 \u2028 \u2029 \u0000\t\n", + "", + ]; + for (let count = 0; count <= 6; count++) { + const slashes = "\\".repeat(count); + for (const suffix of ["`", "${throwIfEvaluated()}", ""]) { + inputs.push(`

prefix

${slashes}${suffix}`); + } + } + for (const html of inputs) { + assert.equal(render(html).props.srcDoc, html, JSON.stringify(html)); + } +}); + +test("documented iframe permits scripts without same-origin or other privileges", () => { + const result = render(""); + assert.equal(result.tag, "iframe"); + assert.equal(result.props.sandbox, "allow-scripts"); +}); diff --git a/package.json b/package.json index eb019f0d4..5ef22a50b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "build": "turbo build", "ci:validate": "turbo check-types && turbo run test:unit --ui stream", + "test:unit": "node --test .agents/skills/dg-create-roam-render-html-code/scripts/renderer.test.mjs", "dev": "turbo dev", "lint": "turbo lint", "deploy": "turbo deploy", diff --git a/turbo.json b/turbo.json index 1ed0c2794..e25f91045 100644 --- a/turbo.json +++ b/turbo.json @@ -78,6 +78,13 @@ "test:unit": { "outputs": [] }, + "//#test:unit": { + "inputs": [ + "package.json", + ".agents/skills/dg-create-roam-render-html-code/**" + ], + "outputs": [] + }, "dev": { "passThroughEnv": ["OBSIDIAN_PLUGIN_PATH"], "cache": false,