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
+
+
+
+