// Unified-diff parsing + rendering for the review UI. Pure parse (testable); the two // views (unified / side-by-side) consume the same parsed structure. Browser-side. import { esc } from "./comments-client"; export type DiffMode = "unified" | "split"; export type DiffLine = { type: "hunk" | "add" | "del" | "ctx"; text: string }; export interface SideRow { left: DiffLine | null; right: DiffLine | null; } const SKIP = /^(diff --git |index |--- |\+\+\+ |new file|deleted file|old mode|new mode|similarity |rename |\\)/; /** Parse a unified diff into typed lines (file headers dropped, `@@` kept, markers stripped). */ export function parseUnifiedDiff(diff: string): DiffLine[] { const lines: DiffLine[] = []; for (const raw of diff.split("\n")) { if (raw.startsWith("@@")) lines.push({ type: "hunk", text: raw }); else if (SKIP.test(raw)) continue; else if (raw.startsWith("+")) lines.push({ type: "add", text: raw.slice(1) }); else if (raw.startsWith("-")) lines.push({ type: "del", text: raw.slice(1) }); else if (raw.startsWith(" ")) lines.push({ type: "ctx", text: raw.slice(1) }); // trailing empty line etc. → ignored } return lines; } /** Pair deletions with the following additions for a side-by-side view. */ export function toSideBySide(lines: DiffLine[]): SideRow[] { const rows: SideRow[] = []; let i = 0; while (i < lines.length) { const l = lines[i]; if (l.type === "hunk" || l.type === "ctx") { rows.push({ left: l, right: l }); i++; continue; } const dels: DiffLine[] = []; const adds: DiffLine[] = []; while (i < lines.length && lines[i].type === "del") dels.push(lines[i++]); while (i < lines.length && lines[i].type === "add") adds.push(lines[i++]); if (!dels.length && !adds.length) { i++; continue; } const n = Math.max(dels.length, adds.length); for (let k = 0; k < n; k++) rows.push({ left: dels[k] ?? null, right: adds[k] ?? null }); } return rows; } const sign = (t: DiffLine["type"]) => (t === "add" ? "+" : t === "del" ? "-" : " "); /** Render a diff to an HTML table in the requested mode (empty string if no changes). */ export function renderDiff(diffText: string, mode: DiffMode): string { const lines = parseUnifiedDiff(diffText); if (!lines.length) return ""; if (mode === "split") { const rows = toSideBySide(lines) .map((r) => r.left?.type === "hunk" ? `