=
useTranslation()
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged)
- const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
- useEffect(() => {
- extractFirstHeadline()
- }, [extractFirstHeadline, markdownContentLines])
return (
diff --git a/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-app-extension.ts
new file mode 100644
index 000000000..9026c0638
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-app-extension.ts
@@ -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
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-editor-extension.tsx b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-editor-extension.tsx
new file mode 100644
index 000000000..8188cfbc0
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-editor-extension.tsx
@@ -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
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-markdown-extension.tsx b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-markdown-extension.tsx
new file mode 100644
index 000000000..400707f30
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-markdown-extension.tsx
@@ -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)]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-node-processor.ts b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-node-processor.ts
new file mode 100644
index 000000000..00b5de6fe
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/extract-first-headline/extract-first-headline-node-processor.ts
@@ -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)
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/hooks/use-extract-first-headline.ts b/frontend/src/components/markdown-renderer/hooks/use-extract-first-headline.ts
deleted file mode 100644
index d0fa729b5..000000000
--- a/frontend/src/components/markdown-renderer/hooks/use-extract-first-headline.ts
+++ /dev/null
@@ -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,
- onFirstHeadingChange?: (firstHeading: string | undefined) => void
-): (() => void) => {
- const lastFirstHeadingContent = useRef()
- const currentFirstHeadingElement = useRef(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])
-}
diff --git a/frontend/src/components/markdown-renderer/slideshow-markdown-renderer.tsx b/frontend/src/components/markdown-renderer/slideshow-markdown-renderer.tsx
index 6f14a7204..4f63d6858 100644
--- a/frontend/src/components/markdown-renderer/slideshow-markdown-renderer.tsx
+++ b/frontend/src/components/markdown-renderer/slideshow-markdown-renderer.tsx
@@ -6,13 +6,12 @@
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
-import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
import { LoadingSlide } from './loading-slide'
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
import type { SlideOptions } from '@hedgedoc/commons'
-import React, { useEffect, useMemo, useRef } from 'react'
+import React, { useMemo, useRef } from 'react'
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
slideOptions?: SlideOptions
@@ -23,8 +22,6 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
*
* @param className Additional class names directly given to the div
* @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 newlinesAreBreaks If newlines are rendered as breaks or not
* @param slideOptions The {@link SlideOptions} to use
@@ -32,7 +29,6 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
export const SlideshowMarkdownRenderer: React.FC = ({
className,
markdownContentLines,
- onFirstHeadingChange,
baseUrl,
newlinesAreBreaks,
slideOptions
@@ -46,13 +42,6 @@ export const SlideshowMarkdownRenderer: React.FC {
- if (revealStatus === REVEAL_STATUS.INITIALISED) {
- extractFirstHeadline()
- }
- }, [extractFirstHeadline, markdownContentLines, revealStatus])
-
const slideShowDOM = useMemo(
() =>
revealStatus === REVEAL_STATUS.INITIALISED ? (
diff --git a/frontend/src/components/render-page/iframe-markdown-renderer.tsx b/frontend/src/components/render-page/iframe-markdown-renderer.tsx
index e194c3a39..947bd349e 100644
--- a/frontend/src/components/render-page/iframe-markdown-renderer.tsx
+++ b/frontend/src/components/render-page/iframe-markdown-renderer.tsx
@@ -73,16 +73,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
}, [communicator])
)
- const onFirstHeadingChange = useCallback(
- (firstHeading?: string) => {
- communicator.sendMessageToOtherSide({
- type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
- firstHeading
- })
- },
- [communicator]
- )
-
const onMakeScrollSource = useCallback(() => {
sendScrolling.current = true
communicator.sendMessageToOtherSide({
@@ -128,7 +118,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
{
@@ -159,16 +147,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
default:
return null
}
- }, [
- baseConfiguration,
- markdownContentLines,
- onFirstHeadingChange,
- onHeightChange,
- onMakeScrollSource,
- onScroll,
- scrollState,
- slideOptions
- ])
+ }, [baseConfiguration, markdownContentLines, onHeightChange, onMakeScrollSource, onScroll, scrollState, slideOptions])
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
diff --git a/frontend/src/components/render-page/markdown-document.tsx b/frontend/src/components/render-page/markdown-document.tsx
index d0eab827c..1a6505ce1 100644
--- a/frontend/src/components/render-page/markdown-document.tsx
+++ b/frontend/src/components/render-page/markdown-document.tsx
@@ -14,7 +14,6 @@ import type { MutableRefObject } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
export interface RendererProps extends ScrollProps {
- onFirstHeadingChange?: (firstHeading: string | undefined) => void
documentRenderPaneRef?: MutableRefObject
markdownContentLines: string[]
onHeightChange?: (height: number) => void
@@ -32,12 +31,9 @@ export interface MarkdownDocumentProps extends RendererProps {
*
* @param additionalOuterContainerClasses Additional classes given to the outer container 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 onTaskCheckedChange The callback to call if a task get's checked or unchecked.
* @param baseUrl The base url for the renderer
* @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 scrollState The current {@link ScrollState}
* @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 = ({
additionalOuterContainerClasses,
additionalRendererClasses,
- onFirstHeadingChange,
onMakeScrollSource,
baseUrl,
markdownContentLines,
@@ -94,7 +89,6 @@ export const MarkdownDocument: React.FC = ({
outerContainerRef={rendererRef}
className={`mb-3 ${additionalRendererClasses ?? ''}`}
markdownContentLines={markdownContentLines}
- onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={recalculateLineMarkers}
baseUrl={baseUrl}
newlinesAreBreaks={newlinesAreBreaks}
diff --git a/frontend/src/components/render-page/window-post-message-communicator/rendering-message.ts b/frontend/src/components/render-page/window-post-message-communicator/rendering-message.ts
index 35cdfd7e1..75008e367 100644
--- a/frontend/src/components/render-page/window-post-message-communicator/rendering-message.ts
+++ b/frontend/src/components/render-page/window-post-message-communicator/rendering-message.ts
@@ -11,7 +11,6 @@ export enum CommunicationMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
RENDERER_READY = 'RENDERER_READY',
SET_DARKMODE = 'SET_DARKMODE',
- ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
@@ -72,11 +71,6 @@ export interface SetScrollStateMessage {
scrollState: ScrollState
}
-export interface OnFirstHeadingChangeMessage {
- type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
- firstHeading: string | undefined
-}
-
export interface SetSlideOptionsMessage {
type: CommunicationMessageType.SET_SLIDE_OPTIONS
slideOptions: SlideOptions
@@ -101,7 +95,6 @@ export type CommunicationMessages =
| GetWordCountMessage
| SetMarkdownContentMessage
| SetScrollStateMessage
- | OnFirstHeadingChangeMessage
| SetSlideOptionsMessage
| OnHeightChangeMessage
| OnWordCountCalculatedMessage
@@ -120,7 +113,6 @@ export type EditorToRendererMessageType =
export type RendererToEditorMessageType =
| CommunicationMessageType.RENDERER_READY
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
- | CommunicationMessageType.ON_FIRST_HEADING_CHANGE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.ON_HEIGHT_CHANGE
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
diff --git a/frontend/src/components/slide-show-page/slide-show-page-content.tsx b/frontend/src/components/slide-show-page/slide-show-page-content.tsx
index 2482572ea..0235f2c5d 100644
--- a/frontend/src/components/slide-show-page/slide-show-page-content.tsx
+++ b/frontend/src/components/slide-show-page/slide-show-page-content.tsx
@@ -5,7 +5,6 @@
*/
import { useApplicationState } from '../../hooks/common/use-application-state'
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 { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
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'}
markdownContentLines={markdownContentLines}
rendererType={RendererType.SLIDESHOW}
- onFirstHeadingChange={updateNoteTitleByFirstHeading}
onRendererStatusChange={setRendererStatus}
/>
diff --git a/frontend/src/extensions/extra-integrations/optional-app-extensions.ts b/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
index 84fe7b538..e96b3eb20 100644
--- a/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
+++ b/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
@@ -6,6 +6,7 @@
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 { 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 { 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'
@@ -60,5 +61,6 @@ export const optionalAppExtensions: AppExtension[] = [
new TableOfContentsAppExtension(),
new ImagePlaceholderAppExtension(),
new IframeCapsuleAppExtension(),
- new BasicMarkdownSyntaxAppExtension()
+ new BasicMarkdownSyntaxAppExtension(),
+ new ExtractFirstHeadlineAppExtension()
]