Spaces:
Running
Running
| import ReactMarkdown from "react-markdown"; | |
| import remarkGfm from "remark-gfm"; | |
| import remarkMath from "remark-math"; | |
| import rehypeKatex from "rehype-katex"; | |
| import type { Components } from "react-markdown"; | |
| function preprocessMarkdown(content: string): string { | |
| let result = content.replace(/\|\|/g, "|\n|"); | |
| const lines = result.split("\n"); | |
| const processed = lines.map((line) => { | |
| const tableStart = line.indexOf("|"); | |
| if (tableStart > 0) { | |
| const tableContent = line.slice(tableStart); | |
| if ((tableContent.match(/\|/g) ?? []).length >= 2) { | |
| return line.slice(0, tableStart).trimEnd() + "\n\n" + tableContent; | |
| } | |
| } | |
| return line; | |
| }); | |
| result = processed.join("\n"); | |
| result = result.replace(/([^|\n])\n(\|)/g, "$1\n\n$2"); | |
| return result; | |
| } | |
| const components: Components = { | |
| p: ({ children }) => ( | |
| <p className="text-sm text-neutral-800 leading-relaxed mb-3 last:mb-0">{children}</p> | |
| ), | |
| h1: ({ children }) => ( | |
| <h1 className="text-xl font-bold text-neutral-900 mt-4 mb-2 first:mt-0">{children}</h1> | |
| ), | |
| h2: ({ children }) => ( | |
| <h2 className="text-lg font-semibold text-neutral-800 mt-4 mb-2 first:mt-0">{children}</h2> | |
| ), | |
| h3: ({ children }) => ( | |
| <h3 className="text-base font-semibold text-neutral-700 mt-3 mb-1.5 first:mt-0">{children}</h3> | |
| ), | |
| ul: ({ children }) => ( | |
| <ul className="list-disc list-outside pl-5 mb-3 space-y-1 text-sm text-neutral-800">{children}</ul> | |
| ), | |
| ol: ({ children }) => ( | |
| <ol className="list-decimal list-outside pl-5 mb-3 space-y-1 text-sm text-neutral-800">{children}</ol> | |
| ), | |
| li: ({ children }) => <li className="leading-relaxed">{children}</li>, | |
| code: ({ children, className }) => { | |
| const isBlock = className?.startsWith("language-"); | |
| const language = className?.replace("language-", "") ?? ""; | |
| if (isBlock) { | |
| return ( | |
| <div className="my-3 rounded-xl overflow-hidden border border-neutral-200"> | |
| {language && ( | |
| <div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2"> | |
| <span className="text-xs font-mono text-neutral-500">{language}</span> | |
| </div> | |
| )} | |
| <div className="p-4 bg-neutral-50 overflow-x-auto"> | |
| <code className="text-xs font-mono leading-relaxed text-neutral-800 whitespace-pre"> | |
| {children} | |
| </code> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <code className="px-1.5 py-0.5 rounded-md bg-neutral-100 text-brand-green font-mono text-xs"> | |
| {children} | |
| </code> | |
| ); | |
| }, | |
| pre: ({ children }) => <>{children}</>, | |
| blockquote: ({ children }) => ( | |
| <blockquote className="border-l-4 border-brand-green pl-4 py-1 my-3 text-sm text-neutral-600 italic bg-brand-green-50 rounded-r-xl"> | |
| {children} | |
| </blockquote> | |
| ), | |
| a: ({ children, href }) => ( | |
| <a | |
| href={href} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-brand-green underline underline-offset-2 hover:text-brand-green/80 transition-colors" | |
| > | |
| {children} | |
| </a> | |
| ), | |
| strong: ({ children }) => <strong className="font-semibold">{children}</strong>, | |
| hr: () => <hr className="border-neutral-200 my-3" />, | |
| table: ({ children }) => ( | |
| <div className="my-3 overflow-x-auto rounded-xl border border-neutral-200 shadow-sm"> | |
| <table className="w-full text-sm border-collapse">{children}</table> | |
| </div> | |
| ), | |
| thead: ({ children }) => ( | |
| <thead className="bg-neutral-50 border-b border-neutral-200">{children}</thead> | |
| ), | |
| th: ({ children }) => ( | |
| <th className="px-4 py-3 text-left text-xs font-semibold text-neutral-600 uppercase tracking-wider"> | |
| {children} | |
| </th> | |
| ), | |
| td: ({ children }) => ( | |
| <td className="px-4 py-3 text-sm text-neutral-800 border-b border-neutral-100">{children}</td> | |
| ), | |
| tr: ({ children }) => ( | |
| <tr className="hover:bg-neutral-50 transition-colors">{children}</tr> | |
| ), | |
| }; | |
| interface MarkdownRendererProps { | |
| content: string; | |
| skipPreprocess?: boolean; | |
| } | |
| export default function MarkdownRenderer({ content, skipPreprocess }: MarkdownRendererProps) { | |
| const processed = skipPreprocess ? content : preprocessMarkdown(content); | |
| if (!skipPreprocess) { | |
| const hasCR = content.includes("\r"); | |
| const hasDoubleNewline = content.includes("\n\n"); | |
| // console.log( | |
| // `[MarkdownRenderer] skipPreprocess=false contentLen=${content.length} same=${content === processed} hasCR=${hasCR} hasDoubleNewline=${hasDoubleNewline}` | |
| // ); | |
| // console.log("[MarkdownRenderer] CONTENT JSON →", JSON.stringify(content.slice(0, 600))); | |
| } | |
| return ( | |
| <ReactMarkdown | |
| remarkPlugins={[remarkGfm, remarkMath]} | |
| rehypePlugins={[rehypeKatex]} | |
| components={components} | |
| > | |
| {processed} | |
| </ReactMarkdown> | |
| ); | |
| } | |