Spaces:
Running
Running
File size: 4,826 Bytes
c0ddd13 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 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>
);
}
|