Skip to content

Commit 1b4dfe1

Browse files
authored
feat: update (#1449)
1 parent fee207b commit 1b4dfe1

File tree

1 file changed

+321
-1
lines changed

1 file changed

+321
-1
lines changed

web/src/components/markdown.tsx

Lines changed: 321 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,321 @@
1-
CN/a
1+
'use client';
2+
3+
import { cn } from '@/lib/utils';
4+
import { ImageIcon } from 'lucide-react';
5+
import Link from 'next/link';
6+
import {
7+
JSX,
8+
MouseEventHandler,
9+
useCallback,
10+
useEffect,
11+
useMemo,
12+
useState,
13+
} from 'react';
14+
import ReactMarkdown from 'react-markdown';
15+
import rehypeHighlight from 'rehype-highlight';
16+
import rehypeHighlightLines from 'rehype-highlight-code-lines';
17+
import rehypeRaw from 'rehype-raw';
18+
import remarkDirective from 'remark-directive';
19+
import remarkFrontmatter from 'remark-frontmatter';
20+
import remarkGfm from 'remark-gfm';
21+
import remarkGithubAdmonitionsToDirectives from 'remark-github-admonitions-to-directives';
22+
import remarkHeaderId from 'remark-heading-id';
23+
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
24+
import { AnchorLink } from './anchor-link';
25+
import { ChartMermaid } from './chart-mermaid';
26+
import { Skeleton } from './ui/skeleton';
27+
import { Table, TableBody, TableCell, TableHeader, TableRow } from './ui/table';
28+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
29+
30+
import './markdown.css';
31+
32+
const securityLink = (props: JSX.IntrinsicElements['a']) => {
33+
const target = props.href?.match(/^http/) ? '_blank' : '_self';
34+
const url = props.href?.replace(/\.md/, '');
35+
36+
const isNavLink = props.className?.includes('toc-link');
37+
return isNavLink ? (
38+
<Tooltip>
39+
<TooltipTrigger asChild>
40+
<AnchorLink {...props} href={url || '/'} target={target} />
41+
</TooltipTrigger>
42+
<TooltipContent side={isNavLink ? 'left' : 'top'}>
43+
{props.children}
44+
</TooltipContent>
45+
</Tooltip>
46+
) : (
47+
<Link {...props} href={url || '/'} target={target} className="underline">
48+
{props.children}
49+
</Link>
50+
);
51+
};
52+
53+
const unSecurityLink = (props: JSX.IntrinsicElements['a']) => {
54+
const url = props.href?.replace(/\.md/, '') || '/';
55+
const isNavLink = props.className?.includes('toc-link');
56+
const handleLinkClick: MouseEventHandler<HTMLAnchorElement> = (e) => {
57+
if (!url.match(/http/)) {
58+
e.preventDefault();
59+
e.stopPropagation();
60+
}
61+
};
62+
return isNavLink ? (
63+
<Tooltip>
64+
<TooltipTrigger asChild>
65+
<AnchorLink
66+
{...props}
67+
href={url}
68+
target="_blank"
69+
onClick={handleLinkClick}
70+
/>
71+
</TooltipTrigger>
72+
<TooltipContent side={isNavLink ? 'left' : 'top'}>
73+
{props.children}
74+
</TooltipContent>
75+
</Tooltip>
76+
) : (
77+
<Link
78+
{...props}
79+
href={url}
80+
target="_blank"
81+
className="underline"
82+
onClick={handleLinkClick}
83+
>
84+
{props.children}
85+
</Link>
86+
);
87+
};
88+
89+
export const CustomImage = ({
90+
src,
91+
...props
92+
}: JSX.IntrinsicElements['img']) => {
93+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
94+
const [imageUrl, setImageUrl] = useState<string>();
95+
96+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
97+
const getImageSrc = useCallback(async () => {
98+
if (typeof src !== 'string') return;
99+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
100+
const [path, queryString] = src.replace('asset://', '').split('?');
101+
}, [src]);
102+
103+
useEffect(() => {}, []);
104+
105+
return;
106+
107+
return imageUrl ? (
108+
<img {...props} alt={props.alt} src={imageUrl} />
109+
) : (
110+
<Skeleton className="my-4 h-[125px] w-full rounded-xl py-4 pt-8 text-center">
111+
<ImageIcon className="mx-auto size-12 opacity-20" />
112+
</Skeleton>
113+
);
114+
};
115+
116+
export const mdComponents = {
117+
h1: (props: JSX.IntrinsicElements['h1']) => (
118+
<h1 className="my-6 text-5xl font-bold first:mt-0 last:mb-0">
119+
{props.children}
120+
</h1>
121+
),
122+
h2: (props: JSX.IntrinsicElements['h2']) => (
123+
<h2 className="my-5 text-4xl font-bold first:mt-0 last:mb-0">
124+
{props.children}
125+
</h2>
126+
),
127+
h3: (props: JSX.IntrinsicElements['h3']) => (
128+
<h3 className="my-4 text-3xl font-bold first:mt-0 last:mb-0">
129+
{props.children}
130+
</h3>
131+
),
132+
h4: (props: JSX.IntrinsicElements['h4']) => (
133+
<h4 className="my-3 text-2xl font-bold first:mt-0 last:mb-0">
134+
{props.children}
135+
</h4>
136+
),
137+
h5: (props: JSX.IntrinsicElements['h5']) => (
138+
<h5 className="my-2 text-xl font-bold first:mt-0 last:mb-0">
139+
{props.children}
140+
</h5>
141+
),
142+
h6: (props: JSX.IntrinsicElements['h6']) => (
143+
<h6 className="my-2 text-lg font-bold first:mt-0 last:mb-0">
144+
{props.children}
145+
</h6>
146+
),
147+
p: (props: JSX.IntrinsicElements['p']) => (
148+
<div className="my-2 first:mt-0 last:mb-0">{props.children}</div>
149+
),
150+
blockquote: ({
151+
className,
152+
...props
153+
}: JSX.IntrinsicElements['blockquote']) => {
154+
return (
155+
<blockquote
156+
className={cn(
157+
'text-muted-foreground my-4 border-l-4 py-1 pl-4 first:mt-0 last:mb-0',
158+
className,
159+
)}
160+
>
161+
{props.children}
162+
</blockquote>
163+
);
164+
},
165+
img: ({ src, ...props }: JSX.IntrinsicElements['img']) => {
166+
if (!src) {
167+
return (
168+
<Skeleton className="my-4 h-[125px] w-full rounded-xl py-4 pt-8 text-center">
169+
<ImageIcon className="mx-auto size-12 opacity-20" />
170+
</Skeleton>
171+
);
172+
} else if (typeof src === 'string' && src.startsWith('asset://')) {
173+
return <CustomImage src={src} {...props} />;
174+
} else {
175+
return (
176+
<img
177+
src={src}
178+
width={props.width}
179+
height={props.height}
180+
alt={props.alt}
181+
title={props.title}
182+
style={{ maxWidth: '100%', height: 'auto' }}
183+
/>
184+
);
185+
}
186+
},
187+
pre: ({ className, ...props }: JSX.IntrinsicElements['pre']) => {
188+
return (
189+
<pre className={cn('my-4 overflow-x-auto', className)}>
190+
{props.children}
191+
</pre>
192+
);
193+
},
194+
code: ({ className, ...props }: JSX.IntrinsicElements['code']) => {
195+
const match = /language-(\w+)/.exec(className || '');
196+
const language = match?.[1];
197+
if (language) {
198+
if (language === 'mermaid') {
199+
return (
200+
<ChartMermaid>
201+
{typeof props.children === 'string' ? props.children : ''}
202+
</ChartMermaid>
203+
);
204+
} else {
205+
return (
206+
<code className={cn('rounded-md text-sm', className)}>
207+
{props.children}
208+
</code>
209+
);
210+
}
211+
} else {
212+
return (
213+
<code
214+
className={cn(
215+
'mx-1 inline-block overflow-x-auto rounded-md bg-gray-500/10 px-1.5 py-0.5 align-middle text-sm',
216+
className,
217+
)}
218+
>
219+
{props.children}
220+
</code>
221+
);
222+
}
223+
},
224+
ol: ({ className, ...props }: JSX.IntrinsicElements['ul']) => {
225+
return (
226+
<ul className={cn('my-4 list-decimal pl-4', className)}>
227+
{props.children}
228+
</ul>
229+
);
230+
},
231+
ul: ({ className, ...props }: JSX.IntrinsicElements['ul']) => {
232+
return (
233+
<ul className={cn('my-4 list-disc pl-4', className)}>{props.children}</ul>
234+
);
235+
},
236+
li: ({ className, ...props }: JSX.IntrinsicElements['li']) => {
237+
return (
238+
<li className={cn('my-1 list-item', className)}>{props.children}</li>
239+
);
240+
},
241+
nav: (props: JSX.IntrinsicElements['nav']) => {
242+
if (props.className === 'toc') {
243+
return <nav {...props} />;
244+
} else {
245+
return <nav {...props} />;
246+
}
247+
},
248+
table: (props: JSX.IntrinsicElements['table']) => (
249+
<div className="my-4 overflow-hidden rounded-lg border">
250+
<Table {...props} />
251+
</div>
252+
),
253+
thead: (props: JSX.IntrinsicElements['thead']) => <TableHeader {...props} />,
254+
tbody: (props: JSX.IntrinsicElements['tbody']) => <TableBody {...props} />,
255+
tr: (props: JSX.IntrinsicElements['tr']) => <TableRow {...props} />,
256+
td: (props: JSX.IntrinsicElements['td']) => (
257+
<TableCell>{props.children}</TableCell>
258+
),
259+
th: (props: JSX.IntrinsicElements['th']) => (
260+
<TableCell>{props.children}</TableCell>
261+
),
262+
};
263+
264+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
265+
export const mdRehypePlugins: any = [
266+
rehypeRaw,
267+
rehypeHighlight,
268+
rehypeHighlightLines,
269+
];
270+
271+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
272+
export const mdRemarkPlugins: any = [
273+
remarkGfm,
274+
remarkFrontmatter,
275+
remarkMdxFrontmatter,
276+
remarkGithubAdmonitionsToDirectives,
277+
remarkDirective,
278+
[
279+
remarkHeaderId,
280+
{
281+
defaults: true,
282+
},
283+
],
284+
];
285+
286+
export const Markdown = ({
287+
rehypeToc = false,
288+
security = false,
289+
children,
290+
}: {
291+
rehypeToc?: boolean;
292+
security?: boolean;
293+
children?: string;
294+
}) => {
295+
const rehypePlugins = useMemo(() => {
296+
const plugins = [...mdRehypePlugins];
297+
if (rehypeToc) {
298+
plugins.push([
299+
rehypeToc,
300+
{
301+
headings: ['h2', 'h3', 'h4', 'h5', 'h6'],
302+
},
303+
]);
304+
}
305+
return plugins;
306+
}, [rehypeToc]);
307+
308+
return (
309+
<ReactMarkdown
310+
rehypePlugins={rehypePlugins}
311+
remarkPlugins={mdRemarkPlugins}
312+
urlTransform={(url) => url}
313+
components={{
314+
a: security ? securityLink : unSecurityLink,
315+
...mdComponents,
316+
}}
317+
>
318+
{children}
319+
</ReactMarkdown>
320+
);
321+
};

0 commit comments

Comments
 (0)