Skip to content

Commit 16dfe4c

Browse files
kermanxantfu
andauthored
perf: use Shiki shorthand (#2026)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 7ac41e6 commit 16dfe4c

File tree

17 files changed

+282
-207
lines changed

17 files changed

+282
-207
lines changed

packages/client/internals/ShikiEditor.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import { getHighlighter } from '#slidev/shiki'
32
import { ref, shallowRef } from 'vue'
43
import { useIME } from '../composables/useIME'
54
@@ -11,14 +10,21 @@ const { composingContent, onInput, onCompositionEnd } = useIME(content)
1110
1211
const textareaEl = ref<HTMLTextAreaElement | null>(null)
1312
14-
const highlight = shallowRef<Awaited<ReturnType<typeof getHighlighter>> | null>(null)
15-
getHighlighter().then(h => highlight.value = h)
13+
const highlight = shallowRef<((code: string) => string) | null>(null)
14+
import('../setup/shiki').then(async (m) => {
15+
const { getEagerHighlighter, defaultHighlightOptions } = await m.default()
16+
const highlighter = await getEagerHighlighter()
17+
highlight.value = (code: string) => highlighter.codeToHtml(code, {
18+
...defaultHighlightOptions,
19+
lang: 'markdown',
20+
})
21+
})
1622
</script>
1723

1824
<template>
1925
<div class="absolute left-3 right-0 inset-y-2 font-mono overflow-x-hidden overflow-y-auto cursor-text">
2026
<div v-if="highlight" class="relative w-full h-max min-h-full">
21-
<div class="relative w-full h-max" v-html="`${highlight(composingContent, 'markdown')}&nbsp;`" />
27+
<div class="relative w-full h-max" v-html="`${highlight(composingContent)}&nbsp;`" />
2228
<textarea
2329
ref="textareaEl" v-model="composingContent" :placeholder="props.placeholder"
2430
class="absolute inset-0 resize-none text-transparent bg-transparent focus:outline-none caret-black dark:caret-white overflow-y-hidden"

packages/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@vueuse/core": "catalog:frontend",
4545
"@vueuse/math": "catalog:frontend",
4646
"@vueuse/motion": "catalog:frontend",
47+
"ansis": "catalog:prod",
4748
"drauu": "catalog:frontend",
4849
"file-saver": "catalog:frontend",
4950
"floating-vue": "catalog:frontend",

packages/client/setup/code-runners.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types'
2+
import type { CodeToHastOptions } from 'shiki'
23
import type ts from 'typescript'
3-
44
import deps from '#slidev/monaco-run-deps'
55
import setups from '#slidev/setups/code-runners'
66
import { createSingletonPromise } from '@antfu/utils'
@@ -15,8 +15,16 @@ export default createSingletonPromise(async () => {
1515
ts: runTypeScript,
1616
}
1717

18-
const { getHighlighter } = await import('#slidev/shiki')
19-
const highlight = await getHighlighter()
18+
const { defaultHighlightOptions, getEagerHighlighter } = await (await import('./shiki')).default()
19+
20+
const highlighter = await getEagerHighlighter()
21+
const highlight = (code: string, lang: string, options?: Partial<CodeToHastOptions>) => {
22+
return highlighter.codeToHtml(code, {
23+
...defaultHighlightOptions,
24+
lang,
25+
...options,
26+
})
27+
}
2028

2129
const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
2230
try {

packages/client/setup/monaco.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { MonacoSetupReturn } from '@slidev/types'
22
import configs from '#slidev/configs'
33
import setups from '#slidev/setups/monaco'
44
import { createSingletonPromise } from '@antfu/utils'
5+
import { shikiToMonaco } from '@shikijs/monaco'
56
import { setupTypeAcquisition } from '@typescript/ata'
67
import * as monaco from 'monaco-editor'
78

@@ -83,14 +84,16 @@ const setup = createSingletonPromise(async () => {
8384
})
8485
: () => { }
8586

87+
const { getEagerHighlighter, languageNames, themeOption } = await (await import('./shiki')).default()
88+
8689
monaco.languages.register({ id: 'vue' })
8790
monaco.languages.register({ id: 'html' })
8891
monaco.languages.register({ id: 'css' })
8992
monaco.languages.register({ id: 'typescript' })
9093
monaco.languages.register({ id: 'javascript' })
91-
92-
const { shiki, languages, themes, shikiToMonaco } = await import('#slidev/shiki')
93-
const highlighter = await shiki
94+
for (const lang of languageNames) {
95+
monaco.languages.register({ id: lang })
96+
}
9497

9598
const editorOptions: MonacoSetupReturn['editorOptions'] & object = {}
9699
for (const setup of setups) {
@@ -111,21 +114,18 @@ const setup = createSingletonPromise(async () => {
111114
})
112115

113116
// Use Shiki to highlight Monaco
117+
const highlighter = await getEagerHighlighter()
114118
shikiToMonaco(highlighter, monaco)
115-
if (typeof themes === 'string') {
116-
monaco.editor.setTheme(themes)
119+
if (typeof themeOption === 'string') {
120+
monaco.editor.setTheme(themeOption)
117121
}
118122
else {
119123
watchEffect(() => {
120124
monaco.editor.setTheme(isDark.value
121-
? themes.dark || 'vitesse-dark'
122-
: themes.light || 'vitesse-light')
125+
? themeOption.dark || 'vitesse-dark'
126+
: themeOption.light || 'vitesse-light')
123127
})
124128
}
125-
// Register all languages, otherwise Monaco will not highlight them
126-
for (const lang of languages) {
127-
monaco.languages.register({ id: lang })
128-
}
129129

130130
return {
131131
monaco,
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// This module also runs in the Node.js environment
2+
3+
import type { ResolvedSlidevUtils, ShikiContext, ShikiSetupReturn } from '@slidev/types'
4+
import type { LanguageInput, ThemeInput, ThemeRegistrationAny } from 'shiki'
5+
import { objectMap } from '@antfu/utils'
6+
import { red, yellow } from 'ansis'
7+
import { bundledLanguages, bundledThemes } from 'shiki'
8+
9+
export const shikiContext: ShikiContext = {
10+
/** @deprecated */
11+
loadTheme() {
12+
throw new Error('`loadTheme` is no longer supported.')
13+
},
14+
}
15+
16+
export function resolveShikiOptions(options: (ShikiSetupReturn | void)[]) {
17+
const mergedOptions: Record<string, any> = Object.assign({}, ...options)
18+
19+
if ('theme' in mergedOptions && 'themes' in mergedOptions)
20+
delete mergedOptions.theme
21+
22+
// Rename theme to themes when provided in multiple themes format, but exclude when it's a theme object.
23+
if (mergedOptions.theme && typeof mergedOptions.theme !== 'string' && !mergedOptions.theme.name && !mergedOptions.theme.tokenColors) {
24+
mergedOptions.themes = mergedOptions.theme
25+
delete mergedOptions.theme
26+
}
27+
28+
// No theme at all, apply the default
29+
if (!mergedOptions.theme && !mergedOptions.themes) {
30+
mergedOptions.themes = {
31+
dark: 'vitesse-dark',
32+
light: 'vitesse-light',
33+
}
34+
}
35+
36+
if (mergedOptions.themes)
37+
mergedOptions.defaultColor = false
38+
39+
const themeOption = extractThemeName(mergedOptions.theme) || extractThemeNames(mergedOptions.themes || {})
40+
const themeNames = typeof themeOption === 'string' ? [themeOption] : Object.values(themeOption)
41+
42+
const themeInput: Record<string, ThemeInput> = Object.assign({}, bundledThemes)
43+
if (typeof mergedOptions.theme === 'object' && mergedOptions.theme?.name) {
44+
themeInput[mergedOptions.theme.name] = mergedOptions.theme
45+
}
46+
if (mergedOptions.themes) {
47+
for (const theme of Object.values<ThemeRegistrationAny | string>(mergedOptions.themes)) {
48+
if (typeof theme === 'object' && theme?.name) {
49+
themeInput[theme.name] = theme
50+
}
51+
}
52+
}
53+
54+
const languageNames = new Set<string>(['markdown', 'vue', 'javascript', 'typescript', 'html', 'css'])
55+
const languageInput: Record<string, LanguageInput> = Object.assign({}, bundledLanguages)
56+
for (const option of options) {
57+
const langs = option?.langs
58+
if (langs == null)
59+
continue
60+
if (Array.isArray(langs)) {
61+
for (const lang of langs.flat()) {
62+
if (typeof lang === 'function') {
63+
console.error(red('[slidev] `langs` option returned by setup/shiki.ts cannot be an array containing functions. Please use the record format (`{ [name]: () => {...} }`) instead.'))
64+
}
65+
else if (typeof lang === 'string') {
66+
// a name of a Shiki built-in language
67+
// which can be loaded on demand without overhead, so all built-in languages are available.
68+
// Only need to include them explicitly in browser environment.
69+
languageNames.add(lang)
70+
}
71+
else if (lang.name) {
72+
// a custom grammar object
73+
languageNames.add(lang.name)
74+
languageInput[lang.name] = lang
75+
for (const alias of lang.aliases || []) {
76+
languageNames.add(alias)
77+
languageInput[alias] = lang
78+
}
79+
}
80+
else {
81+
console.error(red('[slidev] Invalid lang option in shiki setup:'), lang)
82+
}
83+
}
84+
}
85+
else if (typeof langs === 'object') {
86+
// a map from name to loader or grammar object
87+
for (const name of Object.keys(langs))
88+
languageNames.add(name)
89+
Object.assign(languageInput, langs)
90+
}
91+
else {
92+
console.error(red('[slidev] Invalid langs option in shiki setup:'), langs)
93+
}
94+
}
95+
96+
return {
97+
options: mergedOptions as ResolvedSlidevUtils['shikiOptions'],
98+
themeOption,
99+
themeNames,
100+
themeInput,
101+
languageNames,
102+
languageInput,
103+
}
104+
}
105+
106+
function extractThemeName(theme?: ThemeRegistrationAny | string): string | undefined {
107+
if (!theme)
108+
return undefined
109+
if (typeof theme === 'string')
110+
return theme
111+
if (!theme.name)
112+
console.warn(yellow('[slidev] Theme'), theme, yellow('does not have a name, which may cause issues.'))
113+
return theme.name
114+
}
115+
116+
function extractThemeNames(themes?: Record<string, ThemeRegistrationAny | string>): Record<string, string> {
117+
if (!themes)
118+
return {}
119+
return objectMap(themes, (key, theme) => {
120+
const name = extractThemeName(theme)
121+
if (!name)
122+
return undefined
123+
return [key, name]
124+
})
125+
}

packages/client/setup/shiki.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import setups from '#slidev/setups/shiki'
2+
import { createSingletonPromise } from '@antfu/utils'
3+
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
4+
import { createdBundledHighlighter, createSingletonShorthands } from 'shiki/core'
5+
import { resolveShikiOptions, shikiContext } from './shiki-options'
6+
7+
export default createSingletonPromise(async () => {
8+
const { options, languageNames, languageInput, themeOption, themeNames, themeInput } = resolveShikiOptions(await Promise.all(setups.map(setup => setup(shikiContext))))
9+
10+
const createHighlighter = createdBundledHighlighter<string, string>({
11+
engine: createJavaScriptRegexEngine,
12+
langs: languageInput,
13+
themes: themeInput,
14+
})
15+
const shorthands = createSingletonShorthands(createHighlighter)
16+
const getEagerHighlighter = createSingletonPromise(() => shorthands.getSingletonHighlighter({
17+
...options,
18+
langs: [...languageNames],
19+
themes: themeNames,
20+
}))
21+
22+
return {
23+
defaultHighlightOptions: options,
24+
getEagerHighlighter,
25+
shorthands,
26+
languageNames,
27+
themeNames,
28+
themeOption,
29+
}
30+
})
Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
import type { MarkdownItShikiOptions } from '@shikijs/markdown-it'
2-
import type { ShikiSetup } from '@slidev/types'
3-
import type { Highlighter } from 'shiki'
1+
import type { ResolvedSlidevUtils, ShikiSetup } from '@slidev/types'
42
import fs from 'node:fs/promises'
5-
import { bundledLanguages, createHighlighter } from 'shiki'
3+
import { createdBundledHighlighter, createSingletonShorthands } from 'shiki/core'
4+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
5+
import { resolveShikiOptions } from '../../../client/setup/shiki-options'
66
import { loadSetups } from './load'
77

88
let cachedRoots: string[] | undefined
9-
let cachedShiki: {
10-
shiki: Highlighter
11-
shikiOptions: MarkdownItShikiOptions
12-
} | undefined
9+
let cachedShiki: Pick<ResolvedSlidevUtils, 'shiki' | 'shikiOptions'> | undefined
1310

1411
export default async function setupShiki(roots: string[]) {
1512
// Here we use shallow equality because when server is restarted, the roots will be different object.
1613
if (cachedRoots === roots)
1714
return cachedShiki!
18-
cachedShiki?.shiki.dispose()
1915

20-
const options = await loadSetups<ShikiSetup>(
16+
const optionsRaw = await loadSetups<ShikiSetup>(
2117
roots,
2218
'shiki.ts',
2319
[{
@@ -28,37 +24,17 @@ export default async function setupShiki(roots: string[]) {
2824
},
2925
}],
3026
)
31-
const mergedOptions = Object.assign({}, ...options)
27+
const { options, languageInput, themeInput } = resolveShikiOptions(optionsRaw)
3228

33-
if ('theme' in mergedOptions && 'themes' in mergedOptions)
34-
delete mergedOptions.theme
35-
36-
// Rename theme to themes when provided in multiple themes format, but exclude when it's a theme object.
37-
if (mergedOptions.theme && typeof mergedOptions.theme !== 'string' && !mergedOptions.theme.name && !mergedOptions.theme.tokenColors) {
38-
mergedOptions.themes = mergedOptions.theme
39-
delete mergedOptions.theme
40-
}
41-
42-
// No theme at all, apply the default
43-
if (!mergedOptions.theme && !mergedOptions.themes) {
44-
mergedOptions.themes = {
45-
dark: 'vitesse-dark',
46-
light: 'vitesse-light',
47-
}
48-
}
49-
50-
if (mergedOptions.themes)
51-
mergedOptions.defaultColor = false
52-
53-
const shiki = await createHighlighter({
54-
...mergedOptions,
55-
langs: mergedOptions.langs ?? Object.keys(bundledLanguages),
56-
themes: 'themes' in mergedOptions ? Object.values(mergedOptions.themes) : [mergedOptions.theme],
29+
const createHighlighter = createdBundledHighlighter<string, string>({
30+
engine: createJavaScriptRegexEngine,
31+
langs: languageInput,
32+
themes: themeInput,
5733
})
5834

5935
cachedRoots = roots
6036
return cachedShiki = {
61-
shiki,
62-
shikiOptions: mergedOptions,
37+
shiki: createSingletonShorthands(createHighlighter),
38+
shikiOptions: options,
6339
}
6440
}

0 commit comments

Comments
 (0)