1
1
import { programmingLanguages } from "@/utils/langauge-extension"
2
- import { Tooltip , Modal , ConfigProvider , Button } from "antd"
2
+ import { Tooltip } from "antd"
3
3
import {
4
4
CopyCheckIcon ,
5
5
CopyIcon ,
6
6
DownloadIcon ,
7
- InfoIcon ,
8
- ExternalLinkIcon
7
+ EyeIcon ,
8
+ CodeIcon
9
9
} from "lucide-react"
10
- import { FC , useState , useRef } from "react"
10
+ import { FC , useState , useRef , useEffect , useCallback } from "react"
11
11
import { useTranslation } from "react-i18next"
12
12
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
13
13
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
@@ -20,9 +20,37 @@ interface Props {
20
20
21
21
export const CodeBlock : FC < Props > = ( { language, value } ) => {
22
22
const [ isBtnPressed , setIsBtnPressed ] = useState ( false )
23
- const [ previewVisible , setPreviewVisible ] = useState ( false )
23
+ const [ previewValue , setPreviewValue ] = useState ( value )
24
+ const debounceTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
25
+
26
+ const computeKey = ( ) => {
27
+ const base = `${ language } ::${ value ?. slice ( 0 , 200 ) } `
28
+ let hash = 0
29
+ for ( let i = 0 ; i < base . length ; i ++ ) {
30
+ hash = ( hash * 31 + base . charCodeAt ( i ) ) >>> 0
31
+ }
32
+ return hash . toString ( 36 )
33
+ }
34
+ const keyRef = useRef < string > ( computeKey ( ) )
35
+ const mapRef = useRef < Map < string , boolean > | null > ( null )
36
+ if ( ! mapRef . current ) {
37
+ if ( typeof window !== "undefined" ) {
38
+ // @ts -ignore
39
+ if ( ! window . __codeBlockPreviewState ) {
40
+ // @ts -ignore
41
+ window . __codeBlockPreviewState = new Map ( )
42
+ }
43
+ // @ts -ignore
44
+ mapRef . current = window . __codeBlockPreviewState as Map < string , boolean >
45
+ } else {
46
+ mapRef . current = new Map ( )
47
+ }
48
+ }
49
+ const globalStateMap = mapRef . current !
50
+ const [ showPreview , setShowPreview ] = useState < boolean > (
51
+ ( ) => globalStateMap . get ( keyRef . current ) || false
52
+ )
24
53
const { t } = useTranslation ( "common" )
25
- const iframeRef = useRef < HTMLIFrameElement > ( null )
26
54
27
55
const handleCopy = ( ) => {
28
56
navigator . clipboard . writeText ( value )
@@ -32,9 +60,21 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
32
60
} , 4000 )
33
61
}
34
62
35
- const handlePreviewClose = ( ) => {
36
- setPreviewVisible ( false )
37
- }
63
+ const isPreviewable = [ "html" , "svg" , "xml" ] . includes (
64
+ ( language || "" ) . toLowerCase ( )
65
+ )
66
+
67
+ const buildPreviewDoc = useCallback ( ( ) => {
68
+ const code = previewValue || ""
69
+ if ( ( language || "" ) . toLowerCase ( ) === "svg" ) {
70
+ const hasSvgTag = / < s v g [ \s > ] / i. test ( code )
71
+ const svgMarkup = hasSvgTag
72
+ ? code
73
+ : `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>${ code } </svg>`
74
+ return `<!doctype html><html><head><meta charset='utf-8'/><style>html,body{margin:0;padding:0;display:flex;align-items:center;justify-content:center;background:#fff;height:100%;}</style></head><body>${ svgMarkup } </body></html>`
75
+ }
76
+ return `<!doctype html><html><head><meta charset='utf-8'/></head><body>${ code } </body></html>`
77
+ } , [ previewValue , language ] )
38
78
39
79
const handleDownload = ( ) => {
40
80
const blob = new Blob ( [ value ] , { type : "text/plain" } )
@@ -48,30 +88,75 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
48
88
window . URL . revokeObjectURL ( url )
49
89
}
50
90
51
- const handleOpenInNewTab = ( ) => {
52
- const blob = new Blob ( [ value ] , { type : "text/html" } )
53
- const url = URL . createObjectURL ( blob )
54
- window . open ( url , "_blank" )
55
- }
91
+ useEffect ( ( ) => {
92
+ globalStateMap . set ( keyRef . current , showPreview )
93
+ } , [ showPreview ] )
94
+
95
+ useEffect ( ( ) => {
96
+ if ( debounceTimeoutRef . current ) {
97
+ clearTimeout ( debounceTimeoutRef . current )
98
+ }
99
+
100
+ debounceTimeoutRef . current = setTimeout ( ( ) => {
101
+ setPreviewValue ( value )
102
+ } , 300 )
103
+
104
+ return ( ) => {
105
+ if ( debounceTimeoutRef . current ) {
106
+ clearTimeout ( debounceTimeoutRef . current )
107
+ }
108
+ }
109
+ } , [ value ] )
110
+
111
+ useEffect ( ( ) => {
112
+ const newKey = computeKey ( )
113
+ if ( newKey !== keyRef . current ) {
114
+ keyRef . current = newKey
115
+ if ( globalStateMap . has ( newKey ) ) {
116
+ const prev = globalStateMap . get ( newKey ) !
117
+ if ( prev !== showPreview ) setShowPreview ( prev )
118
+ }
119
+ }
120
+ } , [ language , value ] )
121
+
122
+ useEffect ( ( ) => {
123
+ if ( ! isPreviewable && showPreview ) setShowPreview ( false )
124
+ } , [ isPreviewable ] )
56
125
57
126
return (
58
127
< >
59
128
< div className = "not-prose" >
60
129
< div className = " [&_div+div]:!mt-0 my-4 bg-zinc-950 rounded-xl" >
61
- < div className = "flex flex-row px-4 py-2 rounded-t-xl bg-gray-800 " >
130
+ < div className = "flex flex-row px-4 py-2 rounded-t-xl gap-3 bg-gray-800 " >
131
+ { isPreviewable && (
132
+ < div className = "flex rounded-md overflow-hidden border border-gray-700" >
133
+ < button
134
+ onClick = { ( ) => setShowPreview ( false ) }
135
+ className = { `px-2 flex items-center gap-1 text-xs transition-colors ${
136
+ ! showPreview
137
+ ? "bg-gray-700 text-white"
138
+ : "bg-transparent text-gray-300 hover:bg-gray-700/60"
139
+ } `}
140
+ aria-label = { t ( "showCode" ) || "Code" } >
141
+ < CodeIcon className = "size-3" />
142
+ </ button >
143
+ < button
144
+ onClick = { ( ) => setShowPreview ( true ) }
145
+ className = { `px-2 flex items-center gap-1 text-xs transition-colors ${
146
+ showPreview
147
+ ? "bg-gray-700 text-white"
148
+ : "bg-transparent text-gray-300 hover:bg-gray-700/60"
149
+ } `}
150
+ aria-label = { t ( "preview" ) || "Preview" } >
151
+ < EyeIcon className = "size-3" />
152
+ </ button >
153
+ </ div >
154
+ ) }
155
+
62
156
< span className = "font-mono text-xs" > { language || "text" } </ span >
63
157
</ div >
64
158
< div className = "sticky top-9 md:top-[5.75rem]" >
65
- < div className = "absolute bottom-0 right-2 flex h-9 items-center" >
66
- { /* {language === "html" && (
67
- <Tooltip title={t("preview")}>
68
- <button
69
- onClick={() => setPreviewVisible(true)}
70
- className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none">
71
- <InfoIcon className="size-4" />
72
- </button>
73
- </Tooltip>
74
- )} */ }
159
+ < div className = "absolute bottom-0 right-2 flex h-9 items-center gap-1" >
75
160
< Tooltip title = { t ( "downloadCode" ) } >
76
161
< button
77
162
onClick = { handleDownload }
@@ -93,97 +178,41 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
93
178
</ div >
94
179
</ div >
95
180
96
- < SyntaxHighlighter
97
- language = { language }
98
- style = { coldarkDark }
99
- PreTag = "div"
100
- customStyle = { {
101
- margin : 0 ,
102
- width : "100%" ,
103
- background : "transparent" ,
104
- padding : "1.5rem 1rem"
105
- } }
106
- lineNumberStyle = { {
107
- userSelect : "none"
108
- } }
109
- codeTagProps = { {
110
- style : {
111
- fontSize : "0.9rem" ,
112
- fontFamily : "var(--font-mono)"
113
- }
114
- } } >
115
- { value }
116
- </ SyntaxHighlighter >
117
- </ div >
118
- </ div >
119
- { previewVisible && (
120
- < ConfigProvider
121
- theme = { {
122
- components : {
123
- Modal : {
124
- contentBg : "#1e1e1e" ,
125
- headerBg : "#1e1e1e" ,
126
- titleColor : "#ffffff"
127
- }
128
- }
129
- } } >
130
- < Modal
131
- title = {
132
- < div className = "flex items-center text-white" >
133
- < InfoIcon className = "mr-2 size-5" />
134
- < span > HTML Preview</ span >
135
- </ div >
136
- }
137
- open = { previewVisible }
138
- onCancel = { handlePreviewClose }
139
- footer = {
140
- < div className = "flex justify-end gap-2" >
141
- < Button
142
- icon = { < ExternalLinkIcon className = "size-4" /> }
143
- onClick = { handleOpenInNewTab } >
144
- Open in new tab
145
- </ Button >
146
-
147
- < Button
148
- icon = { < DownloadIcon className = "size-4" /> }
149
- onClick = { handleDownload } >
150
- { t ( "downloadCode" ) }
151
- </ Button >
152
- </ div >
153
- }
154
- width = { "80%" }
155
- zIndex = { 999999 }
156
- centered
157
- styles = { {
158
- body : {
159
- padding : 0 ,
160
- backgroundColor : "#f5f5f5" ,
161
- borderRadius : "0 0 8px 8px"
162
- } ,
163
- header : {
164
- borderBottom : "1px solid #333" ,
165
- padding : "12px 24px"
166
- } ,
167
- mask : {
168
- backdropFilter : "blur(4px)" ,
169
- backgroundColor : "rgba(0, 0, 0, 0.6)"
170
- } ,
171
- content : {
172
- boxShadow : "0 25px 50px -12px rgba(0, 0, 0, 0.25)"
173
- }
174
- } } >
175
- < div className = { `relative w-full h-[70vh] bg-white` } >
181
+ { ! showPreview && (
182
+ < SyntaxHighlighter
183
+ language = { language }
184
+ style = { coldarkDark }
185
+ PreTag = "div"
186
+ customStyle = { {
187
+ margin : 0 ,
188
+ width : "100%" ,
189
+ background : "transparent" ,
190
+ padding : "1.5rem 1rem"
191
+ } }
192
+ lineNumberStyle = { {
193
+ userSelect : "none"
194
+ } }
195
+ codeTagProps = { {
196
+ style : {
197
+ fontSize : "0.9rem" ,
198
+ fontFamily : "var(--font-mono)"
199
+ }
200
+ } } >
201
+ { value }
202
+ </ SyntaxHighlighter >
203
+ ) }
204
+ { showPreview && isPreviewable && (
205
+ < div className = "w-full h-[420px] bg-white rounded-b-xl overflow-hidden border-t border-gray-800" >
176
206
< iframe
177
- ref = { iframeRef }
178
- srcDoc = { value }
179
- title = "HTML Preview"
207
+ title = "Preview"
208
+ srcDoc = { buildPreviewDoc ( ) }
180
209
className = "w-full h-full border-0"
181
210
sandbox = "allow-scripts allow-same-origin"
182
211
/>
183
212
</ div >
184
- </ Modal >
185
- </ ConfigProvider >
186
- ) }
213
+ ) }
214
+ </ div >
215
+ </ div >
187
216
</ >
188
217
)
189
218
}
0 commit comments