diff --git a/src/components/editor-page/table-of-contents/__snapshots__/table-of-contents.test.tsx.snap b/src/components/editor-page/table-of-contents/__snapshots__/table-of-contents.test.tsx.snap new file mode 100644 index 000000000..eb42c16e4 --- /dev/null +++ b/src/components/editor-page/table-of-contents/__snapshots__/table-of-contents.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table of contents renders correctly 1`] = ` +
+
+ +
+
+`; + +exports[`Table of contents renders only in requested max depth 1`] = ` +
+
+ +
+
+`; diff --git a/src/components/editor-page/table-of-contents/table-of-contents.test.tsx b/src/components/editor-page/table-of-contents/table-of-contents.test.tsx new file mode 100644 index 000000000..6ae9850b5 --- /dev/null +++ b/src/components/editor-page/table-of-contents/table-of-contents.test.tsx @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n' +import { render } from '@testing-library/react' +import { TableOfContents } from './table-of-contents' +import type { TocAst } from 'markdown-it-toc-done-right' + +describe('Table of contents', () => { + beforeAll(async () => { + await mockI18n() + }) + + const level4Ast: TocAst = { + n: 'Level 4', + l: 4, + c: [] + } + const level3Ast: TocAst = { + n: 'Level 3', + l: 3, + c: [level4Ast] + } + const level2Ast: TocAst = { + n: 'Level 2', + l: 2, + c: [level3Ast] + } + const level1Ast: TocAst = { + n: 'Level 1', + l: 1, + c: [level2Ast] + } + const level0Ast: TocAst = { + n: '', + l: 0, + c: [level1Ast] + } + + it('renders correctly', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + it('renders only in requested max depth', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/editor-page/table-of-contents/table-of-contents.tsx b/src/components/editor-page/table-of-contents/table-of-contents.tsx index 03653be99..b43892b4a 100644 --- a/src/components/editor-page/table-of-contents/table-of-contents.tsx +++ b/src/components/editor-page/table-of-contents/table-of-contents.tsx @@ -5,17 +5,17 @@ */ import type { TocAst } from 'markdown-it-toc-done-right' -import React, { useMemo } from 'react' +import React from 'react' import { Trans, useTranslation } from 'react-i18next' import { ShowIf } from '../../common/show-if/show-if' -import { buildReactDomFromTocAst } from './build-react-dom-from-toc-ast' import styles from './table-of-contents.module.scss' +import { useBuildReactDomFromTocAst } from './use-build-react-dom-from-toc-ast' export interface TableOfContentsProps { ast: TocAst maxDepth?: number className?: string - baseUrl?: string + baseUrl: string } /** @@ -28,10 +28,7 @@ export interface TableOfContentsProps { */ export const TableOfContents: React.FC = ({ ast, maxDepth = 3, className, baseUrl }) => { useTranslation() - const tocTree = useMemo( - () => buildReactDomFromTocAst(ast, maxDepth, new Map(), false, baseUrl), - [ast, maxDepth, baseUrl] - ) + const tocTree = useBuildReactDomFromTocAst(ast, maxDepth, baseUrl) return (
diff --git a/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx b/src/components/editor-page/table-of-contents/use-build-react-dom-from-toc-ast.tsx similarity index 61% rename from src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx rename to src/components/editor-page/table-of-contents/use-build-react-dom-from-toc-ast.tsx index d48e2e3c4..c0e30c8f9 100644 --- a/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx +++ b/src/components/editor-page/table-of-contents/use-build-react-dom-from-toc-ast.tsx @@ -6,7 +6,7 @@ import type { TocAst } from 'markdown-it-toc-done-right' import type { ReactElement } from 'react' -import React, { Fragment } from 'react' +import React, { Fragment, useMemo } from 'react' import { ShowIf } from '../../common/show-if/show-if' import { tocSlugify } from './toc-slugify' import { JumpAnchor } from '../../markdown-renderer/markdown-extension/link-replacer/jump-anchor' @@ -17,15 +17,13 @@ import { JumpAnchor } from '../../markdown-renderer/markdown-extension/link-repl * @param toc The abstract syntax tree of the document for TOC generation * @param levelsToShowUnderThis The amount of levels which should be shown below this TOC item * @param headerCounts Map that contains the number of occurrences of single header names to allow suffixing them with a number to make them distinguishable - * @param wrapInListItem Whether to wrap the TOC content in a list item * @param baseUrl The base URL used for generating absolute links to the note with the correct slug anchor */ -export const buildReactDomFromTocAst = ( +const buildReactDomFromTocAst = ( toc: TocAst, levelsToShowUnderThis: number, headerCounts: Map, - wrapInListItem: boolean, - baseUrl?: string + baseUrl: string ): ReactElement | null => { if (levelsToShowUnderThis < 0) { return null @@ -38,24 +36,35 @@ export const buildReactDomFromTocAst = ( headerCounts.set(rawName, nameCount) - const content = ( + const children = toc.c + .map((child) => buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, baseUrl)) + .filter((value) => !!value) + .map((child, index) =>
  • {child}
  • ) + + return ( 0}> {rawName} - 0}> -
      - {toc.c.map((child) => buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl))} -
    + 0}> +
      {children}
    ) - - if (wrapInListItem) { - return
  • {content}
  • - } else { - return content - } +} + +/** + * Generates a React DOM part for the table of contents from the given AST of the document. + * + * @param toc The abstract syntax tree of the document for TOC generation + * @param maxDepth The maximum depth of levels which should be shown in the TOC + * @param baseUrl The base URL used for generating absolute links to the note with the correct slug anchor + */ +export const useBuildReactDomFromTocAst = (toc: TocAst, maxDepth: number, baseUrl: string) => { + return useMemo( + () => buildReactDomFromTocAst(toc, maxDepth, new Map(), baseUrl), + [toc, maxDepth, baseUrl] + ) }