11import { programmingLanguages } from "@/utils/langauge-extension"
2- import { Tooltip , Modal , ConfigProvider , Button } from "antd"
2+ import { Tooltip } from "antd"
33import {
44 CopyCheckIcon ,
55 CopyIcon ,
66 DownloadIcon ,
7- InfoIcon ,
8- ExternalLinkIcon
7+ EyeIcon ,
8+ CodeIcon
99} from "lucide-react"
10- import { FC , useState , useRef } from "react"
10+ import { FC , useState , useRef , useEffect } from "react"
1111import { useTranslation } from "react-i18next"
1212import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
1313import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
@@ -20,9 +20,34 @@ interface Props {
2020
2121export const CodeBlock : FC < Props > = ( { language, value } ) => {
2222 const [ isBtnPressed , setIsBtnPressed ] = useState ( false )
23- const [ previewVisible , setPreviewVisible ] = useState ( false )
23+ const computeKey = ( ) => {
24+ const base = `${ language } ::${ value ?. slice ( 0 , 200 ) } `
25+ let hash = 0
26+ for ( let i = 0 ; i < base . length ; i ++ ) {
27+ hash = ( hash * 31 + base . charCodeAt ( i ) ) >>> 0
28+ }
29+ return hash . toString ( 36 )
30+ }
31+ const keyRef = useRef < string > ( computeKey ( ) )
32+ const mapRef = useRef < Map < string , boolean > | null > ( null )
33+ if ( ! mapRef . current ) {
34+ if ( typeof window !== "undefined" ) {
35+ // @ts -ignore
36+ if ( ! window . __codeBlockPreviewState ) {
37+ // @ts -ignore
38+ window . __codeBlockPreviewState = new Map ( )
39+ }
40+ // @ts -ignore
41+ mapRef . current = window . __codeBlockPreviewState as Map < string , boolean >
42+ } else {
43+ mapRef . current = new Map ( )
44+ }
45+ }
46+ const globalStateMap = mapRef . current !
47+ const [ showPreview , setShowPreview ] = useState < boolean > (
48+ ( ) => globalStateMap . get ( keyRef . current ) || false
49+ )
2450 const { t } = useTranslation ( "common" )
25- const iframeRef = useRef < HTMLIFrameElement > ( null )
2651
2752 const handleCopy = ( ) => {
2853 navigator . clipboard . writeText ( value )
@@ -32,8 +57,20 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
3257 } , 4000 )
3358 }
3459
35- const handlePreviewClose = ( ) => {
36- setPreviewVisible ( false )
60+ const isPreviewable = [ "html" , "svg" , "xml" ] . includes (
61+ ( language || "" ) . toLowerCase ( )
62+ )
63+
64+ const buildPreviewDoc = ( ) => {
65+ const code = value || ""
66+ if ( ( language || "" ) . toLowerCase ( ) === "svg" ) {
67+ const hasSvgTag = / < s v g [ \s > ] / i. test ( code )
68+ const svgMarkup = hasSvgTag
69+ ? code
70+ : `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>${ code } </svg>`
71+ 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>`
72+ }
73+ return `<!doctype html><html><head><meta charset='utf-8'/></head><body>${ code } </body></html>`
3774 }
3875
3976 const handleDownload = ( ) => {
@@ -48,30 +85,59 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
4885 window . URL . revokeObjectURL ( url )
4986 }
5087
51- const handleOpenInNewTab = ( ) => {
52- const blob = new Blob ( [ value ] , { type : "text/html" } )
53- const url = URL . createObjectURL ( blob )
54- window . open ( url , "_blank" )
55- }
88+ useEffect ( ( ) => {
89+ globalStateMap . set ( keyRef . current , showPreview )
90+ } , [ showPreview ] )
91+
92+ useEffect ( ( ) => {
93+ const newKey = computeKey ( )
94+ if ( newKey !== keyRef . current ) {
95+ keyRef . current = newKey
96+ if ( globalStateMap . has ( newKey ) ) {
97+ const prev = globalStateMap . get ( newKey ) !
98+ if ( prev !== showPreview ) setShowPreview ( prev )
99+ }
100+ }
101+ } , [ language , value ] )
102+
103+ useEffect ( ( ) => {
104+ if ( ! isPreviewable && showPreview ) setShowPreview ( false )
105+ } , [ isPreviewable ] )
56106
57107 return (
58108 < >
59109 < div className = "not-prose" >
60110 < 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 " >
111+ < div className = "flex flex-row px-4 py-2 rounded-t-xl gap-3 bg-gray-800 " >
112+ { isPreviewable && (
113+ < div className = "flex rounded-md overflow-hidden border border-gray-700" >
114+ < button
115+ onClick = { ( ) => setShowPreview ( false ) }
116+ className = { `px-2 flex items-center gap-1 text-xs transition-colors ${
117+ ! showPreview
118+ ? "bg-gray-700 text-white"
119+ : "bg-transparent text-gray-300 hover:bg-gray-700/60"
120+ } `}
121+ aria-label = { t ( "showCode" ) || "Code" } >
122+ < CodeIcon className = "size-3" />
123+ </ button >
124+ < button
125+ onClick = { ( ) => setShowPreview ( true ) }
126+ className = { `px-2 flex items-center gap-1 text-xs transition-colors ${
127+ showPreview
128+ ? "bg-gray-700 text-white"
129+ : "bg-transparent text-gray-300 hover:bg-gray-700/60"
130+ } `}
131+ aria-label = { t ( "preview" ) || "Preview" } >
132+ < EyeIcon className = "size-3" />
133+ </ button >
134+ </ div >
135+ ) }
136+
62137 < span className = "font-mono text-xs" > { language || "text" } </ span >
63138 </ div >
64139 < 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- )} */ }
140+ < div className = "absolute bottom-0 right-2 flex h-9 items-center gap-1" >
75141 < Tooltip title = { t ( "downloadCode" ) } >
76142 < button
77143 onClick = { handleDownload }
@@ -93,97 +159,41 @@ export const CodeBlock: FC<Props> = ({ language, value }) => {
93159 </ div >
94160 </ div >
95161
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` } >
162+ { ! showPreview && (
163+ < SyntaxHighlighter
164+ language = { language }
165+ style = { coldarkDark }
166+ PreTag = "div"
167+ customStyle = { {
168+ margin : 0 ,
169+ width : "100%" ,
170+ background : "transparent" ,
171+ padding : "1.5rem 1rem"
172+ } }
173+ lineNumberStyle = { {
174+ userSelect : "none"
175+ } }
176+ codeTagProps = { {
177+ style : {
178+ fontSize : "0.9rem" ,
179+ fontFamily : "var(--font-mono)"
180+ }
181+ } } >
182+ { value }
183+ </ SyntaxHighlighter >
184+ ) }
185+ { showPreview && isPreviewable && (
186+ < div className = "w-full h-[420px] bg-white rounded-b-xl overflow-hidden border-t border-gray-800" >
176187 < iframe
177- ref = { iframeRef }
178- srcDoc = { value }
179- title = "HTML Preview"
188+ title = "Preview"
189+ srcDoc = { buildPreviewDoc ( ) }
180190 className = "w-full h-full border-0"
181191 sandbox = "allow-scripts allow-same-origin"
182192 />
183193 </ div >
184- </ Modal >
185- </ ConfigProvider >
186- ) }
194+ ) }
195+ </ div >
196+ </ div >
187197 </ >
188198 )
189199}
0 commit comments