import {
createElement,
type CSSProperties,
Fragment,
type ReactNode,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import type { ComarkElement, ComarkNode } from "comark";
import { cached, highlight } from "../../lib/highlighter";
import {
CODE_FONT,
CODE_LABEL_HEIGHT as LABEL_HEIGHT,
CODE_LINE_HEIGHT,
CODE_PADDING,
} from "./constants";
function CodeView(
{ source, language, streaming = false, nodes, height }: {
source: string;
language?: string;
streaming?: boolean;
nodes?: ComarkNode[];
height?: number;
},
) {
let [state, setState] = useState<
{
source: string;
language?: string;
html: string;
} | null
>(() => {
if (streaming || nodes) return null;
let html = cached(source, language);
return html ? { source, language, html } : null;
});
useEffect(() => {
if (streaming || nodes) return;
let cancelled = false;
highlight(source, language).then(h => {
if (!cancelled) setState({ source, language, html: h });
});
return () => {
cancelled = true;
};
}, [source, language, streaming, nodes]);
let html = !streaming && !nodes && state?.source === source && state.language === language
? state.html
: !streaming && !nodes
? cached(source, language)
: undefined;
return (
{language && (
{language}
)}
{nodes
?
: html
?
: streaming
?
: source}
);
}
function elem(node: ComarkNode): node is ComarkElement {
return Array.isArray(node) && typeof node[0] === "string";
}
function style(raw: unknown): CSSProperties | undefined {
if (typeof raw !== "string") return;
let out: CSSProperties = {};
for (let part of raw.split(";")) {
let [key, value] = part.split(":");
if (!key || !value) continue;
let name = key.trim().replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
(out as Record)[name] = value.trim();
}
return out;
}
function props(node: ComarkElement): Record {
let out: Record = {};
for (let [key, value] of Object.entries(node[1])) {
if (key === "$") continue;
if (key === "class") out.className = value;
else if (key === "style") out.style = style(value);
else if (key === "tabindex") out.tabIndex = Number(value);
else out[key] = value;
}
return out;
}
function cls(value: unknown, extra?: string) {
let base = Array.isArray(value) ? value.join(" ") : typeof value === "string" ? value : "";
return [base, extra].filter(Boolean).join(" ") || undefined;
}
function line(node: ComarkNode): node is ComarkElement {
return elem(node) && node[0] === "span"
&& cls(node[1].class)?.split(/\s+/).includes("line") === true;
}
function ast(node: ComarkNode, key: string, extra?: string): ReactNode {
if (typeof node === "string") return node;
if (!elem(node)) return null;
let tag = node[0];
if (tag === "br") return "\n";
if (tag !== "span" && tag !== "code") {
return (
{node.slice(2).map((child, i) => ast(child as ComarkNode, `${key}.${i}`))}
);
}
let p = props(node);
if (extra) p.className = cls(p.className, extra);
return createElement(
tag,
{ ...p, key },
...(node.slice(2) as ComarkNode[]).map((child, i) => ast(child, `${key}.${i}`)),
);
}
function rows(source: string) {
let offset = 0;
return source.split("\n").map(line => {
let key = String(offset);
offset += line.length + 1;
return { key, line, offset: Number(key) };
});
}
function CodeNodes({ nodes, source, streaming }: {
nodes: ComarkNode[];
source: string;
streaming: boolean;
}) {
let list = useMemo(() => rows(source), [source]);
let edge = useRef(source.length);
let index = 0;
useEffect(() => {
edge.current = source.length;
}, [source]);
return (
<>
{nodes.map((node, i) => {
if (!line(node)) return ast(node, String(i));
let row = list[index++];
let fresh = streaming && row ? row.offset >= edge.current : false;
return ast(
node,
row?.key || String(i),
fresh ? "stream-token stream-token-new" : "stream-token",
);
})}
>
);
}
function CodeStream({ source }: { source: string }) {
let list = useMemo(() => rows(source), [source]);
let edge = useRef(list.length);
useEffect(() => {
edge.current = list.length;
}, [list.length]);
return (
<>
{list.map(({ key, line }, i) => (
= edge.current ? "stream-token stream-token-new" : "stream-token"}>
{line}
{i < list.length - 1 && "\n"}
))}
>
);
}
export { CodeView };