Skip to content
Source
Sign inSign up
Open History to see the last commit

MarkdownMath.tsx

79 lines3.5 KB

1import { lazy, memo, Suspense, useLayoutEffect, useRef, useState } from "react";
2​
3import { observeResize } from "../../lib/observeResize";
4​
5import { RenderErrorBoundary } from "../RenderErrorBoundary";
6​
7const MathTypeset = lazy(() => import("./MathTypeset"));
8​
9/**
10 * One formula from `remark-math`. Shows its TeX until KaTeX loads, and
11 * keeps showing it when KaTeX cannot parse it, such as mid-stream. Copying
12 * any part of a formula copies its whole TeX source.
13 */
14export const MarkdownMath = memo(function MarkdownMath(props: {
15 tex: string;
16 display: boolean;
17 focusable?: boolean;
18}) {
19 const viewportRef = useRef<HTMLSpanElement>(null);
20 const contentRef = useRef<HTMLSpanElement>(null);
21 const [overflows, setOverflows] = useState(false);
22 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- observes formula DOM width to make overflowing equations keyboard scrollable
23 useLayoutEffect(() => {
24 const viewport = viewportRef.current;
25 const content = contentRef.current;
26 if (!viewport || !content || props.focusable === false) return;
27 const measure = () => setOverflows(viewport.scrollWidth > viewport.clientWidth);
28 measure();
29 // Typesetting and streamed TeX can change content width without resizing the viewport.
30 return observeResize([viewport, content], measure);
31 }, [props.focusable]);
32 const scrollable = props.focusable !== false && overflows;
33 const tex = props.tex.trim();
34 const source = props.display ? `\\[\n${tex}\n\\]` : `\\(${tex}\\)`;
35 const fallback = <span className="whitespace-pre-wrap">{source}</span>;
36 return (
37 <span
38 className={
39 props.display
40 ? "chat-markdown-math-display my-[0.65rem] block in-[.chat-markdown-table-container]:max-w-none in-[.chat-markdown-table-container]:overflow-visible"
41 : "inline-block max-w-full align-middle whitespace-nowrap wrap-normal break-normal in-[.chat-markdown-table-container]:max-w-none in-[.chat-markdown-table-container]:overflow-visible"
42 }
43 data-markdown-math={props.display ? "display" : "inline"}
44 data-markdown-copy={props.display ? `${source}\n\n` : source}
45 role={props.focusable === false ? undefined : "group"}
46 aria-label={
47 props.focusable === false
48 ? undefined
49 : props.display
50 ? "Display equation"
51 : "Inline equation"
52 }
53 >
54 <span
55 ref={viewportRef}
56 className="block max-w-full overflow-x-auto overflow-y-hidden in-[.chat-markdown-table-container]:max-w-none in-[.chat-markdown-table-container]:overflow-visible px-0.5 py-0.75 focus-visible:outline-1 focus-visible:outline-solid focus-visible:-outline-offset-1 focus-visible:outline-border [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar-thumb]:rounded [&::-webkit-scrollbar-thumb]:border [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-clip-content"
57 tabIndex={scrollable ? 0 : -1}
58 aria-label={
59 !scrollable
60 ? undefined
61 : props.display
62 ? "Scroll display equation"
63 : "Scroll inline equation"
64 }
65 >
66 <span
67 ref={contentRef}
68 className={props.display ? "inline-block w-max min-w-full" : "inline-block w-max"}
69 >
70 <RenderErrorBoundary resetKeys={[tex]} fallback={fallback}>
71 <Suspense fallback={fallback}>
72 <MathTypeset tex={tex} display={props.display} fallback={fallback} />
73 </Suspense>
74 </RenderErrorBoundary>
75 </span>
76 </span>
77 </span>
78 );
79});