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!
+
+
+ "
+ `)
+ })
+
+ 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(`
+ "
+
+ "
+ `)
+ })
+
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(