mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 10:45:20 -04:00
Move markdown split into redux (#1681)
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
71e668cd17
commit
6594e1bb86
30 changed files with 217 additions and 226 deletions
|
@ -18,5 +18,5 @@ export interface CommonMarkdownRendererProps {
|
|||
newlinesAreBreaks?: boolean
|
||||
lineOffset?: number
|
||||
className?: string
|
||||
content: string
|
||||
markdownContentLines: string[]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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`}>
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue