* Update dependency html-react-parser to v6 * Fix renderer utils * Fix CodeBlock similarly --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
/*
|
|
Copyright 2025 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { type JSX } from "react";
|
|
import { type DOMNode, type Element, type HTMLReactParserOptions, type Text } from "html-react-parser";
|
|
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
|
|
|
|
/**
|
|
* The type of a parent node of an element, normally exported by domhandler but that is not a direct dependency of ours
|
|
*/
|
|
export type ParentNode = NonNullable<Element["parentNode"]>;
|
|
|
|
/**
|
|
* Returns the text content of a node if it is the only child and that child is a text node
|
|
* @param node - the node to check
|
|
*/
|
|
export const getSingleTextContentNode = (node: Element): string | null => {
|
|
if (node.childNodes.length === 1 && node.childNodes[0].type === "text") {
|
|
return node.childNodes[0].data;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Returns true if the node has a parent that matches the given matcher
|
|
* @param node - the node to check
|
|
* @param matcher - a function that returns true if the node matches
|
|
*/
|
|
export const hasParentMatching = (node: Element, matcher: (node: ParentNode | null) => boolean): boolean => {
|
|
let parent = node.parentNode;
|
|
while (parent) {
|
|
if (matcher(parent)) return true;
|
|
parent = parent.parentNode;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* A replacer function that can be used with html-react-parser
|
|
*/
|
|
export type Replacer = HTMLReactParserOptions["replace"];
|
|
|
|
interface Parameters {
|
|
isHtml: boolean;
|
|
replace: Replacer;
|
|
// Required for keywordPillRenderer
|
|
keywordRegexpPattern?: RegExp;
|
|
// Required for mentionPillRenderer
|
|
mxEvent?: MatrixEvent;
|
|
room?: Room;
|
|
shouldShowPillAvatar?: boolean;
|
|
}
|
|
|
|
type SpecialisedReplacer<T extends DOMNode> = (
|
|
node: T,
|
|
parameters: Parameters,
|
|
index: number,
|
|
) => JSX.Element | string | void;
|
|
|
|
/**
|
|
* A map of replacer functions for different types of nodes/tags.
|
|
* When a function returns a JSX element, the element will be rendered in place of the node.
|
|
*/
|
|
export type RendererMap = Partial<
|
|
{
|
|
[tagName in keyof HTMLElementTagNameMap]: SpecialisedReplacer<Element>;
|
|
} & {
|
|
[Node.TEXT_NODE]: SpecialisedReplacer<Text>;
|
|
}
|
|
>;
|
|
|
|
type PreparedRenderer = (parameters: Omit<Parameters, "replace">) => Replacer;
|
|
|
|
/**
|
|
* Combines multiple renderers into a single Replacer function
|
|
* @param renderers - the list of renderers to combine
|
|
*/
|
|
export const combineRenderers =
|
|
(...renderers: RendererMap[]): PreparedRenderer =>
|
|
(parameters) => {
|
|
const replace: Replacer = (node, index) => {
|
|
if (node.type === "text") {
|
|
for (const replacer of renderers) {
|
|
const result = replacer[Node.TEXT_NODE]?.(node, parametersWithReplace, index);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
if (node.type === "tag") {
|
|
const tagName = node.tagName.toLowerCase() as keyof HTMLElementTagNameMap;
|
|
for (const replacer of renderers) {
|
|
const result = replacer[tagName]?.(node, parametersWithReplace, index);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
};
|
|
const parametersWithReplace: Parameters = { ...parameters, replace };
|
|
return replace;
|
|
};
|