mirror of https://github.com/vuejs/vitepress
Replaces markdown-it-container and merges the GitHub alerts plugin into
the containers module so both share one title map. Rendered output is
byte-compatible with the previous implementation.
This also restores fence-line attributes on containers ({open}, {.class},
etc.), which broke when markdown-it-attrs was replaced with
@mdit/plugin-attrs - its fence rule handled containers too, but had to be
disabled to keep code block meta intact. The container plugin now parses
the trailing braces itself, so the old markdown-it-attrs patch is not
needed in any form.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5335/head
parent
f1ee91335e
commit
420565236b
@ -0,0 +1,404 @@
|
|||||||
|
import {
|
||||||
|
createMarkdownRenderer,
|
||||||
|
disposeMdItInstance,
|
||||||
|
type MarkdownOptions
|
||||||
|
} from 'node/markdown/markdown'
|
||||||
|
|
||||||
|
async function render(src: string, options: MarkdownOptions = {}) {
|
||||||
|
disposeMdItInstance()
|
||||||
|
const md = await createMarkdownRenderer('.', {
|
||||||
|
highlight: (code) => code,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
return md.renderAsync(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">TIP</p>
|
||||||
|
<p>content of tip</p>
|
||||||
|
</div>
|
||||||
|
<div class="info custom-block"><p class="custom-block-title custom-block-title-default">INFO</p>
|
||||||
|
<p>content of info</p>
|
||||||
|
</div>
|
||||||
|
<div class="warning custom-block"><p class="custom-block-title custom-block-title-default">WARNING</p>
|
||||||
|
<p>content of warning</p>
|
||||||
|
</div>
|
||||||
|
<div class="danger custom-block"><p class="custom-block-title custom-block-title-default">DANGER</p>
|
||||||
|
<p>content of danger</p>
|
||||||
|
</div>
|
||||||
|
<div class="note custom-block"><p class="custom-block-title custom-block-title-default">NOTE</p>
|
||||||
|
<p>content of note</p>
|
||||||
|
</div>
|
||||||
|
<div class="important custom-block"><p class="custom-block-title custom-block-title-default">IMPORTANT</p>
|
||||||
|
<p>content of important</p>
|
||||||
|
</div>
|
||||||
|
<div class="caution custom-block"><p class="custom-block-title custom-block-title-default">CAUTION</p>
|
||||||
|
<p>content of caution</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renders details as a disclosure with summary', async () => {
|
||||||
|
expect(await render('::: details\nhidden content\n:::'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"<details class="details custom-block"><summary>Details</summary>
|
||||||
|
<p>hidden content</p>
|
||||||
|
</details>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="danger custom-block"><p class="custom-block-title">STOP</p>
|
||||||
|
<p>Danger zone, do not proceed</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip custom-block"><p class="custom-block-title">A <strong>bold</strong> <em>title</em> with <code>code</code></p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
<details class="details custom-block"><summary>Click me to toggle the code</summary>
|
||||||
|
<div class="language-js"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">console.log('hi')
|
||||||
|
</code></pre>
|
||||||
|
</div></details>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolves reference links in titles', async () => {
|
||||||
|
const src = [
|
||||||
|
'::: tip See [the guide][guide]',
|
||||||
|
'content',
|
||||||
|
':::',
|
||||||
|
'',
|
||||||
|
'[guide]: /guide/'
|
||||||
|
].join('\n')
|
||||||
|
expect(await render(src)).toMatchInlineSnapshot(`
|
||||||
|
"<div class="tip custom-block"><p class="custom-block-title">See <a href="/guide/">the guide</a></p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">提示</p>
|
||||||
|
<p>提示内容</p>
|
||||||
|
</div>
|
||||||
|
<details class="details custom-block"><summary>详细信息</summary>
|
||||||
|
<p>详情内容</p>
|
||||||
|
</details>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<details open="" class="details custom-block"><summary>Click me</summary>
|
||||||
|
<p>content</p>
|
||||||
|
</details>
|
||||||
|
<div class="extra-class tip custom-block" id="custom-id"><p class="custom-block-title">Custom</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div data-a="b c" data-d="e" class="tip custom-block"><p class="custom-block-title">Custom</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps fence line braces verbatim when attrs are disabled', async () => {
|
||||||
|
expect(
|
||||||
|
await render('::: details Click me {open}\ncontent\n:::', {
|
||||||
|
attrs: false
|
||||||
|
})
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"<details class="details custom-block"><summary>Click me {open}</summary>
|
||||||
|
<p>content</p>
|
||||||
|
</details>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 `<div class="vp-raw">`',
|
||||||
|
':::'
|
||||||
|
].join('\n')
|
||||||
|
expect(await render(src)).toMatchInlineSnapshot(`
|
||||||
|
"<div v-pre>
|
||||||
|
<p>{{ this will be displayed as-is }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="vp-raw">
|
||||||
|
<p>Wraps in a <code><div class="vp-raw"></code></p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-0" id="tab-1" checked><label data-title="config.js" for="tab-1">config.js</label><input type="radio" name="group-0" id="tab-2" ><label data-title="config.ts" for="tab-2">config.ts</label></div><div class="blocks">
|
||||||
|
<div class="language-js active"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
|
||||||
|
</code></pre>
|
||||||
|
</div><div class="language-ts"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre><code class="language-ts">const a: number = 1
|
||||||
|
</code></pre>
|
||||||
|
</div></div></div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('supports nesting via longer fences', async () => {
|
||||||
|
const src = [
|
||||||
|
':::: info Outer',
|
||||||
|
'outer content',
|
||||||
|
'',
|
||||||
|
'::: details Inner',
|
||||||
|
'inner content',
|
||||||
|
':::',
|
||||||
|
'::::'
|
||||||
|
].join('\n')
|
||||||
|
expect(await render(src)).toMatchInlineSnapshot(`
|
||||||
|
"<div class="info custom-block"><p class="custom-block-title">Outer</p>
|
||||||
|
<p>outer content</p>
|
||||||
|
<details class="details custom-block"><summary>Inner</summary>
|
||||||
|
<p>inner content</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('auto-closes unclosed containers', async () => {
|
||||||
|
expect(await render('::: warning\nno closing fence'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"<div class="warning custom-block"><p class="custom-block-title custom-block-title-default">WARNING</p>
|
||||||
|
<p>no closing fence</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses fences without a space before the name', async () => {
|
||||||
|
expect(await render(':::tip\ncontent\n:::')).toMatchInlineSnapshot(`
|
||||||
|
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">TIP</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('leaves non-container fence lines alone', async () => {
|
||||||
|
expect(await render('::: unknown\ncontent\n:::')).toMatchInlineSnapshot(`
|
||||||
|
"<p>::: unknown
|
||||||
|
content
|
||||||
|
:::</p>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
|
||||||
|
<p>note content</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
|
||||||
|
<p>tip content</p>
|
||||||
|
</div>
|
||||||
|
<div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p>
|
||||||
|
<p>important content</p>
|
||||||
|
</div>
|
||||||
|
<div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p>
|
||||||
|
<p>warning content</p>
|
||||||
|
</div>
|
||||||
|
<div class="caution custom-block github-alert"><p class="custom-block-title">CAUTION</p>
|
||||||
|
<p>caution content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('matches markers case-insensitively', async () => {
|
||||||
|
expect(await render('> [!tip]\n> content')).toMatchInlineSnapshot(`
|
||||||
|
"<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('supports custom titles after the marker', async () => {
|
||||||
|
expect(await render('> [!WARNING] Custom Title\n> content'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"<div class="warning custom-block github-alert"><p class="custom-block-title">Custom Title</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('respects custom labels from container options', async () => {
|
||||||
|
expect(
|
||||||
|
await render('> [!TIP]\n> content', { container: { tipLabel: '提示' } })
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
"<div class="tip custom-block github-alert"><p class="custom-block-title">提示</p>
|
||||||
|
<p>content</p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
|
||||||
|
<p>first paragraph
|
||||||
|
lazy continuation</p>
|
||||||
|
<ul>
|
||||||
|
<li>list item</li>
|
||||||
|
</ul>
|
||||||
|
<div class="language-js"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre><code class="language-js">const a = 1
|
||||||
|
</code></pre>
|
||||||
|
</div></div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('converts markers without content', async () => {
|
||||||
|
expect(await render('> [!NOTE]')).toMatchInlineSnapshot(`
|
||||||
|
"<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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(`
|
||||||
|
"<blockquote>
|
||||||
|
<p>just a quote</p>
|
||||||
|
</blockquote>
|
||||||
|
<blockquote>
|
||||||
|
<p>[!FOO]
|
||||||
|
not an alert</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>paragraph [!NOTE] not at blockquote start</p>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can be disabled via gfmAlerts: false', async () => {
|
||||||
|
expect(await render('> [!NOTE]\n> content', { gfmAlerts: false }))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"<blockquote>
|
||||||
|
<p>[!NOTE]
|
||||||
|
content</p>
|
||||||
|
</blockquote>
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,124 +1,191 @@
|
|||||||
|
import { container } from '@mdit/plugin-container'
|
||||||
import type { MarkdownItAsync } from 'markdown-it-async'
|
import type { MarkdownItAsync } from 'markdown-it-async'
|
||||||
import container from 'markdown-it-container'
|
|
||||||
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
|
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
|
||||||
import type Token from 'markdown-it/lib/token.mjs'
|
import type Token from 'markdown-it/lib/token.mjs'
|
||||||
import type { MarkdownEnv } from '../../shared'
|
import type { MarkdownEnv } from '../../shared'
|
||||||
import { extractTitle } from './preWrapper'
|
import { extractTitle } from './preWrapper'
|
||||||
|
|
||||||
|
export interface ContainerOptions {
|
||||||
|
infoLabel?: string
|
||||||
|
noteLabel?: string
|
||||||
|
tipLabel?: string
|
||||||
|
warningLabel?: string
|
||||||
|
dangerLabel?: string
|
||||||
|
detailsLabel?: string
|
||||||
|
importantLabel?: string
|
||||||
|
cautionLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const containerPlugin = (
|
export const containerPlugin = (
|
||||||
md: MarkdownItAsync,
|
md: MarkdownItAsync,
|
||||||
options?: ContainerOptions
|
options?: ContainerOptions,
|
||||||
|
attrs = true
|
||||||
) => {
|
) => {
|
||||||
md.use(...createContainer('tip', options?.tipLabel || 'TIP', md))
|
md
|
||||||
.use(...createContainer('info', options?.infoLabel || 'INFO', md))
|
|
||||||
.use(...createContainer('warning', options?.warningLabel || 'WARNING', md))
|
|
||||||
.use(...createContainer('danger', options?.dangerLabel || 'DANGER', md))
|
|
||||||
.use(...createContainer('details', options?.detailsLabel || 'Details', md))
|
|
||||||
.use(...createContainer('note', options?.noteLabel || 'NOTE', md))
|
|
||||||
.use(
|
|
||||||
...createContainer(
|
|
||||||
'important',
|
|
||||||
options?.importantLabel || 'IMPORTANT',
|
|
||||||
md
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.use(...createContainer('caution', options?.cautionLabel || 'CAUTION', md))
|
|
||||||
// explicitly escape Vue syntax
|
// explicitly escape Vue syntax
|
||||||
.use(container, 'v-pre', {
|
.use(container, {
|
||||||
render: (tokens: Token[], idx: number) =>
|
name: 'v-pre',
|
||||||
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`
|
openRender: () => `<div v-pre>\n`,
|
||||||
|
closeRender: () => `</div>\n`
|
||||||
|
})
|
||||||
|
.use(container, {
|
||||||
|
name: 'raw',
|
||||||
|
openRender: () => `<div class="vp-raw">\n`,
|
||||||
|
closeRender: () => `</div>\n`
|
||||||
|
})
|
||||||
|
.use(container, {
|
||||||
|
name: 'code-group',
|
||||||
|
openRender: createCodeGroupOpenRender(md),
|
||||||
|
closeRender: () => `</div></div>\n`
|
||||||
})
|
})
|
||||||
.use(container, 'raw', {
|
|
||||||
render: (tokens: Token[], idx: number) =>
|
for (const [name, defaultTitle] of Object.entries(resolveTitles(options))) {
|
||||||
tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`
|
md.use(container, {
|
||||||
|
name,
|
||||||
|
openRender: createOpenRender(md, name, defaultTitle, attrs),
|
||||||
|
closeRender: () => (name === 'details' ? `</details>\n` : `</div>\n`)
|
||||||
})
|
})
|
||||||
.use(...createCodeGroup(md))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerArgs = [typeof container, string, { render: RenderRule }]
|
function resolveTitles(options?: ContainerOptions): Record<string, string> {
|
||||||
|
return {
|
||||||
|
tip: options?.tipLabel || 'TIP',
|
||||||
|
info: options?.infoLabel || 'INFO',
|
||||||
|
warning: options?.warningLabel || 'WARNING',
|
||||||
|
danger: options?.dangerLabel || 'DANGER',
|
||||||
|
details: options?.detailsLabel || 'Details',
|
||||||
|
note: options?.noteLabel || 'NOTE',
|
||||||
|
important: options?.importantLabel || 'IMPORTANT',
|
||||||
|
caution: options?.cautionLabel || 'CAUTION'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createContainer(
|
function createOpenRender(
|
||||||
klass: string,
|
md: MarkdownItAsync,
|
||||||
|
name: string,
|
||||||
defaultTitle: string,
|
defaultTitle: string,
|
||||||
md: MarkdownItAsync
|
attrs: boolean
|
||||||
): ContainerArgs {
|
): RenderRule {
|
||||||
return [
|
return (tokens, idx, _options, env: MarkdownEnv & { references?: any }) => {
|
||||||
container,
|
const token = tokens[idx]
|
||||||
klass,
|
let info = token.info.trim().slice(name.length).trim()
|
||||||
{
|
if (attrs) info = applyFenceAttrs(token, info)
|
||||||
render(tokens, idx, _options, env: MarkdownEnv & { references?: any }) {
|
token.attrJoin('class', `${name} custom-block`)
|
||||||
const token = tokens[idx]
|
const renderedAttrs = md.renderer.renderAttrs(token)
|
||||||
if (token.nesting === 1) {
|
const title = md.renderInline(info || defaultTitle, {
|
||||||
token.attrJoin('class', `${klass} custom-block`)
|
references: env.references
|
||||||
const attrs = md.renderer.renderAttrs(token)
|
})
|
||||||
const info = token.info.trim().slice(klass.length).trim()
|
if (name === 'details')
|
||||||
const title = md.renderInline(info || defaultTitle, {
|
return `<details ${renderedAttrs}><summary>${title}</summary>\n`
|
||||||
references: env.references
|
const titleClass =
|
||||||
})
|
'custom-block-title' + (info ? '' : ' custom-block-title-default')
|
||||||
const titleClass =
|
return `<div ${renderedAttrs}><p class="${titleClass}">${title}</p>\n`
|
||||||
'custom-block-title' + (info ? '' : ' custom-block-title-default')
|
}
|
||||||
if (klass === 'details')
|
}
|
||||||
return `<details ${attrs}><summary>${title}</summary>\n`
|
|
||||||
return `<div ${attrs}><p class="${titleClass}">${title}</p>\n`
|
// `::: tip Title {#id .class key="value" bare}` - the attrs plugin only
|
||||||
} else return klass === 'details' ? `</details>\n` : `</div>\n`
|
// handles fence lines through its `fence` rule, which is disabled to keep
|
||||||
}
|
// code block meta intact, so its trailing-braces syntax is parsed here
|
||||||
|
const fenceAttrsRE = /(?:^|\s)\{\s*([^{}]+?)\s*\}$/
|
||||||
|
const attrRE = /([^\s=]+)(?:=("[^"]*"|\S*))?/g
|
||||||
|
|
||||||
|
function applyFenceAttrs(token: Token, info: string): string {
|
||||||
|
const match = fenceAttrsRE.exec(info)
|
||||||
|
if (!match) return info
|
||||||
|
for (const [, name, value = ''] of match[1].matchAll(attrRE)) {
|
||||||
|
if (name.startsWith('.')) {
|
||||||
|
if (name.length > 1) token.attrJoin('class', name.slice(1))
|
||||||
|
} else if (name.startsWith('#')) {
|
||||||
|
if (name.length > 1) token.attrPush(['id', name.slice(1)])
|
||||||
|
} else {
|
||||||
|
token.attrPush([name, value.replace(/^"(.*)"$/, '$1')])
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
return info.slice(0, match.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCodeGroup(md: MarkdownItAsync): ContainerArgs {
|
function createCodeGroupOpenRender(md: MarkdownItAsync): RenderRule {
|
||||||
return [
|
return (tokens, idx) => {
|
||||||
container,
|
let tabs = ''
|
||||||
'code-group',
|
let checked = 'checked'
|
||||||
{
|
|
||||||
render(tokens, idx) {
|
for (
|
||||||
if (tokens[idx].nesting === 1) {
|
let i = idx + 1;
|
||||||
let tabs = ''
|
!(
|
||||||
let checked = 'checked'
|
tokens[i].nesting === -1 &&
|
||||||
|
tokens[i].type === 'container_code-group_close'
|
||||||
for (
|
);
|
||||||
let i = idx + 1;
|
++i
|
||||||
!(
|
) {
|
||||||
tokens[i].nesting === -1 &&
|
const isHtml = tokens[i].type === 'html_block'
|
||||||
tokens[i].type === 'container_code-group_close'
|
|
||||||
);
|
if ((tokens[i].type === 'fence' && tokens[i].tag === 'code') || isHtml) {
|
||||||
++i
|
const title = extractTitle(
|
||||||
) {
|
isHtml ? tokens[i].content : tokens[i].info,
|
||||||
const isHtml = tokens[i].type === 'html_block'
|
isHtml
|
||||||
|
)
|
||||||
if (
|
|
||||||
(tokens[i].type === 'fence' && tokens[i].tag === 'code') ||
|
if (title) {
|
||||||
isHtml
|
tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}><label data-title="${md.utils.escapeHtml(title)}" for="tab-${i}">${title}</label>`
|
||||||
) {
|
|
||||||
const title = extractTitle(
|
if (checked && !isHtml) tokens[i].info += ' active'
|
||||||
isHtml ? tokens[i].content : tokens[i].info,
|
checked = ''
|
||||||
isHtml
|
|
||||||
)
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}><label data-title="${md.utils.escapeHtml(title)}" for="tab-${i}">${title}</label>`
|
|
||||||
|
|
||||||
if (checked && !isHtml) tokens[i].info += ' active'
|
|
||||||
checked = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n`
|
|
||||||
}
|
}
|
||||||
return `</div></div>\n`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerOptions {
|
const alertMarkerRE = /^\[!([\w-]+)\]([^\n\r]*)/
|
||||||
infoLabel?: string
|
|
||||||
noteLabel?: string
|
export const gitHubAlertsPlugin = (
|
||||||
tipLabel?: string
|
md: MarkdownItAsync,
|
||||||
warningLabel?: string
|
options?: ContainerOptions
|
||||||
dangerLabel?: string
|
) => {
|
||||||
detailsLabel?: string
|
const titles = resolveTitles(options)
|
||||||
importantLabel?: string
|
// details makes no sense as a blockquote-style alert
|
||||||
cautionLabel?: string
|
delete titles.details
|
||||||
|
|
||||||
|
md.core.ruler.after('block', 'github-alerts', (state) => {
|
||||||
|
const tokens = state.tokens
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
if (tokens[i].type === 'blockquote_open') {
|
||||||
|
const startIndex = i
|
||||||
|
const open = tokens[startIndex]
|
||||||
|
let endIndex = i + 1
|
||||||
|
while (
|
||||||
|
endIndex < tokens.length &&
|
||||||
|
(tokens[endIndex].type !== 'blockquote_close' ||
|
||||||
|
tokens[endIndex].level !== open.level)
|
||||||
|
)
|
||||||
|
endIndex++
|
||||||
|
if (endIndex === tokens.length) continue
|
||||||
|
const close = tokens[endIndex]
|
||||||
|
const firstContent = tokens
|
||||||
|
.slice(startIndex, endIndex + 1)
|
||||||
|
.find((token) => token.type === 'inline')
|
||||||
|
if (!firstContent) continue
|
||||||
|
const match = firstContent.content.match(alertMarkerRE)
|
||||||
|
if (!match) continue
|
||||||
|
const type = match[1].toLowerCase()
|
||||||
|
if (!Object.hasOwn(titles, type)) continue
|
||||||
|
const title = match[2].trim() || titles[type]
|
||||||
|
firstContent.content = firstContent.content
|
||||||
|
.slice(match[0].length)
|
||||||
|
.trimStart()
|
||||||
|
open.type = 'github_alert_open'
|
||||||
|
open.tag = 'div'
|
||||||
|
open.meta = { title, type }
|
||||||
|
close.type = 'github_alert_close'
|
||||||
|
close.tag = 'div'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
md.renderer.rules.github_alert_open = function (tokens, idx) {
|
||||||
|
const { title, type } = tokens[idx].meta
|
||||||
|
return `<div class="${type} custom-block github-alert"><p class="custom-block-title">${title}</p>\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import type { MarkdownItAsync } from 'markdown-it-async'
|
|
||||||
import type { ContainerOptions } from './containers'
|
|
||||||
|
|
||||||
const markerRE =
|
|
||||||
/^\[!(TIP|NOTE|INFO|IMPORTANT|WARNING|CAUTION|DANGER)\]([^\n\r]*)/i
|
|
||||||
|
|
||||||
export const gitHubAlertsPlugin = (
|
|
||||||
md: MarkdownItAsync,
|
|
||||||
options?: ContainerOptions
|
|
||||||
) => {
|
|
||||||
const titleMark = {
|
|
||||||
tip: options?.tipLabel || 'TIP',
|
|
||||||
note: options?.noteLabel || 'NOTE',
|
|
||||||
info: options?.infoLabel || 'INFO',
|
|
||||||
important: options?.importantLabel || 'IMPORTANT',
|
|
||||||
warning: options?.warningLabel || 'WARNING',
|
|
||||||
caution: options?.cautionLabel || 'CAUTION',
|
|
||||||
danger: options?.dangerLabel || 'DANGER'
|
|
||||||
} as Record<string, string>
|
|
||||||
|
|
||||||
md.core.ruler.after('block', 'github-alerts', (state) => {
|
|
||||||
const tokens = state.tokens
|
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
|
||||||
if (tokens[i].type === 'blockquote_open') {
|
|
||||||
const startIndex = i
|
|
||||||
const open = tokens[startIndex]
|
|
||||||
let endIndex = i + 1
|
|
||||||
while (
|
|
||||||
endIndex < tokens.length &&
|
|
||||||
(tokens[endIndex].type !== 'blockquote_close' ||
|
|
||||||
tokens[endIndex].level !== open.level)
|
|
||||||
)
|
|
||||||
endIndex++
|
|
||||||
if (endIndex === tokens.length) continue
|
|
||||||
const close = tokens[endIndex]
|
|
||||||
const firstContent = tokens
|
|
||||||
.slice(startIndex, endIndex + 1)
|
|
||||||
.find((token) => token.type === 'inline')
|
|
||||||
if (!firstContent) continue
|
|
||||||
const match = firstContent.content.match(markerRE)
|
|
||||||
if (!match) continue
|
|
||||||
const type = match[1].toLowerCase()
|
|
||||||
const title = match[2].trim() || titleMark[type] || capitalize(type)
|
|
||||||
firstContent.content = firstContent.content
|
|
||||||
.slice(match[0].length)
|
|
||||||
.trimStart()
|
|
||||||
open.type = 'github_alert_open'
|
|
||||||
open.tag = 'div'
|
|
||||||
open.meta = {
|
|
||||||
title,
|
|
||||||
type
|
|
||||||
}
|
|
||||||
close.type = 'github_alert_close'
|
|
||||||
close.tag = 'div'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
md.renderer.rules.github_alert_open = function (tokens, idx) {
|
|
||||||
const { title, type } = tokens[idx].meta
|
|
||||||
const attrs = ''
|
|
||||||
return `<div class="${type} custom-block github-alert"${attrs}><p class="custom-block-title">${title}</p>\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function capitalize(str: string) {
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
||||||
}
|
|
||||||
Loading…
Reference in new issue