/* * Wire * Copyright (C) 2023 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React from 'react'; interface ComponentReplacement { start: string; end: string; render: (text: string) => React.ReactNode; } interface StringReplacement { exactMatch: string; render: () => React.ReactNode | string; } type Replacement = ComponentReplacement | StringReplacement; function sanitizeRegexp(text: string) { return text.replaceAll('[', '\\[').replaceAll(']', '\\]'); } /** * Will replace all occurences of `replacements` by a React component returned by `render`. */ export function replaceReactComponents(html: string, replacements: Replacement[]): React.ReactNode[] { const [stringReplacements, componentReplacements] = replacements.reduce( (acc, replacement) => { if ('exactMatch' in replacement) { acc[0].push(replacement); } else { acc[1].push(replacement); } return acc; }, [[], []] as [StringReplacement[], ComponentReplacement[]], ); if (!componentReplacements.length && !stringReplacements.length) { return [html]; } const componentsSplitRegexpStr = componentReplacements.length ? `(${componentReplacements .map(replacement => `${sanitizeRegexp(replacement.start)}.+?${sanitizeRegexp(replacement.end)}`) .join('|')})` : null; const stringSplitRegexpStr = stringReplacements.length ? `(${stringReplacements.map(replacement => sanitizeRegexp(replacement.exactMatch)).join('|')})` : null; const regexpStr = [componentsSplitRegexpStr, stringSplitRegexpStr].filter(Boolean).join('|'); const splitRegexp = new RegExp(regexpStr, 'g'); return html .split(splitRegexp) .map(node => { if (!node) { return false; } const componentsReplacementMatch = componentReplacements.find( replacement => node.startsWith(replacement.start) && node.endsWith(replacement.end), ); if (componentsReplacementMatch) { const text = node.substring( componentsReplacementMatch.start.length, node.length - componentsReplacementMatch.end.length, ); // There is a special case where we have a string replacement inside a component replacement. if (stringSplitRegexpStr) { const regexp = new RegExp(stringSplitRegexpStr, 'g'); const split = text.split(regexp); return split .map(node => { const stringReplacementMatch = stringReplacements.find(replacement => node === replacement.exactMatch); if (stringReplacementMatch) { return stringReplacementMatch.render(); } return componentsReplacementMatch.render(node); }) .filter(Boolean) .map((node, index) => {node}); } return componentsReplacementMatch.render(text); } const stringReplacementMatch = stringReplacements.find(replacement => node === replacement.exactMatch); if (stringReplacementMatch) { return stringReplacementMatch.render(); } return node; }) .filter(Boolean) .map((node, index) => {node}); // Make sure we have a different key for each node. }