Skip to content

Commit fa368ab

Browse files
committed
feat: use mdx-bundler and shiki for syntax highlighting
1 parent 02c0990 commit fa368ab

17 files changed

+2177
-1371
lines changed

Diff for: app/components/CodeBlock.tsx

+23-65
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,25 @@
1-
import { useState, type ReactNode } from 'react'
2-
import invariant from 'tiny-invariant'
3-
import type { Language } from 'prism-react-renderer'
4-
import { Highlight, Prism } from 'prism-react-renderer'
1+
import * as React from 'react'
52
import { FaCopy } from 'react-icons/fa'
6-
import { svelteHighlighter } from '~/utils/svelteHighlighter'
7-
// Add back additional language support after `prism-react` upgrade
8-
;(typeof global !== 'undefined' ? global : window).Prism = Prism
9-
// @ts-expect-error
10-
import('prismjs/components/prism-diff')
11-
// @ts-expect-error
12-
import('prismjs/components/prism-bash')
13-
14-
// @ts-ignore Alias markup as vue highlight
15-
Prism.languages.vue = Prism.languages.markup
16-
17-
// Enable svelte syntax highlighter
18-
svelteHighlighter()
19-
20-
function getLanguageFromClassName(className: string) {
21-
const match = className.match(/language-(\w+)/)
22-
return match ? match[1] : ''
23-
}
3+
import invariant from 'tiny-invariant'
244

25-
function isLanguageSupported(lang: string): lang is Language {
26-
return lang in Prism.languages
5+
function getLanguageFromChildren(children: any): string | undefined {
6+
const language = children[0]?.props?.children
7+
return language ? language : undefined
278
}
289

29-
type Props = {
30-
children: ReactNode
31-
}
10+
export const CodeBlock = (props: React.HTMLProps<HTMLPreElement>) => {
11+
invariant(!!props.children, 'children is required')
12+
const lang = getLanguageFromChildren(props.children)
13+
const [copied, setCopied] = React.useState(false)
14+
const ref = React.useRef<HTMLPreElement>(null)
3215

33-
export const CodeBlock = ({ children }: Props) => {
34-
invariant(!!children, 'children is required')
35-
const [copied, setCopied] = useState(false)
36-
const child = Array.isArray(children) ? children[0] : children
37-
const className = child.props.className || ''
38-
const userLang = getLanguageFromClassName(className)
39-
const lang = isLanguageSupported(userLang) ? userLang : 'bash'
40-
const code = Array.isArray(child.props.children)
41-
? child.props.children[0]
42-
: child.props.children
4316
return (
4417
<div className="w-full max-w-full relative">
4518
<button
4619
className="absolute right-1 top-3 z-10 p-2 group flex items-center"
4720
onClick={() => {
21+
navigator.clipboard.writeText(ref.current?.innerText || '')
4822
setCopied(true)
49-
navigator.clipboard.writeText(code.trim())
5023
setTimeout(() => setCopied(false), 2000)
5124
}}
5225
aria-label="Copy code to clipboard"
@@ -57,34 +30,19 @@ export const CodeBlock = ({ children }: Props) => {
5730
<FaCopy className="text-gray-500 group-hover:text-gray-100 dark:group-hover:text-gray-200 transition duration-200" />
5831
)}
5932
</button>
60-
<div className="relative not-prose">
61-
<div
62-
className="absolute bg-white text-sm z-10 border border-gray-300 px-2 rounded-md -top-3 right-2
63-
dark:bg-gray-600 dark:border-0"
33+
<div className="relative not-prose w-full max-w-full">
34+
{lang ? (
35+
<div className="absolute bg-white text-sm z-10 border border-gray-500/20 px-2 rounded-md -top-3 right-2 dark:bg-gray-600">
36+
{lang}
37+
</div>
38+
) : null}
39+
<pre
40+
className={`${props.className} m-0 rounded-md w-full border border-gray-500/20 dark:border-gray-500/30`}
41+
style={props.style}
42+
ref={ref}
6443
>
65-
{lang}
66-
</div>
67-
<div className="rounded-md font-normal w-full border border-gray-200 dark:border-0">
68-
<Highlight code={code.trim()} language={lang}>
69-
{({ className, tokens, getLineProps, getTokenProps }) => (
70-
<pre className={`overflow-scroll ${className}`} style={{}}>
71-
<code className={className} style={{}}>
72-
{tokens.map((line, i) => (
73-
<div key={i} {...getLineProps({ line, key: i })} style={{}}>
74-
{line.map((token, key) => (
75-
<span
76-
key={key}
77-
{...getTokenProps({ token, key })}
78-
style={{}}
79-
/>
80-
))}
81-
</div>
82-
))}
83-
</code>
84-
</pre>
85-
)}
86-
</Highlight>
87-
</div>
44+
{props.children}
45+
</pre>
8846
</div>
8947
</div>
9048
)

Diff for: app/components/Doc.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FaEdit } from 'react-icons/fa'
22
import { DocTitle } from '~/components/DocTitle'
3-
import { RenderMarkdown } from '~/components/RenderMarkdown'
3+
import { Mdx } from '~/components/RenderMarkdown'
44

55
export function Doc({
66
title,
@@ -22,7 +22,7 @@ export function Doc({
2222
<div className="h-px bg-gray-500 opacity-20" />
2323
<div className="h-4" />
2424
<div className="prose prose-gray prose-md dark:prose-invert max-w-none">
25-
<RenderMarkdown>{content}</RenderMarkdown>
25+
<Mdx code={content} />
2626
</div>
2727
<div className="h-12" />
2828
<div className="w-full h-px bg-gray-500 opacity-30" />

Diff for: app/components/RenderMarkdown.tsx

+32-32
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1+
import { useMemo } from 'react'
2+
import { getMDXComponent } from 'mdx-bundler/client'
13
import { CodeBlock } from '~/components/CodeBlock'
24
import { MarkdownLink } from '~/components/MarkdownLink'
3-
import type { FC, HTMLProps } from 'react'
4-
import ReactMarkdown from 'react-markdown'
5-
import rehypeSlug from 'rehype-slug'
6-
import remarkGfm from 'remark-gfm'
7-
import rehypeRaw from 'rehype-raw'
5+
import type { HTMLProps } from 'react'
86

97
const CustomHeading = ({
108
Comp,
@@ -34,7 +32,7 @@ const makeHeading =
3432
/>
3533
)
3634

37-
const defaultComponents: Record<string, FC> = {
35+
const markdownComponents: Record<string, React.FC> = {
3836
a: MarkdownLink,
3937
pre: CodeBlock,
4038
h1: makeHeading('h1'),
@@ -43,34 +41,36 @@ const defaultComponents: Record<string, FC> = {
4341
h4: makeHeading('h4'),
4442
h5: makeHeading('h5'),
4543
h6: makeHeading('h6'),
46-
iframe: (props) => <iframe {...props} className="w-full" />,
47-
code: ({ className = '', ...props }: React.HTMLProps<HTMLElement>) => {
48-
return (
49-
<code
50-
{...props}
51-
className={`border border-gray-500 border-opacity-20 bg-gray-500 bg-opacity-10 rounded p-1${
52-
className ?? ` ${className}`
53-
}`}
54-
/>
55-
)
44+
code: (props: HTMLProps<HTMLElement>) => {
45+
const { className, children } = props
46+
if (typeof children === 'string') {
47+
// For inline code, this adds a background and outline
48+
return (
49+
<code
50+
{...props}
51+
className={`border border-gray-500 border-opacity-20 bg-gray-500 bg-opacity-10 rounded p-1${
52+
className ?? ` ${className}`
53+
}`}
54+
/>
55+
)
56+
} else {
57+
// For Shiki code blocks, this does nothing
58+
return <code {...props} />
59+
}
5660
},
61+
iframe: (props) => (
62+
<iframe {...props} className="w-full" title="Embedded Content" />
63+
),
5764
}
5865

59-
type Props = {
60-
children: string
61-
components?: Record<string, FC>
62-
}
63-
64-
export const RenderMarkdown = (props: Props) => {
65-
const { components, children } = props
66+
export function Mdx({
67+
code,
68+
components,
69+
}: {
70+
code: string
71+
components?: Record<string, React.FC>
72+
}) {
73+
const Doc = useMemo(() => getMDXComponent(code), [code])
6674

67-
return (
68-
<ReactMarkdown
69-
plugins={[remarkGfm]}
70-
rehypePlugins={[rehypeSlug, rehypeRaw]}
71-
components={{ ...defaultComponents, ...components }}
72-
>
73-
{children}
74-
</ReactMarkdown>
75-
)
75+
return <Doc components={{ ...markdownComponents, ...components }} />
7676
}

Diff for: app/routes/__root.tsx

-12
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
} from '@tanstack/react-router'
1010
import appCss from '~/styles/app.css?url'
1111
import carbonStyles from '~/styles/carbon.css?url'
12-
import prismThemeLight from '~/styles/prismThemeLight.css?url'
13-
import prismThemeDark from '~/styles/prismThemeDark.css?url'
1412
import { seo } from '~/utils/seo'
1513
import ogImage from '~/images/og.png'
1614
import { Meta, RouterManagedTag, Scripts } from '@tanstack/react-router-server'
@@ -41,16 +39,6 @@ export const Route = createRootRouteWithContext<{
4139
],
4240
links: () => [
4341
{ rel: 'stylesheet', href: appCss },
44-
{
45-
rel: 'stylesheet',
46-
href: prismThemeLight,
47-
media: '(prefers-color-scheme: light)',
48-
},
49-
{
50-
rel: 'stylesheet',
51-
href: prismThemeDark,
52-
media: '(prefers-color-scheme: dark)',
53-
},
5442
{
5543
rel: 'stylesheet',
5644
href: carbonStyles,

Diff for: app/routes/blog.index.tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Link, createFileRoute, notFound } from '@tanstack/react-router'
22

33
import { getPostList } from '~/utils/blog'
44
import { DocTitle } from '~/components/DocTitle'
5-
import { RenderMarkdown } from '~/components/RenderMarkdown'
5+
import { Mdx } from '~/components/RenderMarkdown'
66
import { format } from 'date-fns'
77
import { Footer } from '~/components/Footer'
88
import { extractFrontMatter, fetchRepoFile } from '~/utils/documents.server'
@@ -94,13 +94,12 @@ function BlogIndex() {
9494
</div>
9595
) : null}
9696
<div className={`text-sm mt-2 text-black dark:text-white`}>
97-
<RenderMarkdown
97+
<Mdx
9898
components={{
9999
a: (props) => <span {...props} />,
100100
}}
101-
>
102-
{excerpt || ''}
103-
</RenderMarkdown>
101+
code={excerpt || ''}
102+
/>
104103
</div>
105104
</div>
106105
<div>

0 commit comments

Comments
 (0)