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
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| DraftCompare | 25 to 57 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:33> | 33 to 33 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:35> | 35 to 38 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:40> | 40 to 43 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:45> | 45 to 54 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| onChange | 48 to 51 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:53> | 53 to 53 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| DraftPatch | 70 to 74 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:72> | 72 to 72 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No 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 }} />;
}