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

fix: ToC generation when importing markdown (Docusaurus v2) #9682

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,24 @@ Some content here
export const c = 1;
"
`;

exports[`toc remark plugin works with imported markdown 1`] = `
"import Partial from './_partial.md';
import {toc as toc0} from './_partial.md';

export const toc = [
{
value: 'Thanos',
id: 'thanos',
level: 2
},
...toc0
];

## Thanos

Foo

<Partial />
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,9 @@ describe('toc remark plugin', () => {
const result = await processFixture('empty-headings');
expect(result).toMatchSnapshot();
});

it('works with imported markdown', async () => {
const result = await processFixture('imported-markdown');
expect(result).toMatchSnapshot();
});
});
74 changes: 57 additions & 17 deletions packages/docusaurus-mdx-loader/src/remark/toc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

import {parse, type ParserOptions} from '@babel/parser';
import traverse from '@babel/traverse';
import stringifyObject from 'stringify-object';
import toString from 'mdast-util-to-string';
import visit from 'unist-util-visit';
import {toValue} from '../utils';
import {constructArrayString, toValue} from '../utils';

import type {Identifier} from '@babel/types';
import type {TOCItem} from '../..';
import type {Node, Parent} from 'unist';
import type {Heading, Literal} from 'mdast';
import type {Heading, Literal, Text} from 'mdast';
import type {Transformer} from 'unified';

const parseOptions: ParserOptions = {
Expand Down Expand Up @@ -68,31 +67,72 @@ const getOrCreateExistingTargetIndex = (children: Node[]) => {
return targetIndex;
};

const removeTags = (input: string) =>
input.replace('<', '').replace('/>', '').trim();

export default function plugin(): Transformer {
return (root) => {
const headings: TOCItem[] = [];
let importsCount = 0;
const headings: (TOCItem | string)[] = [];

const PartialComponentToHeadingsName = Object.create(null);

visit(root, ['heading', 'jsx', 'import'], (child, index, parent) => {
if (child.type === 'heading') {
const headingNode = child as Heading;
const value = toString(headingNode);

// depth: 1 headings are titles and not included in the TOC
if (parent !== root || !value || headingNode.depth < 2) {
return;
}

headings.push({
value: toValue(headingNode),
id: headingNode.data!.id as string,
level: headingNode.depth,
});
}

if (child.type === 'import') {
const importNode = child as Text;

const markdownExtensionRegex = /\.(?:mdx|md).;?$/;
const imports = importNode.value
.split('\n')
.filter((statement) => markdownExtensionRegex.test(statement));

visit(root, 'heading', (child: Heading, index, parent) => {
const value = toString(child);
for (const importStatement of imports) {
const localName = `${name}${importsCount}`;

// depth: 1 headings are titles and not included in the TOC
if (parent !== root || !value || child.depth < 2) {
return;
const importWords = importStatement!.split(' ');
const partialPath = importWords[importWords.length - 1];
const partialName = importWords[1] as string;
const tocImport = `import {${name} as ${localName}} from ${partialPath}`;

PartialComponentToHeadingsName[partialName] = localName;

importNode.value = `${importNode.value}\n${tocImport}`;
importsCount += 1;
}
}

headings.push({
value: toValue(child),
id: child.data!.id as string,
level: child.depth,
});
if (child.type === 'jsx') {
const jsxNode = child as Text;

const componentName = removeTags(jsxNode.value);
const headingsName = PartialComponentToHeadingsName[componentName];
if (headingsName) {
headings.push(`...${headingsName}`);
}
}
});
const {children} = root as Parent<Literal>;
const targetIndex = getOrCreateExistingTargetIndex(children);

if (headings.length) {
children[targetIndex]!.value = `export const ${name} = ${stringifyObject(
headings,
)};`;
const headingsArray = constructArrayString(headings);
children[targetIndex]!.value = `export const ${name} = ${headingsArray};`;
}
};
}
23 changes: 23 additions & 0 deletions packages/docusaurus-mdx-loader/src/remark/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import escapeHtml from 'escape-html';
import toString from 'mdast-util-to-string';
import stringifyObject from 'stringify-object';
import type {Parent} from 'unist';
import type {PhrasingContent, Heading} from 'mdast';

Expand Down Expand Up @@ -34,3 +35,25 @@ export function toValue(node: PhrasingContent | Heading): string {
return toString(node);
}
}

/**
* Similar to stringify-object, but keeps spread operators,
* instead of turning them into strings.
* @param objects
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function constructArrayString(objects: any[]): string {
let result = '[';
for (const obj of objects) {
if (typeof obj === 'string') {
result = `${result}\n\t${obj},`;
} else {
result = `${result}\n\t${stringifyObject(obj).replace(/\n/g, '\n\t')},`;
}
}
// Remove trailing coma
result = result.replace(/,$/, '');
result += '\n]';

return result;
}
7 changes: 7 additions & 0 deletions website/_dogfooding/_pages tests/_anotherPagePartial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Another page partial content

This is text coming from another page partial

#### Foo

Level 4 headings don't belong in ToC
1 change: 1 addition & 0 deletions website/_dogfooding/_pages tests/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ import Readme from "../README.md"
- [Tabs tests](/tests/pages/tabs-tests)
- [z-index tests](/tests/pages/z-index-tests)
- [Head metadata tests](/tests/pages/head-metadata)
- [Partials tests](/tests/pages/partials-tests)
- [Embeds](/tests/pages/embeds)
12 changes: 12 additions & 0 deletions website/_dogfooding/_pages tests/partials-tests.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import PagePartial from './_pagePartial.md';
import AnotherPagePartial from './_anotherPagePartial.md';

# Partials tests

This page consists of multiple files imported into one. Notice how the table of contents works even for imported headings.

## Imported content

<PagePartial />

<AnotherPagePartial />
Loading