refactor: make MDX export a flat TOC list instead of tree (#6729)

This commit is contained in:
Joshua Chen 2022-02-23 22:12:04 +08:00 committed by GitHub
parent 2d93750caf
commit c3370be64d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 312 additions and 445 deletions

View file

@ -5,32 +5,26 @@ exports[`inline code should be escaped 1`] = `
{
value: '<code>&lt;Head /&gt;</code>',
id: 'head-',
children: [
{
value: '<code>&lt;Head&gt;Test&lt;/Head&gt;</code>',
id: 'headtesthead',
children: [],
level: 3
}
],
level: 2
},
{
value: '<code>&lt;Head&gt;Test&lt;/Head&gt;</code>',
id: 'headtesthead',
level: 3
},
{
value: '<code>&lt;div /&gt;</code>',
id: 'div-',
children: [],
level: 2
},
{
value: '<code>&lt;div&gt; Test &lt;/div&gt;</code>',
id: 'div-test-div',
children: [],
level: 2
},
{
value: '<code>&lt;div&gt;&lt;i&gt;Test&lt;/i&gt;&lt;/div&gt;</code>',
id: 'divitestidiv',
children: [],
level: 2
}
];
@ -52,32 +46,26 @@ exports[`non text phrasing content 1`] = `
{
value: '<em>Emphasis</em>',
id: 'emphasis',
children: [
{
value: '<strong>Importance</strong>',
id: 'importance',
children: [],
level: 3
}
],
level: 2
},
{
value: '<strong>Importance</strong>',
id: 'importance',
level: 3
},
{
value: '<del>Strikethrough</del>',
id: 'strikethrough',
children: [],
level: 2
},
{
value: '<i>HTML</i>',
id: 'html',
children: [],
level: 2
},
{
value: '<code>inline.code()</code>',
id: 'inlinecode',
children: [],
level: 2
}
];

View file

@ -41,26 +41,21 @@ test('text content', async () => {
{
value: 'Endi',
id: 'endi',
children: [],
level: 3
},
{
value: 'Endi',
id: 'endi-1',
children: [
{
value: 'Yangshun',
id: 'yangshun',
children: [],
level: 3
}
],
level: 2
},
{
value: 'Yangshun',
id: 'yangshun',
level: 3
},
{
value: 'I ♥ unicode.',
id: 'i--unicode',
children: [],
level: 2
}
];
@ -91,21 +86,17 @@ test('should export even with existing name', async () => {
{
value: 'Thanos',
id: 'thanos',
children: [],
level: 2
},
{
value: 'Tony Stark',
id: 'tony-stark',
children: [
{
value: 'Avengers',
id: 'avengers',
children: [],
level: 3
}
],
level: 2
},
{
value: 'Avengers',
id: 'avengers',
level: 3
}
];
@ -128,26 +119,21 @@ test('should export with custom name', async () => {
{
value: 'Endi',
id: 'endi',
children: [],
level: 3
},
{
value: 'Endi',
id: 'endi-1',
children: [
{
value: 'Yangshun',
id: 'yangshun',
children: [],
level: 3
}
],
level: 2
},
{
value: 'Yangshun',
id: 'yangshun',
level: 3
},
{
value: 'I ♥ unicode.',
id: 'i--unicode',
children: [],
level: 2
}
];
@ -182,21 +168,17 @@ test('should insert below imports', async () => {
{
value: 'Title',
id: 'title',
children: [],
level: 2
},
{
value: 'Test',
id: 'test',
children: [
{
value: 'Again',
id: 'again',
children: [],
level: 3
}
],
level: 2
},
{
value: 'Again',
id: 'again',
level: 3
}
];

View file

@ -1,182 +0,0 @@
/**
* 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 remark from 'remark';
import mdx from 'remark-mdx';
import search from '../search';
import headings from '../../headings/index';
const getHeadings = async (mdText: string) => {
const node = remark().parse(mdText);
const result = await remark().use(headings).use(mdx).run(node);
return search(result);
};
test('should process all heading levels', async () => {
const md = `
# Alpha
## Bravo
### Charlie
#### Delta
##### Echo
###### Foxtrot
`;
await expect(getHeadings(md)).resolves.toEqual([
{
children: [
{
children: [
{
children: [
{
children: [
{
children: [],
id: 'foxtrot',
level: 6,
value: 'Foxtrot',
},
],
id: 'echo',
level: 5,
value: 'Echo',
},
],
id: 'delta',
level: 4,
value: 'Delta',
},
],
id: 'charlie',
level: 3,
value: 'Charlie',
},
],
id: 'bravo',
level: 2,
value: 'Bravo',
},
]);
});
test('should process real-world well-formatted md', async () => {
const md = `
# title
some text
## section 1
some text
### subsection 1-1
some text
#### subsection 1-1-1
some text
#### subsection 1-1-2
some text
### subsection 1-2
some text
### subsection 1-3
some text
## section 2
some text
### subsection 2-1
some text
### subsection 2-1
some text
## section 3
some text
### subsection 3-1
some text
### subsection 3-2
some text
`;
await expect(getHeadings(md)).resolves.toEqual([
{
children: [
{
children: [
{
children: [],
id: 'subsection-1-1-1',
level: 4,
value: 'subsection 1-1-1',
},
{
children: [],
id: 'subsection-1-1-2',
level: 4,
value: 'subsection 1-1-2',
},
],
id: 'subsection-1-1',
level: 3,
value: 'subsection 1-1',
},
{children: [], id: 'subsection-1-2', level: 3, value: 'subsection 1-2'},
{children: [], id: 'subsection-1-3', level: 3, value: 'subsection 1-3'},
],
id: 'section-1',
level: 2,
value: 'section 1',
},
{
children: [
{children: [], id: 'subsection-2-1', level: 3, value: 'subsection 2-1'},
{
children: [],
id: 'subsection-2-1-1',
level: 3,
value: 'subsection 2-1',
},
],
id: 'section-2',
level: 2,
value: 'section 2',
},
{
children: [
{children: [], id: 'subsection-3-1', level: 3, value: 'subsection 3-1'},
{children: [], id: 'subsection-3-2', level: 3, value: 'subsection 3-2'},
],
id: 'section-3',
level: 2,
value: 'section 3',
},
]);
});

View file

@ -9,10 +9,14 @@ import {parse, type ParserOptions} from '@babel/parser';
import type {Identifier} from '@babel/types';
import traverse from '@babel/traverse';
import stringifyObject from 'stringify-object';
import search from './search';
import type {Plugin, Transformer} from 'unified';
import toString from 'mdast-util-to-string';
import visit from 'unist-util-visit';
import {toValue} from '../utils';
import type {TOCItem} from '@docusaurus/types';
import type {Node, Parent} from 'unist';
import type {Literal} from 'mdast';
import type {Heading, Literal} from 'mdast';
import type {Plugin, Transformer} from 'unified';
const parseOptions: ParserOptions = {
plugins: ['jsx'],
@ -70,7 +74,22 @@ const plugin: Plugin<[PluginOptions?]> = (options = {}) => {
const name = options.name || 'toc';
const transformer: Transformer = (node) => {
const headings = search(node);
const headings: TOCItem[] = [];
visit(node, 'heading', (child: Heading, _index, parent) => {
const value = toString(child);
// depth:1 headings are titles and not included in the TOC
if (parent !== node || !value || child.depth < 2) {
return;
}
headings.push({
value: toValue(child),
id: child.data!.id as string,
level: child.depth,
});
});
const {children} = node as Parent<Literal>;
const targetIndex = getOrCreateExistingTargetIndex(children, name);

View file

@ -1,80 +0,0 @@
/**
* 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 toString from 'mdast-util-to-string';
import visit from 'unist-util-visit';
import {toValue} from '../utils';
import type {TOCItem} from '@docusaurus/types';
import type {Node} from 'unist';
import type {Heading} from 'mdast';
// Intermediate interface for TOC algorithm
interface SearchItem {
node: TOCItem;
level: number;
parentIndex: number;
}
/**
*
* Generate a TOC AST from the raw Markdown contents
*/
export default function search(node: Node): TOCItem[] {
const headings: SearchItem[] = [];
visit(node, 'heading', (child: Heading, _index, parent) => {
const value = toString(child);
// depth:1 headings are titles and not included in the TOC
if (parent !== node || !value || child.depth < 2) {
return;
}
headings.push({
node: {
value: toValue(child),
id: child.data!.id as string,
children: [],
level: child.depth,
},
level: child.depth,
parentIndex: -1,
});
});
// Keep track of which previous index would be the current heading's direct
// parent. Each entry <i> is the last index of the `headings` array at heading
// level <i>. We will modify these indices as we iterate through all headings.
// e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2
// indices 0 and 1 will remain unused.
const prevIndexForLevel = Array(7).fill(-1);
headings.forEach((curr, currIndex) => {
// take the last seen index for each ancestor level. the highest
// index will be the direct ancestor of the current heading.
const ancestorLevelIndexes = prevIndexForLevel.slice(2, curr.level);
curr.parentIndex = Math.max(...ancestorLevelIndexes);
// mark that curr.level was last seen at the current index
prevIndexForLevel[curr.level] = currIndex;
});
const rootNodeIndexes: number[] = [];
// For a given parentIndex, add each Node into that parent's `children` array
headings.forEach((heading, i) => {
if (heading.parentIndex >= 0) {
headings[heading.parentIndex].node.children.push(heading.node);
} else {
rootNodeIndexes.push(i);
}
});
const toc = headings
.filter((_, k) => rootNodeIndexes.includes(k)) // only return root nodes
.map((heading) => heading.node); // only return Node, no metadata
return toc;
}