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