Skip to content

Commit 9396a6f

Browse files
authored
feat: synchronous Shiki usage (#764)
1 parent eb842a3 commit 9396a6f

File tree

14 files changed

+337
-181
lines changed

14 files changed

+337
-181
lines changed

bench/engines.bench.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,43 @@
1+
/* eslint-disable no-console */
12
import fs from 'node:fs/promises'
23
import { bench, describe } from 'vitest'
34
import type { BundledLanguage } from 'shiki'
45
import { createHighlighter, createJavaScriptRegexEngine, createWasmOnigEngine } from 'shiki'
56
import type { ReportItem } from '../scripts/report-engine-js-compat'
67

7-
describe('engines', async () => {
8-
const js = createJavaScriptRegexEngine()
9-
const wasm = await createWasmOnigEngine(() => import('shiki/wasm'))
8+
const js = createJavaScriptRegexEngine()
9+
const wasm = await createWasmOnigEngine(() => import('shiki/wasm'))
1010

11-
// Run `npx jiti scripts/report-engine-js-compat.ts` to generate the report first
12-
const report = await fs.readFile('../scripts/report-engine-js-compat.json', 'utf-8').then(JSON.parse) as ReportItem[]
13-
const langs = report.filter(i => i.highlightMatch === true).map(i => i.lang) as BundledLanguage[]
14-
const samples = await Promise.all(langs.map(lang => fs.readFile(`../tm-grammars-themes/samples/${lang}.sample`, 'utf-8')))
11+
const RANGE = [0, 20]
1512

16-
const shikiJs = await createHighlighter({
17-
langs,
18-
themes: ['vitesse-dark'],
19-
engine: js,
20-
})
13+
// Run `npx jiti scripts/report-engine-js-compat.ts` to generate the report first
14+
const report = await fs.readFile(new URL('../scripts/report-engine-js-compat.json', import.meta.url), 'utf-8').then(JSON.parse) as ReportItem[]
15+
const langs = report.filter(i => i.highlightMatch === true).map(i => i.lang).slice(...RANGE) as BundledLanguage[]
16+
// Clone https://github.com/shikijs/textmate-grammars-themes to `../tm-grammars-themes`
17+
const samples = await Promise.all(langs.map(lang => fs.readFile(`../tm-grammars-themes/samples/${lang}.sample`, 'utf-8')))
2118

22-
const shikiWasm = await createHighlighter({
23-
langs,
24-
themes: ['vitesse-dark'],
25-
engine: wasm,
26-
})
19+
console.log('Benchmarking engines with', langs.length, 'languages')
20+
21+
const shikiJs = await createHighlighter({
22+
langs,
23+
themes: ['vitesse-dark'],
24+
engine: js,
25+
})
2726

28-
bench('js', () => {
29-
for (const lang of langs) {
27+
const shikiWasm = await createHighlighter({
28+
langs,
29+
themes: ['vitesse-dark'],
30+
engine: wasm,
31+
})
32+
33+
for (const lang of langs) {
34+
describe(lang, () => {
35+
bench('js', () => {
3036
shikiJs.codeToTokensBase(samples[langs.indexOf(lang)], { lang, theme: 'vitesse-dark' })
31-
}
32-
}, { warmupIterations: 10, iterations: 30 })
37+
})
3338

34-
bench('wasm', () => {
35-
for (const lang of langs) {
39+
bench('wasm', () => {
3640
shikiWasm.codeToTokensBase(samples[langs.indexOf(lang)], { lang, theme: 'vitesse-dark' })
37-
}
38-
}, { warmupIterations: 10, iterations: 30 })
39-
})
41+
})
42+
})
43+
}

packages/core/src/constructors/highlighter.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { codeToTokensBase, getLastGrammarState } from '../highlight/code-to-toke
55
import { codeToTokensWithThemes } from '../highlight/code-to-tokens-themes'
66
import type { HighlighterCore, HighlighterCoreOptions } from '../types'
77
import { createShikiInternal } from './internal'
8+
import { createShikiInternalSync } from './internal-sync'
89

910
/**
1011
* Create a Shiki core highlighter instance, with no languages or themes bundled.
@@ -27,6 +28,29 @@ export async function createHighlighterCore(options: HighlighterCoreOptions = {}
2728
}
2829
}
2930

31+
/**
32+
* Create a Shiki core highlighter instance, with no languages or themes bundled.
33+
* Wasm and each language and theme must be loaded manually.
34+
*
35+
* Synchronous version of `createHighlighterCore`, which requires to provide the engine and all themes and languages upfront.
36+
*
37+
* @see http://shiki.style/guide/install#fine-grained-bundle
38+
*/
39+
export function createHighlighterCoreSync(options: HighlighterCoreOptions<true> = {}): HighlighterCore {
40+
const internal = createShikiInternalSync(options)
41+
42+
return {
43+
getLastGrammarState: (code, options) => getLastGrammarState(internal, code, options),
44+
codeToTokensBase: (code, options) => codeToTokensBase(internal, code, options),
45+
codeToTokensWithThemes: (code, options) => codeToTokensWithThemes(internal, code, options),
46+
codeToTokens: (code, options) => codeToTokens(internal, code, options),
47+
codeToHast: (code, options) => codeToHast(internal, code, options),
48+
codeToHtml: (code, options) => codeToHtml(internal, code, options),
49+
...internal,
50+
getInternalContext: () => internal,
51+
}
52+
}
53+
3054
export function makeSingletonHighlighterCore(createHighlighter: typeof createHighlighterCore) {
3155
let _shiki: ReturnType<typeof createHighlighterCore>
3256

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type {
2+
HighlighterCoreOptions,
3+
LanguageInput,
4+
LanguageRegistration,
5+
MaybeArray,
6+
ShikiInternal,
7+
SpecialLanguage,
8+
SpecialTheme,
9+
ThemeInput,
10+
ThemeRegistrationAny,
11+
ThemeRegistrationResolved,
12+
} from '../types'
13+
import { Registry } from '../textmate/registry'
14+
import { Resolver } from '../textmate/resolver'
15+
import { normalizeTheme } from '../textmate/normalize-theme'
16+
import { ShikiError } from '../error'
17+
import { resolveLangs, resolveThemes } from '../textmate/getters-resolve'
18+
19+
let instancesCount = 0
20+
21+
/**
22+
* Get the minimal shiki context for rendering.
23+
*
24+
* Synchronous version of `createShikiInternal`, which requires to provide the engine and all themes and languages upfront.
25+
*/
26+
export function createShikiInternalSync(options: HighlighterCoreOptions<true>): ShikiInternal {
27+
instancesCount += 1
28+
if (options.warnings !== false && instancesCount >= 10 && instancesCount % 10 === 0)
29+
console.warn(`[Shiki] ${instancesCount} instances have been created. Shiki is supposed to be used as a singleton, consider refactoring your code to cache your highlighter instance; Or call \`highlighter.dispose()\` to release unused instances.`)
30+
31+
let isDisposed = false
32+
33+
if (!options.engine)
34+
throw new ShikiError('`engine` option is required for synchronous mode')
35+
36+
const langs = (options.langs || []).flat(1)
37+
const themes = (options.themes || []).flat(1).map(normalizeTheme)
38+
39+
const resolver = new Resolver(options.engine, langs)
40+
const _registry = new Registry(resolver, themes, langs, options.langAlias)
41+
42+
let _lastTheme: string | ThemeRegistrationAny
43+
44+
function getLanguage(name: string | LanguageRegistration) {
45+
ensureNotDisposed()
46+
const _lang = _registry.getGrammar(typeof name === 'string' ? name : name.name)
47+
if (!_lang)
48+
throw new ShikiError(`Language \`${name}\` not found, you may need to load it first`)
49+
return _lang
50+
}
51+
52+
function getTheme(name: string | ThemeRegistrationAny): ThemeRegistrationResolved {
53+
if (name === 'none')
54+
return { bg: '', fg: '', name: 'none', settings: [], type: 'dark' }
55+
ensureNotDisposed()
56+
const _theme = _registry.getTheme(name)
57+
if (!_theme)
58+
throw new ShikiError(`Theme \`${name}\` not found, you may need to load it first`)
59+
return _theme
60+
}
61+
62+
function setTheme(name: string | ThemeRegistrationAny) {
63+
ensureNotDisposed()
64+
const theme = getTheme(name)
65+
if (_lastTheme !== name) {
66+
_registry.setTheme(theme)
67+
_lastTheme = name
68+
}
69+
const colorMap = _registry.getColorMap()
70+
return {
71+
theme,
72+
colorMap,
73+
}
74+
}
75+
76+
function getLoadedThemes() {
77+
ensureNotDisposed()
78+
return _registry.getLoadedThemes()
79+
}
80+
81+
function getLoadedLanguages() {
82+
ensureNotDisposed()
83+
return _registry.getLoadedLanguages()
84+
}
85+
86+
function loadLanguageSync(...langs: MaybeArray<LanguageRegistration>[]) {
87+
ensureNotDisposed()
88+
_registry.loadLanguages(langs.flat(1))
89+
}
90+
91+
async function loadLanguage(...langs: (LanguageInput | SpecialLanguage)[]) {
92+
return loadLanguageSync(await resolveLangs(langs))
93+
}
94+
95+
async function loadThemeSync(...themes: MaybeArray<ThemeRegistrationAny>[]) {
96+
ensureNotDisposed()
97+
for (const theme of themes.flat(1)) {
98+
_registry.loadTheme(theme)
99+
}
100+
}
101+
102+
async function loadTheme(...themes: (ThemeInput | SpecialTheme)[]) {
103+
ensureNotDisposed()
104+
return loadThemeSync(await resolveThemes(themes))
105+
}
106+
107+
function ensureNotDisposed() {
108+
if (isDisposed)
109+
throw new ShikiError('Shiki instance has been disposed')
110+
}
111+
112+
function dispose() {
113+
if (isDisposed)
114+
return
115+
isDisposed = true
116+
_registry.dispose()
117+
instancesCount -= 1
118+
}
119+
120+
return {
121+
setTheme,
122+
getTheme,
123+
getLanguage,
124+
getLoadedThemes,
125+
getLoadedLanguages,
126+
loadLanguage,
127+
loadLanguageSync,
128+
loadTheme,
129+
loadThemeSync,
130+
dispose,
131+
[Symbol.dispose]: dispose,
132+
}
133+
}

0 commit comments

Comments
 (0)