Files
element-web/src/renderer/utils.tsx
Bojidar Marinov cf51b256ce Fix highlights in messages (or search results) breaking links (#30264)
* Fix highlights in messages (or search results) breaking links

Fixes #17011 and fixes #29807, by running the linkifier that turns text into links before the highlighter that adds highlights to text.

* Fix jest test

* Fix tests related to emojis and pills-inside-spoilers

* Remove dead code

* Address review comments around sanitizeParams

* Address review comment about linkify-matrix

* Fix code style

* Refactor if statement per review

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-20 06:10:13 +00:00

123 lines
3.9 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 React, { type JSX } from "react";
import { type DOMNode, Element, type HTMLReactParserOptions, 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"];
/**
* Passes through any non-string inputs verbatim, as such they should only be used for emoji bodies
*/
export function applyReplacerOnString(
input: string | JSX.Element[],
replacer: Replacer,
): JSX.Element | JSX.Element[] | string {
if (!replacer) return input;
const arr = Array.isArray(input) ? input : [input];
return arr.map((input, index): JSX.Element => {
if (typeof input === "string") {
return (
<React.Fragment key={index}>{(replacer(new Text(input), 0) as JSX.Element) || input}</React.Fragment>
);
}
return input;
});
}
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 instanceof Element) {
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;
};