DiffLines

tcmts.share.DiffLines · #a6bh2gqw · term · runs in the browser

uses localStorage

A diff's rows (foldDiff's), coloured as code: what's added and removed marked, each line numbered before and after, ⋮ for the lines left out. One line after another, or side by side as DiffMode chooses (on a screen wide enough). more is where the rest of a diff cut short is (its definition's page).

import type { DiffRow } from "#codebase/tcmts/share";
import { useEffect, useState } from "react";

export function DiffLines({ rows, more }: { rows: DiffRow[]; more?: string }) {
  // (side by side if chosen, and the screen is wide enough for it)
  const [split, setSplit] = useState(false);
  useEffect(() => {
    const read = () =>
      setSplit(
        localStorage.getItem("tcm.diff") === "split" &&
          !window.matchMedia("(max-width: 700px)").matches,
      );
    read();
    window.addEventListener("tcm-diff", read);
    return () => window.removeEventListener("tcm-diff", read);
  }, []);
  if (!rows.length) return null;
  type Line = Extract<DiffRow, { op: string }>;
  const tone = (r?: Line) => (!r ? "none" : r.op === "+" ? "a" : r.op === "-" ? "d" : "");
  const code = (r: Line | undefined, cls: string) => (
    <span className={`lc ${cls}`} dangerouslySetInnerHTML={{ __html: r?.html ?? "" }} />
  );
  const gap = (g: Extract<DiffRow, { gap: number }>) =>
    g.cut && more ? (
      <a href={more}>
        ⋮ {g.gap} more line{g.gap === 1 ? "" : "s"}: the whole of it on its page
      </a>
    ) : (
      `⋮ ${g.gap} line${g.gap === 1 ? "" : "s"}`
    );
  if (!split)
    return (
      <div className="diff">
        <div className="unified">
          {rows.map((r, i) =>
            "gap" in r ? (
              <div key={i} className="row gap">
                <span className="ln" />
                <span className="ln" />
                <span className="op" />
                <span className="lc">{gap(r)}</span>
              </div>
            ) : (
              <div key={i} className={`row ${tone(r)}`}>
                <span className="ln" data-n={r.a} />
                <span className="ln" data-n={r.b} />
                <span className="op">{r.op === " " ? "" : r.op}</span>
                {code(r, "")}
              </div>
            ),
          )}
        </div>
      </div>
    );
  // side by side: each run of removed lines beside the run added after it
  const pairs: ({ left?: Line; right?: Line } | Extract<DiffRow, { gap: number }>)[] = [];
  for (let k = 0; k < rows.length;) {
    const r = rows[k]!;
    if ("gap" in r || r.op === " ") {
      pairs.push("gap" in r ? r : { left: r, right: r });
      k++;
      continue;
    }
    const removed: Line[] = [];
    const added: Line[] = [];
    const at = (n: number) => rows[n] as Line | undefined;
    while (k < rows.length && !("gap" in rows[k]!) && at(k)!.op === "-") removed.push(at(k++)!);
    while (k < rows.length && !("gap" in rows[k]!) && at(k)!.op === "+") added.push(at(k++)!);
    for (let i = 0; i < Math.max(removed.length, added.length); i++)
      pairs.push({ left: removed[i], right: added[i] });
  }
  return (
    <div className="diff">
      <div className="split">
        {pairs.map((p, i) =>
          "gap" in p ? (
            <div key={i} className="row gap">
              <span className="ln" />
              <span className="lc">{gap(p)}</span>
              <span className="ln r" />
              <span className="lc" />
            </div>
          ) : (
            <div key={i} className="row">
              <span className={`ln ${tone(p.left)}`} data-n={p.left?.a} />
              {code(p.left, tone(p.left))}
              <span className={`ln r ${tone(p.right)}`} data-n={p.right?.b} />
              {code(p.right, tone(p.right))}
            </div>
          ),
        )}
      </div>
    </div>
  );
}

uses

used by