Skip to content

Commit

Permalink
feat: support multiple themes for renderToHtmlDualThemes
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 13, 2023
1 parent b4744a8 commit cedad3b
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 49 deletions.
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,14 @@ const shiki = await getHighlighter({

const code = shiki.codeToHtmlDualThemes('console.log("hello")', {
lang: 'javascript',
theme: {
light: 'min-light',
themes: {
light: 'vitesse-light',
dark: 'nord',
}
})
```

The following HTML will be generated ([preview](https://htmlpreview.github.io/?https://raw.githubusercontent.com/antfu/shikiji/main/test/out/dual-themes.html)):
The following HTML will be generated ([demo preview](https://htmlpreview.github.io/?https://raw.githubusercontent.com/antfu/shikiji/main/test/out/dual-themes.html)):

```html
<pre
Expand Down Expand Up @@ -285,6 +285,36 @@ html.dark .shiki span {
}
```

#### Multiple Themes

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.

```js
const code = shiki.codeToHtmlDualThemes('console.log("hello")', {
lang: 'javascript',
themes: {
light: 'github-light',
dark: 'github-dark',
dim: 'github-dimmed',
// any number of themes
},

// optional customizations
defaultColor: 'light',
cssVariablePrefix: '--shiki-'
})
```

Token would be generated like:

```html
<span style="color:#1976D2;--shiki-dark:#D8DEE9;--shiki-dim:#566575">console</span>
```

And then update your CSS snippet to control then each theme taking effect. Here is an example:

[Demo preview](https://htmlpreview.github.io/?https://raw.githubusercontent.com/antfu/shikiji/main/test/out/multiple-themes.html)

## Bundle Size

You can inspect the bundle size in detail on [pkg-size.dev/shikiji](https://pkg-size.dev/shikiji).
Expand Down
4 changes: 2 additions & 2 deletions src/bundled/shorthands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ export async function codeToThemedTokens(code: string, options: RequireKeys<Code
*
* Differences from `shiki.codeToHtmlDualThemes()`, this function is async.
*/
export async function codeToHtmlDualThemes(code: string, options: RequireKeys<CodeToHtmlDualThemesOptions<BuiltinLanguages, BuiltinThemes>, 'theme' | 'lang'>) {
export async function codeToHtmlDualThemes(code: string, options: RequireKeys<CodeToHtmlDualThemesOptions<BuiltinLanguages, BuiltinThemes>, 'themes' | 'lang'>) {
const shiki = await getShikiWithThemeLang({
lang: options.lang,
theme: [options.theme.light, options.theme.dark],
theme: Object.values(options.themes).filter(Boolean) as BuiltinThemes[],
})
return shiki.codeToHtmlDualThemes(code, options)
}
38 changes: 17 additions & 21 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CodeToHtmlDualThemesOptions, CodeToHtmlOptions, CodeToThemedTokensOptions, LanguageInput, MaybeGetter, ThemeInput, ThemedToken } from '../types'
import type { CodeToHtmlDualThemesOptions, CodeToHtmlOptions, CodeToThemedTokensOptions, LanguageInput, MaybeGetter, ThemeInput, ThemeRegisteration, ThemedToken } from '../types'
import type { OnigurumaLoadOptions } from '../oniguruma'
import { createOnigScanner, createOnigString, loadWasm } from '../oniguruma'
import { Registry } from './registry'
Expand Down Expand Up @@ -111,29 +111,25 @@ export async function getHighlighterCore(options: HighlighterCoreOptions) {
function codeToHtmlDualThemes(code: string, options: CodeToHtmlDualThemesOptions): string {
const {
defaultColor = 'light',
cssVariableName = '--shiki-dark',
cssVariablePrefix = '--shiki-',
} = options

const tokens1 = codeToThemedTokens(code, {
...options,
theme: defaultColor === 'light' ? options.theme.light : options.theme.dark,
includeExplanation: false,
})

const tokens2 = codeToThemedTokens(code, {
...options,
theme: defaultColor === 'light' ? options.theme.dark : options.theme.light,
includeExplanation: false,
})

const { _theme: _theme1 } = getTheme(defaultColor === 'light' ? options.theme.light : options.theme.dark)
const { _theme: _theme2 } = getTheme(defaultColor === 'light' ? options.theme.dark : options.theme.light)

return renderToHtmlDualThemes(tokens1, tokens2, cssVariableName, {
fg: `${_theme1.fg};${cssVariableName}:${_theme2.fg}`,
bg: `${_theme1.bg};${cssVariableName}-bg:${_theme2.bg}`,
const themes = Object.entries(options.themes)
.filter(i => i[1])
.sort(a => a[0] === defaultColor ? -1 : 1)

const tokens = themes.map(([color, theme]) => [
color,
codeToThemedTokens(code, {
...options,
theme,
includeExplanation: false,
}),
getTheme(theme)._theme,
] as [string, ThemedToken[][], ThemeRegisteration])

return renderToHtmlDualThemes(tokens, cssVariablePrefix, {
lineOptions: options?.lineOptions,
themeName: `shiki-dual-themes ${_theme1.name}--${_theme2.name}`,
})
}

Expand Down
31 changes: 17 additions & 14 deletions src/core/renderer-html-dual-themes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { HtmlRendererOptions, ThemedToken } from '../types'
import type { HtmlRendererOptions, ThemeRegisteration, ThemedToken } from '../types'
import { renderToHtml } from './renderer-html'

/**
Expand Down Expand Up @@ -55,28 +55,31 @@ export function _syncThemedTokens(...themes: ThemedToken[][][]) {
}

export function renderToHtmlDualThemes(
tokens1: ThemedToken[][],
tokens2: ThemedToken[][],
cssName = '--shiki-dark',
themes: [string, ThemedToken[][], ThemeRegisteration][],
cssVariablePrefix = '--shiki-',
options: HtmlRendererOptions = {},
) {
const [synced1, synced2] = _syncThemedTokens(tokens1, tokens2)
const synced = _syncThemedTokens(...themes.map(t => t[1]))

const merged: ThemedToken[][] = []
for (let i = 0; i < synced1.length; i++) {
const line1 = synced1[i]
const line2 = synced2[i]
for (let i = 0; i < synced[0].length; i++) {
const lines = synced.map(t => t[i])
const lineout: any[] = []
merged.push(lineout)
for (let j = 0; j < line1.length; j++) {
const token1 = line1[j]
const token2 = line2[j]
for (let j = 0; j < lines[0].length; j++) {
const tokens = lines.map(t => t[j])
const colors = tokens.map((t, idx) => `${idx === 0 ? '' : `${cssVariablePrefix + themes[idx][0]}:`}${t.color || 'inherit'}`).join(';')
lineout.push({
...token1,
color: `${token1.color || 'inherit'};${cssName}: ${token2.color || 'inherit'}`,
...tokens[0],
color: colors,
})
}
}

return renderToHtml(merged, options)
return renderToHtml(merged, {
...options,
fg: themes.map((t, idx) => (idx === 0 ? '' : `${cssVariablePrefix + t[0]}:`) + t[2].fg).join(';'),
bg: themes.map((t, idx) => (idx === 0 ? '' : `${cssVariablePrefix + t[0]}-bg:`) + t[2].bg).join(';'),
themeName: `shiki-dual-themes ${themes.map(t => t[2].name).join(' ')}`,
})
}
41 changes: 36 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,49 @@ export interface CodeToHtmlOptions<Languages = string, Themes = string> {

export interface CodeToHtmlDualThemesOptions<Languages = string, Themes = string> {
lang?: Languages | PlainTextLanguage
theme: {

/**
* A map of color names to themes.
*
* `light` and `dark` are required, and arbitrary color names can be added.
*
* @example
* ```ts
* themes: {
* light: 'vitesse-light',
* dark: 'vitesse-dark',
* soft: 'nord',
* // custom colors
* }
* ```
*/
themes: {
light: Themes
dark: Themes
}
} & Partial<Record<string, Themes>>

/**
* The default theme applied to the code (via inline `color` style).
* The rest of the themes are applied via CSS variables, and toggled by CSS overrides.
*
* For example, if `defaultColor` is `light`, then `light` theme is applied to the code,
* and the `dark` theme and other custom themes are applied via CSS variables.
*
* ```html
* <span style="color:#{light};--shiki-dark:#{dark};--shiki-custom:#{custom};">code</span>
* ```
*
* @default 'light'
*/
defaultColor?: 'light' | 'dark'
defaultColor?: StringLiteralUnion<'light' | 'dark'>

/**
* @default '--shiki-dark'
* Prefix of CSS variables used to store the color of the other theme.
*
* @default '--shiki-'
*/
cssVariableName?: string
cssVariablePrefix?: string

lineOptions?: LineOption[]
}

Expand Down
72 changes: 69 additions & 3 deletions test/dual-themes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ describe('syncThemedTokens', () => {
})
})

describe('should', () => {
it('codeToHtmlDualThemes', async () => {
describe('codeToHtmlDualThemes', () => {
it('dual themes', async () => {
const code = await codeToHtmlDualThemes('console.log("hello")', {
lang: 'js',
theme: {
themes: {
dark: 'nord',
light: 'min-light',
},
Expand All @@ -74,4 +74,70 @@ describe('should', () => {
expect(snippet + code)
.toMatchFileSnapshot('./out/dual-themes.html')
})

it('multiple themes', async () => {
const code = await codeToHtmlDualThemes('console.log("hello")', {
lang: 'js',
themes: {
'light': 'vitesse-light',
'dark': 'vitesse-dark',
'nord': 'nord',
'min-dark': 'min-dark',
'min-light': 'min-light',
},
cssVariablePrefix: '--s-',
})

const snippet = `
<style>
.shiki {
padding: 0.5em;
border-radius: 0.25em;
}
[data-theme="dark"] .shiki {
background-color: var(--s-dark-bg) !important;
color: var(--s-dark) !important;
}
[data-theme="dark"] .shiki span {
color: var(--s-dark) !important;
}
[data-theme="nord"] .shiki {
background-color: var(--s-nord-bg) !important;
color: var(--s-nord) !important;
}
[data-theme="nord"] .shiki span {
color: var(--s-nord) !important;
}
[data-theme="min-dark"] .shiki {
background-color: var(--s-min-dark-bg) !important;
color: var(--s-min-dark) !important;
}
[data-theme="min-dark"] .shiki span {
color: var(--s-min-dark) !important;
}
[data-theme="min-light"] .shiki {
background-color: var(--s-min-light-bg) !important;
color: var(--s-min-light) !important;
}
[data-theme="min-light"] .shiki span {
color: var(--s-min-light) !important;
}
</style>
<script>
const themes = ['light', 'dark', 'nord', 'min-dark', 'min-light']
function toggleTheme() {
document.body.dataset.theme = themes[(Math.max(themes.indexOf(document.body.dataset.theme), 0) + 1) % themes.length]
}
</script>
<button onclick="toggleTheme()">Toggle theme</button>
`

expect(snippet + code)
.toMatchFileSnapshot('./out/multiple-themes.html')
})
})
2 changes: 1 addition & 1 deletion test/out/dual-themes.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
}
</style>
<button onclick="document.body.classList.toggle('dark')">Toggle theme</button>
<pre class="shiki shiki-dual-themes min-light--nord" style="background-color: #ffffff;--shiki-dark-bg:#2e3440ff; color: #ffffff;--shiki-dark-bg:#2e3440ff" tabindex="0"><code><span class="line"><span style="color: #1976D2;--shiki-dark: #D8DEE9">console</span><span style="color: #6F42C1;--shiki-dark: #ECEFF4">.</span><span style="color: #6F42C1;--shiki-dark: #88C0D0">log</span><span style="color: #24292EFF;--shiki-dark: #D8DEE9FF">(</span><span style="color: #22863A;--shiki-dark: #ECEFF4">&quot;</span><span style="color: #22863A;--shiki-dark: #A3BE8C">hello</span><span style="color: #22863A;--shiki-dark: #ECEFF4">&quot;</span><span style="color: #24292EFF;--shiki-dark: #D8DEE9FF">)</span></span></code></pre>
<pre class="shiki shiki-dual-themes min-light nord" style="background-color: #ffffff;--shiki-dark-bg:#2e3440ff; color: #ffffff;--shiki-dark-bg:#2e3440ff" tabindex="0"><code><span class="line"><span style="color: #1976D2;--shiki-dark:#D8DEE9">console</span><span style="color: #6F42C1;--shiki-dark:#ECEFF4">.</span><span style="color: #6F42C1;--shiki-dark:#88C0D0">log</span><span style="color: #24292EFF;--shiki-dark:#D8DEE9FF">(</span><span style="color: #22863A;--shiki-dark:#ECEFF4">&quot;</span><span style="color: #22863A;--shiki-dark:#A3BE8C">hello</span><span style="color: #22863A;--shiki-dark:#ECEFF4">&quot;</span><span style="color: #24292EFF;--shiki-dark:#D8DEE9FF">)</span></span></code></pre>
48 changes: 48 additions & 0 deletions test/out/multiple-themes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

<style>
.shiki {
padding: 0.5em;
border-radius: 0.25em;
}

[data-theme="dark"] .shiki {
background-color: var(--s-dark-bg) !important;
color: var(--s-dark) !important;
}
[data-theme="dark"] .shiki span {
color: var(--s-dark) !important;
}

[data-theme="nord"] .shiki {
background-color: var(--s-nord-bg) !important;
color: var(--s-nord) !important;
}
[data-theme="nord"] .shiki span {
color: var(--s-nord) !important;
}

[data-theme="min-dark"] .shiki {
background-color: var(--s-min-dark-bg) !important;
color: var(--s-min-dark) !important;
}
[data-theme="min-dark"] .shiki span {
color: var(--s-min-dark) !important;
}

[data-theme="min-light"] .shiki {
background-color: var(--s-min-light-bg) !important;
color: var(--s-min-light) !important;
}
[data-theme="min-light"] .shiki span {
color: var(--s-min-light) !important;
}
</style>
<script>
const themes = ['light', 'dark', 'nord', 'min-dark', 'min-light']

function toggleTheme() {
document.body.dataset.theme = themes[(Math.max(themes.indexOf(document.body.dataset.theme), 0) + 1) % themes.length]
}
</script>
<button onclick="toggleTheme()">Toggle theme</button>
<pre class="shiki shiki-dual-themes vitesse-light vitesse-dark nord min-dark min-light" style="background-color: #ffffff;--s-dark-bg:#121212;--s-nord-bg:#2e3440ff;--s-min-dark-bg:#1f1f1f;--s-min-light-bg:#ffffff; color: #ffffff;--s-dark-bg:#121212;--s-nord-bg:#2e3440ff;--s-min-dark-bg:#1f1f1f;--s-min-light-bg:#ffffff" tabindex="0"><code><span class="line"><span style="color: #B07D48;--s-dark:#BD976A;--s-nord:#D8DEE9;--s-min-dark:#79B8FF;--s-min-light:#1976D2">console</span><span style="color: #999999;--s-dark:#666666;--s-nord:#ECEFF4;--s-min-dark:#B392F0;--s-min-light:#6F42C1">.</span><span style="color: #59873A;--s-dark:#80A665;--s-nord:#88C0D0;--s-min-dark:#B392F0;--s-min-light:#6F42C1">log</span><span style="color: #999999;--s-dark:#666666;--s-nord:#D8DEE9FF;--s-min-dark:#B392F0;--s-min-light:#24292EFF">(</span><span style="color: #B5695999;--s-dark:#C98A7D99;--s-nord:#ECEFF4;--s-min-dark:#FFAB70;--s-min-light:#22863A">&quot;</span><span style="color: #B56959;--s-dark:#C98A7D;--s-nord:#A3BE8C;--s-min-dark:#FFAB70;--s-min-light:#22863A">hello</span><span style="color: #B5695999;--s-dark:#C98A7D99;--s-nord:#ECEFF4;--s-min-dark:#FFAB70;--s-min-light:#22863A">&quot;</span><span style="color: #999999;--s-dark:#666666;--s-nord:#D8DEE9FF;--s-min-dark:#B392F0;--s-min-light:#24292EFF">)</span></span></code></pre>

0 comments on commit cedad3b

Please sign in to comment.