From 962f00e7a3c22dd021729e732e12a758b42aef1c Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:22:30 +0530 Subject: [PATCH] feat(markdown): support registering custom containers New containers can be registered via `markdown.container.customContainers`, mapping a container name to its default title. Registered names work as `::: name` blocks and as GitHub-style alerts (`> [!NAME]`), with custom titles, attributes, and no-title behaving like the built-in types. Styling is left to the theme via `.custom-block.name`. close #3591 close #3603 close #4228 Co-Authored-By: Claude Fable 5 --- .../node/markdown/plugins/containers.test.ts | 49 +++++++++++++++++++ docs/en/guide/markdown.md | 43 +++++++++++++++- src/node/markdown/markdown.ts | 5 +- src/node/markdown/plugins/containers.ts | 22 ++++++++- 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/__tests__/unit/node/markdown/plugins/containers.test.ts b/__tests__/unit/node/markdown/plugins/containers.test.ts index e4b19cee..d49d7c11 100644 --- a/__tests__/unit/node/markdown/plugins/containers.test.ts +++ b/__tests__/unit/node/markdown/plugins/containers.test.ts @@ -126,6 +126,39 @@ describe('node/markdown/plugins/containers', () => { `) }) + test('registers custom containers from container options', async () => { + const src = [ + '::: success', + 'You have completed the walkthrough!', + ':::', + '', + '::: success Well done {no-title}', + 'content', + ':::' + ].join('\n') + expect( + await render(src, { + container: { customContainers: { success: 'SUCCESS' } } + }) + ).toMatchInlineSnapshot(` + "

SUCCESS

+

You have completed the walkthrough!

+
+
+

content

+
+ " + `) + }) + + test('rejects invalid custom container names', async () => { + for (const name of ['raw', 'v-pre', 'code-group', 'Bad Name', 'UPPER']) { + await expect( + render('text', { container: { customContainers: { [name]: 'X' } } }) + ).rejects.toThrow('Invalid custom container name') + } + }) + test('supports attrs on the fence line', async () => { const src = [ '::: details Click me {open}', @@ -362,6 +395,22 @@ describe('node/markdown/plugins/containers (github alerts)', () => { `) }) + test('renders custom containers as alerts', async () => { + expect( + await render('> [!SUCCESS]\n> done\n\n> [!success] With title\n> done', { + container: { customContainers: { success: 'SUCCESS' } } + }) + ).toMatchInlineSnapshot(` + "

SUCCESS

+

done

+
+

With title

+

done

+
+ " + `) + }) + test('supports block content and lazy continuation', async () => { const src = [ '> [!NOTE]', diff --git a/docs/en/guide/markdown.md b/docs/en/guide/markdown.md index 50999dc1..bf5f278b 100644 --- a/docs/en/guide/markdown.md +++ b/docs/en/guide/markdown.md @@ -243,6 +243,47 @@ export default defineConfig({ }) ``` +### Registering New Containers + +Beyond the built-in types, you can register additional containers by mapping their names to their default titles: + +```ts +// config.ts +export default defineConfig({ + // ... + markdown: { + container: { + customContainers: { + success: 'SUCCESS' + } + } + } + // ... +}) +``` + +Registered names work like the built-in ones - including custom titles, attributes, and the [GitHub-style alert syntax](#github-flavored-alerts): + +```md +::: success +You have completed the walkthrough! +::: + +> [!SUCCESS] Custom title +> This renders the same way. +``` + +New containers ship without any styling, so add some in your theme using the container name as the class. For this example, the default theme's palette already provides fitting colors: + +```css +/* .vitepress/theme/custom.css */ +.custom-block.success { + border-color: transparent; + color: var(--vp-c-text-1); + background-color: var(--vp-c-success-soft); +} +``` + ### Nesting The `:::` markers follow the same rules as fenced code blocks (` ``` `): a fence is only closed by a matching fence that is **at least as long** as the one that opened it. To nest containers (or to mix them with [code groups](#code-groups)) make the outer fence longer than the ones inside it. @@ -351,7 +392,7 @@ Wraps in a `
` ## GitHub-flavored Alerts -VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers). +VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers). Unlike on GitHub, text placed right after the marker becomes the title of the alert (`> [!NOTE] Custom Title`), and [containers you registered yourself](#registering-new-containers) work here too. ```md > [!NOTE] diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index 566d5f64..6fe1d6f1 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -251,8 +251,9 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { */ math?: boolean | any /** - * Custom labels for the built-in containers (`::: tip` etc.). Also used - * as the default titles of GitHub-flavored alerts. + * Custom labels for the built-in containers (`::: tip` etc.) and + * additional user-defined containers. Labels are also used as the + * default titles of GitHub-flavored alerts. * @see https://vitepress.dev/guide/markdown#custom-containers */ container?: ContainerOptions diff --git a/src/node/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts index 64b50384..a466e265 100644 --- a/src/node/markdown/plugins/containers.ts +++ b/src/node/markdown/plugins/containers.ts @@ -14,6 +14,14 @@ export interface ContainerOptions { detailsLabel?: string importantLabel?: string cautionLabel?: string + /** + * Additional containers to register, mapping the container name to its + * default title. Registered names work both as `::: name` blocks and as + * GitHub-style alerts (`> [!NAME]`), and are styleable in the theme via + * `.custom-block.name`. Names must be lowercase and may only contain + * letters, numbers, hyphens, and underscores. + */ + customContainers?: Record } export const containerPlugin = ( @@ -49,7 +57,7 @@ export const containerPlugin = ( } function resolveTitles(options?: ContainerOptions): Record { - return { + const titles: Record = { tip: options?.tipLabel || 'TIP', info: options?.infoLabel || 'INFO', warning: options?.warningLabel || 'WARNING', @@ -59,6 +67,18 @@ function resolveTitles(options?: ContainerOptions): Record { important: options?.importantLabel || 'IMPORTANT', caution: options?.cautionLabel || 'CAUTION' } + for (const [name, title] of Object.entries(options?.customContainers ?? {})) { + if ( + !/^[a-z0-9_-]+$/.test(name) || + ['v-pre', 'raw', 'code-group'].includes(name) + ) + throw new Error( + `Invalid custom container name: "${name}". Names must be lowercase ` + + `([a-z0-9_-]) and cannot be "v-pre", "raw", or "code-group".` + ) + titles[name] = title + } + return titles } function createOpenRender(