mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 18:58:36 +02:00
fix(mdx-loader): support nested admonitions #8303
This commit is contained in:
parent
cf3ec180c2
commit
3f55453b8c
6 changed files with 217 additions and 8 deletions
10
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/nesting.md
generated
Normal file
10
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/nesting.md
generated
Normal file
|
@ -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...
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
|
@ -42,3 +42,8 @@ exports[`admonitions remark plugin interpolation 1`] = `
|
||||||
"<p>Test admonition with interpolated title/body</p>
|
"<p>Test admonition with interpolated title/body</p>
|
||||||
<admonition type="tip"><mdxAdmonitionTitle>My <code>interpolated</code> <strong>title</strong> <button style={{color: "red"}} onClick={() => alert("click")}>test</mdxAdmonitionTitle><p><code>body</code> <strong>interpolated</strong> content</p></admonition>"
|
<admonition type="tip"><mdxAdmonitionTitle>My <code>interpolated</code> <strong>title</strong> <button style={{color: "red"}} onClick={() => alert("click")}>test</mdxAdmonitionTitle><p><code>body</code> <strong>interpolated</strong> content</p></admonition>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`admonitions remark plugin nesting 1`] = `
|
||||||
|
"<p>Test nested Admonitions</p>
|
||||||
|
<admonition type="info"><mdxAdmonitionTitle><strong>Weather</strong></mdxAdmonitionTitle><p>On nice days, you can enjoy skiing in the mountains.</p><admonition type="danger"><mdxAdmonitionTitle><em>Storms</em></mdxAdmonitionTitle><p>Take care of snowstorms...</p></admonition></admonition>"
|
||||||
|
`;
|
||||||
|
|
|
@ -50,4 +50,9 @@ describe('admonitions remark plugin', () => {
|
||||||
const result = await processFixture('interpolation');
|
const result = await processFixture('interpolation');
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('nesting', async () => {
|
||||||
|
const result = await processFixture('nesting');
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,9 +52,20 @@ const plugin: Plugin = function plugin(
|
||||||
const options = normalizeOptions(optionsInput);
|
const options = normalizeOptions(optionsInput);
|
||||||
|
|
||||||
const keywords = Object.values(options.keywords).map(escapeRegExp).join('|');
|
const keywords = Object.values(options.keywords).map(escapeRegExp).join('|');
|
||||||
|
const nestingChar = escapeRegExp(options.tag.slice(0, 1));
|
||||||
const tag = escapeRegExp(options.tag);
|
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}(?<nestingLevel>${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
|
// The tokenizer is called on blocks to determine if there is an admonition
|
||||||
// present and create tags for it
|
// present and create tags for it
|
||||||
|
@ -77,6 +88,11 @@ const plugin: Plugin = function plugin(
|
||||||
];
|
];
|
||||||
const food = [];
|
const food = [];
|
||||||
const content = [];
|
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;
|
let newValue = value;
|
||||||
// consume lines until a closing tag
|
// 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);
|
next !== -1 ? newValue.slice(idx + 1, next) : newValue.slice(idx + 1);
|
||||||
food.push(line);
|
food.push(line);
|
||||||
newValue = newValue.slice(idx + 1);
|
newValue = newValue.slice(idx + 1);
|
||||||
// the closing tag is NOT part of the content
|
const nesting = nestingLevelRegex.exec(line);
|
||||||
if (line.startsWith(options.tag)) {
|
idx = newValue.indexOf(NEWLINE);
|
||||||
break;
|
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);
|
content.push(line);
|
||||||
idx = newValue.indexOf(NEWLINE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// consume the processed tag and replace escape sequences
|
// consume the processed tag and replace escape sequences
|
||||||
|
|
|
@ -239,3 +239,31 @@ Can be arbitrarily nested:
|
||||||
Admonition body
|
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
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
import Admonition from '@theme/Admonition';
|
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:
|
Example:
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ Hello world
|
||||||
|
|
||||||
## Specifying title {#specifying-title}
|
## Specifying title {#specifying-title}
|
||||||
|
|
||||||
You may also specify an optional title
|
You may also specify an optional title.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
:::note Your Title
|
:::note Your Title
|
||||||
|
@ -204,3 +204,128 @@ The types that are accepted are the same as above: `note`, `tip`, `danger`, `inf
|
||||||
</Admonition>
|
</Admonition>
|
||||||
</BrowserWindow>
|
</BrowserWindow>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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 <Admonition title="My Custom Admonition Title" {...props} />;
|
||||||
|
}
|
||||||
|
return <Admonition icon={<MyCustomNoteIcon />} {...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 (
|
||||||
|
<div style={{border: 'solid red', padding: 10}}>
|
||||||
|
<h5 style={{color: 'blue', fontSize: 30}}>{props.title}</h5>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
:::
|
||||||
|
```
|
||||||
|
|
||||||
|
<BrowserWindow>
|
||||||
|
|
||||||
|
:::my-custom-admonition Custom Admonition
|
||||||
|
|
||||||
|
It works!
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
</BrowserWindow>
|
||||||
|
|
Loading…
Add table
Reference in a new issue