diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json index c15f5d073238..072fd7f8f4d0 100644 --- a/packages/docusaurus-mdx-loader/package.json +++ b/packages/docusaurus-mdx-loader/package.json @@ -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", diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts index e457e61bfb06..5c4a6acba4ab 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts @@ -7,6 +7,8 @@ import { addTocSliceImportIfNeeded, + createPartialPropsObjAST, + createPropsPlacerAST, createTOCExportNodeAST, findDefaultImportName, getImportDeclarations, @@ -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 { @@ -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') { @@ -111,7 +117,10 @@ async function collectTOCItems({ } }); - return {tocItems}; + return { + tocItems, + partialProps, + }; // Visit Markdown headings function visitHeading(node: Heading) { @@ -137,6 +146,16 @@ 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, @@ -144,7 +163,7 @@ async function collectTOCItems({ tocItems.push({ type: 'slice', - importName: tocSliceImportName, + value: `${propsPlacerName}(${tocSliceImportName}, '${componentName}')`, }); addTocSliceImportIfNeeded({ @@ -157,6 +176,8 @@ async function collectTOCItems({ export default function plugin(options: PluginOptions = {}): Transformer { const tocExportName = options.name || 'toc'; + const partialPropsName = 'partialProps'; + const propsPlacerName = 'placeProps'; return async (root) => { const {markdownImports, existingTocExport} = await collectImportsExports({ @@ -171,17 +192,26 @@ export default function plugin(options: PluginOptions = {}): Transformer { 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})); }; } diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts index 3170dabbba2d..626ed0af3205 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts @@ -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; +}; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts index 3cedf78ed095..6137ae284a48 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts @@ -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, @@ -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}, }; } @@ -175,3 +182,78 @@ export async function createTOCExportNodeAST({ }, }; } + +export async function createPartialPropsObjAST({ + partialProps, + partialPropsName, +}: { + partialProps: PartialProp[]; + partialPropsName: string; +}): Promise { + 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, + }, + }; +} diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/_partial-with-prop.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/_partial-with-prop.mdx new file mode 100644 index 000000000000..51cf7cf4dfa4 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/toc-partials/_partial-with-prop.mdx @@ -0,0 +1 @@ +## This word: {props.word} is from a prop diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/index.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/basic.mdx similarity index 97% rename from website/_dogfooding/_docs tests/tests/toc-partials/index.mdx rename to website/_dogfooding/_docs tests/tests/toc-partials/basic.mdx index 8e608d78ed16..59b61e1b5f33 100644 --- a/website/_dogfooding/_docs tests/tests/toc-partials/index.mdx +++ b/website/_dogfooding/_docs tests/tests/toc-partials/basic.mdx @@ -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 diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx new file mode 100644 index 000000000000..08965bc49c23 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx @@ -0,0 +1,7 @@ +import PartialWithProp from './_partial-with-prop.mdx'; + +# TOC partial test with props + + + +The word in the partial above should be visible in the heading itself and in the TOC as well. diff --git a/yarn.lock b/yarn.lock index c7ec91226c52..f12f3c31abaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"