11"use client" ;
22
33import { useState , useEffect } from "react" ;
4- import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
5- import {
6- vscDarkPlus ,
7- vs ,
8- } from "react-syntax-highlighter/dist/esm/styles/prism" ;
4+ import { codeToHtml } from "shiki" ;
95import { Check , Copy } from "lucide-react" ;
106import { Button } from "@/components/ui/button" ;
117import { toast } from "sonner" ;
@@ -23,103 +19,171 @@ export const CodeBlock = ({
2319 showLineNumbers = true ,
2420} : CodeBlockProps ) => {
2521 const [ copied , setCopied ] = useState ( false ) ;
26- const { theme, resolvedTheme } = useTheme ( ) ;
22+ const [ highlightedCode , setHighlightedCode ] = useState < string > ( "" ) ;
23+ const { resolvedTheme } = useTheme ( ) ;
2724 const [ mounted , setMounted ] = useState ( false ) ;
2825
29- // Prevent hydration mismatch
26+ // Calculate line count and create line numbers array
27+ const lines = code . split ( '\n' ) ;
28+ const lineCount = lines . length ;
29+ const maxLineNumberWidth = lineCount . toString ( ) . length ;
30+
3031 useEffect ( ( ) => {
3132 setMounted ( true ) ;
3233 } , [ ] ) ;
3334
34- const currentTheme = mounted ? resolvedTheme || theme : "dark" ;
35- const syntaxTheme = currentTheme === "dark" ? vscDarkPlus : vs ;
35+ useEffect ( ( ) => {
36+ const generateHighlight = async ( ) => {
37+ try {
38+ const theme = mounted && resolvedTheme === "dark" ? "github-dark" : "github-light" ;
39+ const html = await codeToHtml ( code , {
40+ lang : language ,
41+ theme,
42+ } ) ;
43+
44+ // If line numbers are enabled, process the HTML to add line number structure
45+ if ( showLineNumbers ) {
46+ const processedHtml = addLineNumbersToHtml ( html , lineCount ) ;
47+ setHighlightedCode ( processedHtml ) ;
48+ } else {
49+ setHighlightedCode ( html ) ;
50+ }
51+ } catch ( error ) {
52+ console . error ( "Highlighting error:" , error ) ;
53+ setHighlightedCode ( `<pre><code>${ code } </code></pre>` ) ;
54+ }
55+ } ;
56+
57+ generateHighlight ( ) ;
58+ } , [ code , language , mounted , resolvedTheme , showLineNumbers , lineCount ] ) ;
59+
60+ // Function to add line numbers to the highlighted HTML
61+ const addLineNumbersToHtml = ( html : string , totalLines : number ) => {
62+ // Split HTML by newlines and add line number structure
63+ const htmlLines = html . split ( '\n' ) ;
64+ const preMatch = html . match ( / < p r e [ ^ > ] * > / ) ;
65+ const codeMatch = html . match ( / < c o d e [ ^ > ] * > / ) ;
66+ const preOpenTag = preMatch ? preMatch [ 0 ] : '<pre>' ;
67+ const codeOpenTag = codeMatch ? codeMatch [ 0 ] : '<code>' ;
68+
69+ // Extract the content between <code> and </code>
70+ const codeContent = html . match ( / < c o d e [ ^ > ] * > ( [ \s \S ] * ?) < \/ c o d e > / ) ?. [ 1 ] || '' ;
71+ const lines = codeContent . split ( '\n' ) ;
72+
73+ // Wrap each line with line number data
74+ const numberedLines = lines . map ( ( line , index ) => {
75+ const lineNumber = index + 1 ;
76+ return `<span class="code-line" data-line-number="${ lineNumber } ">${ line } </span>` ;
77+ } ) . join ( '\n' ) ;
78+
79+ return `${ preOpenTag } ${ codeOpenTag } ${ numberedLines } </code></pre>` ;
80+ } ;
3681
3782 const handleCopy = async ( ) => {
3883 try {
3984 await navigator . clipboard . writeText ( code ) ;
4085 setCopied ( true ) ;
41- toast . success ( "Code copied to clipboard!" , {
42- duration : 2000 ,
43- } ) ;
86+ toast . success ( "Code copied to clipboard!" ) ;
4487 setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
4588 } catch ( err ) {
4689 toast . error ( "Failed to copy code" ) ;
4790 }
4891 } ;
4992
5093 return (
51- < div className = "relative rounded-xl overflow-hidden border border-border bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md transition-shadow " >
52- { /* macOS-style Header with Traffic Lights */ }
53- < div className = "flex items-center justify-between px-4 py-2.5 border-b border-border bg-secondary/80 dark:bg-secondary/50 " >
94+ < div className = "group relative overflow-hidden rounded-xl border border-primary/20 bg-card/50 shadow-lg transition-all duration-300 hover:shadow-xl hover:border-primary/30 " >
95+ { /* Header with primary color accents */ }
96+ < div className = "flex items-center justify-between px-4 py-3 border-b border-primary/20 bg-gradient-to-r from-primary/5 to-primary/10 " >
5497 < div className = "flex items-center gap-3" >
55- { /* macOS Traffic Light Dots */ }
56- < div className = "flex items-center gap-1.5" >
57- < div className = "w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-colors " />
58- < div className = "w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-colors " />
59- < div className = "w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-colors " />
98+ { /* VS Code style dots with primary color */ }
99+ < div className = "flex gap-1.5" >
100+ < div className = "w-3 h-3 rounded-full bg-primary/60 " />
101+ < div className = "w-3 h-3 rounded-full bg-primary/40 " />
102+ < div className = "w-3 h-3 rounded-full bg-primary/20 " />
60103 </ div >
61- { /* Language Label */ }
62- < span className = "text-xs sm:text-sm font-mono text-muted-foreground uppercase tracking-wider font-semibold" >
104+ < span className = "text-xs font-mono text-primary font-semibold uppercase tracking-wider" >
63105 { language }
64106 </ span >
65107 </ div >
66108
67- { /* Copy Button */ }
68109 < Button
69110 variant = "ghost"
70111 size = "sm"
71112 onClick = { handleCopy }
72- className = "h-7 sm:h-8 px-2 sm:px-3 hover:bg-accent/50 transition-all"
73- aria-label = { copied ? "Code copied" : "Copy code" }
113+ className = "h-8 px-3 text-primary hover:bg-primary/10 hover:text-primary transition-all"
74114 >
75115 { copied ? (
76116 < >
77- < Check className = "h-3 w-3 sm: h-4 sm:w-4 mr-1 text-green-500 " />
78- < span className = "text-xs hidden sm:inline " > Copied!</ span >
117+ < Check className = "w-4 h-4 mr-2 " />
118+ < span className = "text-xs" > Copied!</ span >
79119 </ >
80120 ) : (
81121 < >
82- < Copy className = "h-3 w-3 sm: h-4 sm:w-4 mr-1 " />
83- < span className = "text-xs hidden sm:inline " > Copy</ span >
122+ < Copy className = "w-4 h-4 mr-2 " />
123+ < span className = "text-xs" > Copy</ span >
84124 </ >
85125 ) }
86126 </ Button >
87127 </ div >
88128
89- { /* Code Content */ }
90- < div className = "overflow-x-auto" >
91- < SyntaxHighlighter
92- language = { language }
93- style = { {
94- ...syntaxTheme ,
95- 'code[class*="language-"]' : {
96- ...syntaxTheme [ 'code[class*="language-"]' ] ,
97- background : "transparent" ,
98- backgroundColor : "transparent" ,
99- } ,
100- } }
101- showLineNumbers = { showLineNumbers }
102- customStyle = { {
103- margin : 0 ,
104- padding : "0.875rem 1rem" ,
105- background : "transparent" ,
106- backgroundColor : "transparent" ,
107- fontSize : "0.8125rem" ,
108- lineHeight : "1.6" ,
109- } }
110- codeTagProps = { {
111- style : {
112- fontSize : "0.8125rem" ,
113- fontFamily :
114- "'Fira Code', 'JetBrains Mono', 'Courier New', monospace" ,
115- } ,
116- } }
117- wrapLongLines = { false }
118- className = "scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent"
119- >
120- { code }
121- </ SyntaxHighlighter >
129+ { /* Code content with Shiki highlighting and logical line numbers */ }
130+ < div
131+ className = "overflow-x-auto text-sm leading-relaxed relative"
132+ style = { {
133+ fontFamily : "'Fira Code', 'JetBrains Mono', Consolas, monospace" ,
134+ } }
135+ >
136+ { showLineNumbers && (
137+ < div
138+ className = "absolute left-0 top-0 flex flex-col py-4 px-2 text-right select-none pointer-events-none z-10"
139+ style = { {
140+ width : `${ Math . max ( 2.5 , maxLineNumberWidth * 0.6 + 1 ) } rem` ,
141+ backgroundColor : 'transparent' ,
142+ borderRight : '1px solid hsl(var(--primary) / 0.2)' ,
143+ } }
144+ >
145+ { Array . from ( { length : lineCount } , ( _ , i ) => (
146+ < span
147+ key = { i + 1 }
148+ className = "block text-xs leading-relaxed opacity-70 hover:opacity-100 transition-opacity"
149+ style = { {
150+ color : 'hsl(var(--primary) / 0.8)' ,
151+ lineHeight : '1.5' ,
152+ height : '1.5em' ,
153+ } }
154+ >
155+ { i + 1 }
156+ </ span >
157+ ) ) }
158+ </ div >
159+ ) }
160+
161+ { highlightedCode ? (
162+ < div
163+ dangerouslySetInnerHTML = { { __html : highlightedCode } }
164+ className = { `
165+ [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:py-4 [&_code]:!bg-transparent
166+ ${ showLineNumbers ? `[&_pre]:pl-[${ Math . max ( 2.5 , maxLineNumberWidth * 0.6 + 1 ) + 1 } rem]` : '[&_pre]:px-4' }
167+ ` }
168+ style = { {
169+ marginLeft : showLineNumbers ? `${ Math . max ( 2.5 , maxLineNumberWidth * 0.6 + 1 ) } rem` : '0' ,
170+ paddingLeft : showLineNumbers ? '1rem' : '1rem' ,
171+ } }
172+ />
173+ ) : (
174+ < pre
175+ className = { `py-4 text-muted-foreground ${ showLineNumbers ? 'pl-16' : 'px-4' } ` }
176+ style = { {
177+ marginLeft : showLineNumbers ? `${ Math . max ( 2.5 , maxLineNumberWidth * 0.6 + 1 ) } rem` : '0' ,
178+ } }
179+ >
180+ < code > { code } </ code >
181+ </ pre >
182+ ) }
122183 </ div >
184+
185+ { /* Subtle primary color accent at bottom */ }
186+ < div className = "h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
123187 </ div >
124188 ) ;
125189} ;
0 commit comments