Collected minor changes from #837 (#962)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-01-23 22:01:53 +01:00 committed by GitHub
parent 0ef81eca5d
commit 1777ba848f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 261 additions and 173 deletions

View file

@ -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 { TocAst } from 'markdown-it-toc-done-right'
import React, { RefObject, useRef, useState } from 'react'
@ -31,23 +31,23 @@ export interface DocumentRenderPaneProps {
wide?: boolean
}
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
extraClasses,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onMetadataChange,
onMouseEnterRenderer,
onScrollRenderer,
onTaskCheckedChange,
documentRenderPaneRef,
wide
}) => {
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
{
extraClasses,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onMetadataChange,
onMouseEnterRenderer,
onScrollRenderer,
onTaskCheckedChange,
documentRenderPaneRef,
wide
}) => {
const [tocAst, setTocAst] = useState<TocAst>()
const { width } = useResizeObserver(documentRenderPaneRef ? { ref: documentRenderPaneRef } : undefined)
const realWidth = width || 0
const rendererRef = useRef<HTMLDivElement|null>(null)
const rendererRef = useRef<HTMLDivElement | null>(null)
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
return (
@ -55,21 +55,19 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
ref={documentRenderPaneRef} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
<div className={'col-md'}/>
<div className={'bg-light flex-fill'}>
<ShowIf condition={yamlDeprecatedTags}>
<YamlArrayDeprecationAlert/>
</ShowIf>
<div >
<FullMarkdownRenderer
rendererRef={rendererRef}
className={'flex-fill mb-3'}
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={changeLineMarker}
onMetaDataChange={onMetadataChange}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={(tocAst) => setTocAst(tocAst)}
wide={wide}
/>
<YamlArrayDeprecationAlert/>
<div>
<FullMarkdownRenderer
rendererRef={rendererRef}
className={'flex-fill mb-3'}
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={changeLineMarker}
onMetaDataChange={onMetadataChange}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={(tocAst) => setTocAst(tocAst)}
wide={wide}
/>
</div>
</div>

View file

@ -6,16 +6,24 @@
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next';
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import links from '../../../links.json'
import { ApplicationState } from '../../../redux'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { ShowIf } from '../../common/show-if/show-if'
export const YamlArrayDeprecationAlert: React.FC = () => {
return (
<Alert data-cy={'yamlArrayDeprecationAlert'} variant='warning' dir='auto'>
<Trans i18nKey='editor.deprecatedTags' />
<br/>
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} href={links.faq} className={'text-primary'}/>
</Alert>
);
useTranslation()
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
return <ShowIf condition={yamlDeprecatedTags}>
<Alert data-cy={'yamlArrayDeprecationAlert'} variant='warning' dir='auto'>
<span className={'text-wrap'}>
<Trans i18nKey='editor.deprecatedTags'/>
</span>
<br/>
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} href={links.faq} className={'text-primary'}/>
</Alert>
</ShowIf>
}

View file

@ -1,71 +1,45 @@
/*
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 React, { ReactElement, RefObject, useMemo, useRef } from 'react'
import { Alert } from 'react-bootstrap'
import ReactHtmlParser from 'react-html-parser'
import { Trans } from 'react-i18next'
import React, { RefObject, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { ShowIf } from '../common/show-if/show-if'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
import { ComponentReplacer } from './replace-components/ComponentReplacer'
import { AdditionalMarkdownRendererProps, LineKeys } from './types'
import { buildTransformer } from './utils/html-react-transformer'
import { calculateNewLineNumberMapping } from './utils/line-number-mapping'
import { AdditionalMarkdownRendererProps } from './types'
export interface BasicMarkdownRendererProps {
componentReplacers?: () => ComponentReplacer[],
markdownIt: MarkdownIt,
documentReference?: RefObject<HTMLDivElement>
onBeforeRendering?: () => void
onPostRendering?: () => void
onAfterRendering?: () => void
}
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
className,
content,
wide,
componentReplacers,
markdownIt,
documentReference,
onBeforeRendering,
onPostRendering
}) => {
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
{
className,
content,
wide,
componentReplacers,
markdownIt,
documentReference,
onBeforeRendering,
onAfterRendering
}) => {
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const oldMarkdownLineKeys = useRef<LineKeys[]>()
const lastUsedLineId = useRef<number>(0)
const markdownReactDom: ReactElement[] = useMemo(() => {
if (onBeforeRendering) {
onBeforeRendering()
}
const trimmedContent = content.substr(0, maxLength)
const html: string = markdownIt.render(trimmedContent)
const contentLines = trimmedContent.split('\n')
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
oldMarkdownLineKeys.current = newLines
lastUsedLineId.current = newLastUsedLineId
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
const rendering = ReactHtmlParser(html, { transform: transformer })
if (onPostRendering) {
onPostRendering()
}
return rendering
}, [onBeforeRendering, onPostRendering, content, maxLength, markdownIt, componentReplacers])
const trimmedContent = useMemo(() => content.length > maxLength ? content.substr(0, maxLength) : content, [content, maxLength])
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, componentReplacers, onBeforeRendering, onAfterRendering)
return (
<div className={`${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
<ShowIf condition={content.length > maxLength}>
<Alert variant='danger' dir={'auto'} data-cy={'limitReachedMessage'}>
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }}/>
</Alert>
</ShowIf>
<div className={`${className ?? ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
<DocumentLengthLimitReachedAlert contentLength={content.length}/>
<div ref={documentReference} className={'markdown-body w-100 d-flex flex-column align-items-center'}>
{markdownReactDom}
</div>

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { ShowIf } from '../common/show-if/show-if'
export interface DocumentLengthLimitReachedAlertProps {
contentLength: number
}
export const DocumentLengthLimitReachedAlert: React.FC<DocumentLengthLimitReachedAlertProps> = ({ contentLength }) => {
useTranslation()
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
return (
<ShowIf condition={contentLength > maxLength}>
<Alert variant='danger' dir={'auto'} data-cy={'limitReachedMessage'}>
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }}/>
</Alert>
</ShowIf>)
}

View file

@ -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 { TocAst } from 'markdown-it-toc-done-right'
import React, { RefObject, useCallback, useMemo, useRef, useState } from 'react'
@ -102,7 +102,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
markdownIt={markdownIt}
documentReference={documentElement}
onBeforeRendering={clearMetadata}
onPostRendering={checkYamlErrorState}
onAfterRendering={checkYamlErrorState}
/>
</div>
)

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it/lib'
import { ReactElement, useMemo, useRef } from 'react'
import ReactHtmlParser from 'react-html-parser'
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
import { LineKeys } from '../types'
import { buildTransformer } from '../utils/html-react-transformer'
import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
export const useConvertMarkdownToReactDom = (
markdownCode: string,
markdownIt: MarkdownIt,
componentReplacers?: () => ComponentReplacer[],
onPreRendering?: () => void,
onPostRendering?: () => void): ReactElement[] => {
const oldMarkdownLineKeys = useRef<LineKeys[]>()
const lastUsedLineId = useRef<number>(0)
return useMemo(() => {
if (onPreRendering) {
onPreRendering()
}
const html = markdownIt.render(markdownCode)
const contentLines = markdownCode.split('\n')
const {
lines: newLines,
lastUsedLineId: newLastUsedLineId
} = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
oldMarkdownLineKeys.current = newLines
lastUsedLineId.current = newLastUsedLineId
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
const rendering = ReactHtmlParser(html, { transform: transformer })
if (onPostRendering) {
onPostRendering()
}
return rendering
}, [onPreRendering, onPostRendering, markdownCode, markdownIt, componentReplacers])
}

View file

@ -1,4 +1,4 @@
/*
/*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
@ -27,18 +27,15 @@
h1, h2, h3, h4, h5, h6 {
a.heading-anchor {
margin-left: -1.25em;
font-size: 0.75em;
margin-top: 0.25em;
opacity: 0;
opacity: 0.3;
transition: opacity 0.1s;
&:hover {
opacity: 1;
}
}
&:hover a.heading-anchor {
opacity: 1;
}
}
blockquote .quote-extra {

View file

@ -10,10 +10,10 @@ import { ReactElement } from 'react'
export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null
export type NativeRenderer = (node: DomElement, key: number) => ReactElement
export type NativeRenderer = () => ReactElement
export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams
export abstract class ComponentReplacer {
public abstract getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined);
public abstract getReplacement (node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined);
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Modal } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import "./lightbox.scss"
export interface ImageLightboxModalProps {
show: boolean
onHide: () => void
alt?: string
src?: string
title?: string
}
export const ImageLightboxModal: React.FC<ImageLightboxModalProps> = ({ show, onHide, src, alt, title }) => {
return (
<Modal
animation={true}
centered={true}
dialogClassName={'text-dark lightbox'}
show={show && !!src}
onHide={onHide}
size={'xl'}>
<Modal.Header closeButton={true}>
<Modal.Title className={'h6'}>
<ForkAwesomeIcon icon={'picture-o'}/>
&nbsp;
<span>{alt ?? title ?? ''}</span>
</Modal.Title>
</Modal.Header>
<img alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide}/>
</Modal>
)
}

View file

@ -1,18 +1,18 @@
/*
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 { DomElement } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
import { ImageFrame } from './image-frame'
import { ProxyImageFrame } from './proxy-image-frame'
export class ImageReplacer extends ComponentReplacer {
public getReplacement (node: DomElement): React.ReactElement | undefined {
if (node.name === 'img' && node.attribs) {
return <ImageFrame
return <ProxyImageFrame
id={node.attribs.id}
className={node.attribs.class}
src={node.attribs.src}

View file

@ -0,0 +1,29 @@
/*
* 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>
)
}

View file

@ -1,36 +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 { Modal } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import "./lightbox.scss"
export const LightboxedImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ alt, ...props }) => {
const [showFullscreenImage, setShowFullscreenImage] = useState(false)
return (
<Fragment>
<img alt={alt} {...props} className={'cursor-zoom-in'} onClick={() => setShowFullscreenImage(true)}/>
<Modal
animation={true}
centered={true}
dialogClassName={'text-dark lightbox'}
show={showFullscreenImage}
onHide={() => setShowFullscreenImage(false)}
size={'xl'}>
<Modal.Header closeButton={true}>
<Modal.Title className={'h6'}>
<ForkAwesomeIcon icon={'picture-o'}/>
&nbsp;
<span>{alt ?? ''}</span>
</Modal.Title>
</Modal.Header>
<img alt={alt} {...props} className={'w-100 cursor-zoom-out'} onClick={() => setShowFullscreenImage(false)}/>
</Modal>
</Fragment>
)
}

View file

@ -1,16 +1,22 @@
/*
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, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { getProxiedUrl } from '../../../../api/media'
import { ApplicationState } from '../../../../redux'
import { LightboxedImageFrame } from './lightboxedImageFrame'
import { LightboxImageFrame } from './lightbox-image-frame'
export const ImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
{
src,
title,
alt,
...props
}) => {
const [imageUrl, setImageUrl] = useState('')
const imageProxyEnabled = useSelector((state: ApplicationState) => state.config.useImageProxy)
@ -25,11 +31,11 @@ export const ImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
if (imageProxyEnabled) {
return (
<LightboxedImageFrame src={imageUrl} title={title ?? alt ?? ''} alt={alt} {...props}/>
<LightboxImageFrame src={imageUrl} title={title ?? alt ?? ''} alt={alt} {...props}/>
)
}
return (
<LightboxedImageFrame src={src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props}/>
<LightboxImageFrame src={src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props}/>
)
}

View file

@ -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 from 'react'
import { Alert } from 'react-bootstrap'
@ -15,8 +15,10 @@ export const DeprecationWarning: React.FC = () => {
return (
<Alert data-cy={'yaml'} className={'mt-2'} variant={'warning'}>
<Trans i18nKey={'renderer.sequence.deprecationWarning'}/>
&nbsp;
<span className={'text-wrap'}>
<Trans i18nKey={'renderer.sequence.deprecationWarning'}/>
</span>
<br/>
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} className={'text-primary'} href={links.faq}/>
</Alert>
)

View file

@ -1,13 +1,13 @@
/*
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 { DomElement } from 'domhandler'
import React, { ReactElement, Suspense } from 'react'
import { convertNodeToElement, Transform } from 'react-html-parser'
import { ComponentReplacer, SubNodeTransform } from '../replace-components/ComponentReplacer'
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../replace-components/ComponentReplacer'
import { LineKeys } from '../types'
export interface TextDifferenceResult {
@ -45,9 +45,9 @@ export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys
return `${lineKeys[startLine].id}_${lineKeys[endLine].id}`
}
export const findNodeReplacement = (node: DomElement, key: string, allReplacers: ComponentReplacer[], subNodeTransform: SubNodeTransform): ReactElement|null|undefined => {
export const findNodeReplacement = (node: DomElement, key: string, allReplacers: ComponentReplacer[], subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): ReactElement | null | undefined => {
return allReplacers
.map((componentReplacer) => componentReplacer.getReplacement(node, subNodeTransform))
.map((componentReplacer) => componentReplacer.getReplacement(node, subNodeTransform, nativeRenderer))
.find((replacement) => replacement !== undefined)
}
@ -62,18 +62,18 @@ export const renderNativeNode = (node: DomElement, key: string, transform: Trans
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]):Transform => {
const transform: Transform = (node, index) => {
const nativeRenderer = (subNode: DomElement, subKey: string) => renderNativeNode(subNode, subKey, transform)
const subNodeTransform:SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
const key = calculateKeyFromLineMarker(node, lineKeys) ?? (-index).toString()
const tryReplacement = findNodeReplacement(node, key, allReplacers, subNodeTransform)
const tryReplacement = findNodeReplacement(node, key, allReplacers, subNodeTransform, nativeRenderer)
if (tryReplacement === null) {
return null
} else if (tryReplacement === undefined) {
return nativeRenderer(node, key)
return nativeRenderer()
} else {
return <Suspense key={key} fallback={<span>Loading...</span>}>
{ tryReplacement }
{tryReplacement}
</Suspense>
}
}

View file

@ -7,7 +7,7 @@
import { useEffect } from 'react'
import { useIsDarkModeActivated } from './use-is-dark-mode-activated'
export const useApplyDarkMode = ():void => {
export const useApplyDarkMode = (): void => {
const darkModeActivated = useIsDarkModeActivated()
useEffect(() => {