1import type { DiffsHighlighter } from "@pierre/diffs";
2import { toHtml } from "hast-util-to-html";
3import { toJsxRuntime } from "hast-util-to-jsx-runtime";
4import { cloneElement, isValidElement, memo, type DOMAttributes } from "react";
5import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7type HighlightedRoot = ReturnType<DiffsHighlighter["codeToHast"]>;
8type HighlightedNode = HighlightedRoot["children"][number];
9const runtime = { Fragment, jsx, jsxs };
10
11function elementShell(node: Extract<HighlightedNode, { type: "element" }>) {
12 const element = toJsxRuntime({ ...node, children: [] }, runtime);
13 if (!isValidElement<DOMAttributes<HTMLElement>>(element)) {
14 throw new Error("Expected a highlighted code element");
15 }
16 return element;
17}
18
19const HighlightedLine = memo(function HighlightedLine({ node }: { node: HighlightedNode }) {
20 if (node.type !== "element") return toJsxRuntime(node, runtime);
21 return cloneElement(elementShell(node), {
22 dangerouslySetInnerHTML: { __html: toHtml({ type: "root", children: node.children }) },
23 });
24});
25
26/** Completed line nodes retain their identity in the incremental highlighter.
27 * Keep their DOM mounted too: replacing the entire pre makes the browser parse
28 * and resolve styles for thousands of unchanged token spans on each update.
29 */
30export function HighlightedCodeLines({ root }: { root: HighlightedRoot }) {
31 const pre = root.children[0];
32 if (pre?.type !== "element" || pre.tagName !== "pre") return toJsxRuntime(root, runtime);
33 const code = pre.children[0];
34 if (code?.type !== "element" || code.tagName !== "code") return toJsxRuntime(root, runtime);
35 return cloneElement(
36 elementShell(pre),
37 undefined,
38 cloneElement(
39 elementShell(code),
40 undefined,
41 code.children.map((node, index) => (
42 // A line's position is stable as tokens and new lines are appended.
43 // oxlint-disable-next-line react/no-array-index-key
44 <HighlightedLine key={index} node={node} />
45 )),
46 ),
47 );
48}