diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js
index 814171de56d6..11cd3b0c767e 100644
--- a/packages/docusaurus-mdx-loader/src/index.js
+++ b/packages/docusaurus-mdx-loader/src/index.js
@@ -7,6 +7,7 @@
const {getOptions} = require('loader-utils');
const {readFile} = require('fs-extra');
+const path = require('path');
const mdx = require('@mdx-js/mdx');
const emoji = require('remark-emoji');
const matter = require('gray-matter');
@@ -17,9 +18,33 @@ const unwrapMdxCodeBlocks = require('./remark/unwrapMdxCodeBlocks');
const transformImage = require('./remark/transformImage');
const transformLinks = require('./remark/transformLinks');
+const customRemarkPlugins = [unwrapMdxCodeBlocks, slug, toc];
+const customRemarkPluginNames = customRemarkPlugins.map((p) => p.name);
+
const DEFAULT_OPTIONS = {
rehypePlugins: [],
- remarkPlugins: [unwrapMdxCodeBlocks, emoji, slug, toc],
+ remarkPlugins: [emoji].concat(customRemarkPlugins),
+};
+
+const resolveLocalRemarkPlugin = (plugin) => {
+ if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
+ try {
+ plugin = [
+ // eslint-disable-next-line import/no-dynamic-require
+ require(path.resolve(__dirname, `./remark/${plugin[0]}`)),
+ plugin[1],
+ ];
+ } catch (err) {
+ throw new Error(
+ `Remark plugin "${
+ plugin[0]
+ }" not found. Plugins available for configure:\n${customRemarkPluginNames.join(
+ '\n',
+ )}`,
+ );
+ }
+ }
+ return plugin;
};
module.exports = async function docusaurusMdxLoader(fileString) {
@@ -27,11 +52,24 @@ module.exports = async function docusaurusMdxLoader(fileString) {
const {data, content} = matter(fileString);
const reqOptions = getOptions(this) || {};
+
+ const userRemarkPlugins = reqOptions.remarkPlugins.map(
+ resolveLocalRemarkPlugin,
+ );
+ const overriddenDefaultRemarkPluginNames = userRemarkPlugins.flatMap((p) =>
+ Array.isArray(p) && customRemarkPluginNames.includes(p[0].name)
+ ? [p[0].name]
+ : [],
+ );
+ const defaultRemarkPlugins = DEFAULT_OPTIONS.remarkPlugins.filter(
+ (p) => !overriddenDefaultRemarkPluginNames.includes(p.name),
+ );
+
const options = {
...reqOptions,
remarkPlugins: [
...(reqOptions.beforeDefaultRemarkPlugins || []),
- ...DEFAULT_OPTIONS.remarkPlugins,
+ ...defaultRemarkPlugins,
[
transformImage,
{staticDir: reqOptions.staticDir, filePath: this.resourcePath},
@@ -40,7 +78,7 @@ module.exports = async function docusaurusMdxLoader(fileString) {
transformLinks,
{staticDir: reqOptions.staticDir, filePath: this.resourcePath},
],
- ...(reqOptions.remarkPlugins || []),
+ ...userRemarkPlugins,
],
rehypePlugins: [
...(reqOptions.beforeDefaultRehypePlugins || []),
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.js.snap b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.js.snap
index 36725f1c984c..3bcf54507212 100644
--- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.js.snap
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`inline code should be escaped 1`] = `
+exports[`toc plugin inline code should be escaped 1`] = `
"export const toc = [
{
value: '<Head />
',
@@ -42,7 +42,54 @@ exports[`inline code should be escaped 1`] = `
"
`;
-exports[`non text phrasing content 1`] = `
+exports[`toc plugin maximum deeply headings 1`] = `
+"export const toc = [
+ {
+ value: 'Bravo',
+ id: 'bravo',
+ children: [
+ {
+ value: 'Charlie',
+ id: 'charlie',
+ children: [
+ {
+ value: 'Delta',
+ id: 'delta',
+ children: [
+ {
+ value: 'Echo',
+ id: 'echo',
+ children: [
+ {
+ value: 'Foxtrot',
+ id: 'foxtrot',
+ children: []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+];
+
+# Alpha
+
+## Bravo
+
+### Charlie
+
+#### Delta
+
+##### Echo
+
+###### Foxtrot
+"
+`;
+
+exports[`toc plugin non text phrasing content 1`] = `
"export const toc = [
{
value: 'Emphasis',
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md
new file mode 100644
index 000000000000..8c0c38a7d514
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md
@@ -0,0 +1,11 @@
+# Alpha
+
+## Bravo
+
+### Charlie
+
+#### Delta
+
+##### Echo
+
+###### Foxtrot
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.js b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.js
index f55c0be063c6..d0be6182c2bd 100644
--- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.js
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.js
@@ -23,20 +23,20 @@ const processFixture = async (name, options) => {
return result.toString();
};
-
-test('non text phrasing content', async () => {
- const result = await processFixture('non-text-content');
- expect(result).toMatchSnapshot();
-});
-
-test('inline code should be escaped', async () => {
- const result = await processFixture('inline-code');
- expect(result).toMatchSnapshot();
-});
-
-test('text content', async () => {
- const result = await processFixture('just-content');
- expect(result).toMatchInlineSnapshot(`
+describe('toc plugin', () => {
+ test('non text phrasing content', async () => {
+ const result = await processFixture('non-text-content');
+ expect(result).toMatchSnapshot();
+ });
+
+ test('inline code should be escaped', async () => {
+ const result = await processFixture('inline-code');
+ expect(result).toMatchSnapshot();
+ });
+
+ test('text content', async () => {
+ const result = await processFixture('just-content');
+ expect(result).toMatchInlineSnapshot(`
"export const toc = [
{
value: 'Endi',
@@ -78,11 +78,11 @@ test('text content', async () => {
## I ♥ unicode.
"
`);
-});
+ });
-test('should export even with existing name', async () => {
- const result = await processFixture('name-exist');
- expect(result).toMatchInlineSnapshot(`
+ test('should export even with existing name', async () => {
+ const result = await processFixture('name-exist');
+ expect(result).toMatchInlineSnapshot(`
"export const toc = [
{
value: 'Thanos',
@@ -109,14 +109,14 @@ test('should export even with existing name', async () => {
### Avengers
"
`);
-});
-
-test('should export with custom name', async () => {
- const options = {
- name: 'customName',
- };
- const result = await processFixture('just-content', options);
- expect(result).toMatchInlineSnapshot(`
+ });
+
+ test('should export with custom name', async () => {
+ const options = {
+ name: 'customName',
+ };
+ const result = await processFixture('just-content', options);
+ expect(result).toMatchInlineSnapshot(`
"export const customName = [
{
value: 'Endi',
@@ -158,11 +158,11 @@ test('should export with custom name', async () => {
## I ♥ unicode.
"
`);
-});
+ });
-test('should insert below imports', async () => {
- const result = await processFixture('insert-below-imports');
- expect(result).toMatchInlineSnapshot(`
+ test('should insert below imports', async () => {
+ const result = await processFixture('insert-below-imports');
+ expect(result).toMatchInlineSnapshot(`
"import something from 'something';
import somethingElse from 'something-else';
@@ -195,11 +195,11 @@ test('should insert below imports', async () => {
Content.
"
`);
-});
+ });
-test('empty headings', async () => {
- const result = await processFixture('empty-headings');
- expect(result).toMatchInlineSnapshot(`
+ test('empty headings', async () => {
+ const result = await processFixture('empty-headings');
+ expect(result).toMatchInlineSnapshot(`
"export const toc = [];
# Ignore this
@@ -209,4 +209,10 @@ test('empty headings', async () => {
## ![](an-image.svg)
"
`);
+ });
+
+ test('maximum deeply headings', async () => {
+ const result = await processFixture('maximum-depth-6', {maxDepth: 6});
+ expect(result).toMatchSnapshot();
+ });
});
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/index.js b/packages/docusaurus-mdx-loader/src/remark/toc/index.js
index 949a7467bcab..1068ea1f5c55 100644
--- a/packages/docusaurus-mdx-loader/src/remark/toc/index.js
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.js
@@ -58,11 +58,12 @@ const getOrCreateExistingTargetIndex = (children, name) => {
return targetIndex;
};
-const plugin = (options = {}) => {
+const toc = (options = {}) => {
const name = options.name || 'toc';
+ const maxDepth = options.maxDepth || 3;
const transformer = (node) => {
- const headings = search(node);
+ const headings = search(node, maxDepth);
const {children} = node;
const targetIndex = getOrCreateExistingTargetIndex(children, name);
@@ -76,4 +77,4 @@ const plugin = (options = {}) => {
return transformer;
};
-module.exports = plugin;
+module.exports = toc;
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/search.js b/packages/docusaurus-mdx-loader/src/remark/toc/search.js
index 0821ec6d5fd0..0f7a1f075a4d 100644
--- a/packages/docusaurus-mdx-loader/src/remark/toc/search.js
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/search.js
@@ -23,17 +23,13 @@ const {toValue} = require('../utils');
* @property {StringValuedNode[]} children
*/
-// Visit all headings. We `slug` all headings (to account for
-// duplicates), but only take h2 and h3 headings.
/**
- * @param {StringValuedNode} node
+ * @param {StringValuedNode} headingNode
+ * @param {Number} maxDepth
* @returns {TOC[]}
*/
-function search(node) {
- /** @type {TOC[]} */
+function search(headingNode, maxDepth) {
const headings = [];
- let current = -1;
- let currentDepth = 0;
/**
* @param {StringValuedNode} child
@@ -44,28 +40,57 @@ function search(node) {
const onHeading = (child, index, parent) => {
const value = toString(child);
- if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
+ if (
+ parent !== headingNode ||
+ !value ||
+ child.depth > maxDepth ||
+ child.depth < 2
+ ) {
return;
}
- const entry = {
+ headings.push({
value: toValue(child),
id: child.data.id,
children: [],
- };
+ // Temporary properties
+ level: child.depth,
+ parentIndex: -1,
+ });
+ };
+
+ visit(headingNode, 'heading', onHeading);
- if (!headings.length || currentDepth >= child.depth) {
- headings.push(entry);
- current += 1;
- currentDepth = child.depth;
- } else {
- headings[current].children.push(entry);
+ const findParent = (toc, parentIndex, level) => {
+ while (parentIndex >= 0 && level < toc[parentIndex].level) {
+ parentIndex = toc[parentIndex].parentIndex;
}
+ return parentIndex >= 0 ? toc[parentIndex].parentIndex : -1;
};
- visit(node, 'heading', onHeading);
+ headings.forEach((node, index) => {
+ const prev = headings[index > 0 ? index - 1 : 0];
+ node.parentIndex =
+ node.level > prev.level
+ ? (node.parentIndex = index - 1)
+ : prev.parentIndex;
+ node.parentIndex =
+ node.level < prev.level
+ ? findParent(headings, node.parentIndex, node.level)
+ : node.parentIndex;
+ });
+
+ const rootNodeIds = [];
+ headings.forEach((node, i) => {
+ if (node.parentIndex >= 0) {
+ rootNodeIds.push(i);
+ headings[node.parentIndex].children.push(node);
+ }
+ delete node.parentIndex;
+ delete node.level;
+ });
- return headings;
+ return headings.filter((v, k) => !rootNodeIds.includes(k));
}
module.exports = search;
diff --git a/packages/docusaurus-mdx-loader/src/remark/unwrapMdxCodeBlocks/index.js b/packages/docusaurus-mdx-loader/src/remark/unwrapMdxCodeBlocks/index.js
index 23d957312e9e..c8479f992247 100644
--- a/packages/docusaurus-mdx-loader/src/remark/unwrapMdxCodeBlocks/index.js
+++ b/packages/docusaurus-mdx-loader/src/remark/unwrapMdxCodeBlocks/index.js
@@ -12,7 +12,7 @@ const visit = require('unist-util-visit');
// We wrap the JSX syntax in code blocks so that translation tools don't mess-up with the markup
// But the JSX inside such code blocks should still be evaluated as JSX
// See https://github.com/facebook/docusaurus/pull/4278
-function plugin() {
+function unwrapMdxCodeBlocks() {
const transformer = (root) => {
visit(root, 'code', (node, _index, parent) => {
if (node.lang === 'mdx-code-block') {
@@ -31,4 +31,4 @@ function plugin() {
return transformer;
}
-module.exports = plugin;
+module.exports = unwrapMdxCodeBlocks;
diff --git a/packages/docusaurus-remark-plugin-npm2yarn/src/index.js b/packages/docusaurus-remark-plugin-npm2yarn/src/index.js
index d447bb0429cb..b65e4cb8b754 100644
--- a/packages/docusaurus-remark-plugin-npm2yarn/src/index.js
+++ b/packages/docusaurus-remark-plugin-npm2yarn/src/index.js
@@ -54,7 +54,7 @@ const nodeForImport = {
"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';",
};
-module.exports = (options = {}) => {
+module.exports = function npm2Yarn(options = {}) {
const {sync = false} = options;
let transformed = false;
const transformer = (node) => {
diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts
index 8aa0abba3511..b2d009630126 100644
--- a/packages/docusaurus-utils-validation/src/validationSchemas.ts
+++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts
@@ -14,7 +14,10 @@ export const PluginIdSchema = Joi.string()
const MarkdownPluginsSchema = Joi.array()
.items(
- Joi.array().ordered(Joi.function().required(), Joi.object().required()),
+ Joi.array().ordered(
+ Joi.alternatives().try(Joi.function(), Joi.string()).required(),
+ Joi.object().required(),
+ ),
Joi.function(),
Joi.object(),
)
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index f83ea28561f8..4cdaa540954e 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -241,6 +241,7 @@ const LocaleConfigs = isI18nStaging
showLastUpdateTime: true,
remarkPlugins: [
[require('@docusaurus/remark-plugin-npm2yarn'), {sync: true}],
+ ['toc', {maxDepth: 4}],
],
disableVersioning: isVersioningDisabled,
lastVersion: isDev ? 'current' : undefined,