diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-23 16:53:42 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-27 15:36:50 +0100 |
| commit | 36b5ee4fd20bf882c214baa839dfef43515a875b (patch) | |
| tree | fe584f393de29481b85bc9fc0d4177d1a89d727d | |
| parent | b8ff10c6506fa2944220f32e83155e29b23b96e7 (diff) | |
| download | mullvadvpn-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.tsx | 51 |
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>; } |
