ccfd2dc94ef44707d9c3abd3109e6f650f4c6253
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Base / Utilities.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 var emDash = "\u2014";
27 var enDash = "\u2013";
28 var figureDash = "\u2012";
29 var ellipsis = "\u2026";
30
31 Object.defineProperty(Object, "shallowCopy",
32 {
33     value: function(object)
34     {
35         // Make a new object and copy all the key/values. The values are not copied.
36         var copy = {};
37         var keys = Object.keys(object);
38         for (var i = 0; i < keys.length; ++i)
39             copy[keys[i]] = object[keys[i]];
40         return copy;
41     }
42 });
43
44 Object.defineProperty(Object, "shallowEqual",
45 {
46     value: function(a, b)
47     {
48         // Checks if two objects have the same top-level properties.
49
50         // Only objects can proceed.
51         if (!(a instanceof Object) || !(b instanceof Object))
52             return false;
53
54         // Check for strict equality in case they are the same object.
55         if (a === b)
56             return true;
57
58         // Use an optimized version of shallowEqual for arrays.
59         if (Array.isArray(a) && Array.isArray(b))
60             return Array.shallowEqual(a, b);
61
62         if (a.constructor !== b.constructor)
63             return false;
64
65         var aKeys = Object.keys(a);
66         var bKeys = Object.keys(b);
67
68         // Check that each object has the same number of keys.
69         if (aKeys.length !== bKeys.length)
70             return false;
71
72         // Check if all the keys and their values are equal.
73         for (var i = 0; i < aKeys.length; ++i) {
74             // Check that b has the same key as a.
75             if (!(aKeys[i] in b))
76                 return false;
77
78             // Check that the values are strict equal since this is only
79             // a shallow check, not a recursive one.
80             if (a[aKeys[i]] !== b[aKeys[i]])
81                 return false;
82         }
83
84         return true;
85     }
86 });
87
88 Object.defineProperty(Object.prototype, "valueForCaseInsensitiveKey",
89 {
90     value: function(key)
91     {
92         if (this.hasOwnProperty(key))
93             return this[key];
94
95         var lowerCaseKey = key.toLowerCase();
96         for (var currentKey in this) {
97             if (currentKey.toLowerCase() === lowerCaseKey)
98                 return this[currentKey];
99         }
100
101         return undefined;
102     }
103 });
104
105 Object.defineProperty(Map, "fromObject",
106 {
107     value: function(object)
108     {
109         let map = new Map;
110         for (let key in object)
111             map.set(key, object[key]);
112         return map;
113     }
114 });
115
116 Object.defineProperty(Map.prototype, "take",
117 {
118     value: function(key)
119     {
120         var deletedValue = this.get(key);
121         this.delete(key);
122         return deletedValue;
123     }
124 });
125
126 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithClass",
127 {
128     value: function(className)
129     {
130         for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
131             if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(className))
132                 return node;
133         return null;
134     }
135 });
136
137 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithNodeNameInArray",
138 {
139     value: function(nameArray)
140     {
141         var lowerCaseNameArray = nameArray.map(function(name) { return name.toLowerCase(); });
142         for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) {
143             for (var i = 0; i < nameArray.length; ++i) {
144                 if (node.nodeName.toLowerCase() === lowerCaseNameArray[i])
145                     return node;
146             }
147         }
148
149         return null;
150     }
151 });
152
153 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithNodeName",
154 {
155     value: function(nodeName)
156     {
157         return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
158     }
159 });
160
161 Object.defineProperty(Node.prototype, "isAncestor",
162 {
163     value: function(node)
164     {
165         if (!node)
166             return false;
167
168         var currentNode = node.parentNode;
169         while (currentNode) {
170             if (this === currentNode)
171                 return true;
172             currentNode = currentNode.parentNode;
173         }
174
175         return false;
176     }
177 });
178
179 Object.defineProperty(Node.prototype, "isDescendant",
180 {
181     value: function(descendant)
182     {
183         return !!descendant && descendant.isAncestor(this);
184     }
185 });
186
187
188 Object.defineProperty(Node.prototype, "isSelfOrAncestor",
189 {
190     value: function(node)
191     {
192         return !!node && (node === this || this.isAncestor(node));
193     }
194 });
195
196
197 Object.defineProperty(Node.prototype, "isSelfOrDescendant",
198 {
199     value: function(node)
200     {
201         return !!node && (node === this || this.isDescendant(node));
202     }
203 });
204
205 Object.defineProperty(Node.prototype, "traverseNextNode",
206 {
207     value: function(stayWithin)
208     {
209         var node = this.firstChild;
210         if (node)
211             return node;
212
213         if (stayWithin && this === stayWithin)
214             return null;
215
216         node = this.nextSibling;
217         if (node)
218             return node;
219
220         node = this;
221         while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
222             node = node.parentNode;
223         if (!node)
224             return null;
225
226         return node.nextSibling;
227     }
228 });
229
230 Object.defineProperty(Node.prototype, "traversePreviousNode",
231 {
232     value: function(stayWithin)
233     {
234        if (stayWithin && this === stayWithin)
235             return null;
236         var node = this.previousSibling;
237         while (node && node.lastChild)
238             node = node.lastChild;
239         if (node)
240             return node;
241         return this.parentNode;
242     }
243 });
244
245
246 Object.defineProperty(Node.prototype, "rangeOfWord",
247 {
248     value: function(offset, stopCharacters, stayWithinNode, direction)
249     {
250         var startNode;
251         var startOffset = 0;
252         var endNode;
253         var endOffset = 0;
254
255         if (!stayWithinNode)
256             stayWithinNode = this;
257
258         if (!direction || direction === "backward" || direction === "both") {
259             var node = this;
260             while (node) {
261                 if (node === stayWithinNode) {
262                     if (!startNode)
263                         startNode = stayWithinNode;
264                     break;
265                 }
266
267                 if (node.nodeType === Node.TEXT_NODE) {
268                     var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
269                     for (var i = start; i >= 0; --i) {
270                         if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
271                             startNode = node;
272                             startOffset = i + 1;
273                             break;
274                         }
275                     }
276                 }
277
278                 if (startNode)
279                     break;
280
281                 node = node.traversePreviousNode(stayWithinNode);
282             }
283
284             if (!startNode) {
285                 startNode = stayWithinNode;
286                 startOffset = 0;
287             }
288         } else {
289             startNode = this;
290             startOffset = offset;
291         }
292
293         if (!direction || direction === "forward" || direction === "both") {
294             node = this;
295             while (node) {
296                 if (node === stayWithinNode) {
297                     if (!endNode)
298                         endNode = stayWithinNode;
299                     break;
300                 }
301
302                 if (node.nodeType === Node.TEXT_NODE) {
303                     var start = (node === this ? offset : 0);
304                     for (var i = start; i < node.nodeValue.length; ++i) {
305                         if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
306                             endNode = node;
307                             endOffset = i;
308                             break;
309                         }
310                     }
311                 }
312
313                 if (endNode)
314                     break;
315
316                 node = node.traverseNextNode(stayWithinNode);
317             }
318
319             if (!endNode) {
320                 endNode = stayWithinNode;
321                 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
322             }
323         } else {
324             endNode = this;
325             endOffset = offset;
326         }
327
328         var result = this.ownerDocument.createRange();
329         result.setStart(startNode, startOffset);
330         result.setEnd(endNode, endOffset);
331
332         return result;
333
334     }
335 });
336
337 Object.defineProperty(Element.prototype, "realOffsetWidth",
338 {
339     get: function()
340     {
341         return this.getBoundingClientRect().width;
342     }
343 });
344
345 Object.defineProperty(Element.prototype, "realOffsetHeight",
346 {
347     get: function()
348     {
349         return this.getBoundingClientRect().height;
350     }
351 });
352
353 Object.defineProperty(Element.prototype, "totalOffsetLeft",
354 {
355     get: function()
356     {
357         return this.getBoundingClientRect().left;
358     }
359 });
360
361 Object.defineProperty(Element.prototype, "totalOffsetTop",
362 {
363     get: function()
364     {
365         return this.getBoundingClientRect().top;
366     }
367 });
368
369 Object.defineProperty(Element.prototype, "removeChildren",
370 {
371     value: function()
372     {
373         // This has been tested to be the fastest removal method.
374         if (this.firstChild)
375             this.textContent = "";
376     }
377 });
378
379 Object.defineProperty(Element.prototype, "isInsertionCaretInside",
380 {
381     value: function()
382     {
383         var selection = window.getSelection();
384         if (!selection.rangeCount || !selection.isCollapsed)
385             return false;
386         var selectionRange = selection.getRangeAt(0);
387         return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
388     }
389 });
390
391 Object.defineProperty(Element.prototype, "removeMatchingStyleClasses",
392 {
393     value: function(classNameRegex)
394     {
395         var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
396         if (regex.test(this.className))
397             this.className = this.className.replace(regex, " ");
398     }
399 });
400
401 Object.defineProperty(Element.prototype, "createChild",
402 {
403     value: function(elementName, className)
404     {
405         var element = this.ownerDocument.createElement(elementName);
406         if (className)
407             element.className = className;
408         this.appendChild(element);
409         return element;
410     }
411 });
412
413 Object.defineProperty(Element.prototype, "isScrolledToBottom",
414 {
415     value: function()
416     {
417         // This code works only for 0-width border
418         return this.scrollTop + this.clientHeight === this.scrollHeight;
419     }
420 });
421
422 Object.defineProperty(Element.prototype, "recalculateStyles",
423 {
424     value: function()
425     {
426         this.ownerDocument.defaultView.getComputedStyle(this);
427     }
428 });
429
430 Object.defineProperty(DocumentFragment.prototype, "createChild",
431 {
432     value: Element.prototype.createChild
433 });
434
435 Object.defineProperty(Array, "shallowEqual",
436 {
437     value: function(a, b)
438     {
439         if (!Array.isArray(a) || !Array.isArray(b))
440             return false;
441
442         if (a === b)
443             return true;
444
445         let length = a.length;
446
447         if (length !== b.length)
448             return false;
449
450         for (var i = 0; i < length; ++i) {
451             if (a[i] !== b[i])
452                 return false;
453         }
454
455         return true;
456     }
457 });
458
459 Object.defineProperty(Array.prototype, "lastValue",
460 {
461     get: function()
462     {
463         if (!this.length)
464             return undefined;
465         return this[this.length - 1];
466     }
467 });
468
469 Object.defineProperty(Array.prototype, "remove",
470 {
471     value: function(value, onlyFirst)
472     {
473         for (var i = this.length - 1; i >= 0; --i) {
474             if (this[i] === value) {
475                 this.splice(i, 1);
476                 if (onlyFirst)
477                     return;
478             }
479         }
480     }
481 });
482
483 Object.defineProperty(Array.prototype, "toggleIncludes",
484 {
485     value: function(value, force)
486     {
487         let exists = this.includes(value);
488         if (exists === !!force)
489             return;
490
491         if (exists)
492             this.remove(value);
493         else
494             this.push(value);
495     }
496 });
497
498 Object.defineProperty(Array.prototype, "insertAtIndex",
499 {
500     value: function(value, index)
501     {
502         this.splice(index, 0, value);
503     }
504 });
505
506 Object.defineProperty(Array.prototype, "keySet",
507 {
508     value: function()
509     {
510         let keys = Object.create(null);
511         for (var i = 0; i < this.length; ++i)
512             keys[this[i]] = true;
513         return keys;
514     }
515 });
516
517 Object.defineProperty(Array.prototype, "partition",
518 {
519     value: function(callback)
520     {
521         let positive = [];
522         let negative = [];
523         for (let i = 0; i < this.length; ++i) {
524             let value = this[i];
525             if (callback(value))
526                 positive.push(value);
527             else
528                 negative.push(value);
529         }
530         return [positive, negative];
531     }
532 });
533
534 Object.defineProperty(String.prototype, "isLowerCase",
535 {
536     value: function()
537     {
538         return String(this) === this.toLowerCase();
539     }
540 });
541
542 Object.defineProperty(String.prototype, "isUpperCase",
543 {
544     value: function()
545     {
546         return String(this) === this.toUpperCase();
547     }
548 });
549
550 Object.defineProperty(String.prototype, "trimMiddle",
551 {
552     value: function(maxLength)
553     {
554         if (this.length <= maxLength)
555             return this;
556         var leftHalf = maxLength >> 1;
557         var rightHalf = maxLength - leftHalf - 1;
558         return this.substr(0, leftHalf) + ellipsis + this.substr(this.length - rightHalf, rightHalf);
559     }
560 });
561
562 Object.defineProperty(String.prototype, "trimEnd",
563 {
564     value: function(maxLength)
565     {
566         if (this.length <= maxLength)
567             return this;
568         return this.substr(0, maxLength - 1) + ellipsis;
569     }
570 });
571
572 Object.defineProperty(String.prototype, "truncate",
573 {
574     value: function(maxLength)
575     {
576         "use strict";
577
578         if (this.length <= maxLength)
579             return this;
580
581         let clipped = this.slice(0, maxLength);
582         let indexOfLastWhitespace = clipped.search(/\s\S*$/);
583         if (indexOfLastWhitespace > Math.floor(maxLength / 2))
584             clipped = clipped.slice(0, indexOfLastWhitespace - 1);
585
586         return clipped + ellipsis;
587     }
588 });
589
590 Object.defineProperty(String.prototype, "collapseWhitespace",
591 {
592     value: function()
593     {
594         return this.replace(/[\s\xA0]+/g, " ");
595     }
596 });
597
598 Object.defineProperty(String.prototype, "removeWhitespace",
599 {
600     value: function()
601     {
602         return this.replace(/[\s\xA0]+/g, "");
603     }
604 });
605
606 Object.defineProperty(String.prototype, "escapeCharacters",
607 {
608     value: function(chars)
609     {
610         var foundChar = false;
611         for (var i = 0; i < chars.length; ++i) {
612             if (this.indexOf(chars.charAt(i)) !== -1) {
613                 foundChar = true;
614                 break;
615             }
616         }
617
618         if (!foundChar)
619             return this;
620
621         var result = "";
622         for (var i = 0; i < this.length; ++i) {
623             if (chars.indexOf(this.charAt(i)) !== -1)
624                 result += "\\";
625             result += this.charAt(i);
626         }
627
628         return result;
629     }
630 });
631
632 Object.defineProperty(String.prototype, "escapeForRegExp",
633 {
634     value: function()
635     {
636         return this.escapeCharacters("^[]{}()\\.$*+?|");
637     }
638 });
639
640 Object.defineProperty(String.prototype, "capitalize",
641 {
642     value: function()
643     {
644         return this.charAt(0).toUpperCase() + this.slice(1);
645     }
646 });
647
648 Object.defineProperty(String, "tokenizeFormatString",
649 {
650     value: function(format)
651     {
652         var tokens = [];
653         var substitutionIndex = 0;
654
655         function addStringToken(str)
656         {
657             tokens.push({type: "string", value: str});
658         }
659
660         function addSpecifierToken(specifier, precision, substitutionIndex)
661         {
662             tokens.push({type: "specifier", specifier, precision, substitutionIndex});
663         }
664
665         var index = 0;
666         for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
667             addStringToken(format.substring(index, precentIndex));
668             index = precentIndex + 1;
669
670             if (format[index] === "%") {
671                 addStringToken("%");
672                 ++index;
673                 continue;
674             }
675
676             if (!isNaN(format[index])) {
677                 // The first character is a number, it might be a substitution index.
678                 var number = parseInt(format.substring(index), 10);
679                 while (!isNaN(format[index]))
680                     ++index;
681
682                 // If the number is greater than zero and ends with a "$",
683                 // then this is a substitution index.
684                 if (number > 0 && format[index] === "$") {
685                     substitutionIndex = (number - 1);
686                     ++index;
687                 }
688             }
689
690             const defaultPrecision = 6;
691
692             let precision = defaultPrecision;
693             if (format[index] === ".") {
694                 // This is a precision specifier. If no digit follows the ".",
695                 // then use the default precision of six digits (ISO C99 specification).
696                 ++index;
697
698                 precision = parseInt(format.substring(index), 10);
699                 if (isNaN(precision))
700                     precision = defaultPrecision;
701
702                 while (!isNaN(format[index]))
703                     ++index;
704             }
705
706             addSpecifierToken(format[index], precision, substitutionIndex);
707
708             ++substitutionIndex;
709             ++index;
710         }
711
712         addStringToken(format.substring(index));
713
714         return tokens;
715     }
716 });
717
718 Object.defineProperty(String.prototype, "hash",
719 {
720     get: function()
721     {
722         // Matches the wtf/Hasher.h (SuperFastHash) algorithm.
723
724         // Arbitrary start value to avoid mapping all 0's to all 0's.
725         const stringHashingStartValue = 0x9e3779b9;
726
727         var result = stringHashingStartValue;
728         var pendingCharacter = null;
729         for (var i = 0; i < this.length; ++i) {
730             var currentCharacter = this[i].charCodeAt(0);
731             if (pendingCharacter === null) {
732                 pendingCharacter = currentCharacter;
733                 continue;
734             }
735
736             result += pendingCharacter;
737             result = (result << 16) ^ ((currentCharacter << 11) ^ result);
738             result += result >> 11;
739
740             pendingCharacter = null;
741         }
742
743         // Handle the last character in odd length strings.
744         if (pendingCharacter !== null) {
745             result += pendingCharacter;
746             result ^= result << 11;
747             result += result >> 17;
748         }
749
750         // Force "avalanching" of final 31 bits.
751         result ^= result << 3;
752         result += result >> 5;
753         result ^= result << 2;
754         result += result >> 15;
755         result ^= result << 10;
756
757         // Prevent 0 and negative results.
758         return (0xffffffff + result + 1).toString(36);
759     }
760 });
761
762 Object.defineProperty(String, "standardFormatters",
763 {
764     value: {
765         d: function(substitution)
766         {
767             return parseInt(substitution);
768         },
769
770         f: function(substitution, token)
771         {
772             let value = parseFloat(substitution);
773             if (isNaN(value))
774                 return NaN;
775
776             let options = {
777                 minimumFractionDigits: token.precision,
778                 maximumFractionDigits: token.precision,
779                 useGrouping: false
780             };
781             return value.toLocaleString(undefined, options);
782         },
783
784         s: function(substitution)
785         {
786             return substitution;
787         }
788     }
789 });
790
791 Object.defineProperty(String, "format",
792 {
793     value: function(format, substitutions, formatters, initialValue, append)
794     {
795         if (!format || !substitutions || !substitutions.length)
796             return {formattedResult: append(initialValue, format), unusedSubstitutions: substitutions};
797
798         function prettyFunctionName()
799         {
800             return "String.format(\"" + format + "\", \"" + Array.from(substitutions).join("\", \"") + "\")";
801         }
802
803         function warn(msg)
804         {
805             console.warn(prettyFunctionName() + ": " + msg);
806         }
807
808         function error(msg)
809         {
810             console.error(prettyFunctionName() + ": " + msg);
811         }
812
813         var result = initialValue;
814         var tokens = String.tokenizeFormatString(format);
815         var usedSubstitutionIndexes = {};
816
817         for (var i = 0; i < tokens.length; ++i) {
818             var token = tokens[i];
819
820             if (token.type === "string") {
821                 result = append(result, token.value);
822                 continue;
823             }
824
825             if (token.type !== "specifier") {
826                 error("Unknown token type \"" + token.type + "\" found.");
827                 continue;
828             }
829
830             if (token.substitutionIndex >= substitutions.length) {
831                 // If there are not enough substitutions for the current substitutionIndex
832                 // just output the format specifier literally and move on.
833                 error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
834                 result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
835                 continue;
836             }
837
838             usedSubstitutionIndexes[token.substitutionIndex] = true;
839
840             if (!(token.specifier in formatters)) {
841                 // Encountered an unsupported format character, treat as a string.
842                 warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
843                 result = append(result, substitutions[token.substitutionIndex]);
844                 continue;
845             }
846
847             result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
848         }
849
850         var unusedSubstitutions = [];
851         for (var i = 0; i < substitutions.length; ++i) {
852             if (i in usedSubstitutionIndexes)
853                 continue;
854             unusedSubstitutions.push(substitutions[i]);
855         }
856
857         return {formattedResult: result, unusedSubstitutions};
858     }
859 });
860
861 Object.defineProperty(String.prototype, "format",
862 {
863     value: function()
864     {
865         return String.format(this, arguments, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
866     }
867 });
868
869 Object.defineProperty(String.prototype, "insertWordBreakCharacters",
870 {
871     value: function()
872     {
873         // Add zero width spaces after characters that are good to break after.
874         // Otherwise a string with no spaces will not break and overflow its container.
875         // This is mainly used on URL strings, so the characters are tailored for URLs.
876         return this.replace(/([\/;:\)\]\}&?])/g, "$1\u200b");
877     }
878 });
879
880 Object.defineProperty(String.prototype, "removeWordBreakCharacters",
881 {
882     value: function()
883     {
884         // Undoes what insertWordBreakCharacters did.
885         return this.replace(/\u200b/g, "");
886     }
887 });
888
889 Object.defineProperty(String.prototype, "getMatchingIndexes",
890 {
891     value: function(needle)
892     {
893         var indexesOfNeedle = [];
894         var index = this.indexOf(needle);
895
896         while (index >= 0) {
897             indexesOfNeedle.push(index);
898             index = this.indexOf(needle, index + 1);
899         }
900
901         return indexesOfNeedle;
902     }
903 });
904
905 Object.defineProperty(String.prototype, "levenshteinDistance",
906 {
907     value: function(s)
908     {
909         var m = this.length;
910         var n = s.length;
911         var d = new Array(m + 1);
912
913         for (var i = 0; i <= m; ++i) {
914             d[i] = new Array(n + 1);
915             d[i][0] = i;
916         }
917
918         for (var j = 0; j <= n; ++j)
919             d[0][j] = j;
920
921         for (var j = 1; j <= n; ++j) {
922             for (var i = 1; i <= m; ++i) {
923                 if (this[i - 1] === s[j - 1])
924                     d[i][j] = d[i - 1][j - 1];
925                 else {
926                     var deletion = d[i - 1][j] + 1;
927                     var insertion = d[i][j - 1] + 1;
928                     var substitution = d[i - 1][j - 1] + 1;
929                     d[i][j] = Math.min(deletion, insertion, substitution);
930                 }
931             }
932         }
933
934         return d[m][n];
935     }
936 });
937
938 Object.defineProperty(String.prototype, "toCamelCase",
939 {
940     value: function()
941     {
942         return this.toLowerCase().replace(/[^\w]+(\w)/g, (match, group) => group.toUpperCase());
943     }
944 });
945
946 Object.defineProperty(String.prototype, "hasMatchingEscapedQuotes",
947 {
948     value: function()
949     {
950         return /^\"(?:[^\"\\]|\\.)*\"$/.test(this) || /^\'(?:[^\'\\]|\\.)*\'$/.test(this);
951     }
952 });
953
954 Object.defineProperty(Math, "roundTo",
955 {
956     value: function(num, step)
957     {
958         return Math.round(num / step) * step;
959     }
960 });
961
962 Object.defineProperty(Number, "constrain",
963 {
964     value: function(num, min, max)
965     {
966         if (max < min)
967             return min;
968
969         if (num < min)
970             num = min;
971         else if (num > max)
972             num = max;
973         return num;
974     }
975 });
976
977 Object.defineProperty(Number, "percentageString",
978 {
979     value: function(fraction, precision = 1)
980     {
981         console.assert(fraction >= 0 && fraction <= 1);
982         return fraction.toLocaleString(undefined, {minimumFractionDigits: precision, style: "percent"});
983     }
984 });
985
986 Object.defineProperty(Number, "secondsToMillisecondsString",
987 {
988     value: function(seconds, higherResolution)
989     {
990         let ms = seconds * 1000;
991
992         if (higherResolution)
993             return WebInspector.UIString("%.2fms").format(ms);
994         return WebInspector.UIString("%.1fms").format(ms);
995     }
996 });
997
998 Object.defineProperty(Number, "secondsToString",
999 {
1000     value: function(seconds, higherResolution)
1001     {
1002         let ms = seconds * 1000;
1003         if (!ms)
1004             return WebInspector.UIString("%.0fms").format(0);
1005
1006         if (Math.abs(ms) < 10) {
1007             if (higherResolution)
1008                 return WebInspector.UIString("%.3fms").format(ms);
1009             return WebInspector.UIString("%.2fms").format(ms);
1010         }
1011
1012         if (Math.abs(ms) < 100) {
1013             if (higherResolution)
1014                 return WebInspector.UIString("%.2fms").format(ms);
1015             return WebInspector.UIString("%.1fms").format(ms);
1016         }
1017
1018         if (Math.abs(ms) < 1000) {
1019             if (higherResolution)
1020                 return WebInspector.UIString("%.1fms").format(ms);
1021             return WebInspector.UIString("%.0fms").format(ms);
1022         }
1023
1024         // Do not go over seconds when in high resolution mode.
1025         if (higherResolution || Math.abs(seconds) < 60)
1026             return WebInspector.UIString("%.2fs").format(seconds);
1027
1028         let minutes = seconds / 60;
1029         if (Math.abs(minutes) < 60)
1030             return WebInspector.UIString("%.1fmin").format(minutes);
1031
1032         let hours = minutes / 60;
1033         if (Math.abs(hours) < 24)
1034             return WebInspector.UIString("%.1fhrs").format(hours);
1035
1036         let days = hours / 24;
1037         return WebInspector.UIString("%.1f days").format(days);
1038     }
1039 });
1040
1041 Object.defineProperty(Number, "bytesToString",
1042 {
1043     value: function(bytes, higherResolution)
1044     {
1045         if (higherResolution === undefined)
1046             higherResolution = true;
1047
1048         if (Math.abs(bytes) < 1024)
1049             return WebInspector.UIString("%.0f B").format(bytes);
1050
1051         let kilobytes = bytes / 1024;
1052         if (Math.abs(kilobytes) < 1024) {
1053             if (higherResolution || Math.abs(kilobytes) < 10)
1054                 return WebInspector.UIString("%.2f KB").format(kilobytes);
1055             return WebInspector.UIString("%.1f KB").format(kilobytes);
1056         }
1057
1058         let megabytes = kilobytes / 1024;
1059         if (higherResolution || Math.abs(megabytes) < 10)
1060             return WebInspector.UIString("%.2f MB").format(megabytes);
1061         return WebInspector.UIString("%.1f MB").format(megabytes);
1062     }
1063 });
1064
1065 Object.defineProperty(Number, "abbreviate",
1066 {
1067     value: function(num)
1068     {
1069         if (num < 1000)
1070             return num;
1071
1072         if (num < 1000000)
1073             return WebInspector.UIString("%.1fK").format(Math.round(num / 100) / 10);
1074
1075         if (num < 1000000000)
1076             return WebInspector.UIString("%.1fM").format(Math.round(num / 100000) / 10);
1077
1078         return WebInspector.UIString("%.1fB").format(Math.round(num / 100000000) / 10);
1079     }
1080 });
1081
1082 Object.defineProperty(Number.prototype, "maxDecimals",
1083 {
1084     value(decimals)
1085     {
1086         let power = 10 ** decimals;
1087         return Math.round(this * power) / power;
1088     }
1089 });
1090
1091 Object.defineProperty(Uint32Array, "isLittleEndian",
1092 {
1093     value: function()
1094     {
1095         if ("_isLittleEndian" in this)
1096             return this._isLittleEndian;
1097
1098         var buffer = new ArrayBuffer(4);
1099         var longData = new Uint32Array(buffer);
1100         var data = new Uint8Array(buffer);
1101
1102         longData[0] = 0x0a0b0c0d;
1103
1104         this._isLittleEndian = data[0] === 0x0d && data[1] === 0x0c && data[2] === 0x0b && data[3] === 0x0a;
1105
1106         return this._isLittleEndian;
1107     }
1108 });
1109
1110 function isEmptyObject(object)
1111 {
1112     for (var property in object)
1113         return false;
1114     return true;
1115 }
1116
1117 function isEnterKey(event)
1118 {
1119     // Check if this is an IME event.
1120     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
1121 }
1122
1123 function resolveDotsInPath(path)
1124 {
1125     if (!path)
1126         return path;
1127
1128     if (path.indexOf("./") === -1)
1129         return path;
1130
1131     console.assert(path.charAt(0) === "/");
1132
1133     var result = [];
1134
1135     var components = path.split("/");
1136     for (var i = 0; i < components.length; ++i) {
1137         var component = components[i];
1138
1139         // Skip over "./".
1140         if (component === ".")
1141             continue;
1142
1143         // Rewind one component for "../".
1144         if (component === "..") {
1145             if (result.length === 1)
1146                 continue;
1147             result.pop();
1148             continue;
1149         }
1150
1151         result.push(component);
1152     }
1153
1154     return result.join("/");
1155 }
1156
1157 function parseMIMEType(fullMimeType)
1158 {
1159     if (!fullMimeType)
1160         return {type: fullMimeType, boundary: null, encoding: null};
1161
1162     var typeParts = fullMimeType.split(/\s*;\s*/);
1163     console.assert(typeParts.length >= 1);
1164
1165     var type = typeParts[0];
1166     var boundary = null;
1167     var encoding = null;
1168
1169     for (var i = 1; i < typeParts.length; ++i) {
1170         var subparts = typeParts[i].split(/\s*=\s*/);
1171         if (subparts.length !== 2)
1172             continue;
1173
1174         if (subparts[0].toLowerCase() === "boundary")
1175             boundary = subparts[1];
1176         else if (subparts[0].toLowerCase() === "charset")
1177             encoding = subparts[1].replace("^\"|\"$", ""); // Trim quotes.
1178     }
1179
1180     return {type, boundary: boundary || null, encoding: encoding || null};
1181 }
1182
1183 function simpleGlobStringToRegExp(globString, regExpFlags)
1184 {
1185     // Only supports "*" globs.
1186
1187     if (!globString)
1188         return null;
1189
1190     // Escape everything from String.prototype.escapeForRegExp except "*".
1191     var regexString = globString.escapeCharacters("^[]{}()\\.$+?|");
1192
1193     // Unescape all doubly escaped backslashes in front of escaped asterisks.
1194     // So "\\*" will become "\*" again, undoing escapeCharacters escaping of "\".
1195     // This makes "\*" match a literal "*" instead of using the "*" for globbing.
1196     regexString = regexString.replace(/\\\\\*/g, "\\*");
1197
1198     // The following regex doesn't match an asterisk that has a backslash in front.
1199     // It also catches consecutive asterisks so they collapse down when replaced.
1200     var unescapedAsteriskRegex = /(^|[^\\])\*+/g;
1201     if (unescapedAsteriskRegex.test(globString)) {
1202         // Replace all unescaped asterisks with ".*".
1203         regexString = regexString.replace(unescapedAsteriskRegex, "$1.*");
1204
1205         // Match edge boundaries when there is an asterisk to better meet the expectations
1206         // of the user. When someone types "*.js" they don't expect "foo.json" to match. They
1207         // would only expect that if they type "*.js*". We use \b (instead of ^ and $) to allow
1208         // matches inside paths or URLs, so "ba*.js" will match "foo/bar.js" but not "boo/bbar.js".
1209         // When there isn't an asterisk the regexString is just a substring search.
1210         regexString = "\\b" + regexString + "\\b";
1211     }
1212
1213     return new RegExp(regexString, regExpFlags);
1214 }
1215
1216 Object.defineProperty(Array.prototype, "lowerBound",
1217 {
1218     // Return index of the leftmost element that is equal or greater
1219     // than the specimen object. If there's no such element (i.e. all
1220     // elements are smaller than the specimen) returns array.length.
1221     // The function works for sorted array.
1222     value: function(object, comparator)
1223     {
1224         function defaultComparator(a, b)
1225         {
1226             return a - b;
1227         }
1228         comparator = comparator || defaultComparator;
1229         var l = 0;
1230         var r = this.length;
1231         while (l < r) {
1232             var m = (l + r) >> 1;
1233             if (comparator(object, this[m]) > 0)
1234                 l = m + 1;
1235             else
1236                 r = m;
1237         }
1238         return r;
1239     }
1240 });
1241
1242 Object.defineProperty(Array.prototype, "upperBound",
1243 {
1244     // Return index of the leftmost element that is greater
1245     // than the specimen object. If there's no such element (i.e. all
1246     // elements are smaller than the specimen) returns array.length.
1247     // The function works for sorted array.
1248     value: function(object, comparator)
1249     {
1250         function defaultComparator(a, b)
1251         {
1252             return a - b;
1253         }
1254         comparator = comparator || defaultComparator;
1255         var l = 0;
1256         var r = this.length;
1257         while (l < r) {
1258             var m = (l + r) >> 1;
1259             if (comparator(object, this[m]) >= 0)
1260                 l = m + 1;
1261             else
1262                 r = m;
1263         }
1264         return r;
1265     }
1266 });
1267
1268 Object.defineProperty(Array.prototype, "binaryIndexOf",
1269 {
1270     value: function(value, comparator)
1271     {
1272         var index = this.lowerBound(value, comparator);
1273         return index < this.length && comparator(value, this[index]) === 0 ? index : -1;
1274     }
1275 });
1276
1277 (function() {
1278     // The `debounce` function lets you call any function on an object with a delay
1279     // and if the function keeps getting called, the delay gets reset. Since `debounce`
1280     // returns a Proxy, you can cache it and call multiple functions with the same delay.
1281
1282     // Use: object.debounce(200).foo("Argument 1", "Argument 2")
1283     // Note: The last call's arguments get used for the delayed call.
1284
1285     const debounceTimeoutSymbol = Symbol("debounce-timeout");
1286     const debounceSoonProxySymbol = Symbol("debounce-soon-proxy");
1287
1288     Object.defineProperty(Object.prototype, "soon",
1289     {
1290         get: function()
1291         {
1292             if (!this[debounceSoonProxySymbol])
1293                 this[debounceSoonProxySymbol] = this.debounce(0);
1294             return this[debounceSoonProxySymbol];
1295         }
1296     });
1297
1298     Object.defineProperty(Object.prototype, "debounce",
1299     {
1300         value: function(delay)
1301         {
1302             console.assert(delay >= 0);
1303
1304             return new Proxy(this, {
1305                 get(target, property, receiver) {
1306                     return (...args) => {
1307                         let original = target[property];
1308                         console.assert(typeof original === "function");
1309
1310                         if (original[debounceTimeoutSymbol])
1311                             clearTimeout(original[debounceTimeoutSymbol]);
1312
1313                         let performWork = () => {
1314                             original[debounceTimeoutSymbol] = undefined;
1315                             original.apply(target, args);
1316                         };
1317
1318                         original[debounceTimeoutSymbol] = setTimeout(performWork, delay);
1319                     };
1320                 }
1321             });
1322         }
1323     });
1324
1325     Object.defineProperty(Function.prototype, "cancelDebounce",
1326     {
1327         value: function()
1328         {
1329             if (!this[debounceTimeoutSymbol])
1330                 return;
1331
1332             clearTimeout(this[debounceTimeoutSymbol]);
1333             this[debounceTimeoutSymbol] = undefined;
1334         }
1335     });
1336
1337     const requestAnimationFrameSymbol = Symbol("peform-on-animation-frame");
1338     const requestAnimationFrameProxySymbol = Symbol("perform-on-animation-frame-proxy");
1339
1340     Object.defineProperty(Object.prototype, "onNextFrame",
1341     {
1342         get: function()
1343         {
1344             if (!this[requestAnimationFrameProxySymbol]) {
1345                 this[requestAnimationFrameProxySymbol] = new Proxy(this, {
1346                     get(target, property, receiver) {
1347                         return (...args) => {
1348                             let original = target[property];
1349                             console.assert(typeof original === "function");
1350
1351                             if (original[requestAnimationFrameSymbol])
1352                                 return;
1353
1354                             let performWork = () => {
1355                                 original[requestAnimationFrameSymbol] = undefined;
1356                                 original.apply(target, args);
1357                             };
1358
1359                             original[requestAnimationFrameSymbol] = requestAnimationFrame(performWork);
1360                         };
1361                     }
1362                 });
1363             }
1364
1365             return this[requestAnimationFrameProxySymbol];
1366         }
1367     });
1368 })();
1369
1370 function appendWebInspectorSourceURL(string)
1371 {
1372     if (string.includes("//# sourceURL"))
1373         return string;
1374     return "\n//# sourceURL=__WebInspectorInternal__\n" + string;
1375 }
1376
1377 function appendWebInspectorConsoleEvaluationSourceURL(string)
1378 {
1379     if (string.includes("//# sourceURL"))
1380         return string;
1381     return "\n//# sourceURL=__WebInspectorConsoleEvaluation__\n" + string;
1382 }
1383
1384 function isWebInspectorInternalScript(url)
1385 {
1386     return url === "__WebInspectorInternal__";
1387 }
1388
1389 function isWebInspectorConsoleEvaluationScript(url)
1390 {
1391     return url === "__WebInspectorConsoleEvaluation__";
1392 }
1393
1394 function isWebKitInjectedScript(url)
1395 {
1396     return url && url.startsWith("__InjectedScript_") && url.endsWith(".js");
1397 }
1398
1399 function isWebKitInternalScript(url)
1400 {
1401     if (isWebInspectorConsoleEvaluationScript(url))
1402         return false;
1403
1404     if (isWebKitInjectedScript(url))
1405         return true;
1406
1407     return url && url.startsWith("__Web") && url.endsWith("__");
1408 }
1409
1410 function isFunctionStringNativeCode(str)
1411 {
1412     return str.endsWith("{\n    [native code]\n}");
1413 }
1414
1415 function isTextLikelyMinified(content)
1416 {
1417     const autoFormatMaxCharactersToCheck = 5000;
1418     const autoFormatWhitespaceRatio = 0.2;
1419
1420     let whitespaceScore = 0;
1421     let size = Math.min(autoFormatMaxCharactersToCheck, content.length);
1422
1423     for (let i = 0; i < size; i++) {
1424         let char = content[i];
1425
1426         if (char === " ")
1427             whitespaceScore++;
1428         else if (char === "\t")
1429             whitespaceScore += 4;
1430         else if (char === "\n")
1431             whitespaceScore += 8;
1432     }
1433
1434     let ratio = whitespaceScore / size;
1435     return ratio < autoFormatWhitespaceRatio;
1436 }
1437
1438 function doubleQuotedString(str)
1439 {
1440     return "\"" + str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
1441 }
1442
1443 function insertionIndexForObjectInListSortedByFunction(object, list, comparator, insertionIndexAfter)
1444 {
1445     if (insertionIndexAfter) {
1446         return list.upperBound(object, comparator);
1447     } else {
1448         return list.lowerBound(object, comparator);
1449     }
1450 }
1451
1452 function insertObjectIntoSortedArray(object, array, comparator)
1453 {
1454     array.splice(insertionIndexForObjectInListSortedByFunction(object, array, comparator), 0, object);
1455 }
1456
1457 function decodeBase64ToBlob(base64Data, mimeType)
1458 {
1459     mimeType = mimeType || '';
1460
1461     const sliceSize = 1024;
1462     var byteCharacters = atob(base64Data);
1463     var bytesLength = byteCharacters.length;
1464     var slicesCount = Math.ceil(bytesLength / sliceSize);
1465     var byteArrays = new Array(slicesCount);
1466
1467     for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
1468         var begin = sliceIndex * sliceSize;
1469         var end = Math.min(begin + sliceSize, bytesLength);
1470
1471         var bytes = new Array(end - begin);
1472         for (var offset = begin, i = 0 ; offset < end; ++i, ++offset)
1473             bytes[i] = byteCharacters[offset].charCodeAt(0);
1474
1475         byteArrays[sliceIndex] = new Uint8Array(bytes);
1476     }
1477
1478     return new Blob(byteArrays, {type: mimeType});
1479 }
1480
1481 // FIXME: This can be removed when WEB_TIMING is enabled for all platforms.
1482 function timestamp()
1483 {
1484     return window.performance ? performance.now() : Date.now();
1485 }
1486
1487 if (!window.handlePromiseException) {
1488     window.handlePromiseException = function handlePromiseException(error)
1489     {
1490         console.error("Uncaught exception in Promise", error);
1491     };
1492 }