@@ -17,6 +17,99 @@ import {
1717 loadTheme ,
1818} from "./highlighter" ;
1919
20+ interface CachedTokens {
21+ tokens : Array < Array < { content : string ; color : string } > > ;
22+ themeBg : string ;
23+ }
24+
25+ const tokensCache = new Map < string , CachedTokens > ( ) ;
26+
27+ function simpleHash ( str : string ) : number {
28+ let hash = 5381 ;
29+ for ( let i = 0 ; i < str . length ; i ++ ) {
30+ hash = ( hash * 33 ) ^ str . charCodeAt ( i ) ;
31+ }
32+ return hash >>> 0 ;
33+ }
34+
35+ function generateBlockHash (
36+ content : string ,
37+ language : string ,
38+ theme : string ,
39+ ) : string {
40+ const contentHash = simpleHash ( content ) ;
41+ return `${ language } :${ theme } :${ contentHash } ` ;
42+ }
43+
44+ function createBlockDecorations (
45+ block : { node : PMNode ; pos : number } ,
46+ language : string ,
47+ theme : string ,
48+ highlighter : ReturnType < typeof getShiki > ,
49+ ) : Decoration [ ] {
50+ if ( ! highlighter ) return [ ] ;
51+
52+ const content = block . node . textContent ;
53+ const hash = generateBlockHash ( content , language , theme ) ;
54+ const decorations : Decoration [ ] = [ ] ;
55+
56+ let cachedData = tokensCache . get ( hash ) ;
57+
58+ if ( ! cachedData ) {
59+ const themeResolved = highlighter . getTheme ( theme ) ;
60+ const rawTokens = highlighter . codeToTokensBase ( content , {
61+ lang : language as BundledLanguage ,
62+ theme : theme as BundledTheme ,
63+ } ) ;
64+
65+ const tokens = rawTokens . map ( ( line ) =>
66+ line . map ( ( token ) => ( {
67+ content : token . content ,
68+ color : token . color || "#000000" ,
69+ } ) ) ,
70+ ) ;
71+
72+ cachedData = {
73+ tokens,
74+ themeBg : themeResolved . bg || "" ,
75+ } ;
76+
77+ tokensCache . set ( hash , cachedData ) ;
78+
79+ if ( tokensCache . size > 100 ) {
80+ const firstKey = tokensCache . keys ( ) . next ( ) . value ;
81+ if ( firstKey ) {
82+ tokensCache . delete ( firstKey ) ;
83+ }
84+ }
85+ }
86+
87+ decorations . push (
88+ Decoration . node ( block . pos , block . pos + block . node . nodeSize , {
89+ style : `background-color: ${ cachedData . themeBg } ` ,
90+ } ) ,
91+ ) ;
92+
93+ let from = block . pos + 1 ;
94+ for ( const line of cachedData . tokens ) {
95+ for ( const token of line ) {
96+ const to = from + token . content . length ;
97+
98+ decorations . push (
99+ Decoration . inline ( from , to , {
100+ style : `color: ${ token . color } ` ,
101+ } ) ,
102+ ) ;
103+
104+ from = to ;
105+ }
106+
107+ from += 1 ;
108+ }
109+
110+ return decorations ;
111+ }
112+
20113/** Create code decorations for the current document */
21114function getDecorations ( {
22115 doc,
@@ -30,17 +123,17 @@ function getDecorations({
30123 defaultTheme : BundledTheme ;
31124} ) {
32125 const decorations : Decoration [ ] = [ ] ;
33-
34126 const codeBlockCodes = findChildren ( doc , ( node ) => node . type . name === name ) ;
127+ const highlighter = getShiki ( ) ;
128+
129+ if ( ! highlighter ) {
130+ return DecorationSet . create ( doc , decorations ) ;
131+ }
35132
36133 codeBlockCodes . forEach ( ( block ) => {
37134 let language = block . node . attrs . language || defaultLanguage ;
38135 const theme = block . node . attrs . theme || defaultTheme ;
39136
40- const highlighter = getShiki ( ) ;
41-
42- if ( ! highlighter ) return ;
43-
44137 if ( ! highlighter . getLoadedLanguages ( ) . includes ( language ) ) {
45138 language = "plaintext" ;
46139 }
@@ -49,34 +142,14 @@ function getDecorations({
49142 ? theme
50143 : highlighter . getLoadedThemes ( ) [ 0 ] ;
51144
52- const themeResolved = highlighter . getTheme ( themeToApply ) ;
53- decorations . push (
54- Decoration . node ( block . pos , block . pos + block . node . nodeSize , {
55- style : `background-color: ${ themeResolved . bg } ` ,
56- } ) ,
145+ const blockDecorations = createBlockDecorations (
146+ block ,
147+ language ,
148+ themeToApply ,
149+ highlighter ,
57150 ) ;
58151
59- const tokens = highlighter . codeToTokensBase ( block . node . textContent , {
60- lang : language ,
61- theme : themeToApply ,
62- } ) ;
63-
64- let from = block . pos + 1 ;
65- for ( const line of tokens ) {
66- for ( const token of line ) {
67- const to = from + token . content . length ;
68-
69- const decoration = Decoration . inline ( from , to , {
70- style : `color: ${ token . color } ` ,
71- } ) ;
72-
73- decorations . push ( decoration ) ;
74-
75- from = to ;
76- }
77-
78- from += 1 ;
79- }
152+ decorations . push ( ...blockDecorations ) ;
80153 } ) ;
81154
82155 return DecorationSet . create ( doc , decorations ) ;
@@ -184,14 +257,12 @@ export function ShikiPlugin({
184257 // (for example, a transaction that affects the entire document).
185258 // Such transactions can happen during collab syncing via y-prosemirror, for example.
186259 transaction . steps . some ( ( step ) => {
187- // @ts -expect-error
188260 return (
189261 // @ts -expect-error
190262 step . from !== undefined &&
191263 // @ts -expect-error
192264 step . to !== undefined &&
193265 oldNodes . some ( ( node ) => {
194- // @ts -expect-error
195266 return (
196267 // @ts -expect-error
197268 node . pos >= step . from &&
@@ -201,7 +272,6 @@ export function ShikiPlugin({
201272 } )
202273 ) ;
203274 } ) ) ;
204-
205275 // only create code decoration when it's necessary to do so
206276 if (
207277 transaction . getMeta ( "shikiPluginForceDecoration" ) ||
0 commit comments