-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmdx-components.tsx
More file actions
167 lines (160 loc) · 5.91 KB
/
Copy pathmdx-components.tsx
File metadata and controls
167 lines (160 loc) · 5.91 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import type { MDXComponents } from 'mdx/types'
import NextImage from 'next/image'
import { Link } from 'next-view-transitions'
import {
Children,
type ComponentPropsWithoutRef,
isValidElement,
type ReactElement,
type ReactNode,
} from 'react'
import Aside from '@/ui/Aside'
import highlighter, { themes } from './lib/highlighter/highlighter'
import { addCopyButton } from './lib/utils/addCopyButton'
import { stripCodeTags } from './lib/utils/stripCodeTags'
import { stripInlineStyles } from './lib/utils/stripInlineStyles'
type HeadingProps = ComponentPropsWithoutRef<'h1'>
type ParagraphProps = ComponentPropsWithoutRef<'p'>
type ListProps = ComponentPropsWithoutRef<'ul'>
type ListItemProps = ComponentPropsWithoutRef<'li'>
type AnchorProps = ComponentPropsWithoutRef<'a'>
type BlockquoteProps = ComponentPropsWithoutRef<'blockquote'>
type ImageProps = ComponentPropsWithoutRef<'img'> & { caption?: string }
function isCodeElement(child: ReactNode): child is ReactElement<ComponentPropsWithoutRef<'code'>> {
return isValidElement(child) && typeof child.type === 'function' && child.type.name === 'code'
}
const components: MDXComponents = {
h1: (props: HeadingProps) => (
<h1 className='fade-in mb-0 pt-12 font-light font-sans text-4xl' {...props} />
),
h2: (props: HeadingProps) => (
<h2 className='mt-8 mb-5 font-medium text-3xl text-text-primary dark:text-link' {...props} />
),
h3: (props: HeadingProps) => (
<h3 className='mt-8 mb-3 font-medium text-contrast text-xl' {...props} />
),
h4: (props: HeadingProps) => (
<h4 className='mt-8 mb-3 font-medium text-contrast text-lg' {...props} />
),
p: (props: ParagraphProps) => <p className='mb-4' {...props} />,
ol: (props: ListProps) => <ol className='list-decimal space-y-1 pl-5' {...props} />,
ul: (props: ListProps) => <ul className='list-disc space-y-0 pl-5' {...props} />,
li: (props: ListItemProps) => <li className='pl-1' {...props} />,
em: (props: ComponentPropsWithoutRef<'em'>) => <em className='italic' {...props} />,
strong: (props: ComponentPropsWithoutRef<'strong'>) => (
<strong className='font-medium' {...props} />
),
a: ({ href, children, ...props }: AnchorProps) => {
const className =
'text-link font-normal hover:underline hover:text-link-hover visited:text-link-visited'
if (href?.startsWith('/')) {
return (
<Link href={href} className={className} {...props}>
{children}
</Link>
)
}
if (href?.startsWith('#')) {
return (
<a href={href} className={className} {...props}>
{children}
</a>
)
}
return (
<a href={href} target='_blank' rel='noopener noreferrer' className={className} {...props}>
{children}
</a>
)
},
code: async ({ children, ...props }: ComponentPropsWithoutRef<'code'>) => {
const lang = props.className ? props.className.split('-')[1] : 'text'
const codeHTML = await highlighter.codeToHtml(children as string, {
lang,
themes: { light: themes.light, dark: themes.dark },
})
// highlighter adds tags which we remove - next/mdx has already inserted pre/code tags
// biome-ignore lint/security/noDangerouslySetInnerHtml: Trusted author
return <code dangerouslySetInnerHTML={{ __html: stripCodeTags(codeHTML) }} {...props} />
},
pre: async ({ children, ...props }: ComponentPropsWithoutRef<'pre'>) => {
// Select the <code> element inside the <pre>
const codeElement = Children.toArray(children)[0]
if (isCodeElement(codeElement)) {
const lang = codeElement.props.className?.split('-')[1] || 'text'
const codeHTML = await highlighter.codeToHtml(codeElement.props.children as string, {
lang,
themes: { light: themes.light, dark: themes.dark },
transformers: [stripInlineStyles(), addCopyButton({ toggle: 2000 })],
})
return (
<div
className={`code-block language-${lang}`}
// biome-ignore lint/security/noDangerouslySetInnerHtml: Trusted source from highlighter
dangerouslySetInnerHTML={{ __html: codeHTML }}
/>
)
}
return <pre>{children}</pre>
},
Table: ({ data }: { data: { headers: string[]; rows: string[][] } }) => (
<table>
<thead>
<tr>
{data.headers.map((header, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Always same order
<th key={index}>{header}</th>
))}
</tr>
</thead>
<tbody>
{data.rows.map((row, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Always same order
<tr key={index}>
{row.map((cell, cellIndex) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Always same order
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
),
blockquote: (props: BlockquoteProps) => (
<blockquote
className='mb-2 ml-2 border-text-primary border-l-2 px-5 py-1 text-md text-text-prose/70 leading-7 [&>p]:mb-0'
{...props}
/>
),
img: (props: ImageProps) => Image(props),
}
export function useMDXComponents(otherComponents: MDXComponents): MDXComponents {
return {
Aside,
Image,
...components,
...otherComponents,
}
}
function Image(props: ImageProps) {
const { caption, src, alt, ...rest } = props
const parsedWidth = Number(props.width)
const width = !Number.isNaN(parsedWidth) ? parsedWidth : 800
return (
<figure className='my-6 flex flex-col gap-4'>
<NextImage
className='mx-auto block h-auto max-w-full rounded-md bg-bg-light xl:max-w-100%'
src={src as string}
alt={alt || ''}
{...rest}
width={width} // Use incoming width or default to 800
height={800}
/>
{caption && (
<figcaption className='block text-center font-detail text-text-primary text-xs'>
{caption}
</figcaption>
)}
</figure>
)
}