Skip to the content

Capsid menu

components/draft-compare/draft-compare.react.tsx

66 lines and 9 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of draft-compare.react.tsx
FunctionLinesComplexityCoveredCRAPMutation
DraftCompare25 to 572No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:33>33 to 332No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:35>35 to 382No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:40>40 to 433No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:45>45 to 543No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onChange48 to 513No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:53>53 to 531No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
DraftPatch70 to 741No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:72>72 to 722No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports components/draft-compare/draft-compare.ts. Imported by components/draft-compare/examples.tsx.

Source

import { useEffect, useId, useMemo, useRef } from "react";
import { enhance, renderCompare, renderPatch, setView, VIEWS, type CompareLabels, type CompareSide, type CompareView, type RunSpan } from "./draft-compare.ts";

export interface DraftCompareProps {
  before: CompareSide;
  after: CompareSide;
  // Words for "Before" and "After" over each side.
  labels?: Partial<CompareLabels>;
  // The view shown. Give it (with onViewChange) to keep the choice in your own state; leave it
  // out and the component remembers the person's choice in localStorage.
  view?: CompareView;
  onViewChange?: (view: CompareView) => void;
  // Unchanged sentences kept beside a change before the rest fold. Default 1.
  context?: number;
  // Who wrote what, per side, by character offset: each sentence is marked with its run.
  runs?: { before?: RunSpan[]; after?: RunSpan[] };
  // How alike two sentences must be (0 to 1) to count as one edited. Default 0.4.
  similarity?: number;
  // Ids and radio names; give each compare on a page its own. Made up when left out.
  id?: string;
}

// Renders exactly the markup renderCompare writes, so a server render (renderToString) and the
// browser agree, and the same behaviour module wires it.
export function DraftCompare({ before, after, labels, view, onViewChange, context, runs, similarity, id }: DraftCompareProps) {
  const own = useId().replace(/[^a-zA-Z0-9]/g, "");
  const host = useRef<HTMLDivElement>(null);
  const first = useRef<CompareView>(view ?? "side");
  const controlled = view !== undefined;
  const key = JSON.stringify([before, after, labels, runs, context, similarity, id, controlled]);
  // The view is not part of the html's identity: the radios hold it, in CSS, so choosing one
  // does not rebuild the page's folds and focus.
  const html = useMemo(() => renderCompare(before, after, { id: id ?? `compare-${own}`, view: first.current, context, labels, runs, similarity, remember: !controlled }), [key, own]);

  useEffect(() => {
    const el = host.current;
    return el ? enhance(el) : undefined;
  }, [html]);

  useEffect(() => {
    const section = host.current?.querySelector<HTMLElement>(".cap-compare");
    if (section && view) setView(section, view);
  }, [view, html]);

  useEffect(() => {
    const el = host.current;
    if (!el || !onViewChange) return undefined;
    const onChange = (e: Event) => {
      const t = e.target as HTMLInputElement;
      if (t.matches(".cap-compare-bar input[type='radio']") && (VIEWS as readonly string[]).includes(t.value)) onViewChange(t.value as CompareView);
    };
    el.addEventListener("change", onChange);
    return () => el.removeEventListener("change", onChange);
  }, [onViewChange, html]);

  return <div ref={host} dangerouslySetInnerHTML={{ __html: html }} />;
}

export interface DraftPatchProps {
  // A unified git patch (git diff, git show, git format-patch).
  patch: string;
  // What the patch is of; names the region.
  title?: string;
  // Give each patch on a page its own id. Made up when left out.
  id?: string;
}

// The patch mode: a unified patch by line, with a literal plus or minus on each changed line.
// Pure markup, so a server render and the browser agree; there is no behaviour to attach.
export function DraftPatch({ patch, title, id }: DraftPatchProps) {
  const own = useId().replace(/[^a-zA-Z0-9]/g, "");
  const html = useMemo(() => renderPatch(patch, { id: id ?? `patch-${own}`, title }), [patch, title, id, own]);
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}