diff --git a/src/trackChangesModel.ts b/src/trackChangesModel.ts index 17f6d54..43801d0 100644 --- a/src/trackChangesModel.ts +++ b/src/trackChangesModel.ts @@ -145,40 +145,51 @@ function wordMergedMarkdown(beforeRaw: string, afterRaw: string): string { .join(""); } -function safeRender(src: string): string { - try { - return md.render(src); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return `
Could not render this block: ${md.utils.escapeHtml(message)}
`; - } +export interface RenderOptions { + /** Per-block markdown→HTML renderer (test seam). Defaults to the bundled markdown-it. */ + render?: (src: string) => string; } -function renderOp(op: BlockOp): string { +function defaultRender(src: string): string { + return md.render(src); +} + +function chip(message: string): string { + return `
Could not render this block: ${md.utils.escapeHtml(message)}
`; +} + +function renderOp(op: BlockOp, render: (src: string) => string): string { + const safe = (src: string): string => { + try { + return render(src); + } catch (err) { + return chip(err instanceof Error ? err.message : String(err)); + } + }; let cls: string; let inner: string; let badge = ""; switch (op.kind) { case "unchanged": cls = "cw-unchanged"; - inner = safeRender(op.block.raw); + inner = safe(op.block.raw); break; case "added": cls = "cw-added"; - inner = safeRender(op.block.raw); + inner = safe(op.block.raw); if (op.block.type !== "prose") badge = 'added'; break; case "removed": cls = "cw-removed"; - inner = safeRender(op.block.raw); + inner = safe(op.block.raw); break; case "changed": cls = "cw-changed"; if (op.atomic) { - inner = safeRender(op.block.raw); // the NEW block, whole (INV-23) + inner = safe(op.block.raw); // the NEW block, whole (INV-23) badge = 'changed'; } else { - inner = safeRender(wordMergedMarkdown(op.before.raw, op.block.raw)); + inner = safe(wordMergedMarkdown(op.before.raw, op.block.raw)); } break; } @@ -186,6 +197,13 @@ function renderOp(op: BlockOp): string { } /** Pure entry point: annotated HTML body for the preview (INV-22). */ -export function renderTrackChanges(baselineText: string, currentText: string): string { - return diffBlocks(baselineText, currentText).map(renderOp).join("\n"); +export function renderTrackChanges( + baselineText: string, + currentText: string, + opts: RenderOptions = {}, +): string { + const render = opts.render ?? defaultRender; + return diffBlocks(baselineText, currentText) + .map((op) => renderOp(op, render)) + .join("\n"); } diff --git a/test/trackChangesModel.test.ts b/test/trackChangesModel.test.ts index b07bc04..d74414f 100644 --- a/test/trackChangesModel.test.ts +++ b/test/trackChangesModel.test.ts @@ -122,3 +122,18 @@ describe("renderTrackChanges", () => { expect(a).toBe(b); }); }); + +describe("error chip (PUC-6)", () => { + it("a block whose render throws becomes an error chip; the rest still renders", () => { + const throwing = (src: string) => { + if (src.includes("BOOM")) throw new Error("kaboom"); + return `

${src}

`; + }; + const html = renderTrackChanges("Good one.\n\nBOOM here.\n", "Good one.\n\nBOOM here.\n", { + render: throwing, + }); + expect(html).toContain("cw-error"); + expect(html).toContain("kaboom"); + expect(html).toContain("

Good one.

"); // the healthy block still rendered + }); +});