11import type { ResolvedSlidevOptions } from '@slidev/types'
2+ import type { MarkdownExit } from 'markdown-exit'
23import type { ShikiTransformer } from 'shiki'
34import { isTruthy } from '@antfu/utils'
45import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async'
5- import { escapeVueInCode } from '../transform/utils'
6+ import lz from 'lz-string'
7+ import { escapeVueInCode , normalizeRangeStr } from '../transform/utils'
68
79export default async function MarkdownItShiki ( { data : { config } , mode, utils : { shiki, shikiOptions } } : ResolvedSlidevOptions ) {
810 async function getTwoslashTransformer ( ) {
9- const [ , , { transformerTwoslash } ] = await Promise . all ( [
11+ const [ , , { transformerTwoslash } ] = await Promise . all ( [
1012 // trigger shiki to load the langs
1113 shiki . codeToHast ( '' , { lang : 'js' , ...shikiOptions } ) ,
1214 shiki . codeToHast ( '' , { lang : 'ts' , ...shikiOptions } ) ,
@@ -31,14 +33,63 @@ export default async function MarkdownItShiki({ data: { config }, mode, utils: {
3133 this . addClassToHast ( pre , 'slidev-code' )
3234 delete pre . properties . tabindex
3335 } ,
34- postprocess ( code ) {
35- return escapeVueInCode ( code )
36- } ,
3736 } satisfies ShikiTransformer ,
3837 ] . filter ( isTruthy ) as ShikiTransformer [ ]
3938
40- return fromAsyncCodeToHtml ( shiki . codeToHtml , {
39+ const highlighterPlugin = fromAsyncCodeToHtml ( shiki . codeToHtml , {
4140 ...shikiOptions ,
4241 transformers,
4342 } )
43+
44+ const monacoEnabled = config . monaco === true || config . monaco === mode
45+
46+ return ( md : MarkdownExit ) => {
47+ // @ts -expect-error @shikijs/markdown-it types expect MarkdownItAsync, but MarkdownExit is API-compatible
48+ md . use ( highlighterPlugin )
49+
50+ // Apply CodeBlockWrapper
51+ const oldFence = md . renderer . rules . fence
52+ md . renderer . rules . fence = async function ( tokens , idx , renderOptions , env , slf ) {
53+ const token = tokens [ idx ]
54+ const { monaco, lang, title, ranges, options, rest } = parseMetaString ( token . info )
55+ const optionsProp = options ? `v-bind="${ options } "` : ''
56+ token . info = `${ lang } ${ rest } `
57+
58+ if ( monaco ) {
59+ if ( ! monacoEnabled ) {
60+ return oldFence ?.( tokens , idx , renderOptions , env , slf ) || ''
61+ }
62+
63+ let encoded , diff
64+ if ( monaco === 'monaco-diff' ) {
65+ const [ code , diffStr ] = token . content . split ( / ^ \s * ~ ~ ~ \s * \n / m, 2 )
66+ encoded = lz . compressToBase64 ( code )
67+ diff = diffStr === undefined ? '' : `diff-lz="${ lz . compressToBase64 ( diffStr ) } "`
68+ }
69+ else {
70+ encoded = lz . compressToBase64 ( token . content )
71+ }
72+
73+ const runnable = monaco === 'monaco-run' ? 'runnable' : ''
74+ return `<Monaco ${ optionsProp } ${ runnable } lang="${ lang } " code-lz="${ encoded } " ${ diff } />`
75+ }
76+
77+ token . info = rest
78+ const code = await oldFence ?.( tokens , idx , renderOptions , env , slf ) || ''
79+ return `<CodeBlockWrapper ${ optionsProp } title=${ JSON . stringify ( title ) } :ranges='${ JSON . stringify ( ranges ) } '>${ escapeVueInCode ( code ) } </CodeBlockWrapper>`
80+ }
81+ }
82+ }
83+
84+ const META_RE = / ( [ \w ' - ] + ) ? (?: [ \t ] * | [ \t ] [ \w \t ' - ] * ) (?: \[ ( [ ^ \] ] * ) \] ) ? [ \t ] * (?: \{ ( [ \w * , | - ] + ) \} [ \t ] * ( \{ [ ^ } ] * \} ) ? ( [ ^ \r \n ] * ) ) ? /
85+
86+ function parseMetaString ( meta : string ) {
87+ const [ , lang = '' , title = '' , rangeStr = '' , options = '' , rest = '' ] = meta . trim ( ) . match ( META_RE ) ?? [ ]
88+
89+ if ( title === '' && rangeStr . startsWith ( 'monaco' ) ) {
90+ return { monaco : rangeStr , lang, options, rest }
91+ }
92+
93+ const ranges = normalizeRangeStr ( rangeStr )
94+ return { title, ranges, options, rest }
4495}
0 commit comments