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, "&").replace(/</g, "<").replace(/>/g, ">"); 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