diff --git a/.changeset/tender-suits-glow.md b/.changeset/tender-suits-glow.md new file mode 100644 index 000000000000..a01662ce6383 --- /dev/null +++ b/.changeset/tender-suits-glow.md @@ -0,0 +1,6 @@ +--- +'@astrojs/markdown-remark': minor +'astro': minor +--- + +Adds experimental support for multiple shiki themes with the new `markdown.shikiConfig.experimentalThemes` option. diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index 0a4fff6b98d9..b1d21fd9ea3d 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -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; /** * 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') { diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 5a5964a12d9f..ea656b9bfc2e 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -292,7 +292,14 @@ export const AstroConfigSchema = z.object({ theme: z .enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]]) .or(z.custom()) - .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()) + ) + .default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.experimentalThemes!), wrap: z.boolean().or(z.null()).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.wrap!), }) .default({}), diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 3c8a1af46e1e..fdfe3db3760f 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -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, }) diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 89c9ca8bdb65..61f97072bf34 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -39,6 +39,7 @@ export const markdownConfigDefaults: Omit, 'draft shikiConfig: { langs: [], theme: 'github-dark', + experimentalThemes: {}, wrap: false, }, remarkPlugins: [], diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts index bf3dd0b7895b..4eaae5ff2529 100644 --- a/packages/markdown/remark/src/remark-shiki.ts +++ b/packages/markdown/remark/src/remark-shiki.ts @@ -30,9 +30,15 @@ const highlighterCacheAsync = new Map>(); export function remarkShiki({ langs = [], theme = 'github-dark', + experimentalThemes = {}, wrap = false, }: ShikiConfig = {}): ReturnType { + 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. diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index 4abcf578d95d..7038e2425353 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -42,6 +42,7 @@ export type RemarkRehype = Omit; wrap?: boolean | null; } diff --git a/packages/markdown/remark/test/shiki.js b/packages/markdown/remark/test/shiki.js index cc5c6b7718f6..c7ace6187aee 100644 --- a/packages/markdown/remark/test/shiki.js +++ b/packages/markdown/remark/test/shiki.js @@ -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'); + }); });