Skip to content

Commit 762d51b

Browse files
committed
feat: improve types
1 parent d7f66fd commit 762d51b

File tree

6 files changed

+148
-35
lines changed

6 files changed

+148
-35
lines changed

src/core.ts

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
import type { CodeToHtmlOptions, LanguageInput, MaybeGetter, ThemeInput } from './types'
12
import type { OnigurumaLoadOptions } from './oniguruma'
23
import { createOnigScanner, createOnigString, loadWasm } from './oniguruma'
34
import { Registry } from './registry'
4-
import type { CodeToHtmlOptions, LanguageInput, ThemeInput } from './types'
55
import { Resolver } from './resolver'
66
import { tokenizeWithTheme } from './themedTokenizer'
77
import { renderToHtml } from './renderer'
8-
import { toShikiTheme } from './normalize'
98

109
export * from './types'
1110

@@ -24,7 +23,7 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
2423
themes,
2524
langs,
2625
] = await Promise.all([
27-
Promise.all(options.themes.map(async t => toShikiTheme(typeof t === 'function' ? await t() : t))),
26+
Promise.all(options.themes.map(async t => typeof t === 'function' ? await t() : t)),
2827
Promise.all(options.langs.map(async t => typeof t === 'function' ? await t() : t)),
2928
typeof options.loadWasm === 'function'
3029
? Promise.resolve(options.loadWasm()).then(r => loadWasm(r))
@@ -43,14 +42,14 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
4342
}), 'vscode-oniguruma', langs)
4443

4544
const registry = new Registry(resolver, themes, langs)
46-
47-
await registry.loadLanguages(langs)
45+
await registry.init()
4846

4947
const defaultTheme = themes[0].name
48+
const defaultLang = registry.getLoadedLanguages()[0] || 'text'
5049

5150
function codeToThemedTokens(
5251
code: string,
53-
lang = 'text',
52+
lang = defaultLang,
5453
theme = defaultTheme,
5554
options = { includeExplanation: true },
5655
) {
@@ -64,7 +63,7 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
6463
}
6564

6665
function getTheme(name = defaultTheme) {
67-
const _theme = themes.find(i => i.name === name)!
66+
const _theme = registry.getTheme(name!)
6867
registry.setTheme(_theme)
6968
const _colorMap = registry.getColorMap()
7069
return {
@@ -86,12 +85,36 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
8685
})
8786
}
8887

88+
async function loadLanguage(...langs: LanguageInput[]) {
89+
await Promise.all(
90+
langs.map(async lang =>
91+
registry.loadLanguage(await normalizeGetter(lang)),
92+
),
93+
)
94+
}
95+
96+
async function loadTheme(...themes: ThemeInput[]) {
97+
await Promise.all(
98+
themes.map(async theme =>
99+
registry.loadTheme(await normalizeGetter(theme)),
100+
),
101+
)
102+
}
103+
89104
return {
90105
codeToThemedTokens,
91106
codeToHtml,
107+
loadLanguage,
108+
loadTheme,
109+
getLoadedThemes: () => registry.getLoadedThemes(),
110+
getLoadedLanguages: () => registry.getLoadedLanguages(),
92111
}
93112
}
94113

95114
function isPlaintext(lang: string | null | undefined) {
96115
return !lang || ['plaintext', 'txt', 'text'].includes(lang)
97116
}
117+
118+
async function normalizeGetter<T>(p: MaybeGetter<T>): Promise<T> {
119+
return typeof p === 'function' ? (p as any)() : p
120+
}

src/index.ts

+37-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BuiltinLanguages, BuiltinThemes, LanguageInput, ThemeInput } from './types'
1+
import type { BuiltinLanguages, BuiltinThemes, CodeToHtmlOptions, LanguageInput, ThemeInput } from './types'
22
import { bundledThemes } from './vendor/themes'
33
import { bundledLanguages } from './vendor/langs'
44
import { getHighlighterCore } from './core'
@@ -18,23 +18,48 @@ export interface HighlighterOptions {
1818
}
1919

2020
export async function getHighlighter(options: HighlighterOptions = {}) {
21-
const _themes = (options.themes ?? ['nord']).map((i) => {
22-
if (typeof i === 'string')
23-
return bundledThemes[i]
24-
return i
25-
}) as ThemeInput[]
21+
function resolveLang(lang: LanguageInput | BuiltinLanguages): LanguageInput {
22+
if (typeof lang === 'string') {
23+
const bundle = bundledLanguages[lang]
24+
if (!bundle)
25+
throw new Error(`[shikiji] Unknown language: ${lang}`)
26+
return bundle
27+
}
28+
return lang
29+
}
30+
31+
function resolveTheme(theme: ThemeInput | BuiltinThemes): ThemeInput {
32+
if (typeof theme === 'string') {
33+
const bundle = bundledThemes[theme]
34+
if (!bundle)
35+
throw new Error(`[shikiji] Unknown theme: ${theme}`)
36+
return bundle
37+
}
38+
return theme
39+
}
40+
41+
const _themes = (options.themes ?? ['nord']).map(resolveTheme) as ThemeInput[]
2642

2743
const langs = (options.langs ?? Object.keys(bundledLanguages) as BuiltinLanguages[])
28-
.map((i) => {
29-
if (typeof i === 'string')
30-
return bundledLanguages[i]
31-
return i
32-
})
44+
.map(resolveLang)
3345

34-
return getHighlighterCore({
46+
const core = await getHighlighterCore({
3547
...options,
3648
themes: _themes,
3749
langs,
3850
loadWasm: getWasmInlined,
3951
})
52+
53+
return {
54+
...core,
55+
codeToHtml(code: string, options: CodeToHtmlOptions<BuiltinLanguages, BuiltinThemes> = {}) {
56+
return core.codeToHtml(code, options)
57+
},
58+
loadLanguage(...langs: (LanguageInput | BuiltinLanguages)[]) {
59+
return core.loadLanguage(...langs.map(resolveLang))
60+
},
61+
loadTheme(...themes: (ThemeInput | BuiltinThemes)[]) {
62+
return core.loadTheme(...themes.map(resolveTheme))
63+
},
64+
}
4065
}

src/registry.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { IGrammar, IGrammarConfiguration } from 'vscode-textmate'
22
import { Registry as TextMateRegistry } from 'vscode-textmate'
3-
import type { LanguageRegistration, ThemeRegisteration } from './types'
3+
import type { LanguageRegistration, ThemeRegisteration, ThemeRegisterationRaw } from './types'
44
import type { Resolver } from './resolver'
5+
import { toShikiTheme } from './normalize'
56

67
export class Registry extends TextMateRegistry {
78
public themesPath: string = 'themes/'
@@ -13,17 +14,13 @@ export class Registry extends TextMateRegistry {
1314

1415
constructor(
1516
private _resolver: Resolver,
16-
public _themes: ThemeRegisteration[],
17+
public _themes: (ThemeRegisteration | ThemeRegisterationRaw)[],
1718
public _langs: LanguageRegistration[],
1819
) {
1920
super(_resolver)
2021

21-
this._resolvedThemes = Object.fromEntries(
22-
_themes.map(i => [i.name, i]),
23-
)
24-
this._langMap = Object.fromEntries(
25-
_langs.map(i => [i.name, i]),
26-
)
22+
_themes.forEach(t => this.loadTheme(t))
23+
_langs.forEach(l => this.loadLanguage(l))
2724
}
2825

2926
public getTheme(theme: ThemeRegisteration | string) {
@@ -33,6 +30,13 @@ export class Registry extends TextMateRegistry {
3330
return theme
3431
}
3532

33+
public loadTheme(theme: ThemeRegisteration | ThemeRegisterationRaw) {
34+
const _theme = toShikiTheme(theme)
35+
if (_theme.name)
36+
this._resolvedThemes[_theme.name] = _theme
37+
return theme
38+
}
39+
3640
public getLoadedThemes() {
3741
return Object.keys(this._resolvedThemes) as string[]
3842
}
@@ -42,6 +46,7 @@ export class Registry extends TextMateRegistry {
4246
}
4347

4448
public async loadLanguage(lang: LanguageRegistration) {
49+
this._resolver.addLanguage(lang)
4550
const embeddedLanguages = lang.embeddedLangs?.reduce(async (acc, l, idx) => {
4651
if (!this.getLoadedLanguages().includes(l) && this._resolver.getLangRegistration(l)) {
4752
await this._resolver.loadGrammar(this._resolver.getLangRegistration(l).scopeName)
@@ -65,6 +70,11 @@ export class Registry extends TextMateRegistry {
6570
}
6671
}
6772

73+
async init() {
74+
this._themes.map(t => this.loadTheme(t))
75+
await this.loadLanguages(this._langs)
76+
}
77+
6878
public async loadLanguages(langs: LanguageRegistration[]) {
6979
for (const lang of langs)
7080
this.resolveEmbeddedLanguages(lang)
@@ -83,9 +93,9 @@ export class Registry extends TextMateRegistry {
8393
}
8494

8595
private resolveEmbeddedLanguages(lang: LanguageRegistration) {
96+
this._langMap[lang.name] = lang
8697
if (!this._langGraph.has(lang.name))
8798
this._langGraph.set(lang.name, lang)
88-
8999
if (lang.embeddedLangs) {
90100
for (const embeddedLang of lang.embeddedLangs)
91101
this._langGraph.set(embeddedLang, this._langMap[embeddedLang])

src/types.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export type MaybeGetter<T> = T | (() => Awaitable<T>)
1212
export type ThemeInput = MaybeGetter<ThemeRegisteration | ThemeRegisterationRaw>
1313
export type LanguageInput = MaybeGetter<LanguageRegistration>
1414

15+
interface Nothing {}
16+
17+
/**
18+
* type StringLiteralUnion<'foo'> = 'foo' | string
19+
* This has auto completion whereas `'foo' | string` doesn't
20+
* Adapted from https://github.com/microsoft/TypeScript/issues/29729
21+
*/
22+
export type StringLiteralUnion<T extends U, U = string> = T | (U & Nothing)
23+
1524
export interface LanguageRegistration extends IRawGrammar {
1625
name: string
1726
scopeName: string
@@ -27,9 +36,9 @@ export interface LanguageRegistration extends IRawGrammar {
2736
unbalancedBracketSelectors?: string[]
2837
}
2938

30-
export interface CodeToHtmlOptions {
31-
lang?: string
32-
theme?: string
39+
export interface CodeToHtmlOptions<Languages = string, Themes = string> {
40+
lang?: Languages
41+
theme?: Themes
3342
lineOptions?: LineOption[]
3443
}
3544

test/core.test.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import nord from '../dist/themes/nord.mjs'
88
import onig from '../dist/onig.mjs'
99

1010
describe('should', () => {
11-
it('exported', async () => {
11+
it('works', async () => {
1212
const shiki = await getHighlighterCore({
1313
themes: [nord],
1414
langs: [js],
@@ -17,7 +17,23 @@ describe('should', () => {
1717
},
1818
})
1919

20-
expect(shiki.codeToHtml('console.log', { lang: 'javascript' }))
21-
.toMatchInlineSnapshot('"<pre class=\\"shiki nord\\" style=\\"background-color: #2e3440ff\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #D8DEE9\\">console</span><span style=\\"color: #ECEFF4\\">.</span><span style=\\"color: #D8DEE9\\">log</span></span></code></pre>"')
20+
expect(shiki.codeToHtml('console.log("Hi")', { lang: 'javascript' }))
21+
.toMatchInlineSnapshot('"<pre class=\\"shiki nord\\" style=\\"background-color: #2e3440ff\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #D8DEE9\\">console</span><span style=\\"color: #ECEFF4\\">.</span><span style=\\"color: #88C0D0\\">log</span><span style=\\"color: #D8DEE9FF\\">(</span><span style=\\"color: #ECEFF4\\">&quot;</span><span style=\\"color: #A3BE8C\\">Hi</span><span style=\\"color: #ECEFF4\\">&quot;</span><span style=\\"color: #D8DEE9FF\\">)</span></span></code></pre>"')
22+
})
23+
24+
it('dynamic load theme and lang', async () => {
25+
const shiki = await getHighlighterCore({
26+
themes: [nord],
27+
langs: [js],
28+
loadWasm: {
29+
instantiator: obj => WebAssembly.instantiate(onig, obj),
30+
},
31+
})
32+
33+
await shiki.loadLanguage(() => import('../dist/languages/python.mjs').then(m => m.default))
34+
await shiki.loadTheme(() => import('../dist/themes/vitesse-light.mjs').then(m => m.default))
35+
36+
expect(shiki.codeToHtml('print 1', { lang: 'python', theme: 'vitesse-light' }))
37+
.toMatchInlineSnapshot('"<pre class=\\"shiki vitesse-light\\" style=\\"background-color: #ffffff\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #998418\\">print</span><span style=\\"color: #393A34\\"> </span><span style=\\"color: #2F798A\\">1</span></span></code></pre>"')
2238
})
2339
})

test/index.test.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import { getHighlighter } from '../src'
33

44
describe('should', () => {
5-
it('exported', async () => {
5+
it('works', async () => {
66
const shiki = await getHighlighter({
77
themes: ['vitesse-light'],
88
langs: ['javascript'],
@@ -11,4 +11,34 @@ describe('should', () => {
1111
expect(shiki.codeToHtml('console.log', { lang: 'javascript' }))
1212
.toMatchInlineSnapshot('"<pre class=\\"shiki vitesse-light\\" style=\\"background-color: #ffffff\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #B07D48\\">console</span><span style=\\"color: #999999\\">.</span><span style=\\"color: #B07D48\\">log</span></span></code></pre>"')
1313
})
14+
15+
it('dynamic load theme and lang', async () => {
16+
const shiki = await getHighlighter({
17+
themes: ['vitesse-light'],
18+
langs: ['javascript'],
19+
})
20+
21+
await shiki.loadLanguage('python')
22+
await shiki.loadTheme('min-dark')
23+
24+
expect(shiki.getLoadedLanguages())
25+
.toMatchInlineSnapshot(`
26+
[
27+
"javascript",
28+
"js",
29+
"python",
30+
"py",
31+
]
32+
`)
33+
expect(shiki.getLoadedThemes())
34+
.toMatchInlineSnapshot(`
35+
[
36+
"vitesse-light",
37+
"min-dark",
38+
]
39+
`)
40+
41+
expect(shiki.codeToHtml('print 1', { lang: 'python', theme: 'min-dark' }))
42+
.toMatchInlineSnapshot('"<pre class=\\"shiki min-dark\\" style=\\"background-color: #1f1f1f\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #B392F0\\">print </span><span style=\\"color: #F8F8F8\\">1</span></span></code></pre>"')
43+
})
1444
})

0 commit comments

Comments
 (0)