main
js 257 lines 6.19 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import escapeStringRegExp from 'escape-string-regexp';
11 import {meta} from '../../hydration';
12 import {formatDataForPreview} from '../../utils';
13 import isArray from 'react-devtools-shared/src/isArray';
14
15 import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
16
17 // $FlowFixMe[method-unbinding]
18 const hasOwnProperty = Object.prototype.hasOwnProperty;
19
20 export function alphaSortEntries(
21 entryA: [string, mixed],
22 entryB: [string, mixed],
23 ): number {
24 const a = entryA[0];
25 const b = entryB[0];
26 if (String(+a) === a) {
27 if (String(+b) !== b) {
28 return -1;
29 }
30 return +a < +b ? -1 : 1;
31 }
32 return a < b ? -1 : 1;
33 }
34
35 export function createRegExp(string: string): RegExp {
36 // Allow /regex/ syntax with optional last /
37 if (string[0] === '/') {
38 // Cut off first slash
39 string = string.slice(1);
40 // Cut off last slash, but only if it's there
41 if (string[string.length - 1] === '/') {
42 string = string.slice(0, string.length - 1);
43 }
44 try {
45 return new RegExp(string, 'i');
46 } catch (err) {
47 // Bad regex. Make it not match anything.
48 // TODO: maybe warn in console?
49 return new RegExp('.^');
50 }
51 }
52
53 function isLetter(char: string) {
54 return char.toLowerCase() !== char.toUpperCase();
55 }
56
57 function matchAnyCase(char: string) {
58 if (!isLetter(char)) {
59 // Don't mess with special characters like [.
60 return char;
61 }
62 return '[' + char.toLowerCase() + char.toUpperCase() + ']';
63 }
64
65 // 'item' should match 'Item' and 'ListItem', but not 'InviteMom'.
66 // To do this, we'll slice off 'tem' and check first letter separately.
67 const escaped = escapeStringRegExp(string);
68 const firstChar = escaped[0];
69 let restRegex = '';
70 // For 'item' input, restRegex becomes '[tT][eE][mM]'
71 // We can't simply make it case-insensitive because first letter case matters.
72 for (let i = 1; i < escaped.length; i++) {
73 restRegex += matchAnyCase(escaped[i]);
74 }
75
76 if (!isLetter(firstChar)) {
77 // We can't put a non-character like [ in a group
78 // so we fall back to the simple case.
79 return new RegExp(firstChar + restRegex);
80 }
81
82 // Construct a smarter regex.
83 return new RegExp(
84 // For example:
85 // (^[iI]|I)[tT][eE][mM]
86 // Matches:
87 // 'Item'
88 // 'ListItem'
89 // but not 'InviteMom'
90 '(^' +
91 matchAnyCase(firstChar) +
92 '|' +
93 firstChar.toUpperCase() +
94 ')' +
95 restRegex,
96 );
97 }
98
99 export function getMetaValueLabel(data: Object): string | null {
100 if (hasOwnProperty.call(data, meta.preview_long)) {
101 return data[meta.preview_long];
102 } else {
103 return formatDataForPreview(data, true);
104 }
105 }
106
107 function sanitize(data: Object): void {
108 for (const key in data) {
109 const value = data[key];
110
111 if (value && value[meta.type]) {
112 data[key] = getMetaValueLabel(value);
113 } else if (value != null) {
114 if (isArray(value)) {
115 sanitize(value);
116 } else if (typeof value === 'object') {
117 sanitize(value);
118 }
119 }
120 }
121 }
122
123 export function serializeDataForCopy(props: Object): string {
124 const cloned = isArray(props) ? props.slice(0) : Object.assign({}, props);
125
126 sanitize(cloned);
127
128 try {
129 return JSON.stringify(cloned, null, 2);
130 } catch (error) {
131 return '';
132 }
133 }
134
135 export function serializeHooksForCopy(hooks: HooksTree | null): string {
136 // $FlowFixMe[not-an-object] "HooksTree is not an object"
137 const cloned = Object.assign([] as Array<any>, hooks);
138
139 const queue = [...cloned];
140
141 while (queue.length > 0) {
142 const current = queue.pop();
143
144 // These aren't meaningful
145 // $FlowFixMe[incompatible-use]
146 delete current.id;
147 // $FlowFixMe[incompatible-use]
148 delete current.isStateEditable;
149
150 // $FlowFixMe[incompatible-use]
151 if (current.subHooks.length > 0) {
152 // $FlowFixMe[incompatible-use]
153 queue.push(...current.subHooks);
154 }
155 }
156
157 sanitize(cloned);
158
159 try {
160 return JSON.stringify(cloned, null, 2);
161 } catch (error) {
162 return '';
163 }
164 }
165
166 // Keeping this in memory seems to be enough to enable the browser to download larger profiles.
167 // Without this, we would see a "Download failed: network error" failure.
168 let downloadUrl = null;
169
170 export function downloadFile(
171 element: HTMLAnchorElement,
172 filename: string,
173 text: string,
174 ): void {
175 const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
176
177 if (downloadUrl !== null) {
178 URL.revokeObjectURL(downloadUrl);
179 }
180
181 downloadUrl = URL.createObjectURL(blob);
182
183 element.setAttribute('href', downloadUrl);
184 element.setAttribute('download', filename);
185
186 element.click();
187 }
188
189 export function truncateText(text: string, maxLength: number): string {
190 const {length} = text;
191 if (length > maxLength) {
192 return (
193 text.slice(0, Math.floor(maxLength / 2)) +
194 '' +
195 text.slice(length - Math.ceil(maxLength / 2) - 1)
196 );
197 } else {
198 return text;
199 }
200 }
201
202 export function pluralize(word: string): string {
203 if (!/^[a-z]+$/i.test(word)) {
204 // If it's not a single a-z word, give up.
205 return word;
206 }
207
208 // Bail out if it's already plural.
209 switch (word) {
210 case 'men':
211 case 'women':
212 case 'children':
213 case 'feet':
214 case 'teeth':
215 case 'mice':
216 case 'people':
217 return word;
218 }
219
220 if (
221 /(ches|shes|ses|xes|zes)$/i.test(word) ||
222 /[^s]ies$/i.test(word) ||
223 /ves$/i.test(word) ||
224 /[^s]s$/i.test(word)
225 ) {
226 return word;
227 }
228
229 switch (word) {
230 case 'man':
231 return 'men';
232 case 'woman':
233 return 'women';
234 case 'child':
235 return 'children';
236 case 'foot':
237 return 'feet';
238 case 'tooth':
239 return 'teeth';
240 case 'mouse':
241 return 'mice';
242 case 'person':
243 return 'people';
244 }
245
246 // Words ending in s, x, z, ch, sh → add "es"
247 if (/(s|x|z|ch|sh)$/i.test(word)) return word + 'es';
248
249 // Words ending in consonant + y → replace y with "ies"
250 if (/[bcdfghjklmnpqrstvwxz]y$/i.test(word)) return word.slice(0, -1) + 'ies';
251
252 // Words ending in f or fe → replace with "ves"
253 if (/(?:f|fe)$/i.test(word)) return word.replace(/(?:f|fe)$/i, 'ves');
254
255 // Default: just add "s"
256 return word + 's';
257 }