mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-04 03:42:34 +02:00
refactor: handle all admonitions via JSX component (#7152)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
17fe43ecc8
commit
5746c58f41
39 changed files with 709 additions and 250 deletions
|
@ -23,8 +23,10 @@ import unwrapMdxCodeBlocks from './remark/unwrapMdxCodeBlocks';
|
|||
import transformImage from './remark/transformImage';
|
||||
import transformLinks from './remark/transformLinks';
|
||||
|
||||
import transformAdmonitions from './remark/admonitions';
|
||||
import type {LoaderContext} from 'webpack';
|
||||
import type {Processor, Plugin} from 'unified';
|
||||
import type {AdmonitionOptions} from './remark/admonitions';
|
||||
|
||||
const {
|
||||
loaders: {inlineMarkdownImageFileLoader},
|
||||
|
@ -37,6 +39,7 @@ const pragma = `
|
|||
`;
|
||||
|
||||
const DEFAULT_OPTIONS: MDXOptions = {
|
||||
admonitions: true,
|
||||
rehypePlugins: [],
|
||||
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
|
@ -48,7 +51,9 @@ const compilerCache = new Map<string | Options, [Processor, Options]>();
|
|||
export type MDXPlugin =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[Plugin<any[]>, any] | Plugin<any[]>;
|
||||
|
||||
export type MDXOptions = {
|
||||
admonitions: boolean | AdmonitionOptions;
|
||||
remarkPlugins: MDXPlugin[];
|
||||
rehypePlugins: MDXPlugin[];
|
||||
beforeDefaultRemarkPlugins: MDXPlugin[];
|
||||
|
@ -132,6 +137,19 @@ function createAssetsExportCode(assets: unknown) {
|
|||
return `{\n${codeLines.join('\n')}\n}`;
|
||||
}
|
||||
|
||||
function getAdmonitionsPlugins(
|
||||
admonitionsOption: MDXOptions['admonitions'],
|
||||
): MDXPlugin[] {
|
||||
if (admonitionsOption) {
|
||||
const plugin: MDXPlugin =
|
||||
admonitionsOption === true
|
||||
? transformAdmonitions
|
||||
: [transformAdmonitions, admonitionsOption];
|
||||
return [plugin];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function mdxLoader(
|
||||
this: LoaderContext<Options>,
|
||||
fileString: string,
|
||||
|
@ -149,32 +167,37 @@ export async function mdxLoader(
|
|||
const hasFrontMatter = Object.keys(frontMatter).length > 0;
|
||||
|
||||
if (!compilerCache.has(this.query)) {
|
||||
const remarkPlugins: MDXPlugin[] = [
|
||||
...(reqOptions.beforeDefaultRemarkPlugins ?? []),
|
||||
...getAdmonitionsPlugins(reqOptions.admonitions ?? false),
|
||||
...DEFAULT_OPTIONS.remarkPlugins,
|
||||
[
|
||||
transformImage,
|
||||
{
|
||||
staticDirs: reqOptions.staticDirs,
|
||||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
transformLinks,
|
||||
{
|
||||
staticDirs: reqOptions.staticDirs,
|
||||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
...(reqOptions.remarkPlugins ?? []),
|
||||
];
|
||||
|
||||
const rehypePlugins: MDXPlugin[] = [
|
||||
...(reqOptions.beforeDefaultRehypePlugins ?? []),
|
||||
...DEFAULT_OPTIONS.rehypePlugins,
|
||||
...(reqOptions.rehypePlugins ?? []),
|
||||
];
|
||||
|
||||
const options: Options = {
|
||||
...reqOptions,
|
||||
remarkPlugins: [
|
||||
...(reqOptions.beforeDefaultRemarkPlugins ?? []),
|
||||
...DEFAULT_OPTIONS.remarkPlugins,
|
||||
[
|
||||
transformImage,
|
||||
{
|
||||
staticDirs: reqOptions.staticDirs,
|
||||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
transformLinks,
|
||||
{
|
||||
staticDirs: reqOptions.staticDirs,
|
||||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
...(reqOptions.remarkPlugins ?? []),
|
||||
],
|
||||
rehypePlugins: [
|
||||
...(reqOptions.beforeDefaultRehypePlugins ?? []),
|
||||
...DEFAULT_OPTIONS.rehypePlugins,
|
||||
...(reqOptions.rehypePlugins ?? []),
|
||||
],
|
||||
remarkPlugins,
|
||||
rehypePlugins,
|
||||
};
|
||||
compilerCache.set(this.query, [createCompiler(options), options]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Elvis Wolcott
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Facebook, Inc. and its affiliates.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
|||
# Docusaurus admonitions
|
||||
|
||||
Code from [remark-admonitions](https://github.com/remarkjs/remark-directive) (MIT license) has been copied to this folder, and highly customized for Docusaurus needs.
|
25
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/base.md
generated
Normal file
25
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/base.md
generated
Normal file
|
@ -0,0 +1,25 @@
|
|||
The blog feature enables you to deploy in no time a full-featured blog.
|
||||
|
||||
:::info Sample Title
|
||||
|
||||
Check the [Blog Plugin API Reference documentation](./api/plugins/plugin-content-blog.md) for an exhaustive list of options.
|
||||
|
||||
:::
|
||||
|
||||
## Initial setup {#initial-setup}
|
||||
|
||||
To set up your site's blog, start by creating a `blog` directory.
|
||||
|
||||
:::tip
|
||||
|
||||
Use the **[Fast Track](introduction.md#fast-track)** to understand Docusaurus in **5 minutes ⏱**!
|
||||
|
||||
Use **[docusaurus.new](https://docusaurus.new)** to test Docusaurus immediately in your browser!
|
||||
|
||||
:::
|
||||
|
||||
++++tip
|
||||
|
||||
Admonition with different syntax
|
||||
|
||||
++++
|
7
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/interpolation.md
generated
Normal file
7
packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__fixtures__/interpolation.md
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
Test admonition with interpolated title/body
|
||||
|
||||
:::tip My `interpolated` **title** <button style={{color: "red"}} onClick={() => alert("click")}>test</button>
|
||||
|
||||
`body` **interpolated** <button>content</button>
|
||||
|
||||
:::
|
|
@ -0,0 +1,44 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`admonitions remark plugin base 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<admonition title="Sample Title" type="info"><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></admonition>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
|
||||
<p>++++tip</p>
|
||||
<p>Admonition with different syntax</p>
|
||||
<p>++++</p>"
|
||||
`;
|
||||
|
||||
exports[`admonitions remark plugin custom keywords 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<p>:::info Sample Title</p>
|
||||
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
|
||||
<p>:::</p>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
|
||||
<p>++++tip</p>
|
||||
<p>Admonition with different syntax</p>
|
||||
<p>++++</p>"
|
||||
`;
|
||||
|
||||
exports[`admonitions remark plugin custom tag 1`] = `
|
||||
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
|
||||
<p>:::info Sample Title</p>
|
||||
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
|
||||
<p>:::</p>
|
||||
<h2>Initial setup {#initial-setup}</h2>
|
||||
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
|
||||
<p>:::tip</p>
|
||||
<p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p>
|
||||
<p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p>
|
||||
<p>:::</p>
|
||||
<admonition type="tip"><p>Admonition with different syntax</p></admonition>"
|
||||
`;
|
||||
|
||||
exports[`admonitions remark plugin interpolation 1`] = `
|
||||
"<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>"
|
||||
`;
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import remark from 'remark';
|
||||
import remark2rehype from 'remark-rehype';
|
||||
import stringify from 'rehype-stringify';
|
||||
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '../index';
|
||||
import type {AdmonitionOptions} from '../index';
|
||||
|
||||
const processFixture = async (
|
||||
name: string,
|
||||
options?: Partial<AdmonitionOptions>,
|
||||
) => {
|
||||
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
|
||||
const file = await vfile.read(filePath);
|
||||
|
||||
const result = await remark()
|
||||
.use(plugin, options)
|
||||
.use(remark2rehype)
|
||||
.use(stringify)
|
||||
.process(file);
|
||||
|
||||
return result.toString();
|
||||
};
|
||||
|
||||
describe('admonitions remark plugin', () => {
|
||||
it('base', async () => {
|
||||
const result = await processFixture('base');
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('custom keywords', async () => {
|
||||
const result = await processFixture('base', {keywords: ['tip']});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('custom tag', async () => {
|
||||
const result = await processFixture('base', {tag: '++++'});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('interpolation', async () => {
|
||||
const result = await processFixture('interpolation');
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
182
packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts
Normal file
182
packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import visit from 'unist-util-visit';
|
||||
import type {Transformer, Processor, Plugin} from 'unified';
|
||||
import type {Literal} from 'mdast';
|
||||
|
||||
const NEWLINE = '\n';
|
||||
|
||||
export type AdmonitionOptions = {
|
||||
tag: string;
|
||||
keywords: string[];
|
||||
};
|
||||
|
||||
export const DefaultAdmonitionOptions: AdmonitionOptions = {
|
||||
tag: ':::',
|
||||
keywords: [
|
||||
'secondary',
|
||||
'info',
|
||||
'success',
|
||||
'danger',
|
||||
'note',
|
||||
'tip',
|
||||
'warning',
|
||||
'important',
|
||||
'caution',
|
||||
],
|
||||
};
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[-[\]{}()*+?.\\^$|/]/g, '\\$&');
|
||||
}
|
||||
|
||||
function normalizeOptions(
|
||||
options: Partial<AdmonitionOptions>,
|
||||
): AdmonitionOptions {
|
||||
return {...DefaultAdmonitionOptions, ...options};
|
||||
}
|
||||
|
||||
// This string value does not matter much
|
||||
// It is ignored because nodes are using hName/hProperties coming from HAST
|
||||
const admonitionNodeType = 'admonitionHTML';
|
||||
|
||||
const plugin: Plugin = function plugin(
|
||||
this: Processor,
|
||||
optionsInput: Partial<AdmonitionOptions> = {},
|
||||
): Transformer {
|
||||
const options = normalizeOptions(optionsInput);
|
||||
|
||||
const keywords = Object.values(options.keywords).map(escapeRegExp).join('|');
|
||||
const tag = escapeRegExp(options.tag);
|
||||
const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`);
|
||||
const escapeTag = new RegExp(escapeRegExp(`\\${options.tag}`), 'g');
|
||||
|
||||
// The tokenizer is called on blocks to determine if there is an admonition
|
||||
// present and create tags for it
|
||||
function blockTokenizer(this: any, eat: any, value: string, silent: boolean) {
|
||||
// Stop if no match or match does not start at beginning of line
|
||||
const match = regex.exec(value);
|
||||
if (!match || match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
// If silent return the match
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = eat.now();
|
||||
const [opening, keyword, title] = match;
|
||||
const food = [];
|
||||
const content = [];
|
||||
|
||||
let newValue = value;
|
||||
// consume lines until a closing tag
|
||||
let idx = newValue.indexOf(NEWLINE);
|
||||
while (idx !== -1) {
|
||||
// grab this line and eat it
|
||||
const next = newValue.indexOf(NEWLINE, idx + 1);
|
||||
const line =
|
||||
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;
|
||||
}
|
||||
content.push(line);
|
||||
idx = newValue.indexOf(NEWLINE);
|
||||
}
|
||||
|
||||
// consume the processed tag and replace escape sequences
|
||||
const contentString = content.join(NEWLINE).replace(escapeTag, options.tag);
|
||||
const add = eat(opening + food.join(NEWLINE));
|
||||
|
||||
// parse the content in block mode
|
||||
const exit = this.enterBlock();
|
||||
const contentNodes = this.tokenizeBlock(contentString, now);
|
||||
exit();
|
||||
|
||||
const titleNodes = this.tokenizeInline(title, now);
|
||||
|
||||
const isSimpleTextTitle =
|
||||
titleNodes.length === 1 && titleNodes[0].type === 'text';
|
||||
|
||||
const element = {
|
||||
type: admonitionNodeType,
|
||||
data: {
|
||||
// hName/hProperties come from HAST
|
||||
// See https://github.com/syntax-tree/mdast-util-to-hast#fields-on-nodes
|
||||
hName: 'admonition',
|
||||
hProperties: {
|
||||
...(title && isSimpleTextTitle && {title}),
|
||||
type: keyword,
|
||||
},
|
||||
},
|
||||
children: [
|
||||
// For titles containing MDX syntax: create a custom element. The theme
|
||||
// component will extract it and render it nicely.
|
||||
//
|
||||
// Temporary workaround, because it's complex in MDX v1 to emit
|
||||
// interpolated JSX prop syntax (title={<>my <code>title</code></>}).
|
||||
// For this reason, we use children instead of the title prop.
|
||||
title &&
|
||||
!isSimpleTextTitle && {
|
||||
type: admonitionNodeType,
|
||||
data: {
|
||||
hName: 'mdxAdmonitionTitle',
|
||||
hProperties: {},
|
||||
},
|
||||
children: titleNodes,
|
||||
},
|
||||
...contentNodes,
|
||||
].filter(Boolean),
|
||||
};
|
||||
|
||||
return add(element);
|
||||
}
|
||||
|
||||
// add tokenizer to parser after fenced code blocks
|
||||
const Parser = this.Parser.prototype;
|
||||
Parser.blockTokenizers.admonition = blockTokenizer;
|
||||
Parser.blockMethods.splice(
|
||||
Parser.blockMethods.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
'admonition',
|
||||
);
|
||||
Parser.interruptParagraph.splice(
|
||||
Parser.interruptParagraph.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
Parser.interruptList.splice(
|
||||
Parser.interruptList.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
Parser.interruptBlockquote.splice(
|
||||
Parser.interruptBlockquote.indexOf('fencedCode') + 1,
|
||||
0,
|
||||
['admonition'],
|
||||
);
|
||||
|
||||
return (root) => {
|
||||
// escape everything except admonitionHTML nodes
|
||||
visit(
|
||||
root,
|
||||
(node: unknown): node is Literal =>
|
||||
(node as Literal)?.type !== admonitionNodeType,
|
||||
(node: Literal) => {
|
||||
if (node.value) {
|
||||
node.value = node.value.replace(escapeTag, options.tag);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default plugin;
|
Loading…
Add table
Add a link
Reference in a new issue