function splitLines(text: string): string[] { return text.match(/[^\n]*\n|[^\n]+$/g) ?? []; } /** One parsed piece of a conflicted file: verbatim text, or the index of a conflict block. */ export type Segment = { text: string } | { block: number }; export type ParsedBlock = { ours: string; theirs: string }; const START = /^<{7}(?: |$)/; const BASE = /^\|{7}(?: |$)/; const MIDDLE = /^={7}$/; const END = /^>{7}(?: |$)/; function stripEol(line: string): string { return line.replace(/\r?\n$/, ""); } /** Splits merged marker text into verbatim text and conflict blocks; null when the markers are malformed. */ export function parseConflictText(text: string): { segments: Segment[]; blocks: ParsedBlock[] } | null { const segments: Segment[] = []; const blocks: ParsedBlock[] = []; let state: "outside" | "ours" | "base" | "theirs" = "outside"; let outside: string[] = []; let ours: string[] = []; let theirs: string[] = []; for (const line of splitLines(text)) { const bare = stripEol(line); if (state === "outside") { if (START.test(bare)) { if (outside.length) segments.push({ text: outside.join("") }); outside = []; ours = []; theirs = []; state = "ours"; } else if (BASE.test(bare) || MIDDLE.test(bare) || END.test(bare)) return null; else outside.push(line); } else if (state === "ours" || state === "base") { if (BASE.test(bare) && state === "ours") state = "base"; else if (MIDDLE.test(bare)) state = "theirs"; else if (START.test(bare) || END.test(bare) || BASE.test(bare)) return null; else if (state === "ours") ours.push(line); } else if (END.test(bare)) { segments.push({ block: blocks.length }); blocks.push({ ours: ours.join(""), theirs: theirs.join("") }); state = "outside"; } else if (START.test(bare) || MIDDLE.test(bare) || BASE.test(bare)) return null; else theirs.push(line); } if (state !== "outside") return null; if (outside.length) segments.push({ text: outside.join("") }); return { segments, blocks }; } /** Replaces each conflict block whose choice is non-null, keeping the markers of blocks still undecided. */ export function applyChoices(content: string, choices: Array): string | null { const parsed = parseConflictText(content); if (!parsed || parsed.blocks.length !== choices.length) return null; const original = splitBlocks(content); return parsed.segments .map((segment) => ("text" in segment ? segment.text : (choices[segment.block] ?? original[segment.block]))) .join(""); } /** The raw marker text of each block, so undecided blocks round-trip unchanged. */ function splitBlocks(content: string): string[] { const blocks: string[] = []; let current: string[] | null = null; for (const line of splitLines(content)) { const bare = stripEol(line); if (current === null && START.test(bare)) current = []; if (current !== null) current.push(line); if (current !== null && END.test(bare)) { blocks.push(current.join("")); current = null; } } return blocks; }