mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
feat: move first heading title extraction into an app extension
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
3529e68511
commit
64e3513988
17 changed files with 126 additions and 168 deletions
|
@ -74,7 +74,7 @@ describe('Document Title', () => {
|
||||||
it('katex code looks right', () => {
|
it('katex code looks right', () => {
|
||||||
cy.setCodemirrorContent(`# $\\alpha$-foo`)
|
cy.setCodemirrorContent(`# $\\alpha$-foo`)
|
||||||
cy.getIframeBody().find('h1').should('contain', 'α')
|
cy.getIframeBody().find('h1').should('contain', 'α')
|
||||||
cy.title().should('eq', `α-foo - HedgeDoc @ ${branding.name}`)
|
cy.title().should('eq', `\\alpha-foo - HedgeDoc @ ${branding.name}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
||||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
|
||||||
import { setRendererStatus } from '../../redux/renderer-status/methods'
|
import { setRendererStatus } from '../../redux/renderer-status/methods'
|
||||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||||
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
||||||
|
@ -27,7 +26,6 @@ export const DocumentReadOnlyPageContent: React.FC = () => {
|
||||||
<RenderIframe
|
<RenderIframe
|
||||||
frameClasses={'flex-fill h-100 w-100'}
|
frameClasses={'flex-fill h-100 w-100'}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
|
||||||
rendererType={RendererType.DOCUMENT}
|
rendererType={RendererType.DOCUMENT}
|
||||||
onRendererStatusChange={setRendererStatus}
|
onRendererStatusChange={setRendererStatus}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
|
||||||
import { Logger } from '../../utils/logger'
|
import { Logger } from '../../utils/logger'
|
||||||
import { MotdModal } from '../common/motd-modal/motd-modal'
|
import { MotdModal } from '../common/motd-modal/motd-modal'
|
||||||
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
|
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
|
||||||
|
@ -112,7 +111,6 @@ export const EditorPageContent: React.FC = () => {
|
||||||
<EditorDocumentRenderer
|
<EditorDocumentRenderer
|
||||||
frameClasses={'h-100 w-100'}
|
frameClasses={'h-100 w-100'}
|
||||||
onMakeScrollSource={setRendererToScrollSource}
|
onMakeScrollSource={setRendererToScrollSource}
|
||||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
|
||||||
onScroll={onMarkdownRendererScroll}
|
onScroll={onMarkdownRendererScroll}
|
||||||
scrollState={scrollState.rendererScrollState}
|
scrollState={scrollState.rendererScrollState}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import type { RendererProps } from '../../render-page/markdown-document'
|
||||||
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
|
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
|
||||||
import type {
|
import type {
|
||||||
ExtensionEvent,
|
ExtensionEvent,
|
||||||
OnFirstHeadingChangeMessage,
|
|
||||||
OnHeightChangeMessage,
|
OnHeightChangeMessage,
|
||||||
RendererType,
|
RendererType,
|
||||||
SetScrollStateMessage
|
SetScrollStateMessage
|
||||||
|
@ -45,7 +44,6 @@ const log = new Logger('RenderIframe')
|
||||||
* @param markdownContentLines Array of lines of the markdown content
|
* @param markdownContentLines Array of lines of the markdown content
|
||||||
* @param onTaskCheckedChange Callback that is fired when a task-list item in the iframe is checked
|
* @param onTaskCheckedChange Callback that is fired when a task-list item in the iframe is checked
|
||||||
* @param scrollState The current {@link ScrollState}
|
* @param scrollState The current {@link ScrollState}
|
||||||
* @param onFirstHeadingChange Callback that is fired when the first heading of the note changes
|
|
||||||
* @param onScroll Callback that is fired when the user scrolls in the iframe
|
* @param onScroll Callback that is fired when the user scrolls in the iframe
|
||||||
* @param onMakeScrollSource Callback that is fired when the renderer requests to be set as the current scroll source
|
* @param onMakeScrollSource Callback that is fired when the renderer requests to be set as the current scroll source
|
||||||
* @param frameClasses CSS classes that should be applied to the iframe
|
* @param frameClasses CSS classes that should be applied to the iframe
|
||||||
|
@ -57,7 +55,6 @@ const log = new Logger('RenderIframe')
|
||||||
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
scrollState,
|
scrollState,
|
||||||
onFirstHeadingChange,
|
|
||||||
onScroll,
|
onScroll,
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
frameClasses,
|
frameClasses,
|
||||||
|
@ -92,14 +89,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
onRendererStatusChange?.(rendererReady)
|
onRendererStatusChange?.(rendererReady)
|
||||||
}, [onRendererStatusChange, rendererReady])
|
}, [onRendererStatusChange, rendererReady])
|
||||||
|
|
||||||
useEditorReceiveHandler(
|
|
||||||
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
|
||||||
useCallback(
|
|
||||||
(values: OnFirstHeadingChangeMessage) => onFirstHeadingChange?.(values.firstHeading),
|
|
||||||
[onFirstHeadingChange]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
useEditorReceiveHandler(
|
useEditorReceiveHandler(
|
||||||
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
|
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
|
||||||
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
|
|
||||||
export interface CommonMarkdownRendererProps {
|
export interface CommonMarkdownRendererProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
outerContainerRef?: Ref<HTMLDivElement>
|
outerContainerRef?: Ref<HTMLDivElement>
|
||||||
newlinesAreBreaks?: boolean
|
newlinesAreBreaks?: boolean
|
||||||
|
|
|
@ -10,10 +10,9 @@ import type { LineMarkers } from './extensions/linemarker/add-line-marker-markdo
|
||||||
import { LinemarkerMarkdownExtension } from './extensions/linemarker/linemarker-markdown-extension'
|
import { LinemarkerMarkdownExtension } from './extensions/linemarker/linemarker-markdown-extension'
|
||||||
import type { LineMarkerPosition } from './extensions/linemarker/types'
|
import type { LineMarkerPosition } from './extensions/linemarker/types'
|
||||||
import { useCalculateLineMarkerPosition } from './hooks/use-calculate-line-marker-positions'
|
import { useCalculateLineMarkerPosition } from './hooks/use-calculate-line-marker-positions'
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
|
||||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||||
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useMemo, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
|
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||||
|
@ -25,9 +24,7 @@ export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererPro
|
||||||
*
|
*
|
||||||
* @param className Additional class names directly given to the div
|
* @param className Additional class names directly given to the div
|
||||||
* @param markdownContentLines The markdown lines
|
* @param markdownContentLines The markdown lines
|
||||||
* @param onFirstHeadingChange The callback to call if the first heading changes.
|
|
||||||
* @param onLineMarkerPositionChanged The callback to call with changed {@link LineMarkers}
|
* @param onLineMarkerPositionChanged The callback to call with changed {@link LineMarkers}
|
||||||
* @param onTaskCheckedChange The callback to call if a task is checked or unchecked.
|
|
||||||
* @param baseUrl The base url of the renderer
|
* @param baseUrl The base url of the renderer
|
||||||
* @param outerContainerRef A reference for the outer container
|
* @param outerContainerRef A reference for the outer container
|
||||||
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
||||||
|
@ -35,7 +32,6 @@ export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererPro
|
||||||
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
|
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
|
||||||
className,
|
className,
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onFirstHeadingChange,
|
|
||||||
onLineMarkerPositionChanged,
|
onLineMarkerPositionChanged,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
outerContainerRef,
|
outerContainerRef,
|
||||||
|
@ -57,10 +53,6 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
||||||
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged)
|
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged)
|
||||||
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
|
||||||
useEffect(() => {
|
|
||||||
extractFirstHeadline()
|
|
||||||
}, [extractFirstHeadline, markdownContentLines])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={outerContainerRef} className={`position-relative`}>
|
<div ref={outerContainerRef} className={`position-relative`}>
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { MarkdownRendererExtensionOptions } from '../../../../extensions/base/app-extension'
|
||||||
|
import { AppExtension } from '../../../../extensions/base/app-extension'
|
||||||
|
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
|
||||||
|
import { ExtractFirstHeadlineEditorExtension } from './extract-first-headline-editor-extension'
|
||||||
|
import { ExtractFirstHeadlineMarkdownExtension } from './extract-first-headline-markdown-extension'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides first headline extraction
|
||||||
|
*/
|
||||||
|
export class ExtractFirstHeadlineAppExtension extends AppExtension {
|
||||||
|
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
|
||||||
|
return [new ExtractFirstHeadlineMarkdownExtension(options.eventEmitter)]
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEditorExtensionComponent(): React.FC {
|
||||||
|
return ExtractFirstHeadlineEditorExtension
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { updateNoteTitleByFirstHeading } from '../../../../redux/note-details/methods'
|
||||||
|
import { useExtensionEventEmitterHandler } from '../../hooks/use-extension-event-emitter'
|
||||||
|
import { ExtractFirstHeadlineNodeProcessor } from './extract-first-headline-node-processor'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives the {@link ExtractFirstHeadlineNodeProcessor.EVENT_NAME first heading extraction event}
|
||||||
|
* and saves the title in the global application state.
|
||||||
|
*/
|
||||||
|
export const ExtractFirstHeadlineEditorExtension: React.FC = () => {
|
||||||
|
useExtensionEventEmitterHandler(ExtractFirstHeadlineNodeProcessor.EVENT_NAME, updateNoteTitleByFirstHeading)
|
||||||
|
return null
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||||
|
import { EventMarkdownRendererExtension } from '../base/event-markdown-renderer-extension'
|
||||||
|
import { ExtractFirstHeadlineNodeProcessor } from './extract-first-headline-node-processor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds first heading extraction to the renderer
|
||||||
|
*/
|
||||||
|
export class ExtractFirstHeadlineMarkdownExtension extends EventMarkdownRendererExtension {
|
||||||
|
buildNodeProcessors(): NodeProcessor[] {
|
||||||
|
return [new ExtractFirstHeadlineNodeProcessor(this.eventEmitter)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||||
|
import { Optional } from '@mrdrogdrog/optional'
|
||||||
|
import type { Document, Node, Element } from 'domhandler'
|
||||||
|
import { isTag, isText } from 'domhandler'
|
||||||
|
import type { EventEmitter2 } from 'eventemitter2'
|
||||||
|
|
||||||
|
const headlineTagRegex = /^h[1-6]$/gi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for the first headline tag and extracts its plain text content.
|
||||||
|
*/
|
||||||
|
export class ExtractFirstHeadlineNodeProcessor extends NodeProcessor {
|
||||||
|
public static readonly EVENT_NAME = 'HeadlineExtracted'
|
||||||
|
|
||||||
|
constructor(private eventEmitter: EventEmitter2) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
process(nodes: Document): Document {
|
||||||
|
Optional.ofNullable(this.checkNodesForHeadline(nodes.children))
|
||||||
|
.map((foundHeadlineNode) => this.extractInnerTextFromNode(foundHeadlineNode).trim())
|
||||||
|
.filter((text) => text !== '')
|
||||||
|
.ifPresent((text) => this.eventEmitter.emit(ExtractFirstHeadlineNodeProcessor.EVENT_NAME, text))
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkNodesForHeadline(nodes: Node[]): Node | undefined {
|
||||||
|
return nodes.find((node) => isTag(node) && node.name.match(headlineTagRegex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractInnerTextFromNode(node: Node): string {
|
||||||
|
if (isText(node)) {
|
||||||
|
return node.nodeValue
|
||||||
|
} else if (isTag(node)) {
|
||||||
|
return this.extractInnerTextFromTag(node)
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractInnerTextFromTag(node: Element): string {
|
||||||
|
if (node.name === 'a' && this.findAttribute(node, 'class')?.value.includes('heading-anchor')) {
|
||||||
|
return ''
|
||||||
|
} else if (node.name === 'img') {
|
||||||
|
return this.findAttribute(node, 'alt')?.value ?? ''
|
||||||
|
} else {
|
||||||
|
return node.children.reduce((state, child) => {
|
||||||
|
return state + this.extractInnerTextFromNode(child)
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findAttribute(node: Element, attributeName: string) {
|
||||||
|
return node.attributes.find((attribute) => attribute.name === attributeName)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { Optional } from '@mrdrogdrog/optional'
|
|
||||||
import type React from 'react'
|
|
||||||
import { useCallback, useEffect, useMemo, 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 || isKatexMathMlElement(node) || isHeadlineLinkElement(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')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the given {@link ChildNode node} is the link icon of a heading.
|
|
||||||
* @param node The node to check
|
|
||||||
*/
|
|
||||||
const isHeadlineLinkElement = (node: ChildNode): boolean => (node as HTMLElement).classList?.contains('heading-anchor')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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>,
|
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
|
||||||
): (() => void) => {
|
|
||||||
const lastFirstHeadingContent = useRef<string | undefined>()
|
|
||||||
const currentFirstHeadingElement = useRef<HTMLHeadingElement | null>(null)
|
|
||||||
|
|
||||||
const extractHeaderText = useCallback(() => {
|
|
||||||
if (!onFirstHeadingChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const headingText = extractInnerText(currentFirstHeadingElement.current).trim()
|
|
||||||
if (headingText !== lastFirstHeadingContent.current) {
|
|
||||||
lastFirstHeadingContent.current = headingText
|
|
||||||
onFirstHeadingChange(headingText)
|
|
||||||
}
|
|
||||||
}, [onFirstHeadingChange])
|
|
||||||
|
|
||||||
const mutationObserver = useMemo(() => new MutationObserver(() => extractHeaderText()), [extractHeaderText])
|
|
||||||
useEffect(() => () => mutationObserver.disconnect(), [mutationObserver])
|
|
||||||
|
|
||||||
return useCallback(() => {
|
|
||||||
const foundFirstHeading = Optional.ofNullable(documentElement.current)
|
|
||||||
.map((currentDocumentElement) => currentDocumentElement.getElementsByTagName('h1').item(0))
|
|
||||||
.orElse(null)
|
|
||||||
if (foundFirstHeading === currentFirstHeadingElement.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mutationObserver.disconnect()
|
|
||||||
currentFirstHeadingElement.current = foundFirstHeading
|
|
||||||
if (foundFirstHeading !== null) {
|
|
||||||
mutationObserver.observe(foundFirstHeading, { subtree: true, childList: true })
|
|
||||||
}
|
|
||||||
extractHeaderText()
|
|
||||||
}, [documentElement, extractHeaderText, mutationObserver])
|
|
||||||
}
|
|
|
@ -6,13 +6,12 @@
|
||||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||||
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
|
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
|
||||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||||
import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
|
import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
|
||||||
import { LoadingSlide } from './loading-slide'
|
import { LoadingSlide } from './loading-slide'
|
||||||
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
||||||
import type { SlideOptions } from '@hedgedoc/commons'
|
import type { SlideOptions } from '@hedgedoc/commons'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||||
slideOptions?: SlideOptions
|
slideOptions?: SlideOptions
|
||||||
|
@ -23,8 +22,6 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
|
||||||
*
|
*
|
||||||
* @param className Additional class names directly given to the div
|
* @param className Additional class names directly given to the div
|
||||||
* @param markdownContentLines The markdown lines
|
* @param markdownContentLines The markdown lines
|
||||||
* @param onFirstHeadingChange The callback to call if the first heading changes.
|
|
||||||
* @param onLineMarkerPositionChanged The callback to call with changed {@link LineMarkers}
|
|
||||||
* @param baseUrl The base url of the renderer
|
* @param baseUrl The base url of the renderer
|
||||||
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
||||||
* @param slideOptions The {@link SlideOptions} to use
|
* @param slideOptions The {@link SlideOptions} to use
|
||||||
|
@ -32,7 +29,6 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
|
||||||
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
|
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
|
||||||
className,
|
className,
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onFirstHeadingChange,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
newlinesAreBreaks,
|
newlinesAreBreaks,
|
||||||
slideOptions
|
slideOptions
|
||||||
|
@ -46,13 +42,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
||||||
|
|
||||||
const revealStatus = useReveal(markdownContentLines, slideOptions)
|
const revealStatus = useReveal(markdownContentLines, slideOptions)
|
||||||
|
|
||||||
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
|
||||||
useEffect(() => {
|
|
||||||
if (revealStatus === REVEAL_STATUS.INITIALISED) {
|
|
||||||
extractFirstHeadline()
|
|
||||||
}
|
|
||||||
}, [extractFirstHeadline, markdownContentLines, revealStatus])
|
|
||||||
|
|
||||||
const slideShowDOM = useMemo(
|
const slideShowDOM = useMemo(
|
||||||
() =>
|
() =>
|
||||||
revealStatus === REVEAL_STATUS.INITIALISED ? (
|
revealStatus === REVEAL_STATUS.INITIALISED ? (
|
||||||
|
|
|
@ -73,16 +73,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
}, [communicator])
|
}, [communicator])
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFirstHeadingChange = useCallback(
|
|
||||||
(firstHeading?: string) => {
|
|
||||||
communicator.sendMessageToOtherSide({
|
|
||||||
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
|
||||||
firstHeading
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[communicator]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onMakeScrollSource = useCallback(() => {
|
const onMakeScrollSource = useCallback(() => {
|
||||||
sendScrolling.current = true
|
sendScrolling.current = true
|
||||||
communicator.sendMessageToOtherSide({
|
communicator.sendMessageToOtherSide({
|
||||||
|
@ -128,7 +118,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
<MarkdownDocument
|
<MarkdownDocument
|
||||||
additionalOuterContainerClasses={'vh-100 bg-light'}
|
additionalOuterContainerClasses={'vh-100 bg-light'}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
|
||||||
onMakeScrollSource={onMakeScrollSource}
|
onMakeScrollSource={onMakeScrollSource}
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
|
@ -141,7 +130,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
<SlideshowMarkdownRenderer
|
<SlideshowMarkdownRenderer
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
baseUrl={baseConfiguration.baseUrl}
|
baseUrl={baseConfiguration.baseUrl}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
slideOptions={slideOptions}
|
slideOptions={slideOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -159,16 +147,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}, [
|
}, [baseConfiguration, markdownContentLines, onHeightChange, onMakeScrollSource, onScroll, scrollState, slideOptions])
|
||||||
baseConfiguration,
|
|
||||||
markdownContentLines,
|
|
||||||
onFirstHeadingChange,
|
|
||||||
onHeightChange,
|
|
||||||
onMakeScrollSource,
|
|
||||||
onScroll,
|
|
||||||
scrollState,
|
|
||||||
slideOptions
|
|
||||||
])
|
|
||||||
|
|
||||||
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
|
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import type { MutableRefObject } from 'react'
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
|
||||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||||
markdownContentLines: string[]
|
markdownContentLines: string[]
|
||||||
onHeightChange?: (height: number) => void
|
onHeightChange?: (height: number) => void
|
||||||
|
@ -32,12 +31,9 @@ export interface MarkdownDocumentProps extends RendererProps {
|
||||||
*
|
*
|
||||||
* @param additionalOuterContainerClasses Additional classes given to the outer container directly
|
* @param additionalOuterContainerClasses Additional classes given to the outer container directly
|
||||||
* @param additionalRendererClasses Additional classes given {@link DocumentMarkdownRenderer} directly
|
* @param additionalRendererClasses Additional classes given {@link DocumentMarkdownRenderer} directly
|
||||||
* @param onFirstHeadingChange The callback to call when the first heading changes.
|
|
||||||
* @param onMakeScrollSource The callback to call if a change of the scroll source is requested-
|
* @param onMakeScrollSource The callback to call if a change of the scroll source is requested-
|
||||||
* @param onTaskCheckedChange The callback to call if a task get's checked or unchecked.
|
|
||||||
* @param baseUrl The base url for the renderer
|
* @param baseUrl The base url for the renderer
|
||||||
* @param markdownContentLines The current content of the markdown document.
|
* @param markdownContentLines The current content of the markdown document.
|
||||||
* @param onImageClick The callback to call if an image is clicked.
|
|
||||||
* @param onScroll The callback to call if the renderer is scrolling.
|
* @param onScroll The callback to call if the renderer is scrolling.
|
||||||
* @param scrollState The current {@link ScrollState}
|
* @param scrollState The current {@link ScrollState}
|
||||||
* @param onHeightChange The callback to call if the height of the document changes
|
* @param onHeightChange The callback to call if the height of the document changes
|
||||||
|
@ -47,7 +43,6 @@ export interface MarkdownDocumentProps extends RendererProps {
|
||||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
additionalOuterContainerClasses,
|
additionalOuterContainerClasses,
|
||||||
additionalRendererClasses,
|
additionalRendererClasses,
|
||||||
onFirstHeadingChange,
|
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
|
@ -94,7 +89,6 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
outerContainerRef={rendererRef}
|
outerContainerRef={rendererRef}
|
||||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
|
||||||
onLineMarkerPositionChanged={recalculateLineMarkers}
|
onLineMarkerPositionChanged={recalculateLineMarkers}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
newlinesAreBreaks={newlinesAreBreaks}
|
newlinesAreBreaks={newlinesAreBreaks}
|
||||||
|
|
|
@ -11,7 +11,6 @@ export enum CommunicationMessageType {
|
||||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
RENDERER_READY = 'RENDERER_READY',
|
RENDERER_READY = 'RENDERER_READY',
|
||||||
SET_DARKMODE = 'SET_DARKMODE',
|
SET_DARKMODE = 'SET_DARKMODE',
|
||||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
|
||||||
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
||||||
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
||||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||||
|
@ -72,11 +71,6 @@ export interface SetScrollStateMessage {
|
||||||
scrollState: ScrollState
|
scrollState: ScrollState
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnFirstHeadingChangeMessage {
|
|
||||||
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
|
||||||
firstHeading: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetSlideOptionsMessage {
|
export interface SetSlideOptionsMessage {
|
||||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||||
slideOptions: SlideOptions
|
slideOptions: SlideOptions
|
||||||
|
@ -101,7 +95,6 @@ export type CommunicationMessages =
|
||||||
| GetWordCountMessage
|
| GetWordCountMessage
|
||||||
| SetMarkdownContentMessage
|
| SetMarkdownContentMessage
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| OnFirstHeadingChangeMessage
|
|
||||||
| SetSlideOptionsMessage
|
| SetSlideOptionsMessage
|
||||||
| OnHeightChangeMessage
|
| OnHeightChangeMessage
|
||||||
| OnWordCountCalculatedMessage
|
| OnWordCountCalculatedMessage
|
||||||
|
@ -120,7 +113,6 @@ export type EditorToRendererMessageType =
|
||||||
export type RendererToEditorMessageType =
|
export type RendererToEditorMessageType =
|
||||||
| CommunicationMessageType.RENDERER_READY
|
| CommunicationMessageType.RENDERER_READY
|
||||||
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||||
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
|
||||||
| CommunicationMessageType.SET_SCROLL_STATE
|
| CommunicationMessageType.SET_SCROLL_STATE
|
||||||
| CommunicationMessageType.ON_HEIGHT_CHANGE
|
| CommunicationMessageType.ON_HEIGHT_CHANGE
|
||||||
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
||||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
|
||||||
import { setRendererStatus } from '../../redux/renderer-status/methods'
|
import { setRendererStatus } from '../../redux/renderer-status/methods'
|
||||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||||
import { useSendToRenderer } from '../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
import { useSendToRenderer } from '../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||||
|
@ -42,7 +41,6 @@ export const SlideShowPageContent: React.FC = () => {
|
||||||
frameClasses={'h-100 w-100'}
|
frameClasses={'h-100 w-100'}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
rendererType={RendererType.SLIDESHOW}
|
rendererType={RendererType.SLIDESHOW}
|
||||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
|
||||||
onRendererStatusChange={setRendererStatus}
|
onRendererStatusChange={setRendererStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { BasicMarkdownSyntaxAppExtension } from '../../components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension'
|
import { BasicMarkdownSyntaxAppExtension } from '../../components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension'
|
||||||
import { BootstrapIconAppExtension } from '../../components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension'
|
import { BootstrapIconAppExtension } from '../../components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension'
|
||||||
import { EmojiAppExtension } from '../../components/markdown-renderer/extensions/emoji/emoji-app-extension'
|
import { EmojiAppExtension } from '../../components/markdown-renderer/extensions/emoji/emoji-app-extension'
|
||||||
|
import { ExtractFirstHeadlineAppExtension } from '../../components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-app-extension'
|
||||||
import { IframeCapsuleAppExtension } from '../../components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension'
|
import { IframeCapsuleAppExtension } from '../../components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension'
|
||||||
import { ImagePlaceholderAppExtension } from '../../components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension'
|
import { ImagePlaceholderAppExtension } from '../../components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension'
|
||||||
import { TableOfContentsAppExtension } from '../../components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension'
|
import { TableOfContentsAppExtension } from '../../components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension'
|
||||||
|
@ -60,5 +61,6 @@ export const optionalAppExtensions: AppExtension[] = [
|
||||||
new TableOfContentsAppExtension(),
|
new TableOfContentsAppExtension(),
|
||||||
new ImagePlaceholderAppExtension(),
|
new ImagePlaceholderAppExtension(),
|
||||||
new IframeCapsuleAppExtension(),
|
new IframeCapsuleAppExtension(),
|
||||||
new BasicMarkdownSyntaxAppExtension()
|
new BasicMarkdownSyntaxAppExtension(),
|
||||||
|
new ExtractFirstHeadlineAppExtension()
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue