mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-23 08:44:06 +00: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
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 = () => {
|
|||
/>
|
||||
<RenderIframe
|
||||
frameClasses={'flex-fill h-100 w-100'}
|
||||
markdownContent={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||
rendererType={RendererType.DOCUMENT}
|
||||
/>
|
||||
|
|
|
@ -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<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
|
||||
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ 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<CheatsheetLineProps> = ({ code, onTaskChec
|
|||
<tr>
|
||||
<td>
|
||||
<DocumentMarkdownRenderer
|
||||
content={code}
|
||||
markdownContentLines={lines}
|
||||
baseUrl={'https://example.org'}
|
||||
onTaskCheckedChange={checkboxClick}
|
||||
/>
|
||||
</td>
|
||||
<td className={'markdown-body'}>
|
||||
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
||||
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
||||
</td>
|
||||
</tr>
|
||||
</Suspense>
|
||||
|
|
|
@ -51,7 +51,7 @@ export const CheatsheetTabContent: React.FC = () => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{codes.map((code) => (
|
||||
<CheatsheetLine code={code} key={code} onTaskCheckedChange={setChecked} />
|
||||
<CheatsheetLine markdown={code} key={code} onTaskCheckedChange={setChecked} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
|
|
@ -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<RenderIframeProps, 'markdownContent'>
|
||||
export type EditorDocumentRendererProps = Omit<RenderIframeProps, 'markdownContentLines'>
|
||||
|
||||
/**
|
||||
* Renders the markdown content from the global application state with the iframe renderer.
|
||||
|
@ -18,9 +18,8 @@ export type EditorDocumentRendererProps = Omit<RenderIframeProps, 'markdownConte
|
|||
* @param props Every property from the {@link RenderIframe} except the markdown content.
|
||||
*/
|
||||
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (props) => {
|
||||
const markdownContent = useNoteMarkdownContentWithoutFrontmatter()
|
||||
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||
|
||||
return <RenderIframe markdownContent={markdownContent} {...props} />
|
||||
return <RenderIframe {...props} markdownContentLines={trimmedContentLines} />
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export interface RenderIframeProps extends RendererProps {
|
|||
const log = new Logger('RenderIframe')
|
||||
|
||||
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||
markdownContent,
|
||||
markdownContentLines,
|
||||
onTaskCheckedChange,
|
||||
scrollState,
|
||||
onFirstHeadingChange,
|
||||
|
@ -133,7 +133,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
||||
useSendScrollState(scrollState)
|
||||
useSendDarkModeStatusToRenderer(forcedDarkMode)
|
||||
useSendMarkdownToRenderer(markdownContent)
|
||||
useSendMarkdownToRenderer(markdownContentLines)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
|
|
@ -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<string | undefined>(undefined)
|
||||
const [content, setContent] = useState<string[] | undefined>(undefined)
|
||||
const customizeAssetsUrl = useCustomizeAssetsUrl()
|
||||
|
||||
useEffect(() => {
|
||||
fetchFrontPageContent(customizeAssetsUrl)
|
||||
.then((content) => setContent(content))
|
||||
.then((content) => setContent(content.split('\n')))
|
||||
.catch(() => setContent(undefined))
|
||||
}, [customizeAssetsUrl, t])
|
||||
|
||||
|
|
|
@ -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 <WaitSpinner />
|
||||
}
|
||||
}, [introPageContent, rendererReady])
|
||||
|
||||
const introContent = useMemo(() => {
|
||||
if (introPageContent !== undefined) {
|
||||
return (
|
||||
<RenderIframe
|
||||
frameClasses={'w-100 overflow-y-hidden'}
|
||||
markdownContentLines={introPageContent}
|
||||
rendererType={RendererType.INTRO}
|
||||
forcedDarkMode={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}, [introPageContent])
|
||||
|
||||
return (
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<div className={'flex-fill mt-3'}>
|
||||
|
@ -39,17 +57,8 @@ export const IntroPage: React.FC = () => {
|
|||
<Branding delimiter={false} />
|
||||
</div>
|
||||
<CoverButtons />
|
||||
<ShowIf condition={!rendererReady && introPageContent !== undefined}>
|
||||
<WaitSpinner />
|
||||
</ShowIf>
|
||||
<ShowIf condition={!!introPageContent}>
|
||||
<RenderIframe
|
||||
frameClasses={'w-100 overflow-y-hidden'}
|
||||
markdownContent={introPageContent as string}
|
||||
rendererType={RendererType.INTRO}
|
||||
forcedDarkMode={true}
|
||||
/>
|
||||
</ShowIf>
|
||||
{spinner}
|
||||
{introContent}
|
||||
<hr className={'mb-5'} />
|
||||
</div>
|
||||
<FeatureLinks />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string[]>([])
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>(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 (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={'vh-100 bg-light'}
|
||||
markdownContent={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={onMakeScrollSource}
|
||||
|
@ -120,7 +120,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
case RendererType.SLIDESHOW:
|
||||
return (
|
||||
<SlideshowMarkdownRenderer
|
||||
content={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onImageClick={onImageClick}
|
||||
|
@ -133,7 +133,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
return (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
|
||||
markdownContent={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onImageClick={onImageClick}
|
||||
disableToc={true}
|
||||
|
|
|
@ -24,7 +24,7 @@ export interface RendererProps extends ScrollProps {
|
|||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||
markdownContent: string
|
||||
markdownContentLines: string[]
|
||||
onImageClick?: ImageClickHandler
|
||||
onHeightChange?: (height: number) => void
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
onMakeScrollSource,
|
||||
onTaskCheckedChange,
|
||||
baseUrl,
|
||||
markdownContent,
|
||||
markdownContentLines,
|
||||
onImageClick,
|
||||
onScroll,
|
||||
scrollState,
|
||||
|
@ -70,7 +70,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
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<MarkdownDocumentProps> = ({
|
|||
<DocumentMarkdownRenderer
|
||||
outerContainerRef={rendererRef}
|
||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||
content={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
|
|
|
@ -62,7 +62,7 @@ export interface ImageClickedMessage {
|
|||
|
||||
export interface SetMarkdownContentMessage {
|
||||
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
|
||||
content: string
|
||||
content: string[]
|
||||
}
|
||||
|
||||
export interface SetScrollStateMessage {
|
||||
|
|
|
@ -10,10 +10,10 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
|||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||
import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter'
|
||||
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
||||
|
||||
export const SlideShowPageContent: React.FC = () => {
|
||||
const markdownContent = useNoteMarkdownContentWithoutFrontmatter()
|
||||
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||
useTranslation()
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
|
||||
|
@ -21,7 +21,7 @@ export const SlideShowPageContent: React.FC = () => {
|
|||
<div className={'vh-100 vw-100'}>
|
||||
<RenderIframe
|
||||
frameClasses={'h-100 w-100'}
|
||||
markdownContent={markdownContent}
|
||||
markdownContentLines={markdownContentLines}
|
||||
rendererType={RendererType.SLIDESHOW}
|
||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||
/>
|
||||
|
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,6 +19,7 @@ export const initialSlideOptions: SlideOptions = {
|
|||
|
||||
export const initialState: NoteDetails = {
|
||||
markdownContent: '',
|
||||
markdownContentLines: [],
|
||||
rawFrontmatter: '',
|
||||
frontmatterRendererInfo: {
|
||||
frontmatterInvalid: false,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { ISO6391 } from './iso6391'
|
|||
*/
|
||||
export interface NoteDetails {
|
||||
markdownContent: string
|
||||
markdownContentLines: string[]
|
||||
rawFrontmatter: string
|
||||
frontmatter: NoteFrontmatter
|
||||
frontmatterRendererInfo: RendererFrontmatterInfo
|
||||
|
|
Loading…
Reference in a new issue