Skip to content

Commit ed31ee5

Browse files
authoredMay 18, 2024··
feat: scoped color replacements (#680)
1 parent c82feb5 commit ed31ee5

File tree

7 files changed

+133
-21
lines changed

7 files changed

+133
-21
lines changed
 

‎docs/guide/theme-colors.md

+22
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ const html = await codeToHtml(
6666
)
6767
```
6868

69+
In addition, `colorReplacements` may contain scoped replacements. This is useful when you provide multiple themes and want to replace the colors of a specific theme:
70+
71+
```js
72+
const html = await codeToHtml(
73+
code,
74+
{
75+
lang: 'js',
76+
themes: { dark: 'min-dark', light: 'min-light' },
77+
colorReplacements: {
78+
'min-dark': {
79+
'#ff79c6': '#189eff'
80+
},
81+
'min-light': {
82+
'#ff79c6': '#defdef'
83+
}
84+
}
85+
}
86+
)
87+
```
88+
89+
This is only allowed for the `colorReplacements` option and not for the theme object.
90+
6991
## CSS Variables Theme
7092

7193
::: warning Experimental

‎packages/core/src/code-to-tokens-ansi.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { createAnsiSequenceParser, createColorPalette, namedColors } from 'ansi-sequence-parser'
22
import type { ThemeRegistrationResolved, ThemedToken, TokenizeWithThemeOptions } from './types'
33
import { FontStyle } from './types'
4-
import { applyColorReplacements, splitLines } from './utils'
4+
import { applyColorReplacements, resolveColorReplacements, splitLines } from './utils'
55

66
export function tokenizeAnsiWithTheme(
77
theme: ThemeRegistrationResolved,
88
fileContents: string,
99
options?: TokenizeWithThemeOptions,
1010
): ThemedToken[][] {
11-
const colorReplacements = {
12-
...theme.colorReplacements,
13-
...options?.colorReplacements,
14-
}
11+
const colorReplacements = resolveColorReplacements(theme, options)
1512
const lines = splitLines(fileContents)
1613

1714
const colorPalette = createColorPalette(

‎packages/core/src/code-to-tokens-base.ts

+2-5
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 { CodeToTokensBaseOptions, FontStyle, ShikiInternal, ThemeRegistrationResolved, ThemedToken, ThemedTokenScopeExplanation, TokenizeWithThemeOptions } from './types'
77
import { StackElementMetadata } from './stack-element-metadata'
8-
import { applyColorReplacements, isNoneTheme, isPlainLang, splitLines } from './utils'
8+
import { applyColorReplacements, isNoneTheme, isPlainLang, resolveColorReplacements, splitLines } from './utils'
99
import { tokenizeAnsiWithTheme } from './code-to-tokens-ansi'
1010

1111
/**
@@ -40,10 +40,7 @@ export function tokenizeWithTheme(
4040
colorMap: string[],
4141
options: TokenizeWithThemeOptions,
4242
): ThemedToken[][] {
43-
const colorReplacements = {
44-
...theme.colorReplacements,
45-
...options?.colorReplacements,
46-
}
43+
const colorReplacements = resolveColorReplacements(theme, options)
4744

4845
const {
4946
tokenizeMaxLineLength = 0,

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

+6-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { codeToTokensBase } from './code-to-tokens-base'
22
import { codeToTokensWithThemes } from './code-to-tokens-themes'
33
import { ShikiError } from './error'
44
import type { CodeToTokensOptions, ShikiInternal, ThemedToken, ThemedTokenWithVariants, TokensResult } from './types'
5-
import { applyColorReplacements, getTokenStyleObject, stringifyTokenStyle } from './utils'
5+
import { applyColorReplacements, getTokenStyleObject, resolveColorReplacements, stringifyTokenStyle } from './utils'
66

77
/**
88
* High-level code-to-tokens API.
@@ -24,7 +24,6 @@ export function codeToTokens(
2424
const {
2525
defaultColor = 'light',
2626
cssVariablePrefix = '--shiki-',
27-
colorReplacements,
2827
} = options
2928

3029
const themes = Object.entries(options.themes)
@@ -49,19 +48,19 @@ export function codeToTokens(
4948
tokens = themeTokens
5049
.map(line => line.map(token => mergeToken(token, themesOrder, cssVariablePrefix, defaultColor)))
5150

51+
const themeColorReplacements = themes.map(t => resolveColorReplacements(t.theme, options))
52+
5253
fg = themes.map((t, idx) => (idx === 0 && defaultColor
5354
? ''
54-
: `${cssVariablePrefix + t.color}:`) + (applyColorReplacements(themeRegs[idx].fg, colorReplacements) || 'inherit')).join(';')
55+
: `${cssVariablePrefix + t.color}:`) + (applyColorReplacements(themeRegs[idx].fg, themeColorReplacements[idx]) || 'inherit')).join(';')
5556
bg = themes.map((t, idx) => (idx === 0 && defaultColor
5657
? ''
57-
: `${cssVariablePrefix + t.color}-bg:`) + (applyColorReplacements(themeRegs[idx].bg, colorReplacements) || 'inherit')).join(';')
58+
: `${cssVariablePrefix + t.color}-bg:`) + (applyColorReplacements(themeRegs[idx].bg, themeColorReplacements[idx]) || 'inherit')).join(';')
5859
themeName = `shiki-themes ${themeRegs.map(t => t.name).join(' ')}`
5960
rootStyle = defaultColor ? undefined : [fg, bg].join(';')
6061
}
6162
else if ('theme' in options) {
62-
const {
63-
colorReplacements,
64-
} = options
63+
const colorReplacements = resolveColorReplacements(options.theme, options.colorReplacements)
6564

6665
tokens = codeToTokensBase(
6766
internal,

‎packages/core/src/types/tokens.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export interface TokenizeWithThemeOptions {
157157
*
158158
* This will be merged with theme's `colorReplacements` if any.
159159
*/
160-
colorReplacements?: Record<string, string>
160+
colorReplacements?: Record<string, string | Record<string, string>>
161161

162162
/**
163163
* Lines above this length will not be tokenized for performance reasons.

‎packages/core/src/utils.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Element } from 'hast'
22
import { FontStyle } from './types'
3-
import type { MaybeArray, PlainTextLanguage, Position, SpecialLanguage, SpecialTheme, ThemeInput, ThemedToken, TokenStyles } from './types'
3+
import type { MaybeArray, PlainTextLanguage, Position, SpecialLanguage, SpecialTheme, ThemeInput, ThemeRegistrationAny, ThemedToken, TokenStyles, TokenizeWithThemeOptions } from './types'
44

55
export function toArray<T>(x: MaybeArray<T>): T[] {
66
return Array.isArray(x) ? x : [x]
@@ -146,6 +146,21 @@ export function splitTokens<
146146
})
147147
}
148148

149+
export function resolveColorReplacements(
150+
theme: ThemeRegistrationAny | string,
151+
options?: TokenizeWithThemeOptions,
152+
) {
153+
const replacements = typeof theme === 'string' ? {} : { ...theme.colorReplacements }
154+
const themeName = typeof theme === 'string' ? theme : theme.name
155+
for (const [key, value] of Object.entries(options?.colorReplacements || {})) {
156+
if (typeof value === 'string')
157+
replacements[key] = value
158+
else if (key === themeName)
159+
Object.assign(replacements, value)
160+
}
161+
return replacements
162+
}
163+
149164
export function applyColorReplacements(color: string, replacements?: Record<string, string>): string
150165
export function applyColorReplacements(color?: string | undefined, replacements?: Record<string, string>): string | undefined
151166
export function applyColorReplacements(color?: string, replacements?: Record<string, string>): string | undefined {

‎packages/shiki/test/color-replacement.test.ts

+84-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
import { expect, it } from 'vitest'
2-
import { codeToHtml } from '../src'
2+
import type { ThemeRegistrationResolved } from '../src'
3+
import { codeToHtml, resolveColorReplacements } from '../src'
34

4-
it('colorReplacements', async () => {
5+
it('resolveColorReplacements', async () => {
6+
expect(resolveColorReplacements('nord', {
7+
colorReplacements: {
8+
'#000000': '#ffffff',
9+
'nord': {
10+
'#000000': '#222222',
11+
'#abcabc': '#defdef',
12+
'#ffffff': '#111111',
13+
},
14+
'other': {
15+
'#000000': '#444444',
16+
'#ffffff': '#333333',
17+
},
18+
'#ffffff': '#000000',
19+
},
20+
})).toEqual(
21+
{
22+
'#abcabc': '#defdef',
23+
'#000000': '#222222',
24+
'#ffffff': '#000000',
25+
},
26+
)
27+
})
28+
29+
it('flat colorReplacements', async () => {
530
const result = await codeToHtml('console.log("hi")', {
631
lang: 'js',
732
themes: {
@@ -44,3 +69,60 @@ it('colorReplacements', async () => {
4469
"
4570
`)
4671
})
72+
73+
it('scoped colorReplacements', async () => {
74+
const customLightTheme: ThemeRegistrationResolved = {
75+
name: 'custom-light',
76+
type: 'light',
77+
settings: [
78+
{ scope: 'string', settings: { foreground: '#a3be8c' } },
79+
],
80+
fg: '#393a34',
81+
bg: '#b07d48',
82+
}
83+
const customDarkTheme: ThemeRegistrationResolved = {
84+
...customLightTheme,
85+
type: 'dark',
86+
name: 'custom-dark',
87+
}
88+
89+
const result = await codeToHtml('console.log("hi")', {
90+
lang: 'js',
91+
themes: {
92+
light: customLightTheme,
93+
dark: customDarkTheme,
94+
},
95+
colorReplacements: {
96+
'custom-dark': {
97+
'#b07d48': 'var(---replaced-1)',
98+
},
99+
'custom-light': {
100+
'#393a34': 'var(---replaced-2)',
101+
'#b07d48': 'var(---replaced-3)',
102+
},
103+
'#393a34': 'var(---replaced-4)',
104+
},
105+
})
106+
107+
expect(result).toContain('var(---replaced-1)')
108+
expect(result).not.toContain('var(---replaced-2)')
109+
expect(result).toContain('var(---replaced-3)')
110+
expect(result).toContain('var(---replaced-4)')
111+
112+
expect(result.replace(/>/g, '>\n'))
113+
.toMatchInlineSnapshot(`
114+
"<pre class="shiki shiki-themes custom-light custom-dark" style="background-color:var(---replaced-3);--shiki-dark-bg:var(---replaced-1);color:var(---replaced-4);--shiki-dark:var(---replaced-4)" tabindex="0">
115+
<code>
116+
<span class="line">
117+
<span style="color:var(---replaced-4);--shiki-dark:var(---replaced-4)">
118+
console.log(</span>
119+
<span style="color:#A3BE8C;--shiki-dark:#A3BE8C">
120+
"hi"</span>
121+
<span style="color:var(---replaced-4);--shiki-dark:var(---replaced-4)">
122+
)</span>
123+
</span>
124+
</code>
125+
</pre>
126+
"
127+
`)
128+
})

0 commit comments

Comments
 (0)
Please sign in to comment.