Move markdown split into redux (#1681)

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-12-14 10:16:25 +01:00 committed by GitHub
parent 71e668cd17
commit 6594e1bb86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 217 additions and 226 deletions

View file

@ -18,5 +18,5 @@ export interface CommonMarkdownRendererProps {
newlinesAreBreaks?: boolean
lineOffset?: number
className?: string
content: string
markdownContentLines: string[]
}

View file

@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { cypressId } from '../../utils/cypress-attribute'
import { ShowIf } from '../common/show-if/show-if'
import type { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export const DocumentLengthLimitReachedAlert: React.FC<SimpleAlertProps> = ({ show }) => {
useTranslation()
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
return (
<ShowIf condition={show}>
<Alert variant='danger' dir={'auto'} {...cypressId('limitReachedMessage')}>
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }} />
</Alert>
</ShowIf>
)
}

View file

@ -4,8 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo, useRef } from 'react'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import React, { useEffect, useMemo, useRef } from 'react'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
import type { LineMarkerPosition } from './markdown-extension/linemarker/types'
@ -15,7 +14,6 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import type { TocAst } from 'markdown-it-toc-done-right'
import { useOnRefChange } from './hooks/use-on-ref-change'
import { useTrimmedContent } from './hooks/use-trimmed-content'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
import { HeadlineAnchorsMarkdownExtension } from './markdown-extension/headline-anchors-markdown-extension'
@ -26,7 +24,7 @@ export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererPro
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
className,
content,
markdownContentLines,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onTaskCheckedChange,
@ -40,7 +38,6 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
const markdownBodyRef = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
const tocAst = useRef<TocAst>()
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
const extensions = useMarkdownExtensions(
baseUrl,
@ -51,7 +48,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
onImageClick,
onTocChange
)
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, extensions, newlinesAreBreaks)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
useTranslation()
useCalculateLineMarkerPosition(
@ -60,12 +57,15 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
onLineMarkerPositionChanged,
markdownBodyRef.current?.offsetTop ?? 0
)
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
useEffect(() => {
extractFirstHeadline()
}, [extractFirstHeadline, markdownContentLines])
useOnRefChange(tocAst, onTocChange)
return (
<div ref={outerContainerRef} className={'position-relative'}>
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
<div
ref={markdownBodyRef}
className={`${className ?? ''} markdown-body w-100 d-flex flex-column align-items-center`}>

View file

@ -24,7 +24,7 @@ import { SanitizerMarkdownExtension } from '../markdown-extension/sanitizer/sani
* @return The React DOM that represents the rendered markdown code
*/
export const useConvertMarkdownToReactDom = (
markdownCode: string,
markdownContentLines: string[],
additionalMarkdownExtensions: MarkdownExtension[],
newlinesAreBreaks?: boolean
): ValidReactDomElement[] => {
@ -63,8 +63,8 @@ export const useConvertMarkdownToReactDom = (
}, [htmlToReactTransformer, markdownExtensions])
useMemo(() => {
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownCode))
}, [htmlToReactTransformer, lineNumberMapper, markdownCode])
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines))
}, [htmlToReactTransformer, lineNumberMapper, markdownContentLines])
const nodePreProcessor = useMemo(() => {
return markdownExtensions
@ -76,7 +76,7 @@ export const useConvertMarkdownToReactDom = (
}, [markdownExtensions])
return useMemo(() => {
const html = markdownIt.render(markdownCode)
const html = markdownIt.render(markdownContentLines.join('\n'))
htmlToReactTransformer.resetReplacers()
@ -84,5 +84,5 @@ export const useConvertMarkdownToReactDom = (
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
preprocessNodes: (document) => nodePreProcessor(document)
})
}, [htmlToReactTransformer, markdownCode, markdownIt, nodePreProcessor])
}, [htmlToReactTransformer, markdownContentLines, markdownIt, nodePreProcessor])
}

View file

@ -5,46 +5,65 @@
*/
import type React from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { useCallback, useRef } from 'react'
/**
* Extracts the plain text content of a {@link ChildNode node}.
*
* @param node The node whose text content should be extracted.
* @return the plain text content
*/
const extractInnerText = (node: ChildNode | null): string => {
if (!node) {
return ''
} else if (isKatexMathMlElement(node)) {
return ''
} else if (node.childNodes && node.childNodes.length > 0) {
return extractInnerTextFromChildren(node)
} else if (node.nodeName.toLowerCase() === 'img') {
return (node as HTMLImageElement).getAttribute('alt') ?? ''
} else {
return node.textContent ?? ''
}
}
/**
* Determines if the given {@link ChildNode node} is the mathml part of a KaTeX rendering.
* @param node The node that might be a katex mathml element
*/
const isKatexMathMlElement = (node: ChildNode): boolean => (node as HTMLElement).classList?.contains('katex-mathml')
/**
* Extracts the text content of the children of the given {@link ChildNode node}.
* @param node The node whose children should be processed. The content of the node itself won't be included.
* @return the concatenated text content of the child nodes
*/
const extractInnerTextFromChildren = (node: ChildNode): string =>
Array.from(node.childNodes).reduce((state, child) => {
return state + extractInnerText(child)
}, '')
/**
* Extracts the plain text content of the first level 1 heading in the document.
*
* @param documentElement The root element of (sub)dom that should be inspected
* @param onFirstHeadingChange A callback that will be executed with the new level 1 heading
*/
export const useExtractFirstHeadline = (
documentElement: React.RefObject<HTMLDivElement>,
content: string | undefined,
onFirstHeadingChange?: (firstHeading: string | undefined) => void
): void => {
const extractInnerText = useCallback((node: ChildNode | null): string => {
if (!node) {
return ''
}
if ((node as HTMLElement).classList?.contains('katex-mathml')) {
return ''
}
let innerText = ''
if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach((child) => {
innerText += extractInnerText(child)
})
} else if (node.nodeName === 'IMG') {
innerText += (node as HTMLImageElement).getAttribute('alt')
} else {
innerText += node.textContent
}
return innerText
}, [])
): (() => void) => {
const lastFirstHeading = useRef<string | undefined>()
useEffect(() => {
if (onFirstHeadingChange && documentElement.current) {
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
const headingText = extractInnerText(firstHeading).trim()
if (headingText !== lastFirstHeading.current) {
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
}
return useCallback(() => {
if (!onFirstHeadingChange || !documentElement.current) {
return
}
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
const headingText = extractInnerText(firstHeading).trim()
if (headingText !== lastFirstHeading.current) {
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
}
}, [documentElement, onFirstHeadingChange])
}

View file

@ -27,7 +27,7 @@ const initialSlideState: SlideState = {
indexVertical: 0
}
export const useReveal = (content: string, slideOptions?: SlideOptions): REVEAL_STATUS => {
export const useReveal = (markdownContentLines: string[], slideOptions?: SlideOptions): REVEAL_STATUS => {
const [deck, setDeck] = useState<Reveal>()
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
const currentSlideState = useRef<SlideState>(initialSlideState)
@ -67,7 +67,7 @@ export const useReveal = (content: string, slideOptions?: SlideOptions): REVEAL_
log.debug('Sync deck')
deck.sync()
deck.slide(currentSlideState.current.indexHorizontal, currentSlideState.current.indexVertical)
}, [content, deck, revealStatus])
}, [markdownContentLines, deck, revealStatus])
useEffect(() => {
if (

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const useTrimmedContent = (content: string): [trimmedContent: string, contentExceedsLimit: boolean] => {
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
const contentExceedsLimit = content.length > maxLength
const trimmedContent = useMemo(
() => (contentExceedsLimit ? content.substr(0, maxLength) : content),
[content, contentExceedsLimit, maxLength]
)
return [trimmedContent, contentExceedsLimit]
}

View file

@ -4,17 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo, useRef } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import type { TocAst } from 'markdown-it-toc-done-right'
import { useOnRefChange } from './hooks/use-on-ref-change'
import { useTrimmedContent } from './hooks/use-trimmed-content'
import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
import './slideshow.scss'
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { LoadingSlide } from './loading-slide'
import { RevealMarkdownExtension } from './markdown-extension/reveal/reveal-markdown-extension'
@ -27,7 +25,7 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
className,
content,
markdownContentLines,
onFirstHeadingChange,
onTaskCheckedChange,
onTocChange,
@ -39,7 +37,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
}) => {
const markdownBodyRef = useRef<HTMLDivElement>(null)
const tocAst = useRef<TocAst>()
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
const extensions = useMarkdownExtensions(
baseUrl,
@ -51,14 +48,18 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
onTocChange
)
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, extensions, newlinesAreBreaks)
const revealStatus = useReveal(content, slideOptions)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
const revealStatus = useReveal(markdownContentLines, slideOptions)
useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
useEffect(() => {
if (revealStatus === REVEAL_STATUS.INITIALISED) {
extractFirstHeadline()
}
}, [extractFirstHeadline, markdownContentLines, revealStatus])
useExtractFirstHeadline(
markdownBodyRef,
revealStatus === REVEAL_STATUS.INITIALISED ? content : undefined,
onFirstHeadingChange
)
useOnRefChange(tocAst, onTocChange)
const slideShowDOM = useMemo(
@ -67,14 +68,11 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
)
return (
<Fragment>
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
<div className={'reveal'}>
<div ref={markdownBodyRef} className={`${className ?? ''} slides`}>
{slideShowDOM}
</div>
<div className={'reveal'}>
<div ref={markdownBodyRef} className={`${className ?? ''} slides`}>
{slideShowDOM}
</div>
</Fragment>
</div>
)
}

View file

@ -14,8 +14,8 @@ describe('line id mapper', () => {
})
it('should be case sensitive', () => {
lineIdMapper.updateLineMapping('this\nis\ntext')
expect(lineIdMapper.updateLineMapping('this\nis\nText')).toEqual([
lineIdMapper.updateLineMapping(['this', 'is', 'text'])
expect(lineIdMapper.updateLineMapping(['this', 'is', 'Text'])).toEqual([
{
line: 'this',
id: 1
@ -32,8 +32,8 @@ describe('line id mapper', () => {
})
it('should not update line ids of shifted lines', () => {
lineIdMapper.updateLineMapping('this\nis\ntext')
expect(lineIdMapper.updateLineMapping('this\nis\nmore\ntext')).toEqual([
lineIdMapper.updateLineMapping(['this', 'is', 'text'])
expect(lineIdMapper.updateLineMapping(['this', 'is', 'more', 'text'])).toEqual([
{
line: 'this',
id: 1
@ -54,8 +54,8 @@ describe('line id mapper', () => {
})
it('should not update line ids if nothing changes', () => {
lineIdMapper.updateLineMapping('this\nis\ntext')
expect(lineIdMapper.updateLineMapping('this\nis\ntext')).toEqual([
lineIdMapper.updateLineMapping(['this', 'is', 'text'])
expect(lineIdMapper.updateLineMapping(['this', 'is', 'text'])).toEqual([
{
line: 'this',
id: 1
@ -72,9 +72,9 @@ describe('line id mapper', () => {
})
it('should not reuse line ids of removed lines', () => {
lineIdMapper.updateLineMapping('this\nis\nold')
lineIdMapper.updateLineMapping('this\nis')
expect(lineIdMapper.updateLineMapping('this\nis\nnew')).toEqual([
lineIdMapper.updateLineMapping(['this', 'is', 'old'])
lineIdMapper.updateLineMapping(['this', 'is'])
expect(lineIdMapper.updateLineMapping(['this', 'is', 'new'])).toEqual([
{
line: 'this',
id: 1
@ -91,8 +91,8 @@ describe('line id mapper', () => {
})
it('should update line ids for changed lines', () => {
lineIdMapper.updateLineMapping('this\nis\nold')
expect(lineIdMapper.updateLineMapping('this\nis\nnew')).toEqual([
lineIdMapper.updateLineMapping(['this', 'is', 'old'])
expect(lineIdMapper.updateLineMapping(['this', 'is', 'new'])).toEqual([
{
line: 'this',
id: 1

View file

@ -23,12 +23,11 @@ export class LineIdMapper {
* Calculates a line id mapping for the given line based text by creating a diff
* with the last lines code.
*
* @param newText The new text for which the line ids should be calculated
* @param newMarkdownContentLines The markdown content for which the line ids should be calculated
* @return the calculated {@link LineWithId lines with unique ids}
*/
public updateLineMapping(newText: string): LineWithId[] {
const lines = newText.split('\n')
const lineDifferences = this.diffNewLinesWithLastLineKeys(lines)
public updateLineMapping(newMarkdownContentLines: string[]): LineWithId[] {
const lineDifferences = this.diffNewLinesWithLastLineKeys(newMarkdownContentLines)
const newLineKeys = this.convertChangesToLinesWithIds(lineDifferences)
this.lastLines = newLineKeys
return newLineKeys