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 0000000000..04cb34755c
--- /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 227eb28b33..d02da45a66 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
@@ -42,3 +42,8 @@ exports[`admonitions remark plugin interpolation 1`] = `
"
Test admonition with interpolated title/body
My interpolated title <button style={{color: "red"}} onClick={() => alert("click")}>testbody interpolated content
"
`;
+
+exports[`admonitions remark plugin nesting 1`] = `
+"Test nested Admonitions
+WeatherOn nice days, you can enjoy skiing in the mountains.
StormsTake care of snowstorms...
"
+`;
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 3794562bb1..ee59edf894 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
@@ -50,4 +50,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 d6393b8c45..d7efad2a81 100644
--- a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts
+++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts
@@ -52,9 +52,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
@@ -77,6 +88,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
@@ -88,12 +104,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 323c741ee3..ce16704134 100644
--- a/website/_dogfooding/_pages tests/markdownPageTests.md
+++ b/website/_dogfooding/_pages tests/markdownPageTests.md
@@ -239,3 +239,31 @@ Can be arbitrarily nested:
Admonition body
:::
+
+:::important
+
+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 719dd371c9..bf95ccbf70 100644
--- a/website/docs/guides/markdown-features/markdown-features-admonitions.mdx
+++ b/website/docs/guides/markdown-features/markdown-features-admonitions.mdx
@@ -11,7 +11,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
-In addition to the basic Markdown syntax, we use [remark-admonitions](https://github.com/elviswolcott/remark-admonitions) alongside MDX to add support for admonitions. Admonitions are wrapped by a set of 3 colons.
+In addition to the basic Markdown syntax, we have a special admonitions syntax by wrapping text with a set of 3 colons, followed by a label denoting its type.
Example:
@@ -107,7 +107,7 @@ Hello world
## Specifying title {#specifying-title}
-You may also specify an optional title
+You may also specify an optional title.
```md
:::note Your Title
@@ -204,3 +204,128 @@ The types that are accepted are the same as above: `note`, `tip`, `danger`, `inf
```
+
+## Customizing admonitions {#customizing-admonitions}
+
+There are two kinds of customizations possible with admonitions: **parsing** and **rendering**.
+
+### Customizing rendering behavior {#customizing-rendering-behavior}
+
+You can customize how each individual admonition type is rendered through [swizzling](../../swizzling.md). You can often achieve your goal through a simple wrapper. For example, in the follow example, we swap out the icon for `info` admonitions only.
+
+```jsx title="src/theme/Admonition.js"
+import React from 'react';
+import Admonition from '@theme-original/Admonition';
+import MyCustomNoteIcon from '@site/static/img/info.svg';
+
+export default function AdmonitionWrapper(props) {
+ if (props.type !== 'info') {
+ return ;
+ }
+ return } {...props} />;
+}
+```
+
+### Customizing parsing behavior {#customizing-parsing-behavior}
+
+Admonitions are implemented with a [Remark plugin](./markdown-features-plugins.mdx). The plugin is designed to be configurable. To customize the Remark plugin for a specific content plugin (docs, blog, pages), pass the options through the `admonitions` key.
+
+```js title="docusaurus.config.js"
+module.exports = {
+ presets: [
+ [
+ '@docusaurus/preset-classic',
+ {
+ docs: {
+ admonitions: {
+ tag: ':::',
+ keywords: ['note', 'tip', 'info', 'caution', 'danger'],
+ extendDefaults: true,
+ },
+ },
+ },
+ ],
+ ],
+};
+```
+
+The plugin accepts the following options:
+
+- `tag`: The tag that encloses the admonition. Defaults to `:::`.
+- `keywords`: An array of keywords that can be used as the type for the admonition.
+- `extendDefaults`: Should the provided options (such as `keywords`) be merged into the existing defaults. Defaults to `false`.
+
+The `keyword` will be passed as the `type` prop of the `Admonition` component.
+
+### Custom admonition type components {#custom-admonition-type-components}
+
+By default, the theme doesn't know what do to with custom admonition keywords such as `:::my-custom-admonition`. It is your responsibility to map each admonition keyword to a React component so that the theme knows how to render them.
+
+If you registered a new admonition type `my-custom-admonition` via the following config:
+
+```js title="docusaurus.config.js"
+module.exports = {
+ // ...
+ presets: [
+ [
+ 'classic',
+ {
+ // ...
+ docs: {
+ admonitions: {
+ tag: ':::',
+ keywords: ['my-custom-admonition'],
+ extendDefaults: true,
+ },
+ },
+ },
+ ],
+ ],
+};
+```
+
+You can provide the corresponding React component for `:::my-custom-admonition` by creating the following file (unfortunately, since it's not a React component file, it's not swizzlable):
+
+```js title="src/theme/Admonition/Types.js"
+import React from 'react';
+import DefaultAdmonitionTypes from '@theme-original/Admonition/Types';
+
+function MyCustomAdmonition(props) {
+ return (
+
+
{props.title}
+
{props.children}
+
+ );
+}
+
+const AdmonitionTypes = {
+ ...DefaultAdmonitionTypes,
+
+ // Add all your custom admonition types here...
+ // You can also override the default ones if you want
+ 'my-custom-admonition': MyCustomAdmonition,
+};
+
+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:
+
+```md
+:::my-custom-admonition Custom Admonition
+
+It works!
+
+:::
+```
+
+
+
+:::my-custom-admonition Custom Admonition
+
+It works!
+
+:::
+
+