From b9266ad4d079178ef5cbb4b015993a166811a3be Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 10:46:54 -0700 Subject: [PATCH 01/36] feat: add all TOC levels to MDX loader --- .../src/remark/toc/search.ts | 85 ++++++++++++++----- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/search.ts b/packages/docusaurus-mdx-loader/src/remark/toc/search.ts index 112a56c3fab2..441bdf28d00d 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/search.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/search.ts @@ -8,40 +8,83 @@ 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 = (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: [], - }; + headings.push({ + node: { + value: toValue(child), + id: child.data!.id as string, + children: [], + }, + level: child.depth, + parentIndex: -1, + }); + }; - if (!headings.length || currentDepth >= child.depth) { - headings.push(entry); - current += 1; - currentDepth = child.depth; - } else { - headings[current].children.push(entry); + visit(node, 'heading', visitor); + + const getParentIndex = (prevParentIndex: number, currLevel: number) => { + let parentIndex = prevParentIndex; + // We start at the parent of the previous heading + // Recurse through its ancestors until the current heading would be a child + while (parentIndex >= 0 && currLevel < headings[parentIndex].level) { + parentIndex = headings[parentIndex].parentIndex; } + return parentIndex >= 0 ? headings[parentIndex].parentIndex : parentIndex; }; - visit(node, 'heading', visitor); + // Assign the correct `parentIndex` for each heading. + headings.forEach((curr, currIndex) => { + if (currIndex > 0) { + const prevIndex = currIndex - 1; + const prev = headings[prevIndex]; + if (curr.level > prev.level) { + curr.parentIndex = prevIndex; + } else if (curr.level < prev.level) { + curr.parentIndex = getParentIndex(prev.parentIndex, curr.level); + } else { + curr.parentIndex = prev.parentIndex; + } + } + }); + + 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); + } + }); - return headings; + const toc = headings + .filter((_, k) => rootNodeIndexes.includes(k)) // only return root nodes + .map((heading) => heading.node); // only return Node, no metadata + return toc; } From 9fbef8806aed8d8c68ac6fa347e0471d523f7a89 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 12:54:34 -0700 Subject: [PATCH 02/36] feat: add theme-level config for heading depth --- .../src/theme/TOC/index.tsx | 15 +++++++-- .../src/theme/TOCCollapsible/index.tsx | 2 +- .../src/theme/hooks/useTOCHighlight.ts | 33 ++++++++++++------- .../docusaurus-theme-classic/src/types.d.ts | 1 + .../src/validateThemeConfig.ts | 6 ++++ .../src/utils/useThemeConfig.ts | 5 +++ .../docs/api/themes/theme-configuration.md | 26 +++++++++++++++ 7 files changed, 73 insertions(+), 15 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx index b069a41aa50b..83c3d2930e14 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx @@ -12,6 +12,7 @@ import useTOCHighlight, { } from '@theme/hooks/useTOCHighlight'; import type {TOCProps, TOCHeadingsProps} from '@theme/TOC'; import styles from './styles.module.css'; +import {useThemeConfig} from '@docusaurus/theme-common'; const LINK_CLASS_NAME = 'table-of-contents__link'; @@ -24,10 +25,16 @@ const TOC_HIGHLIGHT_PARAMS: TOCHighlightParams = { export function TOCHeadings({ toc, isChild, + depth: headingLevel, }: TOCHeadingsProps): JSX.Element | null { + const {tableOfContents} = useThemeConfig(); + const maxDepth = tableOfContents?.maxDepth ?? 3; if (!toc.length) { return null; } + if (headingLevel > maxDepth) { + return null; + } return (
    - + ))}
@@ -53,7 +64,7 @@ function TOC({toc}: TOCProps): JSX.Element { useTOCHighlight(TOC_HIGHLIGHT_PARAMS); return (
- +
); } diff --git a/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx index 29764d2a3152..be23cf60f9b1 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx @@ -45,7 +45,7 @@ export default function TOCCollapsible({ lazy className={styles.tocCollapsibleContent} collapsed={collapsed}> - + ); diff --git a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts index 6d46cdea2b6c..2bb2bb64ee69 100644 --- a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts +++ b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts @@ -25,19 +25,26 @@ function isInViewportTopHalf(boundingRect: DOMRect) { return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2; } -function getAnchors() { - // For toc highlighting, we only consider h2/h3 anchors - const selector = '.anchor.anchor__h2, .anchor.anchor__h3'; - return Array.from(document.querySelectorAll(selector)) as HTMLElement[]; -} +function getAnchors(maxDepth: number = 3) { + const selectors = []; + + for (let i = 2; i <= maxDepth; i += 1) { + selectors.push(`.anchor.anchor__h${i}`); + } -function getActiveAnchor({ - anchorTopOffset, -}: { - anchorTopOffset: number; -}): Element | null { - const anchors = getAnchors(); + return Array.from( + document.querySelectorAll(selectors.join(', ')), + ) as HTMLElement[]; +} +function getActiveAnchor( + anchors: HTMLElement[], + { + anchorTopOffset, + }: { + anchorTopOffset: number; + }, +): Element | null { // Naming is hard // The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary // Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible @@ -100,6 +107,7 @@ function useTOCHighlight(params: Params): void { const lastActiveLinkRef = useRef(undefined); const anchorTopOffsetRef = useAnchorTopOffsetRef(); + const {tableOfContents} = useThemeConfig(); useEffect(() => { const {linkClassName, linkActiveClassName} = params; @@ -118,7 +126,8 @@ function useTOCHighlight(params: Params): void { function updateActiveLink() { const links = getLinks(linkClassName); - const activeAnchor = getActiveAnchor({ + const anchors = getAnchors(tableOfContents?.maxDepth); + const activeAnchor = getActiveAnchor(anchors, { anchorTopOffset: anchorTopOffsetRef.current, }); const activeLink = links.find( diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index e4aa3fd53976..8d3d238fc622 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -591,6 +591,7 @@ declare module '@theme/TOC' { export type TOCHeadingsProps = { readonly toc: readonly TOCItem[]; readonly isChild?: boolean; + readonly depth: number; }; export const TOCHeadings: (props: TOCHeadingsProps) => JSX.Element; diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 4e15ff468821..edeecda62e8b 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -41,6 +41,9 @@ const DEFAULT_CONFIG = { items: [], }, hideableSidebar: false, + tableOfContents: { + maxDepth: 3, + }, }; exports.DEFAULT_CONFIG = DEFAULT_CONFIG; @@ -329,6 +332,9 @@ const ThemeConfigSchema = Joi.object({ 'any.unknown': 'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs', }), + tableOfContents: Joi.object({ + maxDepth: Joi.number().default(DEFAULT_CONFIG.tableOfContents.maxDepth), + }), }); export {ThemeConfigSchema}; diff --git a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts index fe192e31b3fc..b7722cf015a6 100644 --- a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts +++ b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts @@ -85,6 +85,10 @@ export type Footer = { links: FooterLinks[]; }; +export type TableOfContents = { + maxDepth: number; +}; + export type ThemeConfig = { docs: { versionPersistence: DocsVersionPersistence; @@ -104,6 +108,7 @@ export type ThemeConfig = { image?: string; metadatas: Array>; sidebarCollapsible: boolean; + tableOfContents: TableOfContents; }; export function useThemeConfig(): ThemeConfig { diff --git a/website/docs/api/themes/theme-configuration.md b/website/docs/api/themes/theme-configuration.md index c22cdbe65266..43c1fb399c8a 100644 --- a/website/docs/api/themes/theme-configuration.md +++ b/website/docs/api/themes/theme-configuration.md @@ -757,6 +757,32 @@ module.exports = { }; ``` +## Table of Contents {#table-of-contents} + +You can adjust the table of contents via `themeConfig.tableOfContents`. + + + +| Name | Type | Default | Description | +| ---------- | -------- | ------- | -------------------------------------- | +| `maxDepth` | `number` | `3` | Max heading level displayed in the TOC | + + + +Example configuration: + +```js title="docusaurus.config.js" +module.exports = { + themeConfig: { + // highlight-start + tableOfContents: { + maxDepth: 5, + }, + // highlight-end + }, +}; +``` + ## Hooks {#hooks} ### `useThemeContext` {#usethemecontext} From fda510386e14f2cfc61776c93628d0e72253d2df Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 13:28:12 -0700 Subject: [PATCH 03/36] test: add remark MDX loader test --- .../__snapshots__/index.test.ts.snap | 47 +++++++++++++++++++ .../toc/__tests__/fixtures/maximum-depth-6.md | 11 +++++ .../src/remark/toc/__tests__/index.test.ts | 7 ++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap index 36725f1c984c..b0e5c9d9a34a 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap @@ -83,3 +83,50 @@ exports[`non text phrasing content 1`] = ` ## \`inline.code()\` " `; + +exports[`should process all heading levels 1`] = ` +"export const toc = [ + { + value: 'Bravo', + id: 'bravo', + children: [ + { + value: 'Charlie', + id: 'charlie', + children: [ + { + value: 'Delta', + id: 'delta', + children: [ + { + value: 'Echo', + id: 'echo', + children: [ + { + value: 'Foxtrot', + id: 'foxtrot', + children: [] + } + ] + } + ] + } + ] + } + ] + } +]; + +# Alpha + +## Bravo + +### Charlie + +#### Delta + +##### Echo + +###### Foxtrot +" +`; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md new file mode 100644 index 000000000000..8c0c38a7d514 --- /dev/null +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/fixtures/maximum-depth-6.md @@ -0,0 +1,11 @@ +# Alpha + +## Bravo + +### Charlie + +#### Delta + +##### Echo + +###### Foxtrot diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts index e9df4502b208..d146d6f9b043 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts @@ -12,7 +12,7 @@ import vfile from 'to-vfile'; import plugin from '../index'; import headings from '../../headings/index'; -const processFixture = async (name, options) => { +const processFixture = async (name, options?) => { const path = join(__dirname, 'fixtures', `${name}.md`); const file = await vfile.read(path); const result = await remark() @@ -210,3 +210,8 @@ test('empty headings', async () => { " `); }); + +test('should process all heading levels', async () => { + const result = await processFixture('maximum-depth-6'); + expect(result).toMatchSnapshot(); +}); From fd1b5f7291a8907fa26fe9fb0c4f1a2016ce6a38 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 13:40:20 -0700 Subject: [PATCH 04/36] fix: limit maxDepth validation to H2 - H6 --- .../src/__tests__/validateThemeConfig.test.js | 3 +++ .../docusaurus-theme-classic/src/validateThemeConfig.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js index f927038965a5..bfd20e098563 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js +++ b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js @@ -91,6 +91,9 @@ describe('themeConfig', () => { }, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, }, + tableOfContents: { + maxDepth: 4, + }, }; expect(testValidateThemeConfig(userConfig)).toEqual({ ...DEFAULT_CONFIG, diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index edeecda62e8b..44b939bbdd97 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -333,7 +333,11 @@ const ThemeConfigSchema = Joi.object({ 'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs', }), tableOfContents: Joi.object({ - maxDepth: Joi.number().default(DEFAULT_CONFIG.tableOfContents.maxDepth), + maxDepth: Joi.number() + .integer() + .min(2) + .max(6) + .default(DEFAULT_CONFIG.tableOfContents.maxDepth), }), }); From 0785423817478adb51e246f0d6a4819316234760 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 20:34:07 -0700 Subject: [PATCH 05/36] refactor: set default `maxDepth` using `joi` --- packages/docusaurus-theme-classic/src/theme/TOC/index.tsx | 2 +- .../docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts | 2 +- packages/docusaurus-theme-classic/src/validateThemeConfig.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx index 83c3d2930e14..be23035f7f7f 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx @@ -28,7 +28,7 @@ export function TOCHeadings({ depth: headingLevel, }: TOCHeadingsProps): JSX.Element | null { const {tableOfContents} = useThemeConfig(); - const maxDepth = tableOfContents?.maxDepth ?? 3; + const {maxDepth} = tableOfContents; if (!toc.length) { return null; } diff --git a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts index 2bb2bb64ee69..b744901d27ed 100644 --- a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts +++ b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts @@ -126,7 +126,7 @@ function useTOCHighlight(params: Params): void { function updateActiveLink() { const links = getLinks(linkClassName); - const anchors = getAnchors(tableOfContents?.maxDepth); + const anchors = getAnchors(tableOfContents.maxDepth); const activeAnchor = getActiveAnchor(anchors, { anchorTopOffset: anchorTopOffsetRef.current, }); diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 44b939bbdd97..579fc890d4d6 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -338,7 +338,7 @@ const ThemeConfigSchema = Joi.object({ .min(2) .max(6) .default(DEFAULT_CONFIG.tableOfContents.maxDepth), - }), + }).default(DEFAULT_CONFIG.tableOfContents), }); export {ThemeConfigSchema}; From 7269c0f92fe42d6a8119994dae4b631450a49588 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 16 Sep 2021 20:43:02 -0700 Subject: [PATCH 06/36] refactor: `maxDepth` -> `maxHeadingLevel --- .../src/__tests__/validateThemeConfig.test.js | 2 +- packages/docusaurus-theme-classic/src/theme/TOC/index.tsx | 4 ++-- .../src/theme/hooks/useTOCHighlight.ts | 6 +++--- .../docusaurus-theme-classic/src/validateThemeConfig.ts | 6 +++--- .../docusaurus-theme-common/src/utils/useThemeConfig.ts | 2 +- website/docs/api/themes/theme-configuration.md | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js index bfd20e098563..468e98a12287 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js +++ b/packages/docusaurus-theme-classic/src/__tests__/validateThemeConfig.test.js @@ -92,7 +92,7 @@ describe('themeConfig', () => { copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`, }, tableOfContents: { - maxDepth: 4, + maxHeadingLevel: 4, }, }; expect(testValidateThemeConfig(userConfig)).toEqual({ diff --git a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx index be23035f7f7f..e1cff874fbcc 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx @@ -28,11 +28,11 @@ export function TOCHeadings({ depth: headingLevel, }: TOCHeadingsProps): JSX.Element | null { const {tableOfContents} = useThemeConfig(); - const {maxDepth} = tableOfContents; + const {maxHeadingLevel} = tableOfContents; if (!toc.length) { return null; } - if (headingLevel > maxDepth) { + if (headingLevel > maxHeadingLevel) { return null; } return ( diff --git a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts index b744901d27ed..152859e29703 100644 --- a/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts +++ b/packages/docusaurus-theme-classic/src/theme/hooks/useTOCHighlight.ts @@ -25,10 +25,10 @@ function isInViewportTopHalf(boundingRect: DOMRect) { return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2; } -function getAnchors(maxDepth: number = 3) { +function getAnchors(maxHeadingLevel: number) { const selectors = []; - for (let i = 2; i <= maxDepth; i += 1) { + for (let i = 2; i <= maxHeadingLevel; i += 1) { selectors.push(`.anchor.anchor__h${i}`); } @@ -126,7 +126,7 @@ function useTOCHighlight(params: Params): void { function updateActiveLink() { const links = getLinks(linkClassName); - const anchors = getAnchors(tableOfContents.maxDepth); + const anchors = getAnchors(tableOfContents.maxHeadingLevel); const activeAnchor = getActiveAnchor(anchors, { anchorTopOffset: anchorTopOffsetRef.current, }); diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 579fc890d4d6..79785b190b0b 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -42,7 +42,7 @@ const DEFAULT_CONFIG = { }, hideableSidebar: false, tableOfContents: { - maxDepth: 3, + maxHeadingLevel: 3, }, }; exports.DEFAULT_CONFIG = DEFAULT_CONFIG; @@ -333,11 +333,11 @@ const ThemeConfigSchema = Joi.object({ 'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs', }), tableOfContents: Joi.object({ - maxDepth: Joi.number() + maxHeadingLevel: Joi.number() .integer() .min(2) .max(6) - .default(DEFAULT_CONFIG.tableOfContents.maxDepth), + .default(DEFAULT_CONFIG.tableOfContents.maxHeadingLevel), }).default(DEFAULT_CONFIG.tableOfContents), }); diff --git a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts index b7722cf015a6..25165b072963 100644 --- a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts +++ b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts @@ -86,7 +86,7 @@ export type Footer = { }; export type TableOfContents = { - maxDepth: number; + maxHeadingLevel: number; }; export type ThemeConfig = { diff --git a/website/docs/api/themes/theme-configuration.md b/website/docs/api/themes/theme-configuration.md index 43c1fb399c8a..e9697cef62ea 100644 --- a/website/docs/api/themes/theme-configuration.md +++ b/website/docs/api/themes/theme-configuration.md @@ -763,9 +763,9 @@ You can adjust the table of contents via `themeConfig.tableOfContents`. -| Name | Type | Default | Description | -| ---------- | -------- | ------- | -------------------------------------- | -| `maxDepth` | `number` | `3` | Max heading level displayed in the TOC | +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `maxHeadingLevel` | `number` | `3` | Max heading level displayed in the TOC. Should be an integer between 2 and 6. | @@ -776,7 +776,7 @@ module.exports = { themeConfig: { // highlight-start tableOfContents: { - maxDepth: 5, + maxHeadingLevel: 5, }, // highlight-end }, From 213289e5c2dc9e7baec53929d1d7b226a82c2da1 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Sun, 19 Sep 2021 15:31:51 -0700 Subject: [PATCH 07/36] refactor: invert underlying TOC depth API --- .../src/theme/BlogLayout/index.tsx | 7 ++++++- .../src/theme/DocItem/index.tsx | 5 ++++- .../src/theme/MDXPage/index.tsx | 9 +++++++-- .../src/theme/TOC/index.tsx | 17 ++++++----------- .../src/theme/TOCCollapsible/index.tsx | 3 ++- .../src/theme/TOCInline/index.tsx | 17 +++++++++++++---- .../docusaurus-theme-classic/src/types.d.ts | 5 ++++- .../markdown-features-inline-toc.mdx | 2 +- 8 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx index e5c29fcdc03e..8e165bd6c6e2 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import clsx from 'clsx'; +import {useThemeConfig} from '@docusaurus/theme-common'; import Layout from '@theme/Layout'; import BlogSidebar from '@theme/BlogSidebar'; import TOC from '@theme/TOC'; @@ -17,6 +18,7 @@ import type {Props} from '@theme/BlogLayout'; function BlogLayout(props: Props): JSX.Element { const {sidebar, toc, children, ...layoutProps} = props; const hasSidebar = sidebar && sidebar.items.length > 0; + const {tableOfContents} = useThemeConfig(); return ( @@ -38,7 +40,10 @@ function BlogLayout(props: Props): JSX.Element { {toc && (
- +
)} diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx index 18924918aac6..ff098c80d6c2 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/index.tsx @@ -19,7 +19,7 @@ import TOCCollapsible from '@theme/TOCCollapsible'; import {MainHeading} from '@theme/Heading'; import styles from './styles.module.css'; -import {ThemeClassNames} from '@docusaurus/theme-common'; +import {ThemeClassNames, useThemeConfig} from '@docusaurus/theme-common'; export default function DocItem(props: Props): JSX.Element { const {content: DocContent, versionMetadata} = props; @@ -39,6 +39,7 @@ export default function DocItem(props: Props): JSX.Element { !hideTitle && typeof DocContent.contentTitle === 'undefined'; const windowSize = useWindowSize(); + const {tableOfContents} = useThemeConfig(); const canRenderTOC = !hideTableOfContents && DocContent.toc && DocContent.toc.length > 0; @@ -71,6 +72,7 @@ export default function DocItem(props: Props): JSX.Element { {canRenderTOC && ( diff --git a/packages/docusaurus-theme-classic/src/theme/MDXPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/MDXPage/index.tsx index 6305ed272808..2692d0921d45 100644 --- a/packages/docusaurus-theme-classic/src/theme/MDXPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/MDXPage/index.tsx @@ -12,7 +12,7 @@ import {MDXProvider} from '@mdx-js/react'; import MDXComponents from '@theme/MDXComponents'; import type {Props} from '@theme/MDXPage'; import TOC from '@theme/TOC'; -import {ThemeClassNames} from '@docusaurus/theme-common'; +import {ThemeClassNames, useThemeConfig} from '@docusaurus/theme-common'; import styles from './styles.module.css'; @@ -28,6 +28,8 @@ function MDXPage(props: Props): JSX.Element { } = frontMatter; const {permalink} = metadata; + const {tableOfContents} = useThemeConfig(); + return ( {!hideTableOfContents && MDXPageContent.toc && (
- +
)} diff --git a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx index e1cff874fbcc..7a1ede1eb4cf 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx @@ -12,7 +12,6 @@ import useTOCHighlight, { } from '@theme/hooks/useTOCHighlight'; import type {TOCProps, TOCHeadingsProps} from '@theme/TOC'; import styles from './styles.module.css'; -import {useThemeConfig} from '@docusaurus/theme-common'; const LINK_CLASS_NAME = 'table-of-contents__link'; @@ -25,14 +24,9 @@ const TOC_HIGHLIGHT_PARAMS: TOCHighlightParams = { export function TOCHeadings({ toc, isChild, - depth: headingLevel, + recurseDepth, }: TOCHeadingsProps): JSX.Element | null { - const {tableOfContents} = useThemeConfig(); - const {maxHeadingLevel} = tableOfContents; - if (!toc.length) { - return null; - } - if (headingLevel > maxHeadingLevel) { + if (!toc.length || recurseDepth < 1) { return null; } return ( @@ -52,7 +46,7 @@ export function TOCHeadings({ ))} @@ -60,11 +54,12 @@ export function TOCHeadings({ ); } -function TOC({toc}: TOCProps): JSX.Element { +function TOC({toc, maxHeadingLevel}: TOCProps): JSX.Element { useTOCHighlight(TOC_HIGHLIGHT_PARAMS); + const recurseDepth = maxHeadingLevel - 1; return (
- +
); } diff --git a/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx index be23cf60f9b1..040a89dbc638 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOCCollapsible/index.tsx @@ -16,6 +16,7 @@ import type {TOCCollapsibleProps} from '@theme/TOCCollapsible'; export default function TOCCollapsible({ toc, className, + maxHeadingLevel, }: TOCCollapsibleProps): JSX.Element { const {collapsed, toggleCollapsed} = useCollapsible({ initialState: true, @@ -45,7 +46,7 @@ export default function TOCCollapsible({ lazy className={styles.tocCollapsibleContent} collapsed={collapsed}> - + ); diff --git a/packages/docusaurus-theme-classic/src/theme/TOCInline/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOCInline/index.tsx index 1753f0d4c57c..8c9e5bc44fd0 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOCInline/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOCInline/index.tsx @@ -10,16 +10,19 @@ import clsx from 'clsx'; import type {TOCInlineProps} from '@theme/TOCInline'; import styles from './styles.module.css'; import {TOCItem} from '@docusaurus/types'; +import {useThemeConfig} from '@docusaurus/theme-common'; /* eslint-disable jsx-a11y/control-has-associated-label */ function HeadingsInline({ toc, isChild, + recurseDepth, }: { toc: readonly TOCItem[]; isChild?: boolean; + recurseDepth: number; }) { - if (!toc.length) { + if (!toc.length || recurseDepth < 1) { return null; } return ( @@ -32,17 +35,23 @@ function HeadingsInline({ // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{__html: heading.value}} /> - + ))} ); } -function TOCInline({toc}: TOCInlineProps): JSX.Element { +function TOCInline({toc, maxHeadingLevel}: TOCInlineProps): JSX.Element { + const {tableOfContents} = useThemeConfig(); + const recurseDepth = (maxHeadingLevel ?? tableOfContents.maxHeadingLevel) - 1; return (
- +
); } diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 8d3d238fc622..120cbde9d885 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -585,13 +585,14 @@ declare module '@theme/TOC' { export type TOCProps = { readonly toc: readonly TOCItem[]; + readonly maxHeadingLevel: number; readonly className?: string; }; export type TOCHeadingsProps = { readonly toc: readonly TOCItem[]; readonly isChild?: boolean; - readonly depth: number; + readonly recurseDepth: number; }; export const TOCHeadings: (props: TOCHeadingsProps) => JSX.Element; @@ -605,6 +606,7 @@ declare module '@theme/TOCInline' { export type TOCInlineProps = { readonly toc: readonly TOCItem[]; + readonly maxHeadingLevel?: number; }; const TOCInline: (props: TOCInlineProps) => JSX.Element; @@ -616,6 +618,7 @@ declare module '@theme/TOCCollapsible' { export type TOCCollapsibleProps = { readonly className?: string; + readonly maxHeadingLevel: number; readonly toc: readonly TOCItem[]; }; diff --git a/website/docs/guides/markdown-features/markdown-features-inline-toc.mdx b/website/docs/guides/markdown-features/markdown-features-inline-toc.mdx index 5f839da3b835..d580452ca452 100644 --- a/website/docs/guides/markdown-features/markdown-features-inline-toc.mdx +++ b/website/docs/guides/markdown-features/markdown-features-inline-toc.mdx @@ -5,7 +5,7 @@ description: Using inline table-of-contents inside Docusaurus Markdown slug: /markdown-features/inline-toc --- -import BrowserWindow from '@site/src/components/BrowserWindow'; +TODO(erickzhao): add maxHeadingLevel docs import BrowserWindow from '@site/src/components/BrowserWindow'; Each Markdown document displays a tab of content on the top-right corner. From 9c363cc3582a094a10a1880cf04ffb28ee611af3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Sep 2021 10:03:27 -0700 Subject: [PATCH 08/36] refactor: make TOC algorithm level-aware --- .../__snapshots__/index.test.ts.snap | 45 ++++++++++++------- .../src/remark/toc/__tests__/index.test.ts | 42 +++++++++++------ .../src/remark/toc/search.ts | 1 + .../src/theme/TOC/index.tsx | 32 ++++++++----- .../src/theme/TOCCollapsible/index.tsx | 2 +- .../src/theme/TOCInline/index.tsx | 33 +++++++++----- .../docusaurus-theme-classic/src/types.d.ts | 2 +- packages/docusaurus-types/src/index.d.ts | 1 + 8 files changed, 103 insertions(+), 55 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap index b0e5c9d9a34a..e95f28adef16 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap @@ -9,24 +9,29 @@ exports[`inline code should be escaped 1`] = ` { value: '<Head>Test</Head>', id: 'headtesthead', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 }, { value: '<div />', id: 'div-', - children: [] + children: [], + level: 2 }, { value: '<div> Test </div>', id: 'div-test-div', - children: [] + children: [], + level: 2 }, { value: '<div><i>Test</i></div>', id: 'divitestidiv', - children: [] + children: [], + level: 2 } ]; @@ -51,24 +56,29 @@ exports[`non text phrasing content 1`] = ` { value: 'Importance', id: 'importance', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 }, { value: 'Strikethrough', id: 'strikethrough', - children: [] + children: [], + level: 2 }, { value: 'HTML', id: 'html', - children: [] + children: [], + level: 2 }, { value: 'inline.code()', id: 'inlinecode', - children: [] + children: [], + level: 2 } ]; @@ -105,15 +115,20 @@ exports[`should process all heading levels 1`] = ` { value: 'Foxtrot', id: 'foxtrot', - children: [] + children: [], + level: 6 } - ] + ], + level: 5 } - ] + ], + level: 4 } - ] + ], + level: 3 } - ] + ], + level: 2 } ]; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts index d146d6f9b043..a14e5e66a281 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts @@ -41,7 +41,8 @@ test('text content', async () => { { value: 'Endi', id: 'endi', - children: [] + children: [], + level: 3 }, { value: 'Endi', @@ -50,14 +51,17 @@ test('text content', async () => { { value: 'Yangshun', id: 'yangshun', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 }, { value: 'I ♥ unicode.', id: 'i--unicode', - children: [] + children: [], + level: 2 } ]; @@ -87,7 +91,8 @@ test('should export even with existing name', async () => { { value: 'Thanos', id: 'thanos', - children: [] + children: [], + level: 2 }, { value: 'Tony Stark', @@ -96,9 +101,11 @@ test('should export even with existing name', async () => { { value: 'Avengers', id: 'avengers', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 } ]; @@ -121,7 +128,8 @@ test('should export with custom name', async () => { { value: 'Endi', id: 'endi', - children: [] + children: [], + level: 3 }, { value: 'Endi', @@ -130,14 +138,17 @@ test('should export with custom name', async () => { { value: 'Yangshun', id: 'yangshun', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 }, { value: 'I ♥ unicode.', id: 'i--unicode', - children: [] + children: [], + level: 2 } ]; @@ -171,7 +182,8 @@ test('should insert below imports', async () => { { value: 'Title', id: 'title', - children: [] + children: [], + level: 2 }, { value: 'Test', @@ -180,9 +192,11 @@ test('should insert below imports', async () => { { value: 'Again', id: 'again', - children: [] + children: [], + level: 3 } - ] + ], + level: 2 } ]; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/search.ts b/packages/docusaurus-mdx-loader/src/remark/toc/search.ts index 441bdf28d00d..af082692466e 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/search.ts +++ b/packages/docusaurus-mdx-loader/src/remark/toc/search.ts @@ -39,6 +39,7 @@ export default function search(node: Node): TOCItem[] { value: toValue(child), id: child.data!.id as string, children: [], + level: child.depth, }, level: child.depth, parentIndex: -1, diff --git a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx index 7a1ede1eb4cf..2df70f6cf3d3 100644 --- a/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/TOC/index.tsx @@ -24,17 +24,15 @@ const TOC_HIGHLIGHT_PARAMS: TOCHighlightParams = { export function TOCHeadings({ toc, isChild, - recurseDepth, + maxHeadingLevel, }: TOCHeadingsProps): JSX.Element | null { - if (!toc.length || recurseDepth < 1) { + if (!toc.length) { return null; } - return ( -