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,