From 3c982127d77bf19b39cefabf9a3a532a6fde24f2 Mon Sep 17 00:00:00 2001
From: Anatoly Kopyl <33553182+anatolykopyl@users.noreply.github.com>
Date: Fri, 19 Jan 2024 20:58:11 +0300
Subject: [PATCH] fix(mdx-loader): the table-of-contents should display
toc/headings of imported MDX partials (#9684)
Co-authored-by: Titus
Co-authored-by: sebastienlorber
---
packages/docusaurus-mdx-loader/package.json | 2 -
packages/docusaurus-mdx-loader/src/index.ts | 2 +-
.../__fixtures__/partials/SomeComponent.js | 5 +
.../__fixtures__/partials/_partial1.md | 7 +
.../__fixtures__/partials/_partial2-nested.md | 3 +
.../__fixtures__/partials/_partial2.mdx | 11 +
.../partials/_partial3-unused.mdx | 7 +
.../__tests__/__fixtures__/partials/index.mdx | 49 ++
.../partials/partial-used-before-import.mdx | 7 +
.../__snapshots__/index.test.ts.snap | 763 +++++++++++++-----
.../src/remark/toc/__tests__/index.test.ts | 40 +-
.../src/remark/toc/index.ts | 273 ++++---
.../src/remark/toc/types.ts | 29 +
.../src/remark/toc/utils.ts | 177 ++++
.../2021-08-21-blog-post-toc-tests.mdx | 6 +-
.../toc-partials/_first-level-partial.mdx | 7 +
.../tests/toc-partials/_partial.mdx | 19 +
.../toc-partials/_second-level-partial.mdx | 3 +
.../_docs tests/tests/toc-partials/index.mdx | 46 ++
.../_dogfooding/_docs tests/toc/toc-2-2.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-2-3.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-2-4.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-2-5.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-3-5.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-3-_.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-4-5.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-5-5.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-_-5.mdx | 6 +-
.../_dogfooding/_docs tests/toc/toc-_-_.mdx | 6 +-
.../_pages tests/page-toc-tests.mdx | 6 +-
website/community/3-contributing.mdx | 4 +-
website/docusaurus.config.ts | 17 +-
yarn.lock | 2 +-
33 files changed, 1141 insertions(+), 404 deletions(-)
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2-nested.md
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial3-unused.mdx
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/partial-used-before-import.mdx
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/types.ts
create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/_first-level-partial.mdx
create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/_partial.mdx
create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/_second-level-partial.mdx
create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/index.mdx
diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json
index 6890e0f33bfc..c15f5d073238 100644
--- a/packages/docusaurus-mdx-loader/package.json
+++ b/packages/docusaurus-mdx-loader/package.json
@@ -18,8 +18,6 @@
},
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.22.7",
- "@babel/traverse": "^7.22.8",
"@docusaurus/logger": "3.0.0",
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
diff --git a/packages/docusaurus-mdx-loader/src/index.ts b/packages/docusaurus-mdx-loader/src/index.ts
index 4a669fce6732..41fbf60da46b 100644
--- a/packages/docusaurus-mdx-loader/src/index.ts
+++ b/packages/docusaurus-mdx-loader/src/index.ts
@@ -7,7 +7,7 @@
import {mdxLoader} from './loader';
-import type {TOCItem as TOCItemImported} from './remark/toc';
+import type {TOCItem as TOCItemImported} from './remark/toc/types';
export default mdxLoader;
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js
new file mode 100644
index 000000000000..eaaa9a4fcade
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function SomeComponent() {
+ return Some component
;
+}
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md
new file mode 100644
index 000000000000..08a294ad26e4
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md
@@ -0,0 +1,7 @@
+## Partial 1
+
+Partial 1
+
+### Partial 1 Sub Heading
+
+Content
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2-nested.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2-nested.md
new file mode 100644
index 000000000000..9443eaac0087
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2-nested.md
@@ -0,0 +1,3 @@
+## Partial 2 Nested
+
+Partial 2 Nested
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx
new file mode 100644
index 000000000000..f5b6aee9a0ed
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx
@@ -0,0 +1,11 @@
+## Partial 2
+
+Partial 2
+
+### Partial 2 Sub Heading
+
+Content
+
+import Partial2Nested from './partial2-nested.md';
+
+
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial3-unused.mdx b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial3-unused.mdx
new file mode 100644
index 000000000000..da4bf2d00a39
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial3-unused.mdx
@@ -0,0 +1,7 @@
+## Partial 3
+
+Partial 3
+
+### Partial 3 Sub Heading
+
+Content
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx
new file mode 100644
index 000000000000..0742cb932124
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx
@@ -0,0 +1,49 @@
+import Partial1 from './_partial1.md';
+
+import SomeComponent from './SomeComponent';
+
+# Index
+
+Some text
+
+import Partial2 from './_partial2.md';
+
+## Index section 1
+
+Foo
+
+
+
+Some text
+
+
+
+## Index section 2
+
+
+
+## Unused partials
+
+Unused partials (that are only imported but not rendered) shouldn't alter the TOC
+
+import UnusedPartialImport from './_partial3.md';
+
+## NonExisting Partials
+
+Partials that do not exist should alter the TOC
+
+It's not the responsibility of the Remark plugin to check for their existence
+
+import DoesNotExist from './_doesNotExist.md';
+
+
+
+## Duplicate partials
+
+It's fine if we use partials at the end
+
+
+
+And we can use the partial multiple times!
+
+
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/partial-used-before-import.mdx b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/partial-used-before-import.mdx
new file mode 100644
index 000000000000..0c8c47327928
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/partial-used-before-import.mdx
@@ -0,0 +1,7 @@
+# Partial used before import
+
+While it looks weird to import after usage, this remains valid MDX usage.
+
+
+
+import Partial from './_partial.md';
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 5cad7ef21337..0a712732dc29 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
@@ -1,238 +1,601 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toc remark plugin does not overwrite TOC var if no TOC 1`] = `
-"foo
-
-\`bar\`
-
-\`\`\`js
-baz
-\`\`\`
-
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
export const toc = 1;
+function _createMdxContent(props) {
+ const _components = {
+ code: "code",
+ p: "p",
+ pre: "pre",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.p, {
+ children: "foo"
+ }), "/n", _jsx(_components.p, {
+ children: _jsx(_components.code, {
+ children: "bar"
+ })
+ }), "/n", _jsx(_components.pre, {
+ children: _jsx(_components.code, {
+ className: "language-js",
+ children: "baz/n"
+ })
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin escapes inline code 1`] = `
-"export const toc = [
- {
- value: '<Head />
',
- id: 'head-',
- level: 2
- },
- {
- value: '<Head>Test</Head>
',
- id: 'headtesthead',
- level: 3
- },
- {
- value: '<div />
',
- id: 'div-',
- level: 2
- },
- {
- value: '<div> Test </div>
',
- id: 'div-test-div',
- level: 2
- },
- {
- value: '<div><i>Test</i></div>
',
- id: 'divitestidiv',
- level: 2
- },
- {
- value: '<div><i>Test</i></div>
',
- id: 'divitestidiv-1',
- level: 2
- }
-]
-
-## \`\`
-
-### \`Test\`
-
-## \`\`
-
-## \` Test
\`
-
-## \`Test
\`
-
-## [\`Test
\`](/some/link)
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const toc = [{
+ "value": "<Head />
",
+ "id": "head-",
+ "level": 2
+}, {
+ "value": "<Head>Test</Head>
",
+ "id": "headtesthead",
+ "level": 3
+}, {
+ "value": "<div />
",
+ "id": "div-",
+ "level": 2
+}, {
+ "value": "<div> Test </div>
",
+ "id": "div-test-div",
+ "level": 2
+}, {
+ "value": "<div><i>Test</i></div>
",
+ "id": "divitestidiv",
+ "level": 2
+}, {
+ "value": "<div><i>Test</i></div>
",
+ "id": "divitestidiv-1",
+ "level": 2
+}];
+function _createMdxContent(props) {
+ const _components = {
+ a: "a",
+ code: "code",
+ h2: "h2",
+ h3: "h3",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h2, {
+ id: "head-",
+ children: _jsx(_components.code, {
+ children: ""
+ })
+ }), "/n", _jsx(_components.h3, {
+ id: "headtesthead",
+ children: _jsx(_components.code, {
+ children: "
Test"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "div-",
+ children: _jsx(_components.code, {
+ children: ""
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "div-test-div",
+ children: _jsx(_components.code, {
+ children: " Test
"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "divitestidiv",
+ children: _jsx(_components.code, {
+ children: "Test
"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "divitestidiv-1",
+ children: _jsx(_components.a, {
+ href: "/some/link",
+ children: _jsx(_components.code, {
+ children: "Test
"
+ })
+ })
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin exports even with existing name 1`] = `
-"export const toc = [
- {
- value: 'Thanos',
- id: 'thanos',
- level: 2
- },
- {
- value: 'Tony Stark',
- id: 'tony-stark',
- level: 2
- },
- {
- value: 'Avengers',
- id: 'avengers',
- level: 3
- }
-]
-
-## Thanos
-
-## Tony Stark
-
-### Avengers
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const toc = ['replaceMe'];
+function _createMdxContent(props) {
+ const _components = {
+ h2: "h2",
+ h3: "h3",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h2, {
+ id: "thanos",
+ children: "Thanos"
+ }), "/n", _jsx(_components.h2, {
+ id: "tony-stark",
+ children: "Tony Stark"
+ }), "/n", _jsx(_components.h3, {
+ id: "avengers",
+ children: "Avengers"
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin handles empty headings 1`] = `
-"export const toc = []
-
-# Ignore this
-
-##
-
-## ![](an-image.svg)
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const toc = [];
+function _createMdxContent(props) {
+ const _components = {
+ h1: "h1",
+ h2: "h2",
+ img: "img",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h1, {
+ id: "ignore-this",
+ children: "Ignore this"
+ }), "/n", _jsx(_components.h2, {
+ id: ""
+ }), "/n", _jsx(_components.h2, {
+ id: "-1",
+ children: _jsx(_components.img, {
+ src: "an-image.svg",
+ alt: ""
+ })
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin inserts below imports 1`] = `
-"import something from 'something';
-
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+import something from 'something';
import somethingElse from 'something-else';
-
-export const toc = [
- {
- value: 'Title',
- id: 'title',
- level: 2
- },
- {
- value: 'Test',
- id: 'test',
- level: 2
- },
- {
- value: 'Again',
- id: 'again',
- level: 3
- }
-]
-
-## Title
-
-## Test
-
-### Again
-
-Content.
+export const toc = [{
+ "value": "Title",
+ "id": "title",
+ "level": 2
+}, {
+ "value": "Test",
+ "id": "test",
+ "level": 2
+}, {
+ "value": "Again",
+ "id": "again",
+ "level": 3
+}];
+function _createMdxContent(props) {
+ const _components = {
+ h2: "h2",
+ h3: "h3",
+ p: "p",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h2, {
+ id: "title",
+ children: "Title"
+ }), "/n", _jsx(_components.h2, {
+ id: "test",
+ children: "Test"
+ }), "/n", _jsx(_components.h3, {
+ id: "again",
+ children: "Again"
+ }), "/n", _jsx(_components.p, {
+ children: "Content."
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin outputs empty array for no TOC 1`] = `
-"export const toc = []
-
-foo
-
-\`bar\`
-
-\`\`\`js
-baz
-\`\`\`
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const toc = [];
+function _createMdxContent(props) {
+ const _components = {
+ code: "code",
+ p: "p",
+ pre: "pre",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.p, {
+ children: "foo"
+ }), "/n", _jsx(_components.p, {
+ children: _jsx(_components.code, {
+ children: "bar"
+ })
+ }), "/n", _jsx(_components.pre, {
+ children: _jsx(_components.code, {
+ className: "language-js",
+ children: "baz/n"
+ })
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin works on non text phrasing content 1`] = `
-"export const toc = [
- {
- value: 'Emphasis',
- id: 'emphasis',
- level: 2
- },
- {
- value: 'Importance',
- id: 'importance',
- level: 3
- },
- {
- value: 'Strikethrough',
- id: 'strikethrough',
- level: 2
- },
- {
- value: 'HTML',
- id: 'html',
- level: 2
- },
- {
- value: 'inline.code()
',
- id: 'inlinecode',
- level: 2
- },
- {
- value: 'some styled heading test',
- id: 'some-styled-heading--test',
- level: 2
- }
-]
-
-## *Emphasis*
-
-### **Importance**
-
-## ~~Strikethrough~~
-
-## HTML
-
-## \`inline.code()\`
-
-## some styled heading test
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const toc = [{
+ "value": "Emphasis",
+ "id": "emphasis",
+ "level": 2
+}, {
+ "value": "Importance",
+ "id": "importance",
+ "level": 3
+}, {
+ "value": "Strikethrough",
+ "id": "strikethrough",
+ "level": 2
+}, {
+ "value": "HTML",
+ "id": "html",
+ "level": 2
+}, {
+ "value": "inline.code()
",
+ "id": "inlinecode",
+ "level": 2
+}, {
+ "value": "some styled heading test",
+ "id": "some-styled-heading--test",
+ "level": 2
+}];
+function _createMdxContent(props) {
+ const _components = {
+ code: "code",
+ del: "del",
+ em: "em",
+ h2: "h2",
+ h3: "h3",
+ strong: "strong",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h2, {
+ id: "emphasis",
+ children: _jsx(_components.em, {
+ children: "Emphasis"
+ })
+ }), "/n", _jsx(_components.h3, {
+ id: "importance",
+ children: _jsx(_components.strong, {
+ children: "Importance"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "strikethrough",
+ children: _jsx(_components.del, {
+ children: "Strikethrough"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "html",
+ children: _jsx("i", {
+ children: "HTML"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "inlinecode",
+ children: _jsx(_components.code, {
+ children: "inline.code()"
+ })
+ }), "/n", _jsxs(_components.h2, {
+ id: "some-styled-heading--test",
+ children: ["some ", _jsx("span", {
+ className: "some-class",
+ style: {
+ border: "solid"
+ },
+ children: "styled"
+ }), " ", _jsx("strong", {
+ children: "heading"
+ }), " ", _jsx("span", {
+ class: "myClass",
+ className: "myClassName <> weird char",
+ "data-random-attr": "456"
+ }), " test"]
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
exports[`toc remark plugin works on text content 1`] = `
-"export const toc = [
- {
- value: 'Endi',
- id: 'endi',
- level: 3
- },
- {
- value: 'Endi',
- id: 'endi-1',
- level: 2
- },
- {
- value: 'Yangshun',
- id: 'yangshun',
- level: 3
- },
- {
- value: 'I ♥ unicode.',
- id: 'i--unicode',
- level: 2
- }
-]
-
-### Endi
-
-\`\`\`md
-## This is ignored
-\`\`\`
-
-## Endi
-
-Lorem ipsum
-
-### Yangshun
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+export const c = 1;
+export const toc = [{
+ "value": "Endi",
+ "id": "endi",
+ "level": 3
+}, {
+ "value": "Endi",
+ "id": "endi-1",
+ "level": 2
+}, {
+ "value": "Yangshun",
+ "id": "yangshun",
+ "level": 3
+}, {
+ "value": "I ♥ unicode.",
+ "id": "i--unicode",
+ "level": 2
+}];
+function _createMdxContent(props) {
+ const _components = {
+ code: "code",
+ h2: "h2",
+ h3: "h3",
+ p: "p",
+ pre: "pre",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h3, {
+ id: "endi",
+ children: "Endi"
+ }), "/n", _jsx(_components.pre, {
+ children: _jsx(_components.code, {
+ className: "language-md",
+ children: "## This is ignored/n"
+ })
+ }), "/n", _jsx(_components.h2, {
+ id: "endi-1",
+ children: "Endi"
+ }), "/n", _jsx(_components.p, {
+ children: "Lorem ipsum"
+ }), "/n", _jsx(_components.h3, {
+ id: "yangshun",
+ children: "Yangshun"
+ }), "/n", _jsx(_components.p, {
+ children: "Some content here"
+ }), "/n", _jsx(_components.h2, {
+ id: "i--unicode",
+ children: "I ♥ unicode."
+ })]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
+"
+`;
-Some content here
+exports[`toc remark plugin works with imported markdown 1`] = `
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+import Partial1, {toc as __tocPartial1} from './_partial1.md';
+import SomeComponent from './SomeComponent';
+import Partial2, {toc as __tocPartial2} from './_partial2.md';
+import UnusedPartialImport from './_partial3.md';
+import DoesNotExist, {toc as __tocDoesNotExist} from './_doesNotExist.md';
+export const toc = [{
+ "value": "Index section 1",
+ "id": "index-section-1",
+ "level": 2
+}, ...__tocPartial1, {
+ "value": "Index section 2",
+ "id": "index-section-2",
+ "level": 2
+}, ...__tocPartial2, {
+ "value": "Unused partials",
+ "id": "unused-partials",
+ "level": 2
+}, {
+ "value": "NonExisting Partials",
+ "id": "nonexisting-partials",
+ "level": 2
+}, ...__tocDoesNotExist, {
+ "value": "Duplicate partials",
+ "id": "duplicate-partials",
+ "level": 2
+}, ...__tocPartial1, ...__tocPartial1];
+function _createMdxContent(props) {
+ const _components = {
+ h1: "h1",
+ h2: "h2",
+ p: "p",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h1, {
+ id: "index",
+ children: "Index"
+ }), "/n", _jsx(_components.p, {
+ children: "Some text"
+ }), "/n", "/n", _jsx(_components.h2, {
+ id: "index-section-1",
+ children: "Index section 1"
+ }), "/n", _jsx(_components.p, {
+ children: "Foo"
+ }), "/n", _jsx(Partial1, {}), "/n", _jsx(_components.p, {
+ children: "Some text"
+ }), "/n", _jsx(SomeComponent, {}), "/n", _jsx(_components.h2, {
+ id: "index-section-2",
+ children: "Index section 2"
+ }), "/n", _jsx(Partial2, {}), "/n", _jsx(_components.h2, {
+ id: "unused-partials",
+ children: "Unused partials"
+ }), "/n", _jsx(_components.p, {
+ children: "Unused partials (that are only imported but not rendered) shouldn't alter the TOC"
+ }), "/n", "/n", _jsx(_components.h2, {
+ id: "nonexisting-partials",
+ children: "NonExisting Partials"
+ }), "/n", _jsx(_components.p, {
+ children: "Partials that do not exist should alter the TOC"
+ }), "/n", _jsx(_components.p, {
+ children: "It's not the responsibility of the Remark plugin to check for their existence"
+ }), "/n", "/n", _jsx(DoesNotExist, {}), "/n", _jsx(_components.h2, {
+ id: "duplicate-partials",
+ children: "Duplicate partials"
+ }), "/n", _jsx(_components.p, {
+ children: "It's fine if we use partials at the end"
+ }), "/n", _jsx(Partial1, {}), "/n", _jsx(_components.p, {
+ children: "And we can use the partial multiple times!"
+ }), "/n", _jsx(Partial1, {})]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
+"
+`;
-## I ♥ unicode.
+exports[`toc remark plugin works with partial imported after its usage 1`] = `
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+import Partial, {toc as __tocPartial} from './_partial.md';
+export const toc = [...__tocPartial];
+function _createMdxContent(props) {
+ const _components = {
+ h1: "h1",
+ p: "p",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h1, {
+ id: "partial-used-before-import",
+ children: "Partial used before import"
+ }), "/n", _jsx(_components.p, {
+ children: "While it looks weird to import after usage, this remains valid MDX usage."
+ }), "/n", _jsx(Partial, {})]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
+"
+`;
-export const c = 1;
+exports[`toc remark plugin works with partials importing other partials 1`] = `
+"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
+import Partial2Nested, {toc as __tocPartial2Nested} from './partial2-nested.md';
+export const toc = [{
+ "value": "Partial 2",
+ "id": "partial-2",
+ "level": 2
+}, {
+ "value": "Partial 2 Sub Heading",
+ "id": "partial-2-sub-heading",
+ "level": 3
+}, ...__tocPartial2Nested];
+function _createMdxContent(props) {
+ const _components = {
+ h2: "h2",
+ h3: "h3",
+ p: "p",
+ ...props.components
+ };
+ return _jsxs(_Fragment, {
+ children: [_jsx(_components.h2, {
+ id: "partial-2",
+ children: "Partial 2"
+ }), "/n", _jsx(_components.p, {
+ children: "Partial 2"
+ }), "/n", _jsx(_components.h3, {
+ id: "partial-2-sub-heading",
+ children: "Partial 2 Sub Heading"
+ }), "/n", _jsx(_components.p, {
+ children: "Content"
+ }), "/n", "/n", _jsx(Partial2Nested, {})]
+ });
+}
+export default function MDXContent(props = {}) {
+ const {wrapper: MDXLayout} = props.components || ({});
+ return MDXLayout ? _jsx(MDXLayout, {
+ ...props,
+ children: _jsx(_createMdxContent, {
+ ...props
+ })
+ }) : _createMdxContent(props);
+}
"
`;
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 0468feea1d40..87bc5c6793dd 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
@@ -11,18 +11,23 @@ import plugin from '../index';
import headings from '../../headings/index';
const processFixture = async (name: string) => {
- const {remark} = await import('remark');
const {default: gfm} = await import('remark-gfm');
- const {default: mdx} = await import('remark-mdx');
- const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
+ const {compile} = await import('@mdx-js/mdx');
+
+ const filePath = path.join(
+ __dirname,
+ '__fixtures__',
+ name.endsWith('.mdx') ? name : `${name}.md`,
+ );
+
const file = await vfile.read(filePath);
- const result = await remark()
- .use(headings)
- .use(gfm)
- .use(mdx)
- .use(plugin)
- .process(file);
+
+ const result = await compile(file, {
+ format: 'mdx',
+ remarkPlugins: [headings, gfm, plugin],
+ rehypePlugins: [],
+ });
return result.value;
};
@@ -70,4 +75,21 @@ describe('toc remark plugin', () => {
const result = await processFixture('empty-headings');
expect(result).toMatchSnapshot();
});
+
+ it('works with imported markdown', async () => {
+ const result = await processFixture('partials/index.mdx');
+ expect(result).toMatchSnapshot();
+ });
+
+ it('works with partials importing other partials', async () => {
+ const result = await processFixture('partials/_partial2.mdx');
+ expect(result).toMatchSnapshot();
+ });
+
+ it('works with partial imported after its usage', async () => {
+ const result = await processFixture(
+ 'partials/partial-used-before-import.mdx',
+ );
+ 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 31081fb73b4f..e457e61bfb06 100644
--- a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts
@@ -5,154 +5,183 @@
* LICENSE file in the root directory of this source tree.
*/
-import {parse, type ParserOptions} from '@babel/parser';
-import traverse from '@babel/traverse';
-import stringifyObject from 'stringify-object';
-import {toValue} from '../utils';
-import type {Identifier} from '@babel/types';
-import type {Node, Parent} from 'unist';
-import type {Heading, Literal} from 'mdast';
+import {
+ addTocSliceImportIfNeeded,
+ createTOCExportNodeAST,
+ findDefaultImportName,
+ getImportDeclarations,
+ isMarkdownImport,
+ isNamedExport,
+} from './utils';
+import type {Heading, Root} from 'mdast';
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
import type {Transformer} from 'unified';
import type {
MdxjsEsm,
+ MdxJsxFlowElement,
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
} from 'mdast-util-mdx';
-
-// TODO as of April 2023, no way to import/re-export this ESM type easily :/
-// TODO upgrade to TS 5.3
-// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
-// import type {Plugin} from 'unified';
-type Plugin = any; // TODO fix this asap
-
-export type TOCItem = {
- readonly value: string;
- readonly id: string;
- readonly level: number;
-};
-
-const parseOptions: ParserOptions = {
- plugins: ['jsx'],
- sourceType: 'module',
-};
-
-const isImport = (child: any): child is Literal =>
- child.type === 'mdxjsEsm' && child.value.startsWith('import');
-const hasImports = (index: number) => index > -1;
-const isExport = (child: any): child is Literal =>
- child.type === 'mdxjsEsm' && child.value.startsWith('export');
+import type {TOCItems} from './types';
+import type {ImportDeclaration} from 'estree';
interface PluginOptions {
name?: string;
}
-const isTarget = (child: Literal, name: string) => {
- let found = false;
- const ast = parse(child.value, parseOptions);
- traverse(ast, {
- VariableDeclarator: (path) => {
- if ((path.node.id as Identifier).name === name) {
- found = true;
+// ComponentName (default export) => ImportDeclaration mapping
+type MarkdownImports = Map;
+
+// MdxjsEsm node representing an already existing "export const toc" declaration
+type ExistingTOCExport = MdxjsEsm | null;
+
+function createTocSliceImportName({
+ tocExportName,
+ componentName,
+}: {
+ tocExportName: string;
+ componentName: string;
+}) {
+ // The name of the toc slice import alias doesn't matter much
+ // We just need to ensure it's valid and won't conflict with other names
+ return `__${tocExportName}${componentName}`;
+}
+
+async function collectImportsExports({
+ root,
+ tocExportName,
+}: {
+ root: Root;
+ tocExportName: string;
+}): Promise<{
+ markdownImports: MarkdownImports;
+ existingTocExport: ExistingTOCExport;
+}> {
+ const {visit} = await import('unist-util-visit');
+
+ const markdownImports = new Map();
+ let existingTocExport: MdxjsEsm | null = null;
+
+ visit(root, 'mdxjsEsm', (node) => {
+ if (!node.data?.estree) {
+ return;
+ }
+ if (isNamedExport(node, tocExportName)) {
+ existingTocExport = node;
+ }
+
+ getImportDeclarations(node.data.estree).forEach((declaration) => {
+ if (!isMarkdownImport(declaration)) {
+ return;
+ }
+ const componentName = findDefaultImportName(declaration);
+ if (!componentName) {
+ return;
}
- },
+ markdownImports.set(componentName, {
+ declaration,
+ });
+ });
});
- return found;
-};
-
-const getOrCreateExistingTargetIndex = async (
- children: Node[],
- name: string,
-) => {
- let importsIndex = -1;
- let targetIndex = -1;
-
- children.forEach((child, index) => {
- if (isImport(child)) {
- importsIndex = index;
- } else if (isExport(child) && isTarget(child, name)) {
- targetIndex = index;
+
+ return {markdownImports, existingTocExport};
+}
+
+async function collectTOCItems({
+ root,
+ tocExportName,
+ markdownImports,
+}: {
+ root: Root;
+ tocExportName: string;
+ markdownImports: MarkdownImports;
+}): Promise<{
+ // The toc items we collected in the tree
+ tocItems: TOCItems;
+}> {
+ const {toString} = await import('mdast-util-to-string');
+ const {visit} = await import('unist-util-visit');
+
+ const tocItems: TOCItems = [];
+
+ visit(root, (child) => {
+ if (child.type === 'heading') {
+ visitHeading(child);
+ } else if (child.type === 'mdxJsxFlowElement') {
+ visitJSXElement(child);
}
});
- if (targetIndex === -1) {
- const target = await createExportNode(name, []);
+ return {tocItems};
- targetIndex = hasImports(importsIndex) ? importsIndex + 1 : 0;
- children.splice(targetIndex, 0, target);
+ // Visit Markdown headings
+ function visitHeading(node: Heading) {
+ const value = toString(node);
+ // depth:1 headings are titles and not included in the TOC
+ if (!value || node.depth < 2) {
+ return;
+ }
+ tocItems.push({
+ type: 'heading',
+ heading: node,
+ });
}
- return targetIndex;
-};
-
-const plugin: Plugin = function plugin(
- options: PluginOptions = {},
-): Transformer {
- const name = options.name || 'toc';
+ // Visit JSX elements, such as
+ function visitJSXElement(node: MdxJsxFlowElement) {
+ const componentName = node.name;
+ if (!componentName) {
+ return;
+ }
+ const importDeclaration = markdownImports.get(componentName)?.declaration;
+ if (!importDeclaration) {
+ return;
+ }
- return async (root) => {
- const {toString} = await import('mdast-util-to-string');
- const {visit} = await import('unist-util-visit');
+ const tocSliceImportName = createTocSliceImportName({
+ tocExportName,
+ componentName,
+ });
- const headings: TOCItem[] = [];
+ tocItems.push({
+ type: 'slice',
+ importName: tocSliceImportName,
+ });
- visit(root, 'heading', (child: Heading) => {
- const value = toString(child);
+ addTocSliceImportIfNeeded({
+ importDeclaration,
+ tocExportName,
+ tocSliceImportName,
+ });
+ }
+}
- // depth:1 headings are titles and not included in the TOC
- if (!value || child.depth < 2) {
- return;
- }
+export default function plugin(options: PluginOptions = {}): Transformer {
+ const tocExportName = options.name || 'toc';
- headings.push({
- value: toValue(child, toString),
- id: child.data!.id!,
- level: child.depth,
- });
+ return async (root) => {
+ const {markdownImports, existingTocExport} = await collectImportsExports({
+ root,
+ tocExportName,
});
- const {children} = root as Parent;
- const targetIndex = await getOrCreateExistingTargetIndex(children, name);
-
- if (headings?.length) {
- children[targetIndex] = await createExportNode(name, headings);
+ // If user explicitly writes "export const toc" in his mdx file
+ // We keep it as is do not override their explicit toc structure
+ // See https://github.com/facebook/docusaurus/pull/7530#discussion_r1458087876
+ if (existingTocExport) {
+ return;
}
- };
-};
-
-export default plugin;
-
-async function createExportNode(name: string, object: any): Promise {
- const {valueToEstree} = await import('estree-util-value-to-estree');
-
- return {
- type: 'mdxjsEsm',
- value: `export const ${name} = ${stringifyObject(object)}`,
- data: {
- estree: {
- type: 'Program',
- body: [
- {
- type: 'ExportNamedDeclaration',
- declaration: {
- type: 'VariableDeclaration',
- declarations: [
- {
- type: 'VariableDeclarator',
- id: {
- type: 'Identifier',
- name,
- },
- init: valueToEstree(object),
- },
- ],
- kind: 'const',
- },
- specifiers: [],
- source: null,
- },
- ],
- sourceType: 'module',
- },
- },
+
+ const {tocItems} = await collectTOCItems({
+ root,
+ tocExportName,
+ markdownImports,
+ });
+
+ root.children.push(
+ await createTOCExportNodeAST({
+ tocExportName,
+ tocItems,
+ }),
+ );
};
}
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts
new file mode 100644
index 000000000000..3170dabbba2d
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 type {Heading} from 'mdast';
+
+// Note: this type is exported from mdx-loader and used in theme
+// Need to keep it retro compatible
+export type TOCItem = {
+ readonly value: string;
+ readonly id: string;
+ readonly level: number;
+};
+
+export type TOCHeading = {
+ readonly type: 'heading';
+ readonly heading: Heading;
+};
+
+// A TOC slice represents a TOCItem[] imported from a partial
+export type TOCSlice = {
+ readonly type: 'slice';
+ readonly importName: string;
+};
+
+export type TOCItems = (TOCHeading | TOCSlice)[];
diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
new file mode 100644
index 000000000000..3cedf78ed095
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
@@ -0,0 +1,177 @@
+/**
+ * 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 {toValue} from '../utils';
+import type {Node} from 'unist';
+import type {
+ MdxjsEsm,
+ // @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
+} from 'mdast-util-mdx';
+import type {TOCHeading, TOCItem, TOCItems, TOCSlice} from './types';
+import type {
+ Program,
+ SpreadElement,
+ ImportDeclaration,
+ ImportSpecifier,
+} from 'estree';
+
+export function getImportDeclarations(program: Program): ImportDeclaration[] {
+ return program.body.filter(
+ (item): item is ImportDeclaration => item.type === 'ImportDeclaration',
+ );
+}
+
+export function isMarkdownImport(node: Node): node is ImportDeclaration {
+ if (node.type !== 'ImportDeclaration') {
+ return false;
+ }
+ const importPath = (node as ImportDeclaration).source.value;
+ return typeof importPath === 'string' && /\.mdx?$/.test(importPath);
+}
+
+export function findDefaultImportName(
+ importDeclaration: ImportDeclaration,
+): string | undefined {
+ return importDeclaration.specifiers.find(
+ (o: Node) => o.type === 'ImportDefaultSpecifier',
+ )?.local.name;
+}
+
+export function findNamedImportSpecifier(
+ importDeclaration: ImportDeclaration,
+ localName: string,
+): ImportSpecifier | undefined {
+ return importDeclaration?.specifiers.find(
+ (specifier): specifier is ImportSpecifier =>
+ specifier.type === 'ImportSpecifier' &&
+ specifier.local.name === localName,
+ );
+}
+
+// Before: import Partial from "partial"
+// After: import Partial, {toc as __tocPartial} from "partial"
+export function addTocSliceImportIfNeeded({
+ importDeclaration,
+ tocExportName,
+ tocSliceImportName,
+}: {
+ importDeclaration: ImportDeclaration;
+ tocExportName: string;
+ tocSliceImportName: string;
+}): void {
+ // We only add the toc slice named import if it doesn't exist already
+ if (!findNamedImportSpecifier(importDeclaration, tocSliceImportName)) {
+ importDeclaration.specifiers.push({
+ type: 'ImportSpecifier',
+ imported: {type: 'Identifier', name: tocExportName},
+ local: {type: 'Identifier', name: tocSliceImportName},
+ });
+ }
+}
+
+export function isNamedExport(
+ node: Node,
+ exportName: string,
+): node is MdxjsEsm {
+ if (node.type !== 'mdxjsEsm') {
+ return false;
+ }
+ const program = (node as MdxjsEsm).data?.estree;
+ if (!program) {
+ return false;
+ }
+ if (program.body.length !== 1) {
+ return false;
+ }
+ const exportDeclaration = program.body[0]!;
+ if (exportDeclaration.type !== 'ExportNamedDeclaration') {
+ return false;
+ }
+ const variableDeclaration = exportDeclaration.declaration;
+ if (variableDeclaration?.type !== 'VariableDeclaration') {
+ return false;
+ }
+ const {id} = variableDeclaration.declarations[0]!;
+ if (id.type !== 'Identifier') {
+ return false;
+ }
+
+ return id.name === exportName;
+}
+
+export async function createTOCExportNodeAST({
+ tocExportName,
+ tocItems,
+}: {
+ tocExportName: string;
+ tocItems: TOCItems;
+}): Promise {
+ function createTOCSliceAST(tocSlice: TOCSlice): SpreadElement {
+ return {
+ type: 'SpreadElement',
+ argument: {type: 'Identifier', name: tocSlice.importName},
+ };
+ }
+
+ async function createTOCHeadingAST({heading}: TOCHeading) {
+ const {toString} = await import('mdast-util-to-string');
+ const {valueToEstree} = await import('estree-util-value-to-estree');
+ const value: TOCItem = {
+ value: toValue(heading, toString),
+ id: heading.data!.id!,
+ level: heading.depth,
+ };
+ return valueToEstree(value);
+ }
+
+ async function createTOCItemAST(tocItem: TOCItems[number]) {
+ switch (tocItem.type) {
+ case 'slice':
+ return createTOCSliceAST(tocItem);
+ case 'heading':
+ return createTOCHeadingAST(tocItem);
+ default: {
+ throw new Error(`unexpected toc item type`);
+ }
+ }
+ }
+
+ return {
+ type: 'mdxjsEsm',
+ value: '', // See https://github.com/facebook/docusaurus/pull/9684#discussion_r1457595181
+ data: {
+ estree: {
+ type: 'Program',
+ body: [
+ {
+ type: 'ExportNamedDeclaration',
+ declaration: {
+ type: 'VariableDeclaration',
+ declarations: [
+ {
+ type: 'VariableDeclarator',
+ id: {
+ type: 'Identifier',
+ name: tocExportName,
+ },
+ init: {
+ type: 'ArrayExpression',
+ elements: await Promise.all(tocItems.map(createTOCItemAST)),
+ },
+ },
+ ],
+ kind: 'const',
+ },
+ specifiers: [],
+ source: null,
+ },
+ ],
+ sourceType: 'module',
+ },
+ },
+ };
+}
diff --git a/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx b/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx
index fe66256a7427..0d5ae4defdfe 100644
--- a/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx
+++ b/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx
@@ -9,10 +9,6 @@ tags: [paginated-tag]
{/* truncate */}
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/_first-level-partial.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/_first-level-partial.mdx
new file mode 100644
index 000000000000..2cd6eba6c057
--- /dev/null
+++ b/website/_dogfooding/_docs tests/tests/toc-partials/_first-level-partial.mdx
@@ -0,0 +1,7 @@
+import SecondLevelPartial from './_second-level-partial.mdx';
+
+## 1st level partial
+
+I'm 1 level deep.
+
+
diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/_partial.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/_partial.mdx
new file mode 100644
index 000000000000..168e302bf488
--- /dev/null
+++ b/website/_dogfooding/_docs tests/tests/toc-partials/_partial.mdx
@@ -0,0 +1,19 @@
+## Partial
+
+Partial intro
+
+### Partial Sub Heading 1
+
+Partial Sub Heading 1 content
+
+#### Partial Sub Sub Heading 1
+
+Partial Sub Sub Heading 1 content
+
+### Partial Sub Heading 2
+
+Partial Sub Heading 2 content
+
+#### Partial Sub Sub Heading 2
+
+Partial Sub Sub Heading 2 content
diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/_second-level-partial.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/_second-level-partial.mdx
new file mode 100644
index 000000000000..279cc7d74365
--- /dev/null
+++ b/website/_dogfooding/_docs tests/tests/toc-partials/_second-level-partial.mdx
@@ -0,0 +1,3 @@
+### 2nd level partial
+
+I'm 2 levels deep.
diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/index.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/index.mdx
new file mode 100644
index 000000000000..8e608d78ed16
--- /dev/null
+++ b/website/_dogfooding/_docs tests/tests/toc-partials/index.mdx
@@ -0,0 +1,46 @@
+import Partial from './_partial.mdx';
+
+# TOC partial test
+
+This page tests that MDX-imported content appears correctly in the table-of-contents
+
+See also:
+
+- https://github.com/facebook/docusaurus/issues/3915
+- https://github.com/facebook/docusaurus/pull/9684
+
+---
+
+**The table of contents should include headings of this partial**:
+
+
+
+---
+
+**We can import the same partial using a different name and it still works**:
+
+import WeirdLocalName from './_partial.mdx';
+
+
+
+---
+
+**We can import a partial and not use it, the TOC remains unaffected**:
+
+import UnusedPartial from './_partial.mdx';
+
+---
+
+import FirstLevelPartial from './_first-level-partial.mdx';
+
+**It also works for partials importing other partials**
+
+
+
+---
+
+**And we can even use the same partial twice!**
+
+**(although it's useless and not particularly recommended because headings will have the same ids)**
+
+
diff --git a/website/_dogfooding/_docs tests/toc/toc-2-2.mdx b/website/_dogfooding/_docs tests/toc/toc-2-2.mdx
index 09a3b607bfaa..0a96916359fb 100644
--- a/website/_dogfooding/_docs tests/toc/toc-2-2.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-2-2.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 2
toc_max_heading_level: 2
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-2-3.mdx b/website/_dogfooding/_docs tests/toc/toc-2-3.mdx
index 3a7e3597595d..0839b1081b11 100644
--- a/website/_dogfooding/_docs tests/toc/toc-2-3.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-2-3.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 2
toc_max_heading_level: 3
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-2-4.mdx b/website/_dogfooding/_docs tests/toc/toc-2-4.mdx
index 4aa86132bc31..2fc1e4ad3904 100644
--- a/website/_dogfooding/_docs tests/toc/toc-2-4.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-2-4.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 2
toc_max_heading_level: 4
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-2-5.mdx b/website/_dogfooding/_docs tests/toc/toc-2-5.mdx
index ccba840f4d1e..e8c995d1ffd1 100644
--- a/website/_dogfooding/_docs tests/toc/toc-2-5.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-2-5.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 2
toc_max_heading_level: 5
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-3-5.mdx b/website/_dogfooding/_docs tests/toc/toc-3-5.mdx
index b88fb957226f..94eec4cfd589 100644
--- a/website/_dogfooding/_docs tests/toc/toc-3-5.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-3-5.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 3
toc_max_heading_level: 5
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-3-_.mdx b/website/_dogfooding/_docs tests/toc/toc-3-_.mdx
index 11b0acb4e4b6..62e8aacdd473 100644
--- a/website/_dogfooding/_docs tests/toc/toc-3-_.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-3-_.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 3
# toc_max_heading_level:
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-4-5.mdx b/website/_dogfooding/_docs tests/toc/toc-4-5.mdx
index 446a08c83759..2a279c48e537 100644
--- a/website/_dogfooding/_docs tests/toc/toc-4-5.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-4-5.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 4
toc_max_heading_level: 5
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-5-5.mdx b/website/_dogfooding/_docs tests/toc/toc-5-5.mdx
index 274a056d7d3e..22f0d122bcf0 100644
--- a/website/_dogfooding/_docs tests/toc/toc-5-5.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-5-5.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 5
toc_max_heading_level: 5
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-_-5.mdx b/website/_dogfooding/_docs tests/toc/toc-_-5.mdx
index 611dea4f163b..eb2eb05e78eb 100644
--- a/website/_dogfooding/_docs tests/toc/toc-_-5.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-_-5.mdx
@@ -3,10 +3,6 @@
toc_max_heading_level: 5
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_docs tests/toc/toc-_-_.mdx b/website/_dogfooding/_docs tests/toc/toc-_-_.mdx
index 9fa20b9e2822..0418b85b5864 100644
--- a/website/_dogfooding/_docs tests/toc/toc-_-_.mdx
+++ b/website/_dogfooding/_docs tests/toc/toc-_-_.mdx
@@ -3,10 +3,6 @@
# toc_max_heading_level:
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/_dogfooding/_pages tests/page-toc-tests.mdx b/website/_dogfooding/_pages tests/page-toc-tests.mdx
index 4aa86132bc31..2fc1e4ad3904 100644
--- a/website/_dogfooding/_pages tests/page-toc-tests.mdx
+++ b/website/_dogfooding/_pages tests/page-toc-tests.mdx
@@ -3,10 +3,6 @@ toc_min_heading_level: 2
toc_max_heading_level: 4
---
-import Content, {
- toc as ContentToc,
-} from '@site/_dogfooding/_partials/toc-tests.mdx';
+import Content from '@site/_dogfooding/_partials/toc-tests.mdx';
-
-export const toc = ContentToc;
diff --git a/website/community/3-contributing.mdx b/website/community/3-contributing.mdx
index 985859d34cbd..5ea9c6660352 100644
--- a/website/community/3-contributing.mdx
+++ b/website/community/3-contributing.mdx
@@ -5,9 +5,7 @@ sidebar_label: Contributing
---
```mdx-code-block
-import Contributing, {toc as ContributingTOC} from "@site/../CONTRIBUTING.md"
+import Contributing from "@site/../CONTRIBUTING.md"
-
-export const toc = ContributingTOC;
```
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index 99fc07c30732..6599b6fb0744 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -190,16 +190,21 @@ export default async function createConfigAsync() {
preprocessor: ({filePath, fileContent}) => {
let result = fileContent;
+ // This fixes Crowdin bug altering MDX comments on i18n sites...
+ // https://github.com/facebook/docusaurus/pull/9220
result = result.replaceAll('{/_', '{/*');
result = result.replaceAll('_/}', '*/}');
if (isDev) {
- // "vscode://file/${projectPath}${filePath}:${line}:${column}",
- // "webstorm://open?file=${projectPath}${filePath}&line=${line}&column=${column}",
- const vscodeLink = `vscode://file/${filePath}`;
- const webstormLink = `webstorm://open?file=${filePath}`;
- const intellijLink = `idea://open?file=${filePath}`;
- result = `${result}\n\n---\n\n**DEV**: open this file in [VSCode](<${vscodeLink}>) | [WebStorm](<${webstormLink}>) | [IntelliJ](<${intellijLink}>)\n`;
+ const isPartial = path.basename(filePath).startsWith('_');
+ if (!isPartial) {
+ // "vscode://file/${projectPath}${filePath}:${line}:${column}",
+ // "webstorm://open?file=${projectPath}${filePath}&line=${line}&column=${column}",
+ const vscodeLink = `vscode://file/${filePath}`;
+ const webstormLink = `webstorm://open?file=${filePath}`;
+ const intellijLink = `idea://open?file=${filePath}`;
+ result = `${result}\n\n---\n\n**DEV**: open this file in [VSCode](<${vscodeLink}>) | [WebStorm](<${webstormLink}>) | [IntelliJ](<${intellijLink}>)\n`;
+ }
}
return result;
diff --git a/yarn.lock b/yarn.lock
index be5e72d46ad8..c7ec91226c52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -434,7 +434,7 @@
chalk "^2.4.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.7", "@babel/parser@^7.23.3":
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.3.tgz#0ce0be31a4ca4f1884b5786057cadcb6c3be58f9"
integrity sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==