79 lines · 3.1 KB
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;