mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04:00
Refactor replacers and line id mapping
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
3591c90f9f
commit
ec77e672f6
58 changed files with 899 additions and 750 deletions
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 React from 'react'
|
||||
import type { LineWithId } from '../types'
|
||||
import Optional from 'optional-js'
|
||||
|
||||
type LineIndexPair = [startLineIndex: number, endLineIndex: number]
|
||||
|
||||
/**
|
||||
* Converts {@link Node domhandler nodes} to react elements by using direct translation or {@link ComponentReplacer replacers}.
|
||||
*/
|
||||
export class NodeToReactTransformer {
|
||||
private lineIds: LineWithId[] = []
|
||||
private replacers: ComponentReplacer[] = []
|
||||
|
||||
public setLineIds(lineIds: LineWithId[]): void {
|
||||
this.lineIds = lineIds
|
||||
}
|
||||
|
||||
public setReplacers(replacers: ComponentReplacer[]): void {
|
||||
this.replacers = replacers
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link Node} to a react element.
|
||||
*
|
||||
* @param node The {@link Node DOM node} that should be translated.
|
||||
* @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 {
|
||||
return isTag(node)
|
||||
? this.translateElementToReactElement(node, index)
|
||||
: convertNodeToReactElement(node, index, this.translateNodeToReactElement.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given {@link Element} to a react element.
|
||||
*
|
||||
* @param element The {@link Element DOM element} that should be translated.
|
||||
* @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 {
|
||||
const elementKey = this.calculateUniqueKey(element).orElseGet(() => (-index).toString())
|
||||
const replacement = this.findElementReplacement(element, elementKey)
|
||||
if (replacement === REPLACE_WITH_NOTHING) {
|
||||
return null
|
||||
} else if (replacement === DO_NOT_REPLACE) {
|
||||
return this.renderNativeNode(element, elementKey)
|
||||
} else if (typeof replacement === 'string') {
|
||||
return replacement
|
||||
} else {
|
||||
return React.cloneElement(replacement, {
|
||||
...(replacement.props as Record<string, unknown>),
|
||||
key: elementKey
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the unique key for the given {@link Element}.
|
||||
*
|
||||
* @param element The element for which the unique key should be calculated.
|
||||
* @return An {@link Optional} that contains the unique key or is empty if no key could be found.
|
||||
*/
|
||||
private calculateUniqueKey(element: Element): Optional<string> {
|
||||
if (!element.attribs) {
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
return Optional.ofNullable(element.prev)
|
||||
.map((lineMarker) => NodeToReactTransformer.extractLineIndexFromLineMarker(lineMarker))
|
||||
.map(([startLineIndex, endLineIndex]) =>
|
||||
NodeToReactTransformer.convertMarkdownItLineIndexesToInternalLineIndexes(startLineIndex, endLineIndex)
|
||||
)
|
||||
.flatMap((adjustedLineIndexes) => this.findLineIdsByIndex(adjustedLineIndexes))
|
||||
.map(([startLine, endLine]) => `${startLine.id}_${endLine.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks every saved replacer if the given {@link Element element} should be
|
||||
* replaced with another react element or not.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
private findElementReplacement(element: Element, elementKey: string): NodeReplacement {
|
||||
const transformer = this.translateNodeToReactElement.bind(this)
|
||||
const nativeRenderer = () => this.renderNativeNode(element, elementKey)
|
||||
for (const componentReplacer of this.replacers) {
|
||||
const replacement = componentReplacer.replace(element, transformer, nativeRenderer)
|
||||
if (replacement !== DO_NOT_REPLACE) {
|
||||
return replacement
|
||||
}
|
||||
}
|
||||
return DO_NOT_REPLACE
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the start and end line indexes that are saved in a line marker element
|
||||
* and describe in which line, in the markdown code, the node before the marker ends
|
||||
* and which the node after the marker starts.
|
||||
*
|
||||
* @param lineMarker The line marker that saves a start and end line index.
|
||||
* @return the extracted line indexes
|
||||
*/
|
||||
private static extractLineIndexFromLineMarker(lineMarker: Node): LineIndexPair | undefined {
|
||||
if (!isTag(lineMarker) || lineMarker.tagName !== 'app-linemarker' || !lineMarker.attribs) {
|
||||
return
|
||||
}
|
||||
const startLineInMarkdown = lineMarker.attribs['data-start-line']
|
||||
const endLineInMarkdown = lineMarker.attribs['data-end-line']
|
||||
if (startLineInMarkdown === undefined || endLineInMarkdown === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const startLineIndex = Number(startLineInMarkdown)
|
||||
const endLineIndex = Number(endLineInMarkdown)
|
||||
|
||||
if (isNaN(startLineIndex) || isNaN(endLineIndex)) {
|
||||
return
|
||||
}
|
||||
|
||||
return [startLineIndex, endLineIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts markdown it line indexes to internal line indexes.
|
||||
* The differences are:
|
||||
* - Markdown it starts to count at 1, but we start at 0
|
||||
* - Line indexes in markdown it are start(inclusive) to end(exclusive). But we need start(inclusive) to end(inclusive).
|
||||
*
|
||||
* @param startLineIndex The start line index from markdown it
|
||||
* @param endLineIndex The end line index from markdown it
|
||||
* @return The adjusted start and end line index
|
||||
*/
|
||||
private static convertMarkdownItLineIndexesToInternalLineIndexes(
|
||||
startLineIndex: number,
|
||||
endLineIndex: number
|
||||
): LineIndexPair {
|
||||
return [startLineIndex - 1, endLineIndex - 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given node without any replacement
|
||||
*
|
||||
* @param node The node to render
|
||||
* @param key The unique key for the node
|
||||
*/
|
||||
private renderNativeNode = (node: Element, key: string): ValidReactDomElement => {
|
||||
if (node.attribs === undefined) {
|
||||
node.attribs = {}
|
||||
}
|
||||
|
||||
return convertNodeToReactElement(node, key, this.translateNodeToReactElement.bind(this))
|
||||
}
|
||||
|
||||
private findLineIdsByIndex([startLineIndex, endLineIndex]: LineIndexPair): Optional<[LineWithId, LineWithId]> {
|
||||
const startLine = this.lineIds[startLineIndex]
|
||||
const endLine = this.lineIds[endLineIndex]
|
||||
return startLine === undefined || endLine === undefined ? Optional.empty() : Optional.of([startLine, endLine])
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue