Skip to content

Commit

Permalink
Light/dark theming for shikiji's codeblocks (#8903)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
horo-fox and sarah11918 committed Nov 8, 2023
1 parent 1ecc9aa commit c5010aa
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-suits-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/markdown-remark': minor
'astro': minor
---

Adds experimental support for multiple shiki themes with the new `markdown.shikiConfig.experimentalThemes` option.
17 changes: 15 additions & 2 deletions packages/astro/components/Code.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -53,6 +58,7 @@ const {
code,
lang = 'plaintext',
theme = 'github-dark',
experimentalThemes = {},
wrap = false,
inline = false,
} = Astro.props;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
9 changes: 8 additions & 1 deletion packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({}),
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
1 change: 1 addition & 0 deletions packages/markdown/remark/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
shikiConfig: {
langs: [],
theme: 'github-dark',
experimentalThemes: {},
wrap: false,
},
remarkPlugins: [],
Expand Down
11 changes: 9 additions & 2 deletions packages/markdown/remark/src/remark-shiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,23 @@ 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(',');

let highlighterAsync = highlighterCacheAsync.get(cacheId);
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);
}
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/markdown/remark/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
24 changes: 21 additions & 3 deletions packages/markdown/remark/test/shiki.js
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit c5010aa

Please sign in to comment.