diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/_partial.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/_partial.md new file mode 100644 index 000000000000..1b8c899d5217 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/_partial.md @@ -0,0 +1,3 @@ +## Tony Stark + +Bar diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/imported-markdown.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/imported-markdown.md new file mode 100644 index 000000000000..0f1bcdba1bff --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/imported-markdown.md @@ -0,0 +1,7 @@ +import Partial from './_partial.md'; + +## Thanos + +Foo + + diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap index ba71805a2ab5..a7deecc865c1 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap @@ -229,3 +229,24 @@ Some content here export const c = 1; " `; + +exports[`toc remark plugin works with imported markdown 1`] = ` +"import Partial from './_partial.md'; +import {toc as toc0} from './_partial.md'; + +export const toc = [ + { + value: 'Thanos', + id: 'thanos', + level: 2 + }, + ...toc0 +]; + +## Thanos + +Foo + + +" +`; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts index 6df3e94e672b..f33f4753f9ff 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts @@ -67,4 +67,9 @@ describe('toc remark plugin', () => { const result = await processFixture('empty-headings'); expect(result).toMatchSnapshot(); }); + + it('works with imported markdown', async () => { + const result = await processFixture('imported-markdown'); + expect(result).toMatchSnapshot(); + }); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts index 5e6c694dc339..b0545b2aeaad 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts @@ -7,15 +7,14 @@ import {parse, type ParserOptions} from '@babel/parser'; import traverse from '@babel/traverse'; -import stringifyObject from 'stringify-object'; import toString from 'mdast-util-to-string'; import visit from 'unist-util-visit'; -import {toValue} from '../utils'; +import {constructArrayString, toValue} from '../utils'; import type {Identifier} from '@babel/types'; import type {TOCItem} from '../..'; import type {Node, Parent} from 'unist'; -import type {Heading, Literal} from 'mdast'; +import type {Heading, Literal, Text} from 'mdast'; import type {Transformer} from 'unified'; const parseOptions: ParserOptions = { @@ -68,31 +67,72 @@ const getOrCreateExistingTargetIndex = (children: Node[]) => { return targetIndex; }; +const removeTags = (input: string) => + input.replace('<', '').replace('/>', '').trim(); + export default function plugin(): Transformer { return (root) => { - const headings: TOCItem[] = []; + let importsCount = 0; + const headings: (TOCItem | string)[] = []; + + const PartialComponentToHeadingsName = Object.create(null); + + visit(root, ['heading', 'jsx', 'import'], (child, index, parent) => { + if (child.type === 'heading') { + const headingNode = child as Heading; + const value = toString(headingNode); + + // depth: 1 headings are titles and not included in the TOC + if (parent !== root || !value || headingNode.depth < 2) { + return; + } + + headings.push({ + value: toValue(headingNode), + id: headingNode.data!.id as string, + level: headingNode.depth, + }); + } + + if (child.type === 'import') { + const importNode = child as Text; + + const markdownExtensionRegex = /\.(?:mdx|md).;?$/; + const imports = importNode.value + .split('\n') + .filter((statement) => markdownExtensionRegex.test(statement)); - visit(root, 'heading', (child: Heading, index, parent) => { - const value = toString(child); + for (const importStatement of imports) { + const localName = `${name}${importsCount}`; - // depth: 1 headings are titles and not included in the TOC - if (parent !== root || !value || child.depth < 2) { - return; + const importWords = importStatement!.split(' '); + const partialPath = importWords[importWords.length - 1]; + const partialName = importWords[1] as string; + const tocImport = `import {${name} as ${localName}} from ${partialPath}`; + + PartialComponentToHeadingsName[partialName] = localName; + + importNode.value = `${importNode.value}\n${tocImport}`; + importsCount += 1; + } } - headings.push({ - value: toValue(child), - id: child.data!.id as string, - level: child.depth, - }); + if (child.type === 'jsx') { + const jsxNode = child as Text; + + const componentName = removeTags(jsxNode.value); + const headingsName = PartialComponentToHeadingsName[componentName]; + if (headingsName) { + headings.push(`...${headingsName}`); + } + } }); const {children} = root as Parent; const targetIndex = getOrCreateExistingTargetIndex(children); if (headings.length) { - children[targetIndex]!.value = `export const ${name} = ${stringifyObject( - headings, - )};`; + const headingsArray = constructArrayString(headings); + children[targetIndex]!.value = `export const ${name} = ${headingsArray};`; } }; } diff --git a/packages/docusaurus-mdx-loader/src/remark/utils/index.ts b/packages/docusaurus-mdx-loader/src/remark/utils/index.ts index 3bf275085bf5..dda06d578aa5 100644 --- a/packages/docusaurus-mdx-loader/src/remark/utils/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/utils/index.ts @@ -7,6 +7,7 @@ import escapeHtml from 'escape-html'; import toString from 'mdast-util-to-string'; +import stringifyObject from 'stringify-object'; import type {Parent} from 'unist'; import type {PhrasingContent, Heading} from 'mdast'; @@ -34,3 +35,25 @@ export function toValue(node: PhrasingContent | Heading): string { return toString(node); } } + +/** + * Similar to stringify-object, but keeps spread operators, + * instead of turning them into strings. + * @param objects + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function constructArrayString(objects: any[]): string { + let result = '['; + for (const obj of objects) { + if (typeof obj === 'string') { + result = `${result}\n\t${obj},`; + } else { + result = `${result}\n\t${stringifyObject(obj).replace(/\n/g, '\n\t')},`; + } + } + // Remove trailing coma + result = result.replace(/,$/, ''); + result += '\n]'; + + return result; +} diff --git a/website/_dogfooding/_pages tests/_anotherPagePartial.md b/website/_dogfooding/_pages tests/_anotherPagePartial.md new file mode 100644 index 000000000000..c5bb559a9b60 --- /dev/null +++ b/website/_dogfooding/_pages tests/_anotherPagePartial.md @@ -0,0 +1,7 @@ +### Another page partial content + +This is text coming from another page partial + +#### Foo + +Level 4 headings don't belong in ToC diff --git a/website/_dogfooding/_pages tests/index.md b/website/_dogfooding/_pages tests/index.md index a81c9ad79d61..2d8cd4771896 100644 --- a/website/_dogfooding/_pages tests/index.md +++ b/website/_dogfooding/_pages tests/index.md @@ -31,4 +31,5 @@ import Readme from "../README.md" - [Tabs tests](/tests/pages/tabs-tests) - [z-index tests](/tests/pages/z-index-tests) - [Head metadata tests](/tests/pages/head-metadata) +- [Partials tests](/tests/pages/partials-tests) - [Embeds](/tests/pages/embeds) diff --git a/website/_dogfooding/_pages tests/partials-tests.mdx b/website/_dogfooding/_pages tests/partials-tests.mdx new file mode 100644 index 000000000000..a9d5da82d829 --- /dev/null +++ b/website/_dogfooding/_pages tests/partials-tests.mdx @@ -0,0 +1,12 @@ +import PagePartial from './_pagePartial.md'; +import AnotherPagePartial from './_anotherPagePartial.md'; + +# Partials tests + +This page consists of multiple files imported into one. Notice how the table of contents works even for imported headings. + +## Imported content + + + +