-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmarkdown-renderer.tsx
More file actions
129 lines (118 loc) · 4.25 KB
/
markdown-renderer.tsx
File metadata and controls
129 lines (118 loc) · 4.25 KB
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
"use client";
import React, { type DetailedHTMLProps, type HTMLAttributes, useEffect, useMemo, useRef, useState } from "react";
import { Copy, Check } from "lucide-react";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import remarkMath from 'remark-math';
import rehypePrism from 'rehype-prism-plus';
import rehypeKatex from 'rehype-katex';
import { cn, copyToClipboard } from "@/lib/utils";
import './github-markdown.css';
import './prism-ghcolors-auto.css';
import "katex/dist/katex.min.css";
function NormalizeMathTags(input: string): string {
/** {@link https://www.assistant-ui.com/docs/guides/Latex} */
return (
input
/** Convert [/math]...[/math] to $$...$$ */
.replace(/\[\/math\]([\s\S]*?)\[\/math\]/g, (_, content) => `$$${content}$$`)
/** Convert [/inline]...[/inline] to $...$ */
.replace(/\[\/inline\]([\s\S]*?)\[\/inline\]/g, (_, content) => `$${content}$`)
/** Convert \( ... \) to $...$ (inline math) - handles both single and double backslashes */
.replace(/\\{1,2}\(([\s\S]*?)\\{1,2}\)/g, (_, content) => `$${content}$`)
/** Convert \[ ... \] to $$...$$ (block math) - handles both single and double backslashes */
.replace(/\\{1,2}\[([\s\S]*?)\\{1,2}\]/g, (_, content) => `$$${content}$$`)
);
}
function extractText(node: React.ReactNode): string {
return React.Children.toArray(node)
.map((child) => {
if (typeof child === "string" || typeof child === "number") {
return String(child);
}
if (React.isValidElement<{ children?: React.ReactNode }>(child) && child.props?.children) {
return extractText(child.props.children);
}
return "";
})
.join("");
}
function CodeBlock({
inline,
className,
children,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & { inline?: boolean }) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const isInline = inline || !className || !className.includes("language-");
const rest = props;
const codeText = useMemo(() => extractText(children).replace(/\n$/, ""), [children]);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
if (isInline) {
return (
<code className={className} {...rest}>
{children}
</code>
);
}
const handleCopy = async () => {
try {
await copyToClipboard(codeText);
setCopied(true);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => setCopied(false), 1200);
} catch (error) {
console.error("Failed to copy code block", error);
}
};
return (
<div className="relative group">
<button
type="button"
aria-label={copied ? "Copied" : "Copy code"}
onClick={handleCopy}
className={cn(
"absolute right-2 top-2 rounded-md border border-border/50 bg-background/80 px-2 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
copied ? "text-green-600 scale-105 animate-pulse" : "hover:text-foreground hover:-translate-y-0.5",
codeText ? "opacity-100" : "hidden"
)}
>
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
<span className="sr-only">{copied ? "Copied" : "Copy code"}</span>
</button>
<pre className={className}>
<code className={className} {...rest}>{children}</code>
</pre>
</div>
);
}
export default function MarkdownRenderer({ content }: { content: string }) {
return (
<div
className="markdown-body"
style={{ backgroundColor: 'transparent' }}
>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks, remarkMath]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }], rehypeKatex]}
components={{
ul: (props) => <ul className="list-disc" {...props} />,
ol: (props) => <ol className="list-decimal" {...props} />,
code: CodeBlock,
}}
>
{NormalizeMathTags(content)}
</ReactMarkdown>
</div>
)
}