expensify-common 2.0.188 → 2.0.190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/Logger.js +11 -1
  2. package/dist/esm/API.d.ts +11 -0
  3. package/dist/esm/API.js +786 -0
  4. package/dist/esm/APIDeferred.d.ts +7 -0
  5. package/dist/esm/APIDeferred.js +216 -0
  6. package/dist/esm/BrowserDetect.d.ts +19 -0
  7. package/dist/esm/BrowserDetect.js +105 -0
  8. package/dist/esm/CLI.js +37 -29
  9. package/dist/esm/CONST.d.ts +1727 -0
  10. package/dist/esm/CONST.js +1847 -0
  11. package/dist/esm/Cookie.d.ts +68 -0
  12. package/dist/esm/Cookie.js +159 -0
  13. package/dist/esm/CredentialsWrapper.d.ts +32 -0
  14. package/dist/esm/CredentialsWrapper.js +46 -0
  15. package/dist/esm/Device.d.ts +8 -0
  16. package/dist/esm/Device.js +24 -0
  17. package/dist/esm/ExpenseRule.d.ts +39 -0
  18. package/dist/esm/ExpenseRule.js +77 -0
  19. package/dist/esm/ExpensiMark.d.ts +197 -0
  20. package/dist/esm/ExpensiMark.js +1374 -0
  21. package/dist/esm/Func.d.ts +40 -0
  22. package/dist/esm/Func.js +69 -0
  23. package/dist/esm/Log.d.ts +3 -0
  24. package/dist/esm/Log.js +35 -0
  25. package/dist/esm/Logger.d.ts +82 -0
  26. package/dist/esm/Logger.js +148 -0
  27. package/dist/esm/Network.d.ts +6 -0
  28. package/dist/esm/Network.js +168 -0
  29. package/dist/esm/Num.d.ts +95 -0
  30. package/dist/esm/Num.js +190 -0
  31. package/dist/esm/PageEvent.d.ts +25 -0
  32. package/dist/esm/PageEvent.js +22 -0
  33. package/dist/esm/PubSub.d.ts +2 -0
  34. package/dist/esm/PubSub.js +111 -0
  35. package/dist/esm/ReportHistoryStore.d.ts +8 -0
  36. package/dist/esm/ReportHistoryStore.js +199 -0
  37. package/dist/esm/SafeString.js +2 -2
  38. package/dist/esm/Templates.d.ts +58 -0
  39. package/dist/esm/Templates.js +196 -0
  40. package/dist/esm/Url.d.ts +10 -0
  41. package/dist/esm/Url.js +15 -0
  42. package/dist/esm/components/CopyText.d.ts +45 -0
  43. package/dist/esm/components/CopyText.js +56 -0
  44. package/dist/esm/components/StepProgressBar.d.ts +26 -0
  45. package/dist/esm/components/StepProgressBar.js +40 -0
  46. package/dist/esm/components/form/element/combobox.d.ts +231 -0
  47. package/dist/esm/components/form/element/combobox.js +812 -0
  48. package/dist/esm/components/form/element/dropdown.d.ts +35 -0
  49. package/dist/esm/components/form/element/dropdown.js +61 -0
  50. package/dist/esm/components/form/element/dropdownItem.d.ts +55 -0
  51. package/dist/esm/components/form/element/dropdownItem.js +113 -0
  52. package/dist/esm/components/form/element/onOffSwitch.d.ts +94 -0
  53. package/dist/esm/components/form/element/onOffSwitch.js +167 -0
  54. package/dist/esm/components/form/element/switch.d.ts +58 -0
  55. package/dist/esm/components/form/element/switch.js +98 -0
  56. package/dist/esm/fastMerge.d.ts +9 -0
  57. package/dist/esm/fastMerge.js +58 -0
  58. package/dist/esm/index.d.ts +22 -0
  59. package/dist/esm/index.js +22 -0
  60. package/dist/esm/jquery.expensifyIframify.d.ts +9 -0
  61. package/dist/esm/jquery.expensifyIframify.js +397 -0
  62. package/dist/esm/md5.d.ts +2 -0
  63. package/dist/esm/md5.js +170 -0
  64. package/dist/esm/mixins/PubSub.d.ts +20 -0
  65. package/dist/esm/mixins/PubSub.js +47 -0
  66. package/dist/esm/mixins/extraClasses.d.ts +8 -0
  67. package/dist/esm/mixins/extraClasses.js +37 -0
  68. package/dist/esm/mixins/validationClasses.d.ts +12 -0
  69. package/dist/esm/mixins/validationClasses.js +30 -0
  70. package/dist/esm/str.d.ts +604 -0
  71. package/dist/esm/str.js +1036 -0
  72. package/dist/esm/tlds.d.ts +2 -0
  73. package/dist/esm/tlds.js +2 -0
  74. package/dist/esm/utils.d.ts +30 -0
  75. package/dist/esm/utils.js +65 -0
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +8 -1
  78. package/package.json +221 -4
@@ -0,0 +1,1036 @@
1
+ 'worklet';
2
+ /* eslint-disable no-control-regex */
3
+ import $ from 'jquery';
4
+ import { parsePhoneNumber } from 'awesome-phonenumber';
5
+ import Punycode from 'punycode/';
6
+ import * as HtmlEntities from 'html-entities';
7
+ import * as Constants from './CONST';
8
+ import * as UrlPatterns from './Url';
9
+ import * as Utils from './utils';
10
+ import Log from './Log';
11
+ const REMOVE_SMS_DOMAIN_PATTERN = /@expensify\.sms/gi;
12
+ function resultFn(parameter, ...args) {
13
+ if (typeof parameter === 'function') {
14
+ return parameter(...args);
15
+ }
16
+ return parameter;
17
+ }
18
+ const Str = {
19
+ /**
20
+ * Return true if the string is ending with the provided suffix
21
+ *
22
+ * @param str String ot search in
23
+ * @param suffix What to look for
24
+ */
25
+ endsWith(str, suffix) {
26
+ if (!str || !suffix) {
27
+ return false;
28
+ }
29
+ return str.substr(-suffix.length) === suffix;
30
+ },
31
+ /**
32
+ * Converts a USD string into th number of cents it represents.
33
+ *
34
+ * @param amountStr A string representing a USD value.
35
+ * @param allowFraction Flag indicating if fractions of cents should be
36
+ * allowed in the output.
37
+ *
38
+ * @returns The cent value of the @p amountStr.
39
+ */
40
+ fromUSDToNumber(amountStr, allowFraction) {
41
+ let amount = String(amountStr).replaceAll(/[^\d.\-()]+/g, '');
42
+ if (amount.match(/\(.*\)/)) {
43
+ const modifiedAmount = amount.replaceAll(/[()]/g, '');
44
+ amount = `-${modifiedAmount}`;
45
+ }
46
+ amount = Number(amount) * 100;
47
+ amount = Math.round(amount * 1e3) / 1e3;
48
+ return allowFraction ? amount : Math.round(amount);
49
+ },
50
+ /**
51
+ * Truncates the middle section of a string based on the max allowed length
52
+ */
53
+ truncateInMiddle(fullStr, maxLength) {
54
+ if (fullStr.length <= maxLength) {
55
+ return fullStr;
56
+ }
57
+ const separator = '...';
58
+ const halfLengthToShow = (maxLength - separator.length) / 2;
59
+ const beginning = fullStr.substr(0, Math.ceil(halfLengthToShow));
60
+ const end = fullStr.substr(fullStr.length - Math.floor(halfLengthToShow));
61
+ return beginning + separator + end;
62
+ },
63
+ /**
64
+ * Convert new line to <br />
65
+ */
66
+ nl2br(str) {
67
+ return str.replaceAll('\n', '<br />');
68
+ },
69
+ /**
70
+ * Decodes the given HTML encoded string.
71
+ *
72
+ * @param s The string to decode.
73
+ * @returns The decoded string.
74
+ */
75
+ htmlDecode(s) {
76
+ return HtmlEntities.decode(s);
77
+ },
78
+ /**
79
+ * HTML encodes the given string.
80
+ *
81
+ * @param s The string to encode.
82
+ * @return string @p s HTML encoded.
83
+ */
84
+ htmlEncode(s) {
85
+ return HtmlEntities.encode(s);
86
+ },
87
+ /**
88
+ * Decodes the given HTML encoded string.
89
+ *
90
+ * @param s The string to decode.
91
+ * @returns The decoded string.
92
+ */
93
+ htmlDecodeWithJQueryOrNative(s) {
94
+ // Use jQuery if it exists or else use html-entities
95
+ if (typeof $ !== 'undefined') {
96
+ return $('<textarea/>').html(s).text();
97
+ }
98
+ return HtmlEntities.decode(s);
99
+ },
100
+ /**
101
+ * HTML encodes the given string.
102
+ *
103
+ * @param s The string to encode.
104
+ * @return string @p s HTML encoded.
105
+ */
106
+ htmlEncodeWithJQueryOrNative(s) {
107
+ // Use jQuery if it exists or else use html-entities
108
+ if (typeof $ !== 'undefined') {
109
+ return $('<textarea/>').text(s).html();
110
+ }
111
+ return HtmlEntities.encode(s);
112
+ },
113
+ /**
114
+ * Escape text while preventing any sort of double escape, so 'X & Y' -> 'X &amp; Y' and 'X &amp; Y' -> 'X &amp; Y'
115
+ *
116
+ * @param s The string to escape
117
+ * @returns The escaped string
118
+ */
119
+ safeEscape(s) {
120
+ return Utils.escapeText(Utils.unescapeText(s));
121
+ },
122
+ /**
123
+ * HTML encoding insensitive equals.
124
+ *
125
+ * @param first string to compare
126
+ * @param second string to compare
127
+ * @returns True when first === second, ignoring HTML encoding
128
+ */
129
+ htmlEncodingInsensitiveEquals(first, second) {
130
+ return first === second || this.htmlDecode(first) === second || this.htmlEncode(first) === second;
131
+ },
132
+ /**
133
+ * Creates an ID that can be used as an HTML attribute from @p str.
134
+ *
135
+ * @param str A string to create an ID from.
136
+ * @returns The ID string made from @p str.
137
+ */
138
+ makeID(str) {
139
+ const modifiedString = String(str)
140
+ .replaceAll(/[^A-Za-z0-9]/g, '_')
141
+ .toUpperCase();
142
+ return `id_${modifiedString}`;
143
+ },
144
+ /**
145
+ * Extracts an ID made with Str.makeID from a larger string.
146
+ *
147
+ * @param str A string containing an id made with Str.makeID
148
+ * @returns The ID string.
149
+ */
150
+ extractID(str) {
151
+ const matches = str.match(/id[A-Z0-9_]+/);
152
+ return matches && matches.length > 0 ? matches[0] : null;
153
+ },
154
+ /**
155
+ * Modifies the string so the first letter of each word is capitalized and the
156
+ * rest lowercased.
157
+ *
158
+ * @param val The string to modify
159
+ */
160
+ recapitalize(val) {
161
+ // First replace every letter with its lowercase equivalent
162
+ // Cast to string.
163
+ let str = String(val);
164
+ if (str.length <= 0) {
165
+ return str;
166
+ }
167
+ str = str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase();
168
+ function recap_callback(t, a, b) {
169
+ return a + b.toUpperCase();
170
+ }
171
+ return str.replaceAll(
172
+ // **NOTE: Match to _libfop.php
173
+ /([^A-Za-z'.0-9])([a-z])/g, recap_callback);
174
+ },
175
+ /**
176
+ * Replace all the non alphanumerical character by _
177
+ */
178
+ sanitizeToAlphaNumeric(input) {
179
+ return String(input).replaceAll(/[^\d\w]/g, '_');
180
+ },
181
+ /**
182
+ * Strip out all the non numerical characters
183
+ */
184
+ stripNonNumeric(input) {
185
+ return String(input).replaceAll(/[^\d]/g, '');
186
+ },
187
+ /**
188
+ * Strips all non ascii characters from a string
189
+ * @returns The ascii version of the string.
190
+ */
191
+ stripNonASCIICharacters(input) {
192
+ return String(input).replaceAll(/[\u0000-\u0019\u0080-\uffff]/g, '');
193
+ },
194
+ /**
195
+ * Shortens the @p text to @p length and appends an ellipses to it.
196
+ *
197
+ * The ellipses will only be appended if @p text is longer than the @p length
198
+ * given.
199
+ *
200
+ * @param val The string to reduce in size.
201
+ * @param length The maximal length desired.
202
+ * @returns The shortened @p text.
203
+ */
204
+ shortenText(val, length) {
205
+ // Remove extra spaces because they don't show up in html anyway.
206
+ const text = String(val).replaceAll(/\s+/g, ' ');
207
+ const truncatedText = text.substr(0, length - 3);
208
+ return text.length > length ? `${truncatedText}...` : text;
209
+ },
210
+ /**
211
+ * Returns the byte size of a character
212
+ * @param inputChar You can input more than one character, but it will only return the size of the first
213
+ * one.
214
+ * @returns Byte size of the character
215
+ */
216
+ getRawByteSize(inputChar) {
217
+ const onlyChar = String(inputChar);
218
+ const c = onlyChar.charCodeAt(0);
219
+ // If we are grabbing the byte size, we need to temporarily diable no-bitwise for linting
220
+ /* eslint-disable no-bitwise */
221
+ if (c < 1 << 7) {
222
+ return 1;
223
+ }
224
+ if (c < 1 << 11) {
225
+ return 2;
226
+ }
227
+ if (c < 1 << 16) {
228
+ return 3;
229
+ }
230
+ if (c < 1 << 21) {
231
+ return 4;
232
+ }
233
+ if (c < 1 << 26) {
234
+ return 5;
235
+ }
236
+ if (c < 1 << 31) {
237
+ return 6;
238
+ }
239
+ /* eslint-enable no-bitwise */
240
+ return Number.NaN;
241
+ },
242
+ /**
243
+ * Gets the length of a string in bytes, including non-ASCII characters
244
+ * @returns The number of bytes used by string
245
+ */
246
+ getByteLength(input) {
247
+ // Force string type
248
+ const stringInput = String(input);
249
+ const byteLength = Array.from(stringInput).reduce((acc, char) => acc + this.getRawByteSize(char), 0);
250
+ return byteLength;
251
+ },
252
+ /**
253
+ * Shortens the input by max byte size instead of by character length
254
+ * @param maxSize The max size in bytes, e.g. 256
255
+ * @returns Returns a shorted input if the input size exceeds the max
256
+ */
257
+ shortenByByte(input, maxSize) {
258
+ const stringInput = String(input);
259
+ let totalByteLength = 0;
260
+ for (let i = 0; i < stringInput.length; i++) {
261
+ const charByteSize = this.getRawByteSize(stringInput[i]);
262
+ if (charByteSize + totalByteLength > maxSize) {
263
+ // If the next character exceeds the limit, stop and return the truncated string.
264
+ return `${stringInput.substr(0, i - 3)}...`;
265
+ }
266
+ totalByteLength += charByteSize;
267
+ }
268
+ return stringInput;
269
+ },
270
+ /**
271
+ * Returns true if the haystack begins with the needle
272
+ *
273
+ * @param haystack The full string to be searched
274
+ * @param needle The case-sensitive string to search for
275
+ * @returns True if the haystack starts with the needle.
276
+ */
277
+ startsWith(haystack, needle) {
278
+ return this.isString(haystack) && this.isString(needle) && haystack.substring(0, needle.length) === needle;
279
+ },
280
+ /**
281
+ * Gets the textual value of the given string.
282
+ *
283
+ * @param str The string to fetch the text value from.
284
+ * @returns The text from within the HTML string.
285
+ */
286
+ stripHTML(str) {
287
+ if (!this.isString(str)) {
288
+ return '';
289
+ }
290
+ return str.replaceAll(/<[^>]*>?/gm, '');
291
+ },
292
+ /**
293
+ * Modifies the string so the first letter of the string is capitalized
294
+ *
295
+ * @param str The string to modify.
296
+ * @returns The recapitalized string.
297
+ */
298
+ UCFirst(str) {
299
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
300
+ },
301
+ /**
302
+ * Returns a string containing all the characters str from the beginning
303
+ * of str to the first occurrence of substr.
304
+ * Example: Str.cutAfter( 'hello$%world', '$%' ) // returns 'hello'
305
+ *
306
+ * @param str The string to modify.
307
+ * @param substr The substring to search for.
308
+ * @returns The cut/trimmed string.
309
+ */
310
+ cutAfter(str, substr) {
311
+ const index = str.indexOf(substr);
312
+ if (index !== -1) {
313
+ return str.substring(0, index);
314
+ }
315
+ return str;
316
+ },
317
+ /**
318
+ * Returns a string containing all the characters str from after the first
319
+ * occurrence of substr to the end of the string.
320
+ * Example: Str.cutBefore( 'hello$%world', '$%' ) // returns 'world'
321
+ *
322
+ * @param str The string to modify.
323
+ * @param substr The substring to search for.
324
+ * @returns The cut/trimmed string.
325
+ */
326
+ cutBefore(str, substr) {
327
+ const index = str.indexOf(substr);
328
+ if (index !== -1) {
329
+ return str.substring(index + substr.length);
330
+ }
331
+ return str;
332
+ },
333
+ /**
334
+ * Checks that the string is a domain name (e.g. example.com)
335
+ *
336
+ * @param str The string to check for domainnameness.
337
+ *
338
+ * @returns True if the string is a domain name
339
+ */
340
+ isValidDomainName(str) {
341
+ return !!String(str).match(Constants.CONST.REG_EXP.DOMAIN);
342
+ },
343
+ /**
344
+ * Checks that the string is a valid url
345
+ *
346
+ * @returns True if the string is a valid hyperlink
347
+ */
348
+ isValidURL(str) {
349
+ return !!String(str).match(Constants.CONST.REG_EXP.HYPERLINK);
350
+ },
351
+ /**
352
+ * Checks that the string is an email address.
353
+ * NOTE: TLDs are not just 2-4 characters. Keep this in sync with _inputrules.php
354
+ *
355
+ * @param str The string to check for email validity.
356
+ *
357
+ * @returns True if the string is an email
358
+ */
359
+ isValidEmail(str) {
360
+ if (!str || typeof str !== 'string') {
361
+ return false;
362
+ }
363
+ const unicodeVersion = Punycode.toUnicode(str);
364
+ if (String(unicodeVersion).match(Constants.CONST.REG_EXP.EMOJI_RULE)) {
365
+ return false;
366
+ }
367
+ return !!String(str).match(Constants.CONST.REG_EXP.EMAIL);
368
+ },
369
+ /**
370
+ * Checks if the string is an valid email address formed during comment markdown formation.
371
+ *
372
+ * @param str The string to check for email validity.
373
+ *
374
+ * @returns True if the string is an valid email created by comment markdown.
375
+ */
376
+ isValidEmailMarkdown(str) {
377
+ return !!String(str).match(`^${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}$`);
378
+ },
379
+ /**
380
+ * Remove trailing comma from a string.
381
+ *
382
+ * @param str The string with any trailing comma to be removed.
383
+ *
384
+ * @returns string with the trailing comma removed
385
+ */
386
+ removeTrailingComma(str) {
387
+ return str.trim().replaceAll(/(,$)/g, '');
388
+ },
389
+ /**
390
+ * Checks that the string is a list of coma separated email addresss.
391
+ *
392
+ * @param str The string to check for emails validity.
393
+ *
394
+ * @returns True if all emails are valid or if input is empty
395
+ */
396
+ areValidEmails(str) {
397
+ const string = this.removeTrailingComma(str);
398
+ if (string === '') {
399
+ return true;
400
+ }
401
+ const emails = string.split(',');
402
+ const result = emails.every((email) => this.isValidEmail(email.trim()));
403
+ return result;
404
+ },
405
+ /**
406
+ * Extract the email addresses from a string
407
+ */
408
+ extractEmail(str) {
409
+ return String(str).match(Constants.CONST.REG_EXP.EMAIL_SEARCH);
410
+ },
411
+ /**
412
+ * Extracts the domain name from the given email address
413
+ * (e.g. "domain.com" for "joe@domain.com").
414
+ *
415
+ * @param email The email address.
416
+ *
417
+ * @returns The domain name in the email address.
418
+ */
419
+ extractEmailDomain(email) {
420
+ return this.cutBefore(email, '@');
421
+ },
422
+ /**
423
+ * Tries to extract the company name from the given email address
424
+ * (e.g. "yelp" for "joe@yelp.co.uk").
425
+ *
426
+ * @param email The email address.
427
+ *
428
+ * @returns The company name in the email address or null.
429
+ */
430
+ extractCompanyNameFromEmailDomain(email) {
431
+ const domain = this.extractEmailDomain(email);
432
+ if (!domain) {
433
+ return null;
434
+ }
435
+ const domainParts = domain.split('.');
436
+ if (!domainParts.length) {
437
+ return null;
438
+ }
439
+ return domainParts[0];
440
+ },
441
+ /**
442
+ * Extracts the local part from the given email address
443
+ * (e.g. "joe" for "joe@domain.com").
444
+ *
445
+ * @param email The email address.
446
+ *
447
+ * @returns The local part in the email address.
448
+ */
449
+ extractEmailLocalPart(email) {
450
+ return this.cutAfter(email, '@');
451
+ },
452
+ /**
453
+ * Sanitize phone number to return only numbers. Return null if non valid phone number.
454
+ */
455
+ sanitizePhoneNumber(str) {
456
+ const string = str.replaceAll(/(?!^\+)\D/g, '');
457
+ return string.length <= 15 && string.length >= 10 ? string : null;
458
+ },
459
+ /**
460
+ * Sanitize email. Return null if non valid email.
461
+ */
462
+ sanitizeEmail(str) {
463
+ const string = str.toLowerCase().trim();
464
+ return Constants.CONST.REG_EXP.EMAIL.test(string) ? string : null;
465
+ },
466
+ /**
467
+ * Escapes all special RegExp characters from a string
468
+ *
469
+ * @param str The subject
470
+ *
471
+ * @returns The escaped string
472
+ */
473
+ escapeForRegExp(str) {
474
+ return str.replaceAll(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
475
+ },
476
+ /**
477
+ * Escapes all special RegExp characters from a string except for the period
478
+ *
479
+ * @param str The subject
480
+ * @returns The escaped string
481
+ */
482
+ escapeForExpenseRule(str) {
483
+ return str.replaceAll(/[-[\]/{}()*+?\\^$|]/g, '\\$&');
484
+ },
485
+ /**
486
+ * Adds a backslash in front of each of colon
487
+ * if they don't already have a backslash in front of them
488
+ *
489
+ * @param str The subject
490
+ * @returns The escaped string
491
+ */
492
+ addBackslashBeforeColonsForTagNamesComingFromQBD(str) {
493
+ return str.replaceAll(/([^\\]):/g, '$1\\:');
494
+ },
495
+ /**
496
+ * Removes backslashes from string
497
+ * eg: myString\[\]\* -> myString[]*
498
+ */
499
+ stripBackslashes(str) {
500
+ return str.replaceAll('\\', '');
501
+ },
502
+ /**
503
+ * Checks if a string's length is in the specified range
504
+ *
505
+ * @returns true if the length is in the range, false otherwise
506
+ */
507
+ isOfLength(str, minimumLength, maximumLength) {
508
+ if (!this.isString(str)) {
509
+ return false;
510
+ }
511
+ if (str.length < minimumLength) {
512
+ return false;
513
+ }
514
+ if (!this.isUndefined(maximumLength) && str.length > maximumLength) {
515
+ return false;
516
+ }
517
+ return true;
518
+ },
519
+ /**
520
+ * Count the number of occurences of needle in haystack.
521
+ * This is faster than counting the results of haystack.match( /needle/g )
522
+ * via http://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
523
+ *
524
+ * @param haystack The string to look inside of
525
+ * @param needle What we're looking for
526
+ * @param allowOverlapping Defaults to false
527
+ *
528
+ * @returns The number of times needle is in haystack.
529
+ */
530
+ occurences(haystack, needle, allowOverlapping) {
531
+ let count = 0;
532
+ let pos = 0;
533
+ // Force strings for input
534
+ const haystackStr = String(haystack);
535
+ const needleStr = String(needle);
536
+ if (needleStr.length <= 0) {
537
+ return haystackStr.length + 1;
538
+ }
539
+ const step = allowOverlapping ? 1 : needleStr.length;
540
+ while (pos >= 0) {
541
+ pos = haystackStr.indexOf(needleStr, pos);
542
+ if (pos >= 0) {
543
+ count += 1;
544
+ pos += step;
545
+ }
546
+ }
547
+ return count;
548
+ },
549
+ /**
550
+ * Uppercases the first letter of each word
551
+ * via https://github.com/kvz/phpjs/blob/master/functions/strings/ucwords.js
552
+ *
553
+ * @param str to uppercase words
554
+ * @returns Uppercase worded string
555
+ */
556
+ ucwords(str) {
557
+ const capitalize = ($1) => $1.toUpperCase();
558
+ return String(str).replaceAll(/^([a-z\u00E0-\u00FC])|\s+([a-z\u00E0-\u00FC])/g, capitalize);
559
+ },
560
+ /**
561
+ * Returns true if the haystack contains the needle
562
+ *
563
+ * @param haystack The full string to be searched
564
+ * @param needle The case-sensitive string to search for
565
+ *
566
+ * @returns Returns true if the haystack contains the needle
567
+ */
568
+ contains(haystack, needle) {
569
+ return haystack.indexOf(needle) !== -1;
570
+ },
571
+ /**
572
+ * Returns true if the haystack contains the needle, ignoring case
573
+ *
574
+ * @param haystack The full string to be searched
575
+ * @param needle The case-insensitive string to search for
576
+ *
577
+ * @returns Returns true if the haystack contains the needle, ignoring case
578
+ */
579
+ caseInsensitiveContains(haystack, needle) {
580
+ return this.contains(haystack.toLowerCase(), needle.toLowerCase());
581
+ },
582
+ /**
583
+ * Case insensitive compare function
584
+ *
585
+ * @param strA string to compare
586
+ * @param strB string to compare
587
+ *
588
+ * @returns -1 if first string < second string
589
+ * 1 if first string > second string
590
+ * 0 if first string = second string
591
+ */
592
+ caseInsensitiveCompare(strA, strB) {
593
+ const lowerCaseStrA = strA.toLocaleLowerCase();
594
+ const lowerCaseStrB = strB.toLocaleLowerCase();
595
+ return this.compare(lowerCaseStrA, lowerCaseStrB);
596
+ },
597
+ /**
598
+ * Case insensitive equals
599
+ *
600
+ * @param strA string to compare
601
+ * @param strB string to compare
602
+ * @returns true when first == second except for case
603
+ */
604
+ caseInsensitiveEquals(strA, strB) {
605
+ return this.caseInsensitiveCompare(strA, strB) === 0;
606
+ },
607
+ /**
608
+ * Compare function
609
+ *
610
+ * @param strA string to compare
611
+ * @param strB string to compare
612
+ *
613
+ * @returns -1 if first string < second string
614
+ * 1 if first string > second string
615
+ * 0 if first string = second string
616
+ */
617
+ compare(strA, strB) {
618
+ if (strA < strB) {
619
+ return -1;
620
+ }
621
+ if (strA > strB) {
622
+ return 1;
623
+ }
624
+ return 0;
625
+ },
626
+ /**
627
+ * Check if a file extension is supported by SmartReports
628
+ */
629
+ isFileExtensionSmartReportsValid(filename) {
630
+ var _a;
631
+ // Allowed extensions. Make sure to keep them in sync with those defined
632
+ // in SmartReport_Utils::templateFileUploadCheck()
633
+ const allowedExtensions = ['xls', 'xlsx', 'xlsm', 'xltm'];
634
+ const extension = (_a = filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
635
+ return !!extension && allowedExtensions.indexOf(extension) > -1;
636
+ },
637
+ /**
638
+ * Mask Permanent Account Number (PAN) the same way Auth does
639
+ * @param num account number
640
+ * @returns masked account number
641
+ */
642
+ maskPAN(num) {
643
+ // cast to string
644
+ const accountNumber = String(num);
645
+ const len = accountNumber.length;
646
+ // Hide these numbers completely
647
+ // We should not be getting account numbers this small or large
648
+ if (len < 6 || len > 20) {
649
+ return this.maskFirstNCharacters(accountNumber, len, 'X');
650
+ }
651
+ // Can show last 4
652
+ if (len < 14) {
653
+ return this.maskFirstNCharacters(accountNumber, len - 4, 'X');
654
+ }
655
+ // Can show first 6 and last 4
656
+ const first = accountNumber.substr(0, 6);
657
+ const last = accountNumber.substr(7);
658
+ const masked = this.maskFirstNCharacters(last, len - 11, 'X');
659
+ return `${first}${masked}`;
660
+ },
661
+ /**
662
+ * Checks if something is a string
663
+ * Stolen from underscore
664
+ */
665
+ isString(obj) {
666
+ return this.isTypeOf(obj, 'String');
667
+ },
668
+ /**
669
+ * Checks if something is a number
670
+ * Stolen from underscore
671
+ * @param obj
672
+ */
673
+ isNumber(obj) {
674
+ return this.isTypeOf(obj, 'Number');
675
+ },
676
+ /**
677
+ * Checks if something is a certain type
678
+ * Stolen from underscore
679
+ */
680
+ isTypeOf(obj, type) {
681
+ return Object.prototype.toString.call(obj) === `[object ${type}]`;
682
+ },
683
+ /**
684
+ * Checks to see if something is undefined
685
+ * Stolen from underscore
686
+ */
687
+ isUndefined(obj) {
688
+ // eslint-disable-next-line no-void
689
+ return obj === void 0;
690
+ },
691
+ /**
692
+ * Replace first N characters of the string with maskChar
693
+ * eg: maskFirstNCharacters( '1234567890', 6, 'X' ) yields XXXXXX7890
694
+ * @param str String to mask
695
+ * @param num Number of characters we want to mask from the string
696
+ * @param mask String we want replace the first N chars with
697
+ * @returns Masked string
698
+ */
699
+ maskFirstNCharacters(str, num, mask) {
700
+ // if str is empty, str or mask aren't strings,
701
+ // or n is not a number, do nothing
702
+ if (!this.isString(str) || !this.isString(mask) || str.length === 0 || !this.isNumber(num)) {
703
+ return str;
704
+ }
705
+ return str.substring(0, num).replaceAll(/./g, mask) + str.substring(num);
706
+ },
707
+ /**
708
+ * Trim a string
709
+ */
710
+ trim(str) {
711
+ return str.trim();
712
+ },
713
+ /**
714
+ * Convert a percentage string like '25%' to 25/
715
+ * @param percentageString The percentage as a string
716
+ */
717
+ percentageStringToNumber(percentageString) {
718
+ return Number(this.cutAfter(percentageString, '%'));
719
+ },
720
+ /**
721
+ * Remove all the spaces from a string
722
+ */
723
+ removeSpaces(input) {
724
+ return String(input).replace(' ', '');
725
+ },
726
+ /**
727
+ * Returns the proper phrase depending on the count that is passed.
728
+ * Example:
729
+ * console.log(Str.pluralize('puppy', 'puppies', 1)) { // puppy
730
+ * console.log(Str.pluralize('puppy', 'puppies', 3)) { // puppies
731
+ *
732
+ * @param singular form of the phrase
733
+ * @param plural form of the phrase
734
+ * @param num the count which determines the plurality
735
+ */
736
+ pluralize(singular, plural, num) {
737
+ if (!num || num > 1) {
738
+ return plural;
739
+ }
740
+ return singular;
741
+ },
742
+ /**
743
+ * Returns whether or not a string is an encrypted number or not.
744
+ *
745
+ * @param num that we want to see if its encrypted or not
746
+ *
747
+ * @returns Whether or not this string is an encrpypted number
748
+ */
749
+ isEncryptedCardNumber(num) {
750
+ // Older encrypted versioning.
751
+ if (/^[\da-fA-F]+$/.test(num)) {
752
+ return num.length % 32 === 0;
753
+ }
754
+ // Check with the new versioning.
755
+ if (/^[vV][\d]+:[\da-fA-F]+$/.test(num)) {
756
+ return num.split(':')[1].length % 32 === 0;
757
+ }
758
+ return false;
759
+ },
760
+ /**
761
+ * Converts a value to boolean, case-insensitive.
762
+ */
763
+ toBool(value) {
764
+ if (this.isString(value)) {
765
+ return value.toLowerCase() === 'true';
766
+ }
767
+ return !!value;
768
+ },
769
+ /**
770
+ * Checks if a string could be the masked version of another one.
771
+ *
772
+ * @param strA String to compare
773
+ * @param strB String to compare
774
+ * @param [mask] Defaults to X
775
+ * @returns True when first could be the masked version of second
776
+ */
777
+ maskedEquals(strA, strB, mask) {
778
+ var _a, _b;
779
+ const firsts = (_a = strA.match(/.{1,1}/g)) !== null && _a !== void 0 ? _a : [];
780
+ const seconds = (_b = strB.match(/.{1,1}/g)) !== null && _b !== void 0 ? _b : [];
781
+ const defaultMask = mask || 'X';
782
+ if (firsts.length !== seconds.length) {
783
+ return false;
784
+ }
785
+ for (let i = 0; i < firsts.length; i += 1) {
786
+ if (firsts[i] !== seconds[i] && firsts[i] !== defaultMask && seconds[i] !== defaultMask) {
787
+ return false;
788
+ }
789
+ }
790
+ return true;
791
+ },
792
+ /**
793
+ * Bold any word matching the regexp in the text.
794
+ */
795
+ boldify(text, regexp) {
796
+ return text.replace(regexp, '<strong>$1</strong>');
797
+ },
798
+ /**
799
+ * Check for whether a phone number is valid.
800
+ * @deprecated use isValidE164Phone to validate E.164 phone numbers or isValidPhoneFormat to validate phone numbers in general
801
+ */
802
+ isValidPhone(phone) {
803
+ return Constants.CONST.SMS.E164_REGEX.test(phone);
804
+ },
805
+ /**
806
+ * Check for whether a phone number is valid.
807
+ */
808
+ isValidPhoneNumber(phone) {
809
+ return parsePhoneNumber(phone).possible;
810
+ },
811
+ /**
812
+ * Check for whether a phone number is valid according to E.164 standard.
813
+ */
814
+ isValidE164Phone(phone) {
815
+ return Constants.CONST.SMS.E164_REGEX.test(phone);
816
+ },
817
+ /**
818
+ * Check for whether a phone number is valid in different formats/standards. For example:
819
+ * significant: 4404589784
820
+ * international: +1 440-458-9784
821
+ * e164: +14404589784
822
+ * national: (440) 458-9784
823
+ * 123.456.7890
824
+ */
825
+ isValidPhoneFormat(phone) {
826
+ return Constants.CONST.REG_EXP.GENERAL_PHONE_PART.test(phone);
827
+ },
828
+ /**
829
+ * We validate mentions by checking if it's first character is an allowed character.
830
+ */
831
+ isValidMention(mention) {
832
+ // Mentions can start @ proceeded by a space, eg "ping @user@domain.tld"
833
+ if (/[\s@]/g.test(mention.charAt(0))) {
834
+ return true;
835
+ }
836
+ // Mentions can also start and end with a *, _, ~, ', or " (with no other preceding characters)
837
+ // eg "ping *@user@domain.tld*"
838
+ const firstChar = mention.charAt(0);
839
+ const lastChar = mention.charAt(mention.length - 1);
840
+ return /[*~_'"]/g.test(firstChar) && /[*~_'"]/g.test(lastChar) && firstChar === lastChar;
841
+ },
842
+ /**
843
+ * Returns text without our SMS domain
844
+ */
845
+ removeSMSDomain(text) {
846
+ return text.replaceAll(REMOVE_SMS_DOMAIN_PATTERN, '');
847
+ },
848
+ /**
849
+ * Returns true if the text is a valid E.164 phone number with our SMS domain removed
850
+ */
851
+ isSMSLogin(text) {
852
+ return this.isValidE164Phone(this.removeSMSDomain(text));
853
+ },
854
+ /**
855
+ * This method will return all matches of a single regex like preg_match_all() in PHP. This is not a common part of
856
+ * JS yet, so this is a good way of doing it according to
857
+ * https://github.com/airbnb/javascript/issues/1439#issuecomment-306297399 and doesn't get us in trouble with
858
+ * linting rules.
859
+ */
860
+ matchAll(str, regex) {
861
+ const matches = [];
862
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
863
+ const collectMatches = (...args) => {
864
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
865
+ const match = Array.prototype.slice.call(args, 0, -2);
866
+ match.input = args[args.length - 1];
867
+ match.index = args[args.length - 2];
868
+ matches.push(match);
869
+ };
870
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
871
+ str.replace(regex, collectMatches);
872
+ return matches;
873
+ },
874
+ /**
875
+ * A simple GUID generator taken from https://stackoverflow.com/a/32760401/9114791
876
+ *
877
+ * @param [prefix] an optional prefix to put in front of the guid
878
+ */
879
+ guid(prefix = '') {
880
+ function s4() {
881
+ return Math.floor((1 + Math.random()) * 0x10000)
882
+ .toString(16)
883
+ .substring(1);
884
+ }
885
+ return `${prefix}${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
886
+ },
887
+ /**
888
+ * Takes in a URL and returns it with a leading '/'
889
+ *
890
+ * @param url The URL to be formatted
891
+ * @returns The formatted URL
892
+ */
893
+ normalizeUrl(url) {
894
+ return typeof url === 'string' && url.startsWith('/') ? url : `/${url}`;
895
+ },
896
+ /**
897
+ * Formats a URL by converting the domain name to lowercase and adding the missing 'https://' protocol.
898
+ *
899
+ * @param url The URL to be formatted
900
+ * @param defaultScheme The Scheme to use in the URL
901
+ * @returns The formatted URL
902
+ */
903
+ sanitizeURL(url, defaultScheme = 'https') {
904
+ const regex = new RegExp(`^${UrlPatterns.URL_REGEX}$`, 'i');
905
+ const match = regex.exec(url);
906
+ if (!match) {
907
+ return url;
908
+ }
909
+ const website = match[3] ? match[2] : `${defaultScheme}://${match[2]}`;
910
+ return website.toLowerCase() + this.cutBefore(match[1], match[2]);
911
+ },
912
+ /**
913
+ * Checks if parameter is a string or function
914
+ * if it is a function then we will call it with
915
+ * any additional arguments.
916
+ */
917
+ result: resultFn,
918
+ /**
919
+ * Get file extension for a given url with or
920
+ * without query parameters
921
+ */
922
+ getExtension(url) {
923
+ var _a, _b;
924
+ if (typeof url !== 'string') {
925
+ Log.warn('Str.getExtension: url is not a string', { url });
926
+ return undefined;
927
+ }
928
+ return (_b = (_a = url.split('.').pop()) === null || _a === void 0 ? void 0 : _a.split('?')[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
929
+ },
930
+ /**
931
+ * Takes in a URL and checks if the file extension is PDF
932
+ *
933
+ * @param url The URL to be checked
934
+ * @returns Whether file path is PDF or not
935
+ */
936
+ isPDF(url) {
937
+ return this.getExtension(url) === 'pdf';
938
+ },
939
+ /**
940
+ * Takes in a URL and checks if the file extension is an image
941
+ * that can be rendered by React Native. Do NOT add extensions
942
+ * to this list unless they appear in this list and are
943
+ * supported by all platforms.
944
+ *
945
+ * https://reactnative.dev/docs/image#source
946
+ */
947
+ isImage(url) {
948
+ const extension = this.getExtension(url);
949
+ if (!extension) {
950
+ return false;
951
+ }
952
+ return ['jpeg', 'jpg', 'gif', 'png', 'bmp', 'webp'].includes(extension);
953
+ },
954
+ /**
955
+ * Takes in a URL and checks if the file extension is a video
956
+ * that can be rendered by React Native. Do NOT add extensions
957
+ * to this list unless they are supported by all platforms.
958
+ *
959
+ * https://developer.android.com/media/platform/supported-formats#video-formats
960
+ * https://developer.apple.com/documentation/coremedia/1564239-video_codec_constants
961
+ * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs
962
+ */
963
+ isVideo(url) {
964
+ const extension = this.getExtension(url);
965
+ if (!extension) {
966
+ return false;
967
+ }
968
+ return ['mov', 'mp4', 'webm', 'mkv'].includes(extension);
969
+ },
970
+ /**
971
+ * Checks whether the given string is a +@ domain email account, such as
972
+ * +@domain.com
973
+ *
974
+ * @returns True if is a domain account email, otherwise false.
975
+ */
976
+ isDomainEmail(email) {
977
+ return this.startsWith(email, '+@');
978
+ },
979
+ /**
980
+ * Find the minimum indentation of any line in the string,
981
+ * and remove that number of leading spaces from every line in the string.
982
+ *
983
+ * It also removes at most one leading newline, to reflect a common usage:
984
+ *
985
+ * ```
986
+ * Str.dedent(`
987
+ * const myIndentedStr = 'Hello, world!';
988
+ * console.log(myIndentedStr);
989
+ * `)
990
+ * ```
991
+ *
992
+ * This implementation assumes you'd want that to be:
993
+ *
994
+ * ```
995
+ * const myIndentedStr = 'Hello, world!';
996
+ * console.log(myIndentedStr);
997
+ *
998
+ * ```
999
+ *
1000
+ * Rather than:
1001
+ *
1002
+ * ```
1003
+ *
1004
+ * const myIndentedStr = 'Hello, world!';
1005
+ * console.log(myIndentedStr);
1006
+ *
1007
+ * ```
1008
+ */
1009
+ dedent(str) {
1010
+ var _a, _b;
1011
+ // Remove at most one leading newline
1012
+ const stringWithoutLeadingNewlines = str.replaceAll(/^\r?\n/g, '');
1013
+ // Split string by remaining newlines
1014
+ const lines = stringWithoutLeadingNewlines.replaceAll('\r\n', '\n').split('\n');
1015
+ // Find the minimum indentation of non-empty lines
1016
+ let minIndent = Number.MAX_SAFE_INTEGER;
1017
+ for (const line of lines) {
1018
+ if (line.trim().length === 0) {
1019
+ continue;
1020
+ }
1021
+ const indentation = (_b = (_a = line.match(/^ */)) === null || _a === void 0 ? void 0 : _a[0].length) !== null && _b !== void 0 ? _b : 0;
1022
+ if (indentation < minIndent) {
1023
+ minIndent = indentation;
1024
+ }
1025
+ }
1026
+ // Remove the common indentation
1027
+ return lines.map((line) => line.slice(minIndent)).join('\n');
1028
+ },
1029
+ /**
1030
+ * Polyfill for String.prototype.replaceAll
1031
+ */
1032
+ replaceAll(text, searchValue, replaceValue) {
1033
+ return String.prototype.replaceAll.call(text, searchValue, replaceValue);
1034
+ },
1035
+ };
1036
+ export default Str;