2011-01-13 Mikhail Naganov <mnaganov@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / utilities.js
1 /*
2  * Copyright (C) 2007 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  *
28  * Contains diff method based on Javascript Diff Algorithm By John Resig
29  * http://ejohn.org/files/jsdiff.js (released under the MIT license).
30  */
31
32 Function.prototype.bind = function(thisObject)
33 {
34     var func = this;
35     var args = Array.prototype.slice.call(arguments, 1);
36     function bound()
37     {
38         return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0)));
39     }
40     bound.toString = function() {
41         return "bound: " + func;
42     };
43     return bound;
44 }
45
46 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
47 {
48     var startNode;
49     var startOffset = 0;
50     var endNode;
51     var endOffset = 0;
52
53     if (!stayWithinNode)
54         stayWithinNode = this;
55
56     if (!direction || direction === "backward" || direction === "both") {
57         var node = this;
58         while (node) {
59             if (node === stayWithinNode) {
60                 if (!startNode)
61                     startNode = stayWithinNode;
62                 break;
63             }
64
65             if (node.nodeType === Node.TEXT_NODE) {
66                 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
67                 for (var i = start; i >= 0; --i) {
68                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
69                         startNode = node;
70                         startOffset = i + 1;
71                         break;
72                     }
73                 }
74             }
75
76             if (startNode)
77                 break;
78
79             node = node.traversePreviousNode(stayWithinNode);
80         }
81
82         if (!startNode) {
83             startNode = stayWithinNode;
84             startOffset = 0;
85         }
86     } else {
87         startNode = this;
88         startOffset = offset;
89     }
90
91     if (!direction || direction === "forward" || direction === "both") {
92         node = this;
93         while (node) {
94             if (node === stayWithinNode) {
95                 if (!endNode)
96                     endNode = stayWithinNode;
97                 break;
98             }
99
100             if (node.nodeType === Node.TEXT_NODE) {
101                 var start = (node === this ? offset : 0);
102                 for (var i = start; i < node.nodeValue.length; ++i) {
103                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
104                         endNode = node;
105                         endOffset = i;
106                         break;
107                     }
108                 }
109             }
110
111             if (endNode)
112                 break;
113
114             node = node.traverseNextNode(stayWithinNode);
115         }
116
117         if (!endNode) {
118             endNode = stayWithinNode;
119             endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
120         }
121     } else {
122         endNode = this;
123         endOffset = offset;
124     }
125
126     var result = this.ownerDocument.createRange();
127     result.setStart(startNode, startOffset);
128     result.setEnd(endNode, endOffset);
129
130     return result;
131 }
132
133 Node.prototype.traverseNextTextNode = function(stayWithin)
134 {
135     var node = this.traverseNextNode(stayWithin);
136     if (!node)
137         return;
138
139     while (node && node.nodeType !== Node.TEXT_NODE)
140         node = node.traverseNextNode(stayWithin);
141
142     return node;
143 }
144
145 Node.prototype.rangeBoundaryForOffset = function(offset)
146 {
147     var node = this.traverseNextTextNode(this);
148     while (node && offset > node.nodeValue.length) {
149         offset -= node.nodeValue.length;
150         node = node.traverseNextTextNode(this);
151     }
152     if (!node)
153         return { container: this, offset: 0 };
154     return { container: node, offset: offset };
155 }
156
157 Element.prototype.removeStyleClass = function(className) 
158 {
159     // Test for the simple case first.
160     if (this.className === className) {
161         this.className = "";
162         return;
163     }
164
165     var index = this.className.indexOf(className);
166     if (index === -1)
167         return;
168
169     this.className = this.className.split(" ").filter(function(s) {
170         return s && s !== className;
171     }).join(" ");
172 }
173
174 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
175 {
176     var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
177     if (regex.test(this.className))
178         this.className = this.className.replace(regex, " ");
179 }
180
181 Element.prototype.addStyleClass = function(className) 
182 {
183     if (className && !this.hasStyleClass(className))
184         this.className += (this.className.length ? " " + className : className);
185 }
186
187 Element.prototype.hasStyleClass = function(className) 
188 {
189     if (!className)
190         return false;
191     // Test for the simple case
192     if (this.className === className)
193         return true;
194
195     var index = this.className.indexOf(className);
196     if (index === -1)
197         return false;
198     var toTest = " " + this.className + " ";
199     return toTest.indexOf(" " + className + " ", index) !== -1;
200 }
201
202 Element.prototype.positionAt = function(x, y)
203 {
204     this.style.left = x + "px";
205     this.style.top = y + "px";
206 }
207
208 Element.prototype.pruneEmptyTextNodes = function()
209 {
210     var sibling = this.firstChild;
211     while (sibling) {
212         var nextSibling = sibling.nextSibling;
213         if (sibling.nodeType === this.TEXT_NODE && sibling.nodeValue === "")
214             this.removeChild(sibling);
215         sibling = nextSibling;
216     }
217 }
218
219 Element.prototype.isScrolledToBottom = function()
220 {
221     return this.scrollTop === this.scrollHeight - this.offsetHeight;
222 }
223
224 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
225 {
226     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
227         for (var i = 0; i < nameArray.length; ++i)
228             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
229                 return node;
230     return null;
231 }
232
233 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
234 {
235     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
236 }
237
238 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
239 {
240     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
241         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
242             return node;
243     return null;
244 }
245
246 Node.prototype.enclosingNodeWithClass = function(className)
247 {
248     if (!this.parentNode)
249         return null;
250     return this.parentNode.enclosingNodeOrSelfWithClass(className);
251 }
252
253 Element.prototype.query = function(query) 
254 {
255     return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
256 }
257
258 Element.prototype.removeChildren = function()
259 {
260     if (this.firstChild)
261         this.textContent = "";
262 }
263
264 Element.prototype.isInsertionCaretInside = function()
265 {
266     var selection = window.getSelection();
267     if (!selection.rangeCount || !selection.isCollapsed)
268         return false;
269     var selectionRange = selection.getRangeAt(0);
270     return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
271 }
272
273 Element.prototype.createChild = function(elementName, className)
274 {
275     var element = document.createElement(elementName);
276     if (className)
277         element.className = className;
278     this.appendChild(element);
279     return element;
280 }
281
282 Element.prototype.__defineGetter__("totalOffsetLeft", function()
283 {
284     var total = 0;
285     for (var element = this; element; element = element.offsetParent)
286         total += element.offsetLeft + (this !== element ? element.clientLeft : 0);
287     return total;
288 });
289
290 Element.prototype.__defineGetter__("totalOffsetTop", function()
291 {
292     var total = 0;
293     for (var element = this; element; element = element.offsetParent)
294         total += element.offsetTop + (this !== element ? element.clientTop : 0);
295     return total;
296 });
297
298 Element.prototype.offsetRelativeToWindow = function(targetWindow)
299 {
300     var elementOffset = {x: 0, y: 0};
301     var curElement = this;
302     var curWindow = this.ownerDocument.defaultView;
303     while (curWindow && curElement) {
304         elementOffset.x += curElement.totalOffsetLeft;
305         elementOffset.y += curElement.totalOffsetTop;
306         if (curWindow === targetWindow)
307             break;
308
309         curElement = curWindow.frameElement;
310         curWindow = curWindow.parent;
311     }
312
313     return elementOffset;
314 }
315
316 KeyboardEvent.prototype.__defineGetter__("data", function()
317 {
318     // Emulate "data" attribute from DOM 3 TextInput event.
319     // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
320     switch (this.type) {
321         case "keypress":
322             if (!this.ctrlKey && !this.metaKey)
323                 return String.fromCharCode(this.charCode);
324             else
325                 return "";
326         case "keydown":
327         case "keyup":
328             if (!this.ctrlKey && !this.metaKey && !this.altKey)
329                 return String.fromCharCode(this.which);
330             else
331                 return "";
332     }
333 });
334
335 Text.prototype.select = function(start, end)
336 {
337     start = start || 0;
338     end = end || this.textContent.length;
339
340     if (start < 0)
341         start = end + start;
342
343     var selection = window.getSelection();
344     selection.removeAllRanges();
345     var range = document.createRange();
346     range.setStart(this, start);
347     range.setEnd(this, end);
348     selection.addRange(range);
349     return this;
350 }
351
352 Element.prototype.__defineGetter__("selectionLeftOffset", function() {
353     // Calculate selection offset relative to the current element.
354
355     var selection = window.getSelection();
356     if (!selection.containsNode(this, true))
357         return null;
358
359     var leftOffset = selection.anchorOffset;
360     var node = selection.anchorNode;
361
362     while (node !== this) {
363         while (node.previousSibling) {
364             node = node.previousSibling;
365             leftOffset += node.textContent.length;
366         }
367         node = node.parentNode;
368     }
369
370     return leftOffset;
371 });
372
373 Node.prototype.isWhitespace = isNodeWhitespace;
374 Node.prototype.displayName = nodeDisplayName;
375 Node.prototype.isAncestor = function(node)
376 {
377     return isAncestorNode(this, node);
378 };
379 Node.prototype.isDescendant = isDescendantNode;
380 Node.prototype.traverseNextNode = traverseNextNode;
381 Node.prototype.traversePreviousNode = traversePreviousNode;
382 Node.prototype.onlyTextChild = onlyTextChild;
383
384 String.prototype.hasSubstring = function(string, caseInsensitive)
385 {
386     if (!caseInsensitive)
387         return this.indexOf(string) !== -1;
388     return this.match(new RegExp(string.escapeForRegExp(), "i"));
389 }
390
391 String.prototype.asParsedURL = function()
392 {
393     // RegExp groups:
394     // 1 - scheme
395     // 2 - hostname
396     // 3 - ?port
397     // 4 - ?path
398     // 5 - ?fragment
399     var match = this.match(/^([^:]+):\/\/([^\/:]*)(?::([\d]+))?(?:(\/[^#]*)(?:#(.*))?)?$/i);
400     if (!match)
401         return null;
402     var result = {};
403     result.scheme = match[1].toLowerCase();
404     result.host = match[2];
405     result.port = match[3];
406     result.path = match[4] || "/";
407     result.fragment = match[5];
408     return result;
409 }
410
411 String.prototype.escapeCharacters = function(chars)
412 {
413     var foundChar = false;
414     for (var i = 0; i < chars.length; ++i) {
415         if (this.indexOf(chars.charAt(i)) !== -1) {
416             foundChar = true;
417             break;
418         }
419     }
420
421     if (!foundChar)
422         return this;
423
424     var result = "";
425     for (var i = 0; i < this.length; ++i) {
426         if (chars.indexOf(this.charAt(i)) !== -1)
427             result += "\\";
428         result += this.charAt(i);
429     }
430
431     return result;
432 }
433
434 String.prototype.escapeForRegExp = function()
435 {
436     return this.escapeCharacters("^[]{}()\\.$*+?|");
437 }
438
439 String.prototype.escapeHTML = function()
440 {
441     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
442 }
443
444 String.prototype.collapseWhitespace = function()
445 {
446     return this.replace(/[\s\xA0]+/g, " ");
447 }
448
449 String.prototype.trimURL = function(baseURLDomain)
450 {
451     var result = this.replace(/^(https|http|file):\/\//i, "");
452     if (baseURLDomain)
453         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
454     return result;
455 }
456
457 function isNodeWhitespace()
458 {
459     if (!this || this.nodeType !== Node.TEXT_NODE)
460         return false;
461     if (!this.nodeValue.length)
462         return true;
463     return this.nodeValue.match(/^[\s\xA0]+$/);
464 }
465
466 function nodeDisplayName()
467 {
468     if (!this)
469         return "";
470
471     switch (this.nodeType) {
472         case Node.DOCUMENT_NODE:
473             return "Document";
474
475         case Node.ELEMENT_NODE:
476             var name = "<" + this.nodeName.toLowerCase();
477
478             if (this.hasAttributes()) {
479                 var value = this.getAttribute("id");
480                 if (value)
481                     name += " id=\"" + value + "\"";
482                 value = this.getAttribute("class");
483                 if (value)
484                     name += " class=\"" + value + "\"";
485                 if (this.nodeName.toLowerCase() === "a") {
486                     value = this.getAttribute("name");
487                     if (value)
488                         name += " name=\"" + value + "\"";
489                     value = this.getAttribute("href");
490                     if (value)
491                         name += " href=\"" + value + "\"";
492                 } else if (this.nodeName.toLowerCase() === "img") {
493                     value = this.getAttribute("src");
494                     if (value)
495                         name += " src=\"" + value + "\"";
496                 } else if (this.nodeName.toLowerCase() === "iframe") {
497                     value = this.getAttribute("src");
498                     if (value)
499                         name += " src=\"" + value + "\"";
500                 } else if (this.nodeName.toLowerCase() === "input") {
501                     value = this.getAttribute("name");
502                     if (value)
503                         name += " name=\"" + value + "\"";
504                     value = this.getAttribute("type");
505                     if (value)
506                         name += " type=\"" + value + "\"";
507                 } else if (this.nodeName.toLowerCase() === "form") {
508                     value = this.getAttribute("action");
509                     if (value)
510                         name += " action=\"" + value + "\"";
511                 }
512             }
513
514             return name + ">";
515
516         case Node.TEXT_NODE:
517             if (isNodeWhitespace.call(this))
518                 return "(whitespace)";
519             return "\"" + this.nodeValue + "\"";
520
521         case Node.COMMENT_NODE:
522             return "<!--" + this.nodeValue + "-->";
523             
524         case Node.DOCUMENT_TYPE_NODE:
525             var docType = "<!DOCTYPE " + this.nodeName;
526             if (this.publicId) {
527                 docType += " PUBLIC \"" + this.publicId + "\"";
528                 if (this.systemId)
529                     docType += " \"" + this.systemId + "\"";
530             } else if (this.systemId)
531                 docType += " SYSTEM \"" + this.systemId + "\"";
532             if (this.internalSubset)
533                 docType += " [" + this.internalSubset + "]";
534             return docType + ">";
535     }
536
537     return this.nodeName.toLowerCase().collapseWhitespace();
538 }
539
540 function isAncestorNode(ancestor, node)
541 {
542     if (!node || !ancestor)
543         return false;
544
545     var currentNode = node.parentNode;
546     while (currentNode) {
547         if (ancestor === currentNode)
548             return true;
549         currentNode = currentNode.parentNode;
550     }
551     return false;
552 }
553
554 function isDescendantNode(descendant)
555 {
556     return isAncestorNode(descendant, this);
557 }
558
559 function traverseNextNode(stayWithin)
560 {
561     if (!this)
562         return;
563
564     var node = this.firstChild;
565     if (node)
566         return node;
567
568     if (stayWithin && this === stayWithin)
569         return null;
570
571     node = this.nextSibling;
572     if (node)
573         return node;
574
575     node = this;
576     while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
577         node = node.parentNode;
578     if (!node)
579         return null;
580
581     return node.nextSibling;
582 }
583
584 function traversePreviousNode(stayWithin)
585 {
586     if (!this)
587         return;
588     if (stayWithin && this === stayWithin)
589         return null;
590     var node = this.previousSibling;
591     while (node && node.lastChild)
592         node = node.lastChild;
593     if (node)
594         return node;
595     return this.parentNode;
596 }
597
598 function onlyTextChild()
599 {
600     if (!this)
601         return null;
602
603     var firstChild = this.firstChild;
604     if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
605         return null;
606
607     var sibling = firstChild.nextSibling;
608     return sibling ? null : firstChild;
609 }
610
611 function appropriateSelectorForNode(node, justSelector)
612 {
613     if (!node)
614         return "";
615
616     var lowerCaseName = node.localName || node.nodeName.toLowerCase();
617
618     var id = node.getAttribute("id");
619     if (id) {
620         var selector = "#" + id;
621         return (justSelector ? selector : lowerCaseName + selector);
622     }
623
624     var className = node.getAttribute("class");
625     if (className) {
626         var selector = "." + className.replace(/\s+/, ".");
627         return (justSelector ? selector : lowerCaseName + selector);
628     }
629
630     if (lowerCaseName === "input" && node.getAttribute("type"))
631         return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
632
633     return lowerCaseName;
634 }
635
636 function getDocumentForNode(node)
637 {
638     return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
639 }
640
641 function parentNode(node)
642 {
643     return node.parentNode;
644 }
645
646 Number.millisToString = function(ms, higherResolution)
647 {
648     return Number.secondsToString(ms / 1000, higherResolution);
649 }
650
651 Number.secondsToString = function(seconds, higherResolution)
652 {
653     if (seconds === 0)
654         return "0";
655
656     var ms = seconds * 1000;
657     if (higherResolution && ms < 1000)
658         return WebInspector.UIString("%.3fms", ms);
659     else if (ms < 1000)
660         return WebInspector.UIString("%.0fms", ms);
661
662     if (seconds < 60)
663         return WebInspector.UIString("%.2fs", seconds);
664
665     var minutes = seconds / 60;
666     if (minutes < 60)
667         return WebInspector.UIString("%.1fmin", minutes);
668
669     var hours = minutes / 60;
670     if (hours < 24)
671         return WebInspector.UIString("%.1fhrs", hours);
672
673     var days = hours / 24;
674     return WebInspector.UIString("%.1f days", days);
675 }
676
677 Number.bytesToString = function(bytes, higherResolution)
678 {
679     if (typeof higherResolution === "undefined")
680         higherResolution = true;
681
682     if (bytes < 1024)
683         return WebInspector.UIString("%.0fB", bytes);
684
685     var kilobytes = bytes / 1024;
686     if (higherResolution && kilobytes < 1024)
687         return WebInspector.UIString("%.2fKB", kilobytes);
688     else if (kilobytes < 1024)
689         return WebInspector.UIString("%.0fKB", kilobytes);
690
691     var megabytes = kilobytes / 1024;
692     if (higherResolution)
693         return WebInspector.UIString("%.2fMB", megabytes);
694     else
695         return WebInspector.UIString("%.0fMB", megabytes);
696 }
697
698 Number.constrain = function(num, min, max)
699 {
700     if (num < min)
701         num = min;
702     else if (num > max)
703         num = max;
704     return num;
705 }
706
707 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
708 {
709     var length = this.value.length;
710     this.setSelectionRange(length, length);
711 }
712
713 Object.defineProperty(Array.prototype, "remove", { value: function(value, onlyFirst)
714 {
715     if (onlyFirst) {
716         var index = this.indexOf(value);
717         if (index !== -1)
718             this.splice(index, 1);
719         return;
720     }
721
722     var length = this.length;
723     for (var i = 0; i < length; ++i) {
724         if (this[i] === value)
725             this.splice(i, 1);
726     }
727 }});
728
729 Object.defineProperty(Array.prototype, "keySet", { value: function()
730 {
731     var keys = {};
732     for (var i = 0; i < this.length; ++i)
733         keys[this[i]] = true;
734     return keys;
735 }});
736
737 Array.diff = function(left, right)
738 {
739     var o = left;
740     var n = right;
741
742     var ns = {};
743     var os = {};
744
745     for (var i = 0; i < n.length; i++) {
746         if (ns[n[i]] == null)
747             ns[n[i]] = { rows: [], o: null };
748         ns[n[i]].rows.push(i);
749     }
750
751     for (var i = 0; i < o.length; i++) {
752         if (os[o[i]] == null)
753             os[o[i]] = { rows: [], n: null };
754         os[o[i]].rows.push(i);
755     }
756
757     for (var i in ns) {
758         if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
759             n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] };
760             o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] };
761         }
762     }
763
764     for (var i = 0; i < n.length - 1; i++) {
765         if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && n[i + 1] == o[n[i].row + 1]) {
766             n[i + 1] = { text: n[i + 1], row: n[i].row + 1 };
767             o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 };
768         }
769     }
770
771     for (var i = n.length - 1; i > 0; i--) {
772         if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 
773             n[i - 1] == o[n[i].row - 1]) {
774             n[i - 1] = { text: n[i - 1], row: n[i].row - 1 };
775             o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 };
776         }
777     }
778
779     return { left: o, right: n };
780 }
781
782 Array.convert = function(list)
783 {
784     // Cast array-like object to an array.
785     return Array.prototype.slice.call(list);
786 }
787
788 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
789 {
790     var first = 0;
791     var last = aList.length - 1;
792     var floor = Math.floor;
793     var mid, c;
794
795     while (first <= last) {
796         mid = floor((first + last) / 2);
797         c = aFunction(anObject, aList[mid]);
798
799         if (c > 0)
800             first = mid + 1;
801         else if (c < 0)
802             last = mid - 1;
803         else {
804             // Return the first occurance of an item in the list.
805             while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0)
806                 mid--;
807             first = mid;
808             break;
809         }
810     }
811
812     return first;
813 }
814
815 String.sprintf = function(format)
816 {
817     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
818 }
819
820 String.tokenizeFormatString = function(format)
821 {
822     var tokens = [];
823     var substitutionIndex = 0;
824
825     function addStringToken(str)
826     {
827         tokens.push({ type: "string", value: str });
828     }
829
830     function addSpecifierToken(specifier, precision, substitutionIndex)
831     {
832         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
833     }
834
835     var index = 0;
836     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
837         addStringToken(format.substring(index, precentIndex));
838         index = precentIndex + 1;
839
840         if (format[index] === "%") {
841             addStringToken("%");
842             ++index;
843             continue;
844         }
845
846         if (!isNaN(format[index])) {
847             // The first character is a number, it might be a substitution index.
848             var number = parseInt(format.substring(index));
849             while (!isNaN(format[index]))
850                 ++index;
851             // If the number is greater than zero and ends with a "$",
852             // then this is a substitution index.
853             if (number > 0 && format[index] === "$") {
854                 substitutionIndex = (number - 1);
855                 ++index;
856             }
857         }
858
859         var precision = -1;
860         if (format[index] === ".") {
861             // This is a precision specifier. If no digit follows the ".",
862             // then the precision should be zero.
863             ++index;
864             precision = parseInt(format.substring(index));
865             if (isNaN(precision))
866                 precision = 0;
867             while (!isNaN(format[index]))
868                 ++index;
869         }
870
871         addSpecifierToken(format[index], precision, substitutionIndex);
872
873         ++substitutionIndex;
874         ++index;
875     }
876
877     addStringToken(format.substring(index));
878
879     return tokens;
880 }
881
882 String.standardFormatters = {
883     d: function(substitution)
884     {
885         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
886             substitution = substitution.description;
887         substitution = parseInt(substitution);
888         return !isNaN(substitution) ? substitution : 0;
889     },
890
891     f: function(substitution, token)
892     {
893         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
894             substitution = substitution.description;
895         substitution = parseFloat(substitution);
896         if (substitution && token.precision > -1)
897             substitution = substitution.toFixed(token.precision);
898         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
899     },
900
901     s: function(substitution)
902     {
903         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) !== "null")
904             substitution = substitution.description;
905         return substitution;
906     },
907 };
908
909 String.vsprintf = function(format, substitutions)
910 {
911     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
912 }
913
914 String.format = function(format, substitutions, formatters, initialValue, append)
915 {
916     if (!format || !substitutions || !substitutions.length)
917         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
918
919     function prettyFunctionName()
920     {
921         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
922     }
923
924     function warn(msg)
925     {
926         console.warn(prettyFunctionName() + ": " + msg);
927     }
928
929     function error(msg)
930     {
931         console.error(prettyFunctionName() + ": " + msg);
932     }
933
934     var result = initialValue;
935     var tokens = String.tokenizeFormatString(format);
936     var usedSubstitutionIndexes = {};
937
938     for (var i = 0; i < tokens.length; ++i) {
939         var token = tokens[i];
940
941         if (token.type === "string") {
942             result = append(result, token.value);
943             continue;
944         }
945
946         if (token.type !== "specifier") {
947             error("Unknown token type \"" + token.type + "\" found.");
948             continue;
949         }
950
951         if (token.substitutionIndex >= substitutions.length) {
952             // If there are not enough substitutions for the current substitutionIndex
953             // just output the format specifier literally and move on.
954             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
955             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
956             continue;
957         }
958
959         usedSubstitutionIndexes[token.substitutionIndex] = true;
960
961         if (!(token.specifier in formatters)) {
962             // Encountered an unsupported format character, treat as a string.
963             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
964             result = append(result, substitutions[token.substitutionIndex]);
965             continue;
966         }
967
968         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
969     }
970
971     var unusedSubstitutions = [];
972     for (var i = 0; i < substitutions.length; ++i) {
973         if (i in usedSubstitutionIndexes)
974             continue;
975         unusedSubstitutions.push(substitutions[i]);
976     }
977
978     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
979 }
980
981 function isEnterKey(event) {
982     // Check if in IME.
983     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
984 }
985
986
987 function highlightSearchResult(element, offset, length)
988 {
989     var lineText = element.textContent;
990     var endOffset = offset + length;
991     var highlightNode = document.createElement("span");
992     highlightNode.className = "webkit-search-result";
993     highlightNode.textContent = lineText.substring(offset, endOffset);
994
995     var boundary = element.rangeBoundaryForOffset(offset);
996     var textNode = boundary.container;
997     var text = textNode.textContent;
998
999     if (boundary.offset + length < text.length) {
1000         // Selection belong to a single split mode.
1001         textNode.textContent = text.substring(boundary.offset + length);
1002         textNode.parentElement.insertBefore(highlightNode, textNode);
1003         var prefixNode = document.createTextNode(text.substring(0, boundary.offset));
1004         textNode.parentElement.insertBefore(prefixNode, highlightNode);
1005         return highlightNode;
1006     }
1007
1008     var parentElement = textNode.parentElement;
1009     var anchorElement = textNode.nextSibling;
1010
1011     length -= text.length - boundary.offset;
1012     textNode.textContent = text.substring(0, boundary.offset);
1013     textNode = textNode.traverseNextTextNode(element);
1014
1015     while (textNode) {
1016         var text = textNode.textContent;
1017         if (length < text.length) {
1018             textNode.textContent = text.substring(length);
1019             break;
1020         }
1021
1022         length -= text.length;
1023         textNode.textContent = "";
1024         textNode = textNode.traverseNextTextNode(element);
1025     }
1026
1027     parentElement.insertBefore(highlightNode, anchorElement);
1028     return highlightNode;
1029 }
1030
1031 function createSearchRegex(query)
1032 {
1033     var regex = "";
1034     for (var i = 0; i < query.length; ++i) {
1035         var char = query.charAt(i);
1036         if (char === "]")
1037             char = "\\]";
1038         regex += "[" + char + "]";
1039     }
1040     return new RegExp(regex, "i");
1041 }
1042
1043 function offerFileForDownload(contents)
1044 {
1045     var builder = new BlobBuilder();
1046     builder.append(contents);
1047     var blob = builder.getBlob("application/octet-stream");
1048     var url = window.createObjectURL(blob);
1049     window.open(url);
1050 }