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"
*/
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.
* - true: enabled.
@ -53,6 +58,7 @@ const {
code,
lang = 'plaintext',
theme = 'github-dark',
experimentalThemes = {},
wrap = false,
inline = false,
} = Astro.props;
@ -88,12 +94,15 @@ if (typeof lang === 'object') {
const highlighter = await getCachedHighlighter({
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, {
lang: typeof lang === 'string' ? lang : lang.name,
theme,
...themeOptions,
transforms: {
pre(node) {
// Swap to `code` tag if inline
@ -123,6 +132,10 @@ const html = highlighter.codeToHtml(code, {
}
},
root(node) {
if (Object.values(experimentalThemes).length) {
return;
}
// theme.id for shiki -> shikiji compat
const themeName = typeof theme === 'string' ? theme : theme.name;
if (themeName === 'css-variables') {

View file

@ -292,7 +292,14 @@ export const AstroConfigSchema = z.object({
theme: z
.enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]])
.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!),
})
.default({}),

View file

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

View file

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

View file

@ -30,9 +30,15 @@ const highlighterCacheAsync = new Map<string, Promise<Highlighter>>();
export function remarkShiki({
langs = [],
theme = 'github-dark',
experimentalThemes = {},
wrap = false,
}: ShikiConfig = {}): ReturnType<RemarkPlugin> {
const themes = experimentalThemes;
const cacheId =
Object.values(themes)
.map((t) => (typeof t === 'string' ? t : t.name ?? ''))
.join(',') +
(typeof theme === 'string' ? theme : theme.name ?? '') +
langs.map((l) => l.name ?? (l as any).id).join(',');
@ -40,7 +46,7 @@ export function remarkShiki({
if (!highlighterAsync) {
highlighterAsync = getHighlighter({
langs: langs.length ? langs : Object.keys(bundledLanguages),
themes: [theme],
themes: Object.values(themes).length ? Object.values(themes) : [theme],
});
highlighterCacheAsync.set(cacheId, highlighterAsync);
}
@ -64,7 +70,8 @@ export function remarkShiki({
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?
// A: Nope! All rendered HTML is properly escaped.

View file

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

View file

@ -1,12 +1,30 @@
import { createMarkdownProcessor } from '../dist/index.js';
import chai from 'chai';
describe('shiki syntax highlighting', async () => {
const processor = await createMarkdownProcessor();
describe('shiki syntax highlighting', () => {
it('does not add is:raw to the output', async () => {
const processor = await createMarkdownProcessor();
const { code } = await processor.render('```\ntest\n```');
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');
});
});