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 <noreply@anthropic.com>
pull/5335/head
Divyansh Singh 2 months ago
parent 4c7a030bc1
commit 962f00e7a3

@ -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(`
"<div class="success custom-block"><p class="custom-block-title custom-block-title-default">SUCCESS</p>
<p>You have completed the walkthrough!</p>
</div>
<div class="success custom-block">
<p>content</p>
</div>
"
`)
})
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 () => { test('supports attrs on the fence line', async () => {
const src = [ const src = [
'::: details Click me {open}', '::: 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(`
"<div class="success custom-block github-alert"><p class="custom-block-title">SUCCESS</p>
<p>done</p>
</div>
<div class="success custom-block github-alert"><p class="custom-block-title">With title</p>
<p>done</p>
</div>
"
`)
})
test('supports block content and lazy continuation', async () => { test('supports block content and lazy continuation', async () => {
const src = [ const src = [
'> [!NOTE]', '> [!NOTE]',

@ -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 ### 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. 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 `<div class="vp-raw">`
## GitHub-flavored Alerts ## 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 ```md
> [!NOTE] > [!NOTE]

@ -251,8 +251,9 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
math?: boolean | any math?: boolean | any
/** /**
* Custom labels for the built-in containers (`::: tip` etc.). Also used * Custom labels for the built-in containers (`::: tip` etc.) and
* as the default titles of GitHub-flavored alerts. * additional user-defined containers. Labels are also used as the
* default titles of GitHub-flavored alerts.
* @see https://vitepress.dev/guide/markdown#custom-containers * @see https://vitepress.dev/guide/markdown#custom-containers
*/ */
container?: ContainerOptions container?: ContainerOptions

@ -14,6 +14,14 @@ export interface ContainerOptions {
detailsLabel?: string detailsLabel?: string
importantLabel?: string importantLabel?: string
cautionLabel?: 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<string, string>
} }
export const containerPlugin = ( export const containerPlugin = (
@ -49,7 +57,7 @@ export const containerPlugin = (
} }
function resolveTitles(options?: ContainerOptions): Record<string, string> { function resolveTitles(options?: ContainerOptions): Record<string, string> {
return { const titles: Record<string, string> = {
tip: options?.tipLabel || 'TIP', tip: options?.tipLabel || 'TIP',
info: options?.infoLabel || 'INFO', info: options?.infoLabel || 'INFO',
warning: options?.warningLabel || 'WARNING', warning: options?.warningLabel || 'WARNING',
@ -59,6 +67,18 @@ function resolveTitles(options?: ContainerOptions): Record<string, string> {
important: options?.importantLabel || 'IMPORTANT', important: options?.importantLabel || 'IMPORTANT',
caution: options?.cautionLabel || 'CAUTION' 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( function createOpenRender(

Loading…
Cancel
Save