diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/nesting.md b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/nesting.md new file mode 100644 index 000000000000..04cb34755c4f --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/nesting.md @@ -0,0 +1,10 @@ +Test nested Admonitions + +::::info **Weather** +On nice days, you can enjoy skiing in the mountains. + +:::danger *Storms* +Take care of snowstorms... +::: + +:::: \ No newline at end of file diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap index 0b7bdca847d7..b5597e9fb008 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap @@ -54,6 +54,11 @@ exports[`admonitions remark plugin interpolation 1`] = ` My interpolated title <button style={{color: "red"}} onClick={() => alert("click")}>test

body interpolated content

" `; +exports[`admonitions remark plugin nesting 1`] = ` +"

Test nested Admonitions

+Weather

On nice days, you can enjoy skiing in the mountains.

Storms

Take care of snowstorms...

" +`; + exports[`admonitions remark plugin replace custom keyword 1`] = ` "

The blog feature enables you to deploy in no time a full-featured blog.

:::info Sample Title

diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts index eb950e627e40..7e469ceaaa22 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts @@ -71,4 +71,9 @@ describe('admonitions remark plugin', () => { const result = await processFixture('interpolation'); expect(result).toMatchSnapshot(); }); + + it('nesting', async () => { + const result = await processFixture('nesting'); + expect(result).toMatchSnapshot(); + }); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts index 779ef71873d0..c05a88c1c2f8 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts @@ -69,9 +69,20 @@ const plugin: Plugin = function plugin( const options = normalizeOptions(optionsInput); const keywords = Object.values(options.keywords).map(escapeRegExp).join('|'); + const nestingChar = escapeRegExp(options.tag.slice(0, 1)); const tag = escapeRegExp(options.tag); - const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`); - const escapeTag = new RegExp(escapeRegExp(`\\${options.tag}`), 'g'); + + // resolve th nesting level of an opening tag + // ::: -> 0, :::: -> 1, ::::: -> 2 ... + const nestingLevelRegex = new RegExp( + `^${tag}(?${nestingChar}*)`, + ); + + const regex = new RegExp(`${tag}${nestingChar}*(${keywords})(?: *(.*))?\n`); + const escapeTag = new RegExp( + escapeRegExp(`\\${options.tag}${options.tag.slice(0, 1)}*`), + 'g', + ); // The tokenizer is called on blocks to determine if there is an admonition // present and create tags for it @@ -94,6 +105,11 @@ const plugin: Plugin = function plugin( ]; const food = []; const content = []; + // get the nesting level of the opening tag + const openingLevel = + nestingLevelRegex.exec(opening)!.groups!.nestingLevel!.length; + // used as a stack to keep track of nested admonitions + const nestingLevels: number[] = [openingLevel]; let newValue = value; // consume lines until a closing tag @@ -105,12 +121,32 @@ const plugin: Plugin = function plugin( next !== -1 ? newValue.slice(idx + 1, next) : newValue.slice(idx + 1); food.push(line); newValue = newValue.slice(idx + 1); - // the closing tag is NOT part of the content - if (line.startsWith(options.tag)) { - break; + const nesting = nestingLevelRegex.exec(line); + idx = newValue.indexOf(NEWLINE); + if (!nesting) { + content.push(line); + continue; + } + const tagLevel = nesting.groups!.nestingLevel!.length; + // first level + if (nestingLevels.length === 0) { + nestingLevels.push(tagLevel); + content.push(line); + continue; + } + const currentLevel = nestingLevels[nestingLevels.length - 1]!; + if (tagLevel < currentLevel) { + // entering a nested admonition block + nestingLevels.push(tagLevel); + } else if (tagLevel === currentLevel) { + // closing a nested admonition block + nestingLevels.pop(); + // the closing tag is NOT part of the content + if (nestingLevels.length === 0) { + break; + } } content.push(line); - idx = newValue.indexOf(NEWLINE); } // consume the processed tag and replace escape sequences diff --git a/website/_dogfooding/_pages tests/markdownPageTests.md b/website/_dogfooding/_pages tests/markdownPageTests.md index 3097fa779235..ce1670413425 100644 --- a/website/_dogfooding/_pages tests/markdownPageTests.md +++ b/website/_dogfooding/_pages tests/markdownPageTests.md @@ -245,3 +245,25 @@ Admonition body Admonition alias `:::important` should have Important title ::: + +:::::note title + +Some **content** with _Markdown_ `syntax`. + +::::note nested Title + +:::tip very nested Title + +Some **content** with _Markdown_ `syntax`. + +::: + +Some **content** with _Markdown_ `syntax`. + +:::: + +hey + +::::: + +after admonition diff --git a/website/docs/guides/markdown-features/markdown-features-admonitions.mdx b/website/docs/guides/markdown-features/markdown-features-admonitions.mdx index 9f01004a28ac..c2a6179e7e1d 100644 --- a/website/docs/guides/markdown-features/markdown-features-admonitions.mdx +++ b/website/docs/guides/markdown-features/markdown-features-admonitions.mdx @@ -129,6 +129,54 @@ Some **content** with _Markdown_ `syntax`. ``` +## Nested admonitions {#nested-admonitions} + +Admonitions can be nested. Use more colons `:` for each parent admonition level. + +```md +:::::info Parent + +Parent content + +::::danger Child + +Child content + +:::tip Deep Child + +Deep child content + +::: + +:::: + +::::: +``` + +```mdx-code-block + + +:::::info Parent + +Parent content + +::::danger Child + +Child content + +:::tip Deep Child + +Deep child content + +::: + +:::: + +::::: + + +``` + ## Admonitions with MDX {#admonitions-with-mdx} You can use MDX inside admonitions too! @@ -308,7 +356,7 @@ const AdmonitionTypes = { }; export default AdmonitionTypes; -```` +``` Now you can use your new admonition keyword in a Markdown file, and it will be parsed and rendered with your custom logic: