feat(extensions): Introduce app extensions

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-09-02 17:07:50 +02:00
parent afe35ca164
commit 665f93d800
224 changed files with 1621 additions and 1121 deletions

View file

@ -4,8 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Suspense, useCallback, useMemo } from 'react'
import React, { Suspense, useEffect, useMemo } from 'react'
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
import { eventEmitterContext } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import EventEmitter2 from 'eventemitter2'
import type { TaskCheckedEventPayload } from '../../../../extensions/extra-integrations/task-list/event-emitting-task-list-checkbox'
import { TaskListCheckboxAppExtension } from '../../../../extensions/extra-integrations/task-list/task-list-checkbox-app-extension'
export interface CheatsheetLineProps {
markdown: string
@ -13,7 +17,7 @@ export interface CheatsheetLineProps {
}
const HighlightedCode = React.lazy(
() => import('../../../markdown-renderer/markdown-extension/highlighted-fence/highlighted-code')
() => import('../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code')
)
const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
@ -26,12 +30,15 @@ const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-rend
*/
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTaskCheckedChange }) => {
const lines = useMemo(() => markdown.split('\n'), [markdown])
const checkboxClick = useCallback(
(lineInMarkdown: number, newValue: boolean) => {
onTaskCheckedChange(newValue)
},
[onTaskCheckedChange]
)
const eventEmitter = useMemo(() => new EventEmitter2(), [])
useEffect(() => {
const handler = ({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked)
eventEmitter.on(TaskListCheckboxAppExtension.EVENT_NAME, handler)
return () => {
eventEmitter.off(TaskListCheckboxAppExtension.EVENT_NAME, handler)
}
})
return (
<Suspense
@ -44,11 +51,9 @@ export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTask
}>
<tr>
<td>
<DocumentMarkdownRenderer
markdownContentLines={lines}
baseUrl={'https://example.org'}
onTaskCheckedChange={checkboxClick}
/>
<eventEmitterContext.Provider value={eventEmitter}>
<DocumentMarkdownRenderer markdownContentLines={lines} baseUrl={'https://example.org'} />
</eventEmitterContext.Provider>
</td>
<td className={'markdown-body'}>
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />

View file

@ -11,7 +11,6 @@ import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/
import { NoteType } from '../../../redux/note-details/types/note-details'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
import { useSetCheckboxInEditor } from './hooks/use-set-checkbox-in-editor'
import { useOnScrollWithLineOffset } from './hooks/use-on-scroll-with-line-offset'
import { useScrollStateWithoutLineOffset } from './hooks/use-scroll-state-without-line-offset'
import { setRendererStatus } from '../../../redux/renderer-status/methods'
@ -31,7 +30,6 @@ export type EditorDocumentRendererProps = Omit<
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = ({ scrollState, onScroll, ...props }) => {
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
const setCheckboxInEditor = useSetCheckboxInEditor()
const adjustedOnScroll = useOnScrollWithLineOffset(onScroll)
const adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
@ -40,7 +38,6 @@ export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = ({
{...props}
onScroll={adjustedOnScroll}
scrollState={adjustedScrollState}
onTaskCheckedChange={setCheckboxInEditor}
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
markdownContentLines={trimmedContentLines}
onRendererStatusChange={setRendererStatus}

View file

@ -24,6 +24,9 @@ import { NoteAndAppTitleHead } from '../layout/note-and-app-title-head'
import equal from 'fast-deep-equal'
import { EditorPane } from './editor-pane/editor-pane'
import { ChangeEditorContentContextProvider } from './change-content-context/change-content-context'
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { useComponentsFromAppExtensions } from './editor-pane/hooks/use-components-from-app-extensions'
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
export enum ScrollSource {
EDITOR = 'editor',
@ -124,23 +127,29 @@ export const EditorPageContent: React.FC = () => {
[onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
)
const editorExtensionComponents = useComponentsFromAppExtensions()
return (
<ChangeEditorContentContextProvider>
<NoteAndAppTitleHead />
<MotdModal />
<div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR} />
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={leftPane}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={rightPane}
additionalContainerClassName={'overflow-hidden'}
/>
<Sidebar />
<ExtensionEventEmitterProvider>
{editorExtensionComponents}
<CommunicatorImageLightbox />
<NoteAndAppTitleHead />
<MotdModal />
<div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR} />
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={leftPane}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={rightPane}
additionalContainerClassName={'overflow-hidden'}
/>
<Sidebar />
</div>
</div>
</div>
</ExtensionEventEmitterProvider>
</ChangeEditorContentContextProvider>
)
}

View file

@ -21,7 +21,6 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
import { EditorView } from '@codemirror/view'
import { autocompletion } from '@codemirror/autocomplete'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name'
import { languages } from '@codemirror/language-data'
import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
import { useCodeMirrorReference, useSetCodeMirrorReference } from '../change-content-context/change-content-context'
@ -39,12 +38,10 @@ import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced'
import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text'
import { lintGutter } from '@codemirror/lint'
import { useLinter } from './linter/linter'
import { YoutubeMarkdownExtension } from '../../markdown-renderer/markdown-extension/youtube/youtube-markdown-extension'
import { VimeoMarkdownExtension } from '../../markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension'
import { SequenceDiagramMarkdownExtension } from '../../markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension'
import { LegacyShortcodesMarkdownExtension } from '../../markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension'
import { FrontmatterLinter } from './linter/frontmatter-linter'
import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
/**
* Renders the text editor pane of the editor.
@ -90,15 +87,9 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection)
const spellCheck = useApplicationState((state) => state.editorConfig.spellCheck)
// ToDo: Don't initialize new extension array here, instead refactor to global extension array
const markdownExtensionsLinters = useMemo(() => {
return [
new YoutubeMarkdownExtension(),
new VimeoMarkdownExtension(),
new SequenceDiagramMarkdownExtension(),
new LegacyShortcodesMarkdownExtension()
]
.flatMap((extension) => extension.buildLinter())
return optionalAppExtensions
.flatMap((extension) => extension.buildCodeMirrorLinter())
.concat(new FrontmatterLinter())
}, [])
const linter = useLinter(markdownExtensionsLinters)

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react'
import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
/**
* Generator react elements for components that are generated by the used {@link AppExtension app extensions}.
*/
export const useComponentsFromAppExtensions = (): ReactElement => {
return useMemo(() => {
return (
<Fragment key={'app-extensions'}>
{optionalAppExtensions.map((extension, index) =>
React.createElement(extension.buildEditorExtensionComponent(), { key: index })
)}
</Fragment>
)
}, [])
}

View file

@ -1,44 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useState } from 'react'
import { ImageLightboxModal } from '../../markdown-renderer/markdown-extension/image/image-lightbox-modal'
import type {
ImageClickedMessage,
ImageDetails
} from '../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
/**
* Handles messages from the render in the iframe to open a {@link ImageLightboxModal}.
*/
export const CommunicatorImageLightbox: React.FC = () => {
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const [modalVisibility, showModal, closeModal] = useBooleanState()
useEditorReceiveHandler(
CommunicationMessageType.IMAGE_CLICKED,
useCallback(
(values: ImageClickedMessage) => {
setLightboxDetails?.(values.details)
showModal()
},
[showModal]
)
)
return (
<ImageLightboxModal
show={modalVisibility}
onHide={closeModal}
src={lightboxDetails?.src}
alt={lightboxDetails?.alt}
title={lightboxDetails?.title}
/>
)
}

View file

@ -3,20 +3,19 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isTestMode } from '../../../utils/test-modes'
import type { RendererProps } from '../../render-page/markdown-document'
import type {
ExtensionEvent,
OnFirstHeadingChangeMessage,
OnHeightChangeMessage,
OnTaskCheckboxChangeMessage,
RendererType,
SetScrollStateMessage
} from '../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
import { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
import { useForceRenderPageUrlOnIframeLoadCallback } from './hooks/use-force-render-page-url-on-iframe-load-callback'
import { CommunicatorImageLightbox } from './communicator-image-lightbox'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import { useSendDarkModeStatusToRenderer } from './hooks/use-send-dark-mode-status-to-renderer'
import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-to-renderer'
@ -24,10 +23,10 @@ import { useSendScrollState } from './hooks/use-send-scroll-state'
import { Logger } from '../../../utils/logger'
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { getGlobalState } from '../../../redux'
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
import { ShowIf } from '../../common/show-if/show-if'
import { WaitSpinner } from '../../common/wait-spinner/wait-spinner'
import { useExtensionEventEmitter } from '../../markdown-renderer/hooks/use-extension-event-emitter'
export interface RenderIframeProps extends RendererProps {
rendererType: RendererType
@ -58,7 +57,6 @@ const log = new Logger('RenderIframe')
*/
export const RenderIframe: React.FC<RenderIframeProps> = ({
markdownContentLines,
onTaskCheckedChange,
scrollState,
onFirstHeadingChange,
onScroll,
@ -92,6 +90,10 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
}
}, [iframeCommunicator, rendererReady])
useEffect(() => {
onRendererStatusChange?.(rendererReady)
}, [onRendererStatusChange, rendererReady])
useEditorReceiveHandler(
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
useCallback(
@ -105,15 +107,15 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
)
const eventEmitter = useExtensionEventEmitter()
useEditorReceiveHandler(
CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
useCallback(
(values: OnTaskCheckboxChangeMessage) => {
const lineOffset = getGlobalState().noteDetails.frontmatterRendererInfo.lineOffset
onTaskCheckedChange?.(values.lineInMarkdown + lineOffset, values.checked)
},
[onTaskCheckedChange]
)
CommunicationMessageType.EXTENSION_EVENT,
useMemo(() => {
return eventEmitter === undefined
? undefined
: (values: ExtensionEvent) => eventEmitter.emit(values.eventName, values.payload)
}, [eventEmitter])
)
useEditorReceiveHandler(
@ -169,7 +171,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
return (
<Fragment>
<CommunicatorImageLightbox />
<ShowIf condition={!rendererReady}>
<WaitSpinner />
</ShowIf>

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { LineMarkerPosition } from '../../markdown-renderer/markdown-extension/linemarker/types'
import type { LineMarkerPosition } from '../../markdown-renderer/extensions/linemarker/types'
/**
* Finds the {@link LineMarkerPosition line markers} from a list of given line markers that are the closest to the given line number.

View file

@ -9,7 +9,7 @@ import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react'
import { ShowIf } from '../../common/show-if/show-if'
import { tocSlugify } from './toc-slugify'
import { JumpAnchor } from '../../markdown-renderer/markdown-extension/link-replacer/jump-anchor'
import { JumpAnchor } from '../../markdown-renderer/extensions/link-replacer/jump-anchor'
/**
* Generates a React DOM part for the table of contents from the given AST of the document.

View file

@ -1,19 +1,14 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { TocAst } from 'markdown-it-toc-done-right'
import type { ImageClickHandler } from './markdown-extension/image/proxy-image-replacer'
import type { Ref } from 'react'
export interface CommonMarkdownRendererProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast?: TocAst) => void
baseUrl: string
onImageClick?: ImageClickHandler
outerContainerRef?: Ref<HTMLDivElement>
newlinesAreBreaks?: boolean
lineOffset?: number

View file

@ -6,15 +6,15 @@
import React, { useEffect, useMemo, useRef } from 'react'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import type { LineMarkerPosition } from './markdown-extension/linemarker/types'
import { useTranslation } from 'react-i18next'
import type { LineMarkers } from './markdown-extension/linemarker/add-line-marker-markdown-it-plugin'
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
import { HeadlineAnchorsMarkdownExtension } from './markdown-extension/headline-anchors-markdown-extension'
import { cypressId } from '../../utils/cypress-attribute'
import { HeadlineAnchorsMarkdownExtension } from './extensions/headline-anchors-markdown-extension'
import type { LineMarkerPosition } from './extensions/linemarker/types'
import type { LineMarkers } from './extensions/linemarker/add-line-marker-markdown-it-plugin'
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
@ -39,10 +39,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
markdownContentLines,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
outerContainerRef,
newlinesAreBreaks
}) => {
@ -52,12 +49,10 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
const extensions = useMarkdownExtensions(
baseUrl,
currentLineMarkers,
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], []),
onTaskCheckedChange,
onImageClick,
onTocChange
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], [])
)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
useTranslation()
useCalculateLineMarkerPosition(

View file

@ -17,7 +17,7 @@ const ruleName = 'code-highlighter'
* @param state The current state of the processing {@link MarkdownIt} instance.
* @see MarkdownIt.RuleCore
*/
const rule: RuleCore = (state) => {
const rule: RuleCore = (state): void => {
state.tokens.forEach((token) => {
if (token.type === 'fence') {
const highlightInfos = parseCodeBlockParameters(token.info)
@ -29,7 +29,6 @@ const rule: RuleCore = (state) => {
)
}
})
return true
}
/**
@ -37,7 +36,7 @@ const rule: RuleCore = (state) => {
*
* @param markdownIt The {@link MarkdownIt markdown-it instance} to which the rule should be added
*/
export const codeBlockMarkdownPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
export const codeBlockMarkdownPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => {
if (markdownIt.core.ruler.getRules(ruleName).length === 0) {
markdownIt.core.ruler.push(ruleName, rule, { alt: [ruleName] })
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type MarkdownIt from 'markdown-it'
import { codeBlockMarkdownPlugin } from './code-block-markdown-plugin'
import type { ComponentReplacer } from '../../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../markdown-renderer-extension'
/**
* A {@link MarkdownRendererExtension markdown extension} that is used for code fence replacements.
*/
export abstract class CodeBlockMarkdownRendererExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
codeBlockMarkdownPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return []
}
}

View file

@ -4,15 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type EventEmitter2 from 'eventemitter2'
import type MarkdownIt from 'markdown-it'
import type { NodeProcessor } from '../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../replace-components/component-replacer'
import type { Linter } from '../../editor-page/editor-pane/linter/linter'
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
/**
* Base class for Markdown extensions.
*/
export abstract class MarkdownExtension {
export abstract class MarkdownRendererExtension {
constructor(protected readonly eventEmitter?: EventEmitter2) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public configureMarkdownIt(markdownIt: MarkdownIt): void {
return
@ -34,8 +36,4 @@ export abstract class MarkdownExtension {
public buildTagNameAllowList(): string[] {
return []
}
public buildLinter(): Linter[] {
return []
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from './markdown-extension'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import { Logger } from '../../../utils/logger'
import { isDevMode } from '../../../utils/test-modes'
@ -14,7 +14,7 @@ const log = new Logger('DebuggerMarkdownExtension')
/**
* Adds console debug logging to the markdown rendering.
*/
export class DebuggerMarkdownExtension extends MarkdownExtension {
export class DebuggerMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
if (isDevMode) {
markdownIt.core.ruler.push('printStateToConsole', (state) => {

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji/bare'
import { combinedEmojiData } from './mapping'
@ -12,7 +12,7 @@ import { combinedEmojiData } from './mapping'
/**
* Adds support for utf-8 emojis.
*/
export class EmojiMarkdownExtension extends MarkdownExtension {
export class EmojiMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
markdownIt.use(emoji, {
defs: combinedEmojiData

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from './markdown-extension'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import definitionList from 'markdown-it-deflist'
@ -18,7 +18,7 @@ import { imageSize } from '@hedgedoc/markdown-it-image-size'
/**
* Adds some common markdown syntaxes to the markdown rendering.
*/
export class GenericSyntaxMarkdownExtension extends MarkdownExtension {
export class GenericSyntaxMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
abbreviation(markdownIt)
definitionList(markdownIt)

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from './markdown-extension'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import anchor from 'markdown-it-anchor'
/**
* Adds headline anchors to the markdown rendering.
*/
export class HeadlineAnchorsMarkdownExtension extends MarkdownExtension {
export class HeadlineAnchorsMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
anchor(markdownIt, {
permalink: anchor.permalink.ariaHidden({

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { IframeCapsuleReplacer } from './iframe-capsule-replacer'
/**
* Adds a replacer that capsules iframes in a click shield.
*/
export class IframeCapsuleMarkdownExtension extends MarkdownExtension {
export class IframeCapsuleMarkdownExtension extends MarkdownRendererExtension {
public buildReplacers(): ComponentReplacer[] {
return [new IframeCapsuleReplacer()]
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { addLineToPlaceholderImageTags } from './add-line-to-placeholder-image-tags'
import type MarkdownIt from 'markdown-it/lib'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
@ -13,7 +13,7 @@ import { ImagePlaceholderReplacer } from './image-placeholder-replacer'
/**
* Adds support for {@link ImagePlaceholder}.
*/
export class ImagePlaceholderMarkdownExtension extends MarkdownExtension {
export class ImagePlaceholderMarkdownExtension extends MarkdownRendererExtension {
public static readonly PLACEHOLDER_URL = 'https://'
constructor() {

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import type { Element } from 'domhandler'
import { ImagePlaceholder } from './image-placeholder'
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
@ -25,20 +25,21 @@ export class ImagePlaceholderReplacer extends ComponentReplacer {
}
replace(node: Element): NodeReplacement {
if (node.name === 'img' && node.attribs && node.attribs.src === ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
const lineIndex = Number(node.attribs['data-line'])
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
return (
<ImagePlaceholder
alt={node.attribs.alt}
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
lineIndex={isNaN(lineIndex) ? undefined : lineIndex}
placeholderIndexInLine={indexInLine}
/>
)
if (node.name !== 'img' || node.attribs?.src !== ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
return DO_NOT_REPLACE
}
const lineIndex = Number(node.attribs['data-line'])
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
return (
<ImagePlaceholder
alt={node.attribs.alt}
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
lineIndex={isNaN(lineIndex) ? undefined : lineIndex}
placeholderIndexInLine={indexInLine}
/>
)
}
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
/*!
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ImageDetails } from '../../../render-page/window-post-message-communicator/rendering-message'
import React, { useCallback, useState } from 'react'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { ImageLightboxModal } from './image-lightbox-modal'
import { useExtensionEventEmitterHandler } from '../../hooks/use-extension-event-emitter'
import { SHOW_IMAGE_LIGHTBOX_EVENT_NAME } from './event-emitting-proxy-image-frame'
/**
* Handles messages from the render in the iframe to open a {@link ImageLightboxModal}.
*/
export const CommunicatorImageLightbox: React.FC = () => {
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const [modalVisibility, showModal, closeModal] = useBooleanState()
const handler = useCallback(
(values: ImageDetails) => {
setLightboxDetails?.(values)
showModal()
},
[showModal]
)
useExtensionEventEmitterHandler(SHOW_IMAGE_LIGHTBOX_EVENT_NAME, handler)
return (
<ImageLightboxModal
show={modalVisibility}
onHide={closeModal}
src={lightboxDetails?.src}
alt={lightboxDetails?.alt}
title={lightboxDetails?.title}
/>
)
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback } from 'react'
import { useExtensionEventEmitter } from '../../hooks/use-extension-event-emitter'
import { ProxyImageFrame } from './proxy-image-frame'
import type { ImageDetails } from '../../../render-page/window-post-message-communicator/rendering-message'
type EventEmittingProxyImageFrameProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onClick'>
export const SHOW_IMAGE_LIGHTBOX_EVENT_NAME = 'ImageClick'
/**
* Renders a {@link ProxyImageFrame} but claims the `onClick` event to send image information to the current event emitter.
*
* @param props props that will be forwarded to the inner image frame
*/
export const EventEmittingProxyImageFrame: React.FC<EventEmittingProxyImageFrameProps> = (props) => {
const eventEmitter = useExtensionEventEmitter()
const onClick = useCallback(
(event: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
const image = event.target as HTMLImageElement
if (image.src === '') {
return
}
eventEmitter?.emit(SHOW_IMAGE_LIGHTBOX_EVENT_NAME, {
src: image.src,
alt: image.alt,
title: image.title
} as ImageDetails)
},
[eventEmitter]
)
return <ProxyImageFrame {...props} onClick={onClick} />
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
/*!
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -32,7 +32,7 @@ export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>
.catch((err) => log.error(err))
}, [imageProxyEnabled, src])
// The next image processor works with a whitelist of origins. Therefore we can't use it for general images.
// The next image processor works with a whitelist of origins. Therefore, we can't use it for general images.
// eslint-disable-next-line @next/next/no-img-element
return <img src={imageProxyEnabled ? imageUrl : src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props} />
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { ProxyImageReplacer } from './proxy-image-replacer'
/**
* Adds support for image lightbox and image proxy redirection.
*/
export class ProxyImageMarkdownExtension extends MarkdownRendererExtension {
buildReplacers(): ComponentReplacer[] {
return [new ProxyImageReplacer()]
}
}

View file

@ -8,7 +8,7 @@ import type { Element } from 'domhandler'
import React from 'react'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { ProxyImageFrame } from './proxy-image-frame'
import { EventEmittingProxyImageFrame } from './event-emitting-proxy-image-frame'
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void
@ -16,18 +16,11 @@ export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, Mouse
* Detects image tags and loads them via image proxy if configured.
*/
export class ProxyImageReplacer extends ComponentReplacer {
private readonly clickHandler?: ImageClickHandler
constructor(clickHandler?: ImageClickHandler) {
super()
this.clickHandler = clickHandler
}
public replace(node: Element): NodeReplacement {
return node.name !== 'img' ? (
DO_NOT_REPLACE
) : (
<ProxyImageFrame
<EventEmittingProxyImageFrame
id={node.attribs.id}
className={`${node.attribs.class} cursor-zoom-in`}
src={node.attribs.src}
@ -35,7 +28,6 @@ export class ProxyImageReplacer extends ComponentReplacer {
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
onClick={this.clickHandler}
/>
)
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { LinemarkerReplacer } from './linemarker-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import type { LineMarkers } from './add-line-marker-markdown-it-plugin'
@ -14,7 +14,7 @@ import type MarkdownIt from 'markdown-it'
/**
* Adds support for the generation of line marker elements which are needed for synced scrolling.
*/
export class LinemarkerMarkdownExtension extends MarkdownExtension {
export class LinemarkerMarkdownExtension extends MarkdownRendererExtension {
public static readonly tagName = 'app-linemarker'
constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { JumpAnchorReplacer } from './jump-anchor-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
@ -13,7 +13,7 @@ import { AnchorNodePreprocessor } from './anchor-node-preprocessor'
/**
* Adds tweaks for anchor tags which are needed for the use in the secured iframe.
*/
export class LinkAdjustmentMarkdownExtension extends MarkdownExtension {
export class LinkAdjustmentMarkdownExtension extends MarkdownRendererExtension {
constructor(private baseUrl: string) {
super()
}

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../test-utils/mock-i18n'
import { mockI18n } from '../../test-utils/mock-i18n'
import { render } from '@testing-library/react'
import { TestMarkdownRenderer } from '../test-utils/test-markdown-renderer'
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
import { LinkifyFixMarkdownExtension } from './linkify-fix-markdown-extension'
describe('Linkify markdown extensions', () => {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from './markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import linkify from 'markdown-it/lib/rules_core/linkify'
import type MarkdownIt from 'markdown-it'
import tlds from 'tlds'
@ -12,7 +12,7 @@ import tlds from 'tlds'
/**
* A markdown extension that detects plain text URLs and converts them into links.
*/
export class LinkifyFixMarkdownExtension extends MarkdownExtension {
export class LinkifyFixMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
markdownIt.linkify.tlds(tlds)
markdownIt.core.ruler.push('linkify', (state) => {

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import { addSlideSectionsMarkdownItPlugin } from './reveal-sections'
import { RevealCommentCommandNodePreprocessor } from './process-reveal-comment-nodes'
@ -14,7 +14,7 @@ import type { NodeProcessor } from '../../node-preprocessors/node-processor'
* Adds support for reveal.js to the markdown rendering.
* This includes the generation of sections and the manipulation of elements using reveal comments.
*/
export class RevealMarkdownExtension extends MarkdownExtension {
export class RevealMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
addSlideSectionsMarkdownItPlugin(markdownIt)
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,17 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { SanitizerNodePreprocessor } from './dom-purifier-node-preprocessor'
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
/**
* Adds support for html sanitizing using dompurify to the markdown rendering.
*/
export class SanitizerMarkdownExtension extends MarkdownExtension {
export class SanitizerMarkdownExtension extends MarkdownRendererExtension {
constructor(private tagNameWhiteList: string[]) {
super()
}

View file

@ -4,29 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from './markdown-extension'
import type MarkdownIt from 'markdown-it'
import type { TocAst } from 'markdown-it-toc-done-right'
import toc from 'markdown-it-toc-done-right'
import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import equal from 'fast-deep-equal'
/**
* Adds table of content to the markdown rendering.
*/
export class TableOfContentsMarkdownExtension extends MarkdownExtension {
constructor(private onTocChange?: (ast: TocAst) => void) {
super()
}
export class TableOfContentsMarkdownExtension extends MarkdownRendererExtension {
public static readonly EVENT_NAME = 'TocChange'
private lastAst: TocAst | undefined = undefined
public configureMarkdownIt(markdownIt: MarkdownIt): void {
if (!this.onTocChange) {
return
}
toc(markdownIt, {
placeholder: '(\\[TOC\\]|\\[toc\\])',
listType: 'ul',
level: [1, 2, 3],
callback: (code: string, ast: TocAst): void => {
this.onTocChange?.(ast)
if (equal(ast, this.lastAst)) {
return
}
this.lastAst = ast
this.eventEmitter?.emit(TableOfContentsMarkdownExtension.EVENT_NAME, ast)
},
slugify: tocSlugify
})

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -1,17 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { UploadIndicatingImageFrameReplacer } from './upload-indicating-image-frame-replacer'
/**
* A markdown extension that shows {@link UploadIndicatingFrame} for images that are getting uploaded.
*/
export class UploadIndicatingImageFrameMarkdownExtension extends MarkdownExtension {
export class UploadIndicatingImageFrameMarkdownExtension extends MarkdownRendererExtension {
buildReplacers(): ComponentReplacer[] {
return [new UploadIndicatingImageFrameReplacer()]
}

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import type { Document } from 'domhandler'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
/**
* Creates a function that applies the node preprocessors of every given {@link MarkdownRendererExtension} to a {@link Document}.
*
* @param extensions The extensions who provide node processors
* @return The created apply function
*/
export const useCombinedNodePreprocessor = (extensions: MarkdownRendererExtension[]): ((nodes: Document) => Document) =>
useMemo(() => {
return extensions
.flatMap((extension) => extension.buildNodeProcessors())
.reduce(
(state, processor) => (document: Document) => state(processor.process(document)),
(document: Document) => document
)
}, [extensions])

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import MarkdownIt from 'markdown-it/lib'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
/**
* Creates a new {@link MarkdownIt markdown-it instance} and configures it using the given {@link MarkdownRendererExtension markdown renderer extensions}.
*
* @param extensions The extensions that configure the new markdown-it instance
* @param allowHtml Defines if html in markdown is allowed
* @param newlinesAreBreaks Defines if new lines should be treated as line breaks or paragraphs
* @return the created markdown-it instance
*/
export const useConfiguredMarkdownIt = (
extensions: MarkdownRendererExtension[],
allowHtml: boolean,
newlinesAreBreaks: boolean
): MarkdownIt => {
return useMemo(() => {
const newMarkdownIt = new MarkdownIt('default', {
html: allowHtml,
breaks: newlinesAreBreaks,
langPrefix: '',
typographer: true
})
extensions.forEach((extension) => newMarkdownIt.use((markdownIt) => extension.configureMarkdownIt(markdownIt)))
extensions.forEach((extension) => newMarkdownIt.use((markdownIt) => extension.configureMarkdownItPost(markdownIt)))
return newMarkdownIt
}, [allowHtml, extensions, newlinesAreBreaks])
}

View file

@ -1,68 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it/lib'
import { useMemo } from 'react'
import type { ValidReactDomElement } from '../replace-components/component-replacer'
import convertHtmlToReact from '@hedgedoc/html-to-react'
import { NodeToReactTransformer } from '../utils/node-to-react-transformer'
import { LineIdMapper } from '../utils/line-id-mapper'
import type { MarkdownExtension } from '../markdown-extension/markdown-extension'
import { MarkdownExtensionCollection } from '../markdown-extension/markdown-extension-collection'
/**
* Renders Markdown-Code into react elements.
*
* @param markdownContentLines The markdown code lines that should be rendered
* @param additionalMarkdownExtensions A list of {@link MarkdownExtension markdown extensions} that should be used
* @param newlinesAreBreaks Defines if the alternative break mode of markdown it should be used
* @param allowHtml Defines if html is allowed in markdown
* @return The React DOM that represents the rendered markdown code
*/
export const useConvertMarkdownToReactDom = (
markdownContentLines: string[],
additionalMarkdownExtensions: MarkdownExtension[],
newlinesAreBreaks = true,
allowHtml = true
): ValidReactDomElement[] => {
const lineNumberMapper = useMemo(() => new LineIdMapper(), [])
const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), [])
const markdownExtensions = useMemo(
() => new MarkdownExtensionCollection(additionalMarkdownExtensions),
[additionalMarkdownExtensions]
)
const markdownIt = useMemo(() => {
const newMarkdownIt = new MarkdownIt('default', {
html: allowHtml,
breaks: newlinesAreBreaks,
langPrefix: '',
typographer: true
})
markdownExtensions.configureMarkdownIt(newMarkdownIt)
return newMarkdownIt
}, [allowHtml, markdownExtensions, newlinesAreBreaks])
useMemo(() => {
htmlToReactTransformer.setReplacers(markdownExtensions.buildReplacers())
}, [htmlToReactTransformer, markdownExtensions])
useMemo(() => {
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines))
}, [htmlToReactTransformer, lineNumberMapper, markdownContentLines])
const nodePreProcessor = useMemo(() => markdownExtensions.buildFlatNodeProcessor(), [markdownExtensions])
return useMemo(() => {
const html = markdownIt.render(markdownContentLines.join('\n'))
htmlToReactTransformer.resetReplacers()
return convertHtmlToReact(html, {
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
preprocessNodes: (document) => nodePreProcessor(document)
})
}, [htmlToReactTransformer, markdownContentLines, markdownIt, nodePreProcessor])
}

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo } from 'react'
import type { ValidReactDomElement } from '../replace-components/component-replacer'
import convertHtmlToReact from '@hedgedoc/html-to-react'
import { NodeToReactTransformer } from '../utils/node-to-react-transformer'
import { LineIdMapper } from '../utils/line-id-mapper'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import { SanitizerMarkdownExtension } from '../extensions/sanitizer/sanitizer-markdown-extension'
import { useCombinedNodePreprocessor } from './use-combined-node-preprocessor'
import { useConfiguredMarkdownIt } from './use-configured-markdown-it'
/**
* Renders Markdown-Code into react elements.
*
* @param markdownContentLines The Markdown code lines that should be rendered
* @param additionalMarkdownExtensions A list of {@link MarkdownRendererExtension markdown extensions} that should be used
* @param newlinesAreBreaks Defines if the alternative break mode of markdown it should be used
* @param allowHtml Defines if html is allowed in markdown
* @return The React DOM that represents the rendered Markdown code
*/
export const useConvertMarkdownToReactDom = (
markdownContentLines: string[],
additionalMarkdownExtensions: MarkdownRendererExtension[],
newlinesAreBreaks = true,
allowHtml = true
): ValidReactDomElement => {
const lineNumberMapper = useMemo(() => new LineIdMapper(), [])
const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), [])
const markdownExtensions = useMemo(() => {
const tagWhiteLists = additionalMarkdownExtensions.flatMap((extension) => extension.buildTagNameAllowList())
return [...additionalMarkdownExtensions, new SanitizerMarkdownExtension(tagWhiteLists)]
}, [additionalMarkdownExtensions])
useMemo(() => {
htmlToReactTransformer.setReplacers(markdownExtensions.flatMap((extension) => extension.buildReplacers()))
}, [htmlToReactTransformer, markdownExtensions])
useMemo(() => {
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines))
}, [htmlToReactTransformer, lineNumberMapper, markdownContentLines])
const nodePreProcessor = useCombinedNodePreprocessor(markdownExtensions)
const markdownIt = useConfiguredMarkdownIt(markdownExtensions, allowHtml, newlinesAreBreaks)
return useMemo(() => {
const html = markdownIt.render(markdownContentLines.join('\n'))
htmlToReactTransformer.resetReplacers()
return (
<Fragment key={'root'}>
{convertHtmlToReact(html, {
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
preprocessNodes: (document) => nodePreProcessor(document)
})}
</Fragment>
)
}, [htmlToReactTransformer, markdownContentLines, markdownIt, nodePreProcessor])
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useEffect, useMemo } from 'react'
import EventEmitter2 from 'eventemitter2'
export const eventEmitterContext = createContext<EventEmitter2 | undefined>(undefined)
/**
* Provides the {@link EventEmitter2 event emitter} from the current {@link eventEmitterContext context}.
*/
export const useExtensionEventEmitter = () => {
return useContext(eventEmitterContext)
}
/**
* Creates a new {@link EventEmitter2 event emitter} and provides it as {@link eventEmitterContext context}.
*
* @param children The elements that should receive the context value
*/
export const ExtensionEventEmitterProvider: React.FC<PropsWithChildren> = ({ children }) => {
const eventEmitter = useMemo(() => new EventEmitter2(), [])
return <eventEmitterContext.Provider value={eventEmitter}>{children}</eventEmitterContext.Provider>
}
/**
* Registers a handler callback on the current {@link EventEmitter2 event emitter} that is provided in the {@link eventEmitterContext context}.
*
* @param eventName The name of the event which should be subscribed
* @param handler The callback that should be executed. If undefined the event will be unsubscribed.
*/
export const useExtensionEventEmitterHandler = <T,>(
eventName: string,
handler: ((values: T) => void) | undefined
): void => {
const eventEmitter = useExtensionEventEmitter()
useEffect(() => {
if (!eventEmitter || !handler) {
return
}
eventEmitter.on(eventName, handler)
return () => {
eventEmitter.off(eventName, handler)
}
}, [eventEmitter, eventName, handler])
}

View file

@ -5,98 +5,59 @@
*/
import type { MutableRefObject } from 'react'
import { useMemo, useRef } from 'react'
import { TableOfContentsMarkdownExtension } from '../markdown-extension/table-of-contents-markdown-extension'
import { VegaLiteMarkdownExtension } from '../markdown-extension/vega-lite/vega-lite-markdown-extension'
import { LinemarkerMarkdownExtension } from '../markdown-extension/linemarker/linemarker-markdown-extension'
import { GistMarkdownExtension } from '../markdown-extension/gist/gist-markdown-extension'
import { YoutubeMarkdownExtension } from '../markdown-extension/youtube/youtube-markdown-extension'
import { VimeoMarkdownExtension } from '../markdown-extension/vimeo/vimeo-markdown-extension'
import { ProxyImageMarkdownExtension } from '../markdown-extension/image/proxy-image-markdown-extension'
import { CsvTableMarkdownExtension } from '../markdown-extension/csv/csv-table-markdown-extension'
import { AbcjsMarkdownExtension } from '../markdown-extension/abcjs/abcjs-markdown-extension'
import { SequenceDiagramMarkdownExtension } from '../markdown-extension/sequence-diagram/sequence-diagram-markdown-extension'
import { FlowchartMarkdownExtension } from '../markdown-extension/flowchart/flowchart-markdown-extension'
import { MermaidMarkdownExtension } from '../markdown-extension/mermaid/mermaid-markdown-extension'
import { GraphvizMarkdownExtension } from '../markdown-extension/graphviz/graphviz-markdown-extension'
import { BlockquoteExtraTagMarkdownExtension } from '../markdown-extension/blockquote/blockquote-extra-tag-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../markdown-extension/link-replacer/link-adjustment-markdown-extension'
import { KatexMarkdownExtension } from '../markdown-extension/katex/katex-markdown-extension'
import { TaskListMarkdownExtension } from '../markdown-extension/task-list/task-list-markdown-extension'
import { PlantumlMarkdownExtension } from '../markdown-extension/plantuml/plantuml-markdown-extension'
import { LegacyShortcodesMarkdownExtension } from '../markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension'
import { EmojiMarkdownExtension } from '../markdown-extension/emoji/emoji-markdown-extension'
import { GenericSyntaxMarkdownExtension } from '../markdown-extension/generic-syntax-markdown-extension'
import { AlertMarkdownExtension } from '../markdown-extension/alert-markdown-extension'
import { SpoilerMarkdownExtension } from '../markdown-extension/spoiler-markdown-extension'
import { LinkifyFixMarkdownExtension } from '../markdown-extension/linkify-fix-markdown-extension'
import { HighlightedCodeMarkdownExtension } from '../markdown-extension/highlighted-fence/highlighted-code-markdown-extension'
import { DebuggerMarkdownExtension } from '../markdown-extension/debugger-markdown-extension'
import type { LineMarkers } from '../markdown-extension/linemarker/add-line-marker-markdown-it-plugin'
import type { ImageClickHandler } from '../markdown-extension/image/proxy-image-replacer'
import type { TocAst } from 'markdown-it-toc-done-right'
import type { MarkdownExtension } from '../markdown-extension/markdown-extension'
import { IframeCapsuleMarkdownExtension } from '../markdown-extension/iframe-capsule/iframe-capsule-markdown-extension'
import { ImagePlaceholderMarkdownExtension } from '../markdown-extension/image-placeholder/image-placeholder-markdown-extension'
import { UploadIndicatingImageFrameMarkdownExtension } from '../markdown-extension/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
import { useOnRefChange } from './use-on-ref-change'
import { useMemo } from 'react'
import { GenericSyntaxMarkdownExtension } from '../extensions/generic-syntax-markdown-extension'
import type { LineMarkers } from '../extensions/linemarker/add-line-marker-markdown-it-plugin'
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
import { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
import { IframeCapsuleMarkdownExtension } from '../extensions/iframe-capsule/iframe-capsule-markdown-extension'
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
import { EmojiMarkdownExtension } from '../extensions/emoji/emoji-markdown-extension'
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
import { TableOfContentsMarkdownExtension } from '../extensions/table-of-contents-markdown-extension'
import { ImagePlaceholderMarkdownExtension } from '../extensions/image-placeholder/image-placeholder-markdown-extension'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import { useExtensionEventEmitter } from './use-extension-event-emitter'
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
const optionalMarkdownRendererExtensions = optionalAppExtensions.flatMap((value) =>
value.buildMarkdownRendererExtensions()
)
/**
* Provides a list of {@link MarkdownExtension markdown extensions} that is a combination of the common extensions and the given additional.
* Provides a list of {@link MarkdownRendererExtension markdown extensions} that is a combination of the common extensions and the given additional.
*
* @param baseUrl The base url for the {@link LinkAdjustmentMarkdownExtension}
* @param currentLineMarkers A {@link MutableRefObject reference} to {@link LineMarkers} for the {@link LinemarkerMarkdownExtension}
* @param additionalExtensions The additional extensions that should be included in the list
* @param onTaskCheckedChange The checkbox click callback for the {@link TaskListMarkdownExtension}
* @param onImageClick The image click callback for the {@link ProxyImageMarkdownExtension}
* @param onTocChange The toc-changed callback for the {@link TableOfContentsMarkdownExtension}
* @return The created list of markdown extensions
*/
export const useMarkdownExtensions = (
baseUrl: string,
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
additionalExtensions: MarkdownExtension[],
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
onImageClick?: ImageClickHandler,
onTocChange?: (ast?: TocAst) => void
): MarkdownExtension[] => {
const toc = useRef<TocAst | undefined>(undefined)
useOnRefChange(toc, onTocChange)
additionalExtensions: MarkdownRendererExtension[]
): MarkdownRendererExtension[] => {
const extensionEventEmitter = useExtensionEventEmitter()
//replace with global list
return useMemo(() => {
return [
new TableOfContentsMarkdownExtension((ast?: TocAst) => (toc.current = ast)),
...optionalMarkdownRendererExtensions,
...additionalExtensions,
new VegaLiteMarkdownExtension(),
new TableOfContentsMarkdownExtension(extensionEventEmitter),
new LinemarkerMarkdownExtension(
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
),
new IframeCapsuleMarkdownExtension(),
new ImagePlaceholderMarkdownExtension(),
new UploadIndicatingImageFrameMarkdownExtension(),
new GistMarkdownExtension(),
new YoutubeMarkdownExtension(),
new VimeoMarkdownExtension(),
new ProxyImageMarkdownExtension(onImageClick),
new CsvTableMarkdownExtension(),
new AbcjsMarkdownExtension(),
new SequenceDiagramMarkdownExtension(),
new FlowchartMarkdownExtension(),
new MermaidMarkdownExtension(),
new GraphvizMarkdownExtension(),
new BlockquoteExtraTagMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
new KatexMarkdownExtension(),
new TaskListMarkdownExtension(onTaskCheckedChange),
new PlantumlMarkdownExtension(),
new LegacyShortcodesMarkdownExtension(),
new EmojiMarkdownExtension(),
new GenericSyntaxMarkdownExtension(),
new AlertMarkdownExtension(),
new SpoilerMarkdownExtension(),
new LinkifyFixMarkdownExtension(),
new HighlightedCodeMarkdownExtension(),
new DebuggerMarkdownExtension()
new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension()
]
}, [additionalExtensions, baseUrl, currentLineMarkers, onImageClick, onTaskCheckedChange])
}, [additionalExtensions, baseUrl, currentLineMarkers, extensionEventEmitter])
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import { AbcFrame } from './abc-frame'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support for abc.js to the markdown rendering using code fences with "abc" as language.
*/
export class AbcjsMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(AbcFrame, 'abc')]
}
}

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type MarkdownIt from 'markdown-it'
import { codeBlockMarkdownPlugin } from './code-block-markdown-plugin'
import { MarkdownExtension } from '../markdown-extension'
/**
* A {@link MarkdownExtension markdown extension} that is used for code fence replacements.
*/
export abstract class CodeBlockMarkdownExtension extends MarkdownExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
codeBlockMarkdownPlugin(markdownIt)
}
}

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { CsvReplacer } from './csv-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
/**
* Adds support for csv tables to the markdown rendering using code fences with "csv" as language.
*/
export class CsvTableMarkdownExtension extends MarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CsvReplacer()]
}
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { FlowChart } from './flowchart'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support for flow charts to the markdown rendering using code fences with "flow" as language.
*/
export class FlowchartMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(FlowChart, 'flow')]
}
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { GraphvizFrame } from './graphviz-frame'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support for graphviz to the markdown rendering using code fences with "graphviz" as language.
*/
export class GraphvizMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(GraphvizFrame, 'graphviz')]
}
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { HighlightedCodeReplacer } from './highlighted-code-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support code highlighting to the markdown rendering.
* Every code fence that is not replaced by another replacer is highlighted using highlight-js.
*/
export class HighlightedCodeMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new HighlightedCodeReplacer()]
}
}

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import type { ImageClickHandler } from './proxy-image-replacer'
import { ProxyImageReplacer } from './proxy-image-replacer'
/**
* Adds support for image lightbox and image proxy redirection.
*/
export class ProxyImageMarkdownExtension extends MarkdownExtension {
constructor(private onImageClick?: ImageClickHandler) {
super()
}
buildReplacers(): ComponentReplacer[] {
return [new ProxyImageReplacer(this.onImageClick)]
}
}

View file

@ -1,45 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import type MarkdownIt from 'markdown-it'
import { legacyPdfRegex, legacyPdfShortCode } from './replace-legacy-pdf-short-code'
import { legacySlideshareRegex, legacySlideshareShortCode } from './replace-legacy-slideshare-short-code'
import { legacySpeakerdeckRegex, legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-short-code'
import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter'
import type { Linter } from '../../../editor-page/editor-pane/linter/linter'
import { t } from 'i18next'
/**
* Adds support for legacy shortcodes (pdf, slideshare and speakerdeck) by replacing them with anchor elements.
*/
export class LegacyShortcodesMarkdownExtension extends MarkdownExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
legacyPdfShortCode(markdownIt)
legacySlideshareShortCode(markdownIt)
legacySpeakerdeckShortCode(markdownIt)
}
public buildLinter(): Linter[] {
return [
new SingleLineRegexLinter(
legacySpeakerdeckRegex,
t('editor.linter.shortcode', { shortcode: 'SpeakerDeck' }),
(match: string) => `https://speakerdeck.com/${match}`
),
new SingleLineRegexLinter(
legacySlideshareRegex,
t('editor.linter.shortcode', { shortcode: 'SlideShare' }),
(match: string) => `https://www.slideshare.net/${match}`
),
new SingleLineRegexLinter(
legacyPdfRegex,
t('editor.linter.shortcode', { shortcode: 'PDF' }),
(match: string) => match
)
]
}
}

View file

@ -1,57 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownExtension } from './markdown-extension'
import type { ComponentReplacer } from '../replace-components/component-replacer'
import type { Document } from 'domhandler'
import type MarkdownIt from 'markdown-it'
import { SanitizerMarkdownExtension } from './sanitizer/sanitizer-markdown-extension'
/**
* Contains multiple {@link MarkdownExtension} and uses them to configure parts of the renderer.
*/
export class MarkdownExtensionCollection {
private extensions: MarkdownExtension[]
public constructor(additionalExtensions: MarkdownExtension[]) {
const tagWhiteLists = additionalExtensions.flatMap((extension) => extension.buildTagNameAllowList())
this.extensions = [...additionalExtensions, new SanitizerMarkdownExtension(tagWhiteLists)]
}
/**
* Configures the given {@link MarkdownIt markdown-it instance} using every saved {@link MarkdownExtension extension}.
*
* @param markdownIt The markdown-it instance to configure.
*/
public configureMarkdownIt(markdownIt: MarkdownIt): void {
this.extensions.forEach((extension) => markdownIt.use((markdownIt) => extension.configureMarkdownIt(markdownIt)))
this.extensions.forEach((extension) =>
markdownIt.use((markdownIt) => extension.configureMarkdownItPost(markdownIt))
)
}
/**
* Creates a node processor that applies the node processor of every saved {@link MarkdownExtension extension}.
*
* @return the created node processor function
*/
public buildFlatNodeProcessor(): (document: Document) => Document {
return this.extensions
.flatMap((extension) => extension.buildNodeProcessors())
.reduce(
(state, processor) => (document: Document) => state(processor.process(document)),
(document: Document) => document
)
}
/**
* Collects all {@link ComponentReplacer component replacers} from all saved {@link MarkdownExtension extension}.
*/
public buildReplacers(): ComponentReplacer[] {
return this.extensions.flatMap((extension) => extension.buildReplacers())
}
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MermaidChart } from './mermaid-chart'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support for chart rendering using mermaid to the markdown rendering using code fences with "mermaid" as language.
*/
export class MermaidMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(MermaidChart, 'mermaid')]
}
}

View file

@ -1,26 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { SequenceDiagram } from './sequence-diagram'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
import type { Linter } from '../../../editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter'
import { t } from 'i18next'
/**
* Adds legacy support for sequence diagram to the markdown rendering using code fences with "sequence" as language.
*/
export class SequenceDiagramMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(SequenceDiagram, 'sequence')]
}
public buildLinter(): Linter[] {
return [new SingleLineRegexLinter(/```sequence/, t('editor.linter.sequence'), () => '```mermaid\nsequenceDiagram')]
}
}

View file

@ -1,41 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import React from 'react'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { TaskListCheckbox } from './task-list-checkbox'
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
/**
* Detects task lists and renders them as checkboxes that execute a callback if clicked.
*/
export class TaskListReplacer extends ComponentReplacer {
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
super()
this.onTaskCheckedChange = (lineInMarkdown, checked) => onTaskCheckedChange?.(lineInMarkdown, checked)
}
public replace(node: Element): NodeReplacement {
if (node.attribs?.class !== 'task-list-item-checkbox') {
return DO_NOT_REPLACE
}
const lineInMarkdown = Number(node.attribs['data-line'])
return isNaN(lineInMarkdown) ? (
DO_NOT_REPLACE
) : (
<TaskListCheckbox
onTaskCheckedChange={this.onTaskCheckedChange}
checked={node.attribs.checked !== undefined}
lineInMarkdown={lineInMarkdown}
/>
)
}
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { VegaLiteChart } from './vega-lite-chart'
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
/**
* Adds support for chart rendering using vega lite to the markdown rendering using code fences with "vega-lite" as language.
*/
export class VegaLiteMarkdownExtension extends CodeBlockMarkdownExtension {
public buildReplacers(): ComponentReplacer[] {
return [new CodeBlockComponentReplacer(VegaLiteChart, 'vega-lite')]
}
}

View file

@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import type MarkdownIt from 'markdown-it'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
import { VimeoFrame } from './vimeo-frame'
import { legacyVimeoRegex, replaceLegacyVimeoShortCodeMarkdownItPlugin } from './replace-legacy-vimeo-short-code'
import type { Linter } from '../../../editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter'
import { t } from 'i18next'
/**
* Adds vimeo video embeddings using link detection and the legacy vimeo short code syntax.
*/
export class VimeoMarkdownExtension extends MarkdownExtension {
public static readonly tagName = 'app-vimeo'
public configureMarkdownIt(markdownIt: MarkdownIt): void {
replaceLegacyVimeoShortCodeMarkdownItPlugin(markdownIt)
replaceVimeoLinkMarkdownItPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return [new CustomTagWithIdComponentReplacer(VimeoFrame, VimeoMarkdownExtension.tagName)]
}
public buildTagNameAllowList(): string[] {
return [VimeoMarkdownExtension.tagName]
}
public buildLinter(): Linter[] {
return [
new SingleLineRegexLinter(
legacyVimeoRegex,
t('editor.linter.shortcode', { shortcode: 'Vimeo' }),
(match: string) => `https://player.vimeo.com/video/${match}`
)
]
}
}

View file

@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownExtension } from '../markdown-extension'
import { replaceYouTubeLinkMarkdownItPlugin } from './replace-youtube-link'
import { legacyYouTubeRegex, replaceLegacyYoutubeShortCodeMarkdownItPlugin } from './replace-legacy-youtube-short-code'
import type MarkdownIt from 'markdown-it'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
import { YouTubeFrame } from './youtube-frame'
import type { Linter } from '../../../editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter'
import { t } from 'i18next'
/**
* Adds YouTube video embeddings using link detection and the legacy YouTube short code syntax.
*/
export class YoutubeMarkdownExtension extends MarkdownExtension {
public static readonly tagName = 'app-youtube'
public configureMarkdownIt(markdownIt: MarkdownIt): void {
replaceYouTubeLinkMarkdownItPlugin(markdownIt)
replaceLegacyYoutubeShortCodeMarkdownItPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return [new CustomTagWithIdComponentReplacer(YouTubeFrame, YoutubeMarkdownExtension.tagName)]
}
public buildTagNameAllowList(): string[] {
return [YoutubeMarkdownExtension.tagName]
}
public buildLinter(): Linter[] {
return [
new SingleLineRegexLinter(
legacyYouTubeRegex,
t('editor.linter.shortcode', { shortcode: 'YouTube' }),
(match: string) => `https://www.youtube.com/watch?v=${match}`
)
]
}
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -15,7 +15,7 @@ import type { Property } from 'csstype'
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { ProxyImageFrame } from '../../markdown-extension/image/proxy-image-frame'
import { ProxyImageFrame } from '../../extensions/image/proxy-image-frame'
const log = new Logger('OneClickEmbedding')

View file

@ -7,15 +7,13 @@
import React, { useEffect, useMemo, useRef } from 'react'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
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 { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { LoadingSlide } from './loading-slide'
import { RevealMarkdownExtension } from './markdown-extension/reveal/reveal-markdown-extension'
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
slideOptions?: SlideOptions
@ -39,26 +37,19 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
className,
markdownContentLines,
onFirstHeadingChange,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
newlinesAreBreaks,
slideOptions
}) => {
const markdownBodyRef = useRef<HTMLDivElement>(null)
const tocAst = useRef<TocAst>()
const extensions = useMarkdownExtensions(
baseUrl,
undefined,
useMemo(() => [new RevealMarkdownExtension()], []),
onTaskCheckedChange,
onImageClick,
onTocChange
useMemo(() => [new RevealMarkdownExtension()], [])
)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
const revealStatus = useReveal(markdownContentLines, slideOptions)
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
@ -68,8 +59,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
}
}, [extractFirstHeadline, markdownContentLines, revealStatus])
useOnRefChange(tocAst, onTocChange)
const slideShowDOM = useMemo(
() => (revealStatus === REVEAL_STATUS.INITIALISED ? markdownReactDom : <LoadingSlide />),
[markdownReactDom, revealStatus]
@ -83,5 +72,3 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
</div>
)
}
export default SlideshowMarkdownRenderer

View file

@ -6,19 +6,19 @@
import React, { useMemo } from 'react'
import { useConvertMarkdownToReactDom } from '../hooks/use-convert-markdown-to-react-dom'
import type { MarkdownExtension } from '../markdown-extension/markdown-extension'
import { StoreProvider } from '../../../redux/store-provider'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
export interface SimpleMarkdownRendererProps {
content: string
extensions: MarkdownExtension[]
extensions: MarkdownRendererExtension[]
}
/**
* A markdown renderer for tests.
*
* @param content The content to be rendered.
* @param extensions The {@link MarkdownExtension MarkdownExtensions} to use for rendering.
* @param extensions The {@link MarkdownRendererExtension MarkdownExtensions} to use for rendering.
*/
export const TestMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ content, extensions }) => {
const lines = useMemo(() => content.split('\n'), [content])

View file

@ -7,9 +7,9 @@
import equal from 'fast-deep-equal'
import type { RefObject } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import type { LineMarkerPosition } from '../markdown-extension/linemarker/types'
import type { LineMarkers } from '../markdown-extension/linemarker/add-line-marker-markdown-it-plugin'
import useResizeObserver from '@react-hook/resize-observer'
import type { LineMarkerPosition } from '../extensions/linemarker/types'
import type { LineMarkers } from '../extensions/linemarker/add-line-marker-markdown-it-plugin'
const calculateLineMarkerPositions = (
documentElement: HTMLDivElement,

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { LineWithId } from '../markdown-extension/linemarker/types'
import type { ArrayChange } from 'diff'
import { diffArrays } from 'diff'
import type { LineWithId } from '../extensions/linemarker/types'
type NewLine = string
type LineChange = ArrayChange<NewLine | LineWithId>

View file

@ -10,9 +10,9 @@ import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertN
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
import { DO_NOT_REPLACE } from '../replace-components/component-replacer'
import React from 'react'
import type { LineWithId } from '../markdown-extension/linemarker/types'
import { Optional } from '@mrdrogdrog/optional'
import { LinemarkerMarkdownExtension } from '../markdown-extension/linemarker/linemarker-markdown-extension'
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
import type { LineWithId } from '../extensions/linemarker/types'
type LineIndexPair = [startLineIndex: number, endLineIndex: number]

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useState } from 'react'
import styles from './markdown-document.module.scss'
import { ShowIf } from '../common/show-if/show-if'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
import type { TocAst } from 'markdown-it-toc-done-right'
import { useExtensionEventEmitterHandler } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents-markdown-extension'
export interface DocumentTocSidebarProps {
width: number
disableToc: boolean
baseUrl: string
}
export const DocumentTocSidebar: React.FC<DocumentTocSidebarProps> = ({ disableToc, width, baseUrl }) => {
const [tocAst, setTocAst] = useState<TocAst>()
useExtensionEventEmitterHandler(TableOfContentsMarkdownExtension.EVENT_NAME, setTocAst)
return (
<div className={`${styles['markdown-document-side']} pt-4`}>
<ShowIf condition={!!tocAst && !disableToc}>
<WidthBasedTableOfContents tocAst={tocAst as TocAst} baseUrl={baseUrl} width={width} />
</ShowIf>
</div>
)
}

View file

@ -6,10 +6,10 @@
import type React from 'react'
import { useCallback, useMemo, useState } from 'react'
import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types'
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
import { useOnUserScroll } from './use-on-user-scroll'
import { useScrollToLineMark } from './use-scroll-to-line-mark'
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
/**
* Synchronizes the scroll status of the given container with the given scroll state and posts changes if the user scrolls.

View file

@ -6,8 +6,8 @@
import type React from 'react'
import { useCallback } from 'react'
import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types'
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
/**
* Provides a callback to handle user scrolling.

View file

@ -6,9 +6,9 @@
import type { RefObject } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types'
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
import { findLineMarks } from '../../../editor-page/synced-scroll/utils'
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
/**
* Scrolls the given container to the correct {@link LineMarkerPosition}.

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type React from 'react'
import { useCallback } from 'react'
import type { ImageClickHandler } from '../../markdown-renderer/markdown-extension/image/proxy-image-replacer'
import type { RendererToEditorCommunicator } from '../window-post-message-communicator/renderer-to-editor-communicator'
import { CommunicationMessageType } from '../window-post-message-communicator/rendering-message'
/**
* Generates a callback to send information about a clicked image from the iframe back to the editor.
*
* @param iframeCommunicator The communicator to send the message with.
* @return The callback to give to on onClick handler
*/
export const useImageClickHandler = (iframeCommunicator: RendererToEditorCommunicator): ImageClickHandler => {
return useCallback(
(event: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
const image = event.target as HTMLImageElement
if (image.src === '') {
return
}
iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.IMAGE_CLICKED,
details: {
src: image.src,
alt: image.alt,
title: image.title
}
})
},
[iframeCommunicator]
)
}

View file

@ -4,19 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ScrollState } from '../editor-page/synced-scroll/scroll-props'
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
import { setDarkMode } from '../../redux/dark-mode/methods'
import type { ImageClickHandler } from '../markdown-renderer/markdown-extension/image/proxy-image-replacer'
import { useImageClickHandler } from './hooks/use-image-click-handler'
import { MarkdownDocument } from './markdown-document'
import { countWords } from './word-counter'
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
import EventEmitter2 from 'eventemitter2'
import { eventEmitterContext } from '../markdown-renderer/hooks/use-extension-event-emitter'
/**
* Wraps the markdown rendering in an iframe.
@ -74,17 +74,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
}, [communicator])
)
const onTaskCheckedChange = useCallback(
(lineInMarkdown: number, checked: boolean) => {
communicator.sendMessageToOtherSide({
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
checked,
lineInMarkdown
})
},
[communicator]
)
const onFirstHeadingChange = useCallback(
(firstHeading?: string) => {
communicator.sendMessageToOtherSide({
@ -115,8 +104,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
[communicator]
)
const onImageClick: ImageClickHandler = useImageClickHandler(communicator)
const onHeightChange = useCallback(
(height: number) => {
communicator.sendMessageToOtherSide({
@ -127,51 +114,75 @@ export const IframeMarkdownRenderer: React.FC = () => {
[communicator]
)
if (!baseConfiguration) {
return (
<span>This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.</span>
)
}
const renderer = useMemo(() => {
if (!baseConfiguration) {
return (
<span>
This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.
</span>
)
}
switch (baseConfiguration.rendererType) {
case RendererType.DOCUMENT:
return (
<MarkdownDocument
additionalOuterContainerClasses={'vh-100 bg-light'}
markdownContentLines={markdownContentLines}
onTaskCheckedChange={onTaskCheckedChange}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource}
scrollState={scrollState}
onScroll={onScroll}
baseUrl={baseConfiguration.baseUrl}
onImageClick={onImageClick}
/>
)
case RendererType.SLIDESHOW:
return (
<SlideshowMarkdownRenderer
markdownContentLines={markdownContentLines}
baseUrl={baseConfiguration.baseUrl}
onFirstHeadingChange={onFirstHeadingChange}
onImageClick={onImageClick}
scrollState={scrollState}
slideOptions={slideOptions}
/>
)
case RendererType.MOTD:
case RendererType.INTRO:
return (
<MarkdownDocument
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
markdownContentLines={markdownContentLines}
baseUrl={baseConfiguration.baseUrl}
onImageClick={onImageClick}
disableToc={true}
onHeightChange={onHeightChange}
/>
)
default:
return null
}
switch (baseConfiguration.rendererType) {
case RendererType.DOCUMENT:
return (
<MarkdownDocument
additionalOuterContainerClasses={'vh-100 bg-light'}
markdownContentLines={markdownContentLines}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource}
scrollState={scrollState}
onScroll={onScroll}
baseUrl={baseConfiguration.baseUrl}
onHeightChange={onHeightChange}
/>
)
case RendererType.SLIDESHOW:
return (
<SlideshowMarkdownRenderer
markdownContentLines={markdownContentLines}
baseUrl={baseConfiguration.baseUrl}
onFirstHeadingChange={onFirstHeadingChange}
scrollState={scrollState}
slideOptions={slideOptions}
/>
)
case RendererType.MOTD:
case RendererType.INTRO:
return (
<MarkdownDocument
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
markdownContentLines={markdownContentLines}
baseUrl={baseConfiguration.baseUrl}
disableToc={true}
onHeightChange={onHeightChange}
/>
)
default:
return null
}
}, [
baseConfiguration,
markdownContentLines,
onFirstHeadingChange,
onHeightChange,
onMakeScrollSource,
onScroll,
scrollState,
slideOptions
])
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
useEffect(() => {
extensionEventEmitter.onAny((event, values) => {
communicator.sendMessageToOtherSide({
type: CommunicationMessageType.EXTENSION_EVENT,
eventName: typeof event === 'object' ? event.join('.') : event,
payload: values
})
})
}, [communicator, extensionEventEmitter])
return <eventEmitterContext.Provider value={extensionEventEmitter}>{renderer}</eventEmitterContext.Provider>
}

View file

@ -4,25 +4,20 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { TocAst } from 'markdown-it-toc-done-right'
import type { MutableRefObject } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import useResizeObserver from '@react-hook/resize-observer'
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
import type { ImageClickHandler } from '../markdown-renderer/markdown-extension/image/proxy-image-replacer'
import styles from './markdown-document.module.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
import { ShowIf } from '../common/show-if/show-if'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { DocumentTocSidebar } from './document-toc-sidebar'
export interface RendererProps extends ScrollProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
markdownContentLines: string[]
onImageClick?: ImageClickHandler
onHeightChange?: (height: number) => void
}
@ -55,10 +50,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
additionalRendererClasses,
onFirstHeadingChange,
onMakeScrollSource,
onTaskCheckedChange,
baseUrl,
markdownContentLines,
onImageClick,
onScroll,
scrollState,
onHeightChange,
@ -77,8 +70,6 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
setInternalDocumentRenderPaneSize(entry.contentRect)
)
const containerWidth = internalDocumentRenderPaneSize?.width ?? 0
const [tocAst, setTocAst] = useState<TocAst>()
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines])
@ -106,18 +97,15 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
markdownContentLines={markdownContentLines}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={setTocAst}
baseUrl={baseUrl}
onImageClick={onImageClick}
newlinesAreBreaks={newlinesAreBreaks}
/>
</div>
<div className={`${styles['markdown-document-side']} pt-4`}>
<ShowIf condition={!!tocAst && !disableToc}>
<WidthBasedTableOfContents tocAst={tocAst as TocAst} baseUrl={baseUrl} width={containerWidth} />
</ShowIf>
</div>
<DocumentTocSidebar
width={internalDocumentRenderPaneSize?.width ?? 0}
baseUrl={baseUrl}
disableToc={disableToc ?? false}
/>
</div>
)
}

View file

@ -10,18 +10,17 @@ export enum CommunicationMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
RENDERER_READY = 'RENDERER_READY',
SET_DARKMODE = 'SET_DARKMODE',
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
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',
IMAGE_CLICKED = 'IMAGE_CLICKED',
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
GET_WORD_COUNT = 'GET_WORD_COUNT',
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
IMAGE_UPLOAD = 'IMAGE_UPLOAD'
IMAGE_UPLOAD = 'IMAGE_UPLOAD',
EXTENSION_EVENT = 'EXTENSION_EVENT'
}
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
@ -33,6 +32,12 @@ export interface SetDarkModeMessage {
activated: boolean
}
export interface ExtensionEvent {
type: CommunicationMessageType.EXTENSION_EVENT
eventName: string
payload: unknown
}
export interface ImageDetails {
alt?: string
src: string
@ -56,11 +61,6 @@ export interface GetWordCountMessage {
type: CommunicationMessageType.GET_WORD_COUNT
}
export interface ImageClickedMessage {
type: CommunicationMessageType.IMAGE_CLICKED
details: ImageDetails
}
export interface SetMarkdownContentMessage {
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
content: string[]
@ -71,12 +71,6 @@ export interface SetScrollStateMessage {
scrollState: ScrollState
}
export interface OnTaskCheckboxChangeMessage {
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
lineInMarkdown: number
checked: boolean
}
export interface OnFirstHeadingChangeMessage {
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
firstHeading: string | undefined
@ -104,15 +98,14 @@ export type CommunicationMessages =
| SetDarkModeMessage
| SetBaseUrlMessage
| GetWordCountMessage
| ImageClickedMessage
| SetMarkdownContentMessage
| SetScrollStateMessage
| OnTaskCheckboxChangeMessage
| OnFirstHeadingChangeMessage
| SetSlideOptionsMessage
| OnHeightChangeMessage
| OnWordCountCalculatedMessage
| ImageUploadMessage
| ExtensionEvent
export type EditorToRendererMessageType =
| CommunicationMessageType.SET_MARKDOWN_CONTENT
@ -127,12 +120,11 @@ export type RendererToEditorMessageType =
| CommunicationMessageType.RENDERER_READY
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
| CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.IMAGE_CLICKED
| CommunicationMessageType.ON_HEIGHT_CHANGE
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
| CommunicationMessageType.IMAGE_UPLOAD
| CommunicationMessageType.EXTENSION_EVENT
export enum RendererType {
DOCUMENT = 'document',

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import type React from 'react'
import { Fragment } from 'react'
import type EventEmitter2 from 'eventemitter2'
export abstract class AppExtension {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
return []
}
public buildCodeMirrorLinter(): Linter[] {
return []
}
public buildEditorExtensionComponent(): React.FC {
return Fragment
}
}

Some files were not shown because too many files have changed in this diff Show more