From ebe3d4bab5ba7fad480fe56f23ecaeb8f3e1e547 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Fri, 12 Jun 2026 00:08:10 -0700 Subject: [PATCH] F10 SLICE-2: extract colorByAuthor from renderAuthorship sentinels (#29) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/trackChangesModel.ts | 19 +++++++++++++++++-- test/trackChangesModel.test.ts | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/trackChangesModel.ts b/src/trackChangesModel.ts index ffb44bc..d5399ab 100644 --- a/src/trackChangesModel.ts +++ b/src/trackChangesModel.ts @@ -351,6 +351,22 @@ function sentinelsToSpans(html: string): string { .split(SENT.human.close).join(""); } +/** + * Color one prose block's HTML by F3 author spans (PUA-sentinel technique, + * salvaged from F9 renderAuthorship). `blockStart` is the block's char offset in + * the source so spans map correctly. Pure; deterministic. + */ +export function colorByAuthor( + raw: string, + blockStart: number, + spans: AuthorSpan[], + render: (src: string) => string, +): string { + const overlapping = spans.filter((s) => s.end > blockStart && s.start < blockStart + raw.length); + const injected = injectSentinels(raw, blockStart, overlapping); + return sentinelsToSpans(render(injected)); +} + /** * Pure authorship render (INV-26/28): the CURRENT text with each F3-attributed * span colored by author. Prose blocks get inline ``; @@ -379,8 +395,7 @@ export function renderAuthorship( if (!badge) return `
${inner}
`; return `
${badge.label}${inner}
`; } - const injected = injectSentinels(b.raw, b.start, overlapping); - return `
${sentinelsToSpans(safe(injected))}
`; + return `
${colorByAuthor(b.raw, b.start, overlapping, safe)}
`; }) .join("\n"); } diff --git a/test/trackChangesModel.test.ts b/test/trackChangesModel.test.ts index 0297ee1..0fb1b49 100644 --- a/test/trackChangesModel.test.ts +++ b/test/trackChangesModel.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { splitBlocks, splitBlocksWithRanges, diffBlocks, renderTrackChanges, renderAuthorship, type AuthorSpan } from "../src/trackChangesModel"; +import { describe, it, test, expect } from "vitest"; +import { splitBlocks, splitBlocksWithRanges, diffBlocks, renderTrackChanges, renderAuthorship, colorByAuthor, type AuthorSpan } from "../src/trackChangesModel"; describe("splitBlocks", () => { it("splits prose paragraphs on blank lines, dropping empties", () => { @@ -235,6 +235,17 @@ describe("renderAuthorship", () => { }); }); +describe("colorByAuthor", () => { + test("colorByAuthor wraps human-authored prose in cw-by-human spans", () => { + const raw = "hello world"; + const spans: AuthorSpan[] = [{ start: 0, end: 5, author: "human" }]; + const render = (src: string) => `

${src}

`; + const html = colorByAuthor(raw, 0, spans, render); + expect(html).toContain('hello'); + expect(html).toContain("world"); + }); +}); + import { renderTrackChanges as rtc2 } from "../src/trackChangesModel"; describe("renderTrackChanges — intra-diagram mermaid (#22)", () => {