Skip to content

Commit 26cd5b2

Browse files
authored
feat!: use hast, unify codeToHtmlThemes to codeToHtml (#9)
1 parent 88d741a commit 26cd5b2

25 files changed

+860
-440
lines changed

README.md

+28-8
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const shiki = await getHighlighter({
3939
await shiki.loadTheme('vitesse-light')
4040
await shiki.loadLanguage('css')
4141

42-
const code = shiki.codeToHtml('const a = 1', { lang: 'javascript' })
42+
const code = shiki.codeToHtml('const a = 1', { lang: 'javascript', theme: 'vitesse-light' })
4343
```
4444

4545
Unlike `shiki`, `shikiji` does not load any themes or languages when not specified.
@@ -49,7 +49,7 @@ import { getHighlighter } from 'shikiji'
4949

5050
const shiki = await getHighlighter()
5151

52-
shiki.codeToHtml('const a = 1', { lang: 'javascript' }) // throws error, `javascript` is not loaded
52+
shiki.codeToHtml('const a = 1', { lang: 'javascript', theme: 'nord' }) // throws error, `javascript` is not loaded
5353

5454
await shiki.loadLanguage('javascript') // load the language
5555
```
@@ -103,7 +103,7 @@ const shiki = await getHighlighterCore({
103103
// optionally, load themes and languages after creation
104104
await shiki.loadTheme(import('shikiji/themes/vitesse-light.mjs'))
105105

106-
const code = shiki.codeToHtml('const a = 1', { lang: 'javascript' })
106+
const code = shiki.codeToHtml('const a = 1', { lang: 'javascript', theme: 'nord' })
107107
```
108108

109109
### CJS Usage
@@ -197,6 +197,15 @@ export default {
197197

198198
## Additional Features
199199

200+
### `codeToHast`
201+
202+
`shikiji` used [`hast`](https://github.com/syntax-tree/hast) to generate HTML. You can use `codeToHast` to generate the AST and use it with tools like [unified](https://github.com/unifiedjs).
203+
204+
```js
205+
const root = shiki.codeToHast('const a = 1', { lang: 'javascript', theme: 'nord' })
206+
```
207+
208+
200209
### Shorthands
201210

202211
In addition to the `getHighlighter` function, `shikiji` also provides some shorthand functions for simpler usage.
@@ -212,7 +221,7 @@ Currently supports:
212221

213222
- `codeToThemedTokens`
214223
- `codeToHtml`
215-
- `codeToHtmlThemes`
224+
- `codeToHast`
216225

217226
Internally they maintain a singleton highlighter instance and load the theme/language on demand. Different from `shiki.codeToHtml`, the `codeToHtml` shorthand function returns a Promise and `lang` and `theme` options are required.
218227

@@ -222,7 +231,7 @@ Internally they maintain a singleton highlighter instance and load the theme/lan
222231

223232
`shikiji` added an experimental light/dark dual themes support. Different from [markdown-it-shiki](https://github.com/antfu/markdown-it-shiki#dark-mode)'s approach which renders the code twice, `shikiji`'s dual themes approach uses CSS variables to store the colors on each token. It's more performant with a smaller bundle size.
224233

225-
Use `codeToHtmlThemes` to render the code with dual themes:
234+
Changing the `theme` option in `codeToHtml` to `options` with `light` and `dark` key to generate with two themes.
226235

227236
```js
228237
import { getHighlighter } from 'shikiji'
@@ -232,7 +241,7 @@ const shiki = await getHighlighter({
232241
langs: ['javascript'],
233242
})
234243

235-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
244+
const code = shiki.codeToHtml('console.log("hello")', {
236245
lang: 'javascript',
237246
themes: {
238247
light: 'vitesse-light',
@@ -293,7 +302,7 @@ html.dark .shiki span {
293302
It's also possible to support more than two themes. In the `themes` object, you can have an arbitrary number of themes, and specify the default theme with `defaultColor` option.
294303

295304
```js
296-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
305+
const code = shiki.codeToHtml('console.log("hello")', {
297306
lang: 'javascript',
298307
themes: {
299308
light: 'github-light',
@@ -323,7 +332,7 @@ And then update your CSS snippet to control then each theme taking effect. Here
323332
If you want to take full control of the colors, or avoid using `!important` to override, you can optionally disable the default color by setting `defaultColor` to `false`.
324333

325334
```js
326-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
335+
const code = shiki.codeToHtml('console.log("hello")', {
327336
lang: 'javascript',
328337
themes: {
329338
light: 'vitesse-light',
@@ -343,6 +352,17 @@ In that case, the generated HTML would have no style out of the box, you need to
343352

344353
It's also possible to control the theme in CSS variables, for more, reference to the great research and examples by [@mayank99](https://github.com/mayank99) in [this issue #6](https://github.com/antfu/shikiji/issues/6).
345354

355+
## Breaking Changes from Shiki
356+
357+
As of [`shiki@0.4.3`](https://github.com/shikijs/shiki/releases/tag/v0.14.3):
358+
359+
- Top level named export `setCDN`, `loadLanguage`, `loadLanguage`, `setWasm`, are dropped.
360+
- `BUNDLED_LANGUAGES`, `BUNDLED_THEMES` are moved to `shikiji/langs` and `shikiji/themes` and renamed to `bundledLanguages` and `bundledThemes` respectively.
361+
- `theme` option for `getHighlighter` is dropped, use `themes` with an array instead.
362+
- Highlighter does not maintain an internal default theme context. `theme` option is required for `codeToHtml` and `codeToThemedTokens`.
363+
- CJS and IIFE builds are dropped.
364+
- `LanguageRegistration`'s `grammar` field if flattened to `LanguageRegistration` itself (refer to the types for more details).
365+
346366
## Bundle Size
347367

348368
You can inspect the bundle size in detail on [pkg-size.dev/shikiji](https://pkg-size.dev/shikiji).

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
"@rollup/plugin-replace": "^5.0.2",
2121
"@rollup/plugin-terser": "^0.4.3",
2222
"@types/fs-extra": "^11.0.1",
23+
"@types/hast": "^3.0.0",
2324
"@types/node": "^20.4.10",
2425
"@vitest/coverage-v8": "^0.34.1",
2526
"bumpp": "^9.1.1",
2627
"eslint": "^8.47.0",
2728
"esno": "^0.17.0",
2829
"fast-glob": "^3.3.1",
2930
"fs-extra": "^11.1.1",
31+
"hast-util-to-html": "^9.0.0",
32+
"hastscript": "^8.0.0",
3033
"jsonc-parser": "^3.2.0",
3134
"lint-staged": "^13.2.3",
3235
"pnpm": "^8.6.12",

packages/shikiji/README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Currently supports:
209209

210210
- `codeToThemedTokens`
211211
- `codeToHtml`
212-
- `codeToHtmlThemes`
212+
- `codeToHast`
213213

214214
Internally they maintain a singleton highlighter instance and load the theme/language on demand. Different from `shiki.codeToHtml`, the `codeToHtml` shorthand function returns a Promise and `lang` and `theme` options are required.
215215

@@ -219,7 +219,7 @@ Internally they maintain a singleton highlighter instance and load the theme/lan
219219

220220
`shikiji` added an experimental light/dark dual themes support. Different from [markdown-it-shiki](https://github.com/antfu/markdown-it-shiki#dark-mode)'s approach which renders the code twice, `shikiji`'s dual themes approach uses CSS variables to store the colors on each token. It's more performant with a smaller bundle size.
221221

222-
Use `codeToHtmlThemes` to render the code with dual themes:
222+
Use `codeToHtml` to render the code with dual themes:
223223

224224
```js
225225
import { getHighlighter } from 'shikiji'
@@ -229,7 +229,7 @@ const shiki = await getHighlighter({
229229
langs: ['javascript'],
230230
})
231231

232-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
232+
const code = shiki.codeToHtml('console.log("hello")', {
233233
lang: 'javascript',
234234
themes: {
235235
light: 'vitesse-light',
@@ -290,7 +290,7 @@ html.dark .shiki span {
290290
It's also possible to support more than two themes. In the `themes` object, you can have an arbitrary number of themes, and specify the default theme with `defaultColor` option.
291291

292292
```js
293-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
293+
const code = shiki.codeToHtml('console.log("hello")', {
294294
lang: 'javascript',
295295
themes: {
296296
light: 'github-light',
@@ -320,7 +320,7 @@ And then update your CSS snippet to control then each theme taking effect. Here
320320
If you want to take full control of the colors, or avoid using `!important` to override, you can optionally disable the default color by setting `defaultColor` to `false`.
321321

322322
```js
323-
const code = shiki.codeToHtmlThemes('console.log("hello")', {
323+
const code = shiki.codeToHtml('console.log("hello")', {
324324
lang: 'javascript',
325325
themes: {
326326
light: 'vitesse-light',

packages/shikiji/src/core/bundle-factory.ts

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { BundledHighlighterOptions, CodeToHtmlOptions, CodeToHtmlThemesOptions, CodeToThemedTokensOptions, CodeToTokensWithThemesOptions, HighlighterCoreOptions, HighlighterGeneric, LanguageInput, MaybeArray, PlainTextLanguage, RequireKeys, ThemeInput } from '../types'
1+
import type { BundledHighlighterOptions, CodeToHastOptions, CodeToThemedTokensOptions, CodeToTokensWithThemesOptions, HighlighterCoreOptions, HighlighterGeneric, LanguageInput, MaybeArray, PlainTextLanguage, RequireKeys, ThemeInput } from '../types'
22
import { isPlaintext, toArray } from './utils'
3-
import { getHighlighterCore } from './core'
3+
import { getHighlighterCore } from './highlighter'
44

55
export type GetHighlighterFactory<L extends string, T extends string> = (options?: BundledHighlighterOptions<L, T>) => Promise<HighlighterGeneric<L, T>>
66

@@ -53,9 +53,6 @@ export function createdBundledHighlighter<BundledLangs extends string, BundledTh
5353

5454
return {
5555
...core,
56-
codeToHtml(code, options = {}) {
57-
return core.codeToHtml(code, options)
58-
},
5956
loadLanguage(...langs) {
6057
return core.loadLanguage(...langs.map(resolveLang))
6158
},
@@ -95,34 +92,37 @@ export function createSingletonShorthands<L extends string, T extends string >(g
9592
*
9693
* Differences from `shiki.codeToHtml()`, this function is async.
9794
*/
98-
async function codeToHtml(code: string, options: RequireKeys<CodeToHtmlOptions<L, T>, 'theme' | 'lang'>) {
99-
const shiki = await _getHighlighter(options)
95+
async function codeToHtml(code: string, options: CodeToHastOptions<L, T>) {
96+
const shiki = await _getHighlighter({
97+
lang: options.lang,
98+
theme: 'theme' in options ? [options.theme] : Object.values(options.themes) as T[],
99+
})
100100
return shiki.codeToHtml(code, options)
101101
}
102102

103103
/**
104-
* Shorthand for `codeToThemedTokens` with auto-loaded theme and language.
104+
* Shorthand for `codeToHtml` with auto-loaded theme and language.
105105
* A singleton highlighter it maintained internally.
106106
*
107-
* Differences from `shiki.codeToThemedTokens()`, this function is async.
107+
* Differences from `shiki.codeToHtml()`, this function is async.
108108
*/
109-
async function codeToThemedTokens(code: string, options: RequireKeys<CodeToThemedTokensOptions<L, T>, 'theme' | 'lang'>) {
110-
const shiki = await _getHighlighter(options)
111-
return shiki.codeToThemedTokens(code, options)
109+
async function codeToHast(code: string, options: CodeToHastOptions<L, T>) {
110+
const shiki = await _getHighlighter({
111+
lang: options.lang,
112+
theme: 'theme' in options ? [options.theme] : Object.values(options.themes) as T[],
113+
})
114+
return shiki.codeToHast(code, options)
112115
}
113116

114117
/**
115-
* Shorthand for `codeToHtmlThemes` with auto-loaded theme and language.
118+
* Shorthand for `codeToThemedTokens` with auto-loaded theme and language.
116119
* A singleton highlighter it maintained internally.
117120
*
118-
* Differences from `shiki.codeToHtmlThemes()`, this function is async.
121+
* Differences from `shiki.codeToThemedTokens()`, this function is async.
119122
*/
120-
async function codeToHtmlThemes(code: string, options: RequireKeys<CodeToHtmlThemesOptions<L, T>, 'themes' | 'lang'>) {
121-
const shiki = await _getHighlighter({
122-
lang: options.lang,
123-
theme: Object.values(options.themes).filter(Boolean) as T[],
124-
})
125-
return shiki.codeToHtmlThemes(code, options)
123+
async function codeToThemedTokens(code: string, options: RequireKeys<CodeToThemedTokensOptions<L, T>, 'theme' | 'lang'>) {
124+
const shiki = await _getHighlighter(options)
125+
return shiki.codeToThemedTokens(code, options)
126126
}
127127

128128
/**
@@ -141,7 +141,7 @@ export function createSingletonShorthands<L extends string, T extends string >(g
141141

142142
return {
143143
codeToHtml,
144-
codeToHtmlThemes,
144+
codeToHast,
145145
codeToThemedTokens,
146146
codeToTokensWithThemes,
147147
}

packages/shikiji/src/core/context.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { HighlighterCoreOptions, LanguageInput, MaybeGetter, ShikiContext, ThemeInput } from '../types'
2+
import { createOnigScanner, createOnigString, loadWasm } from '../oniguruma'
3+
import { Registry } from './registry'
4+
import { Resolver } from './resolver'
5+
6+
/**
7+
* Get the minimal shiki context for rendering.
8+
*/
9+
export async function getShikiContext(options: HighlighterCoreOptions = {}): Promise<ShikiContext> {
10+
async function normalizeGetter<T>(p: MaybeGetter<T>): Promise<T> {
11+
return Promise.resolve(typeof p === 'function' ? (p as any)() : p).then(r => r.default || r)
12+
}
13+
14+
async function resolveLangs(langs: LanguageInput[]) {
15+
return Array.from(new Set((await Promise.all(
16+
langs.map(async lang => await normalizeGetter(lang).then(r => Array.isArray(r) ? r : [r])),
17+
)).flat()))
18+
}
19+
20+
const [
21+
themes, langs,
22+
] = await Promise.all([
23+
Promise.all((options.themes || []).map(normalizeGetter)),
24+
resolveLangs(options.langs || []),
25+
typeof options.loadWasm === 'function'
26+
? Promise.resolve(options.loadWasm()).then(r => loadWasm(r))
27+
: options.loadWasm
28+
? loadWasm(options.loadWasm)
29+
: undefined,
30+
] as const)
31+
32+
const resolver = new Resolver(Promise.resolve({
33+
createOnigScanner(patterns) {
34+
return createOnigScanner(patterns)
35+
},
36+
createOnigString(s) {
37+
return createOnigString(s)
38+
},
39+
}), 'vscode-oniguruma', langs)
40+
41+
const _registry = new Registry(resolver, themes, langs)
42+
await _registry.init()
43+
44+
function getLangGrammar(name: string) {
45+
const _lang = _registry.getGrammar(name)
46+
if (!_lang)
47+
throw new Error(`[shikiji] Language \`${name}\` not found, you may need to load it first`)
48+
return _lang
49+
}
50+
51+
function getTheme(name: string) {
52+
const _theme = _registry.getTheme(name!)
53+
if (!_theme)
54+
throw new Error(`[shikiji] Theme \`${name}\` not found, you may need to load it first`)
55+
return _theme
56+
}
57+
58+
function setTheme(name: string) {
59+
const theme = getTheme(name)
60+
_registry.setTheme(theme)
61+
const colorMap = _registry.getColorMap()
62+
return {
63+
theme,
64+
colorMap,
65+
}
66+
}
67+
68+
function getLoadedThemes() {
69+
return _registry.getLoadedThemes()
70+
}
71+
72+
function getLoadedLanguages() {
73+
return _registry.getLoadedLanguages()
74+
}
75+
76+
async function loadLanguage(...langs: LanguageInput[]) {
77+
await _registry.loadLanguages(await resolveLangs(langs))
78+
}
79+
80+
async function loadTheme(...themes: ThemeInput[]) {
81+
await Promise.all(
82+
themes.map(async theme => _registry.loadTheme(await normalizeGetter(theme))),
83+
)
84+
}
85+
86+
return {
87+
setTheme,
88+
getTheme,
89+
getLangGrammar,
90+
getLoadedThemes,
91+
getLoadedLanguages,
92+
loadLanguage,
93+
loadTheme,
94+
}
95+
}

0 commit comments

Comments
 (0)