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:
parent
1ecc9aa324
commit
c5010aad34
8 changed files with 62 additions and 8 deletions
6
.changeset/tender-suits-glow.md
Normal file
6
.changeset/tender-suits-glow.md
Normal 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.
|
|
@ -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') {
|
||||||
|
|
|
@ -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({}),
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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: [],
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue