mirror of
https://github.com/withastro/astro.git
synced 2025-01-13 22:11:20 -05:00
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 <bjornlu.dev@gmail.com> * fix build * Fix bug * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/dirty-socks-sip.md --------- Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
3ac2263ff6
commit
710a1a11f4
6 changed files with 78 additions and 4 deletions
34
.changeset/dirty-socks-sip.md
Normal file
34
.changeset/dirty-socks-sip.md
Normal file
|
@ -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';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
|
@ -313,6 +313,10 @@ export const AstroConfigSchema = z.object({
|
||||||
return langs;
|
return langs;
|
||||||
})
|
})
|
||||||
.default([]),
|
.default([]),
|
||||||
|
langAlias: z
|
||||||
|
.record(z.string(), z.string())
|
||||||
|
.optional()
|
||||||
|
.default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.langAlias!),
|
||||||
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>())
|
||||||
|
|
|
@ -37,6 +37,7 @@ export const markdownConfigDefaults: Required<AstroMarkdownOptions> = {
|
||||||
themes: {},
|
themes: {},
|
||||||
wrap: false,
|
wrap: false,
|
||||||
transformers: [],
|
transformers: [],
|
||||||
|
langAlias: {},
|
||||||
},
|
},
|
||||||
remarkPlugins: [],
|
remarkPlugins: [],
|
||||||
rehypePlugins: [],
|
rehypePlugins: [],
|
||||||
|
|
|
@ -45,24 +45,29 @@ export async function createShikiHighlighter({
|
||||||
defaultColor,
|
defaultColor,
|
||||||
wrap = false,
|
wrap = false,
|
||||||
transformers = [],
|
transformers = [],
|
||||||
|
langAlias = {},
|
||||||
}: ShikiConfig = {}): Promise<ShikiHighlighter> {
|
}: ShikiConfig = {}): Promise<ShikiHighlighter> {
|
||||||
theme = theme === 'css-variables' ? cssVariablesTheme() : theme;
|
theme = theme === 'css-variables' ? cssVariablesTheme() : theme;
|
||||||
|
|
||||||
const highlighter = await getHighlighter({
|
const highlighter = await getHighlighter({
|
||||||
langs: ['plaintext', ...langs],
|
langs: ['plaintext', ...langs],
|
||||||
|
langAlias,
|
||||||
themes: Object.values(themes).length ? Object.values(themes) : [theme],
|
themes: Object.values(themes).length ? Object.values(themes) : [theme],
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async highlight(code, lang = 'plaintext', options) {
|
async highlight(code, lang = 'plaintext', options) {
|
||||||
|
const resolvedLang = langAlias[lang] ?? lang;
|
||||||
const loadedLanguages = highlighter.getLoadedLanguages();
|
const loadedLanguages = highlighter.getLoadedLanguages();
|
||||||
|
|
||||||
if (!isSpecialLang(lang) && !loadedLanguages.includes(lang)) {
|
if (!isSpecialLang(lang) && !loadedLanguages.includes(resolvedLang)) {
|
||||||
try {
|
try {
|
||||||
await highlighter.loadLanguage(lang as BundledLanguage);
|
await highlighter.loadLanguage(resolvedLang as BundledLanguage);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
|
const langStr =
|
||||||
|
lang === resolvedLang ? `"${lang}"` : `"${lang}" (aliased to "${resolvedLang}")`;
|
||||||
console.warn(
|
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';
|
lang = 'plaintext';
|
||||||
}
|
}
|
||||||
|
@ -120,7 +125,7 @@ export async function createShikiHighlighter({
|
||||||
// Add "user-select: none;" for "+"/"-" diff symbols.
|
// Add "user-select: none;" for "+"/"-" diff symbols.
|
||||||
// Transform `<span class="line"><span style="...">+ something</span></span>
|
// Transform `<span class="line"><span style="...">+ something</span></span>
|
||||||
// into `<span class="line"><span style="..."><span style="user-select: none;">+</span> something</span></span>`
|
// into `<span class="line"><span style="..."><span style="user-select: none;">+</span> something</span></span>`
|
||||||
if (lang === 'diff') {
|
if (resolvedLang === 'diff') {
|
||||||
const innerSpanNode = node.children[0];
|
const innerSpanNode = node.children[0];
|
||||||
const innerSpanTextNode =
|
const innerSpanTextNode =
|
||||||
innerSpanNode?.type === 'element' && innerSpanNode.children?.[0];
|
innerSpanNode?.type === 'element' && innerSpanNode.children?.[0];
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type * as mdast from 'mdast';
|
||||||
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
||||||
import type {
|
import type {
|
||||||
BuiltinTheme,
|
BuiltinTheme,
|
||||||
|
HighlighterCoreOptions,
|
||||||
LanguageRegistration,
|
LanguageRegistration,
|
||||||
ShikiTransformer,
|
ShikiTransformer,
|
||||||
ThemeRegistration,
|
ThemeRegistration,
|
||||||
|
@ -37,6 +38,7 @@ export type ThemePresets = BuiltinTheme | 'css-variables';
|
||||||
|
|
||||||
export interface ShikiConfig {
|
export interface ShikiConfig {
|
||||||
langs?: LanguageRegistration[];
|
langs?: LanguageRegistration[];
|
||||||
|
langAlias?: HighlighterCoreOptions['langAlias'];
|
||||||
theme?: ThemePresets | ThemeRegistration | ThemeRegistrationRaw;
|
theme?: ThemePresets | ThemeRegistration | ThemeRegistrationRaw;
|
||||||
themes?: Record<string, ThemePresets | ThemeRegistration | ThemeRegistrationRaw>;
|
themes?: Record<string, ThemePresets | ThemeRegistration | ThemeRegistrationRaw>;
|
||||||
defaultColor?: 'light' | 'dark' | string | false;
|
defaultColor?: 'light' | 'dark' | string | false;
|
||||||
|
|
|
@ -101,4 +101,32 @@ describe('shiki syntax highlighting', () => {
|
||||||
// Doesn't have `color` or `background-color` properties.
|
// Doesn't have `color` or `background-color` properties.
|
||||||
assert.doesNotMatch(code, /color:/);
|
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"/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue