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 29Object.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 41Object.properties = function(obj) 42{ 43 var properties = []; 44 for (var prop in obj) 45 properties.push(prop); 46 return properties; 47} 48 49Object.sortedProperties = function(obj, sortFunc) 50{ 51 return Object.properties(obj).sort(sortFunc); 52} 53 54Function.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 61Node.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 148Node.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 160Node.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 172Element.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 188Element.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 195Element.prototype.addStyleClass = function(className) 196{ 197 if (className && !this.hasStyleClass(className)) 198 this.className += (this.className.length ? " " + className : className); 199} 200 201Element.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 216Element.prototype.positionAt = function(x, y) 217{ 218 this.style.left = x + "px"; 219 this.style.top = y + "px"; 220} 221 222Node.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 231Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 232{ 233 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 234} 235 236Node.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 244Node.prototype.enclosingNodeWithClass = function(className) 245{ 246 if (!this.parentNode) 247 return null; 248 return this.parentNode.enclosingNodeOrSelfWithClass(className); 249} 250 251Element.prototype.query = function(query) 252{ 253 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 254} 255 256Element.prototype.removeChildren = function() 257{ 258 this.innerHTML = ""; 259} 260 261Element.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 270Element.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 278Element.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 286Element.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 304Node.prototype.isWhitespace = isNodeWhitespace; 305Node.prototype.displayName = nodeDisplayName; 306Node.prototype.isAncestor = function(node) 307{ 308 return isAncestorNode(this, node); 309}; 310Node.prototype.isDescendant = isDescendantNode; 311Node.prototype.traverseNextNode = traverseNextNode; 312Node.prototype.traversePreviousNode = traversePreviousNode; 313Node.prototype.onlyTextChild = onlyTextChild; 314 315String.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 322String.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 345String.prototype.escapeForRegExp = function() 346{ 347 return this.escapeCharacters("^[]{}()\\.$*+?|"); 348} 349 350String.prototype.escapeHTML = function() 351{ 352 return this.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 353} 354 355String.prototype.collapseWhitespace = function() 356{ 357 return this.replace(/[\s\xA0]+/g, " "); 358} 359 360String.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 368function 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 377function 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 451function 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 465function isDescendantNode(descendant) 466{ 467 return isAncestorNode(descendant, this); 468} 469 470function 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 495function 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 509function 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 522function 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 547function getDocumentForNode(node) 548{ 549 return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument; 550} 551 552function parentNode(node) 553{ 554 return node.parentNode; 555} 556 557Number.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 586Number.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 609Number.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 618HTMLTextAreaElement.prototype.moveCursorToEnd = function() 619{ 620 var length = this.value.length; 621 this.setSelectionRange(length, length); 622} 623 624Array.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 640Array.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 648function 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 654function 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 683String.sprintf = function(format) 684{ 685 return String.vsprintf(format, Array.prototype.slice.call(arguments, 1)); 686} 687 688String.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 750String.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 777String.vsprintf = function(format, substitutions) 778{ 779 return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult; 780} 781 782String.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 849function isEnterKey(event) { 850 // Check if in IME. 851 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 852} 853