/** * 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 {useMemo} from 'react'; import type {TOCItem} from '@docusaurus/mdx-loader'; export type TOCTreeNode = { readonly value: string; readonly id: string; readonly level: number; readonly children: readonly TOCTreeNode[]; }; function treeifyTOC(flatTOC: readonly TOCItem[]): TOCTreeNode[] { const headings = flatTOC.map((heading) => ({ ...heading, parentIndex: -1, children: [] as TOCTreeNode[], })); // Keep track of which previous index would be the current heading's direct // parent. Each entry is the last index of the `headings` array at heading // level . 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 rootNodes: TOCTreeNode[] = []; // For a given parentIndex, add each Node into that parent's `children` array headings.forEach((heading) => { const {parentIndex, ...rest} = heading; if (parentIndex >= 0) { headings[parentIndex]!.children.push(rest); } else { rootNodes.push(rest); } }); return rootNodes; } /** * Takes a flat TOC list (from the MDX loader) and treeifies it into what the * TOC components expect. Memoized for performance. */ export function useTreeifiedTOC(toc: TOCItem[]): readonly TOCTreeNode[] { return useMemo(() => treeifyTOC(toc), [toc]); } function filterTOC({ toc, minHeadingLevel, maxHeadingLevel, }: { toc: readonly TOCTreeNode[]; minHeadingLevel: number; maxHeadingLevel: number; }): TOCTreeNode[] { function isValid(item: TOCTreeNode) { return item.level >= minHeadingLevel && item.level <= maxHeadingLevel; } return toc.flatMap((item) => { const filteredChildren = filterTOC({ toc: item.children, minHeadingLevel, maxHeadingLevel, }); if (isValid(item)) { return [ { ...item, children: filteredChildren, }, ]; } return filteredChildren; }); } /** * Takes a flat TOC list (from the MDX loader) and treeifies it into what the * TOC components expect, applying the `minHeadingLevel` and `maxHeadingLevel`. * Memoized for performance. * * **Important**: this is not the same as `useTreeifiedTOC(toc.filter(...))`, * because we have to filter the TOC after it has been treeified. This is mostly * to ensure that weird TOC structures preserve their semantics. For example, an * h3-h2-h4 sequence should not be treeified as an "h3 > h4" hierarchy with * min=3, max=4, but should rather be "[h3, h4]" (since the h2 heading has split * the two headings and they are not parent-children) */ export function useFilteredAndTreeifiedTOC({ toc, minHeadingLevel, maxHeadingLevel, }: { toc: readonly TOCItem[]; minHeadingLevel: number; maxHeadingLevel: number; }): readonly TOCTreeNode[] { return useMemo( () => filterTOC({toc: treeifyTOC(toc), minHeadingLevel, maxHeadingLevel}), [toc, minHeadingLevel, maxHeadingLevel], ); }