Wrap markdown rendering in iframe (#837)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-01-24 20:50:51 +01:00 committed by GitHub
parent bd31076928
commit 586969f368
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1014 additions and 287 deletions

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export abstract class IframeCommunicator<SEND, RECEIVE> {
protected otherSide?: Window
protected otherOrigin?: string
constructor () {
window.addEventListener("message", this.handleEvent.bind(this))
}
public unregisterEventListener (): void {
window.removeEventListener("message", this.handleEvent.bind(this))
}
public setOtherSide (otherSide: Window, otherOrigin: string): void {
this.otherSide = otherSide
this.otherOrigin = otherOrigin
}
public unsetOtherSide (): void {
this.otherSide = undefined
this.otherOrigin = undefined
}
public getOtherSide (): Window | undefined {
return this.otherSide
}
protected sendMessageToOtherSide (message: SEND): void {
if (this.otherSide === undefined || this.otherOrigin === undefined) {
console.error("Can't send message because otherSide is null", message)
return
}
this.otherSide.postMessage(message, this.otherOrigin)
}
protected abstract handleEvent (event: MessageEvent<RECEIVE>): void;
}

View file

@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator"
import {
EditorToRendererIframeMessage,
ImageDetails,
RendererToEditorIframeMessage,
RenderIframeMessageType
} from "./rendering-message"
export class IframeEditorToRendererCommunicator extends IframeCommunicator<EditorToRendererIframeMessage, RendererToEditorIframeMessage> {
private onSetScrollSourceToRendererHandler?: () => void
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
private onFirstHeadingChangeHandler?: (heading?: string) => void
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onRendererReadyHandler?: () => void
private onImageClickedHandler?: (details: ImageDetails) => void
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_META_DATA:
this.onMetaDataChangeHandler?.(renderMessage.metaData)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
}
}
public onImageClicked (handler?: (details: ImageDetails) => void): void {
this.onImageClickedHandler = handler
}
public onRendererReady (handler?: () => void): void {
this.onRendererReadyHandler = handler
}
public onSetScrollSourceToRenderer (handler?: () => void): void {
this.onSetScrollSourceToRendererHandler = handler
}
public onTaskCheckboxChange (handler?: (lineInMarkdown: number, checked: boolean) => void): void {
this.onTaskCheckboxChangeHandler = handler
}
public onFirstHeadingChange (handler?: (heading?: string) => void): void {
this.onFirstHeadingChangeHandler = handler
}
public onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void {
this.onMetaDataChangeHandler = handler
}
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
this.onSetScrollStateHandler = handler
}
public sendSetBaseUrl (baseUrl: string): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_BASE_URL,
baseUrl
})
}
public sendSetMarkdownContent (markdownContent: string): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
content: markdownContent
})
}
public sendSetDarkmode (darkModeActivated: boolean): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_DARKMODE,
activated: darkModeActivated
})
}
public sendSetWide (isWide: boolean): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_WIDE,
activated: isWide
})
}
public sendScrollState (scrollState?: ScrollState): void {
if (!scrollState) {
return
}
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_STATE,
scrollState
})
}
}

View file

@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator"
import {
EditorToRendererIframeMessage,
ImageDetails,
RendererToEditorIframeMessage,
RenderIframeMessageType
} from "./rendering-message"
export class IframeRendererToEditorCommunicator extends IframeCommunicator<RendererToEditorIframeMessage, EditorToRendererIframeMessage> {
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
private onSetWideHandler?: ((wide: boolean) => void)
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
private onSetBaseUrlHandler?: ((baseUrl: string) => void)
public onSetBaseUrl (handler?: (baseUrl: string) => void): void {
this.onSetBaseUrlHandler = handler
}
public onSetMarkdownContent (handler?: (markdownContent: string) => void): void {
this.onSetMarkdownContentHandler = handler
}
public onSetDarkMode (handler?: (darkModeActivated: boolean) => void): void {
this.onSetDarkModeHandler = handler
}
public onSetWide (handler?: (wide: boolean) => void): void {
this.onSetWideHandler = handler
}
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
this.onSetScrollStateHandler = handler
}
public sendRendererReady (): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.RENDERER_READY
})
}
public sendTaskCheckBoxChange (lineInMarkdown: number, checked: boolean): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
checked,
lineInMarkdown
})
}
public sendFirstHeadingChanged (firstHeading: string | undefined): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
firstHeading
})
}
public sendSetScrollSourceToRenderer (): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
})
}
public sendSetMetaData (metaData: YAMLMetaData | undefined): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_SET_META_DATA,
metaData
})
}
public sendSetScrollState (scrollState: ScrollState): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_STATE,
scrollState
})
}
protected handleEvent (event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.SET_MARKDOWN_CONTENT:
this.onSetMarkdownContentHandler?.(renderMessage.content)
return false
case RenderIframeMessageType.SET_DARKMODE:
this.onSetDarkModeHandler?.(renderMessage.activated)
return false
case RenderIframeMessageType.SET_WIDE:
this.onSetWideHandler?.(renderMessage.activated)
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.SET_BASE_URL:
this.onSetBaseUrlHandler?.(renderMessage.baseUrl)
return false
}
}
public sendClickedImageUrl (details: ImageDetails): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.IMAGE_CLICKED,
details: details
})
}
}

View file

@ -0,0 +1,105 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from "fast-deep-equal"
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { ApplicationState } from '../../redux'
import { setDarkMode } from '../../redux/dark-mode/methods'
import { setDocumentMetadata } from '../../redux/document-content/methods'
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane'
import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
export const RenderPage: React.FC = () => {
useApplyDarkMode()
const [markdownContent, setMarkdownContent] = useState('')
const [isWide, setWide] = useState(false)
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
const [baseUrl, setBaseUrl] = useState<string>()
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
const iframeCommunicator = useMemo(() => {
const newCommunicator = new IframeRendererToEditorCommunicator()
newCommunicator.setOtherSide(window.parent, editorOrigin)
return newCommunicator
}, [editorOrigin])
useEffect(() => {
iframeCommunicator.sendRendererReady()
return () => iframeCommunicator.unregisterEventListener()
}, [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetBaseUrl(setBaseUrl), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => {
if (!equal(scrollState, newScrollState)) {
setScrollState(newScrollState)
}
}), [iframeCommunicator, scrollState])
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
}, [iframeCommunicator])
const onFirstHeadingChange = useCallback((firstHeading?: string) => {
iframeCommunicator.sendFirstHeadingChanged(firstHeading)
}, [iframeCommunicator])
const onMakeScrollSource = useCallback(() => {
iframeCommunicator.sendSetScrollSourceToRenderer()
}, [iframeCommunicator])
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => {
setDocumentMetadata(metaData)
iframeCommunicator.sendSetMetaData(metaData)
}, [iframeCommunicator])
const onScroll = useCallback((scrollState: ScrollState) => {
iframeCommunicator.sendSetScrollState(scrollState)
}, [iframeCommunicator])
const onImageClick: ImageClickHandler = useCallback((event) => {
const image = event.target as HTMLImageElement
if (image.src === '') {
return
}
iframeCommunicator.sendClickedImageUrl({
src: image.src,
alt: image.alt,
title: image.title
})
}, [iframeCommunicator])
if (!baseUrl) {
return null
}
return (
<div className={"vh-100 w-100"}>
<ScrollingDocumentRenderPane
extraClasses={'w-100'}
markdownContent={markdownContent}
wide={isWide}
onTaskCheckedChange={onTaskCheckedChange}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource}
onMetadataChange={onMetaDataChange}
scrollState={scrollState}
onScroll={onScroll}
baseUrl={baseUrl}
onImageClick={onImageClick}/>
</div>
)
}
export default RenderPage

View file

@ -0,0 +1,92 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
export enum RenderIframeMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
RENDERER_READY = 'RENDERER_READY',
SET_DARKMODE = 'SET_DARKMODE',
SET_WIDE = 'SET_WIDE',
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
ON_SET_META_DATA = 'ON_SET_META_DATA',
IMAGE_CLICKED = 'IMAGE_CLICKED',
SET_BASE_URL = 'SET_BASE_URL'
}
export interface RendererToEditorSimpleMessage {
type: RenderIframeMessageType.RENDERER_READY | RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
}
export interface SetDarkModeMessage {
type: RenderIframeMessageType.SET_DARKMODE,
activated: boolean
}
export interface ImageDetails {
alt?: string
src: string
title?: string
}
export interface SetBaseUrlMessage {
type: RenderIframeMessageType.SET_BASE_URL,
baseUrl: string
}
export interface ImageClickedMessage {
type: RenderIframeMessageType.IMAGE_CLICKED,
details: ImageDetails
}
export interface SetWideMessage {
type: RenderIframeMessageType.SET_WIDE,
activated: boolean
}
export interface SetMarkdownContentMessage {
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
content: string
}
export interface SetScrollStateMessage {
type: RenderIframeMessageType.SET_SCROLL_STATE,
scrollState: ScrollState
}
export interface OnTaskCheckboxChangeMessage {
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
lineInMarkdown: number,
checked: boolean
}
export interface OnFirstHeadingChangeMessage {
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
firstHeading: string | undefined
}
export interface OnMetadataChangeMessage {
type: RenderIframeMessageType.ON_SET_META_DATA,
metaData: YAMLMetaData | undefined
}
export type EditorToRendererIframeMessage =
SetMarkdownContentMessage |
SetDarkModeMessage |
SetWideMessage |
SetScrollStateMessage |
SetBaseUrlMessage
export type RendererToEditorIframeMessage =
RendererToEditorSimpleMessage |
OnFirstHeadingChangeMessage |
OnTaskCheckboxChangeMessage |
OnMetadataChangeMessage |
SetScrollStateMessage |
ImageClickedMessage