Skip to content

Commit

Permalink
feat: provide singleton shorthands
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 13, 2023
1 parent e0d7f13 commit e00df00
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 69 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ npm install -D shikiji

## Usage

### Bundled Usage

Basic usage is pretty much the same as `shiki`, only that some APIs are dropped, (for example, the singular `theme` options). Each theme and language file are dynamically imported ES modules, it would be better to list the languages and themes **explicitly** to have the best performance.

```js
Expand All @@ -39,7 +41,20 @@ await shiki.loadLanguage('css')
const code = shiki.codeToHtml('const a = 1', { lang: 'javascript' })
```

### Fine-grained Bundling
#### Shorthands

In addition to the `getHighlighter` function, `shikiji` also provides some shorthand functions for simplier usage.

```js
import { codeToHtml } from 'shikiji'

const code1 = await codeToHtml('const a = 1', { lang: 'javascript', theme: 'nord' })
const code2 = await codeToHtml('<div class="foo">bar</div>', { lang: 'html', theme: 'min-dark' })
```

Internally they maintains 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.

### Fine-grained Bundle

When importing `shikiji`, all the themes and languages are bundled as async chunks. Normally it won't be a concern to you as they are not being loaded if you don't use them. While in some cases you want to control what to bundle size, you can use the core and compose your own bundle.

Expand Down Expand Up @@ -116,7 +131,7 @@ async function main() {

Cloudflare Workers [does not support initializing WebAssembly from binary data](https://community.cloudflare.com/t/fixed-cloudflare-workers-slow-with-moderate-sized-webassembly-bindings/184668/3), so the default wasm build won't work. You need to upload the wasm as assets and import it directly.

Meanwhile, it's also recommended to use the [Fine-grained Bundling](#fine-grained-bundling) approach to reduce the bundle size.
Meanwhile, it's also recommended to use the [Fine-grained Bundle](#fine-grained-bundle) approach to reduce the bundle size.

```ts
import { getHighlighterCore, loadWasm } from 'shikiji/core'
Expand Down
62 changes: 62 additions & 0 deletions src/bundled/highlighter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { bundledThemes } from '../themes'
import { bundledLanguages, bundledLanguagesBase } from '../langs'
import { getHighlighterCore } from '../core'
import { getWasmInlined } from '../wasm'
import type { BuiltinLanguages, BuiltinThemes, CodeToHtmlOptions, LanguageInput, PlainTextLanguage, ThemeInput } from '../types'
import { isPlaintext } from '../core/utils'

export interface HighlighterOptions {
themes?: (ThemeInput | BuiltinThemes)[]
langs?: (LanguageInput | BuiltinLanguages | PlainTextLanguage)[]
}

export type Highlighter = Awaited<ReturnType<typeof getHighlighter>>

export async function getHighlighter(options: HighlighterOptions = {}) {
function resolveLang(lang: LanguageInput | BuiltinLanguages | PlainTextLanguage): LanguageInput {
if (typeof lang === 'string') {
if (isPlaintext(lang))
return []
const bundle = bundledLanguages[lang as BuiltinLanguages]
if (!bundle)
throw new Error(`[shikiji] Language \`${lang}\` is not built-in.`)
return bundle
}
return lang as LanguageInput
}

function resolveTheme(theme: ThemeInput | BuiltinThemes): ThemeInput {
if (typeof theme === 'string') {
const bundle = bundledThemes[theme]
if (!bundle)
throw new Error(`[shikiji] Theme \`${theme}\` is not built-in.`)
return bundle
}
return theme
}

const _themes = (options.themes ?? ['nord']).map(resolveTheme) as ThemeInput[]

const langs = (options.langs ?? Object.keys(bundledLanguagesBase) as BuiltinLanguages[])
.map(resolveLang)

const core = await getHighlighterCore({
...options,
themes: _themes,
langs,
loadWasm: getWasmInlined,
})

return {
...core,
codeToHtml(code: string, options: CodeToHtmlOptions<BuiltinLanguages, BuiltinThemes> = {}) {
return core.codeToHtml(code, options as CodeToHtmlOptions)
},
loadLanguage(...langs: (LanguageInput | BuiltinLanguages | PlainTextLanguage)[]) {
return core.loadLanguage(...langs.map(resolveLang))
},
loadTheme(...themes: (ThemeInput | BuiltinThemes)[]) {
return core.loadTheme(...themes.map(resolveTheme))
},
}
}
2 changes: 2 additions & 0 deletions src/bundled/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './highlighter'
export * from './singleton'
34 changes: 34 additions & 0 deletions src/bundled/singleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { BuiltinLanguages, BuiltinThemes, CodeToHtmlOptions, PlainTextLanguage, RequireKeys } from '../types'
import { getHighlighter } from './highlighter'
import type { Highlighter } from './highlighter'

let _shiki: Promise<Highlighter>

async function getShikiWithThemeLang(options: { theme: BuiltinThemes; lang: BuiltinLanguages | PlainTextLanguage }) {
if (!_shiki) {
_shiki = getHighlighter({
themes: [options.theme],
langs: [options.lang],
})
return _shiki
}
else {
const s = await _shiki
await Promise.all([
s.loadTheme(options.theme),
s.loadLanguage(options.lang),
])
return s
}
}

/**
* Shorthand for codeToHtml with auto-loaded theme and language.
* A singleton highlighter it maintained internally.
*
* Differences from `shiki.codeToHtml()`, this function is async.
*/
export async function codeToHtml(code: string, options: RequireKeys<CodeToHtmlOptions<BuiltinLanguages, BuiltinThemes>, 'theme' | 'lang'>) {
const shiki = await getShikiWithThemeLang(options)
return shiki.codeToHtml(code, options)
}
5 changes: 1 addition & 4 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Registry } from './registry'
import { Resolver } from './resolver'
import { tokenizeWithTheme } from './themedTokenizer'
import { renderToHtml } from './renderer'
import { isPlaintext } from './utils'

export interface HighlighterCoreOptions {
themes: ThemeInput[]
Expand Down Expand Up @@ -121,10 +122,6 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
}
}

function isPlaintext(lang: string | null | undefined) {
return !lang || ['plaintext', 'txt', 'text'].includes(lang)
}

async function normalizeGetter<T>(p: MaybeGetter<T>): Promise<T> {
return Promise.resolve(typeof p === 'function' ? (p as any)() : p).then(r => r.default || r)
}
3 changes: 3 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isPlaintext(lang: string | null | undefined) {
return !lang || ['plaintext', 'txt', 'text'].includes(lang)
}
61 changes: 1 addition & 60 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,5 @@
import type { BuiltinLanguages, BuiltinThemes, CodeToHtmlOptions, LanguageInput, ThemeInput } from './types'
import { bundledThemes } from './themes'
import { bundledLanguages, bundledLanguagesBase } from './langs'
import { getHighlighterCore } from './core'
import { getWasmInlined } from './wasm'

export * from './bundled'
export * from './core'
export * from './themes'
export * from './langs'
export * from './wasm'

export interface HighlighterOptions {
themes?: (ThemeInput | BuiltinThemes)[]
langs?: (LanguageInput | BuiltinLanguages)[]
}

export type Highlighter = Awaited<ReturnType<typeof getHighlighter>>

export async function getHighlighter(options: HighlighterOptions = {}) {
function resolveLang(lang: LanguageInput | BuiltinLanguages): LanguageInput {
if (typeof lang === 'string') {
const bundle = bundledLanguages[lang]
if (!bundle)
throw new Error(`[shikiji] Language \`${lang}\` is not built-in.`)
return bundle
}
return lang as LanguageInput
}

function resolveTheme(theme: ThemeInput | BuiltinThemes): ThemeInput {
if (typeof theme === 'string') {
const bundle = bundledThemes[theme]
if (!bundle)
throw new Error(`[shikiji] Theme \`${theme}\` is not built-in.`)
return bundle
}
return theme
}

const _themes = (options.themes ?? ['nord']).map(resolveTheme) as ThemeInput[]

const langs = (options.langs ?? Object.keys(bundledLanguagesBase) as BuiltinLanguages[])
.map(resolveLang)

const core = await getHighlighterCore({
...options,
themes: _themes,
langs,
loadWasm: getWasmInlined,
})

return {
...core,
codeToHtml(code: string, options: CodeToHtmlOptions<BuiltinLanguages, BuiltinThemes> = {}) {
return core.codeToHtml(code, options as CodeToHtmlOptions)
},
loadLanguage(...langs: (LanguageInput | BuiltinLanguages)[]) {
return core.loadLanguage(...langs.map(resolveLang))
},
loadTheme(...themes: (ThemeInput | BuiltinThemes)[]) {
return core.loadTheme(...themes.map(resolveTheme))
},
}
}
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import type { FontStyle } from './core/stackElementMetadata'
export type BuiltinLanguages = keyof typeof bundledLanguages
export type BuiltinThemes = keyof typeof bundledThemes

export type PlainTextLanguage = 'text' | 'plaintext' | 'txt'

export type Awaitable<T> = T | Promise<T>
export type MaybeGetter<T> = Awaitable<MaybeModule<T>> | (() => Awaitable<MaybeModule<T>>)
export type MaybeModule<T> = T | { default: T }
export type MaybeArray<T> = T | T[]
export type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

export type ThemeInput = MaybeGetter<ThemeRegisteration | ThemeRegisterationRaw>
export type LanguageInput = MaybeGetter<MaybeArray<LanguageRegistration>>
Expand Down Expand Up @@ -39,7 +42,7 @@ export interface LanguageRegistration extends IRawGrammar {
}

export interface CodeToHtmlOptions<Languages = string, Themes = string> {
lang?: Languages | 'text' | 'plaintext' | 'txt'
lang?: Languages | PlainTextLanguage
theme?: Themes
lineOptions?: LineOption[]
}
Expand Down
12 changes: 12 additions & 0 deletions test/singleton.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
import { codeToHtml } from '../src'

describe('should', () => {
it('works', async () => {
expect(await codeToHtml('console.log("hello")', { lang: 'js', theme: 'vitesse-light' }))
.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: #59873A\\">log</span><span style=\\"color: #999999\\">(</span><span style=\\"color: #B5695999\\">&quot;</span><span style=\\"color: #B56959\\">hello</span><span style=\\"color: #B5695999\\">&quot;</span><span style=\\"color: #999999\\">)</span></span></code></pre>"')

expect(await codeToHtml('<div class="foo">bar</div>', { lang: 'html', theme: 'min-dark' }))
.toMatchInlineSnapshot('"<pre class=\\"shiki min-dark\\" style=\\"background-color: #1f1f1f\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color: #B392F0\\">&lt;</span><span style=\\"color: #FFAB70\\">div</span><span style=\\"color: #B392F0\\"> class</span><span style=\\"color: #F97583\\">=</span><span style=\\"color: #FFAB70\\">&quot;foo&quot;</span><span style=\\"color: #B392F0\\">&gt;bar&lt;/</span><span style=\\"color: #FFAB70\\">div</span><span style=\\"color: #B392F0\\">&gt;</span></span></code></pre>"')
})
})
4 changes: 2 additions & 2 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { defineConfig } from 'vitest/config'

// @ts-expect-error no types
import { wasmPlugin } from './build/wasm.mjs'
import { wasmPlugin } from './rollup.config.mjs'

export default defineConfig({
plugins: [
wasmPlugin,
wasmPlugin(),
],
test: {
server: {
Expand Down

0 comments on commit e00df00

Please sign in to comment.