Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools encoding supports multibyte characters (e.g. "🟩") #22424

Merged
merged 2 commits into from Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Expand Up @@ -101,6 +101,19 @@ describe('Store', () => {
`);
});

it('should handle multibyte character strings', () => {
const Component = () => null;
Component.displayName = '🟩💜🔵';

const container = document.createElement('div');

act(() => legacyRender(<Component />, container));
expect(store).toMatchInlineSnapshot(`
[root]
<🟩💜🔵>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test verifies the before/after result of the fix in this PR.

    -     <🟩�💜�🔵�>
    +     <🟩💜🔵>

`);
});

describe('collapseNodesByDefault:false', () => {
beforeEach(() => {
store.collapseNodesByDefault = false;
Expand Down
61 changes: 41 additions & 20 deletions packages/react-devtools-shared/src/backend/renderer.js
Expand Up @@ -1513,11 +1513,16 @@ export function attach(

type OperationsArray = Array<number>;

type StringTableEntry = {|
encodedString: Array<number>,
id: number,
|};

const pendingOperations: OperationsArray = [];
const pendingRealUnmountedIDs: Array<number> = [];
const pendingSimulatedUnmountedIDs: Array<number> = [];
let pendingOperationsQueue: Array<OperationsArray> | null = [];
const pendingStringTable: Map<string, number> = new Map();
const pendingStringTable: Map<string, StringTableEntry> = new Map();
let pendingStringTableLength: number = 0;
let pendingUnmountedRootID: number | null = null;

Expand Down Expand Up @@ -1735,13 +1740,19 @@ export function attach(
// Now fill in the string table.
// [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
operations[i++] = pendingStringTableLength;
pendingStringTable.forEach((value, key) => {
operations[i++] = key.length;
const encodedKey = utfEncodeString(key);
for (let j = 0; j < encodedKey.length; j++) {
operations[i + j] = encodedKey[j];
pendingStringTable.forEach((entry, stringKey) => {
const encodedString = entry.encodedString;

// Don't use the string length.
// It won't work for multibyte characters (like emoji).
const length = encodedString.length;

operations[i++] = length;
for (let j = 0; j < length; j++) {
operations[i + j] = encodedString[j];
}
i += key.length;

i += length;
});

if (numUnmountIDs > 0) {
Expand Down Expand Up @@ -1788,21 +1799,31 @@ export function attach(
pendingStringTableLength = 0;
}

function getStringID(str: string | null): number {
if (str === null) {
function getStringID(string: string | null): number {
if (string === null) {
return 0;
}
const existingID = pendingStringTable.get(str);
if (existingID !== undefined) {
return existingID;
}
const stringID = pendingStringTable.size + 1;
pendingStringTable.set(str, stringID);
// The string table total length needs to account
// both for the string length, and for the array item
// that contains the length itself. Hence + 1.
pendingStringTableLength += str.length + 1;
return stringID;
const existingEntry = pendingStringTable.get(string);
if (existingEntry !== undefined) {
return existingEntry.id;
}

const id = pendingStringTable.size + 1;
const encodedString = utfEncodeString(string);

pendingStringTable.set(string, {
encodedString,
id,
});

// The string table total length needs to account both for the string length,
// and for the array item that contains the length itself.
//
// Don't use string length for this table.
// It won't work for multibyte characters (like emoji).
pendingStringTableLength += encodedString.length + 1;

return id;
}

function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
Expand Down
26 changes: 23 additions & 3 deletions packages/react-devtools-shared/src/utils.js
Expand Up @@ -138,17 +138,37 @@ export function utfDecodeString(array: Array<number>): string {
return string;
}

function surrogatePairToCodePoint(
charCode1: number,
charCode2: number,
): number {
return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000;
}

// Credit for this encoding approach goes to Tim Down:
// https://stackoverflow.com/questions/4877326/how-can-i-tell-if-a-string-contains-multibyte-characters-in-javascript
export function utfEncodeString(string: string): Array<number> {
const cached = encodedStringCache.get(string);
if (cached !== undefined) {
return cached;
}

const encoded = new Array(string.length);
for (let i = 0; i < string.length; i++) {
encoded[i] = string.codePointAt(i);
const encoded = [];
let i = 0;
let charCode;
while (i < string.length) {
charCode = string.charCodeAt(i);
// Handle multibyte unicode characters (like emoji).
if ((charCode & 0xf800) === 0xd800) {
encoded.push(surrogatePairToCodePoint(charCode, string.charCodeAt(++i)));
} else {
encoded.push(charCode);
}
++i;
}

encodedStringCache.set(string, encoded);

return encoded;
}

Expand Down