fix: render added blocks via markdown-safe sentinel path emitting cw-ins-* (heading/list structure preserved)

Defect: an ADDED block was colored via `render(wordDiffByAuthor("", raw, ...))`, which
wrapped the whole raw markdown in an inline `<ins>` BEFORE markdown-it parsed it — so
`## X` rendered as literal text, not `<h2>`. Same structural bug for list, blockquote,
and thematic-break blocks.

Fix (three-part, pure / vscode-free):
1. `blockContentStart(raw)`: new helper returning the offset past block-level markers
   (ATX headings `## `, unordered/ordered list markers, blockquotes). Open sentinels must
   not precede these markers or markdown-it cannot recognise the block construct.
2. `injectSentinels`: clamp the open-sentinel lower-bound to `blockContentStart(raw)`,
   so for `## New Section` the sentinel lands AFTER `## ` (at position 3) instead of at
   position 0, letting the heading parse correctly.
3. `sentinelsToSpans` / `colorByAuthor`: add optional `kind: "by" | "ins" = "by"`
   parameter so the sentinel path can emit `cw-ins-{author}` for added blocks while all
   existing `colorByAuthor`/`cw-by-*` call-sites are unchanged.
4. `renderReview` ADDED block path: `render(wordDiffByAuthor("", raw, ...))` →
   `colorByAuthor(raw, blk.start, landedSpans, render, "ins")`. The CHANGED non-atomic
   prose path (`wordDiffByAuthor(before, after, ...)`) is untouched — it correctly keeps
   the block markers at column 0 of the unchanged prefix.
5. `renderReviewOp`: removed the now-vestigial `colored` parameter (was always `render`
   after the added-block path moved to `changedHtml`); unchanged blocks now call
   `render(op.block.raw)` directly.

Regression test added: "renderReview: an added heading block renders as a heading AND
is author-colored as an insertion" — RED on old code, GREEN after fix. All 259 tests
pass; typecheck clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-26 14:09:19 -07:00
parent 08557827da
commit 44ef0a21db
2 changed files with 52 additions and 12 deletions
+12
View File
@@ -491,6 +491,18 @@ describe("renderReview", () => {
const html = renderReview("Keep.", "Keep.\n\nFresh line.", [{ start: 6, end: 17, author: "human" }], []);
expect(html).toContain("cw-ins-human");
});
test("renderReview: an added heading block renders as a heading AND is author-colored as an insertion", () => {
// "Intro.\n\n" = 8 chars; "## New Section" = 14 chars → offset 8..22 in currentText.
// Before fix: wordDiffByAuthor wrapped raw in <ins> BEFORE markdown-it parsed it,
// so "## New Section" rendered as literal text (structural bug). After fix:
// colorByAuthor uses the sentinel technique — markdown-it parses the block first,
// so ## becomes a real <h2>, THEN sentinels are replaced with cw-ins-* spans.
const html = renderReview("Intro.", "Intro.\n\n## New Section", [{ start: 8, end: 22, author: "human" }], []);
expect(html).toMatch(/<h[12][^>]*>/); // real heading element, not literal "## "
expect(html).toContain("cw-ins-human"); // author-colored insertion
expect(html).not.toContain("&gt;&gt;"); // sanity: markers not escaped as text
});
});
import { renderTrackChanges as rtc2 } from "../src/trackChangesModel";