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 = 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(([]: 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.     delete current.id;
    
  146.     delete current.isStateEditable;
    
  147. 
    
  148.     if (current.subHooks.length > 0) {
    
  149.       queue.push(...current.subHooks);
    
  150.     }
    
  151.   }
    
  152. 
    
  153.   sanitize(cloned);
    
  154. 
    
  155.   try {
    
  156.     return JSON.stringify(cloned, null, 2);
    
  157.   } catch (error) {
    
  158.     return '';
    
  159.   }
    
  160. }
    
  161. 
    
  162. // Keeping this in memory seems to be enough to enable the browser to download larger profiles.
    
  163. // Without this, we would see a "Download failed: network error" failure.
    
  164. let downloadUrl = null;
    
  165. 
    
  166. export function downloadFile(
    
  167.   element: HTMLAnchorElement,
    
  168.   filename: string,
    
  169.   text: string,
    
  170. ): void {
    
  171.   const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
    
  172. 
    
  173.   if (downloadUrl !== null) {
    
  174.     URL.revokeObjectURL(downloadUrl);
    
  175.   }
    
  176. 
    
  177.   downloadUrl = URL.createObjectURL(blob);
    
  178. 
    
  179.   element.setAttribute('href', downloadUrl);
    
  180.   element.setAttribute('download', filename);
    
  181. 
    
  182.   element.click();
    
  183. }
    
  184. 
    
  185. export function truncateText(text: string, maxLength: number): string {
    
  186.   const {length} = text;
    
  187.   if (length > maxLength) {
    
  188.     return (
    
  189.       text.slice(0, Math.floor(maxLength / 2)) +
    
  190.       '' +
    
  191.       text.slice(length - Math.ceil(maxLength / 2) - 1)
    
  192.     );
    
  193.   } else {
    
  194.     return text;
    
  195.   }
    
  196. }