pull/5409/merge
Divyansh Singh 2 weeks ago committed by GitHub
commit 2f5831e3df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,17 +1,15 @@
import { highlight } from 'node/markdown/plugins/highlight' import { highlight } from 'node/markdown/plugins/highlight'
describe('node/markdown/plugins/highlight', () => { describe('node/markdown/plugins/highlight', () => {
test('passes color replacements through markdown options', async () => { test('passes color replacements through shiki options', async () => {
const [render, dispose] = await highlight( const [render, dispose] = await highlight({
{ light: 'github-light', dark: 'github-dark' }, theme: { light: 'github-light', dark: 'github-dark' },
{
colorReplacements: { colorReplacements: {
'github-light': { 'github-light': {
'#005cc5': '#000000' '#005cc5': '#000000'
} }
} }
} })
)
try { try {
const html = await render('const a = 1', 'js', '') const html = await render('const a = 1', 'js', '')
@ -23,4 +21,35 @@ describe('node/markdown/plugins/highlight', () => {
dispose() dispose()
} }
}) })
test('renders with default dual themes when no options are passed', async () => {
const [render, dispose] = await highlight()
try {
const html = await render('const a = 1', 'js', '')
expect(html).toContain('--shiki-light:')
expect(html).toContain('--shiki-dark:')
} finally {
dispose()
}
})
test('loads defaultLang for blocks with an unknown language', async () => {
const warnings: string[] = []
const [render, dispose] = await highlight(
{ defaultLang: 'python' },
{ warn: (msg) => void warnings.push(String(msg)) }
)
try {
const html = await render('print("hi")', 'notalang', '')
expect(html).toContain('class="shiki')
expect(warnings).toHaveLength(1)
expect(warnings[0]).toContain("falling back to 'python'")
} finally {
dispose()
}
})
}) })

@ -46,7 +46,8 @@ export default defineConfig({
markdown: { markdown: {
math: true, math: true,
codeTransformers: [ shiki: {
transformers: [
// We use `[!!code` and `@@include` in demo to prevent transformation, // We use `[!!code` and `@@include` in demo to prevent transformation,
// here we revert it back. // here we revert it back.
{ {
@ -56,7 +57,8 @@ export default defineConfig({
.replaceAll('@@include', '@include') .replaceAll('@@include', '@include')
} }
} }
], ]
},
config(md) { config(md) {
md.use(groupIconMdPlugin) md.use(groupIconMdPlugin)
} }

@ -489,9 +489,9 @@ export default {
</ul> </ul>
``` ```
A [list of valid languages](https://shiki.style/languages) is available on Shiki's repository. A [list of valid languages](https://shiki.style/languages) is available in Shiki's documentation.
You may also customize syntax highlight theme, configure language aliases, and set custom language labels in app config. Please see [`markdown` options](../reference/site-config#markdown) for more details. Highlighting is configured through the `markdown.shiki` option in app config: pass a custom [theme](https://shiki.style/themes), preload or register [languages](https://shiki.style/guide/load-lang), define [language aliases](https://shiki.style/guide/load-lang#custom-language-aliases), or apply [transformers](https://shiki.style/guide/transformers). Options that map directly to Shiki (`langs`, `langAlias`, `transformers`, `colorReplacements`) use Shiki's own names and types. Custom language labels shown in code blocks are configured separately with `markdown.languageLabel`. See [`markdown` options](../reference/site-config#markdown) for more details.
## Line Highlighting in Code Blocks ## Line Highlighting in Code Blocks

@ -603,6 +603,19 @@ export default {
Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available. Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available.
Syntax highlighting options are grouped under `markdown.shiki`. The ones that map directly to Shiki (`langs`, `langAlias`, `transformers`, `colorReplacements`) use [Shiki](https://shiki.style)'s own option names and types — check the [`ShikiOptions` jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/plugins/highlight.ts) for the full list:
```js
export default {
markdown: {
shiki: {
theme: { light: 'github-light', dark: 'github-dark' },
langAlias: { cjs: 'javascript' }
}
}
}
```
Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers) options to collect headings into [`useData().page.headers`](./runtime-api#usedata). This option is disabled by default. Set `markdown.headers` to `true` or pass [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers) options to collect headings into [`useData().page.headers`](./runtime-api#usedata). This option is disabled by default.
### vite ### vite

@ -29,15 +29,6 @@ import {
} from '@mdit/plugin-tasklist' } from '@mdit/plugin-tasklist'
import { MarkdownItAsync, type MarkdownItAsyncOptions } from 'markdown-it-async' import { MarkdownItAsync, type MarkdownItAsyncOptions } from 'markdown-it-async'
import mditCjkFriendly from 'markdown-it-cjk-friendly' import mditCjkFriendly from 'markdown-it-cjk-friendly'
import type {
BuiltinLanguage,
BuiltinTheme,
CodeToHastOptions,
Highlighter,
LanguageInput,
ShikiTransformer,
ThemeRegistrationAny
} from 'shiki'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import type { import type {
@ -51,7 +42,10 @@ import {
gitHubAlertsPlugin, gitHubAlertsPlugin,
type ContainerOptions type ContainerOptions
} from './plugins/containers' } from './plugins/containers'
import { highlight as createHighlighter } from './plugins/highlight' import {
highlight as createHighlighter,
type ShikiOptions
} from './plugins/highlight'
import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { import {
includePlugin, includePlugin,
@ -68,18 +62,11 @@ import {
import { tablePlugin } from './plugins/table' import { tablePlugin } from './plugins/table'
export type { Header } from '../shared' export type { Header } from '../shared'
export type { ShikiOptions, ThemeOptions } from './plugins/highlight'
// not exported from @mdit/plugin-emoji, so derive it from the plugin signature // not exported from @mdit/plugin-emoji, so derive it from the plugin signature
type EmojiPluginOptions = NonNullable<Parameters<typeof emojiPlugin>[1]> type EmojiPluginOptions = NonNullable<Parameters<typeof emojiPlugin>[1]>
export type ThemeOptions =
| ThemeRegistrationAny
| BuiltinTheme
| {
light: ThemeRegistrationAny | BuiltinTheme
dark: ThemeRegistrationAny | BuiltinTheme
}
// highlight is marked as any to avoid type conflicts with plugins expecting // highlight is marked as any to avoid type conflicts with plugins expecting
// regular markdown-it which has sync highlight function. Such plugins will fail // regular markdown-it which has sync highlight function. Such plugins will fail
// if they access highlight directly but currently none of the ones we use do that. // if they access highlight directly but currently none of the ones we use do that.
@ -118,69 +105,11 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/* ==================== Syntax Highlighting ==================== */ /* ==================== Syntax Highlighting ==================== */
/** /**
* Custom theme for syntax highlighting. * Options for syntax highlighting with Shiki: theme, languages, aliases,
* * transformers, color replacements, and the setup hook.
* You can also pass an object with `light` and `dark` themes to support * @see https://shiki.style
* dual themes.
*
* @example { theme: 'github-dark' }
* @example { theme: { light: 'github-light', dark: 'github-dark' } }
*
* You can use an existing theme.
* @see https://shiki.style/themes
* Or add your own theme.
* @see https://shiki.style/guide/load-theme
*/
theme?: ThemeOptions
/**
* Custom languages for syntax highlighting or pre-load built-in languages.
* @see https://shiki.style/languages
*/
languages?: (LanguageInput | BuiltinLanguage)[]
/**
* Custom language aliases for syntax highlighting.
* Maps custom language names to existing languages.
* Alias lookup is case-insensitive and underscores in language names are
* displayed as spaces.
*
* @example
*
* Maps `my_lang` to use Python syntax highlighting.
* ```js
* { 'my_lang': 'python' }
* ```
*
* Usage in markdown:
* ````md
* ```My_Lang
* # This will be highlighted as Python code
* # and will show "My Lang" as the language label
* print("Hello, World!")
* ```
* ````
*
* @see https://shiki.style/guide/load-lang#custom-language-aliases
*/
languageAlias?: Record<string, string>
/**
* Fallback language used when the specified language is not available.
*/
defaultHighlightLang?: string
/**
* Transformers applied to code blocks.
* @see https://shiki.style/guide/transformers
*/
codeTransformers?: ShikiTransformer[]
/**
* Color replacements applied during syntax highlighting.
* Accepts either a flat color map or per-theme replacements.
* @see https://shiki.style/guide/theme-colors#color-replacements
*/
colorReplacements?: CodeToHastOptions['colorReplacements']
/**
* Configure the Shiki instance.
*/ */
shikiSetup?: (shiki: Highlighter) => void | Promise<void> shiki?: ShikiOptions | undefined
/* ==================== Code Blocks ==================== */ /* ==================== Code Blocks ==================== */
@ -373,7 +302,6 @@ export async function createMarkdownRenderer(
publicDir ??= path.resolve(srcDir, 'public') publicDir ??= path.resolve(srcDir, 'public')
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
const codeCopyButton = { const codeCopyButton = {
tooltipText: options.codeCopyButton?.tooltipText || 'Copy code', tooltipText: options.codeCopyButton?.tooltipText || 'Copy code',
copiedText: options.codeCopyButton?.copiedText || 'Copied' copiedText: options.codeCopyButton?.copiedText || 'Copied'
@ -381,7 +309,7 @@ export async function createMarkdownRenderer(
const [highlight, dispose] = options.highlight const [highlight, dispose] = options.highlight
? [options.highlight, () => {}] ? [options.highlight, () => {}]
: await createHighlighter(theme, options, logger) : await createHighlighter(options.shiki, logger)
_disposeHighlighter = dispose _disposeHighlighter = dispose

@ -7,12 +7,114 @@ import {
} from '@shikijs/transformers' } from '@shikijs/transformers'
import { customAlphabet } from 'nanoid' import { customAlphabet } from 'nanoid'
import c from 'picocolors' import c from 'picocolors'
import type { BundledLanguage, ShikiTransformer } from 'shiki' import type {
BuiltinLanguage,
BuiltinTheme,
BundledHighlighterOptions,
BundledLanguage,
CodeToHastOptions,
Highlighter,
ShikiTransformer,
SpecialLanguage,
StringLiteralUnion,
ThemeRegistrationAny
} from 'shiki'
import { createHighlighter, guessEmbeddedLanguages, isSpecialLang } from 'shiki' import { createHighlighter, guessEmbeddedLanguages, isSpecialLang } from 'shiki'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import { isShell } from '../../shared' import { isShell, type Awaitable } from '../../shared'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
export type ThemeOptions =
| ThemeRegistrationAny
| BuiltinTheme
| {
light: ThemeRegistrationAny | BuiltinTheme
dark: ThemeRegistrationAny | BuiltinTheme
}
type BundledShikiOptions = BundledHighlighterOptions<
BuiltinLanguage,
BuiltinTheme
>
/**
* Shiki options for syntax highlighting in code blocks. Members that map
* directly to Shiki (`langs`, `langAlias`, `transformers`,
* `colorReplacements`) use Shiki's own option names and types; `theme`,
* `defaultLang`, and `setup` are VitePress-specific.
* @see https://shiki.style
*/
export interface ShikiOptions {
/**
* Custom theme for syntax highlighting.
*
* You can also pass an object with `light` and `dark` themes to support
* dual themes.
*
* @example { theme: 'github-dark' }
* @example { theme: { light: 'github-light', dark: 'github-dark' } }
*
* You can use an existing theme.
* @see https://shiki.style/themes
* Or add your own theme.
* @see https://shiki.style/guide/load-theme
*
* @default { light: 'github-light', dark: 'github-dark' }
*/
theme?: ThemeOptions
/**
* Custom languages for syntax highlighting or pre-load built-in languages.
* @see https://shiki.style/languages
* @see https://shiki.style/guide/load-lang
*/
langs?: BundledShikiOptions['langs']
/**
* Custom language aliases for syntax highlighting.
* Maps custom language names to existing languages.
* Alias lookup is case-insensitive and underscores in language names are
* displayed as spaces.
*
* @example
*
* Maps `my_lang` to use Python syntax highlighting.
* ```js
* { 'my_lang': 'python' }
* ```
*
* Usage in markdown:
* ````md
* ```My_Lang
* # This will be highlighted as Python code
* # and will show "My Lang" as the language label
* print("Hello, World!")
* ```
* ````
*
* @see https://shiki.style/guide/load-lang#custom-language-aliases
*/
langAlias?: BundledShikiOptions['langAlias']
/**
* Language used for code blocks that don't specify a language, or specify
* a language that is not available.
* @default 'txt'
*/
defaultLang?: StringLiteralUnion<BuiltinLanguage | SpecialLanguage>
/**
* Transformers applied to code blocks.
* @see https://shiki.style/guide/transformers
*/
transformers?: ShikiTransformer[]
/**
* Color replacements applied during syntax highlighting.
* Accepts either a flat color map or per-theme replacements.
* @see https://shiki.style/guide/theme-colors#color-replacements
*/
colorReplacements?: CodeToHastOptions['colorReplacements']
/**
* Configure the Shiki highlighter instance after it is created.
*/
setup?: (shiki: Highlighter) => Awaitable<void>
}
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -51,19 +153,19 @@ function transformerDisableShellSymbolSelect(): ShikiTransformer {
} }
export async function highlight( export async function highlight(
theme: ThemeOptions, options: ShikiOptions = {},
options: MarkdownOptions,
logger: Pick<Logger, 'warn'> = console logger: Pick<Logger, 'warn'> = console
): Promise< ): Promise<
[(str: string, lang: string, attrs: string) => Promise<string>, () => void] [(str: string, lang: string, attrs: string) => Promise<string>, () => void]
> { > {
const { // `??` instead of destructuring defaults so that plain-JS configs passing
defaultHighlightLang: defaultLang = 'txt', // `null` still get the defaults
codeTransformers: userTransformers = [] const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
} = options const defaultLang = options.defaultLang ?? 'txt'
const userTransformers = options.transformers ?? []
const langAlias = Object.fromEntries( const langAlias = Object.fromEntries(
Object.entries(options.languageAlias || {}) // Object.entries(options.langAlias || {}) //
.map(([k, v]) => [k.toLowerCase(), v]) .map(([k, v]) => [k.toLowerCase(), v])
) )
@ -72,11 +174,26 @@ export async function highlight(
typeof theme === 'object' && 'light' in theme && 'dark' in theme typeof theme === 'object' && 'light' in theme && 'dark' in theme
? [theme.light, theme.dark] ? [theme.light, theme.dark]
: [theme], : [theme],
langs: [...(options.languages || []), ...Object.values(langAlias)], langs: [...(options.langs || []), ...Object.values(langAlias)],
langAlias langAlias
}) })
await options?.shikiSetup?.(highlighter) await options.setup?.(highlighter)
// https://github.com/shikijs/shiki/issues/952
const loadLanguage = async (lang: string) => {
try {
if (
!isSpecialLang(lang) &&
!highlighter.getLoadedLanguages().includes(lang)
) {
await highlighter.loadLanguage(lang as any)
}
return true
} catch {
return false
}
}
const transformers: ShikiTransformer[] = [ const transformers: ShikiTransformer[] = [
transformerMetaHighlight(), transformerMetaHighlight(),
@ -115,21 +232,21 @@ export async function highlight(
const vPre = !vueRE.test(lang) const vPre = !vueRE.test(lang)
if (!vPre) lang = lang.slice(0, -4) if (!vPre) lang = lang.slice(0, -4)
try { if (!(await loadLanguage(lang))) {
// https://github.com/shikijs/shiki/issues/952
if (
!isSpecialLang(lang) &&
!highlighter.getLoadedLanguages().includes(lang)
) {
await highlighter.loadLanguage(lang as any)
}
} catch {
logger.warn( logger.warn(
c.yellow( c.yellow(
`\nThe language '${lang}' is not loaded, falling back to '${defaultLang}' for syntax highlighting.` `\nThe language '${lang}' is not loaded, falling back to '${defaultLang}' for syntax highlighting.`
) )
) )
lang = defaultLang lang = defaultLang
if (!(await loadLanguage(lang))) {
logger.warn(
c.yellow(
`\nThe default language '${lang}' is not loaded, falling back to 'txt' for syntax highlighting.`
)
)
lang = 'txt'
}
} }
const mustaches = new Map<string, string>() const mustaches = new Map<string, string>()

Loading…
Cancel
Save