fix(component replacer): Use symbol for DO_NOT_REPLACE instead of undefined

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-09-15 17:52:22 +02:00
parent 22cf68d10c
commit 7a9951e351
16 changed files with 114 additions and 116 deletions

View file

@ -22,22 +22,20 @@ import { Optional } from '@mrdrogdrog/optional'
*/
export class BlockquoteColorExtraTagReplacer extends ComponentReplacer {
replace(element: Element): NodeReplacement {
if (
element.tagName === BlockquoteExtraTagMarkdownExtension.tagName &&
element.attribs?.['data-label'] === 'color' &&
element.children !== undefined
) {
let index = 0
return Optional.ofNullable(element.children[0])
.filter(isText)
.map((child) => (child as Text).data)
.filter((content) => cssColor.test(content))
.map((color) => (
<span className={'blockquote-extra'} key={(index += 1)} style={{ color: color }}>
<ForkAwesomeIcon key='icon' className={'mx-1'} icon={'tag'} />
</span>
))
.orElse(DO_NOT_REPLACE)
}
return Optional.of(element)
.filter(
(element) =>
element.tagName === BlockquoteExtraTagMarkdownExtension.tagName && element.attribs?.['data-label'] === 'color'
)
.map((element) => element.children[0])
.filter(isText)
.map((child) => (child as Text).data)
.filter((content) => cssColor.test(content))
.map((color) => (
<span className={'blockquote-extra'} key={1} style={{ color: color }}>
<ForkAwesomeIcon key='icon' className={'mx-1'} icon={'tag'} />
</span>
))
.orElse(DO_NOT_REPLACE)
}
}

View file

@ -23,16 +23,17 @@ import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-mark
*/
export class BlockquoteExtraTagReplacer extends ComponentReplacer {
replace(element: Element, subNodeTransform: SubNodeTransform): NodeReplacement {
if (element.tagName !== BlockquoteExtraTagMarkdownExtension.tagName || !element.attribs) {
return DO_NOT_REPLACE
}
return (
<span className={'blockquote-extra'}>
{this.buildIconElement(element)}
{BlockquoteExtraTagReplacer.transformChildren(element, subNodeTransform)}
</span>
)
return Optional.of(element)
.filter(
(element) => element.tagName === BlockquoteExtraTagMarkdownExtension.tagName && element.attribs !== undefined
)
.map((element) => (
<span className={'blockquote-extra'} key={1}>
{this.buildIconElement(element)}
{BlockquoteExtraTagReplacer.transformChildren(element, subNodeTransform)}
</span>
))
.orElse(DO_NOT_REPLACE)
}
/**

View file

@ -1,12 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { CsvTable } from './csv-table'
import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer'
@ -14,10 +15,10 @@ import { CodeBlockComponentReplacer } from '../../replace-components/code-block-
* Detects code blocks with "csv" as language and renders them as table.
*/
export class CsvReplacer extends ComponentReplacer {
public replace(codeNode: Element): React.ReactElement | undefined {
public replace(codeNode: Element): NodeReplacement {
const code = CodeBlockComponentReplacer.extractTextFromCodeNode(codeNode, 'csv')
if (!code) {
return
return DO_NOT_REPLACE
}
const extraData = codeNode.attribs['data-extra']

View file

@ -1,12 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { HighlightedCode } from './highlighted-code'
/**
@ -21,10 +22,10 @@ export class HighlightedCodeReplacer extends ComponentReplacer {
: undefined
}
public replace(codeNode: Element): React.ReactElement | undefined {
public replace(codeNode: Element): NodeReplacement {
const code = HighlightedCodeReplacer.extractCode(codeNode)
if (!code) {
return
return DO_NOT_REPLACE
}
const language = codeNode.attribs['data-highlight-language']

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -16,17 +16,15 @@ import { ClickShield } from '../../replace-components/click-shield/click-shield'
*/
export class IframeCapsuleReplacer extends ComponentReplacer {
replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement {
if (node.name === 'iframe') {
return (
<ClickShield
hoverIcon={'globe'}
targetDescription={node.attribs.src}
data-cypress-id={'iframe-capsule-click-shield'}>
{nativeRenderer()}
</ClickShield>
)
} else {
return DO_NOT_REPLACE
}
return node.name !== 'iframe' ? (
DO_NOT_REPLACE
) : (
<ClickShield
hoverIcon={'globe'}
targetDescription={node.attribs.src}
data-cypress-id={'iframe-capsule-click-shield'}>
{nativeRenderer()}
</ClickShield>
)
}
}

View file

@ -1,12 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { ProxyImageFrame } from './proxy-image-frame'
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void
@ -22,20 +23,20 @@ export class ProxyImageReplacer extends ComponentReplacer {
this.clickHandler = clickHandler
}
public replace(node: Element): React.ReactElement | undefined {
if (node.name === 'img') {
return (
<ProxyImageFrame
id={node.attribs.id}
className={`${node.attribs.class} cursor-zoom-in`}
src={node.attribs.src}
alt={node.attribs.alt}
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
onClick={this.clickHandler}
/>
)
}
public replace(node: Element): NodeReplacement {
return node.name !== 'img' ? (
DO_NOT_REPLACE
) : (
<ProxyImageFrame
id={node.attribs.id}
className={`${node.attribs.class} cursor-zoom-in`}
src={node.attribs.src}
alt={node.attribs.alt}
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
onClick={this.clickHandler}
/>
)
}
}

View file

@ -7,6 +7,7 @@
import type { Element } from 'domhandler'
import { isTag } from 'domhandler'
import React from 'react'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { KatexMarkdownExtension } from './katex-markdown-extension'
import { Optional } from '@mrdrogdrog/optional'
@ -17,7 +18,7 @@ const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ './katex-f
* Detects LaTeX syntax and renders it with KaTeX.
*/
export class KatexReplacer extends ComponentReplacer {
public replace(node: Element): React.ReactElement | undefined {
public replace(node: Element): NodeReplacement {
return this.extractKatexContent(node)
.map((latexContent) => {
const isInline = !!node.attribs?.['data-inline']

View file

@ -1,18 +1,19 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { LinemarkerMarkdownExtension } from './linemarker-markdown-extension'
/**
* Detects line markers and suppresses them in the resulting DOM.
*/
export class LinemarkerReplacer extends ComponentReplacer {
public replace(codeNode: Element): null | undefined {
return codeNode.name === LinemarkerMarkdownExtension.tagName ? null : undefined
public replace(codeNode: Element): NodeReplacement {
return codeNode.name === LinemarkerMarkdownExtension.tagName ? null : DO_NOT_REPLACE
}
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -22,10 +22,10 @@ export class JumpAnchorReplacer extends ComponentReplacer {
const jumpId = node.attribs['data-jump-target-id']
delete node.attribs['data-jump-target-id']
const replacement = nativeRenderer()
if (replacement === null || typeof replacement === 'string') {
return replacement
} else {
return <JumpAnchor {...(replacement.props as AllHTMLAttributes<HTMLAnchorElement>)} jumpTargetId={jumpId} />
}
return replacement === null || typeof replacement === 'string' ? (
replacement
) : (
<JumpAnchor {...(replacement.props as AllHTMLAttributes<HTMLAnchorElement>)} jumpTargetId={jumpId} />
)
}
}

View file

@ -1,13 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Element } from 'domhandler'
import type { ReactElement } from 'react'
import React from 'react'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { TaskListCheckbox } from './task-list-checkbox'
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
@ -20,23 +20,17 @@ export class TaskListReplacer extends ComponentReplacer {
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
super()
this.onTaskCheckedChange = (lineInMarkdown, checked) => {
if (onTaskCheckedChange === undefined) {
return
}
onTaskCheckedChange(lineInMarkdown, checked)
}
this.onTaskCheckedChange = (lineInMarkdown, checked) => onTaskCheckedChange?.(lineInMarkdown, checked)
}
public replace(node: Element): ReactElement | undefined {
public replace(node: Element): NodeReplacement {
if (node.attribs?.class !== 'task-list-item-checkbox') {
return
return DO_NOT_REPLACE
}
const lineInMarkdown = Number(node.attribs['data-line'])
if (isNaN(lineInMarkdown)) {
return undefined
}
return (
return isNaN(lineInMarkdown) ? (
DO_NOT_REPLACE
) : (
<TaskListCheckbox
onTaskCheckedChange={this.onTaskCheckedChange}
checked={node.attribs.checked !== undefined}

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import type { Element } from 'domhandler'
import { UploadIndicatingFrame } from './upload-indicating-frame'
@ -16,8 +16,10 @@ const uploadIdRegex = /^upload-(.+)$/
*/
export class UploadIndicatingImageFrameReplacer extends ComponentReplacer {
replace(node: Element): NodeReplacement {
if (node.name === 'img' && uploadIdRegex.test(node.attribs.src)) {
return <UploadIndicatingFrame width={node.attribs.width} height={node.attribs.height} />
}
return node.name !== 'img' || !uploadIdRegex.test(node.attribs.src) ? (
DO_NOT_REPLACE
) : (
<UploadIndicatingFrame width={node.attribs.width} height={node.attribs.height} />
)
}
}

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ValidReactDomElement } from './component-replacer'
import { ComponentReplacer } from './component-replacer'
import type { NodeReplacement } from './component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from './component-replacer'
import type { FunctionComponent } from 'react'
import React from 'react'
import type { Element } from 'domhandler'
@ -22,9 +22,9 @@ export class CodeBlockComponentReplacer extends ComponentReplacer {
super()
}
replace(node: Element): ValidReactDomElement | undefined {
replace(node: Element): NodeReplacement {
const code = CodeBlockComponentReplacer.extractTextFromCodeNode(node, this.language)
return code ? React.createElement(this.component, { code: code }) : undefined
return code ? React.createElement(this.component, { code: code }) : DO_NOT_REPLACE
}
/**

View file

@ -10,13 +10,13 @@ import type { ReactElement } from 'react'
export type ValidReactDomElement = ReactElement | string | null
export type SubNodeTransform = (node: Node, subKey: number | string) => NodeReplacement
export type SubNodeTransform = (node: Node, subKey: number | string) => ValidReactDomElement
export type NativeRenderer = () => ValidReactDomElement
export const REPLACE_WITH_NOTHING = null
export const DO_NOT_REPLACE = undefined
export type NodeReplacement = ValidReactDomElement | typeof REPLACE_WITH_NOTHING | typeof DO_NOT_REPLACE
export const DO_NOT_REPLACE = Symbol()
export type NodeReplacement = ValidReactDomElement | typeof DO_NOT_REPLACE
/**
* Base class for all component replacers.
@ -42,7 +42,7 @@ export abstract class ComponentReplacer {
* @param subNodeTransform The transformer that should be used.
* @return The children as react elements.
*/
protected static transformChildren(node: Element, subNodeTransform: SubNodeTransform): NodeReplacement[] {
protected static transformChildren(node: Element, subNodeTransform: SubNodeTransform): ValidReactDomElement[] {
return node.children.map((value, index) => subNodeTransform(value, index))
}

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeReplacement } from './component-replacer'
import { ComponentReplacer } from './component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from './component-replacer'
import type { FunctionComponent } from 'react'
import React from 'react'
import type { Element } from 'domhandler'
@ -24,7 +24,7 @@ export class CustomTagWithIdComponentReplacer extends ComponentReplacer {
public replace(node: Element): NodeReplacement {
const id = this.extractId(node)
return id ? React.createElement(this.component, { id: id }) : undefined
return id ? React.createElement(this.component, { id: id }) : DO_NOT_REPLACE
}
/**

View file

@ -8,7 +8,7 @@ import { NodeToReactTransformer } from './node-to-react-transformer'
import { Element } from 'domhandler'
import type { ReactElement, ReactHTMLElement } from 'react'
import type { NodeReplacement } from '../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE, REPLACE_WITH_NOTHING } from '../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../replace-components/component-replacer'
describe('node to react transformer', () => {
let nodeToReactTransformer: NodeToReactTransformer
@ -30,7 +30,7 @@ describe('node to react transformer', () => {
nodeToReactTransformer.setReplacers([
new (class extends ComponentReplacer {
replace(): NodeReplacement {
return REPLACE_WITH_NOTHING
return null
}
})()
])

View file

@ -8,7 +8,7 @@ import type { Element, Node } from 'domhandler'
import { isTag } from 'domhandler'
import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertNodeToReactElement'
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
import { DO_NOT_REPLACE, REPLACE_WITH_NOTHING } from '../replace-components/component-replacer'
import { DO_NOT_REPLACE } from '../replace-components/component-replacer'
import React from 'react'
import type { LineWithId } from '../markdown-extension/linemarker/types'
import { Optional } from '@mrdrogdrog/optional'
@ -45,7 +45,7 @@ export class NodeToReactTransformer {
* @param index The index of the node within its parents child list.
* @return the created react element
*/
public translateNodeToReactElement(node: Node, index: number | string): ValidReactDomElement | undefined {
public translateNodeToReactElement(node: Node, index: number | string): ValidReactDomElement {
return isTag(node)
? this.translateElementToReactElement(node, index)
: convertNodeToReactElement(node, index, this.translateNodeToReactElement.bind(this))
@ -58,10 +58,10 @@ export class NodeToReactTransformer {
* @param index The index of the element within its parents child list.
* @return the created react element
*/
private translateElementToReactElement(element: Element, index: number | string): ValidReactDomElement | undefined {
private translateElementToReactElement(element: Element, index: number | string): ValidReactDomElement {
const elementKey = this.calculateUniqueKey(element).orElseGet(() => (-index).toString())
const replacement = this.findElementReplacement(element, elementKey)
if (replacement === REPLACE_WITH_NOTHING) {
if (replacement === null) {
return null
} else if (replacement === DO_NOT_REPLACE) {
return this.renderNativeNode(element, elementKey)
@ -101,7 +101,7 @@ export class NodeToReactTransformer {
*
* @param element The {@link Element} that should be checked.
* @param elementKey The unique key for the element
* @return The replacement or {@link DO_NOT_REPLACE} if the element shouldn't be replaced or {@link REPLACE_WITH_NOTHING} if the node shouldn't be rendered at all.
* @return The replacement or {@link DO_NOT_REPLACE} if the element shouldn't be replaced with a custom component.
*/
private findElementReplacement(element: Element, elementKey: string): NodeReplacement {
const transformer = this.translateNodeToReactElement.bind(this)