diff --git a/src/trackChangesModel.ts b/src/trackChangesModel.ts
index 6c28726..0f92c97 100644
--- a/src/trackChangesModel.ts
+++ b/src/trackChangesModel.ts
@@ -437,7 +437,7 @@ function proposalBlockHtml(p: ProposalView, render: (src: string) => string): st
`` +
`` +
``;
- return `
${actions}${before}${after}
`;
+ return `${actions}${before}${after}
`;
}
function renderReviewOp(
@@ -445,9 +445,9 @@ function renderReviewOp(
render: (src: string) => string,
colored: (raw: string) => string,
): string {
- if (op.kind === "removed") return renderOp(op, render);
- if (op.kind === "changed" && op.atomic) return renderOp(op, render);
- if (op.kind === "changed") return renderOp(op, render); // word-merged /
+ // removed blocks and any changed block (atomic fences diffed whole; non-atomic prose
+ // word-merged) render via renderOp โ no author sentinels (deletions neutral, spec ยง6.7).
+ if (op.kind === "removed" || op.kind === "changed") return renderOp(op, render);
return `${colored(op.block.raw)}
`;
}
@@ -468,12 +468,13 @@ export function renderReview(
const render = opts.render ?? defaultRender;
const ranges = splitBlocksWithRanges(currentText);
const ops = diffBlocks(baselineText, currentText);
- const colored = (raw: string): string => {
- const blk = ranges.find((r) => r.raw === raw);
- if (!blk) return render(raw);
- return colorByAuthor(raw, blk.start, authorSpans, render);
- };
- const bodyParts = ops.map((op) => renderReviewOp(op, render, colored));
+ let ci = 0; // pointer into ranges; advances for every op with a current-side block
+ const bodyParts = ops.map((op) => {
+ const blk = op.kind === "removed" ? undefined : ranges[ci++];
+ const colored = (raw: string): string =>
+ blk ? colorByAuthor(raw, blk.start, authorSpans, render) : render(raw);
+ return renderReviewOp(op, render, colored);
+ });
const anchored = proposals.filter((p) => p.anchorStart !== null);
const unanchored = proposals.filter((p) => p.anchorStart === null);
const proposalParts = [...anchored, ...unanchored].map((p) => proposalBlockHtml(p, render));
diff --git a/test/trackChangesModel.test.ts b/test/trackChangesModel.test.ts
index 0a27195..55c6ef5 100644
--- a/test/trackChangesModel.test.ts
+++ b/test/trackChangesModel.test.ts
@@ -295,6 +295,18 @@ describe("renderReview", () => {
expect(html).toContain("mermaid");
expect(html).not.toContain("cw-by-");
});
+ test("renderReview: author-colors the correct block when two paragraphs are identical", () => {
+ // Two identical paragraphs; baseline has only the first, so the SECOND is an
+ // added block authored by human. Its span must color THAT block, not the first.
+ const baseline = "Hello world";
+ const current = "Hello world\n\nHello world";
+ // second "Hello world" starts at offset 13; "world" at 19..24
+ const spans = [{ start: 19, end: 24, author: "human" as const }];
+ const html = renderReview(baseline, current, spans, []);
+ // exactly one cw-by-human span (the added second block's "world"), not zero, not on the first.
+ const count = (html.match(/cw-by-human/g) ?? []).length;
+ expect(count).toBe(1);
+ });
});
import { renderTrackChanges as rtc2 } from "../src/trackChangesModel";