From 15a1d96ff93cdd24054b3a5a70d05d255ee7fb11 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 6 Sep 2026 20:46:37 -0600 Subject: [PATCH 1/3] Add DG skill for wrapping HTML in Roam render code --- .../dg-create-roam-render-html-code/SKILL.md | 46 +++++++++++++++++++ .../agents/openai.yaml | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 .agents/skills/dg-create-roam-render-html-code/SKILL.md create mode 100644 .agents/skills/dg-create-roam-render-html-code/agents/openai.yaml 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..d45d182ff --- /dev/null +++ b/.agents/skills/dg-create-roam-render-html-code/SKILL.md @@ -0,0 +1,46 @@ +--- +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 USEFUL_NAMED_VARIABLE = String.raw` + +`; + +function usefulNamedFunction(props) { + const React = window.React; + if (!React) return null; + + return React.createElement("iframe", { + title: "Useful named title", + srcDoc: USEFUL_NAMED_VARIABLE, + loading: "eager", + style: { + width: "100%", + height: "calc(100vh - 140px)", + minHeight: "720px", + display: "block", + border: "1px solid #d8d2c5", + borderRadius: "10px", + background: "#f4f0e6", + }, + }); +} +``` + +Replace the placeholder comment with the complete HTML; do not summarize, redesign, minify, or omit any of it. Preserve the wrapper's iframe options and styles unless the user explicitly requests changes. + +Ensure the resulting JavaScript is syntactically valid without changing the HTML produced at runtime. A tagged `String.raw` template preserves escape characters, so do not escape embedded backticks or `${` sequences with a backslash. If either sequence occurs in the HTML, encode it with a template interpolation that evaluates to the original literal text, such as `${"`"}`for a backtick or`${"${"}`for`${`. + +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." From 2d98425d6555fbae36d87fa8577531287a2ed701 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 6 Sep 2026 21:06:09 -0600 Subject: [PATCH 2/3] Fix HTML serialization and sandbox Roam renderer iframe --- .../dg-create-roam-render-html-code/SKILL.md | 24 +++++++--- .../scripts/renderer.test.mjs | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .agents/skills/dg-create-roam-render-html-code/scripts/renderer.test.mjs diff --git a/.agents/skills/dg-create-roam-render-html-code/SKILL.md b/.agents/skills/dg-create-roam-render-html-code/SKILL.md index d45d182ff..0cfb235c3 100644 --- a/.agents/skills/dg-create-roam-render-html-code/SKILL.md +++ b/.agents/skills/dg-create-roam-render-html-code/SKILL.md @@ -14,9 +14,7 @@ Derive concise, descriptive names from the HTML's subject: - Use a short, human-readable iframe title. ```js -const USEFUL_NAMED_VARIABLE = String.raw` - -`; +const ARTIFACT_HTML = "__HTML_SOURCE__"; function usefulNamedFunction(props) { const React = window.React; @@ -24,7 +22,8 @@ function usefulNamedFunction(props) { return React.createElement("iframe", { title: "Useful named title", - srcDoc: USEFUL_NAMED_VARIABLE, + srcDoc: ARTIFACT_HTML, + sandbox: "allow-scripts", loading: "eager", style: { width: "100%", @@ -39,8 +38,21 @@ function usefulNamedFunction(props) { } ``` -Replace the placeholder comment with the complete HTML; do not summarize, redesign, minify, or omit any of it. Preserve the wrapper's iframe options and styles unless the user explicitly requests changes. +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. -Ensure the resulting JavaScript is syntactically valid without changing the HTML produced at runtime. A tagged `String.raw` template preserves escape characters, so do not escape embedded backticks or `${` sequences with a backslash. If either sequence occurs in the HTML, encode it with a template interpolation that evaluates to the original literal text, such as `${"`"}`for a backtick or`${"${"}`for`${`. +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/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"); +}); From 195179106e391fd78964e5ce2a2209524885c90c Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 7 Sep 2026 01:27:31 -0600 Subject: [PATCH 3/3] Run Roam renderer regression tests in root CI validation --- package.json | 1 + turbo.json | 7 +++++++ 2 files changed, 8 insertions(+) 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,