653e53aad9a634494c80988a8b63ceb19ef33ea2
[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, formatterFunction, higherResolution)
647 {
648     return Number.secondsToString(ms / 1000, formatterFunction, higherResolution);
649 }
650
651 Number.secondsToString = function(seconds, formatterFunction, higherResolution)
652 {
653     if (!formatterFunction)
654         formatterFunction = String.sprintf;
655
656     if (seconds === 0)
657         return "0";
658
659     var ms = seconds * 1000;
660     if (higherResolution && ms < 1000)
661         return formatterFunction(/*@LS*/"%.3fms", ms);
662     else if (ms < 1000)
663         return formatterFunction(/*@LS*/"%.0fms", ms);
664
665     if (seconds < 60)
666         return formatterFunction(/*@LS*/"%.2fs", seconds);
667
668     var minutes = seconds / 60;
669     if (minutes < 60)
670         return formatterFunction(/*@LS*/"%.1fmin", minutes);
671
672     var hours = minutes / 60;
673     if (hours < 24)
674         return formatterFunction(/*@LS*/"%.1fhrs", hours);
675
676     var days = hours / 24;
677     return formatterFunction(/*@LS*/"%.1f days", days);
678 }
679
680 Number.bytesToString = function(bytes, formatterFunction, higherResolution)
681 {
682     if (!formatterFunction)
683         formatterFunction = String.sprintf;
684     if (typeof higherResolution === "undefined")
685         higherResolution = true;
686
687     if (bytes < 1024)
688         return formatterFunction(/*@LS*/"%.0fB", bytes);
689
690     var kilobytes = bytes / 1024;
691     if (higherResolution && kilobytes < 1024)
692         return formatterFunction(/*@LS*/"%.2fKB", kilobytes);
693     else if (kilobytes < 1024)
694         return formatterFunction(/*@LS*/"%.0fKB", kilobytes);
695
696     var megabytes = kilobytes / 1024;
697     if (higherResolution)
698         return formatterFunction(/*@LS*/"%.2fMB", megabytes);
699     else
700         return formatterFunction(/*@LS*/"%.0fMB", megabytes);
701 }
702
703 Number.constrain = function(num, min, max)
704 {
705     if (num < min)
706         num = min;
707     else if (num > max)
708         num = max;
709     return num;
710 }
711
712 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
713 {
714     var length = this.value.length;
715     this.setSelectionRange(length, length);
716 }
717
718 Object.defineProperty(Array.prototype, "remove", { value: function(value, onlyFirst)
719 {
720     if (onlyFirst) {
721         var index = this.indexOf(value);
722         if (index !== -1)
723             this.splice(index, 1);
724         return;
725     }
726
727     var length = this.length;
728     for (var i = 0; i < length; ++i) {
729         if (this[i] === value)
730             this.splice(i, 1);
731     }
732 }});
733
734 Object.defineProperty(Array.prototype, "keySet", { value: function()
735 {
736     var keys = {};
737     for (var i = 0; i < this.length; ++i)
738         keys[this[i]] = true;
739     return keys;
740 }});
741
742 Array.diff = function(left, right)
743 {
744     var o = left;
745     var n = right;
746
747     var ns = {};
748     var os = {};
749
750     for (var i = 0; i < n.length; i++) {
751         if (ns[n[i]] == null)
752             ns[n[i]] = { rows: [], o: null };
753         ns[n[i]].rows.push(i);
754     }
755
756     for (var i = 0; i < o.length; i++) {
757         if (os[o[i]] == null)
758             os[o[i]] = { rows: [], n: null };
759         os[o[i]].rows.push(i);
760     }
761
762     for (var i in ns) {
763         if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
764             n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] };
765             o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] };
766         }
767     }
768
769     for (var i = 0; i < n.length - 1; i++) {
770         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]) {
771             n[i + 1] = { text: n[i + 1], row: n[i].row + 1 };
772             o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 };
773         }
774     }
775
776     for (var i = n.length - 1; i > 0; i--) {
777         if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 
778             n[i - 1] == o[n[i].row - 1]) {
779             n[i - 1] = { text: n[i - 1], row: n[i].row - 1 };
780             o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 };
781         }
782     }
783
784     return { left: o, right: n };
785 }
786
787 Array.convert = function(list)
788 {
789     // Cast array-like object to an array.
790     return Array.prototype.slice.call(list);
791 }
792
793 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
794 {
795     var first = 0;
796     var last = aList.length - 1;
797     var floor = Math.floor;
798     var mid, c;
799
800     while (first <= last) {
801         mid = floor((first + last) / 2);
802         c = aFunction(anObject, aList[mid]);
803
804         if (c > 0)
805             first = mid + 1;
806         else if (c < 0)
807             last = mid - 1;
808         else {
809             // Return the first occurance of an item in the list.
810             while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0)
811                 mid--;
812             first = mid;
813             break;
814         }
815     }
816
817     return first;
818 }
819
820 String.sprintf = function(format)
821 {
822     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
823 }
824
825 String.tokenizeFormatString = function(format)
826 {
827     var tokens = [];
828     var substitutionIndex = 0;
829
830     function addStringToken(str)
831     {
832         tokens.push({ type: "string", value: str });
833     }
834
835     function addSpecifierToken(specifier, precision, substitutionIndex)
836     {
837         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
838     }
839
840     var index = 0;
841     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
842         addStringToken(format.substring(index, precentIndex));
843         index = precentIndex + 1;
844
845         if (format[index] === "%") {
846             addStringToken("%");
847             ++index;
848             continue;
849         }
850
851         if (!isNaN(format[index])) {
852             // The first character is a number, it might be a substitution index.
853             var number = parseInt(format.substring(index));
854             while (!isNaN(format[index]))
855                 ++index;
856             // If the number is greater than zero and ends with a "$",
857             // then this is a substitution index.
858             if (number > 0 && format[index] === "$") {
859                 substitutionIndex = (number - 1);
860                 ++index;
861             }
862         }
863
864         var precision = -1;
865         if (format[index] === ".") {
866             // This is a precision specifier. If no digit follows the ".",
867             // then the precision should be zero.
868             ++index;
869             precision = parseInt(format.substring(index));
870             if (isNaN(precision))
871                 precision = 0;
872             while (!isNaN(format[index]))
873                 ++index;
874         }
875
876         addSpecifierToken(format[index], precision, substitutionIndex);
877
878         ++substitutionIndex;
879         ++index;
880     }
881
882     addStringToken(format.substring(index));
883
884     return tokens;
885 }
886
887 String.standardFormatters = {
888     d: function(substitution)
889     {
890         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
891             substitution = substitution.description;
892         substitution = parseInt(substitution);
893         return !isNaN(substitution) ? substitution : 0;
894     },
895
896     f: function(substitution, token)
897     {
898         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
899             substitution = substitution.description;
900         substitution = parseFloat(substitution);
901         if (substitution && token.precision > -1)
902             substitution = substitution.toFixed(token.precision);
903         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
904     },
905
906     s: function(substitution)
907     {
908         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) !== "null")
909             substitution = substitution.description;
910         return substitution;
911     },
912 };
913
914 String.vsprintf = function(format, substitutions)
915 {
916     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
917 }
918
919 String.format = function(format, substitutions, formatters, initialValue, append)
920 {
921     if (!format || !substitutions || !substitutions.length)
922         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
923
924     function prettyFunctionName()
925     {
926         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
927     }
928
929     function warn(msg)
930     {
931         console.warn(prettyFunctionName() + ": " + msg);
932     }
933
934     function error(msg)
935     {
936         console.error(prettyFunctionName() + ": " + msg);
937     }
938
939     var result = initialValue;
940     var tokens = String.tokenizeFormatString(format);
941     var usedSubstitutionIndexes = {};
942
943     for (var i = 0; i < tokens.length; ++i) {
944         var token = tokens[i];
945
946         if (token.type === "string") {
947             result = append(result, token.value);
948             continue;
949         }
950
951         if (token.type !== "specifier") {
952             error("Unknown token type \"" + token.type + "\" found.");
953             continue;
954         }
955
956         if (token.substitutionIndex >= substitutions.length) {
957             // If there are not enough substitutions for the current substitutionIndex
958             // just output the format specifier literally and move on.
959             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
960             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
961             continue;
962         }
963
964         usedSubstitutionIndexes[token.substitutionIndex] = true;
965
966         if (!(token.specifier in formatters)) {
967             // Encountered an unsupported format character, treat as a string.
968             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
969             result = append(result, substitutions[token.substitutionIndex]);
970             continue;
971         }
972
973         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
974     }
975
976     var unusedSubstitutions = [];
977     for (var i = 0; i < substitutions.length; ++i) {
978         if (i in usedSubstitutionIndexes)
979             continue;
980         unusedSubstitutions.push(substitutions[i]);
981     }
982
983     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
984 }
985
986 function isEnterKey(event) {
987     // Check if in IME.
988     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
989 }
990
991
992 function highlightSearchResult(element, offset, length)
993 {
994     var lineText = element.textContent;
995     var endOffset = offset + length;
996     var highlightNode = document.createElement("span");
997     highlightNode.className = "webkit-search-result";
998     highlightNode.textContent = lineText.substring(offset, endOffset);
999
1000     var boundary = element.rangeBoundaryForOffset(offset);
1001     var textNode = boundary.container;
1002     var text = textNode.textContent;
1003
1004     if (boundary.offset + length < text.length) {
1005         // Selection belong to a single split mode.
1006         textNode.textContent = text.substring(boundary.offset + length);
1007         textNode.parentElement.insertBefore(highlightNode, textNode);
1008         var prefixNode = document.createTextNode(text.substring(0, boundary.offset));
1009         textNode.parentElement.insertBefore(prefixNode, highlightNode);
1010         return highlightNode;
1011     }
1012
1013     var parentElement = textNode.parentElement;
1014     var anchorElement = textNode.nextSibling;
1015
1016     length -= text.length - boundary.offset;
1017     textNode.textContent = text.substring(0, boundary.offset);
1018     textNode = textNode.traverseNextTextNode(element);
1019
1020     while (textNode) {
1021         var text = textNode.textContent;
1022         if (length < text.length) {
1023             textNode.textContent = text.substring(length);
1024             break;
1025         }
1026
1027         length -= text.length;
1028         textNode.textContent = "";
1029         textNode = textNode.traverseNextTextNode(element);
1030     }
1031
1032     parentElement.insertBefore(highlightNode, anchorElement);
1033     return highlightNode;
1034 }
1035
1036 function createSearchRegex(query)
1037 {
1038     var regex = "";
1039     for (var i = 0; i < query.length; ++i) {
1040         var char = query.charAt(i);
1041         if (char === "]")
1042             char = "\\]";
1043         regex += "[" + char + "]";
1044     }
1045     return new RegExp(regex, "i");
1046 }
1047
1048 function offerFileForDownload(contents)
1049 {
1050     var builder = new BlobBuilder();
1051     builder.append(contents);
1052     var blob = builder.getBlob("application/octet-stream");
1053     var url = window.createObjectURL(blob);
1054     window.open(url);
1055 }