1- import {
2- useCallback ,
3- useEffect ,
4- useLayoutEffect ,
5- useMemo ,
6- useState ,
7- } from 'react'
8- import clsx from 'clsx'
1+ import { useEffect , useMemo , useState } from 'react'
92import { getHighlighterCore } from 'shiki'
103import getWasm from 'shiki/wasm'
114import type { FC } from 'react'
12- import type { HighlighterCore } from 'shiki'
135
14- import { getViewport } from '~/atoms/hooks'
15- import { AutoResizeHeight } from '~/components/modules/shared/AutoResizeHeight'
16- import { useMaskScrollArea } from '~/hooks/shared/use-mask-scrollarea'
17- import { stopPropagation } from '~/lib/dom'
18- import { clsxm } from '~/lib/helper'
19- import { toast } from '~/lib/toast'
20-
21- import { MotionButtonBase } from '../../button'
22- import styles from './Shiki.module.css'
23- import { codeHighlighter , parseFilenameFromAttrs } from './utils'
6+ import { ShikiHighLighterWrapper } from './ShikiWrapper'
7+ import { codeHighlighter } from './utils'
248
259interface Props {
2610 lang : string | undefined
@@ -29,88 +13,51 @@ interface Props {
2913 attrs ?: string
3014}
3115
32- let highlighterCore : HighlighterCore | null = null
16+ const highlighterCore = await ( async ( ) => {
17+ const loaded = await getHighlighterCore ( {
18+ themes : [
19+ import ( 'shiki/themes/github-light.mjs' ) ,
20+ import ( 'shiki/themes/github-dark.mjs' ) ,
21+ ] ,
22+ langs : [
23+ ( ) => import ( 'shiki/langs/javascript.mjs' ) ,
24+ ( ) => import ( 'shiki/langs/typescript.mjs' ) ,
25+ ( ) => import ( 'shiki/langs/css.mjs' ) ,
26+ ( ) => import ( 'shiki/langs/tsx.mjs' ) ,
27+ ( ) => import ( 'shiki/langs/jsx.mjs' ) ,
28+ ( ) => import ( 'shiki/langs/json.mjs' ) ,
29+ ( ) => import ( 'shiki/langs/sql.mjs' ) ,
30+ ( ) => import ( 'shiki/langs/rust.mjs' ) ,
31+ ( ) => import ( 'shiki/langs/go.mjs' ) ,
32+ ( ) => import ( 'shiki/langs/cpp.mjs' ) ,
33+ ( ) => import ( 'shiki/langs/c.mjs' ) ,
34+ ( ) => import ( 'shiki/langs/markdown.mjs' ) ,
35+ ( ) => import ( 'shiki/langs/vue.mjs' ) ,
36+ ( ) => import ( 'shiki/langs/html.mjs' ) ,
37+ ( ) => import ( 'shiki/langs/asm.mjs' ) ,
38+ ( ) => import ( 'shiki/langs/shell.mjs' ) ,
39+ ( ) => import ( 'shiki/langs/ps.mjs' ) ,
40+ ] ,
41+ loadWasm : getWasm ,
42+ } )
43+
44+ return loaded
45+ } ) ( )
3346
3447export const ShikiHighLighter : FC < Props > = ( props ) => {
3548 const { lang : language , content : value , attrs } = props
3649
37- const handleCopy = useCallback ( ( ) => {
38- navigator . clipboard . writeText ( value )
39- toast . success ( '已复制到剪贴板' )
40- } , [ value ] )
41-
42- const [ highlighter , setHighlighter ] = useState ( highlighterCore )
43-
44- useLayoutEffect ( ( ) => {
45- if ( highlighterCore ) {
46- return
47- }
48- ; ( async ( ) => {
49- const loaded = await getHighlighterCore ( {
50- themes : [
51- import ( 'shiki/themes/github-light.mjs' ) ,
52- import ( 'shiki/themes/github-dark.mjs' ) ,
53- ] ,
54- langs : [
55- ( ) => import ( 'shiki/langs/javascript.mjs' ) ,
56- ( ) => import ( 'shiki/langs/typescript.mjs' ) ,
57- ( ) => import ( 'shiki/langs/css.mjs' ) ,
58- ( ) => import ( 'shiki/langs/tsx.mjs' ) ,
59- ( ) => import ( 'shiki/langs/jsx.mjs' ) ,
60- ( ) => import ( 'shiki/langs/json.mjs' ) ,
61- ( ) => import ( 'shiki/langs/sql.mjs' ) ,
62- ( ) => import ( 'shiki/langs/rust.mjs' ) ,
63- ( ) => import ( 'shiki/langs/go.mjs' ) ,
64- ( ) => import ( 'shiki/langs/cpp.mjs' ) ,
65- ( ) => import ( 'shiki/langs/c.mjs' ) ,
66- ( ) => import ( 'shiki/langs/markdown.mjs' ) ,
67- ( ) => import ( 'shiki/langs/vue.mjs' ) ,
68- ( ) => import ( 'shiki/langs/html.mjs' ) ,
69- ( ) => import ( 'shiki/langs/asm.mjs' ) ,
70- ( ) => import ( 'shiki/langs/shell.mjs' ) ,
71- ( ) => import ( 'shiki/langs/ps.mjs' ) ,
72- ] ,
73- loadWasm : getWasm ,
74- } )
75- setHighlighter ( loaded )
76- highlighterCore = loaded
77- } ) ( )
78- } , [ ] )
79-
80- const [ codeBlockRef , setCodeBlockRef ] = useState < HTMLDivElement | null > ( null )
81-
82- const [ isCollapsed , setIsCollapsed ] = useState ( true )
83- const [ isOverflow , setIsOverflow ] = useState ( false )
84- useEffect ( ( ) => {
85- const $el = codeBlockRef
86-
87- if ( ! $el ) return
88-
89- const windowHeight = getViewport ( ) . h
90- const halfWindowHeight = windowHeight / 2
91- const $elScrollHeight = $el . scrollHeight
92- if ( $elScrollHeight >= halfWindowHeight ) {
93- setIsOverflow ( true )
94-
95- $el . querySelector ( '.highlighted' ) ?. scrollIntoView ( {
96- block : 'center' ,
97- } )
98- } else {
99- setIsOverflow ( false )
100- }
101- } , [ value , codeBlockRef ] )
102-
10350 const highlightedHtml = useMemo ( ( ) => {
104- if ( ! highlighter ) return ''
105- return codeHighlighter ( highlighter , {
51+ return codeHighlighter ( highlighterCore , {
10652 attrs : attrs || '' ,
10753 // code: `${value.split('\n')[0].repeat(10)} // [!code highlight]\n${value}`,
10854 code : value ,
10955 lang : language ? language . toLowerCase ( ) : '' ,
11056 } )
111- } , [ attrs , language , value , highlighter ] )
57+ } , [ attrs , language , value ] )
11258
11359 const [ renderedHtml , setRenderedHtml ] = useState ( highlightedHtml )
60+ const [ codeBlockRef , setCodeBlockRef ] = useState < HTMLDivElement | null > ( null )
11461 useEffect ( ( ) => {
11562 setRenderedHtml ( highlightedHtml )
11663 requestAnimationFrame ( ( ) => {
@@ -132,100 +79,11 @@ export const ShikiHighLighter: FC<Props> = (props) => {
13279 } )
13380 } , [ codeBlockRef , highlightedHtml ] )
13481
135- const filename = useMemo ( ( ) => {
136- return parseFilenameFromAttrs ( attrs || '' )
137- } , [ attrs ] )
138- const [ , maskClassName ] = useMaskScrollArea ( {
139- element : codeBlockRef ! ,
140- size : 'lg' ,
141- } )
142-
143- const hasHeader = ! ! filename
144-
14582 return (
146- < div
147- className = { clsx ( styles [ 'code-card' ] , 'group' ) }
148- onCopy = { stopPropagation }
149- >
150- { ! ! filename && (
151- < div className = "z-10 flex w-full items-center justify-between rounded-t-xl bg-accent/20 px-5 py-2 text-sm" >
152- < span className = "shrink-0 grow truncate" > { filename } </ span >
153- < span className = "pointer-events-none shrink-0 grow-0" aria-hidden >
154- { language ?. toUpperCase ( ) }
155- </ span >
156- </ div >
157- ) }
158-
159- { ! filename && ! ! language && (
160- < div
161- aria-hidden
162- className = "pointer-events-none absolute bottom-3 right-3 z-10 text-sm opacity-60"
163- >
164- { language . toUpperCase ( ) }
165- </ div >
166- ) }
167- < div className = "bg-accent/5 py-4" >
168- < MotionButtonBase
169- onClick = { handleCopy }
170- className = { clsx (
171- 'absolute right-2 top-2 z-[1] flex text-xs center' ,
172- 'rounded-md border border-accent/5 bg-accent/80 p-1.5 text-white backdrop-blur duration-200' ,
173- 'opacity-0 group-hover:opacity-100' ,
174- filename && '!top-12' ,
175- ) }
176- >
177- < i className = "icon-[mingcute--copy-2-fill] size-4" />
178- </ MotionButtonBase >
179- < AutoResizeHeight spring className = "relative" >
180- < div
181- ref = { setCodeBlockRef }
182- className = { clsxm (
183- 'relative max-h-[50vh] w-full overflow-auto' ,
184- ! isCollapsed ? '!max-h-full' : isOverflow ? maskClassName : '' ,
185- styles [ 'scroll-container' ] ,
186- ) }
187- style = {
188- {
189- '--sr-margin' : ! hasHeader
190- ? `${ ( language ?. length || 0 ) * 14 + 4 } px`
191- : '1rem' ,
192- } as any
193- }
194- dangerouslySetInnerHTML = {
195- renderedHtml
196- ? {
197- __html : renderedHtml ,
198- }
199- : undefined
200- }
201- >
202- { renderedHtml ? undefined : (
203- < pre className = "bg-transparent px-5" >
204- < code className = "!px-5" > { value } </ code >
205- </ pre >
206- ) }
207- </ div >
208-
209- { isOverflow && isCollapsed && (
210- < div
211- className = { `absolute inset-x-0 bottom-0 flex justify-center py-2 duration-200 ${
212- [ 'mask-both-lg' , 'mask-b-lg' ] . includes ( maskClassName )
213- ? ''
214- : 'pointer-events-none opacity-0'
215- } `}
216- >
217- < button
218- onClick = { ( ) => setIsCollapsed ( false ) }
219- aria-hidden
220- className = "flex items-center justify-center text-xs"
221- >
222- < i className = "icon-[mingcute--arrow-to-down-line]" />
223- < span className = "ml-2" > 展开</ span >
224- </ button >
225- </ div >
226- ) }
227- </ AutoResizeHeight >
228- </ div >
229- </ div >
83+ < ShikiHighLighterWrapper
84+ { ...props }
85+ renderedHTML = { renderedHtml }
86+ ref = { setCodeBlockRef }
87+ />
23088 )
23189}
0 commit comments