| 1 | import ReactMarkdown from "react-markdown"; |
| 2 | import rehypeKatex from "rehype-katex"; |
| 3 | import remarkGfm from "remark-gfm"; |
| 4 | import remarkMath from "remark-math"; |
| 5 | |
| 6 | import { CodeBlock } from "@/components/CodeBlock"; |
| 7 | import { cn } from "@/lib/utils"; |
| 8 | |
| 9 | import "katex/dist/katex.min.css"; |
| 10 | |
| 11 | interface MarkdownTextRendererProps { |
| 12 | children: string; |
| 13 | className?: string; |
| 14 | } |
| 15 | |
| 16 | /** |
| 17 | * Heavy markdown stack (GFM, math, KaTeX, syntax highlighting) kept in a |
| 18 | * separate chunk so the app shell can paint sooner on refresh. |
| 19 | */ |
| 20 | export default function MarkdownTextRenderer({ |
| 21 | children, |
| 22 | className, |
| 23 | }: MarkdownTextRendererProps) { |
| 24 | return ( |
| 25 | <div |
| 26 | className={cn( |
| 27 | "markdown-content prose prose-base max-w-none dark:prose-invert", |
| 28 | "prose-headings:mt-4 prose-headings:mb-2 prose-headings:font-semibold prose-headings:tracking-tight", |
| 29 | "prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-h4:text-sm", |
| 30 | "prose-p:my-3.5", |
| 31 | "prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5", |
| 32 | "prose-blockquote:my-3 prose-blockquote:border-l-2 prose-blockquote:font-normal", |
| 33 | "prose-blockquote:not-italic prose-blockquote:text-foreground/80", |
| 34 | "prose-a:text-primary prose-a:underline-offset-2 hover:prose-a:opacity-80", |
| 35 | "prose-hr:my-6", |
| 36 | "prose-pre:my-0 prose-pre:bg-transparent prose-pre:p-0", |
| 37 | "prose-code:before:content-none prose-code:after:content-none prose-code:font-normal", |
| 38 | "prose-table:my-3 prose-th:text-left prose-th:font-medium", |
| 39 | className, |
| 40 | )} |
| 41 | style={{ lineHeight: "var(--cjk-line-height)" }} |
| 42 | > |
| 43 | <ReactMarkdown |
| 44 | remarkPlugins={[remarkGfm, remarkMath]} |
| 45 | rehypePlugins={[rehypeKatex]} |
| 46 | components={{ |
| 47 | code({ className: cls, children: kids, ...props }) { |
| 48 | const match = /language-(\w+)/.exec(cls || ""); |
| 49 | if (!match) { |
| 50 | return ( |
| 51 | <code |
| 52 | className={cn( |
| 53 | "rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]", |
| 54 | cls, |
| 55 | )} |
| 56 | {...props} |
| 57 | > |
| 58 | {kids} |
| 59 | </code> |
| 60 | ); |
| 61 | } |
| 62 | const code = String(kids).replace(/\n$/, ""); |
| 63 | return <CodeBlock language={match[1]} code={code} className="my-3" />; |
| 64 | }, |
| 65 | pre({ children: markdownChildren }) { |
| 66 | return <>{markdownChildren}</>; |
| 67 | }, |
| 68 | a({ href, children: markdownChildren, ...props }) { |
| 69 | return ( |
| 70 | <a |
| 71 | href={href} |
| 72 | target="_blank" |
| 73 | rel="noreferrer noopener" |
| 74 | className="text-primary underline underline-offset-2 hover:opacity-80" |
| 75 | {...props} |
| 76 | > |
| 77 | {markdownChildren} |
| 78 | </a> |
| 79 | ); |
| 80 | }, |
| 81 | }} |
| 82 | > |
| 83 | {children} |
| 84 | </ReactMarkdown> |
| 85 | </div> |
| 86 | ); |
| 87 | } |
| 88 |