feat(v2): allow specifying TOC max depth (themeConfig + frontMatter) (#5578)

* feat: add all TOC levels to MDX loader

* feat: add theme-level config for heading depth

* test: add remark MDX loader test

* fix: limit maxDepth validation to H2 - H6

* refactor: set default `maxDepth` using `joi`

* refactor: `maxDepth` -> `maxHeadingLevel

* refactor: invert underlying TOC depth API

* refactor: make TOC algorithm level-aware

* feat: add support for per-doc TOC heading levels

* feat: support document-level heading levels for blog

* fix: correct validation for toc level frontmatter

* fix: ensure TOC doesn't generate redundant DOM

* perf: simpler TOC heading search alg

* docs: document heading level props for `TOCInline`

* Update website/docs/guides/markdown-features/markdown-features-inline-toc.mdx

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>

* docs: fix docs (again)

* create dedicated  test file for heading searching logic: exhaustive tests will be simpler to write

* toc search: add real-world test

* fix test

* add dogfooding tests for toc min/max

* add test for min/max toc frontmatter

* reverse min/max order

* add theme minHeadingLevel + tests

* simpler TOC rendering logic

* simplify TOC implementation (temp, WIP)

* reverse unnatural order for minHeadingLevel/maxHeadingLevel

* add TOC dogfooding tests to all content plugins

* expose toc min/max heading level frontmatter to all 3 content plugins

* refactor blogLayout: accept toc ReactElement directly

* move toc utils to theme-common

* add tests for filterTOC

* create new generic TOCItems component

* useless css file copied

* fix toc highlighting className conflicts

* update doc

* fix types

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Erick Zhao 2021-09-29 02:19:11 -07:00 committed by GitHub
parent caba1e4908
commit c86dfbda61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1522 additions and 214 deletions

View file

@ -8,40 +8,75 @@
import toString from 'mdast-util-to-string';
import visit, {Visitor} from 'unist-util-visit';
import {toValue} from '../utils';
import type {TOCItem as TOC} from '@docusaurus/types';
import type {TOCItem} from '@docusaurus/types';
import type {Node} from 'unist';
import type {Heading} from 'mdast';
// Visit all headings. We `slug` all headings (to account for
// duplicates), but only take h2 and h3 headings.
export default function search(node: Node): TOC[] {
const headings: TOC[] = [];
let current = -1;
let currentDepth = 0;
// 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[] = [];
const visitor: Visitor<Heading> = (child, _index, parent) => {
const value = toString(child);
if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
// depth:1 headings are titles and not included in the TOC
if (parent !== node || !value || child.depth < 2) {
return;
}
const entry: TOC = {
value: toValue(child),
id: child.data!.id as string,
children: [],
};
if (!headings.length || currentDepth >= child.depth) {
headings.push(entry);
current += 1;
currentDepth = child.depth;
} else {
headings[current].children.push(entry);
}
headings.push({
node: {
value: toValue(child),
id: child.data!.id as string,
children: [],
level: child.depth,
},
level: child.depth,
parentIndex: -1,
});
};
visit(node, 'heading', visitor);
return headings;
// Keep track of which previous index would be the current heading's direcy 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;
}