From 9a4e4c46beafa51c152f3e3aaf678d59a09be742 Mon Sep 17 00:00:00 2001 From: anatolykopyl Date: Sun, 21 Jan 2024 21:20:27 +0300 Subject: [PATCH 1/3] Injecting a function and collecting some data --- .../src/remark/toc/index.ts | 98 ++++++++++++++++++- .../src/remark/toc/utils.ts | 96 ++++++++++++++++++ .../tests/toc-partials/_partial-with-prop.mdx | 1 + .../toc-partials/{index.mdx => basic.mdx} | 2 +- .../_docs tests/tests/toc-partials/props.mdx | 11 +++ 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/_partial-with-prop.mdx rename website/_dogfooding/_docs tests/tests/toc-partials/{index.mdx => basic.mdx} (97%) create mode 100644 website/_dogfooding/_docs tests/tests/toc-partials/props.mdx diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts index e457e61bfb06..15189df83ea7 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts @@ -7,6 +7,7 @@ import { addTocSliceImportIfNeeded, + createPropsPlacerAST, createTOCExportNodeAST, findDefaultImportName, getImportDeclarations, @@ -30,6 +31,11 @@ interface PluginOptions { // ComponentName (default export) => ImportDeclaration mapping type MarkdownImports = Map; +type PartialProp = { + componentName: string; + propName: string; + propValue: any; +}; // MdxjsEsm node representing an already existing "export const toc" declaration type ExistingTOCExport = MdxjsEsm | null; @@ -155,10 +161,60 @@ async function collectTOCItems({ } } +async function collectPartialProps({ + root, + tocItemsRaw, + propsPlacerName, +}: { + root: Root; + tocItemsRaw: TOCItems; + propsPlacerName: string; +}): Promise<{ + tocItems: TOCItems; + partialProps: PartialProp[]; +}> { + const partialProps: PartialProp[] = []; + + const {visit} = await import('unist-util-visit'); + + visit(root, 'mdxJsxFlowElement', (child) => { + if (!child.name) { + return; + } + + for (const prop of child.attributes) { + if (prop.type === 'mdxJsxAttribute' && typeof prop.value === 'string') { + partialProps.push({ + componentName: child.name, + propName: prop.name, + propValue: prop.value, + }); + } + } + }); + + const tocItems = tocItemsRaw.map((tocItem) => { + if (tocItem.type === 'heading') { + return tocItem; + } + + return { + ...tocItem, + importName: `${propsPlacerName}(${tocItem.importName})`, + }; + }); + + return {tocItems, partialProps}; +} + export default function plugin(options: PluginOptions = {}): Transformer { const tocExportName = options.name || 'toc'; + const partialPropsName = 'partialProps'; + const propsPlacerName = 'placeProps'; return async (root) => { + const {valueToEstree} = await import('estree-util-value-to-estree'); + const {markdownImports, existingTocExport} = await collectImportsExports({ root, tocExportName, @@ -171,17 +227,57 @@ export default function plugin(options: PluginOptions = {}): Transformer { return; } - const {tocItems} = await collectTOCItems({ + const {tocItems: tocItemsRaw} = await collectTOCItems({ root, tocExportName, markdownImports, }); + const {tocItems, partialProps} = await collectPartialProps({ + root, + tocItemsRaw, + propsPlacerName, + }); + root.children.push( await createTOCExportNodeAST({ tocExportName, tocItems, }), ); + + root.children.push({ + 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', + }, + }, + }); + + root.children.push(createPropsPlacerAST(propsPlacerName)); }; } diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts index 3cedf78ed095..aa489e594566 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts @@ -175,3 +175,99 @@ export async function createTOCExportNodeAST({ }, }; } + +export function createPropsPlacerAST(propsPlacerName: string): MdxjsEsm { + return { + type: 'mdxjsEsm', + value: '', + data: { + estree: { + type: 'Program', + body: [ + { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: propsPlacerName, + }, + generator: false, + async: false, + params: [ + { + type: 'Identifier', + name: 'toc', + }, + ], + body: { + type: 'BlockStatement', + body: [ + { + type: 'ReturnStatement', + argument: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'toc', + }, + property: { + type: 'Identifier', + name: 'map', + }, + computed: false, + optional: false, + }, + arguments: [ + { + type: 'ArrowFunctionExpression', + expression: true, + generator: false, + async: false, + params: [ + { + type: 'Identifier', + name: 'tocItem', + }, + ], + body: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: 'tocItem', + }, + }, + { + type: 'Property', + method: false, + shorthand: false, + computed: false, + key: { + type: 'Identifier', + name: 'value', + }, + value: { + type: 'Literal', + value: 'TEST', + }, + kind: 'init', + }, + ], + }, + }, + ], + optional: false, + }, + }, + ], + }, + }, + ], + sourceType: 'module', + }, + }, + }; +} 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..1cff7009a5d8 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx @@ -0,0 +1,11 @@ +import PartialWithProp from './_partial-with-prop.mdx'; + +export const foo = 'bar'; + +# TOC partial test with props + + + +The word in the partial above should be visible in the heading itself and in the TOC as well. + +## Heading with prop {foo} From a318c22ebd6a8af4740cf996383596a7618d130c Mon Sep 17 00:00:00 2001 From: anatolykopyl Date: Sun, 21 Jan 2024 23:07:31 +0300 Subject: [PATCH 2/3] Crude, but working ToC with props --- packages/docusaurus-mdx-loader/package.json | 1 + .../src/remark/toc/index.ts | 91 +++++--------- .../src/remark/toc/types.ts | 2 +- .../src/remark/toc/utils.ts | 116 ++++-------------- .../_docs tests/tests/toc-partials/props.mdx | 6 +- yarn.lock | 8 +- 6 files changed, 62 insertions(+), 162 deletions(-) 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 15189df83ea7..bc47b847b7b1 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts @@ -103,11 +103,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') { @@ -117,7 +121,10 @@ async function collectTOCItems({ } }); - return {tocItems}; + return { + tocItems, + partialProps, + }; // Visit Markdown headings function visitHeading(node: Heading) { @@ -143,6 +150,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, @@ -150,7 +167,7 @@ async function collectTOCItems({ tocItems.push({ type: 'slice', - importName: tocSliceImportName, + value: `${propsPlacerName}(${tocSliceImportName}, '${componentName}')`, }); addTocSliceImportIfNeeded({ @@ -161,52 +178,6 @@ async function collectTOCItems({ } } -async function collectPartialProps({ - root, - tocItemsRaw, - propsPlacerName, -}: { - root: Root; - tocItemsRaw: TOCItems; - propsPlacerName: string; -}): Promise<{ - tocItems: TOCItems; - partialProps: PartialProp[]; -}> { - const partialProps: PartialProp[] = []; - - const {visit} = await import('unist-util-visit'); - - visit(root, 'mdxJsxFlowElement', (child) => { - if (!child.name) { - return; - } - - for (const prop of child.attributes) { - if (prop.type === 'mdxJsxAttribute' && typeof prop.value === 'string') { - partialProps.push({ - componentName: child.name, - propName: prop.name, - propValue: prop.value, - }); - } - } - }); - - const tocItems = tocItemsRaw.map((tocItem) => { - if (tocItem.type === 'heading') { - return tocItem; - } - - return { - ...tocItem, - importName: `${propsPlacerName}(${tocItem.importName})`, - }; - }); - - return {tocItems, partialProps}; -} - export default function plugin(options: PluginOptions = {}): Transformer { const tocExportName = options.name || 'toc'; const partialPropsName = 'partialProps'; @@ -227,25 +198,12 @@ export default function plugin(options: PluginOptions = {}): Transformer { return; } - const {tocItems: tocItemsRaw} = await collectTOCItems({ + const {tocItems, partialProps} = await collectTOCItems({ root, tocExportName, markdownImports, }); - const {tocItems, partialProps} = await collectPartialProps({ - root, - tocItemsRaw, - propsPlacerName, - }); - - root.children.push( - await createTOCExportNodeAST({ - tocExportName, - tocItems, - }), - ); - root.children.push({ type: 'mdxjsEsm', value: '', @@ -278,6 +236,13 @@ export default function plugin(options: PluginOptions = {}): Transformer { }, }); - root.children.push(createPropsPlacerAST(propsPlacerName)); + 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..2e22295b417f 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts @@ -23,7 +23,7 @@ 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)[]; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts index aa489e594566..16120575bb14 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts @@ -5,6 +5,7 @@ * 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 { @@ -113,7 +114,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}, }; } @@ -176,98 +177,35 @@ export async function createTOCExportNodeAST({ }; } -export function createPropsPlacerAST(propsPlacerName: string): MdxjsEsm { +export function createPropsPlacerAST({ + propsPlacerName, +}: { + propsPlacerName: string; +}): MdxjsEsm { return { type: 'mdxjsEsm', value: '', data: { - estree: { - type: 'Program', - body: [ - { - type: 'FunctionDeclaration', - id: { - type: 'Identifier', - name: propsPlacerName, - }, - generator: false, - async: false, - params: [ - { - type: 'Identifier', - name: 'toc', - }, - ], - body: { - type: 'BlockStatement', - body: [ - { - type: 'ReturnStatement', - argument: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'toc', - }, - property: { - type: 'Identifier', - name: 'map', - }, - computed: false, - optional: false, - }, - arguments: [ - { - type: 'ArrowFunctionExpression', - expression: true, - generator: false, - async: false, - params: [ - { - type: 'Identifier', - name: 'tocItem', - }, - ], - body: { - type: 'ObjectExpression', - properties: [ - { - type: 'SpreadElement', - argument: { - type: 'Identifier', - name: 'tocItem', - }, - }, - { - type: 'Property', - method: false, - shorthand: false, - computed: false, - key: { - type: 'Identifier', - name: 'value', - }, - value: { - type: 'Literal', - value: 'TEST', - }, - kind: 'init', - }, - ], - }, - }, - ], - optional: false, - }, - }, - ], - }, - }, - ], - sourceType: 'module', - }, + 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: 8}, + ) as Program, }, }; } diff --git a/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx b/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx index 1cff7009a5d8..08965bc49c23 100644 --- a/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx +++ b/website/_dogfooding/_docs tests/tests/toc-partials/props.mdx @@ -1,11 +1,7 @@ import PartialWithProp from './_partial-with-prop.mdx'; -export const foo = 'bar'; - # TOC partial test with props - + The word in the partial above should be visible in the heading itself and in the TOC as well. - -## Heading with prop {foo} 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" From f17140fc570c9466d730300d9b9104fb3cfa992a Mon Sep 17 00:00:00 2001 From: anatolykopyl Date: Sun, 21 Jan 2024 23:22:23 +0300 Subject: [PATCH 3/3] Extract function into utils --- .../src/remark/toc/index.ts | 47 +++-------------- .../src/remark/toc/types.ts | 6 +++ .../src/remark/toc/utils.ts | 52 ++++++++++++++++++- 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/index.ts b/packages/docusaurus-mdx-loader/src/remark/toc/index.ts index bc47b847b7b1..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,7 @@ import { addTocSliceImportIfNeeded, + createPartialPropsObjAST, createPropsPlacerAST, createTOCExportNodeAST, findDefaultImportName, @@ -22,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 { @@ -31,11 +32,6 @@ interface PluginOptions { // ComponentName (default export) => ImportDeclaration mapping type MarkdownImports = Map; -type PartialProp = { - componentName: string; - propName: string; - propValue: any; -}; // MdxjsEsm node representing an already existing "export const toc" declaration type ExistingTOCExport = MdxjsEsm | null; @@ -184,8 +180,6 @@ export default function plugin(options: PluginOptions = {}): Transformer { const propsPlacerName = 'placeProps'; return async (root) => { - const {valueToEstree} = await import('estree-util-value-to-estree'); - const {markdownImports, existingTocExport} = await collectImportsExports({ root, tocExportName, @@ -204,37 +198,12 @@ export default function plugin(options: PluginOptions = {}): Transformer { markdownImports, }); - root.children.push({ - 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', - }, - }, - }); + root.children.push( + await createPartialPropsObjAST({ + partialProps, + partialPropsName, + }), + ); root.children.push( await createTOCExportNodeAST({ diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts index 2e22295b417f..626ed0af3205 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/types.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/types.ts @@ -27,3 +27,9 @@ export type TOCSlice = { }; 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 16120575bb14..6137ae284a48 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/utils.ts @@ -12,7 +12,13 @@ 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, @@ -177,6 +183,48 @@ 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, }: { @@ -204,7 +252,7 @@ function ${propsPlacerName}(toc, componentName) { }) } `, - {ecmaVersion: 8}, + {ecmaVersion: 11}, ) as Program, }, };