1- import type { CodeOptionsMeta , CodeOptionsThemes , CodeToHastOptions , CodeToHastOptionsCommon , HighlighterGeneric , TransformerOptions } from 'shiki/core'
1+ import type {
2+ CodeToHastOptions ,
3+ HighlighterGeneric ,
4+ } from 'shiki/core'
25import type { Element , Root } from 'hast'
3- import type { BuiltinTheme } from 'shiki'
46import type { Transformer } from 'unified'
57import { toString } from 'hast-util-to-string'
68import { visit } from 'unist-util-visit'
9+ import { InlineCodeProcessors } from './inline'
10+ import type { RehypeShikiCoreOptions } from './types'
711
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'
7113
7214const languagePrefix = 'language-'
7315
@@ -84,86 +26,129 @@ function rehypeShikiFromHighlighter(
8426 fallbackLanguage,
8527 onError,
8628 stripEndNewline = true ,
29+ inline = false ,
8730 ...rest
8831 } = options
8932
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+ }
12241
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+ }
12594
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+ }
127129
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 )
130134 return
131- }
132135
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 )
135138
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+ }
145142
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'
155145 }
156146
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+ }
167152 }
168153 } )
169154 }
0 commit comments