Web Inspector: move sources panel out of experimental.
[WebKit-https.git] / Source / WebCore / inspector / front-end / utilities.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  * Copyright (C) 2012 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer.
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 String.prototype.hasSubstring = function(string, caseInsensitive)
31 {
32     if (!caseInsensitive)
33         return this.indexOf(string) !== -1;
34     return this.match(new RegExp(string.escapeForRegExp(), "i"));
35 }
36
37 String.prototype.findAll = function(string)
38 {
39     var matches = [];
40     var i = this.indexOf(string);
41     while (i !== -1) {
42         matches.push(i);
43         i = this.indexOf(string, i + string.length);
44     }
45     return matches;
46 }
47
48 String.prototype.lineEndings = function()
49 {
50     if (!this._lineEndings) {
51         this._lineEndings = this.findAll("\n");
52         this._lineEndings.push(this.length);
53     }
54     return this._lineEndings;
55 }
56
57 String.prototype.escapeCharacters = function(chars)
58 {
59     var foundChar = false;
60     for (var i = 0; i < chars.length; ++i) {
61         if (this.indexOf(chars.charAt(i)) !== -1) {
62             foundChar = true;
63             break;
64         }
65     }
66
67     if (!foundChar)
68         return this;
69
70     var result = "";
71     for (var i = 0; i < this.length; ++i) {
72         if (chars.indexOf(this.charAt(i)) !== -1)
73             result += "\\";
74         result += this.charAt(i);
75     }
76
77     return result;
78 }
79
80 String.prototype.escapeForRegExp = function()
81 {
82     return this.escapeCharacters("^[]{}()\\.$*+?|");
83 }
84
85 String.prototype.escapeHTML = function()
86 {
87     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); //" doublequotes just for editor
88 }
89
90 String.prototype.collapseWhitespace = function()
91 {
92     return this.replace(/[\s\xA0]+/g, " ");
93 }
94
95 String.prototype.trimMiddle = function(maxLength)
96 {
97     if (this.length <= maxLength)
98         return this;
99     var leftHalf = maxLength >> 1;
100     var rightHalf = maxLength - leftHalf - 1;
101     return this.substr(0, leftHalf) + "\u2026" + this.substr(this.length - rightHalf, rightHalf);
102 }
103
104 String.prototype.trimEnd = function(maxLength)
105 {
106     if (this.length <= maxLength)
107         return this;
108     return this.substr(0, maxLength - 1) + "\u2026";
109 }
110
111 String.prototype.trimURL = function(baseURLDomain)
112 {
113     var result = this.replace(/^(https|http|file):\/\//i, "");
114     if (baseURLDomain)
115         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
116     return result;
117 }
118
119 String.prototype.removeURLFragment = function()
120 {
121     var fragmentIndex = this.indexOf("#");
122     if (fragmentIndex == -1)
123         fragmentIndex = this.length;
124     return this.substring(0, fragmentIndex);
125 }
126
127 String.prototype.startsWith = function(substring)
128 {
129     return !this.lastIndexOf(substring, 0);
130 }
131
132 String.prototype.endsWith = function(substring)
133 {
134     return this.indexOf(substring, this.length - substring.length) !== -1;
135 }
136
137 Number.constrain = function(num, min, max)
138 {
139     if (num < min)
140         num = min;
141     else if (num > max)
142         num = max;
143     return num;
144 }
145
146 Date.prototype.toISO8601Compact = function()
147 {
148     function leadZero(x)
149     {
150         return x > 9 ? '' + x : '0' + x
151     }
152     return this.getFullYear() +
153            leadZero(this.getMonth() + 1) +
154            leadZero(this.getDate()) + 'T' +
155            leadZero(this.getHours()) +
156            leadZero(this.getMinutes()) +
157            leadZero(this.getSeconds());
158 }
159
160 Object.defineProperty(Array.prototype, "remove",
161 {
162     /**
163      * @this {Array.<*>}
164      */
165     value: function(value, onlyFirst)
166     {
167         if (onlyFirst) {
168             var index = this.indexOf(value);
169             if (index !== -1)
170                 this.splice(index, 1);
171             return;
172         }
173
174         var length = this.length;
175         for (var i = 0; i < length; ++i) {
176             if (this[i] === value)
177                 this.splice(i, 1);
178         }
179     }
180 });
181
182 Object.defineProperty(Array.prototype, "keySet",
183 {
184     /**
185      * @this {Array.<*>}
186      */
187     value: function()
188     {
189         var keys = {};
190         for (var i = 0; i < this.length; ++i)
191             keys[this[i]] = true;
192         return keys;
193     }
194 });
195
196 Object.defineProperty(Array.prototype, "upperBound",
197 {
198     /**
199      * @this {Array.<number>}
200      */
201     value: function(value)
202     {
203         var first = 0;
204         var count = this.length;
205         while (count > 0) {
206           var step = count >> 1;
207           var middle = first + step;
208           if (value >= this[middle]) {
209               first = middle + 1;
210               count -= step + 1;
211           } else
212               count = step;
213         }
214         return first;
215     }
216 });
217
218 Object.defineProperty(Uint32Array.prototype, "sort", {
219    value: Array.prototype.sort
220 });
221
222 (function() {
223 var partition = {
224     /**
225      * @this {Array.<number>}
226      * @param {function(number,number):boolean} comparator
227      * @param {number} left
228      * @param {number} right
229      * @param {number} pivotIndex
230      */
231     value: function(comparator, left, right, pivotIndex)
232     {
233         function swap(array, i1, i2)
234         {
235             var temp = array[i1];
236             array[i1] = array[i2];
237             array[i2] = temp;
238         }
239
240         var pivotValue = this[pivotIndex];
241         swap(this, right, pivotIndex);
242         var storeIndex = left;
243         for (var i = left; i < right; ++i) {
244             if (comparator(this[i], pivotValue) < 0) {
245                 swap(this, storeIndex, i);
246                 ++storeIndex;
247             }
248         }
249         swap(this, right, storeIndex);
250         return storeIndex;
251     }
252 };
253 Object.defineProperty(Array.prototype, "partition", partition);
254 Object.defineProperty(Uint32Array.prototype, "partition", partition);
255
256 var sortRange = {
257     /**
258      * @this {Array.<number>}
259      * @param {function(number,number):boolean} comparator
260      * @param {number} leftBound
261      * @param {number} rightBound
262      * @param {number} k
263      */
264     value: function(comparator, leftBound, rightBound, k)
265     {
266         function quickSortFirstK(array, comparator, left, right, k)
267         {
268             if (right <= left)
269                 return;
270             var pivotIndex = Math.floor(Math.random() * (right - left)) + left;
271             var pivotNewIndex = array.partition(comparator, left, right, pivotIndex);
272             quickSortFirstK(array, comparator, left, pivotNewIndex - 1, k);
273             if (pivotNewIndex < left + k - 1)
274                 quickSortFirstK(array, comparator, pivotNewIndex + 1, right, k);
275         }
276
277         if (leftBound === 0 && rightBound === (this.length - 1) && k === this.length)
278             this.sort(comparator);
279         else
280             quickSortFirstK(this, comparator, leftBound, rightBound, k);
281         return this;
282     }
283 }
284 Object.defineProperty(Array.prototype, "sortRange", sortRange);
285 Object.defineProperty(Uint32Array.prototype, "sortRange", sortRange);
286 })();
287
288 Object.defineProperty(Array.prototype, "qselect",
289 {
290     /**
291      * @this {Array.<number>}
292      * @param {number} k
293      * @param {function(number,number):boolean=} comparator
294      */
295     value: function(k, comparator)
296     {
297         if (k < 0 || k >= this.length)
298             return;
299         if (!comparator)
300             comparator = function(a, b) { return a - b; }
301
302         var low = 0;
303         var high = this.length - 1;
304         for (;;) {
305             var pivotPosition = this.partition(comparator, low, high, Math.floor((high + low) / 2));
306             if (pivotPosition === k)
307                 return this[k];
308             else if (pivotPosition > k)
309                 high = pivotPosition - 1;
310             else
311                 low = pivotPosition + 1;
312         }
313     }
314 });
315
316 /**
317  * @param {*} object
318  * @param {Array.<*>} array
319  * @param {function(*, *):number} comparator
320  */
321 function binarySearch(object, array, comparator)
322 {
323     var first = 0;
324     var last = array.length - 1;
325
326     while (first <= last) {
327         var mid = (first + last) >> 1;
328         var c = comparator(object, array[mid]);
329         if (c > 0)
330             first = mid + 1;
331         else if (c < 0)
332             last = mid - 1;
333         else
334             return mid;
335     }
336
337     // Return the nearest lesser index, "-1" means "0, "-2" means "1", etc.
338     return -(first + 1);
339 }
340
341 Object.defineProperty(Array.prototype, "binaryIndexOf",
342 {
343     /**
344      * @this {Array.<*>}
345      * @param {function(*, *):number} comparator
346      */
347     value: function(value, comparator)
348     {
349         var result = binarySearch(value, this, comparator);
350         return result >= 0 ? result : -1;
351     }
352 });
353
354 /**
355  * @param {*} anObject
356  * @param {Array.<*>} aList
357  * @param {function(*, *)} aFunction
358  */
359 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
360 {
361     var index = binarySearch(anObject, aList, aFunction);
362     if (index < 0)
363         // See binarySearch implementation.
364         return -index - 1;
365     else {
366         // Return the first occurance of an item in the list.
367         while (index > 0 && aFunction(anObject, aList[index - 1]) === 0)
368             index--;
369         return index;
370     }
371 }
372
373 Array.convert = function(list)
374 {
375     // Cast array-like object to an array.
376     return Array.prototype.slice.call(list);
377 }
378
379 /**
380  * @param {string} format
381  * @param {...*} var_arg
382  */
383 String.sprintf = function(format, var_arg)
384 {
385     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
386 }
387
388 String.tokenizeFormatString = function(format, formatters)
389 {
390     var tokens = [];
391     var substitutionIndex = 0;
392
393     function addStringToken(str)
394     {
395         tokens.push({ type: "string", value: str });
396     }
397
398     function addSpecifierToken(specifier, precision, substitutionIndex)
399     {
400         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
401     }
402
403     function isDigit(c)
404     {
405         return !!/[0-9]/.exec(c);
406     }
407
408     var index = 0;
409     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
410         addStringToken(format.substring(index, precentIndex));
411         index = precentIndex + 1;
412
413         if (isDigit(format[index])) {
414             // The first character is a number, it might be a substitution index.
415             var number = parseInt(format.substring(index), 10);
416             while (isDigit(format[index]))
417                 ++index;
418
419             // If the number is greater than zero and ends with a "$",
420             // then this is a substitution index.
421             if (number > 0 && format[index] === "$") {
422                 substitutionIndex = (number - 1);
423                 ++index;
424             }
425         }
426
427         var precision = -1;
428         if (format[index] === ".") {
429             // This is a precision specifier. If no digit follows the ".",
430             // then the precision should be zero.
431             ++index;
432             precision = parseInt(format.substring(index), 10);
433             if (isNaN(precision))
434                 precision = 0;
435
436             while (isDigit(format[index]))
437                 ++index;
438         }
439
440         if (!(format[index] in formatters)) {
441             addStringToken(format.substring(precentIndex, index + 1));
442             ++index;
443             continue;
444         }
445
446         addSpecifierToken(format[index], precision, substitutionIndex);
447
448         ++substitutionIndex;
449         ++index;
450     }
451
452     addStringToken(format.substring(index));
453
454     return tokens;
455 }
456
457 String.standardFormatters = {
458     d: function(substitution)
459     {
460         return !isNaN(substitution) ? substitution : 0;
461     },
462
463     f: function(substitution, token)
464     {
465         if (substitution && token.precision > -1)
466             substitution = substitution.toFixed(token.precision);
467         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
468     },
469
470     s: function(substitution)
471     {
472         return substitution;
473     }
474 }
475
476 String.vsprintf = function(format, substitutions)
477 {
478     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
479 }
480
481 String.format = function(format, substitutions, formatters, initialValue, append)
482 {
483     if (!format || !substitutions || !substitutions.length)
484         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
485
486     function prettyFunctionName()
487     {
488         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
489     }
490
491     function warn(msg)
492     {
493         console.warn(prettyFunctionName() + ": " + msg);
494     }
495
496     function error(msg)
497     {
498         console.error(prettyFunctionName() + ": " + msg);
499     }
500
501     var result = initialValue;
502     var tokens = String.tokenizeFormatString(format, formatters);
503     var usedSubstitutionIndexes = {};
504
505     for (var i = 0; i < tokens.length; ++i) {
506         var token = tokens[i];
507
508         if (token.type === "string") {
509             result = append(result, token.value);
510             continue;
511         }
512
513         if (token.type !== "specifier") {
514             error("Unknown token type \"" + token.type + "\" found.");
515             continue;
516         }
517
518         if (token.substitutionIndex >= substitutions.length) {
519             // If there are not enough substitutions for the current substitutionIndex
520             // just output the format specifier literally and move on.
521             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
522             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
523             continue;
524         }
525
526         usedSubstitutionIndexes[token.substitutionIndex] = true;
527
528         if (!(token.specifier in formatters)) {
529             // Encountered an unsupported format character, treat as a string.
530             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
531             result = append(result, substitutions[token.substitutionIndex]);
532             continue;
533         }
534
535         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
536     }
537
538     var unusedSubstitutions = [];
539     for (var i = 0; i < substitutions.length; ++i) {
540         if (i in usedSubstitutionIndexes)
541             continue;
542         unusedSubstitutions.push(substitutions[i]);
543     }
544
545     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
546 }
547
548 /**
549  * @param {string} query
550  * @param {boolean} caseSensitive
551  * @param {boolean} isRegex
552  * @return {RegExp}
553  */
554 function createSearchRegex(query, caseSensitive, isRegex)
555 {
556     var regexFlags = caseSensitive ? "g" : "gi";
557     var regexObject;
558
559     if (isRegex) {
560         try {
561             regexObject = new RegExp(query, regexFlags);
562         } catch (e) {
563             // Silent catch.
564         }
565     }
566
567     if (!regexObject)
568         regexObject = createPlainTextSearchRegex(query, regexFlags);
569
570     return regexObject;
571 }
572
573 /**
574  * @param {string} query
575  * @param {string=} flags
576  * @return {RegExp}
577  */
578 function createPlainTextSearchRegex(query, flags)
579 {
580     // This should be kept the same as the one in ContentSearchUtils.cpp.
581     var regexSpecialCharacters = "[](){}+-*.,?\\^$|";
582     var regex = "";
583     for (var i = 0; i < query.length; ++i) {
584         var c = query.charAt(i);
585         if (regexSpecialCharacters.indexOf(c) != -1)
586             regex += "\\";
587         regex += c;
588     }
589     return new RegExp(regex, flags || "");
590 }
591
592 /**
593  * @param {RegExp} regex
594  * @param {string} content
595  * @return {number}
596  */
597 function countRegexMatches(regex, content)
598 {
599     var text = content;
600     var result = 0;
601     var match;
602     while (text && (match = regex.exec(text))) {
603         if (match[0].length > 0)
604             ++result;
605         text = text.substring(match.index + 1);
606     }
607     return result;
608 }
609
610 /**
611  * @param {number} value
612  * @param {number} symbolsCount
613  * @return {string}
614  */
615 function numberToStringWithSpacesPadding(value, symbolsCount)
616 {
617     var numberString = value.toString();
618     var paddingLength = Math.max(0, symbolsCount - numberString.length);
619     var paddingString = Array(paddingLength + 1).join("\u00a0");
620     return paddingString + numberString;
621 }
622
623 /**
624  * @constructor
625  */
626 var Map = function()
627 {
628     this._map = {};
629 }
630
631 Map._lastObjectIdentifier = 0;
632
633 Map.prototype = {
634     /**
635      * @param {Object} key
636      */
637     put: function(key, value)
638     {
639         var objectIdentifier = key.__identifier;
640         if (!objectIdentifier) {
641             objectIdentifier = ++Map._lastObjectIdentifier;
642             key.__identifier = objectIdentifier;
643         }
644         this._map[objectIdentifier] = value;
645     },
646     
647     /**
648      * @param {Object} key
649      * @return {Object} value
650      */
651     remove: function(key)
652     {
653         var result = this._map[key.__identifier];
654         delete this._map[key.__identifier];
655         return result;
656     },
657     
658     values: function()
659     {
660         var result = [];
661         for (var objectIdentifier in this._map)
662             result.push(this._map[objectIdentifier]);
663         return result;
664     },
665     
666     /**
667      * @param {Object} key
668      */
669     get: function(key)
670     {
671         return this._map[key.__identifier];
672     },
673     
674     clear: function()
675     {
676         this._map = {};
677     }
678 }