diff --git a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts index 6de430162..b4d03e84e 100644 --- a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts +++ b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts @@ -6,17 +6,15 @@ import MarkdownIt from 'markdown-it/lib' import { useMemo } from 'react' -import type { ComponentReplacer, ValidReactDomElement } from '../replace-components/component-replacer' +import type { ValidReactDomElement } from '../replace-components/component-replacer' import convertHtmlToReact from '@hedgedoc/html-to-react' import { NodeToReactTransformer } from '../utils/node-to-react-transformer' import { LineIdMapper } from '../utils/line-id-mapper' import type { MarkdownExtension } from '../markdown-extension/markdown-extension' -import type { NodeProcessor } from '../node-preprocessors/node-processor' -import type { Document } from 'domhandler' -import { SanitizerMarkdownExtension } from '../markdown-extension/sanitizer/sanitizer-markdown-extension' +import { MarkdownExtensionCollection } from '../markdown-extension/markdown-extension-collection' /** - * Renders markdown code into react elements + * Renders Markdown-Code into react elements * * @param markdownContentLines The markdown code lines that should be rendered * @param additionalMarkdownExtensions A list of {@link MarkdownExtension markdown extensions} that should be used @@ -32,13 +30,10 @@ export const useConvertMarkdownToReactDom = ( ): ValidReactDomElement[] => { const lineNumberMapper = useMemo(() => new LineIdMapper(), []) const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), []) - const markdownExtensions = useMemo(() => { - const tagNameWhiteList = additionalMarkdownExtensions.reduce( - (state, extension) => [...state, ...extension.buildTagNameWhitelist()], - [] as string[] - ) - return [...additionalMarkdownExtensions, new SanitizerMarkdownExtension(tagNameWhiteList)] - }, [additionalMarkdownExtensions]) + const markdownExtensions = useMemo( + () => new MarkdownExtensionCollection(additionalMarkdownExtensions), + [additionalMarkdownExtensions] + ) const markdownIt = useMemo(() => { const newMarkdownIt = new MarkdownIt('default', { @@ -47,35 +42,19 @@ export const useConvertMarkdownToReactDom = ( langPrefix: '', typographer: true }) - markdownExtensions.forEach((extension) => - newMarkdownIt.use((markdownIt) => extension.configureMarkdownIt(markdownIt)) - ) - markdownExtensions.forEach((extension) => - newMarkdownIt.use((markdownIt) => extension.configureMarkdownItPost(markdownIt)) - ) + markdownExtensions.configureMarkdownIt(newMarkdownIt) return newMarkdownIt }, [allowHtml, markdownExtensions, newlinesAreBreaks]) useMemo(() => { - const replacers = markdownExtensions.reduce( - (state, extension) => [...state, ...extension.buildReplacers()], - [] as ComponentReplacer[] - ) - htmlToReactTransformer.setReplacers(replacers) + htmlToReactTransformer.setReplacers(markdownExtensions.buildReplacers()) }, [htmlToReactTransformer, markdownExtensions]) useMemo(() => { htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownContentLines)) }, [htmlToReactTransformer, lineNumberMapper, markdownContentLines]) - const nodePreProcessor = useMemo(() => { - return markdownExtensions - .reduce((state, extension) => [...state, ...extension.buildNodeProcessors()], [] as NodeProcessor[]) - .reduce( - (state, processor) => (document: Document) => state(processor.process(document)), - (document: Document) => document - ) - }, [markdownExtensions]) + const nodePreProcessor = useMemo(() => markdownExtensions.buildFlatNodeProcessor(), [markdownExtensions]) return useMemo(() => { const html = markdownIt.render(markdownContentLines.join('\n')) diff --git a/src/components/markdown-renderer/markdown-extension/markdown-extension-collection.ts b/src/components/markdown-renderer/markdown-extension/markdown-extension-collection.ts new file mode 100644 index 000000000..b99ab5fd0 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/markdown-extension-collection.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { MarkdownExtension } from './markdown-extension' +import type { ComponentReplacer } from '../replace-components/component-replacer' +import type { Document } from 'domhandler' +import type MarkdownIt from 'markdown-it' +import { SanitizerMarkdownExtension } from './sanitizer/sanitizer-markdown-extension' + +/** + * Contains multiple {@link MarkdownExtension} and uses them to configure parts of the renderer. + */ +export class MarkdownExtensionCollection { + private extensions: MarkdownExtension[] + + public constructor(additionalExtensions: MarkdownExtension[]) { + const tagWhiteLists = additionalExtensions.flatMap((extension) => extension.buildTagNameWhitelist()) + + this.extensions = [...additionalExtensions, new SanitizerMarkdownExtension(tagWhiteLists)] + } + + /** + * Configures the given {@link MarkdownIt markdown-it instance} using every saved {@link MarkdownExtension extension}. + * + * @param markdownIt The markdown-it instance to configure. + */ + public configureMarkdownIt(markdownIt: MarkdownIt): void { + this.extensions.forEach((extension) => markdownIt.use((markdownIt) => extension.configureMarkdownIt(markdownIt))) + this.extensions.forEach((extension) => + markdownIt.use((markdownIt) => extension.configureMarkdownItPost(markdownIt)) + ) + } + + /** + * Creates a node processor that applies the node processor of every saved {@link MarkdownExtension extension}. + * + * @return the created node processor function + */ + public buildFlatNodeProcessor(): (document: Document) => Document { + return this.extensions + .flatMap((extension) => extension.buildNodeProcessors()) + .reduce( + (state, processor) => (document: Document) => state(processor.process(document)), + (document: Document) => document + ) + } + + /** + * Collects all {@link ComponentReplacer component replacers} from all saved {@link MarkdownExtension extension}. + */ + public buildReplacers(): ComponentReplacer[] { + return this.extensions.flatMap((extension) => extension.buildReplacers()) + } +}