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