Home | History | Annotate | Download | only in front-end
      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     this.classList.remove(className);
    160 }
    161 
    162 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
    163 {
    164     var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
    165     if (regex.test(this.className))
    166         this.className = this.className.replace(regex, " ");
    167 }
    168 
    169 Element.prototype.addStyleClass = function(className)
    170 {
    171     this.classList.add(className);
    172 }
    173 
    174 Element.prototype.hasStyleClass = function(className)
    175 {
    176     return this.classList.contains(className);
    177 }
    178 
    179 Element.prototype.positionAt = function(x, y)
    180 {
    181     this.style.left = x + "px";
    182     this.style.top = y + "px";
    183 }
    184 
    185 Element.prototype.pruneEmptyTextNodes = function()
    186 {
    187     var sibling = this.firstChild;
    188     while (sibling) {
    189         var nextSibling = sibling.nextSibling;
    190         if (sibling.nodeType === this.TEXT_NODE && sibling.nodeValue === "")
    191             this.removeChild(sibling);
    192         sibling = nextSibling;
    193     }
    194 }
    195 
    196 Element.prototype.isScrolledToBottom = function()
    197 {
    198     // This code works only for 0-width border
    199     return this.scrollTop + this.clientHeight === this.scrollHeight;
    200 }
    201 
    202 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
    203 {
    204     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
    205         for (var i = 0; i < nameArray.length; ++i)
    206             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
    207                 return node;
    208     return null;
    209 }
    210 
    211 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
    212 {
    213     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
    214 }
    215 
    216 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
    217 {
    218     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
    219         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
    220             return node;
    221     return null;
    222 }
    223 
    224 Node.prototype.enclosingNodeWithClass = function(className)
    225 {
    226     if (!this.parentNode)
    227         return null;
    228     return this.parentNode.enclosingNodeOrSelfWithClass(className);
    229 }
    230 
    231 Element.prototype.query = function(query)
    232 {
    233     return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    234 }
    235 
    236 Element.prototype.removeChildren = function()
    237 {
    238     if (this.firstChild)
    239         this.textContent = "";
    240 }
    241 
    242 Element.prototype.isInsertionCaretInside = function()
    243 {
    244     var selection = window.getSelection();
    245     if (!selection.rangeCount || !selection.isCollapsed)
    246         return false;
    247     var selectionRange = selection.getRangeAt(0);
    248     return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
    249 }
    250 
    251 Element.prototype.createChild = function(elementName, className)
    252 {
    253     var element = document.createElement(elementName);
    254     if (className)
    255         element.className = className;
    256     this.appendChild(element);
    257     return element;
    258 }
    259 
    260 Element.prototype.__defineGetter__("totalOffsetLeft", function()
    261 {
    262     var total = 0;
    263     for (var element = this; element; element = element.offsetParent)
    264         total += element.offsetLeft + (this !== element ? element.clientLeft : 0);
    265     return total;
    266 });
    267 
    268 Element.prototype.__defineGetter__("totalOffsetTop", function()
    269 {
    270     var total = 0;
    271     for (var element = this; element; element = element.offsetParent)
    272         total += element.offsetTop + (this !== element ? element.clientTop : 0);
    273     return total;
    274 });
    275 
    276 Element.prototype.offsetRelativeToWindow = function(targetWindow)
    277 {
    278     var elementOffset = {x: 0, y: 0};
    279     var curElement = this;
    280     var curWindow = this.ownerDocument.defaultView;
    281     while (curWindow && curElement) {
    282         elementOffset.x += curElement.totalOffsetLeft;
    283         elementOffset.y += curElement.totalOffsetTop;
    284         if (curWindow === targetWindow)
    285             break;
    286 
    287         curElement = curWindow.frameElement;
    288         curWindow = curWindow.parent;
    289     }
    290 
    291     return elementOffset;
    292 }
    293 
    294 KeyboardEvent.prototype.__defineGetter__("data", function()
    295 {
    296     // Emulate "data" attribute from DOM 3 TextInput event.
    297     // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
    298     switch (this.type) {
    299         case "keypress":
    300             if (!this.ctrlKey && !this.metaKey)
    301                 return String.fromCharCode(this.charCode);
    302             else
    303                 return "";
    304         case "keydown":
    305         case "keyup":
    306             if (!this.ctrlKey && !this.metaKey && !this.altKey)
    307                 return String.fromCharCode(this.which);
    308             else
    309                 return "";
    310     }
    311 });
    312 
    313 Text.prototype.select = function(start, end)
    314 {
    315     start = start || 0;
    316     end = end || this.textContent.length;
    317 
    318     if (start < 0)
    319         start = end + start;
    320 
    321     var selection = window.getSelection();
    322     selection.removeAllRanges();
    323     var range = document.createRange();
    324     range.setStart(this, start);
    325     range.setEnd(this, end);
    326     selection.addRange(range);
    327     return this;
    328 }
    329 
    330 Element.prototype.__defineGetter__("selectionLeftOffset", function() {
    331     // Calculate selection offset relative to the current element.
    332 
    333     var selection = window.getSelection();
    334     if (!selection.containsNode(this, true))
    335         return null;
    336 
    337     var leftOffset = selection.anchorOffset;
    338     var node = selection.anchorNode;
    339 
    340     while (node !== this) {
    341         while (node.previousSibling) {
    342             node = node.previousSibling;
    343             leftOffset += node.textContent.length;
    344         }
    345         node = node.parentNode;
    346     }
    347 
    348     return leftOffset;
    349 });
    350 
    351 Node.prototype.isWhitespace = isNodeWhitespace;
    352 Node.prototype.displayName = nodeDisplayName;
    353 Node.prototype.isAncestor = function(node)
    354 {
    355     return isAncestorNode(this, node);
    356 };
    357 Node.prototype.isDescendant = isDescendantNode;
    358 Node.prototype.traverseNextNode = traverseNextNode;
    359 Node.prototype.traversePreviousNode = traversePreviousNode;
    360 
    361 String.prototype.hasSubstring = function(string, caseInsensitive)
    362 {
    363     if (!caseInsensitive)
    364         return this.indexOf(string) !== -1;
    365     return this.match(new RegExp(string.escapeForRegExp(), "i"));
    366 }
    367 
    368 String.prototype.findAll = function(string)
    369 {
    370     var matches = [];
    371     var i = this.indexOf(string);
    372     while (i !== -1) {
    373         matches.push(i);
    374         i = this.indexOf(string, i + string.length);
    375     }
    376     return matches;
    377 }
    378 
    379 String.prototype.lineEndings = function()
    380 {
    381     if (!this._lineEndings) {
    382         this._lineEndings = this.findAll("\n");
    383         this._lineEndings.push(this.length);
    384     }
    385     return this._lineEndings;
    386 }
    387 
    388 String.prototype.asParsedURL = function()
    389 {
    390     // RegExp groups:
    391     // 1 - scheme
    392     // 2 - hostname
    393     // 3 - ?port
    394     // 4 - ?path
    395     // 5 - ?fragment
    396     var match = this.match(/^([^:]+):\/\/([^\/:]*)(?::([\d]+))?(?:(\/[^#]*)(?:#(.*))?)?$/i);
    397     if (!match)
    398         return null;
    399     var result = {};
    400     result.scheme = match[1].toLowerCase();
    401     result.host = match[2];
    402     result.port = match[3];
    403     result.path = match[4] || "/";
    404     result.fragment = match[5];
    405     return result;
    406 }
    407 
    408 String.prototype.escapeCharacters = function(chars)
    409 {
    410     var foundChar = false;
    411     for (var i = 0; i < chars.length; ++i) {
    412         if (this.indexOf(chars.charAt(i)) !== -1) {
    413             foundChar = true;
    414             break;
    415         }
    416     }
    417 
    418     if (!foundChar)
    419         return this;
    420 
    421     var result = "";
    422     for (var i = 0; i < this.length; ++i) {
    423         if (chars.indexOf(this.charAt(i)) !== -1)
    424             result += "\\";
    425         result += this.charAt(i);
    426     }
    427 
    428     return result;
    429 }
    430 
    431 String.prototype.escapeForRegExp = function()
    432 {
    433     return this.escapeCharacters("^[]{}()\\.$*+?|");
    434 }
    435 
    436 String.prototype.escapeHTML = function()
    437 {
    438     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
    439 }
    440 
    441 String.prototype.collapseWhitespace = function()
    442 {
    443     return this.replace(/[\s\xA0]+/g, " ");
    444 }
    445 
    446 String.prototype.trimURL = function(baseURLDomain)
    447 {
    448     var result = this.replace(/^(https|http|file):\/\//i, "");
    449     if (baseURLDomain)
    450         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
    451     return result;
    452 }
    453 
    454 String.prototype.removeURLFragment = function()
    455 {
    456     var fragmentIndex = this.indexOf("#");
    457     if (fragmentIndex == -1)
    458         fragmentIndex = this.length;
    459     return this.substring(0, fragmentIndex);
    460 }
    461 
    462 function isNodeWhitespace()
    463 {
    464     if (!this || this.nodeType !== Node.TEXT_NODE)
    465         return false;
    466     if (!this.nodeValue.length)
    467         return true;
    468     return this.nodeValue.match(/^[\s\xA0]+$/);
    469 }
    470 
    471 function nodeDisplayName()
    472 {
    473     if (!this)
    474         return "";
    475 
    476     switch (this.nodeType) {
    477         case Node.DOCUMENT_NODE:
    478             return "Document";
    479 
    480         case Node.ELEMENT_NODE:
    481             var name = "<" + this.nodeName.toLowerCase();
    482 
    483             if (this.hasAttributes()) {
    484                 var value = this.getAttribute("id");
    485                 if (value)
    486                     name += " id=\"" + value + "\"";
    487                 value = this.getAttribute("class");
    488                 if (value)
    489                     name += " class=\"" + value + "\"";
    490                 if (this.nodeName.toLowerCase() === "a") {
    491                     value = this.getAttribute("name");
    492                     if (value)
    493                         name += " name=\"" + value + "\"";
    494                     value = this.getAttribute("href");
    495                     if (value)
    496                         name += " href=\"" + value + "\"";
    497                 } else if (this.nodeName.toLowerCase() === "img") {
    498                     value = this.getAttribute("src");
    499                     if (value)
    500                         name += " src=\"" + value + "\"";
    501                 } else if (this.nodeName.toLowerCase() === "iframe") {
    502                     value = this.getAttribute("src");
    503                     if (value)
    504                         name += " src=\"" + value + "\"";
    505                 } else if (this.nodeName.toLowerCase() === "input") {
    506                     value = this.getAttribute("name");
    507                     if (value)
    508                         name += " name=\"" + value + "\"";
    509                     value = this.getAttribute("type");
    510                     if (value)
    511                         name += " type=\"" + value + "\"";
    512                 } else if (this.nodeName.toLowerCase() === "form") {
    513                     value = this.getAttribute("action");
    514                     if (value)
    515                         name += " action=\"" + value + "\"";
    516                 }
    517             }
    518 
    519             return name + ">";
    520 
    521         case Node.TEXT_NODE:
    522             if (isNodeWhitespace.call(this))
    523                 return "(whitespace)";
    524             return "\"" + this.nodeValue + "\"";
    525 
    526         case Node.COMMENT_NODE:
    527             return "<!--" + this.nodeValue + "-->";
    528 
    529         case Node.DOCUMENT_TYPE_NODE:
    530             var docType = "<!DOCTYPE " + this.nodeName;
    531             if (this.publicId) {
    532                 docType += " PUBLIC \"" + this.publicId + "\"";
    533                 if (this.systemId)
    534                     docType += " \"" + this.systemId + "\"";
    535             } else if (this.systemId)
    536                 docType += " SYSTEM \"" + this.systemId + "\"";
    537             if (this.internalSubset)
    538                 docType += " [" + this.internalSubset + "]";
    539             return docType + ">";
    540     }
    541 
    542     return this.nodeName.toLowerCase().collapseWhitespace();
    543 }
    544 
    545 function isAncestorNode(ancestor, node)
    546 {
    547     if (!node || !ancestor)
    548         return false;
    549 
    550     var currentNode = node.parentNode;
    551     while (currentNode) {
    552         if (ancestor === currentNode)
    553             return true;
    554         currentNode = currentNode.parentNode;
    555     }
    556     return false;
    557 }
    558 
    559 function isDescendantNode(descendant)
    560 {
    561     return isAncestorNode(descendant, this);
    562 }
    563 
    564 function traverseNextNode(stayWithin)
    565 {
    566     if (!this)
    567         return;
    568 
    569     var node = this.firstChild;
    570     if (node)
    571         return node;
    572 
    573     if (stayWithin && this === stayWithin)
    574         return null;
    575 
    576     node = this.nextSibling;
    577     if (node)
    578         return node;
    579 
    580     node = this;
    581     while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
    582         node = node.parentNode;
    583     if (!node)
    584         return null;
    585 
    586     return node.nextSibling;
    587 }
    588 
    589 function traversePreviousNode(stayWithin)
    590 {
    591     if (!this)
    592         return;
    593     if (stayWithin && this === stayWithin)
    594         return null;
    595     var node = this.previousSibling;
    596     while (node && node.lastChild)
    597         node = node.lastChild;
    598     if (node)
    599         return node;
    600     return this.parentNode;
    601 }
    602 
    603 function getDocumentForNode(node)
    604 {
    605     return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
    606 }
    607 
    608 function parentNode(node)
    609 {
    610     return node.parentNode;
    611 }
    612 
    613 Number.millisToString = function(ms, higherResolution)
    614 {
    615     return Number.secondsToString(ms / 1000, higherResolution);
    616 }
    617 
    618 Number.secondsToString = function(seconds, higherResolution)
    619 {
    620     if (seconds === 0)
    621         return "0";
    622 
    623     var ms = seconds * 1000;
    624     if (higherResolution && ms < 1000)
    625         return WebInspector.UIString("%.3fms", ms);
    626     else if (ms < 1000)
    627         return WebInspector.UIString("%.0fms", ms);
    628 
    629     if (seconds < 60)
    630         return WebInspector.UIString("%.2fs", seconds);
    631 
    632     var minutes = seconds / 60;
    633     if (minutes < 60)
    634         return WebInspector.UIString("%.1fmin", minutes);
    635 
    636     var hours = minutes / 60;
    637     if (hours < 24)
    638         return WebInspector.UIString("%.1fhrs", hours);
    639 
    640     var days = hours / 24;
    641     return WebInspector.UIString("%.1f days", days);
    642 }
    643 
    644 Number.bytesToString = function(bytes, higherResolution)
    645 {
    646     if (typeof higherResolution === "undefined")
    647         higherResolution = true;
    648 
    649     if (bytes < 1024)
    650         return WebInspector.UIString("%.0fB", bytes);
    651 
    652     var kilobytes = bytes / 1024;
    653     if (higherResolution && kilobytes < 1024)
    654         return WebInspector.UIString("%.2fKB", kilobytes);
    655     else if (kilobytes < 1024)
    656         return WebInspector.UIString("%.0fKB", kilobytes);
    657 
    658     var megabytes = kilobytes / 1024;
    659     if (higherResolution)
    660         return WebInspector.UIString("%.2fMB", megabytes);
    661     else
    662         return WebInspector.UIString("%.0fMB", megabytes);
    663 }
    664 
    665 Number.constrain = function(num, min, max)
    666 {
    667     if (num < min)
    668         num = min;
    669     else if (num > max)
    670         num = max;
    671     return num;
    672 }
    673 
    674 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
    675 {
    676     var length = this.value.length;
    677     this.setSelectionRange(length, length);
    678 }
    679 
    680 Object.defineProperty(Array.prototype, "remove", { value: function(value, onlyFirst)
    681 {
    682     if (onlyFirst) {
    683         var index = this.indexOf(value);
    684         if (index !== -1)
    685             this.splice(index, 1);
    686         return;
    687     }
    688 
    689     var length = this.length;
    690     for (var i = 0; i < length; ++i) {
    691         if (this[i] === value)
    692             this.splice(i, 1);
    693     }
    694 }});
    695 
    696 Object.defineProperty(Array.prototype, "keySet", { value: function()
    697 {
    698     var keys = {};
    699     for (var i = 0; i < this.length; ++i)
    700         keys[this[i]] = true;
    701     return keys;
    702 }});
    703 
    704 Object.defineProperty(Array.prototype, "upperBound", { value: function(value)
    705 {
    706     var first = 0;
    707     var count = this.length;
    708     while (count > 0) {
    709       var step = count >> 1;
    710       var middle = first + step;
    711       if (value >= this[middle]) {
    712           first = middle + 1;
    713           count -= step + 1;
    714       } else
    715           count = step;
    716     }
    717     return first;
    718 }});
    719 
    720 Array.diff = function(left, right)
    721 {
    722     var o = left;
    723     var n = right;
    724 
    725     var ns = {};
    726     var os = {};
    727 
    728     for (var i = 0; i < n.length; i++) {
    729         if (ns[n[i]] == null)
    730             ns[n[i]] = { rows: [], o: null };
    731         ns[n[i]].rows.push(i);
    732     }
    733 
    734     for (var i = 0; i < o.length; i++) {
    735         if (os[o[i]] == null)
    736             os[o[i]] = { rows: [], n: null };
    737         os[o[i]].rows.push(i);
    738     }
    739 
    740     for (var i in ns) {
    741         if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
    742             n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] };
    743             o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] };
    744         }
    745     }
    746 
    747     for (var i = 0; i < n.length - 1; i++) {
    748         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]) {
    749             n[i + 1] = { text: n[i + 1], row: n[i].row + 1 };
    750             o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 };
    751         }
    752     }
    753 
    754     for (var i = n.length - 1; i > 0; i--) {
    755         if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
    756             n[i - 1] == o[n[i].row - 1]) {
    757             n[i - 1] = { text: n[i - 1], row: n[i].row - 1 };
    758             o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 };
    759         }
    760     }
    761 
    762     return { left: o, right: n };
    763 }
    764 
    765 Array.convert = function(list)
    766 {
    767     // Cast array-like object to an array.
    768     return Array.prototype.slice.call(list);
    769 }
    770 
    771 function binarySearch(object, array, comparator)
    772 {
    773     var first = 0;
    774     var last = array.length - 1;
    775 
    776     while (first <= last) {
    777         var mid = (first + last) >> 1;
    778         var c = comparator(object, array[mid]);
    779         if (c > 0)
    780             first = mid + 1;
    781         else if (c < 0)
    782             last = mid - 1;
    783         else
    784             return mid;
    785     }
    786 
    787     // Return the nearest lesser index, "-1" means "0, "-2" means "1", etc.
    788     return -(first + 1);
    789 }
    790 
    791 Object.defineProperty(Array.prototype, "binaryIndexOf", { value: function(value, comparator)
    792 {
    793     var result = binarySearch(value, this, comparator);
    794     return result >= 0 ? result : -1;
    795 }});
    796 
    797 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
    798 {
    799     var index = binarySearch(anObject, aList, aFunction);
    800     if (index < 0)
    801         // See binarySearch implementation.
    802         return -index - 1;
    803     else {
    804         // Return the first occurance of an item in the list.
    805         while (index > 0 && aFunction(anObject, aList[index - 1]) === 0)
    806             index--;
    807         return index;
    808     }
    809 }
    810 
    811 String.sprintf = function(format)
    812 {
    813     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
    814 }
    815 
    816 String.tokenizeFormatString = function(format)
    817 {
    818     var tokens = [];
    819     var substitutionIndex = 0;
    820 
    821     function addStringToken(str)
    822     {
    823         tokens.push({ type: "string", value: str });
    824     }
    825 
    826     function addSpecifierToken(specifier, precision, substitutionIndex)
    827     {
    828         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
    829     }
    830 
    831     var index = 0;
    832     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
    833         addStringToken(format.substring(index, precentIndex));
    834         index = precentIndex + 1;
    835 
    836         if (format[index] === "%") {
    837             addStringToken("%");
    838             ++index;
    839             continue;
    840         }
    841 
    842         if (!isNaN(format[index])) {
    843             // The first character is a number, it might be a substitution index.
    844             var number = parseInt(format.substring(index));
    845             while (!isNaN(format[index]))
    846                 ++index;
    847             // If the number is greater than zero and ends with a "$",
    848             // then this is a substitution index.
    849             if (number > 0 && format[index] === "$") {
    850                 substitutionIndex = (number - 1);
    851                 ++index;
    852             }
    853         }
    854 
    855         var precision = -1;
    856         if (format[index] === ".") {
    857             // This is a precision specifier. If no digit follows the ".",
    858             // then the precision should be zero.
    859             ++index;
    860             precision = parseInt(format.substring(index));
    861             if (isNaN(precision))
    862                 precision = 0;
    863             while (!isNaN(format[index]))
    864                 ++index;
    865         }
    866 
    867         addSpecifierToken(format[index], precision, substitutionIndex);
    868 
    869         ++substitutionIndex;
    870         ++index;
    871     }
    872 
    873     addStringToken(format.substring(index));
    874 
    875     return tokens;
    876 }
    877 
    878 String.standardFormatters = {
    879     d: function(substitution)
    880     {
    881         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
    882             substitution = substitution.description;
    883         substitution = parseInt(substitution);
    884         return !isNaN(substitution) ? substitution : 0;
    885     },
    886 
    887     f: function(substitution, token)
    888     {
    889         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
    890             substitution = substitution.description;
    891         substitution = parseFloat(substitution);
    892         if (substitution && token.precision > -1)
    893             substitution = substitution.toFixed(token.precision);
    894         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
    895     },
    896 
    897     s: function(substitution)
    898     {
    899         if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) !== "null")
    900             substitution = substitution.description;
    901         return substitution;
    902     },
    903 };
    904 
    905 String.vsprintf = function(format, substitutions)
    906 {
    907     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
    908 }
    909 
    910 String.format = function(format, substitutions, formatters, initialValue, append)
    911 {
    912     if (!format || !substitutions || !substitutions.length)
    913         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
    914 
    915     function prettyFunctionName()
    916     {
    917         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
    918     }
    919 
    920     function warn(msg)
    921     {
    922         console.warn(prettyFunctionName() + ": " + msg);
    923     }
    924 
    925     function error(msg)
    926     {
    927         console.error(prettyFunctionName() + ": " + msg);
    928     }
    929 
    930     var result = initialValue;
    931     var tokens = String.tokenizeFormatString(format);
    932     var usedSubstitutionIndexes = {};
    933 
    934     for (var i = 0; i < tokens.length; ++i) {
    935         var token = tokens[i];
    936 
    937         if (token.type === "string") {
    938             result = append(result, token.value);
    939             continue;
    940         }
    941 
    942         if (token.type !== "specifier") {
    943             error("Unknown token type \"" + token.type + "\" found.");
    944             continue;
    945         }
    946 
    947         if (token.substitutionIndex >= substitutions.length) {
    948             // If there are not enough substitutions for the current substitutionIndex
    949             // just output the format specifier literally and move on.
    950             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
    951             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
    952             continue;
    953         }
    954 
    955         usedSubstitutionIndexes[token.substitutionIndex] = true;
    956 
    957         if (!(token.specifier in formatters)) {
    958             // Encountered an unsupported format character, treat as a string.
    959             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
    960             result = append(result, substitutions[token.substitutionIndex]);
    961             continue;
    962         }
    963 
    964         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
    965     }
    966 
    967     var unusedSubstitutions = [];
    968     for (var i = 0; i < substitutions.length; ++i) {
    969         if (i in usedSubstitutionIndexes)
    970             continue;
    971         unusedSubstitutions.push(substitutions[i]);
    972     }
    973 
    974     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
    975 }
    976 
    977 function isEnterKey(event) {
    978     // Check if in IME.
    979     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
    980 }
    981 
    982 function highlightSearchResult(element, offset, length)
    983 {
    984     var result = highlightSearchResults(element, [{offset: offset, length: length }]);
    985     return result.length ? result[0] : null;
    986 }
    987 
    988 function highlightSearchResults(element, resultRanges)
    989 {
    990     var highlightNodes = [];
    991     var lineText = element.textContent;
    992     var textNodeSnapshot = document.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    993 
    994     var snapshotLength = textNodeSnapshot.snapshotLength;
    995     var snapshotNodeOffset = 0;
    996     var currentSnapshotItem = 0;
    997 
    998     for (var i = 0; i < resultRanges.length; ++i) {
    999         var resultLength = resultRanges[i].length;
   1000         var startOffset = resultRanges[i].offset;
   1001         var endOffset = startOffset + resultLength;
   1002         var length = resultLength;
   1003         var textNode;
   1004         var textNodeOffset;
   1005         var found;
   1006 
   1007         while (currentSnapshotItem < snapshotLength) {
   1008             textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
   1009             var textNodeLength = textNode.nodeValue.length;
   1010             if (snapshotNodeOffset + textNodeLength >= startOffset) {
   1011                 textNodeOffset = startOffset - snapshotNodeOffset;
   1012                 snapshotNodeOffset += textNodeLength;
   1013                 found = true;
   1014                 break;
   1015             }
   1016             snapshotNodeOffset += textNodeLength;
   1017         }
   1018 
   1019         if (!found) {
   1020             textNode = element;
   1021             textNodeOffset = 0;
   1022         }
   1023 
   1024         var highlightNode = document.createElement("span");
   1025         highlightNode.className = "webkit-search-result";
   1026         highlightNode.textContent = lineText.substring(startOffset, endOffset);
   1027 
   1028         var text = textNode.textContent;
   1029         if (textNodeOffset + resultLength < text.length) {
   1030             // Selection belongs to a single split mode.
   1031             textNode.textContent = text.substring(textNodeOffset + resultLength);
   1032             textNode.parentElement.insertBefore(highlightNode, textNode);
   1033             var prefixNode = document.createTextNode(text.substring(0, textNodeOffset));
   1034             textNode.parentElement.insertBefore(prefixNode, highlightNode);
   1035 
   1036             highlightNodes.push(highlightNode);
   1037             continue;
   1038         }
   1039 
   1040         var parentElement = textNode.parentElement;
   1041         var anchorElement = textNode.nextSibling;
   1042 
   1043         length -= text.length - textNodeOffset;
   1044         textNode.textContent = text.substring(0, textNodeOffset);
   1045 
   1046         while (currentSnapshotItem < snapshotLength) {
   1047             textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
   1048             snapshotNodeOffset += textNode.nodeValue.length;
   1049             var text = textNode.textContent;
   1050             if (length < text.length) {
   1051                 textNode.textContent = text.substring(length);
   1052                 break;
   1053             }
   1054 
   1055             length -= text.length;
   1056             textNode.textContent = "";
   1057         }
   1058 
   1059         parentElement.insertBefore(highlightNode, anchorElement);
   1060         highlightNodes.push(highlightNode);
   1061     }
   1062 
   1063     return highlightNodes;
   1064 }
   1065 
   1066 function createSearchRegex(query, extraFlags)
   1067 {
   1068     var regex = "";
   1069     for (var i = 0; i < query.length; ++i) {
   1070         var char = query.charAt(i);
   1071         if (char === "]")
   1072             char = "\\]";
   1073         regex += "[" + char + "]";
   1074     }
   1075     return new RegExp(regex, "i" + (extraFlags || ""));
   1076 }
   1077 
   1078 function offerFileForDownload(contents)
   1079 {
   1080     var builder = new BlobBuilder();
   1081     builder.append(contents);
   1082     var blob = builder.getBlob("application/octet-stream");
   1083     var url = window.webkitURL.createObjectURL(blob);
   1084     window.open(url);
   1085 }
   1086