summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-10-23 16:53:42 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-10-27 15:36:50 +0100
commit36b5ee4fd20bf882c214baa839dfef43515a875b (patch)
treefe584f393de29481b85bc9fc0d4177d1a89d727d
parentb8ff10c6506fa2944220f32e83155e29b23b96e7 (diff)
downloadmullvadvpn-36b5ee4fd20bf882c214baa839dfef43515a875b.tar.xz
mullvadvpn-36b5ee4fd20bf882c214baa839dfef43515a875b.zip
Rewrite formatHtml to fix bugs in previous implementation
When the functionality to format `em` tags was added a buggy behavior was introduced. This buggy behavior stems from the logic to match the input string against the tags we want to format, namely <b> and <em>. We mistakenly operate against the original string at all times when we match each tag, which means that for a string which contains '<b>something</b>' the content is correctly identified by the <b> tag's test function and gets rendered with the appropriate React component. However, since the original string is used for every tag this means the same string will also be handled by the next tag. In this case the <em> tag's test function will evaluate the string, '<b>something</b>' and see that it does not match and herein lies the buggy behavior. When a tag's test function does not get a match for the string it will render it as a fragment and push it to the formatted string array. This means that we get duplicated entries in the formatted string array because we get both the React component version of the string and the fragment version of the string. This buggy behavior is now fixed by passing the result of the first tag operations to the next tag. This ensures that once a part of the original string has been matched and turned into a component it can not be matched again by another tag.
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/html-formatter.tsx51
1 files changed, 34 insertions, 17 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/html-formatter.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/html-formatter.tsx
index f0a0457eb0..43c0721f29 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/html-formatter.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/html-formatter.tsx
@@ -35,29 +35,46 @@ const componentMap: Partial<
} as const;
export function formatHtml(inputString: string): React.ReactElement {
- const formattedString: JSX.Element[] = [];
+ const inputStringArray: Array<string | JSX.Element> = [inputString];
- Object.entries(testMap).forEach(([key, { test, replace }]) => {
- const parts = inputString.split(test).filter((part) => part !== '');
- if (parts.length <= 1) {
- return;
- }
+ const transformedStrings = Object.entries(testMap).reduce((strings, [key, { test, replace }]) => {
+ const newStrings = strings.flatMap((value) => {
+ if (typeof value === 'string') {
+ // If the value is a string we should see if our current transformer should transform it.
+ return value
+ .split(test)
+ .filter((v) => v.length > 0)
+ .map((substring) => {
+ // Create a new RegExp object to avoid `lastIndex` side effects, see:
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex#avoiding_side_effects
+ const tester = new RegExp(test);
- parts.map((value, index) => {
- if (test.test(value)) {
- const Component = componentMap[key as keyof typeof componentMap]!;
- const valueWithoutTags = value.replaceAll(replace, '');
+ // If the value is matched for the current transformer then it should be turned into a component
+ if (tester.test(substring)) {
+ const Component = componentMap[key as keyof typeof componentMap]!;
+ const valueWithoutTags = substring.replaceAll(replace, '');
- formattedString.push(<Component key={index}>{valueWithoutTags}</Component>);
+ return <Component key={substring}>{valueWithoutTags}</Component>;
+ } else {
+ // If the value is not a match for the current transformer we should return the string as is,
+ // so it can be potentially manipulated by a later transformer
+ return substring;
+ }
+ });
} else {
- formattedString.push(<React.Fragment key={index}>{value}</React.Fragment>);
+ // If the value is not a string it has already been transformed into a component by a transformer in a previous iteration.
+ return value;
}
});
- });
- if (formattedString.length === 0) {
- formattedString.push(<React.Fragment key={inputString}>{inputString}</React.Fragment>);
- }
+ return newStrings;
+ }, inputStringArray);
+
+ // After all the transformers have been applied,
+ // loop over all non-transformed strings and wrap them in fragments
+ const htmlFormattedInputString = transformedStrings.map((value) =>
+ typeof value === 'string' ? <React.Fragment key={value}>{value}</React.Fragment> : value,
+ );
- return <>{formattedString}</>;
+ return <React.Fragment>{htmlFormattedInputString}</React.Fragment>;
}