From 710a1a11f488ff6ed3da6d3e0723b2322ccfe27b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 9 Oct 2024 10:51:38 +0100 Subject: [PATCH] feat(markdown): add support for `shiki` option `langAlias` (#12039) * feat(shiki): add support for `langAlias` * chore: apply feedback * Update packages/markdown/remark/src/types.ts Co-authored-by: Bjorn Lu * fix build * Fix bug * Apply suggestions from code review Co-authored-by: Sarah Rainsberger * Update .changeset/dirty-socks-sip.md --------- Co-authored-by: Bjorn Lu Co-authored-by: Sarah Rainsberger --- .changeset/dirty-socks-sip.md | 34 +++++++++++++++++++++ packages/astro/src/core/config/schema.ts | 4 +++ packages/markdown/remark/src/index.ts | 1 + packages/markdown/remark/src/shiki.ts | 13 +++++--- packages/markdown/remark/src/types.ts | 2 ++ packages/markdown/remark/test/shiki.test.js | 28 +++++++++++++++++ 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 .changeset/dirty-socks-sip.md diff --git a/.changeset/dirty-socks-sip.md b/.changeset/dirty-socks-sip.md new file mode 100644 index 0000000000..1784e2031e --- /dev/null +++ b/.changeset/dirty-socks-sip.md @@ -0,0 +1,34 @@ +--- +'@astrojs/markdown-remark': minor +'astro': minor +--- + +Adds a `markdown.shikiConfig.langAlias` option that allows [aliasing a non-supported code language to a known language](https://shiki.style/guide/load-lang#custom-language-aliases). This is useful when the language of your code samples is not [a built-in Shiki language](https://shiki.style/languages), but you want your Markdown source to contain an accurate language while also displaying syntax highlighting. + +The following example configures Shiki to highlight `cjs` code blocks using the `javascript` syntax highlighter: + +```js +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + markdown: { + shikiConfig: { + langAlias: { + cjs: 'javascript', + }, + }, + }, +}); +``` + +Then in your Markdown, you can use the alias as the language for a code block for syntax highlighting: + +````md +```cjs +'use strict'; + +function commonJs() { + return 'I am a commonjs file'; +} +``` +```` diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index da70ead1a6..27f703a10e 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -313,6 +313,10 @@ export const AstroConfigSchema = z.object({ return langs; }) .default([]), + langAlias: z + .record(z.string(), z.string()) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.langAlias!), theme: z .enum(Object.keys(bundledThemes) as [BuiltinTheme, ...BuiltinTheme[]]) .or(z.custom()) diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 5849f5799a..a9ae7ed597 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -37,6 +37,7 @@ export const markdownConfigDefaults: Required = { themes: {}, wrap: false, transformers: [], + langAlias: {}, }, remarkPlugins: [], rehypePlugins: [], diff --git a/packages/markdown/remark/src/shiki.ts b/packages/markdown/remark/src/shiki.ts index 28f51c5e85..2f06ef9a9e 100644 --- a/packages/markdown/remark/src/shiki.ts +++ b/packages/markdown/remark/src/shiki.ts @@ -45,24 +45,29 @@ export async function createShikiHighlighter({ defaultColor, wrap = false, transformers = [], + langAlias = {}, }: ShikiConfig = {}): Promise { theme = theme === 'css-variables' ? cssVariablesTheme() : theme; const highlighter = await getHighlighter({ langs: ['plaintext', ...langs], + langAlias, themes: Object.values(themes).length ? Object.values(themes) : [theme], }); return { async highlight(code, lang = 'plaintext', options) { + const resolvedLang = langAlias[lang] ?? lang; const loadedLanguages = highlighter.getLoadedLanguages(); - if (!isSpecialLang(lang) && !loadedLanguages.includes(lang)) { + if (!isSpecialLang(lang) && !loadedLanguages.includes(resolvedLang)) { try { - await highlighter.loadLanguage(lang as BundledLanguage); + await highlighter.loadLanguage(resolvedLang as BundledLanguage); } catch (_err) { + const langStr = + lang === resolvedLang ? `"${lang}"` : `"${lang}" (aliased to "${resolvedLang}")`; console.warn( - `[Shiki] The language "${lang}" doesn't exist, falling back to "plaintext".`, + `[Shiki] The language ${langStr} doesn't exist, falling back to "plaintext".`, ); lang = 'plaintext'; } @@ -120,7 +125,7 @@ export async function createShikiHighlighter({ // Add "user-select: none;" for "+"/"-" diff symbols. // Transform `+ something // into `+ something` - if (lang === 'diff') { + if (resolvedLang === 'diff') { const innerSpanNode = node.children[0]; const innerSpanTextNode = innerSpanNode?.type === 'element' && innerSpanNode.children?.[0]; diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index aa7b62c9a7..d95676b55b 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -3,6 +3,7 @@ import type * as mdast from 'mdast'; import type { Options as RemarkRehypeOptions } from 'remark-rehype'; import type { BuiltinTheme, + HighlighterCoreOptions, LanguageRegistration, ShikiTransformer, ThemeRegistration, @@ -37,6 +38,7 @@ export type ThemePresets = BuiltinTheme | 'css-variables'; export interface ShikiConfig { langs?: LanguageRegistration[]; + langAlias?: HighlighterCoreOptions['langAlias']; theme?: ThemePresets | ThemeRegistration | ThemeRegistrationRaw; themes?: Record; defaultColor?: 'light' | 'dark' | string | false; diff --git a/packages/markdown/remark/test/shiki.test.js b/packages/markdown/remark/test/shiki.test.js index c3cb813702..ca17ab1d8e 100644 --- a/packages/markdown/remark/test/shiki.test.js +++ b/packages/markdown/remark/test/shiki.test.js @@ -101,4 +101,32 @@ describe('shiki syntax highlighting', () => { // Doesn't have `color` or `background-color` properties. assert.doesNotMatch(code, /color:/); }); + + it('the highlighter supports lang alias', async () => { + const highlighter = await createShikiHighlighter({ + langAlias: { + cjs: 'javascript', + }, + }); + + const html = await highlighter.highlight(`let test = "some string"`, 'cjs', { + attributes: { 'data-foo': 'bar', autofocus: true }, + }); + + assert.match(html, /data-language="cjs"/); + }); + + it('the markdown processsor support lang alias', async () => { + const processor = await createMarkdownProcessor({ + shikiConfig: { + langAlias: { + cjs: 'javascript', + }, + }, + }); + + const { code } = await processor.render('```cjs\nlet foo = "bar"\n```'); + + assert.match(code, /data-language="cjs"/); + }); });