- - {state === CONVERSATION_READONLY_STATE.READONLY_ONE_TO_ONE_SELF_UNSUPPORTED_MLS && ( - <> - {' '} - - {t('downloadLatestMLS')} - {' '} - - > - )} -
-+ {children} +
+{result}
); + + expect(result).toHaveLength(3); + expect(getByTestId('parent').textContent).toEqual('Hello Przemek!'); + }); + + it('replaces multiple literal strings', () => { + const username1 = 'John'; + const username2 = 'Jerry'; + const result = replaceReactComponents(`Hello {{username1}} and {{username2}}, my name is also {{username1}}!`, [ + { + exactMatch: '{{username1}}', + render: () => {username1}, + }, + { + exactMatch: '{{username2}}', + render: () => {username2}, + }, + ]); + + const {getByTestId} = render({result}
); + + expect(result).toHaveLength(7); + expect(getByTestId('parent').textContent).toEqual('Hello John and Jerry, my name is also John!'); + }); + + it('replaces components and literal strings at the same time', () => { + const username1 = 'Tom'; + const username2 = 'Tim'; + const result = replaceReactComponents(`Hello [bold]${username1}[/bold] and {{username2}}!`, [ + { + start: '[bold]', + end: '[/bold]', + render: text => {text}, + }, + { + exactMatch: '{{username2}}', + render: () => {username2}, + }, + ]); + + const {getByTestId} = render({result}
); + + expect(result).toHaveLength(5); + expect(getByTestId('parent').textContent).toEqual('Hello Tom and Tim!'); + }); + + it('replaces literal string inside of a component', () => { + const username = 'Jake'; + const username2 = 'Marco'; + const result = replaceReactComponents(`Hello [bold]{{username}}[/bold], [bold]Paul[/bold] and {{username2}}!`, [ + { + start: '[bold]', + end: '[/bold]', + render: text => {text}, + }, + { + exactMatch: '{{username}}', + render: () => {username}, + }, + { + exactMatch: '{{username2}}', + render: () => {username2}, + }, + ]); + + const {getByTestId} = render({result}
); + + expect(result).toHaveLength(7); + expect(getByTestId('parent').textContent).toEqual('Hello Jake, Paul and Marco!'); + }); }); diff --git a/src/script/util/LocalizerUtil/ReactLocalizerUtil.ts b/src/script/util/LocalizerUtil/ReactLocalizerUtil.ts deleted file mode 100644 index 1753fa34002..00000000000 --- a/src/script/util/LocalizerUtil/ReactLocalizerUtil.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 Replacement { - start: string; - end: string; - render: (text: string) => React.ReactNode; -} - -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[] { - if (!replacements.length) { - return [html]; - } - const splitRegexp = new RegExp( - `(${replacements - .map(replacement => `${sanitizeRegexp(replacement.start)}.+?${sanitizeRegexp(replacement.end)}`) - .join('|')})`, - 'g', - ); - return html - .split(splitRegexp) - .map(node => { - const match = replacements.find( - replacement => node.startsWith(replacement.start) && node.endsWith(replacement.end), - ); - - if (match) { - const text = node.substring(match.start.length, node.length - match.end.length); - return match.render(text); - } - return node; - }) - .filter(Boolean); -} diff --git a/src/script/util/LocalizerUtil/ReactLocalizerUtil.tsx b/src/script/util/LocalizerUtil/ReactLocalizerUtil.tsx new file mode 100644 index 00000000000..de96c865330 --- /dev/null +++ b/src/script/util/LocalizerUtil/ReactLocalizerUtil.tsx @@ -0,0 +1,118 @@ +/* + * 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) =>