import { createMarkdownRenderer, disposeMdItInstance, type MarkdownOptions } from 'node/markdown/markdown' import type { MarkdownEnv } from 'node/shared' async function render( src: string, options: MarkdownOptions = {}, env?: Partial ) { disposeMdItInstance() const md = await createMarkdownRenderer('.', { highlight: (code) => code, ...options }) return md.renderAsync(src, env) } describe('node/markdown/plugins/containers', () => { test('renders built-in containers with default titles', async () => { const src = [ 'tip', 'info', 'warning', 'danger', 'note', 'important', 'caution' ] .map((t) => `::: ${t}\ncontent of ${t}\n:::`) .join('\n\n') expect(await render(src)).toMatchInlineSnapshot(` "

TIP

content of tip

INFO

content of info

WARNING

content of warning

DANGER

content of danger

NOTE

content of note

IMPORTANT

content of important

CAUTION

content of caution

" `) }) test('renders details as a disclosure with summary', async () => { expect(await render('::: details\nhidden content\n:::')) .toMatchInlineSnapshot(` "
Details

hidden content

" `) }) test('renders custom titles, including inline markdown', async () => { const src = [ '::: danger STOP', 'Danger zone, do not proceed', ':::', '', '::: tip A **bold** _title_ with `code`', 'content', ':::', '', '::: details Click me to toggle the code', '```js', "console.log('hi')", '```', ':::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

STOP

Danger zone, do not proceed

A bold title with code

content

Click me to toggle the code
js
console.log('hi')
      
" `) }) test('resolves reference links in titles', async () => { const src = [ '::: tip See [the guide][guide]', 'content', ':::', '', '[guide]: /guide/' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

See the guide

content

" `) }) test('respects custom labels from container options', async () => { const src = '::: tip\n提示内容\n:::\n\n::: details\n详情内容\n:::' expect( await render(src, { container: { tipLabel: '提示', detailsLabel: '详细信息' } }) ).toMatchInlineSnapshot(` "

提示

提示内容

详细信息

详情内容

" `) }) 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}', 'content', ':::', '', '::: tip Custom {.extra-class #custom-id}', 'content', ':::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "
Click me

content

Custom

content

" `) }) test('supports quoted and bare attr values on the fence line', async () => { expect(await render('::: tip Custom {data-a="b c" data-d=e}\ncontent\n:::')) .toMatchInlineSnapshot(` "

Custom

content

" `) }) test('skips the title element with a no-title attr', async () => { const src = [ '::: tip {no-title}', 'content', ':::', '', '::: warning Discarded {no-title .extra-class}', 'content', ':::', '', '::: details {no-title}', 'still needs its summary', ':::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

content

content

Details

still needs its summary

" `) }) // FIXME: enable after attrs plugin version bump test.skip('respects attrs plugin options on the fence line', async () => { const delimiters = await render( '::: tip Custom %(.extra-class)%\ncontent\n:::', { attrs: { left: '%(', right: ')%' } } ) expect(delimiters).toContain('
') const allowed = await render( '::: tip Custom {.extra-class data-x=1}\ncontent\n:::', { attrs: { allowed: ['class'] } } ) expect(allowed).toContain('
') expect(allowed).not.toContain('data-x') }) test('keeps fence line braces verbatim when attrs are disabled', async () => { expect( await render('::: details Click me {open}\ncontent\n:::', { attrs: false }) ).toMatchInlineSnapshot(` "
Click me {open}

content

" `) }) test('renders v-pre and raw containers as plain wrappers', async () => { const src = [ '::: v-pre', '{{ this will be displayed as-is }}', ':::', '', '::: raw', 'Wraps in a `
`', ':::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

{{ this will be displayed as-is }}

Wraps in a <div class="vp-raw">

" `) }) test('renders code groups with tabs', async () => { const src = [ '::: code-group', '', '```js [config.js]', 'const a = 1', '```', '', '```ts [config.ts]', 'const a: number = 1', '```', '', ':::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "
js
const a = 1
      
ts
const a: number = 1
      
" `) }) test('supports nesting via longer fences', async () => { const src = [ ':::: info Outer', 'outer content', '', '::: details Inner', 'inner content', ':::', '::::' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

Outer

outer content

Inner

inner content

" `) }) test('auto-closes unclosed containers', async () => { expect(await render('::: warning\nno closing fence')) .toMatchInlineSnapshot(` "

WARNING

no closing fence

" `) }) test('parses fences without a space before the name', async () => { expect(await render(':::tip\ncontent\n:::')).toMatchInlineSnapshot(` "

TIP

content

" `) }) test('leaves non-container fence lines alone', async () => { expect(await render('::: unknown\ncontent\n:::')).toMatchInlineSnapshot(` "

::: unknown content :::

" `) }) }) describe('node/markdown/plugins/containers (github alerts)', () => { test('renders github alerts like containers', async () => { const src = [ '> [!NOTE]', '> note content', '', '> [!TIP]', '> tip content', '', '> [!IMPORTANT]', '> important content', '', '> [!WARNING]', '> warning content', '', '> [!CAUTION]', '> caution content' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

NOTE

note content

TIP

tip content

IMPORTANT

important content

WARNING

warning content

CAUTION

caution content

" `) }) test('matches markers case-insensitively', async () => { expect(await render('> [!tip]\n> content')).toMatchInlineSnapshot(` "

TIP

content

" `) }) test('supports custom titles after the marker', async () => { expect(await render('> [!WARNING] Custom Title\n> content')) .toMatchInlineSnapshot(` "

Custom Title

content

" `) }) test('respects custom labels from container options', async () => { expect( await render('> [!TIP]\n> content', { container: { tipLabel: '提示' } }) ).toMatchInlineSnapshot(` "

提示

content

" `) }) 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]', '> first paragraph', 'lazy continuation', '>', '> - list item', '>', '> ```js', '> const a = 1', '> ```' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

NOTE

first paragraph lazy continuation

  • list item
js
const a = 1
      
" `) }) test('converts markers without content', async () => { expect(await render('> [!NOTE]')).toMatchInlineSnapshot(` "

NOTE

" `) }) test('leaves regular blockquotes and unknown markers alone', async () => { const src = [ '> just a quote', '', '> [!FOO]', '> not an alert', '', 'paragraph [!NOTE] not at blockquote start' ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "

just a quote

[!FOO] not an alert

paragraph [!NOTE] not at blockquote start

" `) }) test('can be disabled via gfmAlerts: false', async () => { expect(await render('> [!NOTE]\n> content', { gfmAlerts: false })) .toMatchInlineSnapshot(` "

[!NOTE] content

" `) }) }) describe('node/markdown/plugins/containers (locales)', () => { const options: MarkdownOptions = { container: { tipLabel: 'ROOT TIP', customContainers: { success: 'SUCCESS' } }, locales: { zh: { container: { tipLabel: '提示', detailsLabel: '详细信息', customContainers: { success: '成功' } } } } } test('resolves container titles for the active locale', async () => { const src = '::: tip\n内容\n:::\n\n::: details\n内容\n:::\n\n::: success\n内容\n:::' expect(await render(src, options, { localeIndex: 'zh' })) .toMatchInlineSnapshot(` "

提示

内容

详细信息

内容

成功

内容

" `) }) test('falls back to root titles for other locales', async () => { const src = '::: tip\ncontent\n:::' const root = await render(src, options) expect(await render(src, options, { localeIndex: 'es' })).toBe(root) expect(root).toMatchInlineSnapshot(` "

ROOT TIP

content

" `) }) test('explicit titles win over locale defaults', async () => { expect( await render('::: tip Custom Title\n内容\n:::', options, { localeIndex: 'zh' }) ).toMatchInlineSnapshot(` "

Custom Title

内容

" `) }) test('resolves alert titles for the active locale', async () => { expect( await render('> [!TIP]\n> 内容\n\n> [!SUCCESS]\n> 内容', options, { localeIndex: 'zh' }) ).toMatchInlineSnapshot(` "

提示

内容

成功

内容

" `) }) test('rejects locale titles for unregistered containers', async () => { await expect( render('text', { locales: { zh: { container: { customContainers: { nope: 'X' } } } } }) ).rejects.toThrow('is not registered in the root markdown config') }) test('resolves the code copy button strings for the active locale', async () => { const src = '```js\nconst a = 1\n```' const opts: MarkdownOptions = { codeCopyButton: { copiedText: 'Copied!' }, locales: { zh: { codeCopyButton: { tooltipText: '复制代码', copiedText: '已复制' } } } } const zh = await render(src, opts, { localeIndex: 'zh' }) expect(zh).toContain('title="复制代码"') expect(zh).toContain('data-copied="已复制"') const es = await render(src, opts, { localeIndex: 'es' }) expect(es).toContain('title="Copy code"') expect(es).toContain('data-copied="Copied!"') const root = await render(src, opts) expect(root).toContain('title="Copy code"') expect(root).toContain('data-copied="Copied!"') }) })