Skip to content

Commit

Permalink
fix(mdx-loader): support nested admonitions (#8303)
Browse files Browse the repository at this point in the history
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
closes undefined
  • Loading branch information
lebalz committed Nov 23, 2022
1 parent a4d935a commit b016686
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 7 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -54,6 +54,11 @@ exports[`admonitions remark plugin interpolation 1`] = `
<admonition type="tip"><mdxAdmonitionTitle>My <code>interpolated</code> <strong>title</strong> &#x3C;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>"
`;

exports[`admonitions remark plugin replace custom keyword 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<p>:::info Sample Title</p>
Expand Down
Expand Up @@ -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();
});
});
48 changes: 42 additions & 6 deletions packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts
Expand Up @@ -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}(?<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
// present and create tags for it
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions website/_dogfooding/_pages tests/markdownPageTests.md
Expand Up @@ -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
Expand Up @@ -129,6 +129,54 @@ Some **content** with _Markdown_ `syntax`.
</BrowserWindow>
```

## 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
<BrowserWindow>

:::::info Parent

Parent content

::::danger Child

Child content

:::tip Deep Child

Deep child content

:::

::::

:::::

</BrowserWindow>
```

## Admonitions with MDX {#admonitions-with-mdx}

You can use MDX inside admonitions too!
Expand Down Expand Up @@ -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:

Expand Down

0 comments on commit b016686

Please sign in to comment.