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 ( / ^ h t t p / ) ? '_blank' : '_self' ;
34+ const url = props . href ?. replace ( / \. m d / , '' ) ;
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 ( / \. m d / , '' ) || '/' ;
55+ const isNavLink = props . className ?. includes ( 'toc-link' ) ;
56+ const handleLinkClick : MouseEventHandler < HTMLAnchorElement > = ( e ) => {
57+ if ( ! url . match ( / h t t p / ) ) {
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 = / l a n g u a g e - ( \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