Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mdx-loader): the table-of-contents should display props passed to headings of imported MDX partials #9773

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/docusaurus-mdx-loader/package.json
Expand Up @@ -23,6 +23,7 @@
"@docusaurus/utils-validation": "3.0.0",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"acorn": "^8.11.3",
"escape-html": "^1.0.3",
"estree-util-value-to-estree": "^3.0.1",
"file-loader": "^6.2.0",
Expand Down
38 changes: 34 additions & 4 deletions packages/docusaurus-mdx-loader/src/remark/toc/index.ts
Expand Up @@ -7,6 +7,8 @@

import {
addTocSliceImportIfNeeded,
createPartialPropsObjAST,
createPropsPlacerAST,
createTOCExportNodeAST,
findDefaultImportName,
getImportDeclarations,
Expand All @@ -21,7 +23,7 @@ import type {
MdxJsxFlowElement,
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
} from 'mdast-util-mdx';
import type {TOCItems} from './types';
import type {TOCItems, PartialProp} from './types';
import type {ImportDeclaration} from 'estree';

interface PluginOptions {
Expand Down Expand Up @@ -97,11 +99,15 @@ async function collectTOCItems({
}): Promise<{
// The toc items we collected in the tree
tocItems: TOCItems;
partialProps: PartialProp[];
}> {
const {toString} = await import('mdast-util-to-string');
const {visit} = await import('unist-util-visit');

const tocItems: TOCItems = [];
const partialProps: PartialProp[] = [];

const propsPlacerName = 'placeProps';

visit(root, (child) => {
if (child.type === 'heading') {
Expand All @@ -111,7 +117,10 @@ async function collectTOCItems({
}
});

return {tocItems};
return {
tocItems,
partialProps,
};

// Visit Markdown headings
function visitHeading(node: Heading) {
Expand All @@ -137,14 +146,24 @@ async function collectTOCItems({
return;
}

for (const prop of node.attributes) {
if (prop.type === 'mdxJsxAttribute' && typeof prop.value === 'string') {
partialProps.push({
componentName,
propName: prop.name,
propValue: prop.value,
});
}
}

const tocSliceImportName = createTocSliceImportName({
tocExportName,
componentName,
});

tocItems.push({
type: 'slice',
importName: tocSliceImportName,
value: `${propsPlacerName}(${tocSliceImportName}, '${componentName}')`,
});

addTocSliceImportIfNeeded({
Expand All @@ -157,6 +176,8 @@ async function collectTOCItems({

export default function plugin(options: PluginOptions = {}): Transformer<Root> {
const tocExportName = options.name || 'toc';
const partialPropsName = 'partialProps';
const propsPlacerName = 'placeProps';

return async (root) => {
const {markdownImports, existingTocExport} = await collectImportsExports({
Expand All @@ -171,17 +192,26 @@ export default function plugin(options: PluginOptions = {}): Transformer<Root> {
return;
}

const {tocItems} = await collectTOCItems({
const {tocItems, partialProps} = await collectTOCItems({
root,
tocExportName,
markdownImports,
});

root.children.push(
await createPartialPropsObjAST({
partialProps,
partialPropsName,
}),
);

root.children.push(
await createTOCExportNodeAST({
tocExportName,
tocItems,
}),
);

root.children.push(createPropsPlacerAST({propsPlacerName}));
};
}
8 changes: 7 additions & 1 deletion packages/docusaurus-mdx-loader/src/remark/toc/types.ts
Expand Up @@ -23,7 +23,13 @@ export type TOCHeading = {
// A TOC slice represents a TOCItem[] imported from a partial
export type TOCSlice = {
readonly type: 'slice';
readonly importName: string;
readonly value: string;
};

export type TOCItems = (TOCHeading | TOCSlice)[];

export type PartialProp = {
componentName: string;
propName: string;
propValue: any;
};
86 changes: 84 additions & 2 deletions packages/docusaurus-mdx-loader/src/remark/toc/utils.ts
Expand Up @@ -5,13 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/

import acorn from 'acorn';
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 {
TOCHeading,
TOCItem,
TOCItems,
TOCSlice,
PartialProp,
} from './types';
import type {
Program,
SpreadElement,
Expand Down Expand Up @@ -113,7 +120,7 @@ export async function createTOCExportNodeAST({
function createTOCSliceAST(tocSlice: TOCSlice): SpreadElement {
return {
type: 'SpreadElement',
argument: {type: 'Identifier', name: tocSlice.importName},
argument: {type: 'Identifier', name: tocSlice.value},
};
}

Expand Down Expand Up @@ -175,3 +182,78 @@ export async function createTOCExportNodeAST({
},
};
}

export async function createPartialPropsObjAST({
partialProps,
partialPropsName,
}: {
partialProps: PartialProp[];
partialPropsName: string;
}): Promise<MdxjsEsm> {
const {valueToEstree} = await import('estree-util-value-to-estree');

return {
type: 'mdxjsEsm',
value: '',
data: {
estree: {
type: 'Program',
body: [
{
type: 'ExportNamedDeclaration',
declaration: {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: partialPropsName,
},
init: valueToEstree(partialProps),
},
],
kind: 'const',
},
specifiers: [],
source: null,
},
],
sourceType: 'module',
},
},
};
}

export function createPropsPlacerAST({
propsPlacerName,
}: {
propsPlacerName: string;
}): MdxjsEsm {
return {
type: 'mdxjsEsm',
value: '',
data: {
estree: acorn.parse(
`
function ${propsPlacerName}(toc, componentName) {
const componentProps = partialProps.filter((partialProp) => partialProp.componentName === componentName)

return toc.map((tocItem) => {
let value = tocItem.value
for (let componentProp of componentProps) {
value = value.replace('props.' + componentProp.propName, componentProp.propValue);
}

return {
...tocItem,
value
}
})
}
`,
{ecmaVersion: 11},
) as Program,
},
};
}
@@ -0,0 +1 @@
## This word: {props.word} is from a prop
@@ -1,6 +1,6 @@
import Partial from './_partial.mdx';

# TOC partial test
# Basic TOC partial test

This page tests that MDX-imported content appears correctly in the table-of-contents

Expand Down
@@ -0,0 +1,7 @@
import PartialWithProp from './_partial-with-prop.mdx';

# TOC partial test with props

<PartialWithProp word="foo" />

The word in the partial above should be visible in the heading itself and in the TOC as well.
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -4083,10 +4083,10 @@ acorn@^6.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==

acorn@^8.0.0, acorn@^8.0.4, acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.3, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.11.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==

add-stream@^1.0.0:
version "1.0.0"
Expand Down