mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 23:54:42 -04:00
Add memorized hook for table of contents (#2242)
* Create hook for converter function Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> * Add tests Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4be2e43914
commit
2e5fd5cc5c
4 changed files with 154 additions and 23 deletions
|
@ -0,0 +1,69 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Table of contents renders correctly 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="markdown-toc customClassName"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://example.org/#level-1"
|
||||||
|
title="Level 1"
|
||||||
|
>
|
||||||
|
Level 1
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://example.org/#level-2"
|
||||||
|
title="Level 2"
|
||||||
|
>
|
||||||
|
Level 2
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://example.org/#level-3"
|
||||||
|
title="Level 3"
|
||||||
|
>
|
||||||
|
Level 3
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Table of contents renders only in requested max depth 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="markdown-toc customClassName"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://example.org/#level-1"
|
||||||
|
title="Level 1"
|
||||||
|
>
|
||||||
|
Level 1
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://example.org/#level-2"
|
||||||
|
title="Level 2"
|
||||||
|
>
|
||||||
|
Level 2
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -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(
|
||||||
|
<TableOfContents ast={level0Ast} className={'customClassName'} baseUrl={'https://example.org'} />
|
||||||
|
)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders only in requested max depth', () => {
|
||||||
|
const view = render(
|
||||||
|
<TableOfContents ast={level0Ast} maxDepth={2} className={'customClassName'} baseUrl={'https://example.org'} />
|
||||||
|
)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -5,17 +5,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
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 styles from './table-of-contents.module.scss'
|
||||||
|
import { useBuildReactDomFromTocAst } from './use-build-react-dom-from-toc-ast'
|
||||||
|
|
||||||
export interface TableOfContentsProps {
|
export interface TableOfContentsProps {
|
||||||
ast: TocAst
|
ast: TocAst
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
className?: string
|
className?: string
|
||||||
baseUrl?: string
|
baseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,10 +28,7 @@ export interface TableOfContentsProps {
|
||||||
*/
|
*/
|
||||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, className, baseUrl }) => {
|
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, className, baseUrl }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const tocTree = useMemo(
|
const tocTree = useBuildReactDomFromTocAst(ast, maxDepth, baseUrl)
|
||||||
() => buildReactDomFromTocAst(ast, maxDepth, new Map<string, number>(), false, baseUrl),
|
|
||||||
[ast, maxDepth, baseUrl]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles['markdown-toc']} ${className ?? ''}`}>
|
<div className={`${styles['markdown-toc']} ${className ?? ''}`}>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import type { ReactElement } from 'react'
|
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 { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { tocSlugify } from './toc-slugify'
|
import { tocSlugify } from './toc-slugify'
|
||||||
import { JumpAnchor } from '../../markdown-renderer/markdown-extension/link-replacer/jump-anchor'
|
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 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 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 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
|
* @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,
|
toc: TocAst,
|
||||||
levelsToShowUnderThis: number,
|
levelsToShowUnderThis: number,
|
||||||
headerCounts: Map<string, number>,
|
headerCounts: Map<string, number>,
|
||||||
wrapInListItem: boolean,
|
baseUrl: string
|
||||||
baseUrl?: string
|
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
if (levelsToShowUnderThis < 0) {
|
if (levelsToShowUnderThis < 0) {
|
||||||
return null
|
return null
|
||||||
|
@ -38,24 +36,35 @@ export const buildReactDomFromTocAst = (
|
||||||
|
|
||||||
headerCounts.set(rawName, nameCount)
|
headerCounts.set(rawName, nameCount)
|
||||||
|
|
||||||
const content = (
|
const children = toc.c
|
||||||
|
.map((child) => buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, baseUrl))
|
||||||
|
.filter((value) => !!value)
|
||||||
|
.map((child, index) => <li key={index}>{child}</li>)
|
||||||
|
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ShowIf condition={toc.l > 0}>
|
<ShowIf condition={toc.l > 0}>
|
||||||
<JumpAnchor href={headlineUrl} title={rawName} jumpTargetId={slug.slice(1)}>
|
<JumpAnchor href={headlineUrl} title={rawName} jumpTargetId={slug.slice(1)}>
|
||||||
{rawName}
|
{rawName}
|
||||||
</JumpAnchor>
|
</JumpAnchor>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={toc.c.length > 0}>
|
<ShowIf condition={children.length > 0}>
|
||||||
<ul>
|
<ul>{children}</ul>
|
||||||
{toc.c.map((child) => buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl))}
|
|
||||||
</ul>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if (wrapInListItem) {
|
|
||||||
return <li key={headlineUrl}>{content}</li>
|
/**
|
||||||
} else {
|
* Generates a React DOM part for the table of contents from the given AST of the document.
|
||||||
return content
|
*
|
||||||
}
|
* @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<string, number>(), baseUrl),
|
||||||
|
[toc, maxDepth, baseUrl]
|
||||||
|
)
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue