0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

Light/dark theming for shikiji's codeblocks (#8903)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
horo 2023-11-08 23:42:05 +09:00 committed by GitHub
parent 1ecc9aa324
commit c5010aad34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 62 additions and 8 deletions

View file

@ -0,0 +1,6 @@
---
'@astrojs/markdown-remark': minor
'astro': minor
---
Adds experimental support for multiple shiki themes with the new `markdown.shikiConfig.experimentalThemes` option.

View file

@ -32,6 +32,11 @@ interface Props {
* @default "github-dark" * @default "github-dark"
*/ */
theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw; theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw;
/**
* Multiple themes to style with -- alternative to "theme" option.
* Supports all themes found above; see https://github.com/antfu/shikiji#lightdark-dual-themes for more information.
*/
experimentalThemes?: Record<string, BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw>;
/** /**
* Enable word wrapping. * Enable word wrapping.
* - true: enabled. * - true: enabled.
@ -53,6 +58,7 @@ const {
code, code,
lang = 'plaintext', lang = 'plaintext',
theme = 'github-dark', theme = 'github-dark',
experimentalThemes = {},
wrap = false, wrap = false,
inline = false, inline = false,
} = Astro.props; } = Astro.props;
@ -88,12 +94,15 @@ if (typeof lang === 'object') {
const highlighter = await getCachedHighlighter({ const highlighter = await getCachedHighlighter({
langs: [lang], langs: [lang],
themes: [theme], themes: Object.values(experimentalThemes).length ? Object.values(experimentalThemes) : [theme],
}); });
const themeOptions = Object.values(experimentalThemes).length
? { themes: experimentalThemes }
: { theme };
const html = highlighter.codeToHtml(code, { const html = highlighter.codeToHtml(code, {
lang: typeof lang === 'string' ? lang : lang.name, lang: typeof lang === 'string' ? lang : lang.name,
theme, ...themeOptions,
transforms: { transforms: {
pre(node) { pre(node) {
// Swap to `code` tag if inline // Swap to `code` tag if inline
@ -123,6 +132,10 @@ const html = highlighter.codeToHtml(code, {
} }
}, },
root(node) { root(node) {
if (Object.values(experimentalThemes).length) {
return;
}
// theme.id for shiki -> shikiji compat // theme.id for shiki -> shikiji compat
const themeName = typeof theme === 'string' ? theme : theme.name; const themeName = typeof theme === 'string' ? theme : theme.name;
if (themeName === 'css-variables') { if (themeName === 'css-variables') {

View file

@ -292,7 +292,14 @@ export const AstroConfigSchema = z.object({
theme: z theme: z
.enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]]) .enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]])
.or(z.custom<ShikiTheme>()) .or(z.custom<ShikiTheme>())
.default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.theme as BuiltinTheme), .default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.theme!),
experimentalThemes: z
.record(
z
.enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]])
.or(z.custom<ShikiTheme>())
)
.default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.experimentalThemes!),
wrap: z.boolean().or(z.null()).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.wrap!), wrap: z.boolean().or(z.null()).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.wrap!),
}) })
.default({}), .default({}),

View file

@ -81,6 +81,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
const renderResult = await processor const renderResult = await processor
.render(raw.content, { .render(raw.content, {
// @ts-expect-error passing internal prop
fileURL, fileURL,
frontmatter: raw.data, frontmatter: raw.data,
}) })

View file

@ -39,6 +39,7 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
shikiConfig: { shikiConfig: {
langs: [], langs: [],
theme: 'github-dark', theme: 'github-dark',
experimentalThemes: {},
wrap: false, wrap: false,
}, },
remarkPlugins: [], remarkPlugins: [],

View file

@ -30,9 +30,15 @@ const highlighterCacheAsync = new Map<string, Promise<Highlighter>>();
export function remarkShiki({ export function remarkShiki({
langs = [], langs = [],
theme = 'github-dark', theme = 'github-dark',
experimentalThemes = {},
wrap = false, wrap = false,
}: ShikiConfig = {}): ReturnType<RemarkPlugin> { }: ShikiConfig = {}): ReturnType<RemarkPlugin> {
const themes = experimentalThemes;
const cacheId = const cacheId =
Object.values(themes)
.map((t) => (typeof t === 'string' ? t : t.name ?? ''))
.join(',') +
(typeof theme === 'string' ? theme : theme.name ?? '') + (typeof theme === 'string' ? theme : theme.name ?? '') +
langs.map((l) => l.name ?? (l as any).id).join(','); langs.map((l) => l.name ?? (l as any).id).join(',');
@ -40,7 +46,7 @@ export function remarkShiki({
if (!highlighterAsync) { if (!highlighterAsync) {
highlighterAsync = getHighlighter({ highlighterAsync = getHighlighter({
langs: langs.length ? langs : Object.keys(bundledLanguages), langs: langs.length ? langs : Object.keys(bundledLanguages),
themes: [theme], themes: Object.values(themes).length ? Object.values(themes) : [theme],
}); });
highlighterCacheAsync.set(cacheId, highlighterAsync); highlighterCacheAsync.set(cacheId, highlighterAsync);
} }
@ -64,7 +70,8 @@ export function remarkShiki({
lang = 'plaintext'; lang = 'plaintext';
} }
let html = highlighter.codeToHtml(node.value, { lang, theme }); let themeOptions = Object.values(themes).length ? { themes } : { theme };
let html = highlighter.codeToHtml(node.value, { ...themeOptions, lang });
// Q: Couldn't these regexes match on a user's inputted code blocks? // Q: Couldn't these regexes match on a user's inputted code blocks?
// A: Nope! All rendered HTML is properly escaped. // A: Nope! All rendered HTML is properly escaped.

View file

@ -42,6 +42,7 @@ export type RemarkRehype = Omit<RemarkRehypeOptions, 'handlers' | 'unknownHandle
export interface ShikiConfig { export interface ShikiConfig {
langs?: LanguageRegistration[]; langs?: LanguageRegistration[];
theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw; theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw;
experimentalThemes?: Record<string, BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw>;
wrap?: boolean | null; wrap?: boolean | null;
} }

View file

@ -1,12 +1,30 @@
import { createMarkdownProcessor } from '../dist/index.js'; import { createMarkdownProcessor } from '../dist/index.js';
import chai from 'chai'; import chai from 'chai';
describe('shiki syntax highlighting', async () => { describe('shiki syntax highlighting', () => {
const processor = await createMarkdownProcessor();
it('does not add is:raw to the output', async () => { it('does not add is:raw to the output', async () => {
const processor = await createMarkdownProcessor();
const { code } = await processor.render('```\ntest\n```'); const { code } = await processor.render('```\ntest\n```');
chai.expect(code).not.to.contain('is:raw'); chai.expect(code).not.to.contain('is:raw');
}); });
it('supports light/dark themes', async () => {
const processor = await createMarkdownProcessor({
shikiConfig: {
experimentalThemes: {
light: 'github-light',
dark: 'github-dark',
},
},
});
const { code } = await processor.render('```\ntest\n```');
// light theme is there:
chai.expect(code).to.contain('background-color:');
chai.expect(code).to.contain('github-light');
// dark theme is there:
chai.expect(code).to.contain('--shiki-dark-bg:');
chai.expect(code).to.contain('github-dark');
});
}); });