mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-09 13:51:57 -04:00
refactor(renderer): convert html/markdown-to-react converters from hooks to components
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
0457a633cc
commit
958b23e25a
30 changed files with 523 additions and 203 deletions
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HTML to React renders basic html correctly 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
This is a test
|
||||
<b>
|
||||
sentence
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`HTML to React will forward the DomPurify settings 1`] = `
|
||||
<div>
|
||||
<test-tag>
|
||||
Test!
|
||||
</test-tag>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`HTML to React will forward the parser options 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
Hijacked!
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`HTML to React won't render script tags 1`] = `<div />`;
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { HtmlToReact } from './html-to-react'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
describe('HTML to React', () => {
|
||||
it('renders basic html correctly', () => {
|
||||
const view = render(<HtmlToReact htmlCode={'<p>This is a test <b>sentence</b></p>'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("won't render script tags", () => {
|
||||
const view = render(<HtmlToReact htmlCode={'<script type="application/javascript">alert("XSS!")</script>'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('will forward the DomPurify settings', () => {
|
||||
const view = render(
|
||||
<HtmlToReact domPurifyConfig={{ ADD_TAGS: ['test-tag'] }} htmlCode={'<test-tag>Test!</test-tag>'} />
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('will forward the parser options', () => {
|
||||
let transformerVisited = false
|
||||
let preprocessNodesVisited = false
|
||||
|
||||
const view = render(
|
||||
<HtmlToReact
|
||||
htmlCode={'<p>This is a sentence</p>'}
|
||||
parserOptions={{
|
||||
transform: () => {
|
||||
transformerVisited = true
|
||||
return <p>Hijacked!</p>
|
||||
},
|
||||
preprocessNodes: (document) => {
|
||||
preprocessNodesVisited = true
|
||||
return document
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
expect(preprocessNodesVisited).toBeTruthy()
|
||||
expect(transformerVisited).toBeTruthy()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ParserOptions } from '@hedgedoc/html-to-react'
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import type DOMPurify from 'dompurify'
|
||||
import { sanitize } from 'dompurify'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
|
||||
export interface HtmlToReactProps {
|
||||
htmlCode: string
|
||||
domPurifyConfig?: DOMPurify.Config
|
||||
parserOptions?: ParserOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders
|
||||
* @param htmlCode
|
||||
* @param domPurifyConfig
|
||||
* @param parserOptions
|
||||
* @constructor
|
||||
*/
|
||||
export const HtmlToReact: React.FC<HtmlToReactProps> = ({ htmlCode, domPurifyConfig, parserOptions }) => {
|
||||
const elements = useMemo(() => {
|
||||
const sanitizedHtmlCode = sanitize(htmlCode, { ...domPurifyConfig, RETURN_DOM_FRAGMENT: false, RETURN_DOM: false })
|
||||
return convertHtmlToReact(sanitizedHtmlCode, parserOptions)
|
||||
}, [domPurifyConfig, htmlCode, parserOptions])
|
||||
|
||||
return <Fragment>{elements}</Fragment>
|
||||
}
|
|
@ -7,11 +7,12 @@ import { cypressId } from '../../utils/cypress-attribute'
|
|||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
import { HeadlineAnchorsMarkdownExtension } from './extensions/headline-anchors-markdown-extension'
|
||||
import type { LineMarkers } from './extensions/linemarker/add-line-marker-markdown-it-plugin'
|
||||
import { LinemarkerMarkdownExtension } from './extensions/linemarker/linemarker-markdown-extension'
|
||||
import type { LineMarkerPosition } from './extensions/linemarker/types'
|
||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||
import { useCalculateLineMarkerPosition } from './hooks/use-calculate-line-marker-positions'
|
||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
@ -27,9 +28,7 @@ export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererPro
|
|||
* @param onFirstHeadingChange The callback to call if the first heading changes.
|
||||
* @param onLineMarkerPositionChanged The callback to call with changed {@link LineMarkers}
|
||||
* @param onTaskCheckedChange The callback to call if a task is checked or unchecked.
|
||||
* @param onTocChange The callback to call if the toc changes.
|
||||
* @param baseUrl The base url of the renderer
|
||||
* @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
|
||||
*/
|
||||
|
@ -47,19 +46,17 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
|
||||
const extensions = useMarkdownExtensions(
|
||||
baseUrl,
|
||||
currentLineMarkers,
|
||||
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], [])
|
||||
useMemo(
|
||||
() => [
|
||||
new HeadlineAnchorsMarkdownExtension(),
|
||||
new LinemarkerMarkdownExtension((values) => (currentLineMarkers.current = values))
|
||||
],
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
|
||||
|
||||
useTranslation()
|
||||
useCalculateLineMarkerPosition(
|
||||
markdownBodyRef,
|
||||
currentLineMarkers.current,
|
||||
onLineMarkerPositionChanged,
|
||||
markdownBodyRef.current?.offsetTop ?? 0
|
||||
)
|
||||
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged)
|
||||
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
||||
useEffect(() => {
|
||||
extractFirstHeadline()
|
||||
|
@ -72,7 +69,12 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
ref={markdownBodyRef}
|
||||
data-word-count-target={true}
|
||||
className={`${className ?? ''} markdown-body w-100 d-flex flex-column align-items-center`}>
|
||||
{markdownReactDom}
|
||||
<MarkdownToReact
|
||||
markdownContentLines={markdownContentLines}
|
||||
markdownRenderExtensions={extensions}
|
||||
newlinesAreBreaks={newlinesAreBreaks}
|
||||
allowHtml={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -15,10 +15,6 @@ import type MarkdownIt from 'markdown-it/lib'
|
|||
export class ImagePlaceholderMarkdownExtension extends MarkdownRendererExtension {
|
||||
public static readonly PLACEHOLDER_URL = 'https://'
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
addLineToPlaceholderImageTags(markdownIt)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ export interface LineWithId {
|
|||
id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the line number of a line marker and its absolute scroll position on the page.
|
||||
*/
|
||||
export interface LineMarkerPosition {
|
||||
line: number
|
||||
position: number
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||
import render from 'dom-serializer'
|
||||
import type { Document } from 'domhandler'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { parseDocument } from 'htmlparser2'
|
||||
|
||||
/**
|
||||
* Sanitizes the given {@link Document document}.
|
||||
*
|
||||
* @see https://cure53.de/purify
|
||||
*/
|
||||
export class SanitizerNodePreprocessor extends NodeProcessor {
|
||||
constructor(private tagNameWhiteList: string[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
process(nodes: Document): Document {
|
||||
const sanitizedHtml = DOMPurify.sanitize(render(nodes), {
|
||||
ADD_TAGS: this.tagNameWhiteList
|
||||
})
|
||||
return parseDocument(sanitizedHtml)
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
|
||||
import { SanitizerNodePreprocessor } from './dom-purifier-node-preprocessor'
|
||||
|
||||
/**
|
||||
* Adds support for html sanitizing using dompurify to the markdown rendering.
|
||||
*/
|
||||
export class SanitizerMarkdownExtension extends MarkdownRendererExtension {
|
||||
constructor(private tagNameWhiteList: string[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
public buildNodeProcessors(): NodeProcessor[] {
|
||||
return [new SanitizerNodePreprocessor(this.tagNameWhiteList)]
|
||||
}
|
||||
}
|
|
@ -18,6 +18,8 @@ export class TableOfContentsMarkdownExtension extends MarkdownRendererExtension
|
|||
private lastAst: TocAst | undefined = undefined
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
const eventEmitter = this.eventEmitter
|
||||
if (eventEmitter !== undefined) {
|
||||
toc(markdownIt, {
|
||||
listType: 'ul',
|
||||
level: [1, 2, 3],
|
||||
|
@ -26,9 +28,10 @@ export class TableOfContentsMarkdownExtension extends MarkdownRendererExtension
|
|||
return
|
||||
}
|
||||
this.lastAst = ast
|
||||
this.eventEmitter?.emit(TableOfContentsMarkdownExtension.EVENT_NAME, ast)
|
||||
eventEmitter.emit(TableOfContentsMarkdownExtension.EVENT_NAME, ast)
|
||||
},
|
||||
slugify: tocSlugify
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,13 +52,11 @@ const calculateLineMarkerPositions = (
|
|||
* @param documentElement A reference to the rendered document.
|
||||
* @param lineMarkers A list of {@link LineMarkers}
|
||||
* @param onLineMarkerPositionChanged The callback to call if the {@link LineMarkerPosition line marker positions} change e.g. by rendering or resizing.
|
||||
* @param offset The optional offset
|
||||
*/
|
||||
export const useCalculateLineMarkerPosition = (
|
||||
documentElement: RefObject<HTMLDivElement>,
|
||||
lineMarkers?: LineMarkers[],
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void,
|
||||
offset?: number
|
||||
lineMarkers: LineMarkers[] | undefined,
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
): void => {
|
||||
const lastLineMarkerPositions = useRef<LineMarkerPosition[]>()
|
||||
|
||||
|
@ -67,13 +65,17 @@ export const useCalculateLineMarkerPosition = (
|
|||
return
|
||||
}
|
||||
|
||||
const newLines = calculateLineMarkerPositions(documentElement.current, lineMarkers, offset)
|
||||
const newLines = calculateLineMarkerPositions(
|
||||
documentElement.current,
|
||||
lineMarkers,
|
||||
documentElement.current.offsetTop ?? 0
|
||||
)
|
||||
|
||||
if (!equal(newLines, lastLineMarkerPositions)) {
|
||||
lastLineMarkerPositions.current = newLines
|
||||
onLineMarkerPositionChanged(newLines)
|
||||
}
|
||||
}, [documentElement, lineMarkers, offset, onLineMarkerPositionChanged])
|
||||
}, [documentElement, lineMarkers, onLineMarkerPositionChanged])
|
||||
|
||||
useEffect(() => {
|
||||
calculateNewLineMarkerPositions()
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||
import { SanitizerMarkdownExtension } from '../extensions/sanitizer/sanitizer-markdown-extension'
|
||||
import type { ValidReactDomElement } from '../replace-components/component-replacer'
|
||||
import { LineIdMapper } from '../utils/line-id-mapper'
|
||||
import { NodeToReactTransformer } from '../utils/node-to-react-transformer'
|
||||
import { useCombinedNodePreprocessor } from './use-combined-node-preprocessor'
|
||||
import { useConfiguredMarkdownIt } from './use-configured-markdown-it'
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Renders Markdown-Code into react elements.
|
||||
*
|
||||
* @param markdownContentLines The Markdown code lines that should be rendered
|
||||
* @param additionalMarkdownExtensions A list of {@link MarkdownRendererExtension markdown extensions} that should be used
|
||||
* @param newlinesAreBreaks Defines if the alternative break mode of markdown it should be used
|
||||
* @param allowHtml Defines if html is allowed in markdown
|
||||
* @return The React DOM that represents the rendered Markdown code
|
||||
*/
|
||||
export const useConvertMarkdownToReactDom = (
|
||||
markdownContentLines: string[],
|
||||
additionalMarkdownExtensions: MarkdownRendererExtension[],
|
||||
newlinesAreBreaks = true,
|
||||
allowHtml = true
|
||||
): ValidReactDomElement => {
|
||||
const lineNumberMapper = useMemo(() => new LineIdMapper(), [])
|
||||
const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), [])
|
||||
const markdownExtensions = useMemo(() => {
|
||||
const tagWhiteLists = additionalMarkdownExtensions.flatMap((extension) => extension.buildTagNameAllowList())
|
||||
return [...additionalMarkdownExtensions, new SanitizerMarkdownExtension(tagWhiteLists)]
|
||||
}, [additionalMarkdownExtensions])
|
||||
|
||||
useMemo(() => {
|
||||
htmlToReactTransformer.setReplacers(markdownExtensions.flatMap((extension) => extension.buildReplacers()))
|
||||
}, [htmlToReactTransformer, markdownExtensions])
|
||||
|
||||
useMemo(() => {
|
||||
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines))
|
||||
}, [htmlToReactTransformer, lineNumberMapper, markdownContentLines])
|
||||
|
||||
const nodePreProcessor = useCombinedNodePreprocessor(markdownExtensions)
|
||||
const markdownIt = useConfiguredMarkdownIt(markdownExtensions, allowHtml, newlinesAreBreaks)
|
||||
|
||||
return useMemo(() => {
|
||||
const html = markdownIt.render(markdownContentLines.join('\n'))
|
||||
htmlToReactTransformer.resetReplacers()
|
||||
|
||||
return (
|
||||
<Fragment key={'root'}>
|
||||
{convertHtmlToReact(html, {
|
||||
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
|
||||
preprocessNodes: (document) => nodePreProcessor(document)
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}, [htmlToReactTransformer, markdownContentLines, markdownIt, nodePreProcessor])
|
||||
}
|
|
@ -12,43 +12,30 @@ import { GenericSyntaxMarkdownExtension } from '../extensions/generic-syntax-mar
|
|||
import { IframeCapsuleMarkdownExtension } from '../extensions/iframe-capsule/iframe-capsule-markdown-extension'
|
||||
import { ImagePlaceholderMarkdownExtension } from '../extensions/image-placeholder/image-placeholder-markdown-extension'
|
||||
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
|
||||
import type { LineMarkers } from '../extensions/linemarker/add-line-marker-markdown-it-plugin'
|
||||
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
|
||||
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
|
||||
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-markdown-extension'
|
||||
import { TableOfContentsMarkdownExtension } from '../extensions/table-of-contents-markdown-extension'
|
||||
import { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
|
||||
import { useExtensionEventEmitter } from './use-extension-event-emitter'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const optionalMarkdownRendererExtensions = optionalAppExtensions.flatMap((value) =>
|
||||
value.buildMarkdownRendererExtensions()
|
||||
)
|
||||
|
||||
/**
|
||||
* Provides a list of {@link MarkdownRendererExtension markdown extensions} that is a combination of the common extensions and the given additional.
|
||||
*
|
||||
* @param baseUrl The base url for the {@link LinkAdjustmentMarkdownExtension}
|
||||
* @param currentLineMarkers A {@link MutableRefObject reference} to {@link LineMarkers} for the {@link LinemarkerMarkdownExtension}
|
||||
* @param additionalExtensions The additional extensions that should be included in the list
|
||||
* @return The created list of markdown extensions
|
||||
*/
|
||||
export const useMarkdownExtensions = (
|
||||
baseUrl: string,
|
||||
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
|
||||
additionalExtensions: MarkdownRendererExtension[]
|
||||
): MarkdownRendererExtension[] => {
|
||||
const extensionEventEmitter = useExtensionEventEmitter()
|
||||
//replace with global list
|
||||
return useMemo(() => {
|
||||
return [
|
||||
...optionalMarkdownRendererExtensions,
|
||||
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
|
||||
...additionalExtensions,
|
||||
new TableOfContentsMarkdownExtension(extensionEventEmitter),
|
||||
new LinemarkerMarkdownExtension(
|
||||
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
|
||||
),
|
||||
new TableOfContentsMarkdownExtension(),
|
||||
new IframeCapsuleMarkdownExtension(),
|
||||
new ImagePlaceholderMarkdownExtension(),
|
||||
new UploadIndicatingImageFrameMarkdownExtension(),
|
||||
|
@ -60,5 +47,5 @@ export const useMarkdownExtensions = (
|
|||
new DebuggerMarkdownExtension(),
|
||||
new ProxyImageMarkdownExtension()
|
||||
]
|
||||
}, [additionalExtensions, baseUrl, currentLineMarkers, extensionEventEmitter])
|
||||
}, [additionalExtensions, baseUrl, extensionEventEmitter])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`markdown to react can render html if allowed 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
test
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`markdown to react can render markdown with newlines as line breaks 1`] = `
|
||||
<div>
|
||||
<h1>
|
||||
This is a headline
|
||||
</h1>
|
||||
|
||||
|
||||
<p>
|
||||
This is content
|
||||
<br />
|
||||
|
||||
This Too
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`markdown to react will use markdown render extensions 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span>test</span>
|
||||
</p>
|
||||
|
||||
|
||||
<span>
|
||||
configure
|
||||
</span>
|
||||
<span>
|
||||
post
|
||||
</span>
|
||||
<span>
|
||||
NodeProcessor!
|
||||
</span>
|
||||
<span
|
||||
data-children="true"
|
||||
>
|
||||
|
||||
node processor children
|
||||
|
||||
</span>
|
||||
<span
|
||||
data-native="true"
|
||||
>
|
||||
|
||||
<nodeprocessor>
|
||||
node processor children
|
||||
</nodeprocessor>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`markdown to react won't render html if forbidden 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span>test</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`markdown to react won't render markdown with newlines as line breaks if forbidden 1`] = `
|
||||
<div>
|
||||
<h1>
|
||||
This is a headline
|
||||
</h1>
|
||||
|
||||
|
||||
<p>
|
||||
This is content
|
||||
This Too
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||
import type { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
|
||||
import type { Document } from 'domhandler'
|
||||
import { useMemo } from 'react'
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||
import type { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { useMemo } from 'react'
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { MarkdownToReact } from './markdown-to-react'
|
||||
import { TestMarkdownRendererExtension } from './test-utils/test-markdown-renderer-extension'
|
||||
import { render } from '@testing-library/react'
|
||||
import type { EventMap } from 'eventemitter2'
|
||||
import { EventEmitter2 } from 'eventemitter2'
|
||||
|
||||
describe('markdown to react', () => {
|
||||
it('can render markdown with newlines as line breaks', () => {
|
||||
const view = render(
|
||||
<MarkdownToReact
|
||||
markdownContentLines={['# This is a headline', 'This is content', 'This Too']}
|
||||
allowHtml={false}
|
||||
newlinesAreBreaks={true}
|
||||
markdownRenderExtensions={[]}></MarkdownToReact>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("won't render markdown with newlines as line breaks if forbidden", () => {
|
||||
const view = render(
|
||||
<MarkdownToReact
|
||||
markdownContentLines={['# This is a headline', 'This is content', 'This Too']}
|
||||
allowHtml={false}
|
||||
newlinesAreBreaks={false}
|
||||
markdownRenderExtensions={[]}></MarkdownToReact>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('can render html if allowed', () => {
|
||||
const view = render(
|
||||
<MarkdownToReact
|
||||
markdownContentLines={['<span>test</span>']}
|
||||
markdownRenderExtensions={[]}
|
||||
newlinesAreBreaks={true}
|
||||
allowHtml={true}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("won't render html if forbidden", () => {
|
||||
const view = render(
|
||||
<MarkdownToReact
|
||||
markdownContentLines={['<span>test</span>']}
|
||||
markdownRenderExtensions={[]}
|
||||
newlinesAreBreaks={true}
|
||||
allowHtml={false}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('will use markdown render extensions', () => {
|
||||
const view = render(
|
||||
<MarkdownToReact
|
||||
markdownContentLines={['<span>test</span>']}
|
||||
markdownRenderExtensions={[new TestMarkdownRendererExtension(new EventEmitter2<EventMap>())]}
|
||||
newlinesAreBreaks={true}
|
||||
allowHtml={false}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { HtmlToReact } from '../../common/html-to-react/html-to-react'
|
||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||
import { useCombinedNodePreprocessor } from './hooks/use-combined-node-preprocessor'
|
||||
import { useConfiguredMarkdownIt } from './hooks/use-configured-markdown-it'
|
||||
import { LineContentToLineIdMapper } from './utils/line-content-to-line-id-mapper'
|
||||
import { NodeToReactTransformer } from './utils/node-to-react-transformer'
|
||||
import type { ParserOptions } from '@hedgedoc/html-to-react'
|
||||
import type DOMPurify from 'dompurify'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
export interface MarkdownToReactProps {
|
||||
markdownContentLines: string[]
|
||||
markdownRenderExtensions: MarkdownRendererExtension[]
|
||||
newlinesAreBreaks?: boolean
|
||||
allowHtml: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Markdown code.
|
||||
*
|
||||
* @param markdownContentLines The Markdown code lines that should be rendered
|
||||
* @param additionalMarkdownExtensions A list of {@link MarkdownRendererExtension markdown extensions} that should be used
|
||||
* @param newlinesAreBreaks Defines if the alternative break mode of markdown it should be used
|
||||
* @param allowHtml Defines if html is allowed in markdown
|
||||
*/
|
||||
export const MarkdownToReact: React.FC<MarkdownToReactProps> = ({
|
||||
markdownContentLines,
|
||||
markdownRenderExtensions,
|
||||
newlinesAreBreaks,
|
||||
allowHtml
|
||||
}) => {
|
||||
const lineNumberMapper = useMemo(() => new LineContentToLineIdMapper(), [])
|
||||
const nodeToReactTransformer = useMemo(() => new NodeToReactTransformer(), [])
|
||||
|
||||
useMemo(() => {
|
||||
nodeToReactTransformer.setReplacers(markdownRenderExtensions.flatMap((extension) => extension.buildReplacers()))
|
||||
}, [nodeToReactTransformer, markdownRenderExtensions])
|
||||
|
||||
useMemo(() => {
|
||||
nodeToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines))
|
||||
}, [nodeToReactTransformer, lineNumberMapper, markdownContentLines])
|
||||
|
||||
const nodePreProcessor = useCombinedNodePreprocessor(markdownRenderExtensions)
|
||||
const markdownIt = useConfiguredMarkdownIt(markdownRenderExtensions, allowHtml, newlinesAreBreaks ?? true)
|
||||
|
||||
const parserOptions: ParserOptions = useMemo(
|
||||
() => ({
|
||||
transform: (node, index) => nodeToReactTransformer.translateNodeToReactElement(node, index),
|
||||
preprocessNodes: (document) => {
|
||||
nodeToReactTransformer.resetReplacers()
|
||||
return nodePreProcessor(document)
|
||||
}
|
||||
}),
|
||||
[nodeToReactTransformer, nodePreProcessor]
|
||||
)
|
||||
|
||||
const html = useMemo(() => markdownIt.render(markdownContentLines.join('\n')), [markdownContentLines, markdownIt])
|
||||
const domPurifyConfig: DOMPurify.Config = useMemo(
|
||||
() => ({
|
||||
ADD_TAGS: markdownRenderExtensions.flatMap((extension) => extension.buildTagNameAllowList())
|
||||
}),
|
||||
[markdownRenderExtensions]
|
||||
)
|
||||
|
||||
return <HtmlToReact htmlCode={html} parserOptions={parserOptions} domPurifyConfig={domPurifyConfig} />
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
|
||||
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { TestNodeProcessor } from './test-node-processor'
|
||||
import { TestReplacer } from './test-replacer'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import Token from 'markdown-it/lib/token'
|
||||
|
||||
export class TestMarkdownRendererExtension extends MarkdownRendererExtension {
|
||||
buildNodeProcessors(): NodeProcessor[] {
|
||||
return [new TestNodeProcessor()]
|
||||
}
|
||||
|
||||
configureMarkdownIt(markdownIt: MarkdownIt) {
|
||||
markdownIt.use(() => {
|
||||
markdownIt.core.ruler.push('configure', (core) => core.tokens.push(new Token('configure', 'configure', 0)))
|
||||
markdownIt.renderer.rules.configure = () => '<span>configure</span>'
|
||||
})
|
||||
}
|
||||
|
||||
buildReplacers(): ComponentReplacer[] {
|
||||
return [new TestReplacer()]
|
||||
}
|
||||
|
||||
buildTagNameAllowList(): string[] {
|
||||
return ['nodeProcessor']
|
||||
}
|
||||
|
||||
configureMarkdownItPost(markdownIt: MarkdownIt) {
|
||||
markdownIt.use(() => {
|
||||
markdownIt.core.ruler.push('post', (core) => core.tokens.push(new Token('post', 'post', 0)))
|
||||
markdownIt.renderer.rules.post = () => '<span>post</span>'
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||
import type { Document } from 'domhandler'
|
||||
import { Element, Text } from 'domhandler'
|
||||
|
||||
export class TestNodeProcessor extends NodeProcessor {
|
||||
process(document: Document): Document {
|
||||
document.childNodes.push(new Element('nodeProcessor', {}, [new Text('node processor children')]))
|
||||
return document
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer'
|
||||
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
|
||||
import type { Element } from 'domhandler'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
export class TestReplacer extends ComponentReplacer {
|
||||
replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement {
|
||||
return node.tagName === 'nodeProcessor' ? (
|
||||
<Fragment>
|
||||
<span>NodeProcessor! </span>
|
||||
<span data-children={true}> {node.childNodes.map(subNodeTransform)} </span>
|
||||
<span data-native={true}> {nativeRenderer()} </span>
|
||||
</Fragment>
|
||||
) : (
|
||||
DO_NOT_REPLACE
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,13 +3,13 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { LineIdMapper } from './line-id-mapper'
|
||||
import { LineContentToLineIdMapper } from './line-content-to-line-id-mapper'
|
||||
|
||||
describe('line id mapper', () => {
|
||||
let lineIdMapper: LineIdMapper
|
||||
let lineIdMapper: LineContentToLineIdMapper
|
||||
|
||||
beforeEach(() => {
|
||||
lineIdMapper = new LineIdMapper()
|
||||
lineIdMapper = new LineContentToLineIdMapper()
|
||||
})
|
||||
|
||||
it('should be case sensitive', () => {
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { LineWithId } from '../extensions/linemarker/types'
|
||||
import type { LineWithId } from '../../extensions/linemarker/types'
|
||||
import type { ArrayChange } from 'diff'
|
||||
import { diffArrays } from 'diff'
|
||||
|
||||
|
@ -14,7 +14,7 @@ type LineChange = ArrayChange<NewLine | LineWithId>
|
|||
* Calculates ids for every line in a given text and memorized the state of the last given text.
|
||||
* It also assigns ids for new lines on every update.
|
||||
*/
|
||||
export class LineIdMapper {
|
||||
export class LineContentToLineIdMapper {
|
||||
private lastLines: LineWithId[] = []
|
||||
private lastUsedLineId = 0
|
||||
|
||||
|
@ -58,7 +58,11 @@ export class LineIdMapper {
|
|||
*/
|
||||
private convertChangesToLinesWithIds(changes: LineChange[]): LineWithId[] {
|
||||
return changes
|
||||
.filter((change) => LineIdMapper.changeIsNotChangingLines(change) || LineIdMapper.changeIsAddingLines(change))
|
||||
.filter(
|
||||
(change) =>
|
||||
LineContentToLineIdMapper.changeIsNotChangingLines(change) ||
|
||||
LineContentToLineIdMapper.changeIsAddingLines(change)
|
||||
)
|
||||
.reduce(
|
||||
(previousLineKeys, currentChange) => [...previousLineKeys, ...this.convertChangeToLinesWithIds(currentChange)],
|
||||
[] as LineWithId[]
|
||||
|
@ -94,7 +98,7 @@ export class LineIdMapper {
|
|||
* @return The created or reused {@link LineWithId lines with ids}
|
||||
*/
|
||||
private convertChangeToLinesWithIds(change: LineChange): LineWithId[] {
|
||||
if (LineIdMapper.changeIsAddingLines(change)) {
|
||||
if (LineContentToLineIdMapper.changeIsAddingLines(change)) {
|
||||
return change.value.map((line) => {
|
||||
this.lastUsedLineId += 1
|
||||
return { line: line, id: this.lastUsedLineId }
|
|
@ -3,8 +3,8 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NodeReplacement } from '../replace-components/component-replacer'
|
||||
import { ComponentReplacer, DO_NOT_REPLACE } from '../replace-components/component-replacer'
|
||||
import type { NodeReplacement } from '../../replace-components/component-replacer'
|
||||
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
|
||||
import { NodeToReactTransformer } from './node-to-react-transformer'
|
||||
import { Element } from 'domhandler'
|
||||
import type { ReactElement, ReactHTMLElement } from 'react'
|
|
@ -3,10 +3,14 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
|
||||
import type { LineWithId } from '../extensions/linemarker/types'
|
||||
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
|
||||
import { DO_NOT_REPLACE } from '../replace-components/component-replacer'
|
||||
import { LinemarkerMarkdownExtension } from '../../extensions/linemarker/linemarker-markdown-extension'
|
||||
import type { LineWithId } from '../../extensions/linemarker/types'
|
||||
import type {
|
||||
ComponentReplacer,
|
||||
NodeReplacement,
|
||||
ValidReactDomElement
|
||||
} from '../../replace-components/component-replacer'
|
||||
import { DO_NOT_REPLACE } from '../../replace-components/component-replacer'
|
||||
import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertNodeToReactElement'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import type { Element, Node } from 'domhandler'
|
|
@ -7,11 +7,11 @@ import type { SlideOptions } from '../../redux/note-details/types/slide-show-opt
|
|||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
import { RevealMarkdownExtension } from './extensions/reveal/reveal-markdown-extension'
|
||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
|
||||
import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
|
||||
import { LoadingSlide } from './loading-slide'
|
||||
import { MarkdownToReact } from './markdown-to-react/markdown-to-react'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
|
@ -25,10 +25,7 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
|
|||
* @param markdownContentLines The markdown lines
|
||||
* @param onFirstHeadingChange The callback to call if the first heading changes.
|
||||
* @param onLineMarkerPositionChanged The callback to call with changed {@link LineMarkers}
|
||||
* @param onTaskCheckedChange The callback to call if a task is checked or unchecked.
|
||||
* @param onTocChange The callback to call if the toc changes.
|
||||
* @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 slideOptions The {@link SlideOptions} to use
|
||||
*/
|
||||
|
@ -44,11 +41,9 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
|
||||
const extensions = useMarkdownExtensions(
|
||||
baseUrl,
|
||||
undefined,
|
||||
useMemo(() => [new RevealMarkdownExtension()], [])
|
||||
)
|
||||
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(markdownContentLines, extensions, newlinesAreBreaks, true)
|
||||
const revealStatus = useReveal(markdownContentLines, slideOptions)
|
||||
|
||||
const extractFirstHeadline = useExtractFirstHeadline(markdownBodyRef, onFirstHeadingChange)
|
||||
|
@ -59,8 +54,18 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
}, [extractFirstHeadline, markdownContentLines, revealStatus])
|
||||
|
||||
const slideShowDOM = useMemo(
|
||||
() => (revealStatus === REVEAL_STATUS.INITIALISED ? markdownReactDom : <LoadingSlide />),
|
||||
[markdownReactDom, revealStatus]
|
||||
() =>
|
||||
revealStatus === REVEAL_STATUS.INITIALISED ? (
|
||||
<MarkdownToReact
|
||||
markdownContentLines={markdownContentLines}
|
||||
markdownRenderExtensions={extensions}
|
||||
allowHtml={true}
|
||||
newlinesAreBreaks={newlinesAreBreaks}
|
||||
/>
|
||||
) : (
|
||||
<LoadingSlide />
|
||||
),
|
||||
[extensions, markdownContentLines, newlinesAreBreaks, revealStatus]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { StoreProvider } from '../../../redux/store-provider'
|
||||
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
|
||||
import { useConvertMarkdownToReactDom } from '../hooks/use-convert-markdown-to-react-dom'
|
||||
import { MarkdownToReact } from '../markdown-to-react/markdown-to-react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
export interface SimpleMarkdownRendererProps {
|
||||
|
@ -21,7 +21,14 @@ export interface SimpleMarkdownRendererProps {
|
|||
*/
|
||||
export const TestMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ content, extensions }) => {
|
||||
const lines = useMemo(() => content.split('\n'), [content])
|
||||
const dom = useConvertMarkdownToReactDom(lines, extensions, true, false)
|
||||
|
||||
return <StoreProvider>{dom}</StoreProvider>
|
||||
return (
|
||||
<StoreProvider>
|
||||
<MarkdownToReact
|
||||
markdownContentLines={lines}
|
||||
markdownRenderExtensions={extensions}
|
||||
newlinesAreBreaks={true}
|
||||
allowHtml={false}
|
||||
/>
|
||||
</StoreProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export const useDocumentSyncScrolling = (
|
|||
): [(lineMarkers: LineMarkerPosition[]) => void, React.UIEventHandler<HTMLElement>] => {
|
||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||
|
||||
const onLineMarkerPositionChanged = useCallback(
|
||||
const recalculateLineMarkerPositions = useCallback(
|
||||
(linkMarkerPositions: LineMarkerPosition[]) => {
|
||||
if (!outerContainerRef.current || !rendererRef.current) {
|
||||
return
|
||||
|
@ -51,5 +51,5 @@ export const useDocumentSyncScrolling = (
|
|||
const onUserScroll = useOnUserScroll(lineMarks, outerContainerRef, onScroll)
|
||||
useScrollToLineMark(scrollState, lineMarks, numberOfLines, outerContainerRef)
|
||||
|
||||
return useMemo(() => [onLineMarkerPositionChanged, onUserScroll], [onLineMarkerPositionChanged, onUserScroll])
|
||||
return useMemo(() => [recalculateLineMarkerPositions, onUserScroll], [recalculateLineMarkerPositions, onUserScroll])
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines])
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useDocumentSyncScrolling(
|
||||
const [recalculateLineMarkers, onUserScroll] = useDocumentSyncScrolling(
|
||||
internalDocumentRenderPaneRef,
|
||||
rendererRef,
|
||||
contentLineCount,
|
||||
|
@ -95,7 +95,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||
onLineMarkerPositionChanged={recalculateLineMarkers}
|
||||
baseUrl={baseUrl}
|
||||
newlinesAreBreaks={newlinesAreBreaks}
|
||||
/>
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import { sanitize } from 'dompurify'
|
||||
import { HtmlToReact } from '../../../../components/common/html-to-react/html-to-react'
|
||||
import type { HLJSApi } from 'highlight.js'
|
||||
import type { ReactElement } from 'react'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Highlights the given code using highlight.js. If the language wasn't recognized then it won't be highlighted.
|
||||
|
@ -38,7 +37,7 @@ export const useCodeDom = (code: string, hljs: HLJSApi | undefined, language?: s
|
|||
* @return the code represented as react elements
|
||||
*/
|
||||
const createHtmlLinesToReactDOM = (code: string[]): ReactElement[] => {
|
||||
return code.map((line, lineIndex) => <Fragment key={lineIndex}>{convertHtmlToReact(sanitize(line))}</Fragment>)
|
||||
return code.map((line, lineIndex) => <HtmlToReact htmlCode={line} key={lineIndex} />)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,10 +20,6 @@ import type Token from 'markdown-it/lib/token'
|
|||
* @see https://plantuml.com
|
||||
*/
|
||||
export class PlantumlMarkdownExtension extends MarkdownRendererExtension {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
private plantumlError(markdownIt: MarkdownIt): void {
|
||||
const defaultRenderer: Renderer.RenderRule = markdownIt.renderer.rules.fence || (() => '')
|
||||
markdownIt.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue