F7: rendered track-changes markdown preview (#21) #23
+33
-15
@@ -145,40 +145,51 @@ function wordMergedMarkdown(beforeRaw: string, afterRaw: string): string {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeRender(src: string): string {
|
export interface RenderOptions {
|
||||||
try {
|
/** Per-block markdown→HTML renderer (test seam). Defaults to the bundled markdown-it. */
|
||||||
return md.render(src);
|
render?: (src: string) => string;
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
return `<div class="cw-error">Could not render this block: ${md.utils.escapeHtml(message)}</div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOp(op: BlockOp): string {
|
function defaultRender(src: string): string {
|
||||||
|
return md.render(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chip(message: string): string {
|
||||||
|
return `<div class="cw-error">Could not render this block: ${md.utils.escapeHtml(message)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cls: string;
|
||||||
let inner: string;
|
let inner: string;
|
||||||
let badge = "";
|
let badge = "";
|
||||||
switch (op.kind) {
|
switch (op.kind) {
|
||||||
case "unchanged":
|
case "unchanged":
|
||||||
cls = "cw-unchanged";
|
cls = "cw-unchanged";
|
||||||
inner = safeRender(op.block.raw);
|
inner = safe(op.block.raw);
|
||||||
break;
|
break;
|
||||||
case "added":
|
case "added":
|
||||||
cls = "cw-added";
|
cls = "cw-added";
|
||||||
inner = safeRender(op.block.raw);
|
inner = safe(op.block.raw);
|
||||||
if (op.block.type !== "prose") badge = '<span class="cw-badge">added</span>';
|
if (op.block.type !== "prose") badge = '<span class="cw-badge">added</span>';
|
||||||
break;
|
break;
|
||||||
case "removed":
|
case "removed":
|
||||||
cls = "cw-removed";
|
cls = "cw-removed";
|
||||||
inner = safeRender(op.block.raw);
|
inner = safe(op.block.raw);
|
||||||
break;
|
break;
|
||||||
case "changed":
|
case "changed":
|
||||||
cls = "cw-changed";
|
cls = "cw-changed";
|
||||||
if (op.atomic) {
|
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 = '<span class="cw-badge">changed</span>';
|
badge = '<span class="cw-badge">changed</span>';
|
||||||
} else {
|
} else {
|
||||||
inner = safeRender(wordMergedMarkdown(op.before.raw, op.block.raw));
|
inner = safe(wordMergedMarkdown(op.before.raw, op.block.raw));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -186,6 +197,13 @@ function renderOp(op: BlockOp): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Pure entry point: annotated HTML body for the preview (INV-22). */
|
/** Pure entry point: annotated HTML body for the preview (INV-22). */
|
||||||
export function renderTrackChanges(baselineText: string, currentText: string): string {
|
export function renderTrackChanges(
|
||||||
return diffBlocks(baselineText, currentText).map(renderOp).join("\n");
|
baselineText: string,
|
||||||
|
currentText: string,
|
||||||
|
opts: RenderOptions = {},
|
||||||
|
): string {
|
||||||
|
const render = opts.render ?? defaultRender;
|
||||||
|
return diffBlocks(baselineText, currentText)
|
||||||
|
.map((op) => renderOp(op, render))
|
||||||
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,3 +122,18 @@ describe("renderTrackChanges", () => {
|
|||||||
expect(a).toBe(b);
|
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 `<p>${src}</p>`;
|
||||||
|
};
|
||||||
|
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("<p>Good one.</p>"); // the healthy block still rendered
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user