Skip to content

Commit dd835d6

Browse files
committed
feat: added syntax highlighting to the docs!
1 parent af63b06 commit dd835d6

File tree

3 files changed

+168
-8
lines changed

3 files changed

+168
-8
lines changed

bun.lock

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"pg": "^8.14.1",
7979
"pino": "^9.6.0",
8080
"posthog-js": "^1.234.10",
81+
"prism-react-renderer": "^2.4.1",
8182
"react": "^18",
8283
"react-dom": "^18",
8384
"react-hook-form": "^7.55.0",

web/src/components/docs/mdx/code-demo.tsx

Lines changed: 161 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

33
import { Check, Copy } from 'lucide-react'
4+
import { Highlight, themes } from 'prism-react-renderer'
45
import { useMemo, useState } from 'react'
56

6-
import { MermaidDiagram } from './mermaid-diagram'
7-
87
import { Separator } from '@/components/ui/separator'
98

9+
import { MermaidDiagram } from './mermaid-diagram'
10+
1011
type CodeDemoChildren = string | JSX.Element | JSX.Element[]
1112

1213
interface CodeDemoProps {
@@ -33,6 +34,107 @@ const getContent = (c: CodeDemoChildren): string => {
3334
return ''
3435
}
3536

37+
const trimLeadingWhitespace = (content: string): string => {
38+
const lines = content.split('\n')
39+
const nonEmptyLines = lines.filter((line) => line.trim() !== '')
40+
41+
if (nonEmptyLines.length === 0) return content
42+
43+
// Find the minimum indentation among non-empty lines
44+
const minIndent = Math.min(
45+
...nonEmptyLines.map((line) => {
46+
const match = line.match(/^\s*/)
47+
return match ? match[0].length : 0
48+
})
49+
)
50+
51+
// Remove the minimum indentation from all lines
52+
const trimmedLines = lines.map((line) => {
53+
if (line.trim() === '') return line // Keep empty lines as-is
54+
return line.slice(minIndent)
55+
})
56+
57+
return trimmedLines.join('\n')
58+
}
59+
60+
// Map common language aliases to Prism language identifiers
61+
const languageMap: Record<string, string> = {
62+
js: 'javascript',
63+
ts: 'typescript',
64+
jsx: 'jsx',
65+
tsx: 'tsx',
66+
py: 'python',
67+
sh: 'bash',
68+
shell: 'bash',
69+
yml: 'yaml',
70+
md: 'markdown',
71+
json: 'json',
72+
html: 'html',
73+
css: 'css',
74+
sql: 'sql',
75+
go: 'go',
76+
rust: 'rust',
77+
java: 'java',
78+
php: 'php',
79+
ruby: 'ruby',
80+
c: 'c',
81+
cpp: 'cpp',
82+
'c++': 'cpp',
83+
csharp: 'csharp',
84+
'c#': 'csharp',
85+
swift: 'swift',
86+
kotlin: 'kotlin',
87+
scala: 'scala',
88+
dart: 'dart',
89+
dockerfile: 'docker',
90+
yaml: 'yaml',
91+
toml: 'toml',
92+
ini: 'ini',
93+
xml: 'xml',
94+
graphql: 'graphql',
95+
prisma: 'prisma',
96+
}
97+
98+
// Language-specific color constants
99+
const LANGUAGE_COLORS = {
100+
bash: '#8FE457', // BetweenGreen for bash commands
101+
white: '#ffffff', // White for text/plain/markdown
102+
default: null, // Use theme default for other languages
103+
} as const
104+
105+
// Define which languages should use which color scheme
106+
const LANGUAGE_COLOR_MAP: Record<string, keyof typeof LANGUAGE_COLORS> = {
107+
bash: 'bash',
108+
text: 'white',
109+
plain: 'white',
110+
markdown: 'white',
111+
}
112+
113+
const getLanguageTheme = (language: string) => {
114+
const baseTheme = themes.vsDark
115+
const colorScheme = LANGUAGE_COLOR_MAP[language]
116+
const overrideColor = colorScheme && LANGUAGE_COLORS[colorScheme]
117+
118+
// For white-only languages, use minimal theme with no token styles
119+
if (colorScheme === 'white') {
120+
return {
121+
theme: {
122+
plain: { color: '#ffffff', backgroundColor: 'transparent' },
123+
styles: [],
124+
},
125+
tokenColor: '#ffffff',
126+
}
127+
}
128+
129+
return {
130+
theme: {
131+
...baseTheme,
132+
plain: { ...baseTheme.plain, backgroundColor: 'transparent' },
133+
},
134+
tokenColor: overrideColor || null,
135+
}
136+
}
137+
36138
export function CodeDemo({ children, language, rawContent }: CodeDemoProps) {
37139
const [copied, setCopied] = useState(false)
38140

@@ -50,7 +152,7 @@ export function CodeDemo({ children, language, rawContent }: CodeDemoProps) {
50152
const childrenContent = useMemo(() => {
51153
// Use rawContent if available (from remark plugin), otherwise fall back to processing children
52154
const content = rawContent || getContent(children)
53-
return content
155+
return trimLeadingWhitespace(content)
54156
}, [children, language, rawContent])
55157

56158
// Check if this is a mermaid diagram
@@ -83,8 +185,24 @@ export function CodeDemo({ children, language, rawContent }: CodeDemoProps) {
83185
)
84186
}
85187

188+
// Normalize language and get theme/color in one useMemo
189+
const {
190+
normalizedLanguage,
191+
theme: highlightTheme,
192+
tokenColor,
193+
} = useMemo(() => {
194+
const normalized = language.toLowerCase().trim()
195+
const normalizedLang = languageMap[normalized] || normalized
196+
const { theme, tokenColor } = getLanguageTheme(normalizedLang)
197+
return {
198+
normalizedLanguage: normalizedLang,
199+
theme,
200+
tokenColor,
201+
}
202+
}, [language])
203+
86204
return (
87-
<div className="rounded-lg border bg-muted/30 px-4 w-full max-w-80 md:max-w-full my-3 transition-all group hover:bg-muted/40 overflow-x-auto">
205+
<div className="rounded-lg border px-4 w-full max-w-80 md:max-w-full my-3 transition-all group overflow-x-auto">
88206
<div className="flex items-center justify-between h-6 mt-0.5 mb-0.5">
89207
<div className="text-[10px] text-muted-foreground/40 font-mono tracking-wide">
90208
{language.toLowerCase()}
@@ -103,9 +221,45 @@ export function CodeDemo({ children, language, rawContent }: CodeDemoProps) {
103221
</div>
104222
{language && <Separator className="bg-border/20 mb-0.5" />}
105223
<div>
106-
<pre className="text-[13px] leading-relaxed py-1 bg-transparent text-foreground/90 rounded-lg scrollbar-thin scrollbar-thumb-muted-foreground/10 scrollbar-track-transparent">
107-
<code className="font-mono">{childrenContent}</code>
108-
</pre>
224+
<Highlight
225+
theme={highlightTheme}
226+
code={childrenContent}
227+
language={normalizedLanguage}
228+
>
229+
{({ className, style, tokens, getLineProps, getTokenProps }) => {
230+
return (
231+
<pre
232+
className={`${className} text-[13px] leading-relaxed py-2 bg-transparent rounded-lg scrollbar-thin scrollbar-thumb-muted-foreground/10 scrollbar-track-transparent`}
233+
style={{
234+
...style,
235+
backgroundColor: 'transparent',
236+
color: tokenColor || style.color,
237+
}}
238+
>
239+
{tokens.map((line, i) => (
240+
<div key={i} {...getLineProps({ line })}>
241+
{line.map((token, key) => {
242+
const tokenProps = getTokenProps({ token, key })
243+
// Override colors for special languages in render loop
244+
const color = tokenColor || tokenProps.style?.color
245+
246+
return (
247+
<span
248+
key={key}
249+
{...tokenProps}
250+
style={{
251+
...tokenProps.style,
252+
color,
253+
}}
254+
/>
255+
)
256+
})}
257+
</div>
258+
))}
259+
</pre>
260+
)
261+
}}
262+
</Highlight>
109263
</div>
110264
</div>
111265
)

0 commit comments

Comments
 (0)