From 09d2d2db333997596e2b53d34e51c0034cfd9a72 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 15 Jul 2026 10:45:50 +0000 Subject: [PATCH] fix(export): rendered inline markdown in list items - Parsed nested marked text tokens through the inline parser. - Added browser-template regression coverage for ordered, unordered, and nested lists. Fixes #5567 --- bun.lock | 1 + packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 3 +- .../coding-agent/src/export/html/template.js | 2 +- .../test/export-html-markdown.test.ts | 85 +++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/export-html-markdown.test.ts diff --git a/bun.lock b/bun.lock index 38a267324..a9331bb0d 100644 --- a/bun.lock +++ b/bun.lock @@ -114,6 +114,7 @@ }, "devDependencies": { "@types/bun": "catalog:", + "marked": "catalog:", }, "optionalDependencies": { "@huggingface/transformers": "catalog:", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 71e86facb..65b010975 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). - Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd. - Fixed prewalk repeatedly continuing after a bash-only task such as `commit` had already completed ([#5551](https://github.com/can1357/oh-my-pi/issues/5551)). +- Fixed `/share` and `/export` web views rendering inline Markdown inside list items as literal text ([#5567](https://github.com/can1357/oh-my-pi/issues/5567)). ## [16.5.2] - 2026-07-14 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index e5ff63875..6bbcef8d7 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -92,7 +92,8 @@ "sherpa-onnx-node": "1.13.2" }, "devDependencies": { - "@types/bun": "catalog:" + "@types/bun": "catalog:", + "marked": "catalog:" }, "engines": { "bun": ">=1.3.14" diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js index 68fa096ab..5fe4d9be2 100644 --- a/packages/coding-agent/src/export/html/template.js +++ b/packages/coding-agent/src/export/html/template.js @@ -1386,7 +1386,7 @@ }, // Text content: escape HTML tags text(token) { - return escapeHtmlTags(escapeHtml(token.text)); + return token.tokens ? this.parser.parseInline(token.tokens) : escapeHtmlTags(escapeHtml(token.text)); }, // Inline code: escape HTML codespan(token) { diff --git a/packages/coding-agent/test/export-html-markdown.test.ts b/packages/coding-agent/test/export-html-markdown.test.ts new file mode 100644 index 000000000..627a0b228 --- /dev/null +++ b/packages/coding-agent/test/export-html-markdown.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from "bun:test"; +import * as vm from "node:vm"; +import { parseHTML } from "linkedom"; +import { Marked } from "marked"; + +const [templateHtml, templateJs] = await Promise.all([ + Bun.file(new URL("../src/export/html/template.html", import.meta.url)).text(), + Bun.file(new URL("../src/export/html/template.js", import.meta.url)).text(), +]); + +function renderMarkdown(source: string): Element { + const { document, window } = parseHTML(templateHtml); + const session = { + header: { + type: "session", + version: 3, + id: "markdown-test", + timestamp: "2026-01-01T00:00:00.000Z", + cwd: "/tmp", + }, + entries: [ + { + type: "message", + id: "message-1", + parentId: null, + timestamp: "2026-01-01T00:00:00.000Z", + message: { + role: "user", + content: source, + timestamp: 0, + }, + }, + ], + leafId: "message-1", + }; + + const sessionData = document.getElementById("session-data"); + if (!sessionData) throw new Error("Export template is missing session data"); + sessionData.textContent = Buffer.from(JSON.stringify(session)).toBase64(); + Object.defineProperty(window, "location", { + value: new URL("https://example.test/export.html"), + configurable: true, + }); + Object.defineProperty(window, "matchMedia", { + value: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }), + configurable: true, + }); + + const context = vm.createContext({ + window, + document, + marked: new Marked(), + hljs: { + getLanguage: () => false, + highlight: () => ({ value: "" }), + highlightAuto: () => ({ value: "" }), + }, + URL, + URLSearchParams, + TextDecoder, + Uint8Array, + atob, + navigator: { clipboard: null }, + localStorage: { getItem: () => null, setItem() {} }, + setTimeout: () => 0, + clearTimeout() {}, + }); + vm.runInContext(templateJs, context); + + const rendered = document.querySelector(".markdown-content"); + if (!rendered) throw new Error("Export viewer did not render Markdown content"); + return rendered; +} + +describe("HTML export Markdown", () => { + test("renders inline Markdown in ordered, unordered, and nested list items", () => { + const rendered = renderMarkdown("**outside**\n\n- **bold** and *italic* and `code`\n 1. **nested**"); + + expect(rendered.querySelector("p strong")?.textContent).toBe("outside"); + expect(rendered.querySelector("ul > li > strong")?.textContent).toBe("bold"); + expect(rendered.querySelector("ul > li > em")?.textContent).toBe("italic"); + expect(rendered.querySelector("ul > li > code")?.textContent).toBe("code"); + expect(rendered.querySelector("ol > li > strong")?.textContent).toBe("nested"); + }); +});