22
33import { Button } from "@repo/shadcn-ui/components/ui/button" ;
44import { cn } from "@repo/shadcn-ui/lib/utils" ;
5+ import type { Element } from "hast" ;
56import { CheckIcon , CopyIcon } from "lucide-react" ;
6- import type { ComponentProps , HTMLAttributes , ReactNode } from "react" ;
7- import { createContext , useContext , useState } from "react" ;
8- import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
97import {
10- oneDark ,
11- oneLight ,
12- } from "react-syntax-highlighter/dist/esm/styles/prism" ;
8+ type ComponentProps ,
9+ createContext ,
10+ type HTMLAttributes ,
11+ useContext ,
12+ useEffect ,
13+ useRef ,
14+ useState ,
15+ } from "react" ;
16+ import { type BundledLanguage , codeToHtml , type ShikiTransformer } from "shiki" ;
17+
18+ type CodeBlockProps = HTMLAttributes < HTMLDivElement > & {
19+ code : string ;
20+ language : BundledLanguage ;
21+ showLineNumbers ?: boolean ;
22+ } ;
1323
1424type CodeBlockContextType = {
1525 code : string ;
@@ -19,85 +29,106 @@ const CodeBlockContext = createContext<CodeBlockContextType>({
1929 code : "" ,
2030} ) ;
2131
22- export type CodeBlockProps = HTMLAttributes < HTMLDivElement > & {
23- code : string ;
24- language : string ;
25- showLineNumbers ?: boolean ;
26- children ?: ReactNode ;
32+ const lineNumberTransformer : ShikiTransformer = {
33+ name : "line-numbers" ,
34+ line ( node : Element , line : number ) {
35+ node . children . unshift ( {
36+ type : "element" ,
37+ tagName : "span" ,
38+ properties : {
39+ className : [
40+ "inline-block" ,
41+ "min-w-10" ,
42+ "mr-4" ,
43+ "text-right" ,
44+ "select-none" ,
45+ "text-muted-foreground" ,
46+ ] ,
47+ } ,
48+ children : [ { type : "text" , value : String ( line ) } ] ,
49+ } ) ;
50+ } ,
2751} ;
2852
53+ export async function highlightCode (
54+ code : string ,
55+ language : BundledLanguage ,
56+ showLineNumbers = false
57+ ) {
58+ const transformers : ShikiTransformer [ ] = showLineNumbers
59+ ? [ lineNumberTransformer ]
60+ : [ ] ;
61+
62+ return await Promise . all ( [
63+ codeToHtml ( code , {
64+ lang : language ,
65+ theme : "one-light" ,
66+ transformers,
67+ } ) ,
68+ codeToHtml ( code , {
69+ lang : language ,
70+ theme : "one-dark-pro" ,
71+ transformers,
72+ } ) ,
73+ ] ) ;
74+ }
75+
2976export const CodeBlock = ( {
3077 code,
3178 language,
3279 showLineNumbers = false ,
3380 className,
3481 children,
3582 ...props
36- } : CodeBlockProps ) => (
37- < CodeBlockContext . Provider value = { { code } } >
38- < div
39- className = { cn (
40- "relative w-full overflow-hidden rounded-md border bg-background text-foreground" ,
41- className
42- ) }
43- { ...props }
44- >
45- < div className = "relative" >
46- < SyntaxHighlighter
47- className = "overflow-hidden dark:hidden"
48- codeTagProps = { {
49- className : "font-mono text-sm" ,
50- } }
51- customStyle = { {
52- margin : 0 ,
53- padding : "1rem" ,
54- fontSize : "0.875rem" ,
55- background : "hsl(var(--background))" ,
56- color : "hsl(var(--foreground))" ,
57- } }
58- language = { language }
59- lineNumberStyle = { {
60- color : "hsl(var(--muted-foreground))" ,
61- paddingRight : "1rem" ,
62- minWidth : "2.5rem" ,
63- } }
64- showLineNumbers = { showLineNumbers }
65- style = { oneLight }
66- >
67- { code }
68- </ SyntaxHighlighter >
69- < SyntaxHighlighter
70- className = "hidden overflow-hidden dark:block"
71- codeTagProps = { {
72- className : "font-mono text-sm" ,
73- } }
74- customStyle = { {
75- margin : 0 ,
76- padding : "1rem" ,
77- fontSize : "0.875rem" ,
78- background : "hsl(var(--background))" ,
79- color : "hsl(var(--foreground))" ,
80- } }
81- language = { language }
82- lineNumberStyle = { {
83- color : "hsl(var(--muted-foreground))" ,
84- paddingRight : "1rem" ,
85- minWidth : "2.5rem" ,
86- } }
87- showLineNumbers = { showLineNumbers }
88- style = { oneDark }
89- >
90- { code }
91- </ SyntaxHighlighter >
92- { children && (
93- < div className = "absolute top-2 right-2 flex items-center gap-2" >
94- { children }
95- </ div >
83+ } : CodeBlockProps ) => {
84+ const [ html , setHtml ] = useState < string > ( "" ) ;
85+ const [ darkHtml , setDarkHtml ] = useState < string > ( "" ) ;
86+ const mounted = useRef ( false ) ;
87+
88+ useEffect ( ( ) => {
89+ highlightCode ( code , language , showLineNumbers ) . then ( ( [ light , dark ] ) => {
90+ if ( ! mounted . current ) {
91+ setHtml ( light ) ;
92+ setDarkHtml ( dark ) ;
93+ mounted . current = true ;
94+ }
95+ } ) ;
96+
97+ return ( ) => {
98+ mounted . current = false ;
99+ } ;
100+ } , [ code , language , showLineNumbers ] ) ;
101+
102+ return (
103+ < CodeBlockContext . Provider value = { { code } } >
104+ < div
105+ className = { cn (
106+ "group relative w-full overflow-hidden rounded-md border bg-background text-foreground" ,
107+ className
96108 ) }
109+ { ...props }
110+ >
111+ < div className = "relative" >
112+ < div
113+ className = "overflow-hidden dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
114+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
115+ dangerouslySetInnerHTML = { { __html : html } }
116+ />
117+ < div
118+ className = "hidden overflow-hidden dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
119+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
120+ dangerouslySetInnerHTML = { { __html : darkHtml } }
121+ />
122+ { children && (
123+ < div className = "absolute top-2 right-2 flex items-center gap-2" >
124+ { children }
125+ </ div >
126+ ) }
127+ </ div >
97128 </ div >
98- </ div >
99- </ CodeBlockContext . Provider >
100- ) ;
129+ </ CodeBlockContext . Provider >
130+ ) ;
131+ } ;
101132
102133export type CodeBlockCopyButtonProps = ComponentProps < typeof Button > & {
103134 onCopy ?: ( ) => void ;
@@ -117,7 +148,7 @@ export const CodeBlockCopyButton = ({
117148 const { code } = useContext ( CodeBlockContext ) ;
118149
119150 const copyToClipboard = async ( ) => {
120- if ( typeof window === "undefined" || ! navigator . clipboard . writeText ) {
151+ if ( typeof window === "undefined" || ! navigator ? .clipboard ? .writeText ) {
121152 onError ?.( new Error ( "Clipboard API not available" ) ) ;
122153 return ;
123154 }
0 commit comments