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 
     29 Object.proxyType = function(objectProxy)
     30 {
     31     if (objectProxy === null)
     32         return "null";
     33 
     34     var type = typeof objectProxy;
     35     if (type !== "object" && type !== "function")
     36         return type;
     37 
     38     return objectProxy.type;
     39 }
     40 
     41 Object.properties = function(obj)
     42 {
     43     var properties = [];
     44     for (var prop in obj)
     45         properties.push(prop);
     46     return properties;
     47 }
     48 
     49 Object.sortedProperties = function(obj, sortFunc)
     50 {
     51     return Object.properties(obj).sort(sortFunc);
     52 }
     53 
     54 Function.prototype.bind = function(thisObject)
     55 {
     56     var func = this;
     57     var args = Array.prototype.slice.call(arguments, 1);
     58     return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) };
     59 }
     60 
     61 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
     62 {
     63     var startNode;
     64     var startOffset = 0;
     65     var endNode;
     66     var endOffset = 0;
     67 
     68     if (!stayWithinNode)
     69         stayWithinNode = this;
     70 
     71     if (!direction || direction === "backward" || direction === "both") {
     72         var node = this;
     73         while (node) {
     74             if (node === stayWithinNode) {
     75                 if (!startNode)
     76                     startNode = stayWithinNode;
     77                 break;
     78             }
     79 
     80             if (node.nodeType === Node.TEXT_NODE) {
     81                 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
     82                 for (var i = start; i >= 0; --i) {
     83                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
     84                         startNode = node;
     85                         startOffset = i + 1;
     86                         break;
     87                     }
     88                 }
     89             }
     90 
     91             if (startNode)
     92                 break;
     93 
     94             node = node.traversePreviousNode(stayWithinNode);
     95         }
     96 
     97         if (!startNode) {
     98             startNode = stayWithinNode;
     99             startOffset = 0;
    100         }
    101     } else {
    102         startNode = this;
    103         startOffset = offset;
    104     }
    105 
    106     if (!direction || direction === "forward" || direction === "both") {
    107         node = this;
    108         while (node) {
    109             if (node === stayWithinNode) {
    110                 if (!endNode)
    111                     endNode = stayWithinNode;
    112                 break;
    113             }
    114 
    115             if (node.nodeType === Node.TEXT_NODE) {
    116                 var start = (node === this ? offset : 0);
    117                 for (var i = start; i < node.nodeValue.length; ++i) {
    118                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
    119                         endNode = node;
    120                         endOffset = i;
    121                         break;
    122                     }
    123                 }
    124             }
    125 
    126             if (endNode)
    127                 break;
    128 
    129             node = node.traverseNextNode(stayWithinNode);
    130         }
    131 
    132         if (!endNode) {
    133             endNode = stayWithinNode;
    134             endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
    135         }
    136     } else {
    137         endNode = this;
    138         endOffset = offset;
    139     }
    140 
    141     var result = this.ownerDocument.createRange();
    142     result.setStart(startNode, startOffset);
    143     result.setEnd(endNode, endOffset);
    144 
    145     return result;
    146 }
    147 
    148 Node.prototype.traverseNextTextNode = function(stayWithin)
    149 {
    150     var node = this.traverseNextNode(stayWithin);
    151     if (!node)
    152         return;
    153 
    154     while (node && node.nodeType !== Node.TEXT_NODE)
    155         node = node.traverseNextNode(stayWithin);
    156 
    157     return node;
    158 }
    159 
    160 Node.prototype.rangeBoundaryForOffset = function(offset)
    161 {
    162     var node = this.traverseNextTextNode(this);
    163     while (node && offset > node.nodeValue.length) {
    164         offset -= node.nodeValue.length;
    165         node = node.traverseNextTextNode(this);
    166     }
    167     if (!node)
    168         return { container: this, offset: 0 };
    169     return { container: node, offset: offset };
    170 }
    171 
    172 Element.prototype.removeStyleClass = function(className)
    173 {
    174     // Test for the simple case first.
    175     if (this.className === className) {
    176         this.className = "";
    177         return;
    178     }
    179 
    180     var index = this.className.indexOf(className);
    181     if (index === -1)
    182         return;
    183 
    184     var newClassName = " " + this.className + " ";
    185     this.className = newClassName.replace(" " + className + " ", " ");
    186 }
    187 
    188 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
    189 {
    190     var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
    191     if (regex.test(this.className))
    192         this.className = this.className.replace(regex, " ");
    193 }
    194 
    195 Element.prototype.addStyleClass = function(className)
    196 {
    197     if (className && !this.hasStyleClass(className))
    198         this.className += (this.className.length ? " " + className : className);
    199 }
    200 
    201 Element.prototype.hasStyleClass = function(className)
    202 {
    203     if (!className)
    204         return false;
    205     // Test for the simple case
    206     if (this.className === className)
    207         return true;
    208 
    209     var index = this.className.indexOf(className);
    210     if (index === -1)
    211         return false;
    212     var toTest = " " + this.className + " ";
    213     return toTest.indexOf(" " + className + " ", index) !== -1;
    214 }
    215 
    216 Element.prototype.positionAt = function(x, y)
    217 {
    218     this.style.left = x + "px";
    219     this.style.top = y + "px";
    220 }
    221 
    222 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
    223 {
    224     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
    225         for (var i = 0; i < nameArray.length; ++i)
    226             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
    227                 return node;
    228     return null;
    229 }
    230 
    231 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
    232 {
    233     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
    234 }
    235 
    236 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
    237 {
    238     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
    239         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
    240             return node;
    241     return null;
    242 }
    243 
    244 Node.prototype.enclosingNodeWithClass = function(className)
    245 {
    246     if (!this.parentNode)
    247         return null;
    248     return this.parentNode.enclosingNodeOrSelfWithClass(className);
    249 }
    250 
    251 Element.prototype.query = function(query)
    252 {
    253     return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    254 }
    255 
    256 Element.prototype.removeChildren = function()
    257 {
    258     this.innerHTML = "";
    259 }
    260 
    261 Element.prototype.isInsertionCaretInside = function()
    262 {
    263     var selection = window.getSelection();
    264     if (!selection.rangeCount || !selection.isCollapsed)
    265         return false;
    266     var selectionRange = selection.getRangeAt(0);
    267     return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
    268 }
    269 
    270 Element.prototype.__defineGetter__("totalOffsetLeft", function()
    271 {
    272     var total = 0;
    273     for (var element = this; element; element = element.offsetParent)
    274         total += element.offsetLeft;
    275     return total;
    276 });
    277 
    278 Element.prototype.__defineGetter__("totalOffsetTop", function()
    279 {
    280     var total = 0;
    281     for (var element = this; element; element = element.offsetParent)
    282         total += element.offsetTop;
    283     return total;
    284 });
    285 
    286 Element.prototype.offsetRelativeToWindow = function(targetWindow)
    287 {
    288     var elementOffset = {x: 0, y: 0};
    289     var curElement = this;
    290     var curWindow = this.ownerDocument.defaultView;
    291     while (curWindow && curElement) {
    292         elementOffset.x += curElement.totalOffsetLeft;
    293         elementOffset.y += curElement.totalOffsetTop;
    294         if (curWindow === targetWindow)
    295             break;
    296 
    297         curElement = curWindow.frameElement;
    298         curWindow = curWindow.parent;
    299     }
    300 
    301     return elementOffset;
    302 }
    303 
    304 Node.prototype.isWhitespace = isNodeWhitespace;
    305 Node.prototype.displayName = nodeDisplayName;
    306 Node.prototype.isAncestor = function(node)
    307 {
    308     return isAncestorNode(this, node);
    309 };
    310 Node.prototype.isDescendant = isDescendantNode;
    311 Node.prototype.traverseNextNode = traverseNextNode;
    312 Node.prototype.traversePreviousNode = traversePreviousNode;
    313 Node.prototype.onlyTextChild = onlyTextChild;
    314 
    315 String.prototype.hasSubstring = function(string, caseInsensitive)
    316 {
    317     if (!caseInsensitive)
    318         return this.indexOf(string) !== -1;
    319     return this.match(new RegExp(string.escapeForRegExp(), "i"));
    320 }
    321 
    322 String.prototype.escapeCharacters = function(chars)
    323 {
    324     var foundChar = false;
    325     for (var i = 0; i < chars.length; ++i) {
    326         if (this.indexOf(chars.charAt(i)) !== -1) {
    327             foundChar = true;
    328             break;
    329         }
    330     }
    331 
    332     if (!foundChar)
    333         return this;
    334 
    335     var result = "";
    336     for (var i = 0; i < this.length; ++i) {
    337         if (chars.indexOf(this.charAt(i)) !== -1)
    338             result += "\\";
    339         result += this.charAt(i);
    340     }
    341 
    342     return result;
    343 }
    344 
    345 String.prototype.escapeForRegExp = function()
    346 {
    347     return this.escapeCharacters("^[]{}()\\.$*+?|");
    348 }
    349 
    350 String.prototype.escapeHTML = function()
    351 {
    352     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    353 }
    354 
    355 String.prototype.collapseWhitespace = function()
    356 {
    357     return this.replace(/[\s\xA0]+/g, " ");
    358 }
    359 
    360 String.prototype.trimURL = function(baseURLDomain)
    361 {
    362     var result = this.replace(/^https?:\/\//i, "");
    363     if (baseURLDomain)
    364         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
    365     return result;
    366 }
    367 
    368 function isNodeWhitespace()
    369 {
    370     if (!this || this.nodeType !== Node.TEXT_NODE)
    371         return false;
    372     if (!this.nodeValue.length)
    373         return true;
    374     return this.nodeValue.match(/^[\s\xA0]+$/);
    375 }
    376 
    377 function nodeDisplayName()
    378 {
    379     if (!this)
    380         return "";
    381 
    382     switch (this.nodeType) {
    383         case Node.DOCUMENT_NODE:
    384             return "Document";
    385 
    386         case Node.ELEMENT_NODE:
    387             var name = "<" + this.nodeName.toLowerCase();
    388 
    389             if (this.hasAttributes()) {
    390                 var value = this.getAttribute("id");
    391                 if (value)
    392                     name += " id=\"" + value + "\"";
    393                 value = this.getAttribute("class");
    394                 if (value)
    395                     name += " class=\"" + value + "\"";
    396                 if (this.nodeName.toLowerCase() === "a") {
    397                     value = this.getAttribute("name");
    398                     if (value)
    399                         name += " name=\"" + value + "\"";
    400                     value = this.getAttribute("href");
    401                     if (value)
    402                         name += " href=\"" + value + "\"";
    403                 } else if (this.nodeName.toLowerCase() === "img") {
    404                     value = this.getAttribute("src");
    405                     if (value)
    406                         name += " src=\"" + value + "\"";
    407                 } else if (this.nodeName.toLowerCase() === "iframe") {
    408                     value = this.getAttribute("src");
    409                     if (value)
    410                         name += " src=\"" + value + "\"";
    411                 } else if (this.nodeName.toLowerCase() === "input") {
    412                     value = this.getAttribute("name");
    413                     if (value)
    414                         name += " name=\"" + value + "\"";
    415                     value = this.getAttribute("type");
    416                     if (value)
    417                         name += " type=\"" + value + "\"";
    418                 } else if (this.nodeName.toLowerCase() === "form") {
    419                     value = this.getAttribute("action");
    420                     if (value)
    421                         name += " action=\"" + value + "\"";
    422                 }
    423             }
    424 
    425             return name + ">";
    426 
    427         case Node.TEXT_NODE:
    428             if (isNodeWhitespace.call(this))
    429                 return "(whitespace)";
    430             return "\"" + this.nodeValue + "\"";
    431 
    432         case Node.COMMENT_NODE:
    433             return "<!--" + this.nodeValue + "-->";
    434 
    435         case Node.DOCUMENT_TYPE_NODE:
    436             var docType = "<!DOCTYPE " + this.nodeName;
    437             if (this.publicId) {
    438                 docType += " PUBLIC \"" + this.publicId + "\"";
    439                 if (this.systemId)
    440                     docType += " \"" + this.systemId + "\"";
    441             } else if (this.systemId)
    442                 docType += " SYSTEM \"" + this.systemId + "\"";
    443             if (this.internalSubset)
    444                 docType += " [" + this.internalSubset + "]";
    445             return docType + ">";
    446     }
    447 
    448     return this.nodeName.toLowerCase().collapseWhitespace();
    449 }
    450 
    451 function isAncestorNode(ancestor, node)
    452 {
    453     if (!node || !ancestor)
    454         return false;
    455 
    456     var currentNode = node.parentNode;
    457     while (currentNode) {
    458         if (ancestor === currentNode)
    459             return true;
    460         currentNode = currentNode.parentNode;
    461     }
    462     return false;
    463 }
    464 
    465 function isDescendantNode(descendant)
    466 {
    467     return isAncestorNode(descendant, this);
    468 }
    469 
    470 function traverseNextNode(stayWithin)
    471 {
    472     if (!this)
    473         return;
    474 
    475     var node = this.firstChild;
    476     if (node)
    477         return node;
    478 
    479     if (stayWithin && this === stayWithin)
    480         return null;
    481 
    482     node = this.nextSibling;
    483     if (node)
    484         return node;
    485 
    486     node = this;
    487     while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
    488         node = node.parentNode;
    489     if (!node)
    490         return null;
    491 
    492     return node.nextSibling;
    493 }
    494 
    495 function traversePreviousNode(stayWithin)
    496 {
    497     if (!this)
    498         return;
    499     if (stayWithin && this === stayWithin)
    500         return null;
    501     var node = this.previousSibling;
    502     while (node && node.lastChild)
    503         node = node.lastChild;
    504     if (node)
    505         return node;
    506     return this.parentNode;
    507 }
    508 
    509 function onlyTextChild()
    510 {
    511     if (!this)
    512         return null;
    513 
    514     var firstChild = this.firstChild;
    515     if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
    516         return null;
    517 
    518     var sibling = firstChild.nextSibling;
    519     return sibling ? null : firstChild;
    520 }
    521 
    522 function appropriateSelectorForNode(node, justSelector)
    523 {
    524     if (!node)
    525         return "";
    526 
    527     var lowerCaseName = node.localName || node.nodeName.toLowerCase();
    528 
    529     var id = node.getAttribute("id");
    530     if (id) {
    531         var selector = "#" + id;
    532         return (justSelector ? selector : lowerCaseName + selector);
    533     }
    534 
    535     var className = node.getAttribute("class");
    536     if (className) {
    537         var selector = "." + className.replace(/\s+/, ".");
    538         return (justSelector ? selector : lowerCaseName + selector);
    539     }
    540 
    541     if (lowerCaseName === "input" && node.getAttribute("type"))
    542         return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
    543 
    544     return lowerCaseName;
    545 }
    546 
    547 function getDocumentForNode(node)
    548 {
    549     return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
    550 }
    551 
    552 function parentNode(node)
    553 {
    554     return node.parentNode;
    555 }
    556 
    557 Number.secondsToString = function(seconds, formatterFunction, higherResolution)
    558 {
    559     if (!formatterFunction)
    560         formatterFunction = String.sprintf;
    561 
    562     if (seconds === 0)
    563         return "0";
    564 
    565     var ms = seconds * 1000;
    566     if (higherResolution && ms < 1000)
    567         return formatterFunction("%.3fms", ms);
    568     else if (ms < 1000)
    569         return formatterFunction("%.0fms", ms);
    570 
    571     if (seconds < 60)
    572         return formatterFunction("%.2fs", seconds);
    573 
    574     var minutes = seconds / 60;
    575     if (minutes < 60)
    576         return formatterFunction("%.1fmin", minutes);
    577 
    578     var hours = minutes / 60;
    579     if (hours < 24)
    580         return formatterFunction("%.1fhrs", hours);
    581 
    582     var days = hours / 24;
    583     return formatterFunction("%.1f days", days);
    584 }
    585 
    586 Number.bytesToString = function(bytes, formatterFunction, higherResolution)
    587 {
    588     if (!formatterFunction)
    589         formatterFunction = String.sprintf;
    590     if (typeof higherResolution === "undefined")
    591         higherResolution = true;
    592 
    593     if (bytes < 1024)
    594         return formatterFunction("%.0fB", bytes);
    595 
    596     var kilobytes = bytes / 1024;
    597     if (higherResolution && kilobytes < 1024)
    598         return formatterFunction("%.2fKB", kilobytes);
    599     else if (kilobytes < 1024)
    600         return formatterFunction("%.0fKB", kilobytes);
    601 
    602     var megabytes = kilobytes / 1024;
    603     if (higherResolution)
    604         return formatterFunction("%.3fMB", megabytes);
    605     else
    606         return formatterFunction("%.0fMB", megabytes);
    607 }
    608 
    609 Number.constrain = function(num, min, max)
    610 {
    611     if (num < min)
    612         num = min;
    613     else if (num > max)
    614         num = max;
    615     return num;
    616 }
    617 
    618 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
    619 {
    620     var length = this.value.length;
    621     this.setSelectionRange(length, length);
    622 }
    623 
    624 Array.prototype.remove = function(value, onlyFirst)
    625 {
    626     if (onlyFirst) {
    627         var index = this.indexOf(value);
    628         if (index !== -1)
    629             this.splice(index, 1);
    630         return;
    631     }
    632 
    633     var length = this.length;
    634     for (var i = 0; i < length; ++i) {
    635         if (this[i] === value)
    636             this.splice(i, 1);
    637     }
    638 }
    639 
    640 Array.prototype.keySet = function()
    641 {
    642     var keys = {};
    643     for (var i = 0; i < this.length; ++i)
    644         keys[this[i]] = true;
    645     return keys;
    646 }
    647 
    648 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
    649 {
    650     // indexOf returns (-lowerBound - 1). Taking (-result - 1) works out to lowerBound.
    651     return (-indexOfObjectInListSortedByFunction(anObject, aList, aFunction) - 1);
    652 }
    653 
    654 function indexOfObjectInListSortedByFunction(anObject, aList, aFunction)
    655 {
    656     var first = 0;
    657     var last = aList.length - 1;
    658     var floor = Math.floor;
    659     var mid, c;
    660 
    661     while (first <= last) {
    662         mid = floor((first + last) / 2);
    663         c = aFunction(anObject, aList[mid]);
    664 
    665         if (c > 0)
    666             first = mid + 1;
    667         else if (c < 0)
    668             last = mid - 1;
    669         else {
    670             // Return the first occurance of an item in the list.
    671             while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0)
    672                 mid--;
    673             first = mid;
    674             break;
    675         }
    676     }
    677 
    678     // By returning 1 less than the negative lower search bound, we can reuse this function
    679     // for both indexOf and insertionIndexFor, with some simple arithmetic.
    680     return (-first - 1);
    681 }
    682 
    683 String.sprintf = function(format)
    684 {
    685     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
    686 }
    687 
    688 String.tokenizeFormatString = function(format)
    689 {
    690     var tokens = [];
    691     var substitutionIndex = 0;
    692 
    693     function addStringToken(str)
    694     {
    695         tokens.push({ type: "string", value: str });
    696     }
    697 
    698     function addSpecifierToken(specifier, precision, substitutionIndex)
    699     {
    700         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
    701     }
    702 
    703     var index = 0;
    704     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
    705         addStringToken(format.substring(index, precentIndex));
    706         index = precentIndex + 1;
    707 
    708         if (format[index] === "%") {
    709             addStringToken("%");
    710             ++index;
    711             continue;
    712         }
    713 
    714         if (!isNaN(format[index])) {
    715             // The first character is a number, it might be a substitution index.
    716             var number = parseInt(format.substring(index));
    717             while (!isNaN(format[index]))
    718                 ++index;
    719             // If the number is greater than zero and ends with a "$",
    720             // then this is a substitution index.
    721             if (number > 0 && format[index] === "$") {
    722                 substitutionIndex = (number - 1);
    723                 ++index;
    724             }
    725         }
    726 
    727         var precision = -1;
    728         if (format[index] === ".") {
    729             // This is a precision specifier. If no digit follows the ".",
    730             // then the precision should be zero.
    731             ++index;
    732             precision = parseInt(format.substring(index));
    733             if (isNaN(precision))
    734                 precision = 0;
    735             while (!isNaN(format[index]))
    736                 ++index;
    737         }
    738 
    739         addSpecifierToken(format[index], precision, substitutionIndex);
    740 
    741         ++substitutionIndex;
    742         ++index;
    743     }
    744 
    745     addStringToken(format.substring(index));
    746 
    747     return tokens;
    748 }
    749 
    750 String.standardFormatters = {
    751     d: function(substitution)
    752     {
    753         if (typeof substitution == "object" && Object.proxyType(substitution) === "number")
    754             substitution = substitution.description;
    755         substitution = parseInt(substitution);
    756         return !isNaN(substitution) ? substitution : 0;
    757     },
    758 
    759     f: function(substitution, token)
    760     {
    761         if (typeof substitution == "object" && Object.proxyType(substitution) === "number")
    762             substitution = substitution.description;
    763         substitution = parseFloat(substitution);
    764         if (substitution && token.precision > -1)
    765             substitution = substitution.toFixed(token.precision);
    766         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
    767     },
    768 
    769     s: function(substitution)
    770     {
    771         if (typeof substitution == "object" && Object.proxyType(substitution) !== "null")
    772             substitution = substitution.description;
    773         return substitution;
    774     },
    775 };
    776 
    777 String.vsprintf = function(format, substitutions)
    778 {
    779     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
    780 }
    781 
    782 String.format = function(format, substitutions, formatters, initialValue, append)
    783 {
    784     if (!format || !substitutions || !substitutions.length)
    785         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
    786 
    787     function prettyFunctionName()
    788     {
    789         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
    790     }
    791 
    792     function warn(msg)
    793     {
    794         console.warn(prettyFunctionName() + ": " + msg);
    795     }
    796 
    797     function error(msg)
    798     {
    799         console.error(prettyFunctionName() + ": " + msg);
    800     }
    801 
    802     var result = initialValue;
    803     var tokens = String.tokenizeFormatString(format);
    804     var usedSubstitutionIndexes = {};
    805 
    806     for (var i = 0; i < tokens.length; ++i) {
    807         var token = tokens[i];
    808 
    809         if (token.type === "string") {
    810             result = append(result, token.value);
    811             continue;
    812         }
    813 
    814         if (token.type !== "specifier") {
    815             error("Unknown token type \"" + token.type + "\" found.");
    816             continue;
    817         }
    818 
    819         if (token.substitutionIndex >= substitutions.length) {
    820             // If there are not enough substitutions for the current substitutionIndex
    821             // just output the format specifier literally and move on.
    822             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
    823             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
    824             continue;
    825         }
    826 
    827         usedSubstitutionIndexes[token.substitutionIndex] = true;
    828 
    829         if (!(token.specifier in formatters)) {
    830             // Encountered an unsupported format character, treat as a string.
    831             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
    832             result = append(result, substitutions[token.substitutionIndex]);
    833             continue;
    834         }
    835 
    836         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
    837     }
    838 
    839     var unusedSubstitutions = [];
    840     for (var i = 0; i < substitutions.length; ++i) {
    841         if (i in usedSubstitutionIndexes)
    842             continue;
    843         unusedSubstitutions.push(substitutions[i]);
    844     }
    845 
    846     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
    847 }
    848 
    849 function isEnterKey(event) {
    850     // Check if in IME.
    851     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
    852 }
    853