mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 22:54:42 -04:00
feat(extensions): Introduce app extensions
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
afe35ca164
commit
665f93d800
224 changed files with 1621 additions and 1121 deletions
|
@ -4,8 +4,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { 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 {
|
export interface CheatsheetLineProps {
|
||||||
markdown: string
|
markdown: string
|
||||||
|
@ -13,7 +17,7 @@ export interface CheatsheetLineProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const HighlightedCode = React.lazy(
|
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'))
|
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 }) => {
|
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTaskCheckedChange }) => {
|
||||||
const lines = useMemo(() => markdown.split('\n'), [markdown])
|
const lines = useMemo(() => markdown.split('\n'), [markdown])
|
||||||
const checkboxClick = useCallback(
|
const eventEmitter = useMemo(() => new EventEmitter2(), [])
|
||||||
(lineInMarkdown: number, newValue: boolean) => {
|
|
||||||
onTaskCheckedChange(newValue)
|
useEffect(() => {
|
||||||
},
|
const handler = ({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked)
|
||||||
[onTaskCheckedChange]
|
eventEmitter.on(TaskListCheckboxAppExtension.EVENT_NAME, handler)
|
||||||
)
|
return () => {
|
||||||
|
eventEmitter.off(TaskListCheckboxAppExtension.EVENT_NAME, handler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
|
@ -44,11 +51,9 @@ export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTask
|
||||||
}>
|
}>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<DocumentMarkdownRenderer
|
<eventEmitterContext.Provider value={eventEmitter}>
|
||||||
markdownContentLines={lines}
|
<DocumentMarkdownRenderer markdownContentLines={lines} baseUrl={'https://example.org'} />
|
||||||
baseUrl={'https://example.org'}
|
</eventEmitterContext.Provider>
|
||||||
onTaskCheckedChange={checkboxClick}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td className={'markdown-body'}>
|
<td className={'markdown-body'}>
|
||||||
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/
|
||||||
import { NoteType } from '../../../redux/note-details/types/note-details'
|
import { NoteType } from '../../../redux/note-details/types/note-details'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
|
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 { useOnScrollWithLineOffset } from './hooks/use-on-scroll-with-line-offset'
|
||||||
import { useScrollStateWithoutLineOffset } from './hooks/use-scroll-state-without-line-offset'
|
import { useScrollStateWithoutLineOffset } from './hooks/use-scroll-state-without-line-offset'
|
||||||
import { setRendererStatus } from '../../../redux/renderer-status/methods'
|
import { setRendererStatus } from '../../../redux/renderer-status/methods'
|
||||||
|
@ -31,7 +30,6 @@ export type EditorDocumentRendererProps = Omit<
|
||||||
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = ({ scrollState, onScroll, ...props }) => {
|
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = ({ scrollState, onScroll, ...props }) => {
|
||||||
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
const trimmedContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||||
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
|
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
|
||||||
const setCheckboxInEditor = useSetCheckboxInEditor()
|
|
||||||
const adjustedOnScroll = useOnScrollWithLineOffset(onScroll)
|
const adjustedOnScroll = useOnScrollWithLineOffset(onScroll)
|
||||||
const adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
|
const adjustedScrollState = useScrollStateWithoutLineOffset(scrollState)
|
||||||
|
|
||||||
|
@ -40,7 +38,6 @@ export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = ({
|
||||||
{...props}
|
{...props}
|
||||||
onScroll={adjustedOnScroll}
|
onScroll={adjustedOnScroll}
|
||||||
scrollState={adjustedScrollState}
|
scrollState={adjustedScrollState}
|
||||||
onTaskCheckedChange={setCheckboxInEditor}
|
|
||||||
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
|
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
|
||||||
markdownContentLines={trimmedContentLines}
|
markdownContentLines={trimmedContentLines}
|
||||||
onRendererStatusChange={setRendererStatus}
|
onRendererStatusChange={setRendererStatus}
|
||||||
|
|
|
@ -24,6 +24,9 @@ import { NoteAndAppTitleHead } from '../layout/note-and-app-title-head'
|
||||||
import equal from 'fast-deep-equal'
|
import equal from 'fast-deep-equal'
|
||||||
import { EditorPane } from './editor-pane/editor-pane'
|
import { EditorPane } from './editor-pane/editor-pane'
|
||||||
import { ChangeEditorContentContextProvider } from './change-content-context/change-content-context'
|
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 {
|
export enum ScrollSource {
|
||||||
EDITOR = 'editor',
|
EDITOR = 'editor',
|
||||||
|
@ -124,8 +127,13 @@ export const EditorPageContent: React.FC = () => {
|
||||||
[onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
|
[onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const editorExtensionComponents = useComponentsFromAppExtensions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeEditorContentContextProvider>
|
<ChangeEditorContentContextProvider>
|
||||||
|
<ExtensionEventEmitterProvider>
|
||||||
|
{editorExtensionComponents}
|
||||||
|
<CommunicatorImageLightbox />
|
||||||
<NoteAndAppTitleHead />
|
<NoteAndAppTitleHead />
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column vh-100'}>
|
<div className={'d-flex flex-column vh-100'}>
|
||||||
|
@ -141,6 +149,7 @@ export const EditorPageContent: React.FC = () => {
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ExtensionEventEmitterProvider>
|
||||||
</ChangeEditorContentContextProvider>
|
</ChangeEditorContentContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
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 { languages } from '@codemirror/language-data'
|
||||||
import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
|
import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
|
||||||
import { useCodeMirrorReference, useSetCodeMirrorReference } from '../change-content-context/change-content-context'
|
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 { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text'
|
||||||
import { lintGutter } from '@codemirror/lint'
|
import { lintGutter } from '@codemirror/lint'
|
||||||
import { useLinter } from './linter/linter'
|
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 { FrontmatterLinter } from './linter/frontmatter-linter'
|
||||||
import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
|
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.
|
* Renders the text editor pane of the editor.
|
||||||
|
@ -90,15 +87,9 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection)
|
useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection)
|
||||||
const spellCheck = useApplicationState((state) => state.editorConfig.spellCheck)
|
const spellCheck = useApplicationState((state) => state.editorConfig.spellCheck)
|
||||||
|
|
||||||
// ToDo: Don't initialize new extension array here, instead refactor to global extension array
|
|
||||||
const markdownExtensionsLinters = useMemo(() => {
|
const markdownExtensionsLinters = useMemo(() => {
|
||||||
return [
|
return optionalAppExtensions
|
||||||
new YoutubeMarkdownExtension(),
|
.flatMap((extension) => extension.buildCodeMirrorLinter())
|
||||||
new VimeoMarkdownExtension(),
|
|
||||||
new SequenceDiagramMarkdownExtension(),
|
|
||||||
new LegacyShortcodesMarkdownExtension()
|
|
||||||
]
|
|
||||||
.flatMap((extension) => extension.buildLinter())
|
|
||||||
.concat(new FrontmatterLinter())
|
.concat(new FrontmatterLinter())
|
||||||
}, [])
|
}, [])
|
||||||
const linter = useLinter(markdownExtensionsLinters)
|
const linter = useLinter(markdownExtensionsLinters)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -3,20 +3,19 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { isTestMode } from '../../../utils/test-modes'
|
||||||
import type { RendererProps } from '../../render-page/markdown-document'
|
import type { RendererProps } from '../../render-page/markdown-document'
|
||||||
import type {
|
import type {
|
||||||
|
ExtensionEvent,
|
||||||
OnFirstHeadingChangeMessage,
|
OnFirstHeadingChangeMessage,
|
||||||
OnHeightChangeMessage,
|
OnHeightChangeMessage,
|
||||||
OnTaskCheckboxChangeMessage,
|
|
||||||
RendererType,
|
RendererType,
|
||||||
SetScrollStateMessage
|
SetScrollStateMessage
|
||||||
} from '../../render-page/window-post-message-communicator/rendering-message'
|
} from '../../render-page/window-post-message-communicator/rendering-message'
|
||||||
import { CommunicationMessageType } 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 { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
|
||||||
import { useForceRenderPageUrlOnIframeLoadCallback } from './hooks/use-force-render-page-url-on-iframe-load-callback'
|
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 { 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 { useSendDarkModeStatusToRenderer } from './hooks/use-send-dark-mode-status-to-renderer'
|
||||||
import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-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 { Logger } from '../../../utils/logger'
|
||||||
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
||||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { getGlobalState } from '../../../redux'
|
|
||||||
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { WaitSpinner } from '../../common/wait-spinner/wait-spinner'
|
import { WaitSpinner } from '../../common/wait-spinner/wait-spinner'
|
||||||
|
import { useExtensionEventEmitter } from '../../markdown-renderer/hooks/use-extension-event-emitter'
|
||||||
|
|
||||||
export interface RenderIframeProps extends RendererProps {
|
export interface RenderIframeProps extends RendererProps {
|
||||||
rendererType: RendererType
|
rendererType: RendererType
|
||||||
|
@ -58,7 +57,6 @@ const log = new Logger('RenderIframe')
|
||||||
*/
|
*/
|
||||||
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onTaskCheckedChange,
|
|
||||||
scrollState,
|
scrollState,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
@ -92,6 +90,10 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
}
|
}
|
||||||
}, [iframeCommunicator, rendererReady])
|
}, [iframeCommunicator, rendererReady])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onRendererStatusChange?.(rendererReady)
|
||||||
|
}, [onRendererStatusChange, rendererReady])
|
||||||
|
|
||||||
useEditorReceiveHandler(
|
useEditorReceiveHandler(
|
||||||
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
||||||
useCallback(
|
useCallback(
|
||||||
|
@ -105,15 +107,15 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const eventEmitter = useExtensionEventEmitter()
|
||||||
|
|
||||||
useEditorReceiveHandler(
|
useEditorReceiveHandler(
|
||||||
CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
|
CommunicationMessageType.EXTENSION_EVENT,
|
||||||
useCallback(
|
useMemo(() => {
|
||||||
(values: OnTaskCheckboxChangeMessage) => {
|
return eventEmitter === undefined
|
||||||
const lineOffset = getGlobalState().noteDetails.frontmatterRendererInfo.lineOffset
|
? undefined
|
||||||
onTaskCheckedChange?.(values.lineInMarkdown + lineOffset, values.checked)
|
: (values: ExtensionEvent) => eventEmitter.emit(values.eventName, values.payload)
|
||||||
},
|
}, [eventEmitter])
|
||||||
[onTaskCheckedChange]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEditorReceiveHandler(
|
useEditorReceiveHandler(
|
||||||
|
@ -169,7 +171,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<CommunicatorImageLightbox />
|
|
||||||
<ShowIf condition={!rendererReady}>
|
<ShowIf condition={!rendererReady}>
|
||||||
<WaitSpinner />
|
<WaitSpinner />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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.
|
* Finds the {@link LineMarkerPosition line markers} from a list of given line markers that are the closest to the given line number.
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { ReactElement } from 'react'
|
||||||
import React, { Fragment, useMemo } from 'react'
|
import React, { Fragment, useMemo } from 'react'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { tocSlugify } from './toc-slugify'
|
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.
|
* Generates a React DOM part for the table of contents from the given AST of the document.
|
||||||
|
|
|
@ -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
|
* 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'
|
import type { Ref } from 'react'
|
||||||
|
|
||||||
export interface CommonMarkdownRendererProps {
|
export interface CommonMarkdownRendererProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
|
||||||
onTocChange?: (ast?: TocAst) => void
|
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
onImageClick?: ImageClickHandler
|
|
||||||
outerContainerRef?: Ref<HTMLDivElement>
|
outerContainerRef?: Ref<HTMLDivElement>
|
||||||
newlinesAreBreaks?: boolean
|
newlinesAreBreaks?: boolean
|
||||||
lineOffset?: number
|
lineOffset?: number
|
||||||
|
|
|
@ -6,15 +6,15 @@
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||||
import type { LineMarkerPosition } from './markdown-extension/linemarker/types'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||||
import { HeadlineAnchorsMarkdownExtension } from './markdown-extension/headline-anchors-markdown-extension'
|
|
||||||
import { cypressId } from '../../utils/cypress-attribute'
|
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 {
|
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||||
|
@ -39,10 +39,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onLineMarkerPositionChanged,
|
onLineMarkerPositionChanged,
|
||||||
onTaskCheckedChange,
|
|
||||||
onTocChange,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
onImageClick,
|
|
||||||
outerContainerRef,
|
outerContainerRef,
|
||||||
newlinesAreBreaks
|
newlinesAreBreaks
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -52,12 +49,10 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
||||||
const extensions = useMarkdownExtensions(
|
const extensions = useMarkdownExtensions(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
currentLineMarkers,
|
currentLineMarkers,
|
||||||
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], []),
|
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], [])
|
||||||
onTaskCheckedChange,
|
|
||||||
onImageClick,
|
|
||||||
onTocChange
|
|
||||||
)
|
)
|
||||||
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
|
|
||||||
|
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
|
||||||
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
useCalculateLineMarkerPosition(
|
useCalculateLineMarkerPosition(
|
||||||
|
|
|
@ -17,7 +17,7 @@ const ruleName = 'code-highlighter'
|
||||||
* @param state The current state of the processing {@link MarkdownIt} instance.
|
* @param state The current state of the processing {@link MarkdownIt} instance.
|
||||||
* @see MarkdownIt.RuleCore
|
* @see MarkdownIt.RuleCore
|
||||||
*/
|
*/
|
||||||
const rule: RuleCore = (state) => {
|
const rule: RuleCore = (state): void => {
|
||||||
state.tokens.forEach((token) => {
|
state.tokens.forEach((token) => {
|
||||||
if (token.type === 'fence') {
|
if (token.type === 'fence') {
|
||||||
const highlightInfos = parseCodeBlockParameters(token.info)
|
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
|
* @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) {
|
if (markdownIt.core.ruler.getRules(ruleName).length === 0) {
|
||||||
markdownIt.core.ruler.push(ruleName, rule, { alt: [ruleName] })
|
markdownIt.core.ruler.push(ruleName, rule, { alt: [ruleName] })
|
||||||
}
|
}
|
|
@ -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 []
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,15 +4,17 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type EventEmitter2 from 'eventemitter2'
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import type { NodeProcessor } from '../node-preprocessors/node-processor'
|
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||||
import type { ComponentReplacer } from '../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import type { Linter } from '../../editor-page/editor-pane/linter/linter'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for Markdown extensions.
|
* 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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
return
|
return
|
||||||
|
@ -34,8 +36,4 @@ export abstract class MarkdownExtension {
|
||||||
public buildTagNameAllowList(): string[] {
|
public buildTagNameAllowList(): string[] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildLinter(): Linter[] {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 type MarkdownIt from 'markdown-it'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { isDevMode } from '../../../utils/test-modes'
|
import { isDevMode } from '../../../utils/test-modes'
|
||||||
|
@ -14,7 +14,7 @@ const log = new Logger('DebuggerMarkdownExtension')
|
||||||
/**
|
/**
|
||||||
* Adds console debug logging to the markdown rendering.
|
* Adds console debug logging to the markdown rendering.
|
||||||
*/
|
*/
|
||||||
export class DebuggerMarkdownExtension extends MarkdownExtension {
|
export class DebuggerMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
|
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
markdownIt.core.ruler.push('printStateToConsole', (state) => {
|
markdownIt.core.ruler.push('printStateToConsole', (state) => {
|
|
@ -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
|
* 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 type MarkdownIt from 'markdown-it'
|
||||||
import emoji from 'markdown-it-emoji/bare'
|
import emoji from 'markdown-it-emoji/bare'
|
||||||
import { combinedEmojiData } from './mapping'
|
import { combinedEmojiData } from './mapping'
|
||||||
|
@ -12,7 +12,7 @@ import { combinedEmojiData } from './mapping'
|
||||||
/**
|
/**
|
||||||
* Adds support for utf-8 emojis.
|
* Adds support for utf-8 emojis.
|
||||||
*/
|
*/
|
||||||
export class EmojiMarkdownExtension extends MarkdownExtension {
|
export class EmojiMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
markdownIt.use(emoji, {
|
markdownIt.use(emoji, {
|
||||||
defs: combinedEmojiData
|
defs: combinedEmojiData
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 type MarkdownIt from 'markdown-it'
|
||||||
import abbreviation from 'markdown-it-abbr'
|
import abbreviation from 'markdown-it-abbr'
|
||||||
import definitionList from 'markdown-it-deflist'
|
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.
|
* Adds some common markdown syntaxes to the markdown rendering.
|
||||||
*/
|
*/
|
||||||
export class GenericSyntaxMarkdownExtension extends MarkdownExtension {
|
export class GenericSyntaxMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
abbreviation(markdownIt)
|
abbreviation(markdownIt)
|
||||||
definitionList(markdownIt)
|
definitionList(markdownIt)
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 type MarkdownIt from 'markdown-it'
|
||||||
import anchor from 'markdown-it-anchor'
|
import anchor from 'markdown-it-anchor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds headline anchors to the markdown rendering.
|
* Adds headline anchors to the markdown rendering.
|
||||||
*/
|
*/
|
||||||
export class HeadlineAnchorsMarkdownExtension extends MarkdownExtension {
|
export class HeadlineAnchorsMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
anchor(markdownIt, {
|
anchor(markdownIt, {
|
||||||
permalink: anchor.permalink.ariaHidden({
|
permalink: anchor.permalink.ariaHidden({
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import { IframeCapsuleReplacer } from './iframe-capsule-replacer'
|
import { IframeCapsuleReplacer } from './iframe-capsule-replacer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a replacer that capsules iframes in a click shield.
|
* Adds a replacer that capsules iframes in a click shield.
|
||||||
*/
|
*/
|
||||||
export class IframeCapsuleMarkdownExtension extends MarkdownExtension {
|
export class IframeCapsuleMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public buildReplacers(): ComponentReplacer[] {
|
public buildReplacers(): ComponentReplacer[] {
|
||||||
return [new IframeCapsuleReplacer()]
|
return [new IframeCapsuleReplacer()]
|
||||||
}
|
}
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { addLineToPlaceholderImageTags } from './add-line-to-placeholder-image-tags'
|
||||||
import type MarkdownIt from 'markdown-it/lib'
|
import type MarkdownIt from 'markdown-it/lib'
|
||||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
|
@ -13,7 +13,7 @@ import { ImagePlaceholderReplacer } from './image-placeholder-replacer'
|
||||||
/**
|
/**
|
||||||
* Adds support for {@link ImagePlaceholder}.
|
* Adds support for {@link ImagePlaceholder}.
|
||||||
*/
|
*/
|
||||||
export class ImagePlaceholderMarkdownExtension extends MarkdownExtension {
|
export class ImagePlaceholderMarkdownExtension extends MarkdownRendererExtension {
|
||||||
public static readonly PLACEHOLDER_URL = 'https://'
|
public static readonly PLACEHOLDER_URL = 'https://'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NodeReplacement } from '../../replace-components/component-replacer'
|
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 type { Element } from 'domhandler'
|
||||||
import { ImagePlaceholder } from './image-placeholder'
|
import { ImagePlaceholder } from './image-placeholder'
|
||||||
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
|
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
|
||||||
|
@ -25,7 +25,9 @@ export class ImagePlaceholderReplacer extends ComponentReplacer {
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(node: Element): NodeReplacement {
|
replace(node: Element): NodeReplacement {
|
||||||
if (node.name === 'img' && node.attribs && node.attribs.src === ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
|
if (node.name !== 'img' || node.attribs?.src !== ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
|
||||||
|
return DO_NOT_REPLACE
|
||||||
|
}
|
||||||
const lineIndex = Number(node.attribs['data-line'])
|
const lineIndex = Number(node.attribs['data-line'])
|
||||||
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
|
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
|
||||||
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
|
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
|
||||||
|
@ -40,5 +42,4 @@ export class ImagePlaceholderReplacer extends ComponentReplacer {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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} />
|
||||||
|
}
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -32,7 +32,7 @@ export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>
|
||||||
.catch((err) => log.error(err))
|
.catch((err) => log.error(err))
|
||||||
}, [imageProxyEnabled, src])
|
}, [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
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
return <img src={imageProxyEnabled ? imageUrl : src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props} />
|
return <img src={imageProxyEnabled ? imageUrl : src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props} />
|
||||||
}
|
}
|
|
@ -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()]
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import type { Element } from 'domhandler'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { NodeReplacement } from '../../replace-components/component-replacer'
|
import type { NodeReplacement } from '../../replace-components/component-replacer'
|
||||||
import { ComponentReplacer, DO_NOT_REPLACE } 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
|
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.
|
* Detects image tags and loads them via image proxy if configured.
|
||||||
*/
|
*/
|
||||||
export class ProxyImageReplacer extends ComponentReplacer {
|
export class ProxyImageReplacer extends ComponentReplacer {
|
||||||
private readonly clickHandler?: ImageClickHandler
|
|
||||||
|
|
||||||
constructor(clickHandler?: ImageClickHandler) {
|
|
||||||
super()
|
|
||||||
this.clickHandler = clickHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
public replace(node: Element): NodeReplacement {
|
public replace(node: Element): NodeReplacement {
|
||||||
return node.name !== 'img' ? (
|
return node.name !== 'img' ? (
|
||||||
DO_NOT_REPLACE
|
DO_NOT_REPLACE
|
||||||
) : (
|
) : (
|
||||||
<ProxyImageFrame
|
<EventEmittingProxyImageFrame
|
||||||
id={node.attribs.id}
|
id={node.attribs.id}
|
||||||
className={`${node.attribs.class} cursor-zoom-in`}
|
className={`${node.attribs.class} cursor-zoom-in`}
|
||||||
src={node.attribs.src}
|
src={node.attribs.src}
|
||||||
|
@ -35,7 +28,6 @@ export class ProxyImageReplacer extends ComponentReplacer {
|
||||||
title={node.attribs.title}
|
title={node.attribs.title}
|
||||||
width={node.attribs.width}
|
width={node.attribs.width}
|
||||||
height={node.attribs.height}
|
height={node.attribs.height}
|
||||||
onClick={this.clickHandler}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { LinemarkerReplacer } from './linemarker-replacer'
|
||||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import type { LineMarkers } from './add-line-marker-markdown-it-plugin'
|
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.
|
* 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'
|
public static readonly tagName = 'app-linemarker'
|
||||||
|
|
||||||
constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {
|
constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* 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 { JumpAnchorReplacer } from './jump-anchor-replacer'
|
||||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
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.
|
* 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) {
|
constructor(private baseUrl: string) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
|
@ -4,9 +4,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { 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'
|
import { LinkifyFixMarkdownExtension } from './linkify-fix-markdown-extension'
|
||||||
|
|
||||||
describe('Linkify markdown extensions', () => {
|
describe('Linkify markdown extensions', () => {
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 linkify from 'markdown-it/lib/rules_core/linkify'
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import tlds from 'tlds'
|
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.
|
* 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 {
|
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
|
||||||
markdownIt.linkify.tlds(tlds)
|
markdownIt.linkify.tlds(tlds)
|
||||||
markdownIt.core.ruler.push('linkify', (state) => {
|
markdownIt.core.ruler.push('linkify', (state) => {
|
|
@ -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
|
* 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 type MarkdownIt from 'markdown-it'
|
||||||
import { addSlideSectionsMarkdownItPlugin } from './reveal-sections'
|
import { addSlideSectionsMarkdownItPlugin } from './reveal-sections'
|
||||||
import { RevealCommentCommandNodePreprocessor } from './process-reveal-comment-nodes'
|
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.
|
* Adds support for reveal.js to the markdown rendering.
|
||||||
* This includes the generation of sections and the manipulation of elements using reveal comments.
|
* 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 {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
addSlideSectionsMarkdownItPlugin(markdownIt)
|
addSlideSectionsMarkdownItPlugin(markdownIt)
|
||||||
}
|
}
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownExtension } from '../markdown-extension'
|
|
||||||
import { SanitizerNodePreprocessor } from './dom-purifier-node-preprocessor'
|
import { SanitizerNodePreprocessor } from './dom-purifier-node-preprocessor'
|
||||||
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
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.
|
* 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[]) {
|
constructor(private tagNameWhiteList: string[]) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
|
@ -4,29 +4,31 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownExtension } from './markdown-extension'
|
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import toc 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 { 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.
|
* Adds table of content to the markdown rendering.
|
||||||
*/
|
*/
|
||||||
export class TableOfContentsMarkdownExtension extends MarkdownExtension {
|
export class TableOfContentsMarkdownExtension extends MarkdownRendererExtension {
|
||||||
constructor(private onTocChange?: (ast: TocAst) => void) {
|
public static readonly EVENT_NAME = 'TocChange'
|
||||||
super()
|
private lastAst: TocAst | undefined = undefined
|
||||||
}
|
|
||||||
|
|
||||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
if (!this.onTocChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toc(markdownIt, {
|
toc(markdownIt, {
|
||||||
placeholder: '(\\[TOC\\]|\\[toc\\])',
|
placeholder: '(\\[TOC\\]|\\[toc\\])',
|
||||||
listType: 'ul',
|
listType: 'ul',
|
||||||
level: [1, 2, 3],
|
level: [1, 2, 3],
|
||||||
callback: (code: string, ast: TocAst): void => {
|
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
|
slugify: tocSlugify
|
||||||
})
|
})
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
|
@ -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
|
* 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 type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import { UploadIndicatingImageFrameReplacer } from './upload-indicating-image-frame-replacer'
|
import { UploadIndicatingImageFrameReplacer } from './upload-indicating-image-frame-replacer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A markdown extension that shows {@link UploadIndicatingFrame} for images that are getting uploaded.
|
* 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[] {
|
buildReplacers(): ComponentReplacer[] {
|
||||||
return [new UploadIndicatingImageFrameReplacer()]
|
return [new UploadIndicatingImageFrameReplacer()]
|
||||||
}
|
}
|
|
@ -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])
|
|
@ -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])
|
||||||
|
}
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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])
|
||||||
|
}
|
|
@ -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])
|
||||||
|
}
|
|
@ -5,98 +5,59 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MutableRefObject } from 'react'
|
import type { MutableRefObject } from 'react'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { TableOfContentsMarkdownExtension } from '../markdown-extension/table-of-contents-markdown-extension'
|
import { GenericSyntaxMarkdownExtension } from '../extensions/generic-syntax-markdown-extension'
|
||||||
import { VegaLiteMarkdownExtension } from '../markdown-extension/vega-lite/vega-lite-markdown-extension'
|
import type { LineMarkers } from '../extensions/linemarker/add-line-marker-markdown-it-plugin'
|
||||||
import { LinemarkerMarkdownExtension } from '../markdown-extension/linemarker/linemarker-markdown-extension'
|
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
|
||||||
import { GistMarkdownExtension } from '../markdown-extension/gist/gist-markdown-extension'
|
import { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
|
||||||
import { YoutubeMarkdownExtension } from '../markdown-extension/youtube/youtube-markdown-extension'
|
import { IframeCapsuleMarkdownExtension } from '../extensions/iframe-capsule/iframe-capsule-markdown-extension'
|
||||||
import { VimeoMarkdownExtension } from '../markdown-extension/vimeo/vimeo-markdown-extension'
|
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-markdown-extension'
|
||||||
import { ProxyImageMarkdownExtension } from '../markdown-extension/image/proxy-image-markdown-extension'
|
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
|
||||||
import { CsvTableMarkdownExtension } from '../markdown-extension/csv/csv-table-markdown-extension'
|
import { EmojiMarkdownExtension } from '../extensions/emoji/emoji-markdown-extension'
|
||||||
import { AbcjsMarkdownExtension } from '../markdown-extension/abcjs/abcjs-markdown-extension'
|
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
|
||||||
import { SequenceDiagramMarkdownExtension } from '../markdown-extension/sequence-diagram/sequence-diagram-markdown-extension'
|
import { TableOfContentsMarkdownExtension } from '../extensions/table-of-contents-markdown-extension'
|
||||||
import { FlowchartMarkdownExtension } from '../markdown-extension/flowchart/flowchart-markdown-extension'
|
import { ImagePlaceholderMarkdownExtension } from '../extensions/image-placeholder/image-placeholder-markdown-extension'
|
||||||
import { MermaidMarkdownExtension } from '../markdown-extension/mermaid/mermaid-markdown-extension'
|
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||||
import { GraphvizMarkdownExtension } from '../markdown-extension/graphviz/graphviz-markdown-extension'
|
import { useExtensionEventEmitter } from './use-extension-event-emitter'
|
||||||
import { BlockquoteExtraTagMarkdownExtension } from '../markdown-extension/blockquote/blockquote-extra-tag-markdown-extension'
|
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
|
||||||
import { LinkAdjustmentMarkdownExtension } from '../markdown-extension/link-replacer/link-adjustment-markdown-extension'
|
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
|
||||||
import { KatexMarkdownExtension } from '../markdown-extension/katex/katex-markdown-extension'
|
|
||||||
import { TaskListMarkdownExtension } from '../markdown-extension/task-list/task-list-markdown-extension'
|
const optionalMarkdownRendererExtensions = optionalAppExtensions.flatMap((value) =>
|
||||||
import { PlantumlMarkdownExtension } from '../markdown-extension/plantuml/plantuml-markdown-extension'
|
value.buildMarkdownRendererExtensions()
|
||||||
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'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 baseUrl The base url for the {@link LinkAdjustmentMarkdownExtension}
|
||||||
* @param currentLineMarkers A {@link MutableRefObject reference} to {@link LineMarkers} for the {@link LinemarkerMarkdownExtension}
|
* @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 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
|
* @return The created list of markdown extensions
|
||||||
*/
|
*/
|
||||||
export const useMarkdownExtensions = (
|
export const useMarkdownExtensions = (
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
|
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
|
||||||
additionalExtensions: MarkdownExtension[],
|
additionalExtensions: MarkdownRendererExtension[]
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
): MarkdownRendererExtension[] => {
|
||||||
onImageClick?: ImageClickHandler,
|
const extensionEventEmitter = useExtensionEventEmitter()
|
||||||
onTocChange?: (ast?: TocAst) => void
|
//replace with global list
|
||||||
): MarkdownExtension[] => {
|
|
||||||
const toc = useRef<TocAst | undefined>(undefined)
|
|
||||||
useOnRefChange(toc, onTocChange)
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
new TableOfContentsMarkdownExtension((ast?: TocAst) => (toc.current = ast)),
|
...optionalMarkdownRendererExtensions,
|
||||||
...additionalExtensions,
|
...additionalExtensions,
|
||||||
new VegaLiteMarkdownExtension(),
|
new TableOfContentsMarkdownExtension(extensionEventEmitter),
|
||||||
new LinemarkerMarkdownExtension(
|
new LinemarkerMarkdownExtension(
|
||||||
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
|
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
|
||||||
),
|
),
|
||||||
new IframeCapsuleMarkdownExtension(),
|
new IframeCapsuleMarkdownExtension(),
|
||||||
new ImagePlaceholderMarkdownExtension(),
|
new ImagePlaceholderMarkdownExtension(),
|
||||||
new UploadIndicatingImageFrameMarkdownExtension(),
|
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 LinkAdjustmentMarkdownExtension(baseUrl),
|
||||||
new KatexMarkdownExtension(),
|
|
||||||
new TaskListMarkdownExtension(onTaskCheckedChange),
|
|
||||||
new PlantumlMarkdownExtension(),
|
|
||||||
new LegacyShortcodesMarkdownExtension(),
|
|
||||||
new EmojiMarkdownExtension(),
|
new EmojiMarkdownExtension(),
|
||||||
new GenericSyntaxMarkdownExtension(),
|
new GenericSyntaxMarkdownExtension(),
|
||||||
new AlertMarkdownExtension(),
|
|
||||||
new SpoilerMarkdownExtension(),
|
|
||||||
new LinkifyFixMarkdownExtension(),
|
new LinkifyFixMarkdownExtension(),
|
||||||
new HighlightedCodeMarkdownExtension(),
|
new DebuggerMarkdownExtension(),
|
||||||
new DebuggerMarkdownExtension()
|
new ProxyImageMarkdownExtension()
|
||||||
]
|
]
|
||||||
}, [additionalExtensions, baseUrl, currentLineMarkers, onImageClick, onTaskCheckedChange])
|
}, [additionalExtensions, baseUrl, currentLineMarkers, extensionEventEmitter])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}`
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}`
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -15,7 +15,7 @@ import type { Property } from 'csstype'
|
||||||
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
|
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
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')
|
const log = new Logger('OneClickEmbedding')
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,13 @@
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
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 { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
|
||||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||||
import { LoadingSlide } from './loading-slide'
|
import { LoadingSlide } from './loading-slide'
|
||||||
import { RevealMarkdownExtension } from './markdown-extension/reveal/reveal-markdown-extension'
|
|
||||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||||
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
||||||
|
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
|
||||||
|
|
||||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||||
slideOptions?: SlideOptions
|
slideOptions?: SlideOptions
|
||||||
|
@ -39,26 +37,19 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
||||||
className,
|
className,
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onTaskCheckedChange,
|
|
||||||
onTocChange,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
onImageClick,
|
|
||||||
newlinesAreBreaks,
|
newlinesAreBreaks,
|
||||||
slideOptions
|
slideOptions
|
||||||
}) => {
|
}) => {
|
||||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||||
const tocAst = useRef<TocAst>()
|
|
||||||
|
|
||||||
const extensions = useMarkdownExtensions(
|
const extensions = useMarkdownExtensions(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
undefined,
|
undefined,
|
||||||
useMemo(() => [new RevealMarkdownExtension()], []),
|
useMemo(() => [new RevealMarkdownExtension()], [])
|
||||||
onTaskCheckedChange,
|
|
||||||
onImageClick,
|
|
||||||
onTocChange
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks)
|
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
|
||||||
const revealStatus = useReveal(markdownContentLines, slideOptions)
|
const revealStatus = useReveal(markdownContentLines, slideOptions)
|
||||||
|
|
||||||
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
||||||
|
@ -68,8 +59,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
||||||
}
|
}
|
||||||
}, [extractFirstHeadline, markdownContentLines, revealStatus])
|
}, [extractFirstHeadline, markdownContentLines, revealStatus])
|
||||||
|
|
||||||
useOnRefChange(tocAst, onTocChange)
|
|
||||||
|
|
||||||
const slideShowDOM = useMemo(
|
const slideShowDOM = useMemo(
|
||||||
() => (revealStatus === REVEAL_STATUS.INITIALISED ? markdownReactDom : <LoadingSlide />),
|
() => (revealStatus === REVEAL_STATUS.INITIALISED ? markdownReactDom : <LoadingSlide />),
|
||||||
[markdownReactDom, revealStatus]
|
[markdownReactDom, revealStatus]
|
||||||
|
@ -83,5 +72,3 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlideshowMarkdownRenderer
|
|
||||||
|
|
|
@ -6,19 +6,19 @@
|
||||||
|
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useConvertMarkdownToReactDom } from '../hooks/use-convert-markdown-to-react-dom'
|
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 { StoreProvider } from '../../../redux/store-provider'
|
||||||
|
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||||
|
|
||||||
export interface SimpleMarkdownRendererProps {
|
export interface SimpleMarkdownRendererProps {
|
||||||
content: string
|
content: string
|
||||||
extensions: MarkdownExtension[]
|
extensions: MarkdownRendererExtension[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A markdown renderer for tests.
|
* A markdown renderer for tests.
|
||||||
*
|
*
|
||||||
* @param content The content to be rendered.
|
* @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 }) => {
|
export const TestMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ content, extensions }) => {
|
||||||
const lines = useMemo(() => content.split('\n'), [content])
|
const lines = useMemo(() => content.split('\n'), [content])
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
import equal from 'fast-deep-equal'
|
import equal from 'fast-deep-equal'
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import { useCallback, useEffect, useRef } 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 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 = (
|
const calculateLineMarkerPositions = (
|
||||||
documentElement: HTMLDivElement,
|
documentElement: HTMLDivElement,
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { LineWithId } from '../markdown-extension/linemarker/types'
|
|
||||||
import type { ArrayChange } from 'diff'
|
import type { ArrayChange } from 'diff'
|
||||||
import { diffArrays } from 'diff'
|
import { diffArrays } from 'diff'
|
||||||
|
import type { LineWithId } from '../extensions/linemarker/types'
|
||||||
|
|
||||||
type NewLine = string
|
type NewLine = string
|
||||||
type LineChange = ArrayChange<NewLine | LineWithId>
|
type LineChange = ArrayChange<NewLine | LineWithId>
|
||||||
|
|
|
@ -10,9 +10,9 @@ import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertN
|
||||||
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
|
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
|
||||||
import { DO_NOT_REPLACE } from '../replace-components/component-replacer'
|
import { DO_NOT_REPLACE } from '../replace-components/component-replacer'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { LineWithId } from '../markdown-extension/linemarker/types'
|
|
||||||
import { Optional } from '@mrdrogdrog/optional'
|
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]
|
type LineIndexPair = [startLineIndex: number, endLineIndex: number]
|
||||||
|
|
||||||
|
|
31
src/components/render-page/document-toc-sidebar.tsx
Normal file
31
src/components/render-page/document-toc-sidebar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { useCallback, useMemo, useState } 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 type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||||
import { useOnUserScroll } from './use-on-user-scroll'
|
import { useOnUserScroll } from './use-on-user-scroll'
|
||||||
import { useScrollToLineMark } from './use-scroll-to-line-mark'
|
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.
|
* Synchronizes the scroll status of the given container with the given scroll state and posts changes if the user scrolls.
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { useCallback } 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 { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||||
|
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a callback to handle user scrolling.
|
* Provides a callback to handle user scrolling.
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import { useCallback, useEffect, useRef } 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 type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||||
import { findLineMarks } from '../../../editor-page/synced-scroll/utils'
|
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}.
|
* Scrolls the given container to the correct {@link LineMarkerPosition}.
|
||||||
|
|
|
@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -4,19 +4,19 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
||||||
import { CommunicationMessageType, RendererType } 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 { 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 { MarkdownDocument } from './markdown-document'
|
||||||
import { countWords } from './word-counter'
|
import { countWords } from './word-counter'
|
||||||
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
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 { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
|
||||||
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
|
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
|
||||||
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
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.
|
* Wraps the markdown rendering in an iframe.
|
||||||
|
@ -74,17 +74,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
}, [communicator])
|
}, [communicator])
|
||||||
)
|
)
|
||||||
|
|
||||||
const onTaskCheckedChange = useCallback(
|
|
||||||
(lineInMarkdown: number, checked: boolean) => {
|
|
||||||
communicator.sendMessageToOtherSide({
|
|
||||||
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
|
|
||||||
checked,
|
|
||||||
lineInMarkdown
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[communicator]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onFirstHeadingChange = useCallback(
|
const onFirstHeadingChange = useCallback(
|
||||||
(firstHeading?: string) => {
|
(firstHeading?: string) => {
|
||||||
communicator.sendMessageToOtherSide({
|
communicator.sendMessageToOtherSide({
|
||||||
|
@ -115,8 +104,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
[communicator]
|
[communicator]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onImageClick: ImageClickHandler = useImageClickHandler(communicator)
|
|
||||||
|
|
||||||
const onHeightChange = useCallback(
|
const onHeightChange = useCallback(
|
||||||
(height: number) => {
|
(height: number) => {
|
||||||
communicator.sendMessageToOtherSide({
|
communicator.sendMessageToOtherSide({
|
||||||
|
@ -127,9 +114,12 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
[communicator]
|
[communicator]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderer = useMemo(() => {
|
||||||
if (!baseConfiguration) {
|
if (!baseConfiguration) {
|
||||||
return (
|
return (
|
||||||
<span>This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.</span>
|
<span>
|
||||||
|
This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,13 +129,12 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
<MarkdownDocument
|
<MarkdownDocument
|
||||||
additionalOuterContainerClasses={'vh-100 bg-light'}
|
additionalOuterContainerClasses={'vh-100 bg-light'}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onMakeScrollSource={onMakeScrollSource}
|
onMakeScrollSource={onMakeScrollSource}
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
baseUrl={baseConfiguration.baseUrl}
|
baseUrl={baseConfiguration.baseUrl}
|
||||||
onImageClick={onImageClick}
|
onHeightChange={onHeightChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case RendererType.SLIDESHOW:
|
case RendererType.SLIDESHOW:
|
||||||
|
@ -154,7 +143,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
baseUrl={baseConfiguration.baseUrl}
|
baseUrl={baseConfiguration.baseUrl}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onImageClick={onImageClick}
|
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
slideOptions={slideOptions}
|
slideOptions={slideOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -166,7 +154,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
|
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
baseUrl={baseConfiguration.baseUrl}
|
baseUrl={baseConfiguration.baseUrl}
|
||||||
onImageClick={onImageClick}
|
|
||||||
disableToc={true}
|
disableToc={true}
|
||||||
onHeightChange={onHeightChange}
|
onHeightChange={onHeightChange}
|
||||||
/>
|
/>
|
||||||
|
@ -174,4 +161,28 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
default:
|
default:
|
||||||
return null
|
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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,25 +4,20 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
|
||||||
import type { MutableRefObject } from 'react'
|
import type { MutableRefObject } from 'react'
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import useResizeObserver from '@react-hook/resize-observer'
|
import useResizeObserver from '@react-hook/resize-observer'
|
||||||
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
|
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
|
||||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
|
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 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 { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
|
import { DocumentTocSidebar } from './document-toc-sidebar'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
|
||||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||||
markdownContentLines: string[]
|
markdownContentLines: string[]
|
||||||
onImageClick?: ImageClickHandler
|
|
||||||
onHeightChange?: (height: number) => void
|
onHeightChange?: (height: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,10 +50,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
additionalRendererClasses,
|
additionalRendererClasses,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
onTaskCheckedChange,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
onImageClick,
|
|
||||||
onScroll,
|
onScroll,
|
||||||
scrollState,
|
scrollState,
|
||||||
onHeightChange,
|
onHeightChange,
|
||||||
|
@ -77,8 +70,6 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
setInternalDocumentRenderPaneSize(entry.contentRect)
|
setInternalDocumentRenderPaneSize(entry.contentRect)
|
||||||
)
|
)
|
||||||
|
|
||||||
const containerWidth = internalDocumentRenderPaneSize?.width ?? 0
|
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
|
||||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
||||||
|
|
||||||
const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines])
|
const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines])
|
||||||
|
@ -106,18 +97,15 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
|
||||||
onTocChange={setTocAst}
|
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
onImageClick={onImageClick}
|
|
||||||
newlinesAreBreaks={newlinesAreBreaks}
|
newlinesAreBreaks={newlinesAreBreaks}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles['markdown-document-side']} pt-4`}>
|
<DocumentTocSidebar
|
||||||
<ShowIf condition={!!tocAst && !disableToc}>
|
width={internalDocumentRenderPaneSize?.width ?? 0}
|
||||||
<WidthBasedTableOfContents tocAst={tocAst as TocAst} baseUrl={baseUrl} width={containerWidth} />
|
baseUrl={baseUrl}
|
||||||
</ShowIf>
|
disableToc={disableToc ?? false}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,18 +10,17 @@ export enum CommunicationMessageType {
|
||||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
RENDERER_READY = 'RENDERER_READY',
|
RENDERER_READY = 'RENDERER_READY',
|
||||||
SET_DARKMODE = 'SET_DARKMODE',
|
SET_DARKMODE = 'SET_DARKMODE',
|
||||||
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
|
|
||||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||||
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
||||||
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
||||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
|
||||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
||||||
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
||||||
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
|
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
|
||||||
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
|
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
|
||||||
IMAGE_UPLOAD = 'IMAGE_UPLOAD'
|
IMAGE_UPLOAD = 'IMAGE_UPLOAD',
|
||||||
|
EXTENSION_EVENT = 'EXTENSION_EVENT'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
|
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
|
||||||
|
@ -33,6 +32,12 @@ export interface SetDarkModeMessage {
|
||||||
activated: boolean
|
activated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExtensionEvent {
|
||||||
|
type: CommunicationMessageType.EXTENSION_EVENT
|
||||||
|
eventName: string
|
||||||
|
payload: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImageDetails {
|
export interface ImageDetails {
|
||||||
alt?: string
|
alt?: string
|
||||||
src: string
|
src: string
|
||||||
|
@ -56,11 +61,6 @@ export interface GetWordCountMessage {
|
||||||
type: CommunicationMessageType.GET_WORD_COUNT
|
type: CommunicationMessageType.GET_WORD_COUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageClickedMessage {
|
|
||||||
type: CommunicationMessageType.IMAGE_CLICKED
|
|
||||||
details: ImageDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetMarkdownContentMessage {
|
export interface SetMarkdownContentMessage {
|
||||||
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
|
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
|
||||||
content: string[]
|
content: string[]
|
||||||
|
@ -71,12 +71,6 @@ export interface SetScrollStateMessage {
|
||||||
scrollState: ScrollState
|
scrollState: ScrollState
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnTaskCheckboxChangeMessage {
|
|
||||||
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
|
|
||||||
lineInMarkdown: number
|
|
||||||
checked: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OnFirstHeadingChangeMessage {
|
export interface OnFirstHeadingChangeMessage {
|
||||||
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
||||||
firstHeading: string | undefined
|
firstHeading: string | undefined
|
||||||
|
@ -104,15 +98,14 @@ export type CommunicationMessages =
|
||||||
| SetDarkModeMessage
|
| SetDarkModeMessage
|
||||||
| SetBaseUrlMessage
|
| SetBaseUrlMessage
|
||||||
| GetWordCountMessage
|
| GetWordCountMessage
|
||||||
| ImageClickedMessage
|
|
||||||
| SetMarkdownContentMessage
|
| SetMarkdownContentMessage
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| OnTaskCheckboxChangeMessage
|
|
||||||
| OnFirstHeadingChangeMessage
|
| OnFirstHeadingChangeMessage
|
||||||
| SetSlideOptionsMessage
|
| SetSlideOptionsMessage
|
||||||
| OnHeightChangeMessage
|
| OnHeightChangeMessage
|
||||||
| OnWordCountCalculatedMessage
|
| OnWordCountCalculatedMessage
|
||||||
| ImageUploadMessage
|
| ImageUploadMessage
|
||||||
|
| ExtensionEvent
|
||||||
|
|
||||||
export type EditorToRendererMessageType =
|
export type EditorToRendererMessageType =
|
||||||
| CommunicationMessageType.SET_MARKDOWN_CONTENT
|
| CommunicationMessageType.SET_MARKDOWN_CONTENT
|
||||||
|
@ -127,12 +120,11 @@ export type RendererToEditorMessageType =
|
||||||
| CommunicationMessageType.RENDERER_READY
|
| CommunicationMessageType.RENDERER_READY
|
||||||
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||||
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
||||||
| CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
|
|
||||||
| CommunicationMessageType.SET_SCROLL_STATE
|
| CommunicationMessageType.SET_SCROLL_STATE
|
||||||
| CommunicationMessageType.IMAGE_CLICKED
|
|
||||||
| CommunicationMessageType.ON_HEIGHT_CHANGE
|
| CommunicationMessageType.ON_HEIGHT_CHANGE
|
||||||
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
||||||
| CommunicationMessageType.IMAGE_UPLOAD
|
| CommunicationMessageType.IMAGE_UPLOAD
|
||||||
|
| CommunicationMessageType.EXTENSION_EVENT
|
||||||
|
|
||||||
export enum RendererType {
|
export enum RendererType {
|
||||||
DOCUMENT = 'document',
|
DOCUMENT = 'document',
|
||||||
|
|
26
src/extensions/base/app-extension.ts
Normal file
26
src/extensions/base/app-extension.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue