From 6594e1bb8688ce80828e6dc34365b90ccd70f0b7 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Tue, 14 Dec 2021 10:16:25 +0100 Subject: [PATCH] Move markdown split into redux (#1681) Signed-off-by: Tilman Vatteroth --- cypress/integration/maxLength.spec.ts | 3 +- .../document-read-only-page-content.tsx | 6 +- .../app-bar/help-button/cheatsheet-line.tsx | 11 +-- .../help-button/cheatsheet-tab-content.tsx | 2 +- .../editor-document-renderer.tsx | 9 +- .../hooks/use-send-markdown-to-renderer.ts | 8 +- .../renderer-pane/render-iframe.tsx | 4 +- .../hooks/use-intro-page-content.ts | 6 +- src/components/intro-page/intro-page.tsx | 35 +++++--- .../common-markdown-renderer-props.ts | 2 +- .../document-length-limit-reached-alert.tsx | 27 ------ .../document-markdown-renderer.tsx | 16 ++-- .../use-convert-markdown-to-react-dom.ts | 10 +-- .../hooks/use-extract-first-headline.ts | 89 +++++++++++-------- .../markdown-renderer/hooks/use-reveal.ts | 4 +- .../hooks/use-trimmed-content.ts | 19 ---- .../slideshow-markdown-renderer.tsx | 36 ++++---- .../utils/line-id-mapper.test.ts | 22 ++--- .../markdown-renderer/utils/line-id-mapper.ts | 7 +- .../render-page/iframe-markdown-renderer.tsx | 10 +-- .../render-page/markdown-document.tsx | 8 +- .../rendering-message.ts | 2 +- .../slide-show-page-content.tsx | 6 +- ...te-markdown-content-without-frontmatter.ts | 20 ----- ...te-markdown-content-without-frontmatter.ts | 28 ++++++ .../frontmatter-extractor/extractor.test.ts | 30 +++---- .../frontmatter-extractor/extractor.ts | 5 +- src/redux/note-details/initial-state.ts | 1 + src/redux/note-details/reducer.ts | 16 ++-- src/redux/note-details/types/note-details.ts | 1 + 30 files changed, 217 insertions(+), 226 deletions(-) delete mode 100644 src/components/markdown-renderer/document-length-limit-reached-alert.tsx delete mode 100644 src/components/markdown-renderer/hooks/use-trimmed-content.ts delete mode 100644 src/hooks/common/use-note-markdown-content-without-frontmatter.ts create mode 100644 src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts diff --git a/cypress/integration/maxLength.spec.ts b/cypress/integration/maxLength.spec.ts index 830b14f26..5308a455f 100644 --- a/cypress/integration/maxLength.spec.ts +++ b/cypress/integration/maxLength.spec.ts @@ -27,9 +27,8 @@ describe('The status bar text length info', () => { cy.getById('remainingCharacters').should('have.class', 'text-danger') }) - it('shows a warning and opens a modal', () => { + it('opens a modal', () => { cy.setCodemirrorContent(tooMuchTestContent) cy.getById('limitReachedModal').should('be.visible') - cy.getIframeBody().findById('limitReachedMessage').should('be.visible') }) }) diff --git a/src/components/document-read-only-page/document-read-only-page-content.tsx b/src/components/document-read-only-page/document-read-only-page-content.tsx index 662fd15c2..561fbedb6 100644 --- a/src/components/document-read-only-page/document-read-only-page-content.tsx +++ b/src/components/document-read-only-page/document-read-only-page-content.tsx @@ -7,17 +7,17 @@ import React, { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods' -import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter' import { useApplicationState } from '../../hooks/common/use-application-state' import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer' import { DocumentInfobar } from './document-infobar' import { RenderIframe } from '../editor-page/renderer-pane/render-iframe' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message' +import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter' export const DocumentReadOnlyPageContent: React.FC = () => { useTranslation() - const markdownContent = useNoteMarkdownContentWithoutFrontmatter() + const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter() const noteDetails = useApplicationState((state) => state.noteDetails) useSendFrontmatterInfoFromReduxToRenderer() @@ -34,7 +34,7 @@ export const DocumentReadOnlyPageContent: React.FC = () => { /> diff --git a/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx index 1be416c76..9d4398a6e 100644 --- a/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx +++ b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx @@ -4,11 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Suspense, useCallback } from 'react' +import React, { Suspense, useCallback, useMemo } from 'react' import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner' export interface CheatsheetLineProps { - code: string + markdown: string onTaskCheckedChange: (newValue: boolean) => void } @@ -17,7 +17,8 @@ const HighlightedCode = React.lazy( ) const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer')) -export const CheatsheetLine: React.FC = ({ code, onTaskCheckedChange }) => { +export const CheatsheetLine: React.FC = ({ markdown, onTaskCheckedChange }) => { + const lines = useMemo(() => markdown.split('\n'), [markdown]) const checkboxClick = useCallback( (lineInMarkdown: number, newValue: boolean) => { onTaskCheckedChange(newValue) @@ -37,13 +38,13 @@ export const CheatsheetLine: React.FC = ({ code, onTaskChec - + diff --git a/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx b/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx index 7624db6b5..076f46157 100644 --- a/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx +++ b/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx @@ -51,7 +51,7 @@ export const CheatsheetTabContent: React.FC = () => { {codes.map((code) => ( - + ))} diff --git a/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx b/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx index 06db499a3..8a7875d69 100644 --- a/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx +++ b/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx @@ -7,10 +7,10 @@ import React from 'react' import type { RenderIframeProps } from '../renderer-pane/render-iframe' import { RenderIframe } from '../renderer-pane/render-iframe' -import { useNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/common/use-note-markdown-content-without-frontmatter' import { useSendFrontmatterInfoFromReduxToRenderer } from '../renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer' +import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter' -export type EditorDocumentRendererProps = Omit +export type EditorDocumentRendererProps = Omit /** * Renders the markdown content from the global application state with the iframe renderer. @@ -18,9 +18,8 @@ export type EditorDocumentRendererProps = Omit = (props) => { - const markdownContent = useNoteMarkdownContentWithoutFrontmatter() - useSendFrontmatterInfoFromReduxToRenderer() + const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter() - return + return } diff --git a/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts b/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts index 0f39e78bd..0c28a8e54 100644 --- a/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts +++ b/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts @@ -11,16 +11,16 @@ import { CommunicationMessageType } from '../../../render-page/window-post-messa /** * Sends the given markdown content to the renderer. * - * @param markdownContent The markdown content to send. + * @param markdownContentLines The markdown content to send. */ -export const useSendMarkdownToRenderer = (markdownContent: string): void => { +export const useSendMarkdownToRenderer = (markdownContentLines: string[]): void => { return useSendToRenderer( useMemo( () => ({ type: CommunicationMessageType.SET_MARKDOWN_CONTENT, - content: markdownContent + content: markdownContentLines }), - [markdownContent] + [markdownContentLines] ) ) } diff --git a/src/components/editor-page/renderer-pane/render-iframe.tsx b/src/components/editor-page/renderer-pane/render-iframe.tsx index 68ab6763e..ad6dd824d 100644 --- a/src/components/editor-page/renderer-pane/render-iframe.tsx +++ b/src/components/editor-page/renderer-pane/render-iframe.tsx @@ -37,7 +37,7 @@ export interface RenderIframeProps extends RendererProps { const log = new Logger('RenderIframe') export const RenderIframe: React.FC = ({ - markdownContent, + markdownContentLines, onTaskCheckedChange, scrollState, onFirstHeadingChange, @@ -133,7 +133,7 @@ export const RenderIframe: React.FC = ({ useEffectOnRenderTypeChange(rendererType, onIframeLoad) useSendScrollState(scrollState) useSendDarkModeStatusToRenderer(forcedDarkMode) - useSendMarkdownToRenderer(markdownContent) + useSendMarkdownToRenderer(markdownContentLines) return ( diff --git a/src/components/intro-page/hooks/use-intro-page-content.ts b/src/components/intro-page/hooks/use-intro-page-content.ts index 29c17ea19..5be3af5a2 100644 --- a/src/components/intro-page/hooks/use-intro-page-content.ts +++ b/src/components/intro-page/hooks/use-intro-page-content.ts @@ -9,14 +9,14 @@ import { useTranslation } from 'react-i18next' import { fetchFrontPageContent } from '../requests' import { useCustomizeAssetsUrl } from '../../../hooks/common/use-customize-assets-url' -export const useIntroPageContent = (): string | undefined => { +export const useIntroPageContent = (): string[] | undefined => { const { t } = useTranslation() - const [content, setContent] = useState(undefined) + const [content, setContent] = useState(undefined) const customizeAssetsUrl = useCustomizeAssetsUrl() useEffect(() => { fetchFrontPageContent(customizeAssetsUrl) - .then((content) => setContent(content)) + .then((content) => setContent(content.split('\n'))) .catch(() => setContent(undefined)) }, [customizeAssetsUrl, t]) diff --git a/src/components/intro-page/intro-page.tsx b/src/components/intro-page/intro-page.tsx index 59e7fcee9..d8eb32438 100644 --- a/src/components/intro-page/intro-page.tsx +++ b/src/components/intro-page/intro-page.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React from 'react' +import React, { useMemo } from 'react' import { Trans } from 'react-i18next' import { Branding } from '../common/branding/branding' import { @@ -16,7 +16,6 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe' import { CoverButtons } from './cover-buttons/cover-buttons' import { FeatureLinks } from './feature-links' import { useIntroPageContent } from './hooks/use-intro-page-content' -import { ShowIf } from '../common/show-if/show-if' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message' import { WaitSpinner } from '../common/wait-spinner/wait-spinner' import { useApplicationState } from '../../hooks/common/use-application-state' @@ -26,6 +25,25 @@ export const IntroPage: React.FC = () => { const introPageContent = useIntroPageContent() const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) + const spinner = useMemo(() => { + if (!rendererReady && introPageContent !== undefined) { + return + } + }, [introPageContent, rendererReady]) + + const introContent = useMemo(() => { + if (introPageContent !== undefined) { + return ( + + ) + } + }, [introPageContent]) + return (
@@ -39,17 +57,8 @@ export const IntroPage: React.FC = () => {
- - - - - - + {spinner} + {introContent}
diff --git a/src/components/markdown-renderer/common-markdown-renderer-props.ts b/src/components/markdown-renderer/common-markdown-renderer-props.ts index 24e287d0d..3fb9190cb 100644 --- a/src/components/markdown-renderer/common-markdown-renderer-props.ts +++ b/src/components/markdown-renderer/common-markdown-renderer-props.ts @@ -18,5 +18,5 @@ export interface CommonMarkdownRendererProps { newlinesAreBreaks?: boolean lineOffset?: number className?: string - content: string + markdownContentLines: string[] } diff --git a/src/components/markdown-renderer/document-length-limit-reached-alert.tsx b/src/components/markdown-renderer/document-length-limit-reached-alert.tsx deleted file mode 100644 index 2c9a2334d..000000000 --- a/src/components/markdown-renderer/document-length-limit-reached-alert.tsx +++ /dev/null @@ -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 = ({ show }) => { - useTranslation() - - const maxLength = useApplicationState((state) => state.config.maxDocumentLength) - - return ( - - - - - - ) -} diff --git a/src/components/markdown-renderer/document-markdown-renderer.tsx b/src/components/markdown-renderer/document-markdown-renderer.tsx index 7eb40cd88..218ae0194 100644 --- a/src/components/markdown-renderer/document-markdown-renderer.tsx +++ b/src/components/markdown-renderer/document-markdown-renderer.tsx @@ -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 = ({ className, - content, + markdownContentLines, onFirstHeadingChange, onLineMarkerPositionChanged, onTaskCheckedChange, @@ -40,7 +38,6 @@ export const DocumentMarkdownRenderer: React.FC = const markdownBodyRef = useRef(null) const currentLineMarkers = useRef() const tocAst = useRef() - const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content) const extensions = useMarkdownExtensions( baseUrl, @@ -51,7 +48,7 @@ export const DocumentMarkdownRenderer: React.FC = 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 = onLineMarkerPositionChanged, markdownBodyRef.current?.offsetTop ?? 0 ) - useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange) + const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange) + useEffect(() => { + extractFirstHeadline() + }, [extractFirstHeadline, markdownContentLines]) + useOnRefChange(tocAst, onTocChange) return (
-
diff --git a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts index 2b89e50dc..f21330545 100644 --- a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts +++ b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts @@ -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]) } diff --git a/src/components/markdown-renderer/hooks/use-extract-first-headline.ts b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts index 6a7e21af6..67ebcfa11 100644 --- a/src/components/markdown-renderer/hooks/use-extract-first-headline.ts +++ b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts @@ -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, - 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() - 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]) } diff --git a/src/components/markdown-renderer/hooks/use-reveal.ts b/src/components/markdown-renderer/hooks/use-reveal.ts index 6400dc65c..38dd3df07 100644 --- a/src/components/markdown-renderer/hooks/use-reveal.ts +++ b/src/components/markdown-renderer/hooks/use-reveal.ts @@ -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() const [revealStatus, setRevealStatus] = useState(REVEAL_STATUS.NOT_INITIALISED) const currentSlideState = useRef(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 ( diff --git a/src/components/markdown-renderer/hooks/use-trimmed-content.ts b/src/components/markdown-renderer/hooks/use-trimmed-content.ts deleted file mode 100644 index 595077e62..000000000 --- a/src/components/markdown-renderer/hooks/use-trimmed-content.ts +++ /dev/null @@ -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] -} diff --git a/src/components/markdown-renderer/slideshow-markdown-renderer.tsx b/src/components/markdown-renderer/slideshow-markdown-renderer.tsx index ad1537e03..5773c30e1 100644 --- a/src/components/markdown-renderer/slideshow-markdown-renderer.tsx +++ b/src/components/markdown-renderer/slideshow-markdown-renderer.tsx @@ -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 = ({ className, - content, + markdownContentLines, onFirstHeadingChange, onTaskCheckedChange, onTocChange, @@ -39,7 +37,6 @@ export const SlideshowMarkdownRenderer: React.FC { const markdownBodyRef = useRef(null) const tocAst = useRef() - const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content) const extensions = useMarkdownExtensions( baseUrl, @@ -51,14 +48,18 @@ export const SlideshowMarkdownRenderer: React.FC { + 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 - -
-
- {slideShowDOM} -
+
+
+ {slideShowDOM}
- +
) } diff --git a/src/components/markdown-renderer/utils/line-id-mapper.test.ts b/src/components/markdown-renderer/utils/line-id-mapper.test.ts index 16244524b..4b73d61f7 100644 --- a/src/components/markdown-renderer/utils/line-id-mapper.test.ts +++ b/src/components/markdown-renderer/utils/line-id-mapper.test.ts @@ -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 diff --git a/src/components/markdown-renderer/utils/line-id-mapper.ts b/src/components/markdown-renderer/utils/line-id-mapper.ts index 79b9ae1ca..7e115c7c8 100644 --- a/src/components/markdown-renderer/utils/line-id-mapper.ts +++ b/src/components/markdown-renderer/utils/line-id-mapper.ts @@ -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 diff --git a/src/components/render-page/iframe-markdown-renderer.tsx b/src/components/render-page/iframe-markdown-renderer.tsx index 7c0f2703a..78f819c89 100644 --- a/src/components/render-page/iframe-markdown-renderer.tsx +++ b/src/components/render-page/iframe-markdown-renderer.tsx @@ -20,7 +20,7 @@ import { initialState } from '../../redux/note-details/initial-state' import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details' export const IframeMarkdownRenderer: React.FC = () => { - const [markdownContent, setMarkdownContent] = useState('') + const [markdownContentLines, setMarkdownContentLines] = useState([]) const [scrollState, setScrollState] = useState({ firstLineInView: 1, scrolledPercentage: 0 }) const [baseConfiguration, setBaseConfiguration] = useState(undefined) const [frontmatterInfo, setFrontmatterInfo] = useState(initialState.frontmatterRendererInfo) @@ -39,7 +39,7 @@ export const IframeMarkdownRenderer: React.FC = () => { setBaseConfiguration(values.baseConfiguration) ) useRendererReceiveHandler(CommunicationMessageType.SET_MARKDOWN_CONTENT, (values) => - setMarkdownContent(values.content) + setMarkdownContentLines(values.content) ) useRendererReceiveHandler(CommunicationMessageType.SET_DARKMODE, (values) => setDarkMode(values.activated)) useRendererReceiveHandler(CommunicationMessageType.SET_SCROLL_STATE, (values) => setScrollState(values.scrollState)) @@ -106,7 +106,7 @@ export const IframeMarkdownRenderer: React.FC = () => { return ( { case RendererType.SLIDESHOW: return ( { return ( void onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void documentRenderPaneRef?: MutableRefObject - markdownContent: string + markdownContentLines: string[] onImageClick?: ImageClickHandler onHeightChange?: (height: number) => void } @@ -44,7 +44,7 @@ export const MarkdownDocument: React.FC = ({ onMakeScrollSource, onTaskCheckedChange, baseUrl, - markdownContent, + markdownContentLines, onImageClick, onScroll, scrollState, @@ -70,7 +70,7 @@ export const MarkdownDocument: React.FC = ({ onHeightChange(rendererSize.height ? rendererSize.height + 1 : 0) }, [rendererSize.height, onHeightChange]) - const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent]) + const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines]) const [onLineMarkerPositionChanged, onUserScroll] = useDocumentSyncScrolling( internalDocumentRenderPaneRef, rendererRef, @@ -92,7 +92,7 @@ export const MarkdownDocument: React.FC = ({ { - const markdownContent = useNoteMarkdownContentWithoutFrontmatter() + const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter() useTranslation() useSendFrontmatterInfoFromReduxToRenderer() @@ -21,7 +21,7 @@ export const SlideShowPageContent: React.FC = () => {
diff --git a/src/hooks/common/use-note-markdown-content-without-frontmatter.ts b/src/hooks/common/use-note-markdown-content-without-frontmatter.ts deleted file mode 100644 index edb2940ef..000000000 --- a/src/hooks/common/use-note-markdown-content-without-frontmatter.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useNoteMarkdownContent } from './use-note-markdown-content' -import { useApplicationState } from './use-application-state' -import { useMemo } from 'react' - -/** - * Extracts the markdown content of the current note from the global application state and removes the frontmatter. - * @return the markdown content of the note without frontmatter - */ -export const useNoteMarkdownContentWithoutFrontmatter = (): string => { - const markdownContent = useNoteMarkdownContent() - const lineOffset = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo.lineOffset) - - return useMemo(() => markdownContent.split('\n').slice(lineOffset).join('\n'), [markdownContent, lineOffset]) -} diff --git a/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts b/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts new file mode 100644 index 000000000..65a0f818c --- /dev/null +++ b/src/hooks/common/use-trimmed-note-markdown-content-without-frontmatter.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useMemo } from 'react' +import { useApplicationState } from './use-application-state' +import { useNoteMarkdownContent } from './use-note-markdown-content' + +export const useTrimmedNoteMarkdownContentWithoutFrontmatter = (): string[] => { + const maxLength = useApplicationState((state) => state.config.maxDocumentLength) + const markdownContent = useNoteMarkdownContent() + const markdownContentLines = useApplicationState((state) => state.noteDetails.markdownContentLines) + const lineOffset = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo.lineOffset) + + const trimmedLines = useMemo(() => { + if (markdownContent.length > maxLength) { + return markdownContent.slice(0, maxLength).split('\n') + } else { + return markdownContentLines + } + }, [markdownContent, markdownContentLines, maxLength]) + + return useMemo(() => { + return trimmedLines.slice(lineOffset) + }, [lineOffset, trimmedLines]) +} diff --git a/src/redux/note-details/frontmatter-extractor/extractor.test.ts b/src/redux/note-details/frontmatter-extractor/extractor.test.ts index 9cc20df37..a070091d7 100644 --- a/src/redux/note-details/frontmatter-extractor/extractor.test.ts +++ b/src/redux/note-details/frontmatter-extractor/extractor.test.ts @@ -10,47 +10,47 @@ import type { PresentFrontmatterExtractionResult } from './types' describe('frontmatter extraction', () => { describe('isPresent property', () => { it('is false when note does not contain three dashes at all', () => { - const testNote = 'abcdef\nmore text' + const testNote = ['abcdef', 'more text'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is false when note does not start with three dashes', () => { - const testNote = '\n---\nthis is not frontmatter' + const testNote = ['', '---', 'this is not frontmatter'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is false when note start with less than three dashes', () => { - const testNote = '--\nthis is not frontmatter' + const testNote = ['--', 'this is not frontmatter'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is false when note starts with three dashes but contains other characters in the same line', () => { - const testNote = '--- a\nthis is not frontmatter' + const testNote = ['--- a', 'this is not frontmatter'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is false when note has no ending marker for frontmatter', () => { - const testNote = '---\nthis is not frontmatter\nbecause\nthere is no\nend marker' + const testNote = ['---', 'this is not frontmatter', 'because', 'there is no', 'end marker'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is false when note end marker is present but with not the same amount of dashes as start marker', () => { - const testNote = '---\nthis is not frontmatter\n----\ncontent' + const testNote = ['---', 'this is not frontmatter', '----', 'content'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(false) }) it('is true when note end marker is present with the same amount of dashes as start marker', () => { - const testNote = '---\nthis is frontmatter\n---\ncontent' + const testNote = ['---', 'this is frontmatter', '---', 'content'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(true) }) it('is true when note end marker is present with the same amount of dashes as start marker but without content', () => { - const testNote = '---\nthis is frontmatter\n---' + const testNote = ['---', 'this is frontmatter', '---'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(true) }) it('is true when note end marker is present with the same amount of dots as start marker', () => { - const testNote = '---\nthis is frontmatter\n...\ncontent' + const testNote = ['---', 'this is frontmatter', '...', 'content'] const extraction = extractFrontmatter(testNote) expect(extraction.isPresent).toBe(true) }) @@ -58,22 +58,22 @@ describe('frontmatter extraction', () => { describe('lineOffset property', () => { it('is correct for single line frontmatter without content', () => { - const testNote = '---\nsingle line frontmatter\n...' + const testNote = ['---', 'single line frontmatter', '...'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.lineOffset).toEqual(3) }) it('is correct for single line frontmatter with content', () => { - const testNote = '---\nsingle line frontmatter\n...\ncontent' + const testNote = ['---', 'single line frontmatter', '...', 'content'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.lineOffset).toEqual(3) }) it('is correct for multi-line frontmatter without content', () => { - const testNote = '---\nabc\n123\ndef\n...' + const testNote = ['---', 'abc', '123', 'def', '...'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.lineOffset).toEqual(5) }) it('is correct for multi-line frontmatter with content', () => { - const testNote = '---\nabc\n123\ndef\n...\ncontent' + const testNote = ['---', 'abc', '123', 'def', '...', 'content'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.lineOffset).toEqual(5) }) @@ -81,12 +81,12 @@ describe('frontmatter extraction', () => { describe('rawText property', () => { it('contains single-line frontmatter text', () => { - const testNote = '---\nsingle-line\n...\ncontent' + const testNote = ['---', 'single-line', '...', 'content'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.rawText).toEqual('single-line') }) it('contains multi-line frontmatter text', () => { - const testNote = '---\nmulti\nline\n...\ncontent' + const testNote = ['---', 'multi', 'line', '...', 'content'] const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult expect(extraction.rawText).toEqual('multi\nline') }) diff --git a/src/redux/note-details/frontmatter-extractor/extractor.ts b/src/redux/note-details/frontmatter-extractor/extractor.ts index 0cc7f4a18..e5f3e6be4 100644 --- a/src/redux/note-details/frontmatter-extractor/extractor.ts +++ b/src/redux/note-details/frontmatter-extractor/extractor.ts @@ -12,14 +12,13 @@ const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/ * Extracts a frontmatter block from a given multiline string. * A valid frontmatter block requires the content to start with a line containing at least three dashes. * The block is terminated by a line containing the same amount of dashes or dots as the first line. - * @param content The multiline string from which the frontmatter should be extracted. + * @param lines The lines from which the frontmatter should be extracted. * @return { isPresent } false if no frontmatter block could be found, true if a block was found. * { rawFrontmatterText } if a block was found, this property contains the extracted text without the fencing. * { frontmatterLines } if a block was found, this property contains the number of lines to skip from the * given multiline string for retrieving the non-frontmatter content. */ -export const extractFrontmatter = (content: string): FrontmatterExtractionResult => { - const lines = content.split('\n') +export const extractFrontmatter = (lines: string[]): FrontmatterExtractionResult => { if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) { return { isPresent: false diff --git a/src/redux/note-details/initial-state.ts b/src/redux/note-details/initial-state.ts index 6d3778678..bc8861014 100644 --- a/src/redux/note-details/initial-state.ts +++ b/src/redux/note-details/initial-state.ts @@ -19,6 +19,7 @@ export const initialSlideOptions: SlideOptions = { export const initialState: NoteDetails = { markdownContent: '', + markdownContentLines: [], rawFrontmatter: '', frontmatterRendererInfo: { frontmatterInvalid: false, diff --git a/src/redux/note-details/reducer.ts b/src/redux/note-details/reducer.ts index c779b1cd6..65a8d6bab 100644 --- a/src/redux/note-details/reducer.ts +++ b/src/redux/note-details/reducer.ts @@ -74,7 +74,7 @@ const buildStateFromTaskListUpdate = ( changedLine: number, checkboxChecked: boolean ): NoteDetails => { - const lines = state.markdownContent.split('\n') + const lines = state.markdownContentLines const results = TASK_REGEX.exec(lines[changedLine]) if (results) { const before = results[1] @@ -88,23 +88,26 @@ const buildStateFromTaskListUpdate = ( /** * Builds a {@link NoteDetails} redux state from a fresh document content. * @param state The previous redux state. - * @param markdownContent The fresh document content consisting of the frontmatter and markdown part. + * @param newMarkdownContent The fresh document content consisting of the frontmatter and markdown part. * @return An updated {@link NoteDetails} redux state. */ -const buildStateFromMarkdownContentUpdate = (state: NoteDetails, markdownContent: string): NoteDetails => { - const frontmatterExtraction = extractFrontmatter(markdownContent) +const buildStateFromMarkdownContentUpdate = (state: NoteDetails, newMarkdownContent: string): NoteDetails => { + const markdownContentLines = newMarkdownContent.split('\n') + const frontmatterExtraction = extractFrontmatter(markdownContentLines) if (frontmatterExtraction.isPresent) { return buildStateFromFrontmatterUpdate( { ...state, - markdownContent: markdownContent + markdownContent: newMarkdownContent, + markdownContentLines: markdownContentLines }, frontmatterExtraction ) } else { return { ...state, - markdownContent: markdownContent, + markdownContent: newMarkdownContent, + markdownContentLines: markdownContentLines, rawFrontmatter: '', noteTitle: generateNoteTitle(initialState.frontmatter, state.firstHeading), frontmatter: initialState.frontmatter, @@ -193,6 +196,7 @@ const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string) const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => { return { markdownContent: note.content, + markdownContentLines: note.content.split('\n'), rawFrontmatter: '', frontmatterRendererInfo: initialState.frontmatterRendererInfo, frontmatter: initialState.frontmatter, diff --git a/src/redux/note-details/types/note-details.ts b/src/redux/note-details/types/note-details.ts index 4052d7bf2..b3d197204 100644 --- a/src/redux/note-details/types/note-details.ts +++ b/src/redux/note-details/types/note-details.ts @@ -13,6 +13,7 @@ import type { ISO6391 } from './iso6391' */ export interface NoteDetails { markdownContent: string + markdownContentLines: string[] rawFrontmatter: string frontmatter: NoteFrontmatter frontmatterRendererInfo: RendererFrontmatterInfo