Web Inspector: Remove unnecessary promise rejection handlers now that we use the...
[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 var zeroWidthSpace = "\u200b";
31 var multiplicationSign = "\u00d7";
32
33 Object.defineProperty(Object, "shallowCopy",
34 {
35     value(object)
36     {
37         // Make a new object and copy all the key/values. The values are not copied.
38         var copy = {};
39         var keys = Object.keys(object);
40         for (var i = 0; i < keys.length; ++i)
41             copy[keys[i]] = object[keys[i]];
42         return copy;
43     }
44 });
45
46 Object.defineProperty(Object, "shallowEqual",
47 {
48     value(a, b)
49     {
50         // Checks if two objects have the same top-level properties.
51
52         if (!(a instanceof Object) || !(b instanceof Object))
53             return false;
54
55         if (a === b)
56             return true;
57
58         if (Array.shallowEqual(a, b))
59             return true;
60
61         if (a.constructor !== b.constructor)
62             return false;
63
64         let aKeys = Object.keys(a);
65         let bKeys = Object.keys(b);
66         if (aKeys.length !== bKeys.length)
67             return false;
68
69         for (let aKey of aKeys) {
70             if (!(aKey in b))
71                 return false;
72
73             let aValue = a[aKey];
74             let bValue = b[aKey];
75             if (aValue !== bValue && !Array.shallowEqual(aValue, bValue))
76                 return false;
77         }
78
79         return true;
80     }
81 });
82
83 Object.defineProperty(Object.prototype, "valueForCaseInsensitiveKey",
84 {
85     value(key)
86     {
87         if (this.hasOwnProperty(key))
88             return this[key];
89
90         var lowerCaseKey = key.toLowerCase();
91         for (var currentKey in this) {
92             if (currentKey.toLowerCase() === lowerCaseKey)
93                 return this[currentKey];
94         }
95
96         return undefined;
97     }
98 });
99
100 Object.defineProperty(Map, "fromObject",
101 {
102     value(object)
103     {
104         let map = new Map;
105         for (let key in object)
106             map.set(key, object[key]);
107         return map;
108     }
109 });
110
111 Object.defineProperty(Map.prototype, "take",
112 {
113     value(key)
114     {
115         var deletedValue = this.get(key);
116         this.delete(key);
117         return deletedValue;
118     }
119 });
120
121 Object.defineProperty(Set.prototype, "intersects",
122 {
123     value(other)
124     {
125         if (!this.size || !other.size)
126             return false;
127
128         for (let item of this) {
129             if (other.has(item))
130                 return true;
131         }
132
133         return false;
134     }
135 });
136
137 Object.defineProperty(Set.prototype, "isSubsetOf",
138 {
139     value(other)
140     {
141         for (let item of this) {
142             if (!other.has(item))
143                 return false;
144         }
145
146         return true;
147     }
148 });
149
150 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithClass",
151 {
152     value(className)
153     {
154         for (let node = this; node; node = node.parentElement) {
155             if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(className))
156                 return node;
157         }
158
159         return null;
160     }
161 });
162
163 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithNodeNameInArray",
164 {
165     value(nodeNames)
166     {
167         let upperCaseNodeNames = nodeNames.map((name) => name.toUpperCase());
168
169         for (let node = this; node; node = node.parentElement) {
170             for (let nodeName of upperCaseNodeNames) {
171                 if (node.nodeName === nodeName)
172                     return node;
173             }
174         }
175
176         return null;
177     }
178 });
179
180 Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithNodeName",
181 {
182     value(nodeName)
183     {
184         return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
185     }
186 });
187
188 Object.defineProperty(Node.prototype, "traverseNextNode",
189 {
190     value(stayWithin)
191     {
192         var node = this.firstChild;
193         if (node)
194             return node;
195
196         if (stayWithin && this === stayWithin)
197             return null;
198
199         node = this.nextSibling;
200         if (node)
201             return node;
202
203         node = this;
204         while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
205             node = node.parentNode;
206         if (!node)
207             return null;
208
209         return node.nextSibling;
210     }
211 });
212
213 Object.defineProperty(Node.prototype, "traversePreviousNode",
214 {
215     value(stayWithin)
216     {
217        if (stayWithin && this === stayWithin)
218             return null;
219         var node = this.previousSibling;
220         while (node && node.lastChild)
221             node = node.lastChild;
222         if (node)
223             return node;
224         return this.parentNode;
225     }
226 });
227
228
229 Object.defineProperty(Node.prototype, "rangeOfWord",
230 {
231     value(offset, stopCharacters, stayWithinNode, direction)
232     {
233         var startNode;
234         var startOffset = 0;
235         var endNode;
236         var endOffset = 0;
237
238         if (!stayWithinNode)
239             stayWithinNode = this;
240
241         if (!direction || direction === "backward" || direction === "both") {
242             var node = this;
243             while (node) {
244                 if (node === stayWithinNode) {
245                     if (!startNode)
246                         startNode = stayWithinNode;
247                     break;
248                 }
249
250                 if (node.nodeType === Node.TEXT_NODE) {
251                     let start = node === this ? (offset - 1) : (node.nodeValue.length - 1);
252                     for (var i = start; i >= 0; --i) {
253                         if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
254                             startNode = node;
255                             startOffset = i + 1;
256                             break;
257                         }
258                     }
259                 }
260
261                 if (startNode)
262                     break;
263
264                 node = node.traversePreviousNode(stayWithinNode);
265             }
266
267             if (!startNode) {
268                 startNode = stayWithinNode;
269                 startOffset = 0;
270             }
271         } else {
272             startNode = this;
273             startOffset = offset;
274         }
275
276         if (!direction || direction === "forward" || direction === "both") {
277             node = this;
278             while (node) {
279                 if (node === stayWithinNode) {
280                     if (!endNode)
281                         endNode = stayWithinNode;
282                     break;
283                 }
284
285                 if (node.nodeType === Node.TEXT_NODE) {
286                     let start = node === this ? offset : 0;
287                     for (var i = start; i < node.nodeValue.length; ++i) {
288                         if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
289                             endNode = node;
290                             endOffset = i;
291                             break;
292                         }
293                     }
294                 }
295
296                 if (endNode)
297                     break;
298
299                 node = node.traverseNextNode(stayWithinNode);
300             }
301
302             if (!endNode) {
303                 endNode = stayWithinNode;
304                 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
305             }
306         } else {
307             endNode = this;
308             endOffset = offset;
309         }
310
311         var result = this.ownerDocument.createRange();
312         result.setStart(startNode, startOffset);
313         result.setEnd(endNode, endOffset);
314
315         return result;
316
317     }
318 });
319
320 Object.defineProperty(Element.prototype, "realOffsetWidth",
321 {
322     get()
323     {
324         return this.getBoundingClientRect().width;
325     }
326 });
327
328 Object.defineProperty(Element.prototype, "realOffsetHeight",
329 {
330     get()
331     {
332         return this.getBoundingClientRect().height;
333     }
334 });
335
336 Object.defineProperty(Element.prototype, "totalOffsetLeft",
337 {
338     get()
339     {
340         return this.getBoundingClientRect().left;
341     }
342 });
343
344 Object.defineProperty(Element.prototype, "totalOffsetRight",
345 {
346     get()
347     {
348         return this.getBoundingClientRect().right;
349     }
350 });
351
352 Object.defineProperty(Element.prototype, "totalOffsetTop",
353 {
354     get()
355     {
356         return this.getBoundingClientRect().top;
357     }
358 });
359
360 Object.defineProperty(Element.prototype, "removeChildren",
361 {
362     value()
363     {
364         // This has been tested to be the fastest removal method.
365         if (this.firstChild)
366             this.textContent = "";
367     }
368 });
369
370 Object.defineProperty(Element.prototype, "isInsertionCaretInside",
371 {
372     value()
373     {
374         var selection = window.getSelection();
375         if (!selection.rangeCount || !selection.isCollapsed)
376             return false;
377         var selectionRange = selection.getRangeAt(0);
378         return selectionRange.startContainer === this || this.contains(selectionRange.startContainer);
379     }
380 });
381
382 Object.defineProperty(Element.prototype, "createChild",
383 {
384     value(elementName, className)
385     {
386         var element = this.ownerDocument.createElement(elementName);
387         if (className)
388             element.className = className;
389         this.appendChild(element);
390         return element;
391     }
392 });
393
394 Object.defineProperty(Element.prototype, "isScrolledToBottom",
395 {
396     value()
397     {
398         // This code works only for 0-width border
399         return this.scrollTop + this.clientHeight === this.scrollHeight;
400     }
401 });
402
403 Object.defineProperty(Element.prototype, "recalculateStyles",
404 {
405     value()
406     {
407         this.ownerDocument.defaultView.getComputedStyle(this);
408     }
409 });
410
411 Object.defineProperty(DocumentFragment.prototype, "createChild",
412 {
413     value: Element.prototype.createChild
414 });
415
416 Object.defineProperty(Event.prototype, "stop",
417 {
418     value()
419     {
420         this.stopImmediatePropagation();
421         this.preventDefault();
422     }
423 });
424
425 Object.defineProperty(KeyboardEvent.prototype, "commandOrControlKey",
426 {
427     get()
428     {
429         return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey;
430     }
431 });
432
433 Object.defineProperty(MouseEvent.prototype, "commandOrControlKey",
434 {
435     get()
436     {
437         return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey;
438     }
439 });
440
441 Object.defineProperty(Array, "isTypedArray",
442 {
443     value(array)
444     {
445         if (!array)
446             return false;
447
448         let constructor = array.constructor;
449         return constructor === Int8Array
450             || constructor === Int16Array
451             || constructor === Int32Array
452             || constructor === Uint8Array
453             || constructor === Uint8ClampedArray
454             || constructor === Uint16Array
455             || constructor === Uint32Array
456             || constructor === Float32Array
457             || constructor === Float64Array;
458     }
459 });
460
461 Object.defineProperty(Array, "shallowEqual",
462 {
463     value(a, b)
464     {
465         function isArrayLike(x) {
466             return Array.isArray(x) || Array.isTypedArray(x);
467         }
468
469         if (!isArrayLike(a) || !isArrayLike(b))
470             return false;
471
472         if (a === b)
473             return true;
474
475         let length = a.length;
476
477         if (length !== b.length)
478             return false;
479
480         for (let i = 0; i < length; ++i) {
481             if (a[i] === b[i])
482                 continue;
483
484             if (!Object.shallowEqual(a[i], b[i]))
485                 return false;
486         }
487
488         return true;
489     }
490 });
491
492 Object.defineProperty(Array, "diffArrays",
493 {
494     value(initialArray, currentArray, onEach)
495     {
496         let initialSet = new Set(initialArray);
497         let currentSet = new Set(currentArray);
498         let indexInitial = 0;
499         let indexCurrent = 0;
500         let deltaInitial = 0;
501         let deltaCurrent = 0;
502
503         let i = 0;
504         while (true) {
505             if (indexInitial >= initialArray.length || indexCurrent >= currentArray.length)
506                 break;
507
508             let initial = initialArray[indexInitial];
509             let current = currentArray[indexCurrent];
510
511             if (initial === current)
512                 onEach(current, 0);
513             else if (currentSet.has(initial)) {
514                 if (initialSet.has(current)) {
515                     // Moved.
516                     onEach(current, 0);
517                 } else {
518                     // Added.
519                     onEach(current, 1);
520                     --i;
521                     ++deltaCurrent;
522                 }
523             } else {
524                 // Removed.
525                 onEach(initial, -1);
526                 if (!initialSet.has(current)) {
527                     // Added.
528                     onEach(current, 1);
529                 } else {
530                     --i;
531                     ++deltaInitial;
532                 }
533             }
534
535             ++i;
536             indexInitial = i + deltaInitial;
537             indexCurrent = i + deltaCurrent;
538         }
539
540         for (let i = indexInitial; i < initialArray.length; ++i) {
541             // Removed.
542             onEach(initialArray[i], -1);
543         }
544
545         for (let i = indexCurrent; i < currentArray.length; ++i) {
546             // Added.
547             onEach(currentArray[i], 1);
548         }
549
550     }
551 });
552
553 Object.defineProperty(Array.prototype, "lastValue",
554 {
555     get()
556     {
557         if (!this.length)
558             return undefined;
559         return this[this.length - 1];
560     }
561 });
562
563 Object.defineProperty(Array.prototype, "adjacencies",
564 {
565     value: function*() {
566         for (let i = 1; i < this.length; ++i)
567             yield [this[i - 1], this[i]];
568     }
569 });
570
571 Object.defineProperty(Array.prototype, "remove",
572 {
573     value(value)
574     {
575         for (let i = 0; i < this.length; ++i) {
576             if (this[i] === value) {
577                 this.splice(i, 1);
578                 return;
579             }
580         }
581     }
582 });
583
584 Object.defineProperty(Array.prototype, "removeAll",
585 {
586     value(value)
587     {
588         for (let i = this.length - 1; i >= 0; --i) {
589             if (this[i] === value)
590                 this.splice(i, 1);
591         }
592     }
593 });
594
595 Object.defineProperty(Array.prototype, "toggleIncludes",
596 {
597     value(value, force)
598     {
599         let exists = this.includes(value);
600         if (exists === !!force)
601             return;
602
603         if (exists)
604             this.remove(value);
605         else
606             this.push(value);
607     }
608 });
609
610 Object.defineProperty(Array.prototype, "insertAtIndex",
611 {
612     value(value, index)
613     {
614         this.splice(index, 0, value);
615     }
616 });
617
618 Object.defineProperty(Array.prototype, "keySet",
619 {
620     value()
621     {
622         let keys = Object.create(null);
623         for (var i = 0; i < this.length; ++i)
624             keys[this[i]] = true;
625         return keys;
626     }
627 });
628
629 Object.defineProperty(Array.prototype, "partition",
630 {
631     value(callback)
632     {
633         let positive = [];
634         let negative = [];
635         for (let i = 0; i < this.length; ++i) {
636             let value = this[i];
637             if (callback(value))
638                 positive.push(value);
639             else
640                 negative.push(value);
641         }
642         return [positive, negative];
643     }
644 });
645
646 Object.defineProperty(String.prototype, "isLowerCase",
647 {
648     value()
649     {
650         return String(this) === this.toLowerCase();
651     }
652 });
653
654 Object.defineProperty(String.prototype, "isUpperCase",
655 {
656     value()
657     {
658         return String(this) === this.toUpperCase();
659     }
660 });
661
662 Object.defineProperty(String.prototype, "truncateStart",
663 {
664     value(maxLength)
665     {
666         "use strict";
667
668         if (this.length <= maxLength)
669             return this;
670         return ellipsis + this.substr(this.length - maxLength + 1);
671     }
672 });
673
674 Object.defineProperty(String.prototype, "truncateMiddle",
675 {
676     value(maxLength)
677     {
678         "use strict";
679
680         if (this.length <= maxLength)
681             return this;
682         var leftHalf = maxLength >> 1;
683         var rightHalf = maxLength - leftHalf - 1;
684         return this.substr(0, leftHalf) + ellipsis + this.substr(this.length - rightHalf, rightHalf);
685     }
686 });
687
688 Object.defineProperty(String.prototype, "truncateEnd",
689 {
690     value(maxLength)
691     {
692         "use strict";
693
694         if (this.length <= maxLength)
695             return this;
696         return this.substr(0, maxLength - 1) + ellipsis;
697     }
698 });
699
700 Object.defineProperty(String.prototype, "truncate",
701 {
702     value(maxLength)
703     {
704         "use strict";
705
706         if (this.length <= maxLength)
707             return this;
708
709         let clipped = this.slice(0, maxLength);
710         let indexOfLastWhitespace = clipped.search(/\s\S*$/);
711         if (indexOfLastWhitespace > Math.floor(maxLength / 2))
712             clipped = clipped.slice(0, indexOfLastWhitespace - 1);
713
714         return clipped + ellipsis;
715     }
716 });
717
718 Object.defineProperty(String.prototype, "collapseWhitespace",
719 {
720     value()
721     {
722         return this.replace(/[\s\xA0]+/g, " ");
723     }
724 });
725
726 Object.defineProperty(String.prototype, "removeWhitespace",
727 {
728     value()
729     {
730         return this.replace(/[\s\xA0]+/g, "");
731     }
732 });
733
734 Object.defineProperty(String.prototype, "escapeCharacters",
735 {
736     value(charactersToEscape)
737     {
738         if (!charactersToEscape)
739             return this.valueOf();
740
741         let charactersToEscapeSet = new Set(charactersToEscape);
742
743         let foundCharacter = false;
744         for (let c of this) {
745             if (!charactersToEscapeSet.has(c))
746                 continue;
747             foundCharacter = true;
748             break;
749         }
750
751         if (!foundCharacter)
752             return this.valueOf();
753
754         let result = "";
755         for (let c of this) {
756             if (charactersToEscapeSet.has(c))
757                 result += "\\";
758             result += c;
759         }
760
761         return result.valueOf();
762     }
763 });
764
765 Object.defineProperty(String.prototype, "escapeForRegExp",
766 {
767     value()
768     {
769         return this.escapeCharacters("^[]{}()\\.$*+?|");
770     }
771 });
772
773 Object.defineProperty(String.prototype, "capitalize",
774 {
775     value()
776     {
777         return this.charAt(0).toUpperCase() + this.slice(1);
778     }
779 });
780
781 Object.defineProperty(String.prototype, "extendedLocaleCompare",
782 {
783     value(other)
784     {
785         return this.localeCompare(other, undefined, {numeric: true});
786     }
787 });
788
789 Object.defineProperty(String, "tokenizeFormatString",
790 {
791     value(format)
792     {
793         var tokens = [];
794         var substitutionIndex = 0;
795
796         function addStringToken(str)
797         {
798             tokens.push({type: "string", value: str});
799         }
800
801         function addSpecifierToken(specifier, precision, substitutionIndex)
802         {
803             tokens.push({type: "specifier", specifier, precision, substitutionIndex});
804         }
805
806         var index = 0;
807         for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
808             addStringToken(format.substring(index, precentIndex));
809             index = precentIndex + 1;
810
811             if (format[index] === "%") {
812                 addStringToken("%");
813                 ++index;
814                 continue;
815             }
816
817             if (!isNaN(format[index])) {
818                 // The first character is a number, it might be a substitution index.
819                 var number = parseInt(format.substring(index), 10);
820                 while (!isNaN(format[index]))
821                     ++index;
822
823                 // If the number is greater than zero and ends with a "$",
824                 // then this is a substitution index.
825                 if (number > 0 && format[index] === "$") {
826                     substitutionIndex = (number - 1);
827                     ++index;
828                 }
829             }
830
831             const defaultPrecision = 6;
832
833             let precision = defaultPrecision;
834             if (format[index] === ".") {
835                 // This is a precision specifier. If no digit follows the ".",
836                 // then use the default precision of six digits (ISO C99 specification).
837                 ++index;
838
839                 precision = parseInt(format.substring(index), 10);
840                 if (isNaN(precision))
841                     precision = defaultPrecision;
842
843                 while (!isNaN(format[index]))
844                     ++index;
845             }
846
847             addSpecifierToken(format[index], precision, substitutionIndex);
848
849             ++substitutionIndex;
850             ++index;
851         }
852
853         addStringToken(format.substring(index));
854
855         return tokens;
856     }
857 });
858
859 Object.defineProperty(String.prototype, "lineCount",
860 {
861     get()
862     {
863         "use strict";
864
865         let lineCount = 1;
866         let index = 0;
867         while (true) {
868             index = this.indexOf("\n", index);
869             if (index === -1)
870                 return lineCount;
871
872             index += "\n".length;
873             lineCount++;
874         }
875     }
876 });
877
878 Object.defineProperty(String.prototype, "lastLine",
879 {
880     get()
881     {
882         "use strict";
883
884         let index = this.lastIndexOf("\n");
885         if (index === -1)
886             return this;
887
888         return this.slice(index + "\n".length);
889     }
890 });
891
892 Object.defineProperty(String.prototype, "hash",
893 {
894     get()
895     {
896         // Matches the wtf/Hasher.h (SuperFastHash) algorithm.
897
898         // Arbitrary start value to avoid mapping all 0's to all 0's.
899         const stringHashingStartValue = 0x9e3779b9;
900
901         var result = stringHashingStartValue;
902         var pendingCharacter = null;
903         for (var i = 0; i < this.length; ++i) {
904             var currentCharacter = this[i].charCodeAt(0);
905             if (pendingCharacter === null) {
906                 pendingCharacter = currentCharacter;
907                 continue;
908             }
909
910             result += pendingCharacter;
911             result = (result << 16) ^ ((currentCharacter << 11) ^ result);
912             result += result >> 11;
913
914             pendingCharacter = null;
915         }
916
917         // Handle the last character in odd length strings.
918         if (pendingCharacter !== null) {
919             result += pendingCharacter;
920             result ^= result << 11;
921             result += result >> 17;
922         }
923
924         // Force "avalanching" of final 31 bits.
925         result ^= result << 3;
926         result += result >> 5;
927         result ^= result << 2;
928         result += result >> 15;
929         result ^= result << 10;
930
931         // Prevent 0 and negative results.
932         return (0xffffffff + result + 1).toString(36);
933     }
934 });
935
936 Object.defineProperty(String, "standardFormatters",
937 {
938     value: {
939         d: function(substitution)
940         {
941             return parseInt(substitution).toLocaleString();
942         },
943
944         f: function(substitution, token)
945         {
946             let value = parseFloat(substitution);
947             if (isNaN(value))
948                 return NaN;
949
950             let options = {
951                 minimumFractionDigits: token.precision,
952                 maximumFractionDigits: token.precision,
953                 useGrouping: false
954             };
955             return value.toLocaleString(undefined, options);
956         },
957
958         s: function(substitution)
959         {
960             return substitution;
961         }
962     }
963 });
964
965 Object.defineProperty(String, "format",
966 {
967     value(format, substitutions, formatters, initialValue, append)
968     {
969         if (!format || !substitutions || !substitutions.length)
970             return {formattedResult: append(initialValue, format), unusedSubstitutions: substitutions};
971
972         function prettyFunctionName()
973         {
974             return "String.format(\"" + format + "\", \"" + Array.from(substitutions).join("\", \"") + "\")";
975         }
976
977         function warn(msg)
978         {
979             console.warn(prettyFunctionName() + ": " + msg);
980         }
981
982         function error(msg)
983         {
984             console.error(prettyFunctionName() + ": " + msg);
985         }
986
987         var result = initialValue;
988         var tokens = String.tokenizeFormatString(format);
989         var usedSubstitutionIndexes = {};
990
991         for (var i = 0; i < tokens.length; ++i) {
992             var token = tokens[i];
993
994             if (token.type === "string") {
995                 result = append(result, token.value);
996                 continue;
997             }
998
999             if (token.type !== "specifier") {
1000                 error("Unknown token type \"" + token.type + "\" found.");
1001                 continue;
1002             }
1003
1004             if (token.substitutionIndex >= substitutions.length) {
1005                 // If there are not enough substitutions for the current substitutionIndex
1006                 // just output the format specifier literally and move on.
1007                 error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
1008                 result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
1009                 continue;
1010             }
1011
1012             usedSubstitutionIndexes[token.substitutionIndex] = true;
1013
1014             if (!(token.specifier in formatters)) {
1015                 // Encountered an unsupported format character, treat as a string.
1016                 warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
1017                 result = append(result, substitutions[token.substitutionIndex]);
1018                 continue;
1019             }
1020
1021             result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
1022         }
1023
1024         var unusedSubstitutions = [];
1025         for (var i = 0; i < substitutions.length; ++i) {
1026             if (i in usedSubstitutionIndexes)
1027                 continue;
1028             unusedSubstitutions.push(substitutions[i]);
1029         }
1030
1031         return {formattedResult: result, unusedSubstitutions};
1032     }
1033 });
1034
1035 Object.defineProperty(String.prototype, "format",
1036 {
1037     value()
1038     {
1039         return String.format(this, arguments, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
1040     }
1041 });
1042
1043 Object.defineProperty(String.prototype, "insertWordBreakCharacters",
1044 {
1045     value()
1046     {
1047         // Add zero width spaces after characters that are good to break after.
1048         // Otherwise a string with no spaces will not break and overflow its container.
1049         // This is mainly used on URL strings, so the characters are tailored for URLs.
1050         return this.replace(/([\/;:\)\]\}&?])/g, "$1\u200b");
1051     }
1052 });
1053
1054 Object.defineProperty(String.prototype, "removeWordBreakCharacters",
1055 {
1056     value()
1057     {
1058         // Undoes what insertWordBreakCharacters did.
1059         return this.replace(/\u200b/g, "");
1060     }
1061 });
1062
1063 Object.defineProperty(String.prototype, "levenshteinDistance",
1064 {
1065     value(s)
1066     {
1067         var m = this.length;
1068         var n = s.length;
1069         var d = new Array(m + 1);
1070
1071         for (var i = 0; i <= m; ++i) {
1072             d[i] = new Array(n + 1);
1073             d[i][0] = i;
1074         }
1075
1076         for (var j = 0; j <= n; ++j)
1077             d[0][j] = j;
1078
1079         for (var j = 1; j <= n; ++j) {
1080             for (var i = 1; i <= m; ++i) {
1081                 if (this[i - 1] === s[j - 1])
1082                     d[i][j] = d[i - 1][j - 1];
1083                 else {
1084                     var deletion = d[i - 1][j] + 1;
1085                     var insertion = d[i][j - 1] + 1;
1086                     var substitution = d[i - 1][j - 1] + 1;
1087                     d[i][j] = Math.min(deletion, insertion, substitution);
1088                 }
1089             }
1090         }
1091
1092         return d[m][n];
1093     }
1094 });
1095
1096 Object.defineProperty(String.prototype, "toCamelCase",
1097 {
1098     value()
1099     {
1100         return this.toLowerCase().replace(/[^\w]+(\w)/g, (match, group) => group.toUpperCase());
1101     }
1102 });
1103
1104 Object.defineProperty(String.prototype, "hasMatchingEscapedQuotes",
1105 {
1106     value()
1107     {
1108         return /^\"(?:[^\"\\]|\\.)*\"$/.test(this) || /^\'(?:[^\'\\]|\\.)*\'$/.test(this);
1109     }
1110 });
1111
1112 Object.defineProperty(Math, "roundTo",
1113 {
1114     value(num, step)
1115     {
1116         return Math.round(num / step) * step;
1117     }
1118 });
1119
1120 Object.defineProperty(Number, "constrain",
1121 {
1122     value(num, min, max)
1123     {
1124         if (isNaN(num) || max < min)
1125             return min;
1126
1127         if (num < min)
1128             num = min;
1129         else if (num > max)
1130             num = max;
1131         return num;
1132     }
1133 });
1134
1135 Object.defineProperty(Number, "percentageString",
1136 {
1137     value(fraction, precision = 1)
1138     {
1139         return fraction.toLocaleString(undefined, {minimumFractionDigits: precision, style: "percent"});
1140     }
1141 });
1142
1143 Object.defineProperty(Number, "secondsToMillisecondsString",
1144 {
1145     value(seconds, higherResolution)
1146     {
1147         let ms = seconds * 1000;
1148
1149         if (higherResolution)
1150             return WI.UIString("%.2fms").format(ms);
1151         return WI.UIString("%.1fms").format(ms);
1152     }
1153 });
1154
1155 Object.defineProperty(Number, "secondsToString",
1156 {
1157     value(seconds, higherResolution)
1158     {
1159         let ms = seconds * 1000;
1160         if (!ms)
1161             return WI.UIString("%.0fms").format(0);
1162
1163         const epsilon = 0.0001;
1164
1165         if (Math.abs(ms) < (10 + epsilon)) {
1166             if (higherResolution)
1167                 return WI.UIString("%.3fms").format(ms);
1168             return WI.UIString("%.2fms").format(ms);
1169         }
1170
1171         if (Math.abs(ms) < (100 + epsilon)) {
1172             if (higherResolution)
1173                 return WI.UIString("%.2fms").format(ms);
1174             return WI.UIString("%.1fms").format(ms);
1175         }
1176
1177         if (Math.abs(ms) < (1000 + epsilon)) {
1178             if (higherResolution)
1179                 return WI.UIString("%.1fms").format(ms);
1180             return WI.UIString("%.0fms").format(ms);
1181         }
1182
1183         // Do not go over seconds when in high resolution mode.
1184         if (higherResolution || Math.abs(seconds) < 60)
1185             return WI.UIString("%.2fs").format(seconds);
1186
1187         let minutes = seconds / 60;
1188         if (Math.abs(minutes) < 60)
1189             return WI.UIString("%.1fmin").format(minutes);
1190
1191         let hours = minutes / 60;
1192         if (Math.abs(hours) < 24)
1193             return WI.UIString("%.1fhrs").format(hours);
1194
1195         let days = hours / 24;
1196         return WI.UIString("%.1f days").format(days);
1197     }
1198 });
1199
1200 Object.defineProperty(Number, "bytesToString",
1201 {
1202     value(bytes, higherResolution)
1203     {
1204         if (higherResolution === undefined)
1205             higherResolution = true;
1206
1207         if (Math.abs(bytes) < 1024)
1208             return WI.UIString("%.0f B").format(bytes);
1209
1210         let kilobytes = bytes / 1024;
1211         if (Math.abs(kilobytes) < 1024) {
1212             if (higherResolution || Math.abs(kilobytes) < 10)
1213                 return WI.UIString("%.2f KB").format(kilobytes);
1214             return WI.UIString("%.1f KB").format(kilobytes);
1215         }
1216
1217         let megabytes = kilobytes / 1024;
1218         if (Math.abs(megabytes) < 1024) {
1219             if (higherResolution || Math.abs(megabytes) < 10)
1220                 return WI.UIString("%.2f MB").format(megabytes);
1221             return WI.UIString("%.1f MB").format(megabytes);
1222         }
1223
1224         let gigabytes = megabytes / 1024;
1225         if (higherResolution || Math.abs(gigabytes) < 10)
1226             return WI.UIString("%.2f GB").format(gigabytes);
1227         return WI.UIString("%.1f GB").format(gigabytes);
1228     }
1229 });
1230
1231 Object.defineProperty(Number, "abbreviate",
1232 {
1233     value(num)
1234     {
1235         if (num < 1000)
1236             return num.toLocaleString();
1237
1238         if (num < 1000000)
1239             return WI.UIString("%.1fK").format(Math.round(num / 100) / 10);
1240
1241         if (num < 1000000000)
1242             return WI.UIString("%.1fM").format(Math.round(num / 100000) / 10);
1243
1244         return WI.UIString("%.1fB").format(Math.round(num / 100000000) / 10);
1245     }
1246 });
1247
1248 Object.defineProperty(Number, "zeroPad",
1249 {
1250     value(num, length)
1251     {
1252         let string = num.toLocaleString();
1253         return string.padStart(length, "0");
1254     },
1255 });
1256
1257 Object.defineProperty(Number, "countDigits",
1258 {
1259     value(num)
1260     {
1261         if (num === 0)
1262             return 1;
1263
1264         num = Math.abs(num);
1265         return Math.floor(Math.log(num) * Math.LOG10E) + 1;
1266     }
1267 });
1268
1269 Object.defineProperty(Number.prototype, "maxDecimals",
1270 {
1271     value(decimals)
1272     {
1273         let power = 10 ** decimals;
1274         return Math.round(this * power) / power;
1275     }
1276 });
1277
1278 Object.defineProperty(Uint32Array, "isLittleEndian",
1279 {
1280     value()
1281     {
1282         if ("_isLittleEndian" in this)
1283             return this._isLittleEndian;
1284
1285         var buffer = new ArrayBuffer(4);
1286         var longData = new Uint32Array(buffer);
1287         var data = new Uint8Array(buffer);
1288
1289         longData[0] = 0x0a0b0c0d;
1290
1291         this._isLittleEndian = data[0] === 0x0d && data[1] === 0x0c && data[2] === 0x0b && data[3] === 0x0a;
1292
1293         return this._isLittleEndian;
1294     }
1295 });
1296
1297 function isEmptyObject(object)
1298 {
1299     for (var property in object)
1300         return false;
1301     return true;
1302 }
1303
1304 function isEnterKey(event)
1305 {
1306     // Check if this is an IME event.
1307     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
1308 }
1309
1310 function resolveDotsInPath(path)
1311 {
1312     if (!path)
1313         return path;
1314
1315     if (path.indexOf("./") === -1)
1316         return path;
1317
1318     console.assert(path.charAt(0) === "/");
1319
1320     var result = [];
1321
1322     var components = path.split("/");
1323     for (var i = 0; i < components.length; ++i) {
1324         var component = components[i];
1325
1326         // Skip over "./".
1327         if (component === ".")
1328             continue;
1329
1330         // Rewind one component for "../".
1331         if (component === "..") {
1332             if (result.length === 1)
1333                 continue;
1334             result.pop();
1335             continue;
1336         }
1337
1338         result.push(component);
1339     }
1340
1341     return result.join("/");
1342 }
1343
1344 function parseMIMEType(fullMimeType)
1345 {
1346     if (!fullMimeType)
1347         return {type: fullMimeType, boundary: null, encoding: null};
1348
1349     var typeParts = fullMimeType.split(/\s*;\s*/);
1350     console.assert(typeParts.length >= 1);
1351
1352     var type = typeParts[0];
1353     var boundary = null;
1354     var encoding = null;
1355
1356     for (var i = 1; i < typeParts.length; ++i) {
1357         var subparts = typeParts[i].split(/\s*=\s*/);
1358         if (subparts.length !== 2)
1359             continue;
1360
1361         if (subparts[0].toLowerCase() === "boundary")
1362             boundary = subparts[1];
1363         else if (subparts[0].toLowerCase() === "charset")
1364             encoding = subparts[1].replace("^\"|\"$", ""); // Trim quotes.
1365     }
1366
1367     return {type, boundary: boundary || null, encoding: encoding || null};
1368 }
1369
1370 function simpleGlobStringToRegExp(globString, regExpFlags)
1371 {
1372     // Only supports "*" globs.
1373
1374     if (!globString)
1375         return null;
1376
1377     // Escape everything from String.prototype.escapeForRegExp except "*".
1378     var regexString = globString.escapeCharacters("^[]{}()\\.$+?|");
1379
1380     // Unescape all doubly escaped backslashes in front of escaped asterisks.
1381     // So "\\*" will become "\*" again, undoing escapeCharacters escaping of "\".
1382     // This makes "\*" match a literal "*" instead of using the "*" for globbing.
1383     regexString = regexString.replace(/\\\\\*/g, "\\*");
1384
1385     // The following regex doesn't match an asterisk that has a backslash in front.
1386     // It also catches consecutive asterisks so they collapse down when replaced.
1387     var unescapedAsteriskRegex = /(^|[^\\])\*+/g;
1388     if (unescapedAsteriskRegex.test(globString)) {
1389         // Replace all unescaped asterisks with ".*".
1390         regexString = regexString.replace(unescapedAsteriskRegex, "$1.*");
1391
1392         // Match edge boundaries when there is an asterisk to better meet the expectations
1393         // of the user. When someone types "*.js" they don't expect "foo.json" to match. They
1394         // would only expect that if they type "*.js*". We use \b (instead of ^ and $) to allow
1395         // matches inside paths or URLs, so "ba*.js" will match "foo/bar.js" but not "boo/bbar.js".
1396         // When there isn't an asterisk the regexString is just a substring search.
1397         regexString = "\\b" + regexString + "\\b";
1398     }
1399
1400     return new RegExp(regexString, regExpFlags);
1401 }
1402
1403 Object.defineProperty(Array.prototype, "lowerBound",
1404 {
1405     // Return index of the leftmost element that is equal or greater
1406     // than the specimen object. If there's no such element (i.e. all
1407     // elements are smaller than the specimen) returns array.length.
1408     // The function works for sorted array.
1409     value(object, comparator)
1410     {
1411         function defaultComparator(a, b)
1412         {
1413             return a - b;
1414         }
1415         comparator = comparator || defaultComparator;
1416         var l = 0;
1417         var r = this.length;
1418         while (l < r) {
1419             var m = (l + r) >> 1;
1420             if (comparator(object, this[m]) > 0)
1421                 l = m + 1;
1422             else
1423                 r = m;
1424         }
1425         return r;
1426     }
1427 });
1428
1429 Object.defineProperty(Array.prototype, "upperBound",
1430 {
1431     // Return index of the leftmost element that is greater
1432     // than the specimen object. If there's no such element (i.e. all
1433     // elements are smaller than the specimen) returns array.length.
1434     // The function works for sorted array.
1435     value(object, comparator)
1436     {
1437         function defaultComparator(a, b)
1438         {
1439             return a - b;
1440         }
1441         comparator = comparator || defaultComparator;
1442         var l = 0;
1443         var r = this.length;
1444         while (l < r) {
1445             var m = (l + r) >> 1;
1446             if (comparator(object, this[m]) >= 0)
1447                 l = m + 1;
1448             else
1449                 r = m;
1450         }
1451         return r;
1452     }
1453 });
1454
1455 Object.defineProperty(Array.prototype, "binaryIndexOf",
1456 {
1457     value(value, comparator)
1458     {
1459         var index = this.lowerBound(value, comparator);
1460         return index < this.length && comparator(value, this[index]) === 0 ? index : -1;
1461     }
1462 });
1463
1464 Object.defineProperty(Promise, "chain",
1465 {
1466     async value(callbacks, initialValue)
1467     {
1468         let results = [];
1469         for (let i = 0; i < callbacks.length; ++i)
1470             results.push(await callbacks[i](results.lastValue || initialValue || null, i));
1471         return results;
1472     }
1473 });
1474
1475 Object.defineProperty(Promise, "delay",
1476 {
1477     value(delay)
1478     {
1479         return new Promise((resolve) => setTimeout(resolve, delay || 0));
1480     }
1481 });
1482
1483 (function() {
1484     // The `debounce` function lets you call any function on an object with a delay
1485     // and if the function keeps getting called, the delay gets reset. Since `debounce`
1486     // returns a Proxy, you can cache it and call multiple functions with the same delay.
1487
1488     // Use: object.debounce(200).foo("Argument 1", "Argument 2")
1489     // Note: The last call's arguments get used for the delayed call.
1490
1491     const debounceTimeoutSymbol = Symbol("debounce-timeout");
1492     const debounceSoonProxySymbol = Symbol("debounce-soon-proxy");
1493
1494     Object.defineProperty(Object.prototype, "soon",
1495     {
1496         get()
1497         {
1498             if (!this[debounceSoonProxySymbol])
1499                 this[debounceSoonProxySymbol] = this.debounce(0);
1500             return this[debounceSoonProxySymbol];
1501         }
1502     });
1503
1504     Object.defineProperty(Object.prototype, "debounce",
1505     {
1506         value(delay)
1507         {
1508             console.assert(delay >= 0);
1509
1510             return new Proxy(this, {
1511                 get(target, property, receiver) {
1512                     return (...args) => {
1513                         let original = target[property];
1514                         console.assert(typeof original === "function");
1515
1516                         if (original[debounceTimeoutSymbol])
1517                             clearTimeout(original[debounceTimeoutSymbol]);
1518
1519                         let performWork = () => {
1520                             original[debounceTimeoutSymbol] = undefined;
1521                             original.apply(target, args);
1522                         };
1523
1524                         original[debounceTimeoutSymbol] = setTimeout(performWork, delay);
1525                     };
1526                 }
1527             });
1528         }
1529     });
1530
1531     Object.defineProperty(Function.prototype, "cancelDebounce",
1532     {
1533         value()
1534         {
1535             if (!this[debounceTimeoutSymbol])
1536                 return;
1537
1538             clearTimeout(this[debounceTimeoutSymbol]);
1539             this[debounceTimeoutSymbol] = undefined;
1540         }
1541     });
1542
1543     const requestAnimationFrameSymbol = Symbol("peform-on-animation-frame");
1544     const requestAnimationFrameProxySymbol = Symbol("perform-on-animation-frame-proxy");
1545
1546     Object.defineProperty(Object.prototype, "onNextFrame",
1547     {
1548         get()
1549         {
1550             if (!this[requestAnimationFrameProxySymbol]) {
1551                 this[requestAnimationFrameProxySymbol] = new Proxy(this, {
1552                     get(target, property, receiver) {
1553                         return (...args) => {
1554                             let original = target[property];
1555                             console.assert(typeof original === "function");
1556
1557                             if (original[requestAnimationFrameSymbol])
1558                                 return;
1559
1560                             let performWork = () => {
1561                                 original[requestAnimationFrameSymbol] = undefined;
1562                                 original.apply(target, args);
1563                             };
1564
1565                             original[requestAnimationFrameSymbol] = requestAnimationFrame(performWork);
1566                         };
1567                     }
1568                 });
1569             }
1570
1571             return this[requestAnimationFrameProxySymbol];
1572         }
1573     });
1574
1575     const throttleTimeoutSymbol = Symbol("throttle-timeout");
1576
1577     Object.defineProperty(Object.prototype, "throttle",
1578     {
1579         value(delay)
1580         {
1581             console.assert(delay >= 0);
1582
1583             let lastFireTime = NaN;
1584             let mostRecentArguments = null;
1585
1586             return new Proxy(this, {
1587                 get(target, property, receiver) {
1588                     return (...args) => {
1589                         let original = target[property];
1590                         console.assert(typeof original === "function");
1591                         mostRecentArguments = args;
1592
1593                         function performWork() {
1594                             lastFireTime = Date.now();
1595                             original[throttleTimeoutSymbol] = undefined;
1596                             original.apply(target, mostRecentArguments);
1597                         }
1598
1599                         if (isNaN(lastFireTime)) {
1600                             performWork();
1601                             return;
1602                         }
1603
1604                         let remaining = delay - (Date.now() - lastFireTime);
1605                         if (remaining <= 0) {
1606                             original.cancelThrottle();
1607                             performWork();
1608                             return;
1609                         }
1610
1611                         if (!original[throttleTimeoutSymbol])
1612                             original[throttleTimeoutSymbol] = setTimeout(performWork, remaining);
1613                     };
1614                 }
1615             });
1616         }
1617     });
1618
1619     Object.defineProperty(Function.prototype, "cancelThrottle",
1620     {
1621         value()
1622         {
1623             if (!this[throttleTimeoutSymbol])
1624                 return;
1625
1626             clearTimeout(this[throttleTimeoutSymbol]);
1627             this[throttleTimeoutSymbol] = undefined;
1628         }
1629     });
1630 })();
1631
1632 function appendWebInspectorSourceURL(string)
1633 {
1634     if (string.includes("//# sourceURL"))
1635         return string;
1636     return "\n//# sourceURL=__WebInspectorInternal__\n" + string;
1637 }
1638
1639 function appendWebInspectorConsoleEvaluationSourceURL(string)
1640 {
1641     if (string.includes("//# sourceURL"))
1642         return string;
1643     return "\n//# sourceURL=__WebInspectorConsoleEvaluation__\n" + string;
1644 }
1645
1646 function isWebInspectorInternalScript(url)
1647 {
1648     return url === "__WebInspectorInternal__";
1649 }
1650
1651 function isWebInspectorConsoleEvaluationScript(url)
1652 {
1653     return url === "__WebInspectorConsoleEvaluation__";
1654 }
1655
1656 function isWebKitInjectedScript(url)
1657 {
1658     return url && url.startsWith("__InjectedScript_") && url.endsWith(".js");
1659 }
1660
1661 function isWebKitInternalScript(url)
1662 {
1663     if (isWebInspectorConsoleEvaluationScript(url))
1664         return false;
1665
1666     if (isWebKitInjectedScript(url))
1667         return true;
1668
1669     return url && url.startsWith("__Web") && url.endsWith("__");
1670 }
1671
1672 function isFunctionStringNativeCode(str)
1673 {
1674     return str.endsWith("{\n    [native code]\n}");
1675 }
1676
1677 function whitespaceRatio(content, start, end)
1678 {
1679     let whitespaceScore = 0;
1680     let size = end - start;
1681
1682     for (let i = start; i < end; i++) {
1683         let char = content[i];
1684         if (char === " ")
1685             whitespaceScore++;
1686         else if (char === "\t")
1687             whitespaceScore += 4;
1688         else if (char === "\n")
1689             whitespaceScore += 8;
1690     }
1691
1692     let ratio = whitespaceScore / size;
1693     return ratio;
1694 }
1695
1696 function isTextLikelyMinified(content)
1697 {
1698     const autoFormatMaxCharactersToCheck = 2500;
1699     const autoFormatWhitespaceRatio = 0.2;
1700
1701     if (content.length <= autoFormatMaxCharactersToCheck) {
1702         let ratio = whitespaceRatio(content, 0, content.length);
1703         return ratio < autoFormatWhitespaceRatio;
1704     }
1705
1706     let startRatio = whitespaceRatio(content, 0, autoFormatMaxCharactersToCheck);
1707     if (startRatio < autoFormatWhitespaceRatio)
1708         return true;
1709
1710     let endRatio = whitespaceRatio(content, content.length - autoFormatMaxCharactersToCheck, content.length)
1711     if (endRatio < autoFormatWhitespaceRatio)
1712         return true;
1713
1714     return false;
1715 }
1716
1717 function doubleQuotedString(str)
1718 {
1719     return "\"" + str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
1720 }
1721
1722 function insertionIndexForObjectInListSortedByFunction(object, list, comparator, insertionIndexAfter)
1723 {
1724     if (insertionIndexAfter) {
1725         return list.upperBound(object, comparator);
1726     } else {
1727         return list.lowerBound(object, comparator);
1728     }
1729 }
1730
1731 function insertObjectIntoSortedArray(object, array, comparator)
1732 {
1733     array.splice(insertionIndexForObjectInListSortedByFunction(object, array, comparator), 0, object);
1734 }
1735
1736 function decodeBase64ToBlob(base64Data, mimeType)
1737 {
1738     mimeType = mimeType || "";
1739
1740     const sliceSize = 1024;
1741     var byteCharacters = atob(base64Data);
1742     var bytesLength = byteCharacters.length;
1743     var slicesCount = Math.ceil(bytesLength / sliceSize);
1744     var byteArrays = new Array(slicesCount);
1745
1746     for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
1747         var begin = sliceIndex * sliceSize;
1748         var end = Math.min(begin + sliceSize, bytesLength);
1749
1750         var bytes = new Array(end - begin);
1751         for (var offset = begin, i = 0; offset < end; ++i, ++offset)
1752             bytes[i] = byteCharacters[offset].charCodeAt(0);
1753
1754         byteArrays[sliceIndex] = new Uint8Array(bytes);
1755     }
1756
1757     return new Blob(byteArrays, {type: mimeType});
1758 }
1759
1760 function textToBlob(text, mimeType)
1761 {
1762     return new Blob([text], {type: mimeType});
1763 }
1764
1765 function blobAsText(blob, callback)
1766 {
1767     console.assert(blob instanceof Blob);
1768     let fileReader = new FileReader;
1769     fileReader.addEventListener("loadend", () => { callback(fileReader.result); });
1770     fileReader.readAsText(blob);
1771 }