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
+ });
+});