diff --git a/README.md b/README.md index dd3287e08..11c5d1d33 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,12 @@ To make it reactive to your site's theme, you need to add a short CSS snippet: @media (prefers-color-scheme: dark) { .shiki, .shiki span { - background-color: var(--shiki-dark-bg) !important; color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; } } ``` @@ -289,8 +293,12 @@ To make it reactive to your site's theme, you need to add a short CSS snippet: ```css html.dark .shiki, html.dark .shiki span { - background-color: var(--shiki-dark-bg) !important; color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; } ``` @@ -413,7 +421,7 @@ console.log(root) } ] } - ]s + ] } ] } diff --git a/packages/shikiji/src/core/renderer-hast.ts b/packages/shikiji/src/core/renderer-hast.ts index b37096d93..99df40c15 100644 --- a/packages/shikiji/src/core/renderer-hast.ts +++ b/packages/shikiji/src/core/renderer-hast.ts @@ -47,11 +47,34 @@ export function codeToHast( tokens.push(lineout) for (let j = 0; j < lineMap[0].length; j++) { const tokenMap = lineMap.map(t => t[j]) - const colors = tokenMap.map((t, idx) => `${idx === 0 && defaultColor ? '' : `${cssVariablePrefix + themeTokens[idx][0]}:`}${t.color || 'inherit'}`).join(';') + const tokenStyles = tokenMap.map(t => getTokenStyles(t)) + + // Get all style keys, for themes that missing some style, we put `inherit` to override as needed + const styleKeys = new Set(tokenStyles.flatMap(t => Object.keys(t))) + const mergedStyles = tokenStyles.reduce((acc, cur, idx) => { + for (const key of styleKeys) { + const value = cur[key] || 'inherit' + + if (idx === 0 && defaultColor) { + acc[key] = value + } + else { + const varKey = cssVariablePrefix + themeTokens[idx][0] + (key === 'color' ? '' : `-${key}`) + if (acc[key]) + acc[key] += `;${varKey}:${value}` + else + acc[key] = `${varKey}:${value}` + } + } + return acc + }, {} as Record) + lineout.push({ ...tokenMap[0], - color: colors, - htmlStyle: defaultColor ? undefined : colors, + color: '', + htmlStyle: defaultColor + ? stringifyTokenStyle(mergedStyles) + : Object.values(mergedStyles).join(';'), }) } } @@ -134,15 +157,7 @@ export function tokensToHast( let col = 0 for (const token of line) { - const styles = [token.htmlStyle || `color:${token.color}`] - if (token.fontStyle) { - if (token.fontStyle & FontStyle.Italic) - styles.push('font-style:italic') - if (token.fontStyle & FontStyle.Bold) - styles.push('font-weight:bold') - if (token.fontStyle & FontStyle.Underline) - styles.push('text-decoration:underline') - } + const styles = [token.htmlStyle || stringifyTokenStyle(getTokenStyles(token))] let tokenNode: Element = { type: 'element', @@ -172,6 +187,25 @@ export function tokensToHast( return options.transforms?.root?.(tree) || tree } +function getTokenStyles(token: ThemedToken) { + const styles: Record = {} + if (token.color) + styles.color = token.color + if (token.fontStyle) { + if (token.fontStyle & FontStyle.Italic) + styles['font-style'] = 'italic' + if (token.fontStyle & FontStyle.Bold) + styles['font-weight'] = 'bold' + if (token.fontStyle & FontStyle.Underline) + styles['text-decoration'] = 'underline' + } + return styles +} + +function stringifyTokenStyle(token: Record) { + return Object.entries(token).map(([key, value]) => `${key}:${value}`).join(';') +} + function mergeWhitespaceTokens(tokens: ThemedToken[][]) { return tokens.map((line) => { const newLine: ThemedToken[] = [] diff --git a/packages/shikiji/test/out/multiple-themes.html b/packages/shikiji/test/out/multiple-themes.html index 509c9a4c6..36185cf1b 100644 --- a/packages/shikiji/test/out/multiple-themes.html +++ b/packages/shikiji/test/out/multiple-themes.html @@ -10,6 +10,9 @@ [data-theme="light"] .shiki span { background-color: var(--s-light-bg) !important; color: var(--s-light) !important; + font-style: var(--s-light-font-style) !important; + font-weight: var(--s-light-font-weight) !important; + text-decoration: var(--s-light-text-decoration) !important; } @@ -17,6 +20,9 @@ [data-theme="dark"] .shiki span { background-color: var(--s-dark-bg) !important; color: var(--s-dark) !important; + font-style: var(--s-dark-font-style) !important; + font-weight: var(--s-dark-font-weight) !important; + text-decoration: var(--s-dark-text-decoration) !important; } @@ -24,6 +30,9 @@ [data-theme="nord"] .shiki span { background-color: var(--s-nord-bg) !important; color: var(--s-nord) !important; + font-style: var(--s-nord-font-style) !important; + font-weight: var(--s-nord-font-weight) !important; + text-decoration: var(--s-nord-text-decoration) !important; } @@ -31,6 +40,9 @@ [data-theme="min-dark"] .shiki span { background-color: var(--s-min-dark-bg) !important; color: var(--s-min-dark) !important; + font-style: var(--s-min-dark-font-style) !important; + font-weight: var(--s-min-dark-font-weight) !important; + text-decoration: var(--s-min-dark-text-decoration) !important; } @@ -38,15 +50,28 @@ [data-theme="min-light"] .shiki span { background-color: var(--s-min-light-bg) !important; color: var(--s-min-light) !important; + font-style: var(--s-min-light-font-style) !important; + font-weight: var(--s-min-light-font-weight) !important; + text-decoration: var(--s-min-light-text-decoration) !important; +} + + +[data-theme="palenight"] .shiki, +[data-theme="palenight"] .shiki span { + background-color: var(--s-palenight-bg) !important; + color: var(--s-palenight) !important; + font-style: var(--s-palenight-font-style) !important; + font-weight: var(--s-palenight-font-weight) !important; + text-decoration: var(--s-palenight-text-decoration) !important; } -
console.log("hello")
\ No newline at end of file +
import * as Shiki from "shikiji"
\ No newline at end of file diff --git a/packages/shikiji/test/themes.test.ts b/packages/shikiji/test/themes.test.ts index e83b69323..85492559d 100644 --- a/packages/shikiji/test/themes.test.ts +++ b/packages/shikiji/test/themes.test.ts @@ -80,9 +80,10 @@ describe('codeToHtml', () => { 'nord': 'nord', 'min-dark': 'min-dark', 'min-light': 'min-light', + 'palenight': 'material-theme-palenight', } as const - const code = await codeToHtml('console.log("hello")', { + const code = await codeToHtml('import * as Shiki from "shikiji"', { lang: 'js', themes, cssVariablePrefix: '--s-', @@ -100,6 +101,9 @@ ${Object.keys(themes).map(theme => ` [data-theme="${theme}"] .shiki span { background-color: var(--s-${theme}-bg) !important; color: var(--s-${theme}) !important; + font-style: var(--s-${theme}-font-style) !important; + font-weight: var(--s-${theme}-font-weight) !important; + text-decoration: var(--s-${theme}-text-decoration) !important; } `).join('\n')} @@ -167,6 +171,32 @@ function toggleTheme() { expect(snippet + code) .toMatchFileSnapshot('./out/multiple-themes-no-default.html') }) + + it('should support font style', async () => { + const input = 'import * as Shiki from \'shiki\';\n' + const code1 = await codeToHtml(input, { + lang: 'js', + themes: { + light: 'material-theme-palenight', + dark: 'nord', + }, + }) + + expect(code1) + .toContain('font-style:italic;--shiki-dark-font-style:inherit') + + const code2 = await codeToHtml(input, { + lang: 'js', + themes: { + light: 'material-theme-palenight', + dark: 'nord', + }, + defaultColor: 'dark', + }) + + expect(code2) + .toContain('font-style:inherit;--shiki-light-font-style:italic') + }) }) describe('codeToTokensWithThemes', () => {