mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-15 17:36:44 +00:00
Don't send frontmatter to renderer (#2259)
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
c50037fc9f
commit
f68b3ff056
24 changed files with 215 additions and 176 deletions
|
@ -7,7 +7,6 @@
|
|||
import React, { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||
import { DocumentInfobar } from './document-infobar'
|
||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
||||
|
@ -20,7 +19,6 @@ export const DocumentReadOnlyPageContent: React.FC = () => {
|
|||
useTranslation()
|
||||
|
||||
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
|
||||
// TODO Change todo values with real ones as soon as the backend is ready.
|
||||
return (
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
import React from 'react'
|
||||
import type { RenderIframeProps } from '../renderer-pane/render-iframe'
|
||||
import { RenderIframe } from '../renderer-pane/render-iframe'
|
||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
||||
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'
|
||||
|
||||
export type EditorDocumentRendererProps = Omit<
|
||||
RenderIframeProps,
|
||||
|
@ -22,17 +23,22 @@ export type EditorDocumentRendererProps = Omit<
|
|||
/**
|
||||
* Renders the markdown content from the global application state with the iframe renderer.
|
||||
*
|
||||
* @param props Every property from the {@link RenderIframe} except the markdown content.
|
||||
* @param scrollState The {@link ScrollState} that should be sent to the renderer
|
||||
* @param onScroll A callback that is executed when the view in the rendered is scrolled
|
||||
* @param props Every property from the {@link RenderIframe} except the markdown content
|
||||
*/
|
||||
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (props) => {
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
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)
|
||||
|
||||
return (
|
||||
<RenderIframe
|
||||
{...props}
|
||||
onScroll={adjustedOnScroll}
|
||||
scrollState={adjustedScrollState}
|
||||
onTaskCheckedChange={setCheckboxInEditor}
|
||||
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
|
||||
markdownContentLines={trimmedContentLines}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import { getGlobalState } from '../../../../redux'
|
||||
import { useMemo } from 'react'
|
||||
import type { ScrollCallback } from '../../synced-scroll/scroll-props'
|
||||
|
||||
/**
|
||||
* Adjusts the given onScroll callback to include the frontmatter line offset.
|
||||
*
|
||||
* @param onScroll The callback that posts a scroll state
|
||||
* @return the modified callback that posts the same scroll state but with line offset
|
||||
*/
|
||||
export const useOnScrollWithLineOffset = (onScroll: ScrollCallback | undefined): ScrollCallback | undefined => {
|
||||
return useMemo(() => {
|
||||
if (onScroll === undefined) {
|
||||
return undefined
|
||||
} else {
|
||||
return (scrollState: ScrollState) => {
|
||||
onScroll({
|
||||
firstLineInView:
|
||||
scrollState.firstLineInView + getGlobalState().noteDetails.frontmatterRendererInfo.lineOffset,
|
||||
scrolledPercentage: scrollState.scrolledPercentage
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [onScroll])
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Adjusts the given {@link ScrollState scroll state} to exclude the frontmatter line offset.
|
||||
*
|
||||
* @param scrollState The original scroll state with the line offset
|
||||
* @return the adjusted scroll state without the line offset
|
||||
*/
|
||||
export const useScrollStateWithoutLineOffset = (scrollState: ScrollState | undefined): ScrollState | undefined => {
|
||||
const lineOffset = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo.lineOffset)
|
||||
return useMemo(() => {
|
||||
return scrollState === undefined
|
||||
? undefined
|
||||
: {
|
||||
firstLineInView: scrollState.firstLineInView - lineOffset,
|
||||
scrolledPercentage: scrollState.scrolledPercentage
|
||||
}
|
||||
}, [lineOffset, scrollState])
|
||||
}
|
|
@ -39,7 +39,10 @@ export const useOnImageUploadFromRenderer = (): void => {
|
|||
.then((blob) => {
|
||||
const file = new File([blob], fileName, { type: blob.type })
|
||||
const { cursorSelection, alt, title } = Optional.ofNullable(lineIndex)
|
||||
.flatMap((actualLineIndex) => findPlaceholderInMarkdownContent(actualLineIndex, placeholderIndexInLine))
|
||||
.flatMap((actualLineIndex) => {
|
||||
const lineOffset = getGlobalState().noteDetails.frontmatterRendererInfo.lineOffset
|
||||
return findPlaceholderInMarkdownContent(actualLineIndex + lineOffset, placeholderIndexInLine)
|
||||
})
|
||||
.orElse({} as ExtractResult)
|
||||
handleUpload(file, cursorSelection, alt, title)
|
||||
})
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import equal from 'fast-deep-equal'
|
||||
import type { RendererFrontmatterInfo } from '../../../../redux/note-details/types/note-details'
|
||||
|
||||
/**
|
||||
* Extracts the {@link RendererFrontmatterInfo frontmatter data}
|
||||
* from the global application state and sends it to the renderer.
|
||||
*/
|
||||
export const useSendFrontmatterInfoFromReduxToRenderer = (): void => {
|
||||
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
|
||||
const lastFrontmatter = useRef<RendererFrontmatterInfo | undefined>(undefined)
|
||||
|
||||
const cachedFrontmatterInfo = useMemo(() => {
|
||||
if (lastFrontmatter.current !== undefined && equal(lastFrontmatter.current, frontmatterInfo)) {
|
||||
return lastFrontmatter.current
|
||||
} else {
|
||||
lastFrontmatter.current = frontmatterInfo
|
||||
return frontmatterInfo
|
||||
}
|
||||
}, [frontmatterInfo])
|
||||
|
||||
return useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_FRONTMATTER_INFO,
|
||||
frontmatterInfo: cachedFrontmatterInfo
|
||||
}),
|
||||
[cachedFrontmatterInfo]
|
||||
)
|
||||
)
|
||||
}
|
|
@ -27,6 +27,7 @@ import { Logger } from '../../../utils/logger'
|
|||
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||
import { ORIGIN_TYPE, useOriginFromConfig } from '../render-context/use-origin-from-config'
|
||||
import { getGlobalState } from '../../../redux'
|
||||
|
||||
export interface RenderIframeProps extends RendererProps {
|
||||
rendererType: RendererType
|
||||
|
@ -89,11 +90,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
)
|
||||
)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_STATE,
|
||||
useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll])
|
||||
)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
||||
|
@ -102,7 +98,10 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
|
||||
useCallback(
|
||||
(values: OnTaskCheckboxChangeMessage) => onTaskCheckedChange?.(values.lineInMarkdown, values.checked),
|
||||
(values: OnTaskCheckboxChangeMessage) => {
|
||||
const lineOffset = getGlobalState().noteDetails.frontmatterRendererInfo.lineOffset
|
||||
onTaskCheckedChange?.(values.lineInMarkdown + lineOffset, values.checked)
|
||||
},
|
||||
[onTaskCheckedChange]
|
||||
)
|
||||
)
|
||||
|
@ -140,10 +139,16 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
)
|
||||
|
||||
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
||||
useSendScrollState(scrollState)
|
||||
useSendDarkModeStatusToRenderer(forcedDarkMode)
|
||||
useSendMarkdownToRenderer(markdownContentLines)
|
||||
|
||||
useSendScrollState(scrollState)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_STATE,
|
||||
useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll])
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CommunicatorImageLightbox />
|
||||
|
|
|
@ -1,12 +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
|
||||
*/
|
||||
|
||||
export type ScrollCallback = (scrollState: ScrollState) => void
|
||||
|
||||
export interface ScrollProps {
|
||||
scrollState?: ScrollState
|
||||
onScroll?: (scrollState: ScrollState) => void
|
||||
onScroll?: ScrollCallback
|
||||
onMakeScrollSource?: () => void
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererPro
|
|||
* @param onImageClick The callback to call if a image is clicked
|
||||
* @param outerContainerRef A reference for the outer container
|
||||
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
||||
* @param lineOffset The line offset
|
||||
*/
|
||||
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
|
||||
className,
|
||||
|
@ -45,8 +44,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
baseUrl,
|
||||
onImageClick,
|
||||
outerContainerRef,
|
||||
newlinesAreBreaks,
|
||||
lineOffset
|
||||
newlinesAreBreaks
|
||||
}) => {
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
|
@ -55,7 +53,6 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
baseUrl,
|
||||
currentLineMarkers,
|
||||
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], []),
|
||||
lineOffset ?? 0,
|
||||
onTaskCheckedChange,
|
||||
onImageClick,
|
||||
onTocChange
|
||||
|
|
|
@ -34,7 +34,6 @@ import { SpoilerMarkdownExtension } from '../markdown-extension/spoiler-markdown
|
|||
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 { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
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'
|
||||
|
@ -50,7 +49,6 @@ import { useOnRefChange } from './use-on-ref-change'
|
|||
* @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 lineOffset The line offset for the {@link LinemarkerMarkdownExtension} and {@link TaskListMarkdownExtension}
|
||||
* @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}
|
||||
|
@ -60,12 +58,10 @@ export const useMarkdownExtensions = (
|
|||
baseUrl: string,
|
||||
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
|
||||
additionalExtensions: MarkdownExtension[],
|
||||
lineOffset: number,
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
||||
onImageClick?: ImageClickHandler,
|
||||
onTocChange?: (ast?: TocAst) => void
|
||||
): MarkdownExtension[] => {
|
||||
const plantumlServer = useApplicationState((state) => state.config.plantumlServer)
|
||||
const toc = useRef<TocAst | undefined>(undefined)
|
||||
useOnRefChange(toc, onTocChange)
|
||||
|
||||
|
@ -76,11 +72,10 @@ export const useMarkdownExtensions = (
|
|||
new VegaLiteMarkdownExtension(),
|
||||
// new MarkmapMarkdownExtension(),
|
||||
new LinemarkerMarkdownExtension(
|
||||
lineOffset,
|
||||
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
|
||||
),
|
||||
new IframeCapsuleMarkdownExtension(),
|
||||
new ImagePlaceholderMarkdownExtension(lineOffset),
|
||||
new ImagePlaceholderMarkdownExtension(),
|
||||
new UploadIndicatingImageFrameMarkdownExtension(),
|
||||
new GistMarkdownExtension(),
|
||||
new YoutubeMarkdownExtension(),
|
||||
|
@ -95,8 +90,8 @@ export const useMarkdownExtensions = (
|
|||
new BlockquoteExtraTagMarkdownExtension(),
|
||||
new LinkAdjustmentMarkdownExtension(baseUrl),
|
||||
new KatexMarkdownExtension(),
|
||||
new TaskListMarkdownExtension(lineOffset, onTaskCheckedChange),
|
||||
new PlantumlMarkdownExtension(plantumlServer),
|
||||
new TaskListMarkdownExtension(onTaskCheckedChange),
|
||||
new PlantumlMarkdownExtension(),
|
||||
new LegacyShortcodesMarkdownExtension(),
|
||||
new EmojiMarkdownExtension(),
|
||||
new GenericSyntaxMarkdownExtension(),
|
||||
|
@ -106,5 +101,5 @@ export const useMarkdownExtensions = (
|
|||
new HighlightedCodeMarkdownExtension(),
|
||||
new DebuggerMarkdownExtension()
|
||||
]
|
||||
}, [additionalExtensions, baseUrl, currentLineMarkers, lineOffset, onImageClick, onTaskCheckedChange, plantumlServer])
|
||||
}, [additionalExtensions, baseUrl, currentLineMarkers, onImageClick, onTaskCheckedChange])
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { ImagePlaceholderReplacer } from './image-placeholder-replacer'
|
|||
export class ImagePlaceholderMarkdownExtension extends MarkdownExtension {
|
||||
public static readonly PLACEHOLDER_URL = 'https://'
|
||||
|
||||
constructor(private lineOffset: number) {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,6 @@ export class ImagePlaceholderMarkdownExtension extends MarkdownExtension {
|
|||
}
|
||||
|
||||
buildReplacers(): ComponentReplacer[] {
|
||||
return [new ImagePlaceholderReplacer(this.lineOffset)]
|
||||
return [new ImagePlaceholderReplacer()]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-
|
|||
export class ImagePlaceholderReplacer extends ComponentReplacer {
|
||||
private countPerSourceLine = new Map<number, number>()
|
||||
|
||||
constructor(private lineOffset: number) {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ export class ImagePlaceholderReplacer extends ComponentReplacer {
|
|||
title={node.attribs.title}
|
||||
width={node.attribs.width}
|
||||
height={node.attribs.height}
|
||||
lineIndex={isNaN(lineIndex) ? undefined : lineIndex + this.lineOffset}
|
||||
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
|
||||
*/
|
||||
|
@ -13,19 +13,53 @@ export interface LineMarkers {
|
|||
endLine: number
|
||||
}
|
||||
|
||||
const insertNewLineMarker = (
|
||||
startLineNumber: number,
|
||||
endLineNumber: number,
|
||||
tokenPosition: number,
|
||||
level: number,
|
||||
tokens: Token[]
|
||||
) => {
|
||||
const startToken = new Token('app_linemarker', LinemarkerMarkdownExtension.tagName, 0)
|
||||
startToken.level = level
|
||||
startToken.attrPush(['data-start-line', `${startLineNumber}`])
|
||||
startToken.attrPush(['data-end-line', `${endLineNumber}`])
|
||||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden || !token.map) {
|
||||
continue
|
||||
}
|
||||
|
||||
const startLineNumber = token.map[0] + 1
|
||||
const endLineNumber = token.map[1] + 1
|
||||
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
}
|
||||
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
||||
* It also provides a list of line numbers for the top level dom elements.
|
||||
*/
|
||||
export const addLineMarkerMarkdownItPlugin: (
|
||||
markdownIt: MarkdownIt,
|
||||
lineOffset: number,
|
||||
onLineMarkerChange?: (lineMarkers: LineMarkers[]) => void
|
||||
) => void = (md: MarkdownIt, lineOffset, onLineMarkerChange) => {
|
||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||
) => void = (md, onLineMarkerChange) => {
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
tagTokens(state.tokens, lineMarkers, lineOffset)
|
||||
tagTokens(state.tokens, lineMarkers)
|
||||
if (onLineMarkerChange) {
|
||||
onLineMarkerChange(lineMarkers)
|
||||
}
|
||||
|
@ -36,52 +70,8 @@ export const addLineMarkerMarkdownItPlugin: (
|
|||
const startLineNumber = tokens[index].attrGet('data-start-line')
|
||||
const endLineNumber = tokens[index].attrGet('data-end-line')
|
||||
|
||||
if (!startLineNumber || !endLineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<${LinemarkerMarkdownExtension.tagName} data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></${LinemarkerMarkdownExtension.tagName}>`
|
||||
}
|
||||
|
||||
const insertNewLineMarker = (
|
||||
startLineNumber: number,
|
||||
endLineNumber: number,
|
||||
tokenPosition: number,
|
||||
level: number,
|
||||
tokens: Token[]
|
||||
) => {
|
||||
const startToken = new Token('app_linemarker', LinemarkerMarkdownExtension.tagName, 0)
|
||||
startToken.level = level
|
||||
startToken.attrPush(['data-start-line', `${startLineNumber}`])
|
||||
startToken.attrPush(['data-end-line', `${endLineNumber}`])
|
||||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], lineOffset: number) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
|
||||
const startLineNumber = token.map[0] + 1
|
||||
const endLineNumber = token.map[1] + 1
|
||||
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber + lineOffset, endLine: endLineNumber + lineOffset })
|
||||
}
|
||||
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers, lineOffset)
|
||||
}
|
||||
}
|
||||
return startLineNumber && endLineNumber
|
||||
? `<${LinemarkerMarkdownExtension.tagName} data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></${LinemarkerMarkdownExtension.tagName}>`
|
||||
: ''
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,12 @@ import type MarkdownIt from 'markdown-it'
|
|||
export class LinemarkerMarkdownExtension extends MarkdownExtension {
|
||||
public static readonly tagName = 'app-linemarker'
|
||||
|
||||
constructor(private lineOffset: number, private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {
|
||||
constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {
|
||||
super()
|
||||
}
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
addLineMarkerMarkdownItPlugin(markdownIt, this.lineOffset ?? 0, this.onLineMarkers)
|
||||
addLineMarkerMarkdownItPlugin(markdownIt, this.onLineMarkers)
|
||||
}
|
||||
|
||||
public buildReplacers(): ComponentReplacer[] {
|
||||
|
|
|
@ -4,7 +4,7 @@ exports[`PlantUML markdown extensions renders a plantuml codeblock 1`] = `
|
|||
<div>
|
||||
<img
|
||||
alt="uml diagram"
|
||||
src="http://example.org/svg/SoWkIImgAStDuKhEIImkLd2jICmjo4dbSaZDIm6A0W00"
|
||||
src="https://example.org/svg/SoWkIImgAStDuKhEIImkLd2jICmjo4dbSaZDIm6A0W00"
|
||||
/>
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@ import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
|||
import React from 'react'
|
||||
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import * as reduxModule from '../../../../redux'
|
||||
import { Mock } from 'ts-mockery'
|
||||
import type { ApplicationState } from '../../../../redux/application-state'
|
||||
|
||||
describe('PlantUML markdown extensions', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -16,9 +19,17 @@ describe('PlantUML markdown extensions', () => {
|
|||
})
|
||||
|
||||
it('renders a plantuml codeblock', () => {
|
||||
jest.spyOn(reduxModule, 'getGlobalState').mockReturnValue(
|
||||
Mock.of<ApplicationState>({
|
||||
config: {
|
||||
plantumlServer: 'https://example.org'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new PlantumlMarkdownExtension('http://example.org')]}
|
||||
extensions={[new PlantumlMarkdownExtension()]}
|
||||
content={'```plantuml\nclass Example\n```'}
|
||||
/>
|
||||
)
|
||||
|
@ -26,9 +37,17 @@ describe('PlantUML markdown extensions', () => {
|
|||
})
|
||||
|
||||
it('renders an error if no server is defined', () => {
|
||||
jest.spyOn(reduxModule, 'getGlobalState').mockReturnValue(
|
||||
Mock.of<ApplicationState>({
|
||||
config: {
|
||||
plantumlServer: undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new PlantumlMarkdownExtension(null)]}
|
||||
extensions={[new PlantumlMarkdownExtension()]}
|
||||
content={'```plantuml\nclass Example\n```'}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -12,6 +12,8 @@ import type Token from 'markdown-it/lib/token'
|
|||
import type { Options } from 'markdown-it/lib'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { PlantumlNotConfiguredComponentReplacer } from './plantuml-not-configured-component-replacer'
|
||||
import { getGlobalState } from '../../../../redux'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
|
||||
/**
|
||||
* Adds support for chart rendering using plantuml to the markdown rendering using code fences with "plantuml" as language.
|
||||
|
@ -19,7 +21,7 @@ import { PlantumlNotConfiguredComponentReplacer } from './plantuml-not-configure
|
|||
* @see https://plantuml.com
|
||||
*/
|
||||
export class PlantumlMarkdownExtension extends MarkdownExtension {
|
||||
constructor(private plantumlServer?: string) {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -33,15 +35,15 @@ export class PlantumlMarkdownExtension extends MarkdownExtension {
|
|||
}
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
if (this.plantumlServer) {
|
||||
plantuml(markdownIt, {
|
||||
openMarker: '```plantuml',
|
||||
closeMarker: '```',
|
||||
server: this.plantumlServer
|
||||
})
|
||||
} else {
|
||||
this.plantumlError(markdownIt)
|
||||
}
|
||||
Optional.ofNullable(getGlobalState().config.plantumlServer)
|
||||
.map((plantumlServer) =>
|
||||
plantuml(markdownIt, {
|
||||
openMarker: '```plantuml',
|
||||
closeMarker: '```',
|
||||
server: plantumlServer
|
||||
})
|
||||
)
|
||||
.orElseGet(() => this.plantumlError(markdownIt))
|
||||
}
|
||||
|
||||
public buildTagNameWhitelist(): string[] {
|
||||
|
|
|
@ -15,7 +15,7 @@ import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
|
|||
* Adds support for interactive checkbox lists to the markdown rendering using the github checklist syntax.
|
||||
*/
|
||||
export class TaskListMarkdownExtension extends MarkdownExtension {
|
||||
constructor(private frontmatterLinesToSkip: number, private onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
constructor(private onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,6 @@ export class TaskListMarkdownExtension extends MarkdownExtension {
|
|||
}
|
||||
|
||||
public buildReplacers(): ComponentReplacer[] {
|
||||
return [new TaskListReplacer(this.frontmatterLinesToSkip, this.onTaskCheckedChange)]
|
||||
return [new TaskListReplacer(this.onTaskCheckedChange)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
|
|||
export class TaskListReplacer extends ComponentReplacer {
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
constructor(frontmatterLinesToSkip: number, onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
this.onTaskCheckedChange = (lineInMarkdown, checked) => {
|
||||
if (onTaskCheckedChange === undefined) {
|
||||
return
|
||||
}
|
||||
onTaskCheckedChange(frontmatterLinesToSkip + lineInMarkdown, checked)
|
||||
onTaskCheckedChange(lineInMarkdown, checked)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
|||
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
||||
|
||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
slideOptions: SlideOptions
|
||||
slideOptions?: SlideOptions
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,6 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
|
|||
* @param baseUrl The base url of the renderer
|
||||
* @param onImageClick The callback to call if a image is clicked
|
||||
* @param newlinesAreBreaks If newlines are rendered as breaks or not
|
||||
* @param lineOffset The line offset
|
||||
* @param slideOptions The {@link SlideOptions} to use
|
||||
*/
|
||||
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
|
||||
|
@ -45,7 +44,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
baseUrl,
|
||||
onImageClick,
|
||||
newlinesAreBreaks,
|
||||
lineOffset,
|
||||
slideOptions
|
||||
}) => {
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
|
@ -55,7 +53,6 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
baseUrl,
|
||||
undefined,
|
||||
useMemo(() => [new RevealMarkdownExtension()], []),
|
||||
lineOffset ?? 0,
|
||||
onTaskCheckedChange,
|
||||
onImageClick,
|
||||
onTocChange
|
||||
|
|
|
@ -16,8 +16,7 @@ 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 { initialState } from '../../redux/note-details/initial-state'
|
||||
import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details'
|
||||
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
||||
|
||||
/**
|
||||
* Wraps the markdown rendering in an iframe.
|
||||
|
@ -26,12 +25,17 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
const [markdownContentLines, setMarkdownContentLines] = useState<string[]>([])
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>(initialState.frontmatterRendererInfo)
|
||||
const [slideOptions, setSlideOptions] = useState<SlideOptions>()
|
||||
|
||||
const communicator = useRendererToEditorCommunicator()
|
||||
|
||||
const sendScrolling = useRef<boolean>(false)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
useCallback((values) => setSlideOptions(values.slideOptions), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => {
|
||||
|
@ -59,11 +63,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
useCallback((values) => setScrollState(values.scrollState), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_FRONTMATTER_INFO,
|
||||
useCallback((values) => setFrontmatterInfo(values.frontmatterInfo), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.GET_WORD_COUNT,
|
||||
useCallback(() => {
|
||||
|
@ -145,7 +144,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
onScroll={onScroll}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onImageClick={onImageClick}
|
||||
frontmatterInfo={frontmatterInfo}
|
||||
/>
|
||||
)
|
||||
case RendererType.SLIDESHOW:
|
||||
|
@ -156,8 +154,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onImageClick={onImageClick}
|
||||
scrollState={scrollState}
|
||||
lineOffset={frontmatterInfo.lineOffset}
|
||||
slideOptions={frontmatterInfo.slideOptions}
|
||||
slideOptions={slideOptions}
|
||||
/>
|
||||
)
|
||||
case RendererType.INTRO:
|
||||
|
|
|
@ -16,7 +16,6 @@ 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 type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details'
|
||||
|
||||
export interface RendererProps extends ScrollProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
|
@ -32,7 +31,6 @@ export interface MarkdownDocumentProps extends RendererProps {
|
|||
additionalRendererClasses?: string
|
||||
disableToc?: boolean
|
||||
baseUrl: string
|
||||
frontmatterInfo?: RendererFrontmatterInfo
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,7 +48,6 @@ export interface MarkdownDocumentProps extends RendererProps {
|
|||
* @param scrollState The current {@link ScrollState}
|
||||
* @param onHeightChange The callback to call if the height of the document changes
|
||||
* @param disableToc If the table of contents should be disabled.
|
||||
* @param frontmatterInfo The frontmatter information for the renderer.
|
||||
* @see https://markdown-it.github.io/
|
||||
*/
|
||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||
|
@ -65,8 +62,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
onScroll,
|
||||
scrollState,
|
||||
onHeightChange,
|
||||
disableToc,
|
||||
frontmatterInfo
|
||||
disableToc
|
||||
}) => {
|
||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||
const [rendererSize, setRendererSize] = useState<DOMRectReadOnly>()
|
||||
|
@ -115,7 +111,6 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
baseUrl={baseUrl}
|
||||
onImageClick={onImageClick}
|
||||
newlinesAreBreaks={newlinesAreBreaks}
|
||||
lineOffset={frontmatterInfo?.lineOffset}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles['markdown-document-side']} pt-4`}>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
||||
import type { RendererFrontmatterInfo } from '../../../redux/note-details/types/note-details'
|
||||
import type { SlideOptions } from '../../../redux/note-details/types/slide-show-options'
|
||||
|
||||
export enum CommunicationMessageType {
|
||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||
|
@ -20,7 +20,7 @@ export enum CommunicationMessageType {
|
|||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
||||
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
||||
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
|
||||
SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO',
|
||||
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
|
||||
IMAGE_UPLOAD = 'IMAGE_UPLOAD'
|
||||
}
|
||||
|
||||
|
@ -82,9 +82,9 @@ export interface OnFirstHeadingChangeMessage {
|
|||
firstHeading: string | undefined
|
||||
}
|
||||
|
||||
export interface SetFrontmatterInfoMessage {
|
||||
type: CommunicationMessageType.SET_FRONTMATTER_INFO
|
||||
frontmatterInfo: RendererFrontmatterInfo
|
||||
export interface SetSlideOptionsMessage {
|
||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||
slideOptions: SlideOptions
|
||||
}
|
||||
|
||||
export interface OnHeightChangeMessage {
|
||||
|
@ -109,7 +109,7 @@ export type CommunicationMessages =
|
|||
| SetScrollStateMessage
|
||||
| OnTaskCheckboxChangeMessage
|
||||
| OnFirstHeadingChangeMessage
|
||||
| SetFrontmatterInfoMessage
|
||||
| SetSlideOptionsMessage
|
||||
| OnHeightChangeMessage
|
||||
| OnWordCountCalculatedMessage
|
||||
| ImageUploadMessage
|
||||
|
@ -120,7 +120,7 @@ export type EditorToRendererMessageType =
|
|||
| CommunicationMessageType.SET_SCROLL_STATE
|
||||
| CommunicationMessageType.SET_BASE_CONFIGURATION
|
||||
| CommunicationMessageType.GET_WORD_COUNT
|
||||
| CommunicationMessageType.SET_FRONTMATTER_INFO
|
||||
| CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||
| CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE
|
||||
|
||||
export type RendererToEditorMessageType =
|
||||
|
|
|
@ -4,13 +4,17 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
CommunicationMessageType,
|
||||
RendererType
|
||||
} from '../render-page/window-post-message-communicator/rendering-message'
|
||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||
import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-trimmed-note-markdown-content-without-frontmatter'
|
||||
import { useSendToRenderer } from '../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Renders the current markdown content as a slideshow.
|
||||
|
@ -18,7 +22,17 @@ import { useTrimmedNoteMarkdownContentWithoutFrontmatter } from '../../hooks/com
|
|||
export const SlideShowPageContent: React.FC = () => {
|
||||
const markdownContentLines = useTrimmedNoteMarkdownContentWithoutFrontmatter()
|
||||
useTranslation()
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
|
||||
const slideOptions = useApplicationState((state) => state.noteDetails.frontmatter.slideOptions)
|
||||
useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
slideOptions
|
||||
}),
|
||||
[slideOptions]
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={'vh-100 vw-100'}>
|
||||
|
|
Loading…
Add table
Reference in a new issue