1
- import type { CodeOptionsMeta , CodeOptionsThemes , CodeToHastOptions , CodeToHastOptionsCommon , HighlighterGeneric , TransformerOptions } from 'shiki/core'
1
+ import type {
2
+ CodeToHastOptions ,
3
+ HighlighterGeneric ,
4
+ } from 'shiki/core'
2
5
import type { Element , Root } from 'hast'
3
- import type { BuiltinTheme } from 'shiki'
4
6
import type { Transformer } from 'unified'
5
7
import { toString } from 'hast-util-to-string'
6
8
import { visit } from 'unist-util-visit'
9
+ import { InlineCodeProcessors } from './inline'
10
+ import type { RehypeShikiCoreOptions } from './types'
7
11
8
- export interface MapLike < K = any , V = any > {
9
- get : ( key : K ) => V | undefined
10
- set : ( key : K , value : V ) => this
11
- }
12
-
13
- export interface RehypeShikiExtraOptions {
14
- /**
15
- * Add `language-*` class to code element
16
- *
17
- * @default false
18
- */
19
- addLanguageClass ?: boolean
20
-
21
- /**
22
- * The default language to use when is not specified
23
- */
24
- defaultLanguage ?: string
25
-
26
- /**
27
- * The fallback language to use when specified language is not loaded
28
- */
29
- fallbackLanguage ?: string
30
-
31
- /**
32
- * `mdast-util-to-hast` adds a newline to the end of code blocks
33
- *
34
- * This option strips that newline from the code block
35
- *
36
- * @default true
37
- * @see https://github.com/syntax-tree/mdast-util-to-hast/blob/f511a93817b131fb73419bf7d24d73a5b8b0f0c2/lib/handlers/code.js#L22
38
- */
39
- stripEndNewline ?: boolean
40
-
41
- /**
42
- * Custom meta string parser
43
- * Return an object to merge with `meta`
44
- */
45
- parseMetaString ?: (
46
- metaString : string ,
47
- node : Element ,
48
- tree : Root
49
- ) => Record < string , any > | undefined | null
50
-
51
- /**
52
- * Custom map to cache transformed codeToHast result
53
- *
54
- * @default undefined
55
- */
56
- cache ?: MapLike
57
-
58
- /**
59
- * Chance to handle the error
60
- * If not provided, the error will be thrown
61
- */
62
- onError ?: ( error : unknown ) => void
63
- }
64
-
65
- export type RehypeShikiCoreOptions =
66
- & CodeOptionsThemes < BuiltinTheme >
67
- & TransformerOptions
68
- & CodeOptionsMeta
69
- & RehypeShikiExtraOptions
70
- & Omit < CodeToHastOptionsCommon , 'lang' >
12
+ export * from './types'
71
13
72
14
const languagePrefix = 'language-'
73
15
@@ -84,86 +26,129 @@ function rehypeShikiFromHighlighter(
84
26
fallbackLanguage,
85
27
onError,
86
28
stripEndNewline = true ,
29
+ inline = false ,
87
30
...rest
88
31
} = options
89
32
90
- return function ( tree ) {
91
- visit ( tree , 'element' , ( node , index , parent ) => {
92
- if ( ! parent || index == null || node . tagName !== 'pre' )
93
- return
94
-
95
- const head = node . children [ 0 ]
96
-
97
- if (
98
- ! head
99
- || head . type !== 'element'
100
- || head . tagName !== 'code'
101
- || ! head . properties
102
- ) {
103
- return
104
- }
105
-
106
- const classes = head . properties . className
107
- const languageClass = Array . isArray ( classes )
108
- ? classes . find (
109
- d => typeof d === 'string' && d . startsWith ( languagePrefix ) ,
110
- )
111
- : undefined
112
-
113
- let lang = typeof languageClass === 'string' ? languageClass . slice ( languagePrefix . length ) : defaultLanguage
114
-
115
- if ( ! lang )
116
- return
117
-
118
- if ( fallbackLanguage && ! langs . includes ( lang ) )
119
- lang = fallbackLanguage
120
-
121
- let code = toString ( head )
33
+ /**
34
+ * Get the determined language of code block (with default language & fallbacks)
35
+ */
36
+ function getLanguage ( lang = defaultLanguage ) : string | undefined {
37
+ if ( lang && fallbackLanguage && ! langs . includes ( lang ) )
38
+ return fallbackLanguage
39
+ return lang
40
+ }
122
41
123
- if ( stripEndNewline && code . endsWith ( '\n' ) )
124
- code = code . slice ( 0 , - 1 )
42
+ function highlight (
43
+ lang : string ,
44
+ code : string ,
45
+ metaString : string = '' ,
46
+ meta : Record < string , unknown > = { } ,
47
+ ) : Root | undefined {
48
+ const cacheKey = `${ lang } :${ metaString } :${ code } `
49
+ const cachedValue = cache ?. get ( cacheKey )
50
+
51
+ if ( cachedValue ) {
52
+ return cachedValue
53
+ }
54
+
55
+ const codeOptions : CodeToHastOptions = {
56
+ ...rest ,
57
+ lang,
58
+ meta : {
59
+ ...rest . meta ,
60
+ ...meta ,
61
+ __raw : metaString ,
62
+ } ,
63
+ }
64
+
65
+ if ( addLanguageClass ) {
66
+ // always construct a new array, avoid adding the transformer repeatedly
67
+ codeOptions . transformers = [
68
+ ...codeOptions . transformers ?? [ ] ,
69
+ {
70
+ name : 'rehype-shiki:code-language-class' ,
71
+ code ( node ) {
72
+ this . addClassToHast ( node , `${ languagePrefix } ${ lang } ` )
73
+ return node
74
+ } ,
75
+ } ,
76
+ ]
77
+ }
78
+
79
+ if ( stripEndNewline && code . endsWith ( '\n' ) )
80
+ code = code . slice ( 0 , - 1 )
81
+
82
+ try {
83
+ const fragment = highlighter . codeToHast ( code , codeOptions )
84
+ cache ?. set ( cacheKey , fragment )
85
+ return fragment
86
+ }
87
+ catch ( error ) {
88
+ if ( onError )
89
+ onError ( error )
90
+ else
91
+ throw error
92
+ }
93
+ }
125
94
126
- const cachedValue = cache ?. get ( code )
95
+ function processPre ( tree : Root , node : Element ) : Root | undefined {
96
+ const head = node . children [ 0 ]
97
+
98
+ if (
99
+ ! head
100
+ || head . type !== 'element'
101
+ || head . tagName !== 'code'
102
+ || ! head . properties
103
+ ) {
104
+ return
105
+ }
106
+
107
+ const classes = head . properties . className
108
+ const languageClass = Array . isArray ( classes )
109
+ ? classes . find (
110
+ d => typeof d === 'string' && d . startsWith ( languagePrefix ) ,
111
+ )
112
+ : undefined
113
+
114
+ const lang = getLanguage (
115
+ typeof languageClass === 'string'
116
+ ? languageClass . slice ( languagePrefix . length )
117
+ : undefined ,
118
+ )
119
+
120
+ if ( ! lang )
121
+ return
122
+
123
+ const code = toString ( head )
124
+ const metaString = head . data ?. meta ?? head . properties . metastring ?. toString ( ) ?? ''
125
+ const meta = parseMetaString ?.( metaString , node , tree ) || { }
126
+
127
+ return highlight ( lang , code , metaString , meta )
128
+ }
127
129
128
- if ( cachedValue ) {
129
- parent . children . splice ( index , 1 , ...cachedValue )
130
+ return function ( tree ) {
131
+ visit ( tree , 'element' , ( node , index , parent ) => {
132
+ // needed for hast node replacement
133
+ if ( ! parent || index == null )
130
134
return
131
- }
132
135
133
- const metaString = head . data ?. meta ?? head . properties . metastring ?. toString ( ) ?? ''
134
- const meta = parseMetaString ?. ( metaString , node , tree ) || { }
136
+ if ( node . tagName === 'pre' ) {
137
+ const result = processPre ( tree , node )
135
138
136
- const codeOptions : CodeToHastOptions = {
137
- ...rest ,
138
- lang,
139
- meta : {
140
- ...rest . meta ,
141
- ...meta ,
142
- __raw : metaString ,
143
- } ,
144
- }
139
+ if ( result ) {
140
+ parent . children . splice ( index , 1 , ...result . children )
141
+ }
145
142
146
- if ( addLanguageClass ) {
147
- codeOptions . transformers ||= [ ]
148
- codeOptions . transformers . push ( {
149
- name : 'rehype-shiki:code-language-class' ,
150
- code ( node ) {
151
- this . addClassToHast ( node , `${ languagePrefix } ${ lang } ` )
152
- return node
153
- } ,
154
- } )
143
+ // don't look for the `code` node inside
144
+ return 'skip'
155
145
}
156
146
157
- try {
158
- const fragment = highlighter . codeToHast ( code , codeOptions )
159
- cache ?. set ( code , fragment . children )
160
- parent . children . splice ( index , 1 , ...fragment . children )
161
- }
162
- catch ( error ) {
163
- if ( onError )
164
- onError ( error )
165
- else
166
- throw error
147
+ if ( node . tagName === 'code' && inline ) {
148
+ const result = InlineCodeProcessors [ inline ] ?.( { node, getLanguage, highlight } )
149
+ if ( result ) {
150
+ parent . children . splice ( index , 1 , ...result . children )
151
+ }
167
152
}
168
153
} )
169
154
}
0 commit comments