mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04:00
Wrap markdown rendering in iframe (#837)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
bd31076928
commit
586969f368
45 changed files with 1014 additions and 287 deletions
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import React, { RefObject, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { InternalLink } from '../common/links/internal-link'
|
||||
|
@ -17,6 +17,7 @@ import { usePostMetaDataOnChange } from './hooks/use-post-meta-data-on-change'
|
|||
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
|
||||
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
||||
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
|
@ -27,21 +28,26 @@ export interface FullMarkdownRendererProps {
|
|||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
rendererRef?: RefObject<HTMLDivElement>
|
||||
rendererRef?: Ref<HTMLDivElement>
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
}
|
||||
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
wide,
|
||||
rendererRef
|
||||
}) => {
|
||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange)
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||
{
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
wide,
|
||||
rendererRef,
|
||||
baseUrl,
|
||||
onImageClick
|
||||
}) => {
|
||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange, onImageClick, baseUrl)
|
||||
useTranslation()
|
||||
|
||||
const [showYamlError, setShowYamlError] = useState(false)
|
||||
|
|
|
@ -16,14 +16,14 @@ export const useConvertMarkdownToReactDom = (
|
|||
markdownCode: string,
|
||||
markdownIt: MarkdownIt,
|
||||
componentReplacers?: () => ComponentReplacer[],
|
||||
onPreRendering?: () => void,
|
||||
onPostRendering?: () => void): ReactElement[] => {
|
||||
onBeforeRendering?: () => void,
|
||||
onAfterRendering?: () => void): ReactElement[] => {
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
return useMemo(() => {
|
||||
if (onPreRendering) {
|
||||
onPreRendering()
|
||||
if (onBeforeRendering) {
|
||||
onBeforeRendering()
|
||||
}
|
||||
const html = markdownIt.render(markdownCode)
|
||||
const contentLines = markdownCode.split('\n')
|
||||
|
@ -35,9 +35,9 @@ export const useConvertMarkdownToReactDom = (
|
|||
lastUsedLineId.current = newLastUsedLineId
|
||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
||||
const rendering = ReactHtmlParser(html, { transform: transformer })
|
||||
if (onPostRendering) {
|
||||
onPostRendering()
|
||||
if (onAfterRendering) {
|
||||
onAfterRendering()
|
||||
}
|
||||
return rendering
|
||||
}, [onPreRendering, onPostRendering, markdownCode, markdownIt, componentReplacers])
|
||||
}, [onBeforeRendering, onAfterRendering, markdownCode, markdownIt, componentReplacers])
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
|||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||
import { CsvReplacer } from '../replace-components/csv/csv-replacer'
|
||||
import { LinkInNewTabReplacer } from '../replace-components/external-links-in-new-tabs/external-links-in-new-tabs'
|
||||
import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer'
|
||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer'
|
||||
import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||
import { ImageReplacer } from '../replace-components/image/image-replacer'
|
||||
import { ImageClickHandler, ImageReplacer } from '../replace-components/image/image-replacer'
|
||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||
import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer'
|
||||
import { LinkReplacer } from '../replace-components/link-replacer/link-replacer'
|
||||
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
||||
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
||||
import { PdfReplacer } from '../replace-components/pdf/pdf-replacer'
|
||||
|
@ -28,9 +28,10 @@ import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
|
|||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
|
||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void): () => ComponentReplacer[] => {
|
||||
return useMemo(() => () => [
|
||||
new LinkInNewTabReplacer(),
|
||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
||||
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
|
||||
() => [
|
||||
new LinkReplacer(baseUrl),
|
||||
new LinemarkerReplacer(),
|
||||
new PossibleWiderReplacer(),
|
||||
new GistReplacer(),
|
||||
|
@ -39,7 +40,7 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
|||
new AsciinemaReplacer(),
|
||||
new AbcReplacer(),
|
||||
new PdfReplacer(),
|
||||
new ImageReplacer(),
|
||||
new ImageReplacer(onImageClick),
|
||||
new SequenceDiagramReplacer(),
|
||||
new CsvReplacer(),
|
||||
new FlowchartReplacer(),
|
||||
|
@ -51,5 +52,4 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
|||
new QuoteOptionsReplacer(),
|
||||
new KatexReplacer(),
|
||||
new TaskListReplacer(onTaskCheckedChange)
|
||||
], [onTaskCheckedChange])
|
||||
}
|
||||
], [onImageClick, onTaskCheckedChange, baseUrl])
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
|
@ -45,9 +45,9 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
|||
!this.useFrontmatter
|
||||
? undefined
|
||||
: {
|
||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
},
|
||||
headlineAnchors,
|
||||
KatexReplacer.markdownItPlugin,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { DomElement } from 'domhandler'
|
||||
import { ReactElement } from 'react'
|
||||
import { ComponentReplacer, SubNodeTransform } from '../ComponentReplacer'
|
||||
|
||||
export class LinkInNewTabReplacer extends ComponentReplacer {
|
||||
public getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined) {
|
||||
const isJumpMark = node.attribs?.href?.substr(0, 1) === '#'
|
||||
|
||||
if (node.name !== 'a' || isJumpMark) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return <a className={node.attribs?.class} title={node.attribs?.title} href={node.attribs?.href} rel='noopener noreferrer' target='_blank'>
|
||||
{
|
||||
node.children?.map((child, index) => subNodeTransform(child, index))
|
||||
}
|
||||
</a>
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, ReactElement, useEffect, useState } from 'react'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
|
@ -59,7 +59,7 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
|||
{ dom }
|
||||
</code>
|
||||
<div className={'text-right button-inside'}>
|
||||
<CopyToClipboardButton content={code}/>
|
||||
<CopyToClipboardButton content={code} data-cy="copy-code-button"/>
|
||||
</div>
|
||||
</Fragment>)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react'
|
|||
import { Modal } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import "./lightbox.scss"
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export interface ImageLightboxModalProps {
|
||||
show: boolean
|
||||
|
@ -33,7 +34,7 @@ export const ImageLightboxModal: React.FC<ImageLightboxModalProps> = ({ show, on
|
|||
<span>{alt ?? title ?? ''}</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<img alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide}/>
|
||||
<ProxyImageFrame alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide}/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,17 +9,27 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void;
|
||||
|
||||
export class ImageReplacer extends ComponentReplacer {
|
||||
private readonly clickHandler?: ImageClickHandler
|
||||
|
||||
constructor (clickHandler?: ImageClickHandler) {
|
||||
super()
|
||||
this.clickHandler = clickHandler
|
||||
}
|
||||
|
||||
public getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
if (node.name === 'img' && node.attribs) {
|
||||
return <ProxyImageFrame
|
||||
id={node.attribs.id}
|
||||
className={node.attribs.class}
|
||||
className={`${node.attribs.class} cursor-zoom-in`}
|
||||
src={node.attribs.src}
|
||||
alt={node.attribs.alt}
|
||||
title={node.attribs.title}
|
||||
width={node.attribs.width}
|
||||
height={node.attribs.height}
|
||||
onClick={this.clickHandler}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { ImageLightboxModal } from './image-lightbox-modal'
|
||||
import "./lightbox.scss"
|
||||
|
||||
export const LightboxImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
||||
{
|
||||
alt,
|
||||
title,
|
||||
src,
|
||||
...props
|
||||
}) => {
|
||||
const [showFullscreenImage, setShowFullscreenImage] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<img alt={alt} src={src} title={title} {...props} className={'cursor-zoom-in'}
|
||||
onClick={() => setShowFullscreenImage(true)}/>
|
||||
<ImageLightboxModal
|
||||
show={showFullscreenImage}
|
||||
onHide={() => setShowFullscreenImage(false)} title={title} src={src} alt={alt}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { getProxiedUrl } from '../../../../api/media'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { LightboxImageFrame } from './lightbox-image-frame'
|
||||
|
||||
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
||||
{
|
||||
|
@ -29,13 +28,6 @@ export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>
|
|||
.catch(err => console.error(err))
|
||||
}, [imageProxyEnabled, src])
|
||||
|
||||
if (imageProxyEnabled) {
|
||||
return (
|
||||
<LightboxImageFrame src={imageUrl} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LightboxImageFrame src={src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||
)
|
||||
return <img src={imageProxyEnabled ? imageUrl : (src ?? '')} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { DomElement } from 'domhandler'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
|
||||
|
||||
export const createJumpToMarkClickEventHandler = (id: string) => {
|
||||
return (event: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
document.getElementById(id)?.scrollIntoView()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkReplacer extends ComponentReplacer {
|
||||
constructor (private baseUrl?: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
public getReplacement (node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined) {
|
||||
if (node.name !== 'a' || !node.attribs || !node.attribs.href) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const url = node.attribs.href
|
||||
const isJumpMark = url.substr(0, 1) === '#'
|
||||
|
||||
const id = url.substr(1)
|
||||
|
||||
try {
|
||||
node.attribs.href = new URL(url, this.baseUrl).toString()
|
||||
} catch (e) {
|
||||
node.attribs.href = url
|
||||
}
|
||||
|
||||
if (isJumpMark) {
|
||||
return <span onClick={createJumpToMarkClickEventHandler(id)}>
|
||||
{nativeRenderer()}
|
||||
</span>
|
||||
} else {
|
||||
node.attribs.rel = "noreferer noopener"
|
||||
node.attribs.target = "_blank"
|
||||
return nativeRenderer()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ export interface TextDifferenceResult {
|
|||
lastUsedLineId: number
|
||||
}
|
||||
|
||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string|undefined => {
|
||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string | undefined => {
|
||||
if (!node.attribs || lineKeys === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export const renderNativeNode = (node: DomElement, key: string, transform: Trans
|
|||
return convertNodeToElement(node, key as unknown as number, transform)
|
||||
}
|
||||
|
||||
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]):Transform => {
|
||||
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]): Transform => {
|
||||
const transform: Transform = (node, index) => {
|
||||
const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
|
||||
const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue