1
- import { bundledLanguages , getHighlighter } from 'shikiji' ;
1
+ import { bundledLanguages , createCssVariablesTheme , getHighlighter } from 'shikiji' ;
2
2
import { visit } from 'unist-util-visit' ;
3
3
import type { ShikiConfig } from './types.js' ;
4
4
5
5
export interface ShikiHighlighter {
6
6
highlight ( code : string , lang ?: string , options ?: { inline ?: boolean } ) : string ;
7
7
}
8
8
9
+ // TODO: Remove this special replacement in Astro 5
9
10
const ASTRO_COLOR_REPLACEMENTS : Record < string , string > = {
10
- '#000001' : 'var(--astro-code-color-text)' ,
11
- '#000002' : 'var(--astro-code-color-background)' ,
12
- '#000004' : 'var(--astro-code-token-constant)' ,
13
- '#000005' : 'var(--astro-code-token-string)' ,
14
- '#000006' : 'var(--astro-code-token-comment)' ,
15
- '#000007' : 'var(--astro-code-token-keyword)' ,
16
- '#000008' : 'var(--astro-code-token-parameter)' ,
17
- '#000009' : 'var(--astro-code-token-function)' ,
18
- '#000010' : 'var(--astro-code-token-string-expression)' ,
19
- '#000011' : 'var(--astro-code-token-punctuation)' ,
20
- '#000012' : 'var(--astro-code-token-link)' ,
11
+ '--astro-code-foreground' : '--astro-code-color-text' ,
12
+ '--astro-code-background' : '--astro-code-color-background' ,
21
13
} ;
22
14
const COLOR_REPLACEMENT_REGEX = new RegExp (
23
15
`(${ Object . keys ( ASTRO_COLOR_REPLACEMENTS ) . join ( '|' ) } )` ,
24
16
'g'
25
17
) ;
26
18
19
+ let _cssVariablesTheme : ReturnType < typeof createCssVariablesTheme > ;
20
+ const cssVariablesTheme = ( ) =>
21
+ _cssVariablesTheme ??
22
+ ( _cssVariablesTheme = createCssVariablesTheme ( { variablePrefix : '--astro-code-' } ) ) ;
23
+
27
24
export async function createShikiHighlighter ( {
28
25
langs = [ ] ,
29
26
theme = 'github-dark' ,
30
27
experimentalThemes = { } ,
31
28
wrap = false ,
29
+ transformers = [ ] ,
32
30
} : ShikiConfig = { } ) : Promise < ShikiHighlighter > {
33
31
const themes = experimentalThemes ;
34
32
33
+ theme = theme === 'css-variables' ? cssVariablesTheme ( ) : theme ;
34
+
35
35
const highlighter = await getHighlighter ( {
36
36
langs : langs . length ? langs : Object . keys ( bundledLanguages ) ,
37
37
themes : Object . values ( themes ) . length ? Object . values ( themes ) : [ theme ] ,
@@ -53,74 +53,77 @@ export async function createShikiHighlighter({
53
53
return highlighter . codeToHtml ( code , {
54
54
...themeOptions ,
55
55
lang,
56
- transforms : {
57
- pre ( node ) {
58
- // Swap to `code` tag if inline
59
- if ( inline ) {
60
- node . tagName = 'code' ;
61
- }
62
-
63
- // Cast to string as shikiji will always pass them as strings instead of any other types
64
- const classValue = ( node . properties . class as string ) ?? '' ;
65
- const styleValue = ( node . properties . style as string ) ?? '' ;
66
-
67
- // Replace "shiki" class naming with "astro-code"
68
- node . properties . class = classValue . replace ( / s h i k i / g, 'astro-code' ) ;
69
-
70
- // Handle code wrapping
71
- // if wrap=null, do nothing.
72
- if ( wrap === false ) {
73
- node . properties . style = styleValue + '; overflow-x: auto;' ;
74
- } else if ( wrap === true ) {
75
- node . properties . style =
76
- styleValue + '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;' ;
77
- }
78
- } ,
79
- line ( node ) {
80
- // Add "user-select: none;" for "+"/"-" diff symbols.
81
- // Transform `<span class="line"><span style="...">+ something</span></span>
82
- // into `<span class="line"><span style="..."><span style="user-select: none;">+</span> something</span></span>`
83
- if ( lang === 'diff' ) {
84
- const innerSpanNode = node . children [ 0 ] ;
85
- const innerSpanTextNode =
86
- innerSpanNode ?. type === 'element' && innerSpanNode . children ?. [ 0 ] ;
87
-
88
- if ( innerSpanTextNode && innerSpanTextNode . type === 'text' ) {
89
- const start = innerSpanTextNode . value [ 0 ] ;
90
- if ( start === '+' || start === '-' ) {
91
- innerSpanTextNode . value = innerSpanTextNode . value . slice ( 1 ) ;
92
- innerSpanNode . children . unshift ( {
93
- type : 'element' ,
94
- tagName : 'span' ,
95
- properties : { style : 'user-select: none;' } ,
96
- children : [ { type : 'text' , value : start } ] ,
97
- } ) ;
98
- }
56
+ transformers : [
57
+ {
58
+ pre ( node ) {
59
+ // Swap to `code` tag if inline
60
+ if ( inline ) {
61
+ node . tagName = 'code' ;
99
62
}
100
- }
101
- } ,
102
- code ( node ) {
103
- if ( inline ) {
104
- return node . children [ 0 ] as typeof node ;
105
- }
106
- } ,
107
- root ( node ) {
108
- if ( Object . values ( experimentalThemes ) . length ) {
109
- return ;
110
- }
111
-
112
- // theme.id for shiki -> shikiji compat
113
- const themeName = typeof theme === 'string' ? theme : theme . name ;
114
- if ( themeName === 'css-variables' ) {
115
- // Replace special color tokens to CSS variables
116
- visit ( node as any , 'element' , ( child ) => {
117
- if ( child . properties ?. style ) {
118
- child . properties . style = replaceCssVariables ( child . properties . style ) ;
63
+
64
+ // Cast to string as shikiji will always pass them as strings instead of any other types
65
+ const classValue = ( node . properties . class as string ) ?? '' ;
66
+ const styleValue = ( node . properties . style as string ) ?? '' ;
67
+
68
+ // Replace "shiki" class naming with "astro-code"
69
+ node . properties . class = classValue . replace ( / s h i k i / g, 'astro-code' ) ;
70
+
71
+ // Handle code wrapping
72
+ // if wrap=null, do nothing.
73
+ if ( wrap === false ) {
74
+ node . properties . style = styleValue + '; overflow-x: auto;' ;
75
+ } else if ( wrap === true ) {
76
+ node . properties . style =
77
+ styleValue + '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;' ;
78
+ }
79
+ } ,
80
+ line ( node ) {
81
+ // Add "user-select: none;" for "+"/"-" diff symbols.
82
+ // Transform `<span class="line"><span style="...">+ something</span></span>
83
+ // into `<span class="line"><span style="..."><span style="user-select: none;">+</span> something</span></span>`
84
+ if ( lang === 'diff' ) {
85
+ const innerSpanNode = node . children [ 0 ] ;
86
+ const innerSpanTextNode =
87
+ innerSpanNode ?. type === 'element' && innerSpanNode . children ?. [ 0 ] ;
88
+
89
+ if ( innerSpanTextNode && innerSpanTextNode . type === 'text' ) {
90
+ const start = innerSpanTextNode . value [ 0 ] ;
91
+ if ( start === '+' || start === '-' ) {
92
+ innerSpanTextNode . value = innerSpanTextNode . value . slice ( 1 ) ;
93
+ innerSpanNode . children . unshift ( {
94
+ type : 'element' ,
95
+ tagName : 'span' ,
96
+ properties : { style : 'user-select: none;' } ,
97
+ children : [ { type : 'text' , value : start } ] ,
98
+ } ) ;
99
+ }
119
100
}
120
- } ) ;
121
- }
101
+ }
102
+ } ,
103
+ code ( node ) {
104
+ if ( inline ) {
105
+ return node . children [ 0 ] as typeof node ;
106
+ }
107
+ } ,
108
+ root ( node ) {
109
+ if ( Object . values ( experimentalThemes ) . length ) {
110
+ return ;
111
+ }
112
+
113
+ // theme.id for shiki -> shikiji compat
114
+ const themeName = typeof theme === 'string' ? theme : theme . name ;
115
+ if ( themeName === 'css-variables' ) {
116
+ // Replace special color tokens to CSS variables
117
+ visit ( node as any , 'element' , ( child ) => {
118
+ if ( child . properties ?. style ) {
119
+ child . properties . style = replaceCssVariables ( child . properties . style ) ;
120
+ }
121
+ } ) ;
122
+ }
123
+ } ,
122
124
} ,
123
- } ,
125
+ ...transformers ,
126
+ ] ,
124
127
} ) ;
125
128
} ,
126
129
} ;
0 commit comments