mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 10:48:05 +02:00
fix(mdx-loader): the table-of-contents should display toc/headings of imported MDX partials (#9684)
Co-authored-by: Titus <tituswormer@gmail.com> Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
bed11f62bc
commit
3c982127d7
33 changed files with 1145 additions and 408 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
5
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js
generated
Normal file
5
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/SomeComponent.js
generated
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SomeComponent() {
|
||||
return <div>Some component</div>;
|
||||
}
|
7
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md
generated
Normal file
7
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial1.md
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
## Partial 1
|
||||
|
||||
Partial 1
|
||||
|
||||
### Partial 1 Sub Heading
|
||||
|
||||
Content
|
|
@ -0,0 +1,3 @@
|
|||
## Partial 2 Nested
|
||||
|
||||
Partial 2 Nested
|
11
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx
generated
Normal file
11
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/_partial2.mdx
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
## Partial 2
|
||||
|
||||
Partial 2
|
||||
|
||||
### Partial 2 Sub Heading
|
||||
|
||||
Content
|
||||
|
||||
import Partial2Nested from './partial2-nested.md';
|
||||
|
||||
<Partial2Nested />
|
|
@ -0,0 +1,7 @@
|
|||
## Partial 3
|
||||
|
||||
Partial 3
|
||||
|
||||
### Partial 3 Sub Heading
|
||||
|
||||
Content
|
49
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx
generated
Normal file
49
packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/partials/index.mdx
generated
Normal file
|
@ -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
|
||||
|
||||
<Partial1 />
|
||||
|
||||
Some text
|
||||
|
||||
<SomeComponent />
|
||||
|
||||
## Index section 2
|
||||
|
||||
<Partial2 />
|
||||
|
||||
## 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';
|
||||
|
||||
<DoesNotExist />
|
||||
|
||||
## Duplicate partials
|
||||
|
||||
It's fine if we use partials at the end
|
||||
|
||||
<Partial1 />
|
||||
|
||||
And we can use the partial multiple times!
|
||||
|
||||
<Partial1 />
|
|
@ -0,0 +1,7 @@
|
|||
# Partial used before import
|
||||
|
||||
While it looks weird to import after usage, this remains valid MDX usage.
|
||||
|
||||
<Partial />
|
||||
|
||||
import Partial from './_partial.md';
|
|
@ -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: '<code><Head /></code>',
|
||||
id: 'head-',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<code><Head>Test</Head></code>',
|
||||
id: 'headtesthead',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
value: '<code><div /></code>',
|
||||
id: 'div-',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<code><div> Test </div></code>',
|
||||
id: 'div-test-div',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<code><div><i>Test</i></div></code>',
|
||||
id: 'divitestidiv',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<code><div><i>Test</i></div></code>',
|
||||
id: 'divitestidiv-1',
|
||||
level: 2
|
||||
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
|
||||
export const toc = [{
|
||||
"value": "<code><Head /></code>",
|
||||
"id": "head-",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<code><Head>Test</Head></code>",
|
||||
"id": "headtesthead",
|
||||
"level": 3
|
||||
}, {
|
||||
"value": "<code><div /></code>",
|
||||
"id": "div-",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<code><div> Test </div></code>",
|
||||
"id": "div-test-div",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<code><div><i>Test</i></div></code>",
|
||||
"id": "divitestidiv",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<code><div><i>Test</i></div></code>",
|
||||
"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: "<Head />"
|
||||
})
|
||||
}), "/n", _jsx(_components.h3, {
|
||||
id: "headtesthead",
|
||||
children: _jsx(_components.code, {
|
||||
children: "<Head>Test</Head>"
|
||||
})
|
||||
}), "/n", _jsx(_components.h2, {
|
||||
id: "div-",
|
||||
children: _jsx(_components.code, {
|
||||
children: "<div />"
|
||||
})
|
||||
}), "/n", _jsx(_components.h2, {
|
||||
id: "div-test-div",
|
||||
children: _jsx(_components.code, {
|
||||
children: "<div> Test </div>"
|
||||
})
|
||||
}), "/n", _jsx(_components.h2, {
|
||||
id: "divitestidiv",
|
||||
children: _jsx(_components.code, {
|
||||
children: "<div><i>Test</i></div>"
|
||||
})
|
||||
}), "/n", _jsx(_components.h2, {
|
||||
id: "divitestidiv-1",
|
||||
children: _jsx(_components.a, {
|
||||
href: "/some/link",
|
||||
children: _jsx(_components.code, {
|
||||
children: "<div><i>Test</i></div>"
|
||||
})
|
||||
})
|
||||
})]
|
||||
});
|
||||
}
|
||||
export default function MDXContent(props = {}) {
|
||||
const {wrapper: MDXLayout} = props.components || ({});
|
||||
return MDXLayout ? _jsx(MDXLayout, {
|
||||
...props,
|
||||
children: _jsx(_createMdxContent, {
|
||||
...props
|
||||
})
|
||||
}) : _createMdxContent(props);
|
||||
}
|
||||
]
|
||||
|
||||
## \`<Head />\`
|
||||
|
||||
### \`<Head>Test</Head>\`
|
||||
|
||||
## \`<div />\`
|
||||
|
||||
## \`<div> Test </div>\`
|
||||
|
||||
## \`<div><i>Test</i></div>\`
|
||||
|
||||
## [\`<div><i>Test</i></div>\`](/some/link)
|
||||
"
|
||||
`;
|
||||
|
||||
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
|
||||
"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);
|
||||
}
|
||||
]
|
||||
|
||||
## Thanos
|
||||
|
||||
## Tony Stark
|
||||
|
||||
### Avengers
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc remark plugin handles empty headings 1`] = `
|
||||
"export const toc = []
|
||||
|
||||
# Ignore this
|
||||
|
||||
##
|
||||
|
||||
## 
|
||||
"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
|
||||
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);
|
||||
}
|
||||
]
|
||||
|
||||
## Title
|
||||
|
||||
## Test
|
||||
|
||||
### Again
|
||||
|
||||
Content.
|
||||
"
|
||||
`;
|
||||
|
||||
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: '<em>Emphasis</em>',
|
||||
id: 'emphasis',
|
||||
level: 2
|
||||
"import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
|
||||
export const toc = [{
|
||||
"value": "<em>Emphasis</em>",
|
||||
"id": "emphasis",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<strong>Importance</strong>",
|
||||
"id": "importance",
|
||||
"level": 3
|
||||
}, {
|
||||
"value": "<del>Strikethrough</del>",
|
||||
"id": "strikethrough",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<i>HTML</i>",
|
||||
"id": "html",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "<code>inline.code()</code>",
|
||||
"id": "inlinecode",
|
||||
"level": 2
|
||||
}, {
|
||||
"value": "some <span class=\\"some-class\\">styled</span> <strong>heading</strong> <span class=\\"myClassName <> weird char\\"></span> 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"
|
||||
},
|
||||
{
|
||||
value: '<strong>Importance</strong>',
|
||||
id: 'importance',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
value: '<del>Strikethrough</del>',
|
||||
id: 'strikethrough',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<i>HTML</i>',
|
||||
id: 'html',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: '<code>inline.code()</code>',
|
||||
id: 'inlinecode',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
value: 'some <span class="some-class">styled</span> <strong>heading</strong> <span class="myClassName <> weird char"></span> test',
|
||||
id: 'some-styled-heading--test',
|
||||
level: 2
|
||||
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);
|
||||
}
|
||||
]
|
||||
|
||||
## *Emphasis*
|
||||
|
||||
### **Importance**
|
||||
|
||||
## ~~Strikethrough~~
|
||||
|
||||
## <i>HTML</i>
|
||||
|
||||
## \`inline.code()\`
|
||||
|
||||
## some <span className="some-class" style={{border: "solid"}}>styled</span> <strong>heading</strong> <span class="myClass" className="myClassName <> weird char" data-random-attr="456" /> test
|
||||
"
|
||||
`;
|
||||
|
||||
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
|
||||
|
||||
Some content here
|
||||
|
||||
## I ♥ unicode.
|
||||
|
||||
"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);
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<string, {declaration: ImportDeclaration}>;
|
||||
|
||||
// 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<string, {declaration: ImportDeclaration}>();
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetIndex === -1) {
|
||||
const target = await createExportNode(name, []);
|
||||
|
||||
targetIndex = hasImports(importsIndex) ? importsIndex + 1 : 0;
|
||||
children.splice(targetIndex, 0, target);
|
||||
return {markdownImports, existingTocExport};
|
||||
}
|
||||
|
||||
return targetIndex;
|
||||
};
|
||||
|
||||
const plugin: Plugin = function plugin(
|
||||
options: PluginOptions = {},
|
||||
): Transformer {
|
||||
const name = options.name || 'toc';
|
||||
|
||||
return async (root) => {
|
||||
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 headings: TOCItem[] = [];
|
||||
const tocItems: TOCItems = [];
|
||||
|
||||
visit(root, 'heading', (child: Heading) => {
|
||||
const value = toString(child);
|
||||
visit(root, (child) => {
|
||||
if (child.type === 'heading') {
|
||||
visitHeading(child);
|
||||
} else if (child.type === 'mdxJsxFlowElement') {
|
||||
visitJSXElement(child);
|
||||
}
|
||||
});
|
||||
|
||||
return {tocItems};
|
||||
|
||||
// Visit Markdown headings
|
||||
function visitHeading(node: Heading) {
|
||||
const value = toString(node);
|
||||
// depth:1 headings are titles and not included in the TOC
|
||||
if (!value || child.depth < 2) {
|
||||
if (!value || node.depth < 2) {
|
||||
return;
|
||||
}
|
||||
tocItems.push({
|
||||
type: 'heading',
|
||||
heading: node,
|
||||
});
|
||||
}
|
||||
|
||||
// Visit JSX elements, such as <Partial/>
|
||||
function visitJSXElement(node: MdxJsxFlowElement) {
|
||||
const componentName = node.name;
|
||||
if (!componentName) {
|
||||
return;
|
||||
}
|
||||
const importDeclaration = markdownImports.get(componentName)?.declaration;
|
||||
if (!importDeclaration) {
|
||||
return;
|
||||
}
|
||||
|
||||
headings.push({
|
||||
value: toValue(child, toString),
|
||||
id: child.data!.id!,
|
||||
level: child.depth,
|
||||
});
|
||||
const tocSliceImportName = createTocSliceImportName({
|
||||
tocExportName,
|
||||
componentName,
|
||||
});
|
||||
|
||||
const {children} = root as Parent;
|
||||
const targetIndex = await getOrCreateExistingTargetIndex(children, name);
|
||||
tocItems.push({
|
||||
type: 'slice',
|
||||
importName: tocSliceImportName,
|
||||
});
|
||||
|
||||
if (headings?.length) {
|
||||
children[targetIndex] = await createExportNode(name, headings);
|
||||
addTocSliceImportIfNeeded({
|
||||
importDeclaration,
|
||||
tocExportName,
|
||||
tocSliceImportName,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
export default function plugin(options: PluginOptions = {}): Transformer<Root> {
|
||||
const tocExportName = options.name || 'toc';
|
||||
|
||||
async function createExportNode(name: string, object: any): Promise<MdxjsEsm> {
|
||||
const {valueToEstree} = await import('estree-util-value-to-estree');
|
||||
return async (root) => {
|
||||
const {markdownImports, existingTocExport} = await collectImportsExports({
|
||||
root,
|
||||
tocExportName,
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
// 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;
|
||||
}
|
||||
|
||||
const {tocItems} = await collectTOCItems({
|
||||
root,
|
||||
tocExportName,
|
||||
markdownImports,
|
||||
});
|
||||
|
||||
root.children.push(
|
||||
await createTOCExportNodeAST({
|
||||
tocExportName,
|
||||
tocItems,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
29
packages/docusaurus-mdx-loader/src/remark/toc/types.ts
Normal file
29
packages/docusaurus-mdx-loader/src/remark/toc/types.ts
Normal file
|
@ -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)[];
|
177
packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
Normal file
177
packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
Normal file
|
@ -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<MdxjsEsm> {
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import SecondLevelPartial from './_second-level-partial.mdx';
|
||||
|
||||
## 1st level partial
|
||||
|
||||
I'm 1 level deep.
|
||||
|
||||
<SecondLevelPartial />
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
### 2nd level partial
|
||||
|
||||
I'm 2 levels deep.
|
46
website/_dogfooding/_docs tests/tests/toc-partials/index.mdx
Normal file
46
website/_dogfooding/_docs tests/tests/toc-partials/index.mdx
Normal file
|
@ -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**:
|
||||
|
||||
<Partial />
|
||||
|
||||
---
|
||||
|
||||
**We can import the same partial using a different name and it still works**:
|
||||
|
||||
import WeirdLocalName from './_partial.mdx';
|
||||
|
||||
<WeirdLocalName />
|
||||
|
||||
---
|
||||
|
||||
**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**
|
||||
|
||||
<FirstLevelPartial />
|
||||
|
||||
---
|
||||
|
||||
**And we can even use the same partial twice!**
|
||||
|
||||
**(although it's useless and not particularly recommended because headings will have the same ids)**
|
||||
|
||||
<FirstLevelPartial />
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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';
|
||||
|
||||
<Content />
|
||||
|
||||
export const toc = ContentToc;
|
||||
|
|
|
@ -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"
|
||||
|
||||
<Contributing />
|
||||
|
||||
export const toc = ContributingTOC;
|
||||
```
|
||||
|
|
|
@ -190,10 +190,14 @@ 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) {
|
||||
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}`;
|
||||
|
@ -201,6 +205,7 @@ export default async function createConfigAsync() {
|
|||
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;
|
||||
},
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Add table
Reference in a new issue