From 82472227f97aaa8ca0f05b9820302962fc3a30d1 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Fri, 18 Jun 2021 23:26:36 +0200 Subject: [PATCH] Replace react-html-parser with html-to-react (#1327) * Replace react-html-parser with html-to-react Signed-off-by: Tilman Vatteroth --- cypress/integration/autocompletion.spec.ts | 4 +- cypress/integration/emoji.spec.ts | 2 +- cypress/support/getMarkdownRenderer.ts | 18 ++-- package.json | 4 +- .../basic-markdown-renderer.tsx | 8 +- .../hooks/use-component-replacers.ts | 9 ++ .../use-convert-markdown-to-react-dom.ts | 29 ++++-- .../replace-components/ComponentReplacer.ts | 36 ++++++- .../replace-components/abc/abc-replacer.tsx | 11 ++- .../asciinema/asciinema-replacer.tsx | 7 +- .../colored-blockquote-replacer.tsx | 37 +++++--- .../replace-components/csv/csv-replacer.tsx | 9 +- .../flow/flowchart-replacer.tsx | 9 +- .../replace-components/gist/gist-replacer.tsx | 7 +- .../graphviz/graphviz-replacer.tsx | 11 ++- .../highlighted-code/highlighted-code.tsx | 6 +- .../highlighted-fence-replacer.tsx | 9 +- .../image/image-replacer.tsx | 7 +- .../katex/katex-replacer.tsx | 27 ++++-- .../linemarker/linemarker-replacer.tsx | 7 +- .../link-replacer/link-replacer.tsx | 15 ++- .../markmap/markmap-replacer.tsx | 11 ++- .../mermaid/mermaid-replacer.tsx | 11 ++- .../sequence-diagram-replacer.tsx | 12 ++- .../task-list/task-list-replacer.tsx | 7 +- .../replace-components/utils.ts | 7 +- .../vega-lite/vega-replacer.tsx | 11 ++- .../vimeo/vimeo-replacer.tsx | 7 +- .../youtube/youtube-replacer.tsx | 7 +- .../utils/html-react-transformer.tsx | 58 ++++++++---- yarn.lock | 93 ++++++++++--------- 31 files changed, 329 insertions(+), 167 deletions(-) diff --git a/cypress/integration/autocompletion.spec.ts b/cypress/integration/autocompletion.spec.ts index 8785e1ee9..9fe5cb55a 100644 --- a/cypress/integration/autocompletion.spec.ts +++ b/cypress/integration/autocompletion.spec.ts @@ -144,7 +144,7 @@ describe('Autocompletion', () => { .should('have.text', '# ') cy.getMarkdownBody() .find('h1 ') - .should('have.text', ' ') + .should('have.text', '\n ') }) it('via doubleclick', () => { cy.codemirrorFill('#') @@ -157,7 +157,7 @@ describe('Autocompletion', () => { .should('have.text', '# ') cy.getMarkdownBody() .find('h1') - .should('have.text', ' ') + .should('have.text', '\n ') }) }) diff --git a/cypress/integration/emoji.spec.ts b/cypress/integration/emoji.spec.ts index becd42f49..d59991d0c 100644 --- a/cypress/integration/emoji.spec.ts +++ b/cypress/integration/emoji.spec.ts @@ -6,7 +6,7 @@ describe('emojis', () => { - const HEDGEHOG_UNICODE_CHARACTER = 'šŸ¦”' + const HEDGEHOG_UNICODE_CHARACTER = '\nšŸ¦”\n' beforeEach(() => { cy.visitTestEditor() diff --git a/cypress/support/getMarkdownRenderer.ts b/cypress/support/getMarkdownRenderer.ts index 8e5c434d4..1b772a0bd 100644 --- a/cypress/support/getMarkdownRenderer.ts +++ b/cypress/support/getMarkdownRenderer.ts @@ -13,16 +13,16 @@ declare namespace Cypress { } Cypress.Commands.add('getMarkdownRenderer', () => { - return cy.get(`iframe[data-cy="documentIframe"]`) - .should('be.visible') - .its('0.contentDocument') - .should('exist') - .its('body') - .should('not.be.undefined') - .then(cy.wrap.bind(cy)) + return cy + .get(`iframe[data-cy="documentIframe"]`) + .should('be.visible') + .its('0.contentDocument') + .should('exist') + .its('body') + .should('not.be.undefined') + .then(cy.wrap.bind(cy)) }) Cypress.Commands.add('getMarkdownBody', () => { - return cy.getMarkdownRenderer() - .find('.markdown-body') + return cy.getMarkdownRenderer().find('.markdown-body') }) diff --git a/package.json b/package.json index 67d8c78c5..df69be317 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@craco/craco": "6.1.2", "@fontsource/source-sans-pro": "4.4.5", + "@hedgedoc/html-to-react": "1.0.0", "@hedgedoc/markdown-it-task-lists": "1.0.0", "@matejmazur/react-katex": "3.1.3", "@testing-library/jest-dom": "5.14.1", @@ -13,7 +14,6 @@ "@types/codemirror": "5.60.0", "@types/d3-graphviz": "2.6.6", "@types/diff": "5.0.0", - "@types/domhandler": "2.4.1", "@types/jest": "26.0.23", "@types/js-yaml": "4.0.1", "@types/luxon": "1.27.0", @@ -26,7 +26,6 @@ "@types/react": "17.0.11", "@types/react-bootstrap-typeahead": "5.1.5", "@types/react-dom": "17.0.7", - "@types/react-html-parser": "2.0.1", "@types/react-router": "5.1.15", "@types/react-router-bootstrap": "0.24.5", "@types/react-router-dom": "5.1.7", @@ -87,7 +86,6 @@ "react-codemirror2": "7.2.1", "react-diff-viewer": "3.1.1", "react-dom": "17.0.2", - "react-html-parser": "2.0.2", "react-i18next": "11.11.0", "react-redux": "7.2.4", "react-router": "5.2.0", diff --git a/src/components/markdown-renderer/basic-markdown-renderer.tsx b/src/components/markdown-renderer/basic-markdown-renderer.tsx index 46bb0e303..2beccb491 100644 --- a/src/components/markdown-renderer/basic-markdown-renderer.tsx +++ b/src/components/markdown-renderer/basic-markdown-renderer.tsx @@ -90,11 +90,15 @@ export const BasicMarkdownRenderer: React.FC baseReplacers().concat(additionalReplacers ? additionalReplacers() : []), + [additionalReplacers, baseReplacers] + ) + const markdownReactDom = useConvertMarkdownToReactDom( trimmedContent, markdownIt, - baseReplacers, - additionalReplacers, + replacers, clearFrontmatter, checkYamlErrorState ) diff --git a/src/components/markdown-renderer/hooks/use-component-replacers.ts b/src/components/markdown-renderer/hooks/use-component-replacers.ts index 86d6e9851..e515d72a4 100644 --- a/src/components/markdown-renderer/hooks/use-component-replacers.ts +++ b/src/components/markdown-renderer/hooks/use-component-replacers.ts @@ -26,6 +26,15 @@ 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' +/** + * Provides a function that creates a list of {@link ComponentReplacer component replacer} instances. + * + * @param onTaskCheckedChange A callback that gets executed if a task checkbox gets clicked + * @param onImageClick A callback that should be executed if an image gets clicked + * @param baseUrl The base url for relative links + * + * @return the created list + */ export const useComponentReplacers = ( onTaskCheckedChange?: TaskCheckedChangeHandler, onImageClick?: ImageClickHandler, 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 3f022d855..e8393a32b 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 @@ -5,21 +5,30 @@ */ 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 { useMemo, useRef } from 'react' +import { ComponentReplacer, ValidReactDomElement } from '../replace-components/ComponentReplacer' import { LineKeys } from '../types' import { buildTransformer } from '../utils/html-react-transformer' import { calculateNewLineNumberMapping } from '../utils/line-number-mapping' +import convertHtmlToReact from '@hedgedoc/html-to-react' +/** + * Renders markdown code into react elements + * + * @param markdownCode The markdown code that should be rendered + * @param markdownIt The configured {@link MarkdownIt markdown it} instance that should render the code + * @param replacers A function that provides a list of {@link ComponentReplacer component replacers} + * @param onBeforeRendering A callback that gets executed before the rendering + * @param onAfterRendering A callback that gets executed after the rendering + * @return The React DOM that represents the rendered markdown code + */ export const useConvertMarkdownToReactDom = ( markdownCode: string, markdownIt: MarkdownIt, - baseReplacers: () => ComponentReplacer[], - additionalReplacers?: () => ComponentReplacer[], + replacers: () => ComponentReplacer[], onBeforeRendering?: () => void, onAfterRendering?: () => void -): ReactElement[] => { +): ValidReactDomElement[] => { const oldMarkdownLineKeys = useRef() const lastUsedLineId = useRef(0) @@ -37,12 +46,12 @@ export const useConvertMarkdownToReactDom = ( oldMarkdownLineKeys.current = newLines lastUsedLineId.current = newLastUsedLineId - const replacers = baseReplacers().concat(additionalReplacers ? additionalReplacers() : []) - const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined - const rendering = ReactHtmlParser(html, { transform: transformer }) + const currentReplacers = replacers() + const transformer = currentReplacers.length > 0 ? buildTransformer(newLines, currentReplacers) : undefined + const rendering = convertHtmlToReact(html, { transform: transformer }) if (onAfterRendering) { onAfterRendering() } return rendering - }, [onBeforeRendering, markdownIt, markdownCode, baseReplacers, additionalReplacers, onAfterRendering]) + }, [onBeforeRendering, markdownIt, markdownCode, replacers, onAfterRendering]) } diff --git a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts b/src/components/markdown-renderer/replace-components/ComponentReplacer.ts index 17e1a252b..93c11b656 100644 --- a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts +++ b/src/components/markdown-renderer/replace-components/ComponentReplacer.ts @@ -4,20 +4,46 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element, isText, NodeWithChildren } from 'domhandler' import MarkdownIt from 'markdown-it' import { ReactElement } from 'react' -export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null +export type ValidReactDomElement = ReactElement | string | null -export type NativeRenderer = () => ReactElement +export type SubNodeTransform = (node: Element, subKey: number | string) => ValidReactDomElement | void + +export type NativeRenderer = () => ValidReactDomElement export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams +/** + * Base class for all component replacers. + * Component replacers detect structures in the HTML DOM from markdown it + * and replace them with some special react components. + */ export abstract class ComponentReplacer { + /** + * Extracts the content of the first text child node. + * + * @param node the node with the text node child + * @return the string content + */ + protected static extractTextChildContent(node: NodeWithChildren): string { + const childrenTextNode = node.children[0] + return isText(childrenTextNode) ? childrenTextNode.data : '' + } + + /** + * Checks if the current node should be altered or replaced and does if needed. + * + * @param node The current html dom node + * @param subNodeTransform should be used to convert child elements of the current node + * @param nativeRenderer renders the current node as it is without any replacement. + * @return the replacement for the current node or undefined if the current replacer replacer hasn't done anything. + */ public abstract getReplacement( - node: DomElement, + node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer - ): ReactElement | null | undefined + ): ValidReactDomElement | undefined } diff --git a/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx b/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx index 40476eb2b..9e93c46f5 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { AbcFrame } from './abc-frame' -export class AbcReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with "abc" as language and renders them as ABC.js + */ +export class AbcReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class AbcReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx b/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx index 3f4533f6d..a0fc14d37 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import MarkdownIt from 'markdown-it' import markdownItRegex from 'markdown-it-regex' import React from 'react' @@ -13,12 +13,15 @@ import { getAttributesFromHedgeDocTag } from '../utils' import { AsciinemaFrame } from './asciinema-frame' import { replaceAsciinemaLink } from './replace-asciinema-link' +/** + * Detects code blocks with "asciinema" as language and renders them Asciinema frame + */ export class AsciinemaReplacer extends ComponentReplacer { public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, replaceAsciinemaLink) } - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { const attributes = getAttributesFromHedgeDocTag(node, 'asciinema') if (attributes && attributes.id) { const asciinemaId = attributes.id diff --git a/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx b/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx index 780010c70..bcf319f6c 100644 --- a/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx @@ -4,45 +4,60 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' -import { ReactElement } from 'react' -import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer' +import { Element, isTag } from 'domhandler' +import { ComponentReplacer, NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../ComponentReplacer' -const isColorExtraElement = (node: DomElement | undefined): boolean => { +/** + * Checks if the given node is a blockquote color definition + * + * @param node The node to check + * @return true if the checked node is a blockquote color definition + */ +const isBlockquoteColorDefinition = (node: Element | undefined): boolean => { if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) { return false } return node.name === 'span' && node.attribs.class === 'quote-extra' } -const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined => { +/** + * Checks if any of the given nodes is the parent element of a color extra element. + * + * @param nodes The array of nodes to check + * @return the found element or undefined if no element was found + */ +const findBlockquoteColorParentElement = (nodes: Element[]): Element | undefined => { return nodes.find((child) => { if (child.name !== 'p' || !child.children || child.children.length < 1) { return false } - return child.children.find(isColorExtraElement) !== undefined + return child.children.filter(isTag).find(isBlockquoteColorDefinition) !== undefined }) } +/** + * Detects blockquotes and checks if they contain a color tag. + * If a color tag was found then the color will be applied to the node as border. + */ export class ColoredBlockquoteReplacer extends ComponentReplacer { public getReplacement( - node: DomElement, + node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer - ): ReactElement | undefined { + ): ValidReactDomElement | undefined { if (node.name !== 'blockquote' || !node.children || node.children.length < 1) { return } - const paragraph = findQuoteOptionsParent(node.children) + const paragraph = findBlockquoteColorParentElement(node.children.filter(isTag)) if (!paragraph) { return } const childElements = paragraph.children || [] - const optionsTag = childElements.find(isColorExtraElement) + const optionsTag = childElements.filter(isTag).find(isBlockquoteColorDefinition) if (!optionsTag) { return } - paragraph.children = childElements.filter((elem) => !isColorExtraElement(elem)) + paragraph.children = childElements.filter((elem) => !isTag(elem) || !isBlockquoteColorDefinition(elem)) const attributes = optionsTag.attribs if (!attributes || !attributes['data-color']) { return diff --git a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx index 94ca81078..ca24c2940 100644 --- a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { CsvTable } from './csv-table' +/** + * Detects code blocks with "csv" as language and renders them as table. + */ export class CsvReplacer extends ComponentReplacer { - public getReplacement(codeNode: DomElement): React.ReactElement | undefined { + public getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class CsvReplacer extends ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) const extraData = codeNode.attribs['data-extra'] const extraRegex = /\s*(delimiter=([^\s]*))?\s*(header)?/ diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx index 734a80a29..8160d9953 100644 --- a/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { FlowChart } from './flowchart/flowchart' +/** + * Detects code blocks with "flow" as language and renders them as flow chart. + */ export class FlowchartReplacer extends ComponentReplacer { - public getReplacement(codeNode: DomElement): React.ReactElement | undefined { + public getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class FlowchartReplacer extends ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx b/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx index 1486642e6..22f1f1a99 100644 --- a/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import MarkdownIt from 'markdown-it' import markdownItRegex from 'markdown-it-regex' import React from 'react' @@ -16,13 +16,16 @@ import preview from './gist-preview.png' import { replaceGistLink } from './replace-gist-link' import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code' +/** + * Detects "app-gist" tags and renders them as gist frames. + */ export class GistReplacer extends ComponentReplacer { public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, replaceGistLink) markdownItRegex(markdownIt, replaceLegacyGistShortCode) } - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { const attributes = getAttributesFromHedgeDocTag(node, 'gist') if (attributes && attributes.id) { const gistId = attributes.id diff --git a/src/components/markdown-renderer/replace-components/graphviz/graphviz-replacer.tsx b/src/components/markdown-renderer/replace-components/graphviz/graphviz-replacer.tsx index f85d55978..ed7cd6304 100644 --- a/src/components/markdown-renderer/replace-components/graphviz/graphviz-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/graphviz/graphviz-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { GraphvizFrame } from './graphviz-frame' -export class GraphvizReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with "graphviz" as language and renders them as graphviz graph. + */ +export class GraphvizReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class GraphvizReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx index cbe26c194..f3641fd74 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment, ReactElement, useEffect, useState } from 'react' -import ReactHtmlParser from 'react-html-parser' +import convertHtmlToReact from '@hedgedoc/html-to-react' import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button' import '../../../utils/button-inside.scss' import './highlighted-code.scss' @@ -29,11 +29,11 @@ const escapeHtml = (unsafe: string): string => { .replaceAll(/'/g, ''') } -const replaceCode = (code: string): ReactElement[][] => { +const replaceCode = (code: string): (ReactElement | null | string)[][] => { return code .split('\n') .filter((line) => !!line) - .map((line) => ReactHtmlParser(line)) + .map((line) => convertHtmlToReact(line, {})) } export const HighlightedCode: React.FC = ({ code, language, startLineNumber, wrapLines }) => { diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx index a15fe2c86..3d9bf5409 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx @@ -4,15 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { HighlightedCode } from './highlighted-code/highlighted-code' +/** + * Detects code blocks and renders them as highlighted code blocks + */ export class HighlightedCodeReplacer extends ComponentReplacer { private lastLineNumber = 0 - public getReplacement(codeNode: DomElement): React.ReactElement | undefined { + public getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -39,7 +42,7 @@ export class HighlightedCodeReplacer extends ComponentReplacer { const startLineNumber = startLineNumberAttribute === '+' ? this.lastLineNumber : parseInt(startLineNumberAttribute) || 1 - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) if (showLineNumbers) { this.lastLineNumber = startLineNumber + code.split('\n').filter((line) => !!line).length diff --git a/src/components/markdown-renderer/replace-components/image/image-replacer.tsx b/src/components/markdown-renderer/replace-components/image/image-replacer.tsx index d4fe5a7c8..7524c9446 100644 --- a/src/components/markdown-renderer/replace-components/image/image-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/image/image-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { ProxyImageFrame } from './proxy-image-frame' export type ImageClickHandler = (event: React.MouseEvent) => void +/** + * Detects image tags and loads them via image proxy if configured. + */ export class ImageReplacer extends ComponentReplacer { private readonly clickHandler?: ImageClickHandler @@ -19,7 +22,7 @@ export class ImageReplacer extends ComponentReplacer { this.clickHandler = clickHandler } - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { if (node.name === 'img' && node.attribs) { return ( { +/** + * Checks if the given node is a KaTeX block. + * + * @param node the node to check + * @return The given node if it is a KaTeX block element, undefined otherwise. + */ +const getNodeIfKatexBlock = (node: Element): Element | undefined => { if (node.name !== 'p' || !node.children || node.children.length === 0) { return } - return node.children.find((subnode) => { + return node.children.filter(isTag).find((subnode) => { return subnode.name === 'app-katex' && subnode.attribs?.inline === undefined }) } -const getNodeIfInlineKatex = (node: DomElement): DomElement | undefined => { +/** + * Checks if the given node is a KaTeX inline element. + * + * @param node the node to check + * @return The given node if it is a KaTeX inline element, undefined otherwise. + */ +const getNodeIfInlineKatex = (node: Element): Element | undefined => { return node.name === 'app-katex' && node.attribs?.inline !== undefined ? node : undefined } const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex')) +/** + * Detects LaTeX syntax and renders it with KaTeX. + */ export class KatexReplacer extends ComponentReplacer { public static readonly markdownItPlugin: MarkdownIt.PluginSimple = mathJax({ beforeMath: '', @@ -36,10 +51,10 @@ export class KatexReplacer extends ComponentReplacer { afterDisplayMath: '' }) - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node) if (katex?.children && katex.children[0]) { - const mathJaxContent = katex.children[0]?.data as string + const mathJaxContent = ComponentReplacer.extractTextChildContent(katex) const isInline = katex.attribs?.inline !== undefined return } diff --git a/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx b/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx index 594ce952b..feee334d3 100644 --- a/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx @@ -4,11 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import { ComponentReplacer } from '../ComponentReplacer' +/** + * Detects line markers and suppresses them in the resulting DOM. + */ export class LinemarkerReplacer extends ComponentReplacer { - public getReplacement(codeNode: DomElement): null | undefined { + public getReplacement(codeNode: Element): null | undefined { return codeNode.name === 'app-linemarker' ? null : undefined } } diff --git a/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx b/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx index 55a6e44e6..74e06ed00 100644 --- a/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' -import React, { ReactElement } from 'react' -import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer' +import { Element } from 'domhandler' +import React from 'react' +import { ComponentReplacer, NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../ComponentReplacer' export const createJumpToMarkClickEventHandler = (id: string) => { return (event: React.MouseEvent): void => { @@ -14,16 +14,21 @@ export const createJumpToMarkClickEventHandler = (id: string) => { } } +/** + * Detects link tags and polishs them. + * This replacer prevents data and javascript links, + * extends relative links with the base url and creates working jump links. + */ export class LinkReplacer extends ComponentReplacer { constructor(private baseUrl?: string) { super() } public getReplacement( - node: DomElement, + node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer - ): ReactElement | null | undefined { + ): ValidReactDomElement | undefined { if (node.name !== 'a' || !node.attribs || !node.attribs.href) { return undefined } diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-replacer.tsx b/src/components/markdown-renderer/replace-components/markmap/markmap-replacer.tsx index 177a59c88..aad085c37 100644 --- a/src/components/markdown-renderer/replace-components/markmap/markmap-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/markmap/markmap-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { MarkmapFrame } from './markmap-frame' -export class MarkmapReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with 'markmap' as language and renders them with Markmap. + */ +export class MarkmapReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class MarkmapReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/mermaid/mermaid-replacer.tsx b/src/components/markdown-renderer/replace-components/mermaid/mermaid-replacer.tsx index e5f144d3c..018edc53e 100644 --- a/src/components/markdown-renderer/replace-components/mermaid/mermaid-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/mermaid/mermaid-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { MermaidChart } from './mermaid-chart' -export class MermaidReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with 'mermaid' as language and renders them with mermaid. + */ +export class MermaidReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class MermaidReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram-replacer.tsx b/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram-replacer.tsx index e4abf2afe..47d374db4 100644 --- a/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram-replacer.tsx @@ -4,14 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React, { Fragment } from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { MermaidChart } from '../mermaid/mermaid-chart' import { DeprecationWarning } from './deprecation-warning' -export class SequenceDiagramReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with 'sequence' as language and renders them as + * sequence diagram with mermaid. + */ +export class SequenceDiagramReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -23,7 +27,7 @@ export class SequenceDiagramReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return ( diff --git a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx index 2e1b290d3..a0a8dcb3a 100644 --- a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx @@ -4,12 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React, { ReactElement } from 'react' import { ComponentReplacer } from '../ComponentReplacer' export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void +/** + * Detects task lists and renders them as checkboxes that execute a callback if clicked. + */ export class TaskListReplacer extends ComponentReplacer { onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void @@ -25,7 +28,7 @@ export class TaskListReplacer extends ComponentReplacer { } } - public getReplacement(node: DomElement): ReactElement | undefined { + public getReplacement(node: Element): ReactElement | undefined { if (node.attribs?.class !== 'task-list-item-checkbox') { return } diff --git a/src/components/markdown-renderer/replace-components/utils.ts b/src/components/markdown-renderer/replace-components/utils.ts index fae9d1236..6b290c77d 100644 --- a/src/components/markdown-renderer/replace-components/utils.ts +++ b/src/components/markdown-renderer/replace-components/utils.ts @@ -4,12 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' -export const getAttributesFromHedgeDocTag = ( - node: DomElement, - tagName: string -): { [s: string]: string } | undefined => { +export const getAttributesFromHedgeDocTag = (node: Element, tagName: string): { [s: string]: string } | undefined => { if (node.name !== `app-${tagName}` || !node.attribs) { return } diff --git a/src/components/markdown-renderer/replace-components/vega-lite/vega-replacer.tsx b/src/components/markdown-renderer/replace-components/vega-lite/vega-replacer.tsx index 96bdc22ed..ba701e489 100644 --- a/src/components/markdown-renderer/replace-components/vega-lite/vega-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/vega-lite/vega-replacer.tsx @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import { VegaChart } from './vega-chart' -export class VegaReplacer implements ComponentReplacer { - getReplacement(codeNode: DomElement): React.ReactElement | undefined { +/** + * Detects code blocks with 'vega-lite' as language and renders them with Vega. + */ +export class VegaReplacer extends ComponentReplacer { + getReplacement(codeNode: Element): React.ReactElement | undefined { if ( codeNode.name !== 'code' || !codeNode.attribs || @@ -22,7 +25,7 @@ export class VegaReplacer implements ComponentReplacer { return } - const code = codeNode.children[0].data as string + const code = ComponentReplacer.extractTextChildContent(codeNode) return } diff --git a/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx b/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx index a09336462..abd47c965 100644 --- a/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import MarkdownIt from 'markdown-it' import markdownItRegex from 'markdown-it-regex' import React from 'react' @@ -14,13 +14,16 @@ import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code' import { replaceVimeoLink } from './replace-vimeo-link' import { VimeoFrame } from './vimeo-frame' +/** + * Detects 'app-vimeo' tags and renders them as vimeo embedding. + */ export class VimeoReplacer extends ComponentReplacer { public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, replaceVimeoLink) markdownItRegex(markdownIt, replaceLegacyVimeoShortCode) } - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { const attributes = getAttributesFromHedgeDocTag(node, 'vimeo') if (attributes && attributes.id) { const videoId = attributes.id diff --git a/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx b/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx index 6b1e7c87d..fff2d8031 100644 --- a/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { DomElement } from 'domhandler' +import { Element } from 'domhandler' import MarkdownIt from 'markdown-it' import markdownItRegex from 'markdown-it-regex' import React from 'react' @@ -14,13 +14,16 @@ import { replaceLegacyYoutubeShortCode } from './replace-legacy-youtube-short-co import { replaceYouTubeLink } from './replace-youtube-link' import { YouTubeFrame } from './youtube-frame' +/** + * Detects 'app-youtube' tags and renders them as youtube embedding. + */ export class YoutubeReplacer extends ComponentReplacer { public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, replaceYouTubeLink) markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode) } - public getReplacement(node: DomElement): React.ReactElement | undefined { + public getReplacement(node: Element): React.ReactElement | undefined { const attributes = getAttributesFromHedgeDocTag(node, 'youtube') if (attributes && attributes.id) { const videoId = attributes.id diff --git a/src/components/markdown-renderer/utils/html-react-transformer.tsx b/src/components/markdown-renderer/utils/html-react-transformer.tsx index 8097a3701..3a60a234e 100644 --- a/src/components/markdown-renderer/utils/html-react-transformer.tsx +++ b/src/components/markdown-renderer/utils/html-react-transformer.tsx @@ -4,18 +4,24 @@ * 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, NativeRenderer, SubNodeTransform } from '../replace-components/ComponentReplacer' +import { Element, isTag } from 'domhandler' +import React, { Suspense } from 'react' +import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertNodeToReactElement' +import { + ComponentReplacer, + NativeRenderer, + SubNodeTransform, + ValidReactDomElement +} from '../replace-components/ComponentReplacer' import { LineKeys } from '../types' +import { NodeToReactElementTransformer } from '@hedgedoc/html-to-react/dist/NodeToReactElementTransformer' export interface TextDifferenceResult { lines: LineKeys[] lastUsedLineId: number } -export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string | undefined => { +export const calculateKeyFromLineMarker = (node: Element, lineKeys?: LineKeys[]): string | undefined => { if (!node.attribs || lineKeys === undefined) { return } @@ -26,7 +32,7 @@ export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys } const lineMarker = node.prev - if (!lineMarker || !lineMarker.attribs) { + if (!lineMarker || !isTag(lineMarker) || !lineMarker.attribs) { return } @@ -48,29 +54,49 @@ export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys } export const findNodeReplacement = ( - node: DomElement, + node: Element, allReplacers: ComponentReplacer[], subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer -): ReactElement | null | undefined => { - return allReplacers - .map((componentReplacer) => componentReplacer.getReplacement(node, subNodeTransform, nativeRenderer)) - .find((replacement) => replacement !== undefined) +): ValidReactDomElement | undefined => { + for (const componentReplacer of allReplacers) { + const replacement = componentReplacer.getReplacement(node, subNodeTransform, nativeRenderer) + if (replacement !== undefined) { + return replacement + } + } } -export const renderNativeNode = (node: DomElement, key: string, transform: Transform): ReactElement => { +/** + * Renders the given node without any replacement + * + * @param node The node to render + * @param key The unique key for the node + * @param transform The transform function that should be applied to the child nodes + */ +export const renderNativeNode = ( + node: Element, + key: string, + transform: NodeToReactElementTransformer +): ValidReactDomElement => { if (node.attribs === undefined) { node.attribs = {} } delete node.attribs['data-key'] - return convertNodeToElement(node, key as unknown as number, transform) + return convertNodeToReactElement(node, key, transform) } -export const buildTransformer = (lineKeys: LineKeys[] | undefined, allReplacers: ComponentReplacer[]): Transform => { - const transform: Transform = (node, index) => { +export const buildTransformer = ( + lineKeys: LineKeys[] | undefined, + allReplacers: ComponentReplacer[] +): NodeToReactElementTransformer => { + const transform: NodeToReactElementTransformer = (node, index) => { + if (!isTag(node)) { + return convertNodeToReactElement(node, index) + } const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform) - const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform) + const subNodeTransform: SubNodeTransform = (subNode, subKey) => transform(subNode, subKey, transform) const key = calculateKeyFromLineMarker(node, lineKeys) ?? (-index).toString() const tryReplacement = findNodeReplacement(node, allReplacers, subNodeTransform, nativeRenderer) diff --git a/yarn.lock b/yarn.lock index bfe9281e9..20486abef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1402,6 +1402,16 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hedgedoc/html-to-react@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@hedgedoc/html-to-react/-/html-to-react-1.0.0.tgz#275233189392addc9d2db4d943edd84eec618edf" + integrity sha512-bsEHYBPjj8MoSYSCuIwiMh+SOjgIVuRTYbJVpzJ8u3oxaJCAtysgrfsot7juRCClqQyprqAHmNT7f9lXBVXZtw== + dependencies: + "@types/react" "^17.0.11" + htmlparser2 "^6.1.0" + react "^17.0.2" + react-dom "^17.0.2" + "@hedgedoc/markdown-it-task-lists@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@hedgedoc/markdown-it-task-lists/-/markdown-it-task-lists-1.0.0.tgz#6514c411fb582d3de58ded393a3c4f69ba762028" @@ -2197,18 +2207,6 @@ resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.0.tgz#eb71e94feae62548282c4889308a3dfb57e36020" integrity sha512-jrm2K65CokCCX4NmowtA+MfXyuprZC13jbRuwprs6/04z/EcFg/MCwYdsHn+zgV4CQBiATiI7AEq7y1sZCtWKA== -"@types/domhandler@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf" - integrity sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA== - -"@types/domutils@*": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@types/domutils/-/domutils-1.7.3.tgz#890de5a79d86896e9e6a7445a43c9512e62d6f04" - integrity sha512-EucnS75OnnEdypNt+UpARisSF8eJBq4no+aVOis3Bs5kyABDXm1hEDv6jJxcMJPjR+a2YCrEANaW+BMT2QVG2Q== - dependencies: - domhandler "^2.4.0" - "@types/eslint@^7.2.6": version "7.2.13" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53" @@ -2277,15 +2275,6 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== -"@types/htmlparser2@*": - version "3.10.2" - resolved "https://registry.yarnpkg.com/@types/htmlparser2/-/htmlparser2-3.10.2.tgz#bd43702eaf2f15c2d26784c8427352a0a4aa75eb" - integrity sha512-81vjuO800UMoHjYbCbqtBmfC3iCsrROKpqndo0acKiN6k/cpW+YOw9FzRP0ghujHeUNCOox2AQPrrMy6+j5UpQ== - dependencies: - "@types/domutils" "*" - "@types/node" "*" - domhandler "^2.4.0" - "@types/invariant@^2.2.33": version "2.2.34" resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe" @@ -2456,14 +2445,6 @@ dependencies: "@types/react" "*" -"@types/react-html-parser@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/react-html-parser/-/react-html-parser-2.0.1.tgz#2d9002ac5bf1adf9aff8eae77ace5488bd78c98d" - integrity sha512-Lyw0AtG3gahw78CX2pzmzhKaoZCfJNzzuhhPsFVhzFrylMv8NaCmzYaPKglMv3RRHpwBbHuMOkVx0HiwGZKgSA== - dependencies: - "@types/htmlparser2" "*" - "@types/react" "*" - "@types/react-redux@^7.1.16": version "7.1.16" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" @@ -2523,7 +2504,7 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@17.0.11": +"@types/react@17.0.11", "@types/react@^17.0.11": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== @@ -5978,6 +5959,15 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -5988,7 +5978,7 @@ domelementtype@1, domelementtype@^1.3.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1: +domelementtype@^2.0.1, domelementtype@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== @@ -6000,13 +5990,20 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^2.3.0, domhandler@^2.4.0: +domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== dependencies: domelementtype "1" +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== + dependencies: + domelementtype "^2.2.0" + domutils@^1.5.1, domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -6015,6 +6012,15 @@ domutils@^1.5.1, domutils@^1.7.0: dom-serializer "0" domelementtype "1" +domutils@^2.5.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" + integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -7702,7 +7708,7 @@ html-webpack-plugin@4.5.0: tapable "^1.1.3" util.promisify "1.0.0" -htmlparser2@^3.10.1, htmlparser2@^3.9.0: +htmlparser2@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -7714,6 +7720,16 @@ htmlparser2@^3.10.1, htmlparser2@^3.9.0: inherits "^2.0.1" readable-stream "^3.1.1" +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -11904,7 +11920,7 @@ react-diff-viewer@3.1.1: memoize-one "^5.0.4" prop-types "^15.6.2" -react-dom@17.0.2: +react-dom@17.0.2, react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -11918,13 +11934,6 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== -react-html-parser@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/react-html-parser/-/react-html-parser-2.0.2.tgz#6dbe1ddd2cebc1b34ca15215158021db5fc5685e" - integrity sha512-XeerLwCVjTs3njZcgCOeDUqLgNIt/t+6Jgi5/qPsO/krUWl76kWKXMeVs2LhY2gwM6X378DkhLjur0zUQdpz0g== - dependencies: - htmlparser2 "^3.9.0" - react-i18next@11.11.0: version "11.11.0" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.11.0.tgz#2f7c6cb4f81f94d1728a02d60e4bb5216709f942" @@ -12150,7 +12159,7 @@ react-use@17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" -react@17.0.2: +react@17.0.2, react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==