Skip to content

Commit 32f8a6f

Browse files
authored
feat: add a special none theme (#115)
1 parent 57d298e commit 32f8a6f

File tree

11 files changed

+198
-20
lines changed

11 files changed

+198
-20
lines changed

docs/languages.md

+44
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
# Languages
22

3+
## Bundled Languages
4+
35
Language grammars listed below are re-distributed via [`tm-grammars`](https://github.com/antfu/textmate-grammars-themes/tree/main/packages/tm-grammars) into the `shikiji` package.
46

57
<LanguagesList />
68

79
Grammars are covered by their repositories’ respective licenses, which are permissive (apache-2.0, mit, etc), and made available in [this NOTICE](https://github.com/antfu/textmate-grammars-themes/blob/main/packages/tm-grammars/NOTICE).
810

911
For loading your custom languages, please reference to [this guide](/guide/load-lang).
12+
13+
## Special Languages
14+
15+
### Plain Text
16+
17+
You can set lang to `text` to bypass highlighting. This is useful as the fallback when you receive user specified language that are not available. For example:
18+
19+
```txt
20+
import { codeToHtml } from 'shikiji'
21+
22+
const html = codeToHtml('console.log("Hello World")', {
23+
lang: 'text', // [!code hl]
24+
theme: 'vitesse-light',
25+
})
26+
```
27+
28+
`txt`, `plain` are provided as aliases to `text` as well.
29+
30+
### ANSI
31+
32+
A special processed language `ansi` is provided to highlight terminal outputs. For example:
33+
34+
```ansi
35+
┌ Welcome to VitePress!
36+
│
37+
◇ Where should VitePress initialize the config?
38+
│ ./docs
39+
│
40+
◇ Site title:
41+
│ My Awesome Project
42+
│
43+
◇ Site description:
44+
│ A VitePress Site
45+
│
46+
◆ Theme:
47+
│ ● Default Theme (Out of the box, good-looking docs)
48+
│ ○ Default Theme + Customization
49+
│ ○ Custom Theme
50+
└
51+
```
52+
53+
Check the [raw markdown of code snippet above](https://github.com/antfu/shikiji/blob/main/docs/languages.md?plain=1#L35).

docs/themes.md

+16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
# Themes
22

3+
## Bundled Themes
4+
35
Themes listed below are re-distributed via [`tm-themes`](https://github.com/antfu/textmate-grammars-themes/tree/main/packages/tm-themes) into the `shikiji` package.
46

57
<ThemesList />
68

79
Themes are covered by their repositories’ respective licenses, which are permissive (apache-2.0, mit, etc), and made available in [this NOTICE](https://github.com/antfu/textmate-grammars-themes/blob/main/packages/tm-themes/NOTICE).
810

911
For loading your custom themes, please reference to [this guide](/guide/load-theme).
12+
13+
## Special Themes
14+
15+
You can set theme to `none` to bypass highlighting. This is useful as the fallback when you receive user specified theme names that are not available. For example:
16+
17+
```ts twoslash theme:none
18+
import { codeToHtml } from 'shikiji'
19+
20+
const html = codeToHtml('console.log("Hello World")', {
21+
lang: 'javascript',
22+
theme: 'none', // [!code hl]
23+
})
24+
```
25+

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Root } from 'hast'
2-
import type { BundledHighlighterOptions, CodeToHastOptions, CodeToThemedTokensOptions, CodeToTokensWithThemesOptions, HighlighterCoreOptions, HighlighterGeneric, LanguageInput, MaybeArray, RequireKeys, SpecialLanguage, ThemeInput, ThemeRegistrationAny, ThemedToken, ThemedTokenWithVariants } from './types'
3-
import { isSpecialLang, toArray } from './utils'
2+
import type { BundledHighlighterOptions, CodeToHastOptions, CodeToThemedTokensOptions, CodeToTokensWithThemesOptions, HighlighterCoreOptions, HighlighterGeneric, LanguageInput, MaybeArray, RequireKeys, SpecialLanguage, SpecialTheme, ThemeInput, ThemeRegistrationAny, ThemedToken, ThemedTokenWithVariants } from './types'
3+
import { isSpecialLang, isSpecialTheme, toArray } from './utils'
44
import { getHighlighterCore } from './highlighter'
55

66
export type GetHighlighterFactory<L extends string, T extends string> = (options?: BundledHighlighterOptions<L, T>) => Promise<HighlighterGeneric<L, T>>
@@ -30,7 +30,9 @@ export function createdBundledHighlighter<BundledLangs extends string, BundledTh
3030
return lang as LanguageInput
3131
}
3232

33-
function resolveTheme(theme: ThemeInput | BundledThemes): ThemeInput {
33+
function resolveTheme(theme: ThemeInput | BundledThemes | SpecialTheme): ThemeInput | SpecialTheme {
34+
if (isSpecialTheme(theme))
35+
return 'none'
3436
if (typeof theme === 'string') {
3537
const bundle = bundledThemes[theme]
3638
if (!bundle)
@@ -111,7 +113,7 @@ export function createSingletonShorthands<L extends string, T extends string >(g
111113
let _shiki: ReturnType<typeof getHighlighter>
112114

113115
async function _getHighlighter(options: {
114-
theme?: MaybeArray<T | ThemeRegistrationAny>
116+
theme?: MaybeArray<T | ThemeRegistrationAny | SpecialTheme>
115117
lang?: MaybeArray<L | SpecialLanguage>
116118
} = {}) {
117119
if (!_shiki) {

packages/shikiji-core/src/code-to-hast.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export function codeToHast(
6262
tokens = themeTokens
6363
.map(line => line.map(token => mergeToken(token, themesOrder, cssVariablePrefix, defaultColor)))
6464

65-
fg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}:`) + themeRegs[idx].fg).join(';')
66-
bg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}-bg:`) + themeRegs[idx].bg).join(';')
65+
fg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}:`) + (themeRegs[idx].fg || 'inherit')).join(';')
66+
bg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}-bg:`) + (themeRegs[idx].bg || 'inherit')).join(';')
6767
themeName = `shiki-themes ${themeRegs.map(t => t.name).join(' ')}`
6868
rootStyle = defaultColor ? undefined : [fg, bg].join(';')
6969
}

packages/shikiji-core/src/code-to-tokens.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { IGrammar } from './textmate'
55
import { INITIAL } from './textmate'
66
import type { CodeToThemedTokensOptions, FontStyle, ShikiInternal, ThemeRegistrationResolved, ThemedToken, ThemedTokenScopeExplanation, TokenizeWithThemeOptions } from './types'
77
import { StackElementMetadata } from './stack-element-metadata'
8-
import { applyColorReplacements, isPlaintext, splitLines } from './utils'
8+
import { applyColorReplacements, isNoneTheme, isPlainLang, splitLines } from './utils'
99
import { tokenizeAnsiWithTheme } from './code-to-tokens-ansi'
1010

1111
export function codeToThemedTokens(
@@ -18,7 +18,7 @@ export function codeToThemedTokens(
1818
theme: themeName = internal.getLoadedThemes()[0],
1919
} = options
2020

21-
if (isPlaintext(lang))
21+
if (isPlainLang(lang) || isNoneTheme(themeName))
2222
return splitLines(code).map(line => [{ content: line[0], offset: line[1] }])
2323

2424
const { theme, colorMap } = internal.setTheme(themeName)

packages/shikiji-core/src/internal.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { HighlighterCoreOptions, LanguageInput, MaybeGetter, ShikiInternal, ThemeInput, ThemeRegistrationResolved } from './types'
1+
import type { HighlighterCoreOptions, LanguageInput, MaybeGetter, ShikiInternal, SpecialTheme, ThemeInput, ThemeRegistrationResolved } from './types'
22
import type { LoadWasmOptions } from './oniguruma'
33
import { createOnigScanner, createOnigString, loadWasm } from './oniguruma'
44
import { Registry } from './registry'
55
import { Resolver } from './resolver'
66
import { normalizeTheme } from './normalize'
7+
import { isSpecialTheme } from './utils'
78

89
let _defaultWasmLoader: LoadWasmOptions | undefined
910
/**
@@ -64,7 +65,9 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
6465
return _lang
6566
}
6667

67-
function getTheme(name: string | ThemeRegistrationResolved) {
68+
function getTheme(name: string | ThemeRegistrationResolved): ThemeRegistrationResolved {
69+
if (name === 'none')
70+
return { bg: '', fg: '', name: 'none', settings: [], type: 'dark' }
6871
const _theme = _registry.getTheme(name)
6972
if (!_theme)
7073
throw new Error(`[shikiji] Theme \`${name}\` not found, you may need to load it first`)
@@ -96,9 +99,13 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
9699
await _registry.loadLanguages(await resolveLangs(langs))
97100
}
98101

99-
async function loadTheme(...themes: ThemeInput[]) {
102+
async function loadTheme(...themes: (ThemeInput | SpecialTheme)[]) {
100103
await Promise.all(
101-
themes.map(async theme => _registry.loadTheme(await normalizeGetter(theme))),
104+
themes.map(async theme =>
105+
isSpecialTheme(theme)
106+
? null
107+
: _registry.loadTheme(await normalizeGetter(theme)),
108+
),
102109
)
103110
}
104111

packages/shikiji-core/src/types.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type PlainTextLanguage = 'text' | 'plaintext' | 'txt'
2727
export type AnsiLanguage = 'ansi'
2828
export type SpecialLanguage = PlainTextLanguage | AnsiLanguage
2929

30+
export type SpecialTheme = 'none'
31+
3032
export type Awaitable<T> = T | Promise<T>
3133
export type MaybeGetter<T> = Awaitable<MaybeModule<T>> | (() => Awaitable<MaybeModule<T>>)
3234
export type MaybeModule<T> = T | { default: T }
@@ -107,7 +109,7 @@ export interface HighlighterGeneric<BundledLangKeys extends string, BundledTheme
107109
/**
108110
* Load a theme to the highlighter, so later it can be used synchronously.
109111
*/
110-
loadTheme(...themes: (ThemeInput | BundledThemeKeys)[]): Promise<void>
112+
loadTheme(...themes: (ThemeInput | BundledThemeKeys | SpecialTheme)[]): Promise<void>
111113
/**
112114
* Load a language to the highlighter, so later it can be used synchronously.
113115
*/
@@ -136,6 +138,8 @@ export interface HighlighterGeneric<BundledLangKeys extends string, BundledTheme
136138
getLoadedLanguages(): string[]
137139
/**
138140
* Get the names of loaded themes
141+
*
142+
* Special-handled themes like `none` are not included.
139143
*/
140144
getLoadedThemes(): string[]
141145

@@ -220,7 +224,7 @@ export interface LanguageRegistration extends RawGrammar {
220224

221225
export interface CodeToThemedTokensOptions<Languages = string, Themes = string> extends TokenizeWithThemeOptions {
222226
lang?: Languages | SpecialLanguage
223-
theme?: Themes | ThemeRegistrationAny
227+
theme?: Themes | ThemeRegistrationAny | SpecialTheme
224228
}
225229

226230
export interface CodeToHastOptionsCommon<Languages extends string = string> extends
@@ -259,7 +263,7 @@ export interface CodeToTokensWithThemesOptions<Languages = string, Themes = stri
259263
* }
260264
* ```
261265
*/
262-
themes: Partial<Record<string, Themes | ThemeRegistrationAny>>
266+
themes: Partial<Record<string, Themes | ThemeRegistrationAny | SpecialTheme>>
263267
}
264268

265269
export interface CodeOptionsSingleTheme<Themes extends string = string> {

packages/shikiji-core/src/utils.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Element } from 'hast'
2-
import type { MaybeArray, ThemedToken } from './types'
2+
import type { MaybeArray, SpecialTheme, ThemeInput, ThemedToken } from './types'
33

44
export function toArray<T>(x: MaybeArray<T>): T[] {
55
return Array.isArray(x) ? x : [x]
@@ -15,19 +15,37 @@ export function splitLines(str: string) {
1515
/**
1616
* Check if the language is plaintext that is ignored by Shikiji.
1717
*
18-
* Hard-coded languages: `plaintext`, `txt`, `text`, `plain`.
18+
* Hard-coded plain text languages: `plaintext`, `txt`, `text`, `plain`.
1919
*/
20-
export function isPlaintext(lang: string | null | undefined) {
20+
export function isPlainLang(lang: string | null | undefined) {
2121
return !lang || ['plaintext', 'txt', 'text', 'plain'].includes(lang)
2222
}
2323

2424
/**
25-
* Check if the language is specially handled by Shikiji.
25+
* Check if the language is specially handled or bypassed by Shikiji.
2626
*
2727
* Hard-coded languages: `ansi` and plaintexts like `plaintext`, `txt`, `text`, `plain`.
2828
*/
2929
export function isSpecialLang(lang: string) {
30-
return lang === 'ansi' || isPlaintext(lang)
30+
return lang === 'ansi' || isPlainLang(lang)
31+
}
32+
33+
/**
34+
* Check if the theme is specially handled or bypassed by Shikiji.
35+
*
36+
* Hard-coded themes: `none`.
37+
*/
38+
export function isNoneTheme(theme: string | ThemeInput | null | undefined): theme is 'none' {
39+
return theme === 'none'
40+
}
41+
42+
/**
43+
* Check if the theme is specially handled or bypassed by Shikiji.
44+
*
45+
* Hard-coded themes: `none`.
46+
*/
47+
export function isSpecialTheme(theme: string | ThemeInput | null | undefined): theme is SpecialTheme {
48+
return isNoneTheme(theme)
3149
}
3250

3351
/**
@@ -91,3 +109,8 @@ export function splitToken<
91109
export function applyColorReplacements(color: string, replacements?: Record<string, string>): string {
92110
return replacements?.[color.toLowerCase()] || color
93111
}
112+
113+
/**
114+
* @deprecated Use `isPlainLang` instead.
115+
*/
116+
export const isPlaintext = isPlainLang
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
<link rel="stylesheet" href="../../../style-rich.css" />
3+
<style>
4+
html, body { margin: 0; }
5+
.shiki { padding: 2em; }
6+
7+
.dark .shiki,
8+
.dark .shiki span {
9+
color: var(--shiki-dark, inherit);
10+
--twoslash-popup-bg: var(--shiki-dark-bg, inherit);
11+
}
12+
13+
.dark .shiki {
14+
background-color: var(--shiki-dark-bg, inherit);
15+
}
16+
17+
html:not(.dark) .shiki,
18+
html:not(.dark) .shiki span {
19+
color: var(--shiki-light, inherit);
20+
--twoslash-popup-bg: var(--shiki-light-bg, inherit);
21+
}
22+
23+
html:not(.dark) .shiki {
24+
background-color: var(--shiki-light-bg, inherit);
25+
}
26+
</style>
27+
<pre class="shiki none twoslash lsp" style="background-color:;color:" tabindex="0"><code><span class="line"><span>interface </span><span>Todo</span><span> {</span></span>
28+
<span class="line"><span> </span><span>/** The title of the todo item */</span></span>
29+
<span class="line"><span> </span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>Todo.title: string</span></span></code><div class="twoslash-popup-docs">The title of the todo item</div></span>title</span></span><span>: string;</span></span>
30+
<span class="line"><span>}</span></span>
31+
<span class="line"><span></span></span>
32+
<span class="line"><span>const </span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>const todo: Readonly&#x3C;Todo></span></span></code></span>todo</span></span><span>: </span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>type Readonly&#x3C;T> = { readonly [P in keyof T]: T[P]; }</span></span></code><div class="twoslash-popup-docs">Make all properties in T readonly</div></span>Readonly</span></span><span>&#x3C;</span><span>Todo</span><span>> = {</span></span>
33+
<span class="line"><span> </span><span><span class="twoslash-hover twoslash-query-presisted"><span class="twoslash-popup-container"><div class="twoslash-popup-arrow"></div><code class="twoslash-popup-code"><span class="line"><span>title: string</span></span></code><div class="twoslash-popup-docs">The title of the todo item</div></span>title</span></span><span>: "Delete inactive users".</span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>String.toUpperCase(): string</span></span></code><div class="twoslash-popup-docs">Converts all the alphabetic characters in a string to uppercase.</div></span>toUpperCase</span></span><span>(),</span></span>
34+
<span class="line"><span>};</span></span>
35+
<span class="line"><span></span></span>
36+
<span class="line"><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>const todo: Readonly&#x3C;Todo></span></span></code></span>todo</span></span><span>.</span><span><span class="twoslash-error">title</span></span><span> = "Hello";</span></span><div class="twoslash-meta-line twoslash-error-line">Cannot assign to 'title' because it is a read-only property.</div><span class="line"><span></span></span>
37+
<span class="line"><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>var Number: NumberConstructor</span></span></code><div class="twoslash-popup-docs">An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.</div></span>Number</span></span><span>.</span><span><span>p<span class="twoslash-completion-cursor"><ul class="twoslash-completion-list"><li><span class="twoslash-completions-icon completions-method"><svg viewBox="0 0 32 32"><path fill="currentColor" d="m19.626 29.526l-.516-1.933a12.004 12.004 0 0 0 6.121-19.26l1.538-1.28a14.003 14.003 0 0 1-7.143 22.473"></path><path fill="currentColor" d="M10 29H8v-3.82l.804-.16C10.262 24.727 12 23.62 12 20v-1.382l-4-2v-2.236l4-2V12c0-5.467 3.925-9 10-9h2v3.82l-.804.16C21.738 7.273 20 8.38 20 12v.382l4 2v2.236l-4 2V20c0 5.467-3.925 9-10 9m0-2c4.935 0 8-2.682 8-7v-2.618l3.764-1.882L18 13.618V12c0-4.578 2.385-6.192 4-6.76V5c-4.935 0-8 2.682-8 7v1.618L10.236 15.5L14 17.382V20c0 4.578-2.385 6.192-4 6.76Z"></path><path fill="currentColor" d="M5.231 24.947a14.003 14.003 0 0 1 7.147-22.474l.516 1.932a12.004 12.004 0 0 0-6.125 19.263Z"></path></svg></span><span><span class="twoslash-completions-matched">p</span><span class="twoslash-completions-unmatched">arseFloat</span></span></li><li><span class="twoslash-completions-icon completions-method"><svg viewBox="0 0 32 32"><path fill="currentColor" d="m19.626 29.526l-.516-1.933a12.004 12.004 0 0 0 6.121-19.26l1.538-1.28a14.003 14.003 0 0 1-7.143 22.473"></path><path fill="currentColor" d="M10 29H8v-3.82l.804-.16C10.262 24.727 12 23.62 12 20v-1.382l-4-2v-2.236l4-2V12c0-5.467 3.925-9 10-9h2v3.82l-.804.16C21.738 7.273 20 8.38 20 12v.382l4 2v2.236l-4 2V20c0 5.467-3.925 9-10 9m0-2c4.935 0 8-2.682 8-7v-2.618l3.764-1.882L18 13.618V12c0-4.578 2.385-6.192 4-6.76V5c-4.935 0-8 2.682-8 7v1.618L10.236 15.5L14 17.382V20c0 4.578-2.385 6.192-4 6.76Z"></path><path fill="currentColor" d="M5.231 24.947a14.003 14.003 0 0 1 7.147-22.474l.516 1.932a12.004 12.004 0 0 0-6.125 19.263Z"></path></svg></span><span><span class="twoslash-completions-matched">p</span><span class="twoslash-completions-unmatched">arseInt</span></span></li><li><span class="twoslash-completions-icon completions-property"><svg viewBox="0 0 32 32"><path fill="currentColor" d="M12.1 2a9.8 9.8 0 0 0-5.4 1.6l6.4 6.4a2.1 2.1 0 0 1 .2 3a2.1 2.1 0 0 1-3-.2L3.7 6.4A9.84 9.84 0 0 0 2 12.1a10.14 10.14 0 0 0 10.1 10.1a10.9 10.9 0 0 0 2.6-.3l6.7 6.7a5 5 0 0 0 7.1-7.1l-6.7-6.7a10.9 10.9 0 0 0 .3-2.6A10 10 0 0 0 12.1 2m8 10.1a7.61 7.61 0 0 1-.3 2.1l-.3 1.1l.8.8l6.7 6.7a2.88 2.88 0 0 1 .9 2.1A2.72 2.72 0 0 1 27 27a2.9 2.9 0 0 1-4.2 0l-6.7-6.7l-.8-.8l-1.1.3a7.61 7.61 0 0 1-2.1.3a8.27 8.27 0 0 1-5.7-2.3A7.63 7.63 0 0 1 4 12.1a8.33 8.33 0 0 1 .3-2.2l4.4 4.4a4.14 4.14 0 0 0 5.9.2a4.14 4.14 0 0 0-.2-5.9L10 4.2a6.45 6.45 0 0 1 2-.3a8.27 8.27 0 0 1 5.7 2.3a8.49 8.49 0 0 1 2.4 5.9"></path></svg></span><span><span class="twoslash-completions-matched">p</span><span class="twoslash-completions-unmatched">rototype</span></span></li></ul></span></span></span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>NumberConstructor.parseInt(string: string, radix?: number | undefined): number</span></span></code><div class="twoslash-popup-docs">Converts A string to an integer.</div><div class="twoslash-popup-docs twoslash-popup-docs-tags"><span class="twoslash-popup-docs-tag"><span class="twoslash-popup-docs-tag-name">@param</span><span class="twoslash-popup-docs-tag-value">string A string to convert into a number.</span></span><span class="twoslash-popup-docs-tag"><span class="twoslash-popup-docs-tag-name">@param</span><span class="twoslash-popup-docs-tag-value">radix A value between 2 and 36 that specifies the base of the number in `string`.
38+
If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal.
39+
All other strings are considered decimal.</span></span></div></span>arseInt</span></span><span>(</span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>const todo: Readonly&#x3C;Todo></span></span></code></span>todo</span></span><span>.</span><span><span class="twoslash-hover"><span class="twoslash-popup-container"><code class="twoslash-popup-code"><span class="line"><span>title: string</span></span></code><div class="twoslash-popup-docs">The title of the todo item</div></span>title</span></span><span>, 10);</span></span>
40+
<span class="line"><span></span></span></code></pre>

0 commit comments

Comments
 (0)