| 1 | import { memo, useMemo, useRef } from "react"; |
| 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import "katex/dist/katex.min.css"; |
| 4 | import { normalizeMath } from "./mathNormalize"; |
| 5 | import { createComponents } from "./markdownComponents"; |
| 6 | import { reasonixRehypePlugins, reasonixRemarkPlugins } from "./markdownRemarkPlugins"; |
| 7 | import { markdownImageUrlTransform, markdownUrlTransform } from "../lib/markdownPipeline"; |
| 8 | |
| 9 | // Markdown rendering via react-markdown + remark-gfm (tables, task lists, |
| 10 | // strike, autolinks) and remark-math + rehype-katex for $/$$ KaTeX math. |
| 11 | // This is the STREAMING path (incremental commits of a live answer); history |
| 12 | // rows render worker-parsed HAST blocks through MarkdownHistory instead, with |
| 13 | // the same plugins, components map, and urlTransform. |
| 14 | // |
| 15 | // The math pre-pass repairs LLM-native delimiters and display structure. |
| 16 | // remarkMathPolicy then classifies parsed inline-math AST nodes using their |
| 17 | // surrounding prose, avoiding false positives on currency and env vars. |
| 18 | // |
| 19 | // file:/// hrefs come from local-path linkification (remarkLocalPathLinks) |
| 20 | // and must survive URL sanitisation; markdownUrlTransform (shared with the |
| 21 | // worker parse pipeline) keeps them while blanking javascript: and friends. |
| 22 | |
| 23 | const MarkdownRenderer = memo(function MarkdownRenderer({ |
| 24 | text, |
| 25 | plainStatusBlocks = false, |
| 26 | bare = false, |
| 27 | }: { |
| 28 | text: string; |
| 29 | plainStatusBlocks?: boolean; |
| 30 | bare?: boolean; |
| 31 | }) { |
| 32 | const containerRef = useRef<HTMLDivElement>(null); |
| 33 | const mathContent = useMemo(() => normalizeMath(text), [text]); |
| 34 | const components = useMemo(() => createComponents(plainStatusBlocks), [plainStatusBlocks]); |
| 35 | const content = ( |
| 36 | <ReactMarkdown |
| 37 | remarkPlugins={reasonixRemarkPlugins} |
| 38 | rehypePlugins={reasonixRehypePlugins} |
| 39 | components={components} |
| 40 | // file:/// anchors (local path linkification) are safe to keep; the |
| 41 | // default transform would blank them along with javascript: etc. |
| 42 | urlTransform={(value, key, node) => node.tagName === "img" && key === "src" |
| 43 | ? markdownImageUrlTransform(value) |
| 44 | : markdownUrlTransform(value)} |
| 45 | > |
| 46 | {mathContent} |
| 47 | </ReactMarkdown> |
| 48 | ); |
| 49 | return bare ? content : <div className="md" ref={containerRef}>{content}</div>; |
| 50 | }); |
| 51 | |
| 52 | export default MarkdownRenderer; |
| 53 |