diff --git a/.changeset/dirty-socks-sip.md b/.changeset/dirty-socks-sip.md new file mode 100644 index 000000000000..1784e2031ec0 --- /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 da70ead1a6a3..27f703a10e9c 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 5849f5799a3b..a9ae7ed59752 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 28f51c5e8554..2f06ef9a9e9e 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 aa7b62c9a780..d95676b55b63 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 c3cb813702db..ca17ab1d8e27 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"/); + }); });