Skip to content

Commit 3acf1bf

Browse files
committed
feat!: improve return type of codeToTokensWithThemes, close #37
1 parent 11b6871 commit 3acf1bf

File tree

4 files changed

+213
-155
lines changed

4 files changed

+213
-155
lines changed

packages/shikiji/src/core/renderer-hast.ts

+65-49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import type { Element, Root, Text } from 'hast'
2-
import type { CodeToHastOptions, HtmlRendererOptions, ShikiContext, ShikijiTransformerContext, ThemedToken } from '../types'
2+
import type {
3+
CodeToHastOptions,
4+
HtmlRendererOptions,
5+
ShikiContext,
6+
ShikijiTransformerContext,
7+
ThemedToken,
8+
ThemedTokenWithVariants,
9+
TokenStyles,
10+
} from '../types'
11+
import css from '../assets/langs/css'
312
import { codeToThemedTokens } from './tokenizer'
413
import { FontStyle } from './stackElementMetadata'
514
import { codeToTokensWithThemes } from './renderer-html-themes'
@@ -22,7 +31,9 @@ export function codeToHast(
2231
} = options
2332

2433
const themes = Object.entries(options.themes)
25-
.filter(i => i[1]) as [string, string][]
34+
.filter(i => i[1])
35+
.map(i => ({ color: i[0], theme: i[1]! }))
36+
.sort((a, b) => a.color === defaultColor ? -1 : b.color === defaultColor ? 1 : 0)
2637

2738
if (themes.length === 0)
2839
throw new Error('[shikiji] `themes` option must not be empty')
@@ -32,55 +43,18 @@ export function codeToHast(
3243
code,
3344
options,
3445
)
35-
.sort(a => a[0] === defaultColor ? -1 : 1)
3646

37-
if (defaultColor && !themeTokens.find(t => t[0] === defaultColor))
47+
if (defaultColor && !themes.find(t => t.color === defaultColor))
3848
throw new Error(`[shikiji] \`themes\` option must contain the defaultColor key \`${defaultColor}\``)
3949

40-
const themeRegs = themeTokens.map(t => context.getTheme(t[1]))
41-
const themeMap = themeTokens.map(t => t[2])
42-
tokens = []
43-
44-
for (let i = 0; i < themeMap[0].length; i++) {
45-
const lineMap = themeMap.map(t => t[i])
46-
const lineout: any[] = []
47-
tokens.push(lineout)
48-
for (let j = 0; j < lineMap[0].length; j++) {
49-
const tokenMap = lineMap.map(t => t[j])
50-
const tokenStyles = tokenMap.map(t => getTokenStyles(t))
51-
52-
// Get all style keys, for themes that missing some style, we put `inherit` to override as needed
53-
const styleKeys = new Set(tokenStyles.flatMap(t => Object.keys(t)))
54-
const mergedStyles = tokenStyles.reduce((acc, cur, idx) => {
55-
for (const key of styleKeys) {
56-
const value = cur[key] || 'inherit'
57-
58-
if (idx === 0 && defaultColor) {
59-
acc[key] = value
60-
}
61-
else {
62-
const varKey = cssVariablePrefix + themeTokens[idx][0] + (key === 'color' ? '' : `-${key}`)
63-
if (acc[key])
64-
acc[key] += `;${varKey}:${value}`
65-
else
66-
acc[key] = `${varKey}:${value}`
67-
}
68-
}
69-
return acc
70-
}, {} as Record<string, string>)
71-
72-
lineout.push({
73-
...tokenMap[0],
74-
color: '',
75-
htmlStyle: defaultColor
76-
? stringifyTokenStyle(mergedStyles)
77-
: Object.values(mergedStyles).join(';'),
78-
})
79-
}
80-
}
50+
const themeRegs = themes.map(t => context.getTheme(t.theme))
8151

82-
fg = themeTokens.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t[0]}:`) + themeRegs[idx].fg).join(';')
83-
bg = themeTokens.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t[0]}-bg:`) + themeRegs[idx].bg).join(';')
52+
const themesOrder = themes.map(t => t.color)
53+
tokens = themeTokens
54+
.map(line => line.map(token => flattenToken(token, themesOrder, cssVariablePrefix, defaultColor)))
55+
56+
fg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}:`) + themeRegs[idx].fg).join(';')
57+
bg = themes.map((t, idx) => (idx === 0 && defaultColor ? '' : `${cssVariablePrefix + t.color}-bg:`) + themeRegs[idx].bg).join(';')
8458
themeName = `shiki-themes ${themeRegs.map(t => t.name).join(' ')}`
8559
rootStyle = defaultColor ? undefined : [fg, bg].join(';')
8660
}
@@ -108,6 +82,48 @@ export function codeToHast(
10882
})
10983
}
11084

85+
/**
86+
*
87+
*/
88+
function flattenToken(
89+
merged: ThemedTokenWithVariants,
90+
variantsOrder: string[],
91+
cssVariablePrefix: string,
92+
defaultColor: string | boolean,
93+
) {
94+
const token: ThemedToken = {
95+
content: merged.content,
96+
explanation: merged.explanation,
97+
}
98+
99+
const styles = variantsOrder.map(t => getTokenStyleObject(merged.variants[t]))
100+
101+
// Get all style keys, for themes that missing some style, we put `inherit` to override as needed
102+
const styleKeys = new Set(styles.flatMap(t => Object.keys(t)))
103+
const mergedStyles = styles.reduce((acc, cur, idx) => {
104+
for (const key of styleKeys) {
105+
const value = cur[key] || 'inherit'
106+
107+
if (idx === 0 && defaultColor) {
108+
acc[key] = value
109+
}
110+
else {
111+
const varKey = cssVariablePrefix + variantsOrder[idx] + (key === 'color' ? '' : `-${key}`)
112+
if (acc[key])
113+
acc[key] += `;${varKey}:${value}`
114+
else
115+
acc[key] = `${varKey}:${value}`
116+
}
117+
}
118+
return acc
119+
}, {} as Record<string, string>)
120+
121+
token.htmlStyle = defaultColor
122+
? stringifyTokenStyle(mergedStyles)
123+
: Object.values(mergedStyles).join(';')
124+
return token
125+
}
126+
111127
export function tokensToHast(
112128
tokens: ThemedToken[][],
113129
options: HtmlRendererOptions,
@@ -190,7 +206,7 @@ export function tokensToHast(
190206
children: [{ type: 'text', value: token.content }],
191207
}
192208

193-
const style = token.htmlStyle || stringifyTokenStyle(getTokenStyles(token))
209+
const style = token.htmlStyle || stringifyTokenStyle(getTokenStyleObject(token))
194210
if (style)
195211
tokenNode.properties.style = style
196212

@@ -223,7 +239,7 @@ export function tokensToHast(
223239
return result
224240
}
225241

226-
function getTokenStyles(token: ThemedToken) {
242+
function getTokenStyleObject(token: TokenStyles) {
227243
const styles: Record<string, string> = {}
228244
if (token.color)
229245
styles.color = token.color

packages/shikiji/src/core/renderer-html-themes.ts

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
1-
import type { CodeToTokensWithThemesOptions, ShikiContext, ThemedToken } from '../types'
1+
import type {
2+
CodeToTokensWithThemesOptions,
3+
ShikiContext,
4+
ThemedToken,
5+
ThemedTokenWithVariants,
6+
} from '../types'
27
import { codeToThemedTokens } from './tokenizer'
38

49
/**
5-
* Get tokens with multiple themes, with synced
10+
* Get tokens with multiple themes
611
*/
712
export function codeToTokensWithThemes(
813
context: ShikiContext,
914
code: string,
1015
options: CodeToTokensWithThemesOptions,
1116
) {
1217
const themes = Object.entries(options.themes)
13-
.filter(i => i[1]) as [string, string][]
18+
.filter(i => i[1])
19+
.map(i => ({ color: i[0], theme: i[1]! }))
1420

1521
const tokens = syncThemesTokenization(
1622
...themes.map(t => codeToThemedTokens(context, code, {
1723
...options,
18-
theme: t[1],
24+
theme: t.theme,
1925
includeExplanation: false,
2026
})),
2127
)
2228

23-
return themes.map(([color, theme], idx) => [
24-
color,
25-
theme,
26-
tokens[idx],
27-
] as [string, string, ThemedToken[][]])
29+
const mergedTokens: ThemedTokenWithVariants[][] = tokens[0]
30+
.map((line, lineIdx) => line
31+
.map((_token, tokenIdx) => {
32+
const mergedToken: ThemedTokenWithVariants = {
33+
content: _token.content,
34+
variants: {},
35+
}
36+
37+
tokens.forEach((t, themeIdx) => {
38+
const {
39+
content: _,
40+
explanation: __,
41+
...styles
42+
} = t[lineIdx][tokenIdx]
43+
44+
mergedToken.variants[themes[themeIdx].color] = styles
45+
})
46+
47+
return mergedToken
48+
}),
49+
)
50+
51+
return mergedTokens
2852
}
2953

3054
/**

packages/shikiji/src/types.ts

+72-16
Original file line numberDiff line numberDiff line change
@@ -54,40 +54,84 @@ export interface ShikiContext {
5454
}
5555

5656
export interface HighlighterGeneric<BundledLangKeys extends string, BundledThemeKeys extends string> {
57+
/**
58+
* Get highlighted code in HTML string
59+
*/
5760
codeToHtml(
5861
code: string,
5962
options: CodeToHastOptions<ResolveBundleKey<BundledLangKeys>, ResolveBundleKey<BundledThemeKeys>>
6063
): string
64+
/**
65+
* Get highlighted code in HAST.
66+
* @see https://github.com/syntax-tree/hast
67+
*/
68+
codeToHast(
69+
code: string,
70+
options: CodeToHastOptions<ResolveBundleKey<BundledLangKeys>, ResolveBundleKey<BundledThemeKeys>>
71+
): Root
72+
/**
73+
* Get highlighted code in tokens.
74+
* @returns A 2D array of tokens, first dimension is lines, second dimension is tokens in a line.
75+
*/
6176
codeToThemedTokens(
6277
code: string,
6378
options: CodeToThemedTokensOptions<ResolveBundleKey<BundledLangKeys>, ResolveBundleKey<BundledThemeKeys>>
6479
): ThemedToken[][]
80+
/**
81+
* Get highlighted code in tokens with multiple themes.
82+
*
83+
* Different from `codeToThemedTokens`, each token will have a `variants` property consisting of an object of color name to token styles.
84+
*
85+
* @returns A 2D array of tokens, first dimension is lines, second dimension is tokens in a line.
86+
*/
6587
codeToTokensWithThemes(
6688
code: string,
6789
options: CodeToTokensWithThemesOptions<ResolveBundleKey<BundledLangKeys>, ResolveBundleKey<BundledThemeKeys>>
68-
): [color: string, theme: string, tokens: ThemedToken[][]][]
69-
codeToHast(
70-
code: string,
71-
options: CodeToHastOptions<ResolveBundleKey<BundledLangKeys>, ResolveBundleKey<BundledThemeKeys>>
72-
): Root
90+
): ThemedTokenWithVariants[][]
7391

92+
/**
93+
* Load a theme to the highlighter, so later it can be used synchronously.
94+
*/
7495
loadTheme(...themes: (ThemeInput | BundledThemeKeys)[]): Promise<void>
96+
/**
97+
* Load a language to the highlighter, so later it can be used synchronously.
98+
*/
7599
loadLanguage(...langs: (LanguageInput | BundledLangKeys | SpecialLanguage)[]): Promise<void>
76100

101+
/**
102+
* Get the theme registration object
103+
*/
77104
getTheme(name: string | ThemeRegistration | ThemeRegistrationRaw): ThemeRegistration
78105

106+
/**
107+
* Get the names of loaded languages
108+
*
109+
* Special-handled languages like `text`, `plain` and `ansi` are not included.
110+
*/
79111
getLoadedLanguages(): string[]
112+
/**
113+
* Get the names of loaded themes
114+
*/
80115
getLoadedThemes(): string[]
81116
}
82117

83118
export interface HighlighterCoreOptions {
119+
/**
120+
* Theme names, or theme registration objects to be loaded upfront.
121+
*/
84122
themes?: ThemeInput[]
123+
/**
124+
* Language names, or language registration objects to be loaded upfront.
125+
*/
85126
langs?: LanguageInput[]
86127
/**
87128
* Alias of languages
88129
* @example { 'my-lang': 'javascript' }
89130
*/
90131
langAlias?: Record<string, string>
132+
/**
133+
* Load wasm file from a custom path or using a custom function.
134+
*/
91135
loadWasm?: OnigurumaLoadOptions | (() => Promise<OnigurumaLoadOptions>)
92136
}
93137

@@ -426,31 +470,43 @@ export interface ThemedTokenExplanation {
426470
* }
427471
*
428472
*/
429-
export interface ThemedToken {
473+
export interface ThemedToken extends TokenStyles, TokenBase {}
474+
475+
export interface TokenBase {
430476
/**
431477
* The content of the token
432478
*/
433479
content: string
434480
/**
435-
* 6 or 8 digit hex code representation of the token's color
481+
* Explanation of
482+
*
483+
* - token text's matching scopes
484+
* - reason that token text is given a color (one matching scope matches a rule (scope -> color) in the theme)
436485
*/
437-
color?: string
486+
explanation?: ThemedTokenExplanation[]
487+
}
488+
489+
export interface TokenStyles {
438490
/**
439-
* Override with custom inline style for HTML renderer.
440-
* When specified, `color` will be ignored.
491+
* 6 or 8 digit hex code representation of the token's color
441492
*/
442-
htmlStyle?: string
493+
color?: string
443494
/**
444495
* Font style of token. Can be None/Italic/Bold/Underline
445496
*/
446497
fontStyle?: FontStyle
447498
/**
448-
* Explanation of
449-
*
450-
* - token text's matching scopes
451-
* - reason that token text is given a color (one matching scope matches a rule (scope -> color) in the theme)
499+
* Override with custom inline style for HTML renderer.
500+
* When specified, `color` and `fontStyle` will be ignored.
452501
*/
453-
explanation?: ThemedTokenExplanation[]
502+
htmlStyle?: string
503+
}
504+
505+
export interface ThemedTokenWithVariants extends TokenBase {
506+
/**
507+
* An object of color name to token styles
508+
*/
509+
variants: Record<string, TokenStyles>
454510
}
455511

456512
export {}

0 commit comments

Comments
 (0)