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>
  );
}