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.type = function(obj, win) 30{ 31 if (obj === null) 32 return "null"; 33 34 var type = typeof obj; 35 if (type !== "object" && type !== "function") 36 return type; 37 38 win = win || window; 39 40 if (obj instanceof win.Node) 41 return (obj.nodeType === undefined ? type : "node"); 42 if (obj instanceof win.String) 43 return "string"; 44 if (obj instanceof win.Array) 45 return "array"; 46 if (obj instanceof win.Boolean) 47 return "boolean"; 48 if (obj instanceof win.Number) 49 return "number"; 50 if (obj instanceof win.Date) 51 return "date"; 52 if (obj instanceof win.RegExp) 53 return "regexp"; 54 if (obj instanceof win.Error) 55 return "error"; 56 return type; 57} 58 59Object.hasProperties = function(obj) 60{ 61 if (typeof obj === "undefined" || typeof obj === "null") 62 return false; 63 for (var name in obj) 64 return true; 65 return false; 66} 67 68Object.describe = function(obj, abbreviated) 69{ 70 var type1 = Object.type(obj); 71 var type2 = Object.prototype.toString.call(obj).replace(/^\[object (.*)\]$/i, "$1"); 72 73 switch (type1) { 74 case "object": 75 case "node": 76 return type2; 77 case "array": 78 return "[" + obj.toString() + "]"; 79 case "string": 80 if (obj.length > 100) 81 return "\"" + obj.substring(0, 100) + "\u2026\""; 82 return "\"" + obj + "\""; 83 case "function": 84 var objectText = String(obj); 85 if (!/^function /.test(objectText)) 86 objectText = (type2 == "object") ? type1 : type2; 87 else if (abbreviated) 88 objectText = /.*/.exec(obj)[0].replace(/ +$/g, ""); 89 return objectText; 90 case "regexp": 91 return String(obj).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1); 92 default: 93 return String(obj); 94 } 95} 96 97Object.properties = function(obj) 98{ 99 var properties = []; 100 for (var prop in obj) 101 properties.push(prop); 102 return properties; 103} 104 105Object.sortedProperties = function(obj, sortFunc) 106{ 107 return Object.properties(obj).sort(sortFunc); 108} 109 110Function.prototype.bind = function(thisObject) 111{ 112 var func = this; 113 var args = Array.prototype.slice.call(arguments, 1); 114 return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) }; 115} 116 117Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction) 118{ 119 var startNode; 120 var startOffset = 0; 121 var endNode; 122 var endOffset = 0; 123 124 if (!stayWithinNode) 125 stayWithinNode = this; 126 127 if (!direction || direction === "backward" || direction === "both") { 128 var node = this; 129 while (node) { 130 if (node === stayWithinNode) { 131 if (!startNode) 132 startNode = stayWithinNode; 133 break; 134 } 135 136 if (node.nodeType === Node.TEXT_NODE) { 137 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1)); 138 for (var i = start; i >= 0; --i) { 139 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 140 startNode = node; 141 startOffset = i + 1; 142 break; 143 } 144 } 145 } 146 147 if (startNode) 148 break; 149 150 node = node.traversePreviousNode(false, stayWithinNode); 151 } 152 153 if (!startNode) { 154 startNode = stayWithinNode; 155 startOffset = 0; 156 } 157 } else { 158 startNode = this; 159 startOffset = offset; 160 } 161 162 if (!direction || direction === "forward" || direction === "both") { 163 node = this; 164 while (node) { 165 if (node === stayWithinNode) { 166 if (!endNode) 167 endNode = stayWithinNode; 168 break; 169 } 170 171 if (node.nodeType === Node.TEXT_NODE) { 172 var start = (node === this ? offset : 0); 173 for (var i = start; i < node.nodeValue.length; ++i) { 174 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 175 endNode = node; 176 endOffset = i; 177 break; 178 } 179 } 180 } 181 182 if (endNode) 183 break; 184 185 node = node.traverseNextNode(false, stayWithinNode); 186 } 187 188 if (!endNode) { 189 endNode = stayWithinNode; 190 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length; 191 } 192 } else { 193 endNode = this; 194 endOffset = offset; 195 } 196 197 var result = this.ownerDocument.createRange(); 198 result.setStart(startNode, startOffset); 199 result.setEnd(endNode, endOffset); 200 201 return result; 202} 203 204Element.prototype.removeStyleClass = function(className) 205{ 206 // Test for the simple case before using a RegExp. 207 if (this.className === className) { 208 this.className = ""; 209 return; 210 } 211 212 this.removeMatchingStyleClasses(className.escapeForRegExp()); 213} 214 215Element.prototype.removeMatchingStyleClasses = function(classNameRegex) 216{ 217 var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)"); 218 if (regex.test(this.className)) 219 this.className = this.className.replace(regex, " "); 220} 221 222Element.prototype.addStyleClass = function(className) 223{ 224 if (className && !this.hasStyleClass(className)) 225 this.className += (this.className.length ? " " + className : className); 226} 227 228Element.prototype.hasStyleClass = function(className) 229{ 230 if (!className) 231 return false; 232 // Test for the simple case before using a RegExp. 233 if (this.className === className) 234 return true; 235 var regex = new RegExp("(^|\\s)" + className.escapeForRegExp() + "($|\\s)"); 236 return regex.test(this.className); 237} 238 239Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) 240{ 241 for (var node = this; node && !objectsAreSame(node, this.ownerDocument); node = node.parentNode) 242 for (var i = 0; i < nameArray.length; ++i) 243 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) 244 return node; 245 return null; 246} 247 248Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 249{ 250 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 251} 252 253Node.prototype.enclosingNodeOrSelfWithClass = function(className) 254{ 255 for (var node = this; node && !objectsAreSame(node, this.ownerDocument); node = node.parentNode) 256 if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className)) 257 return node; 258 return null; 259} 260 261Node.prototype.enclosingNodeWithClass = function(className) 262{ 263 if (!this.parentNode) 264 return null; 265 return this.parentNode.enclosingNodeOrSelfWithClass(className); 266} 267 268Element.prototype.query = function(query) 269{ 270 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 271} 272 273Element.prototype.removeChildren = function() 274{ 275 while (this.firstChild) 276 this.removeChild(this.firstChild); 277} 278 279Element.prototype.isInsertionCaretInside = function() 280{ 281 var selection = window.getSelection(); 282 if (!selection.rangeCount || !selection.isCollapsed) 283 return false; 284 var selectionRange = selection.getRangeAt(0); 285 return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this); 286} 287 288Element.prototype.__defineGetter__("totalOffsetLeft", function() 289{ 290 var total = 0; 291 for (var element = this; element; element = element.offsetParent) 292 total += element.offsetLeft; 293 return total; 294}); 295 296Element.prototype.__defineGetter__("totalOffsetTop", function() 297{ 298 var total = 0; 299 for (var element = this; element; element = element.offsetParent) 300 total += element.offsetTop; 301 return total; 302}); 303 304Element.prototype.firstChildSkippingWhitespace = firstChildSkippingWhitespace; 305Element.prototype.lastChildSkippingWhitespace = lastChildSkippingWhitespace; 306 307Node.prototype.isWhitespace = isNodeWhitespace; 308Node.prototype.nodeTypeName = nodeTypeName; 309Node.prototype.displayName = nodeDisplayName; 310Node.prototype.contentPreview = nodeContentPreview; 311Node.prototype.isAncestor = isAncestorNode; 312Node.prototype.isDescendant = isDescendantNode; 313Node.prototype.firstCommonAncestor = firstCommonNodeAncestor; 314Node.prototype.nextSiblingSkippingWhitespace = nextSiblingSkippingWhitespace; 315Node.prototype.previousSiblingSkippingWhitespace = previousSiblingSkippingWhitespace; 316Node.prototype.traverseNextNode = traverseNextNode; 317Node.prototype.traversePreviousNode = traversePreviousNode; 318Node.prototype.onlyTextChild = onlyTextChild; 319 320String.prototype.hasSubstring = function(string, caseInsensitive) 321{ 322 if (!caseInsensitive) 323 return this.indexOf(string) !== -1; 324 return this.match(new RegExp(string.escapeForRegExp(), "i")); 325} 326 327String.prototype.escapeCharacters = function(chars) 328{ 329 var foundChar = false; 330 for (var i = 0; i < chars.length; ++i) { 331 if (this.indexOf(chars.charAt(i)) !== -1) { 332 foundChar = true; 333 break; 334 } 335 } 336 337 if (!foundChar) 338 return this; 339 340 var result = ""; 341 for (var i = 0; i < this.length; ++i) { 342 if (chars.indexOf(this.charAt(i)) !== -1) 343 result += "\\"; 344 result += this.charAt(i); 345 } 346 347 return result; 348} 349 350String.prototype.escapeForRegExp = function() 351{ 352 return this.escapeCharacters("^[]{}()\\.$*+?|"); 353} 354 355String.prototype.escapeHTML = function() 356{ 357 return this.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 358} 359 360String.prototype.collapseWhitespace = function() 361{ 362 return this.replace(/[\s\xA0]+/g, " "); 363} 364 365String.prototype.trimLeadingWhitespace = function() 366{ 367 return this.replace(/^[\s\xA0]+/g, ""); 368} 369 370String.prototype.trimTrailingWhitespace = function() 371{ 372 return this.replace(/[\s\xA0]+$/g, ""); 373} 374 375String.prototype.trimWhitespace = function() 376{ 377 return this.replace(/^[\s\xA0]+|[\s\xA0]+$/g, ""); 378} 379 380String.prototype.trimURL = function(baseURLDomain) 381{ 382 var result = this.replace(new RegExp("^http[s]?:\/\/", "i"), ""); 383 if (baseURLDomain) 384 result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), ""); 385 return result; 386} 387 388function isNodeWhitespace() 389{ 390 if (!this || this.nodeType !== Node.TEXT_NODE) 391 return false; 392 if (!this.nodeValue.length) 393 return true; 394 return this.nodeValue.match(/^[\s\xA0]+$/); 395} 396 397function nodeTypeName() 398{ 399 if (!this) 400 return "(unknown)"; 401 402 switch (this.nodeType) { 403 case Node.ELEMENT_NODE: return "Element"; 404 case Node.ATTRIBUTE_NODE: return "Attribute"; 405 case Node.TEXT_NODE: return "Text"; 406 case Node.CDATA_SECTION_NODE: return "Character Data"; 407 case Node.ENTITY_REFERENCE_NODE: return "Entity Reference"; 408 case Node.ENTITY_NODE: return "Entity"; 409 case Node.PROCESSING_INSTRUCTION_NODE: return "Processing Instruction"; 410 case Node.COMMENT_NODE: return "Comment"; 411 case Node.DOCUMENT_NODE: return "Document"; 412 case Node.DOCUMENT_TYPE_NODE: return "Document Type"; 413 case Node.DOCUMENT_FRAGMENT_NODE: return "Document Fragment"; 414 case Node.NOTATION_NODE: return "Notation"; 415 } 416 417 return "(unknown)"; 418} 419 420function nodeDisplayName() 421{ 422 if (!this) 423 return ""; 424 425 switch (this.nodeType) { 426 case Node.DOCUMENT_NODE: 427 return "Document"; 428 429 case Node.ELEMENT_NODE: 430 var name = "<" + this.nodeName.toLowerCase(); 431 432 if (this.hasAttributes()) { 433 var value = this.getAttribute("id"); 434 if (value) 435 name += " id=\"" + value + "\""; 436 value = this.getAttribute("class"); 437 if (value) 438 name += " class=\"" + value + "\""; 439 if (this.nodeName.toLowerCase() === "a") { 440 value = this.getAttribute("name"); 441 if (value) 442 name += " name=\"" + value + "\""; 443 value = this.getAttribute("href"); 444 if (value) 445 name += " href=\"" + value + "\""; 446 } else if (this.nodeName.toLowerCase() === "img") { 447 value = this.getAttribute("src"); 448 if (value) 449 name += " src=\"" + value + "\""; 450 } else if (this.nodeName.toLowerCase() === "iframe") { 451 value = this.getAttribute("src"); 452 if (value) 453 name += " src=\"" + value + "\""; 454 } else if (this.nodeName.toLowerCase() === "input") { 455 value = this.getAttribute("name"); 456 if (value) 457 name += " name=\"" + value + "\""; 458 value = this.getAttribute("type"); 459 if (value) 460 name += " type=\"" + value + "\""; 461 } else if (this.nodeName.toLowerCase() === "form") { 462 value = this.getAttribute("action"); 463 if (value) 464 name += " action=\"" + value + "\""; 465 } 466 } 467 468 return name + ">"; 469 470 case Node.TEXT_NODE: 471 if (isNodeWhitespace.call(this)) 472 return "(whitespace)"; 473 return "\"" + this.nodeValue + "\""; 474 475 case Node.COMMENT_NODE: 476 return "<!--" + this.nodeValue + "-->"; 477 478 case Node.DOCUMENT_TYPE_NODE: 479 var docType = "<!DOCTYPE " + this.nodeName; 480 if (this.publicId) { 481 docType += " PUBLIC \"" + this.publicId + "\""; 482 if (this.systemId) 483 docType += " \"" + this.systemId + "\""; 484 } else if (this.systemId) 485 docType += " SYSTEM \"" + this.systemId + "\""; 486 if (this.internalSubset) 487 docType += " [" + this.internalSubset + "]"; 488 return docType + ">"; 489 } 490 491 return this.nodeName.toLowerCase().collapseWhitespace(); 492} 493 494function nodeContentPreview() 495{ 496 if (!this || !this.hasChildNodes || !this.hasChildNodes()) 497 return ""; 498 499 var limit = 0; 500 var preview = ""; 501 502 // always skip whitespace here 503 var currentNode = traverseNextNode.call(this, true, this); 504 while (currentNode) { 505 if (currentNode.nodeType === Node.TEXT_NODE) 506 preview += currentNode.nodeValue.escapeHTML(); 507 else 508 preview += nodeDisplayName.call(currentNode).escapeHTML(); 509 510 currentNode = traverseNextNode.call(currentNode, true, this); 511 512 if (++limit > 4) { 513 preview += "…"; // ellipsis 514 break; 515 } 516 } 517 518 return preview.collapseWhitespace(); 519} 520 521function objectsAreSame(a, b) 522{ 523 // FIXME: Make this more generic so is works with any wrapped object, not just nodes. 524 // This function is used to compare nodes that might be JSInspectedObjectWrappers, since 525 // JavaScript equality is not true for JSInspectedObjectWrappers of the same node wrapped 526 // with different global ExecStates, we use isSameNode to compare them. 527 if (a === b) 528 return true; 529 if (!a || !b) 530 return false; 531 if (a.isSameNode && b.isSameNode) 532 return a.isSameNode(b); 533 return false; 534} 535 536function isAncestorNode(ancestor) 537{ 538 if (!this || !ancestor) 539 return false; 540 541 var currentNode = ancestor.parentNode; 542 while (currentNode) { 543 if (objectsAreSame(this, currentNode)) 544 return true; 545 currentNode = currentNode.parentNode; 546 } 547 548 return false; 549} 550 551function isDescendantNode(descendant) 552{ 553 return isAncestorNode.call(descendant, this); 554} 555 556function firstCommonNodeAncestor(node) 557{ 558 if (!this || !node) 559 return; 560 561 var node1 = this.parentNode; 562 var node2 = node.parentNode; 563 564 if ((!node1 || !node2) || !objectsAreSame(node1, node2)) 565 return null; 566 567 while (node1 && node2) { 568 if (!node1.parentNode || !node2.parentNode) 569 break; 570 if (!objectsAreSame(node1, node2)) 571 break; 572 573 node1 = node1.parentNode; 574 node2 = node2.parentNode; 575 } 576 577 return node1; 578} 579 580function nextSiblingSkippingWhitespace() 581{ 582 if (!this) 583 return; 584 var node = this.nextSibling; 585 while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node)) 586 node = node.nextSibling; 587 return node; 588} 589 590function previousSiblingSkippingWhitespace() 591{ 592 if (!this) 593 return; 594 var node = this.previousSibling; 595 while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node)) 596 node = node.previousSibling; 597 return node; 598} 599 600function firstChildSkippingWhitespace() 601{ 602 if (!this) 603 return; 604 var node = this.firstChild; 605 while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node)) 606 node = nextSiblingSkippingWhitespace.call(node); 607 return node; 608} 609 610function lastChildSkippingWhitespace() 611{ 612 if (!this) 613 return; 614 var node = this.lastChild; 615 while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node)) 616 node = previousSiblingSkippingWhitespace.call(node); 617 return node; 618} 619 620function traverseNextNode(skipWhitespace, stayWithin) 621{ 622 if (!this) 623 return; 624 625 var node = skipWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild; 626 if (node) 627 return node; 628 629 if (stayWithin && objectsAreSame(this, stayWithin)) 630 return null; 631 632 node = skipWhitespace ? nextSiblingSkippingWhitespace.call(this) : this.nextSibling; 633 if (node) 634 return node; 635 636 node = this; 637 while (node && !(skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling) && (!stayWithin || !node.parentNode || !objectsAreSame(node.parentNode, stayWithin))) 638 node = node.parentNode; 639 if (!node) 640 return null; 641 642 return skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling; 643} 644 645function traversePreviousNode(skipWhitespace, stayWithin) 646{ 647 if (!this) 648 return; 649 if (stayWithin && objectsAreSame(this, stayWithin)) 650 return null; 651 var node = skipWhitespace ? previousSiblingSkippingWhitespace.call(this) : this.previousSibling; 652 while (node && (skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild) ) 653 node = skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild; 654 if (node) 655 return node; 656 return this.parentNode; 657} 658 659function onlyTextChild(ignoreWhitespace) 660{ 661 if (!this) 662 return null; 663 664 var firstChild = ignoreWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild; 665 if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE) 666 return null; 667 668 var sibling = ignoreWhitespace ? nextSiblingSkippingWhitespace.call(firstChild) : firstChild.nextSibling; 669 return sibling ? null : firstChild; 670} 671 672function nodeTitleInfo(hasChildren, linkify) 673{ 674 var info = {title: "", hasChildren: hasChildren}; 675 676 switch (this.nodeType) { 677 case Node.DOCUMENT_NODE: 678 info.title = "Document"; 679 break; 680 681 case Node.ELEMENT_NODE: 682 info.title = "<span class=\"webkit-html-tag\"><" + this.nodeName.toLowerCase().escapeHTML(); 683 684 if (this.hasAttributes()) { 685 for (var i = 0; i < this.attributes.length; ++i) { 686 var attr = this.attributes[i]; 687 info.title += " <span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=​\""; 688 689 var value = attr.value; 690 if (linkify && (attr.name === "src" || attr.name === "href")) { 691 var value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B"); 692 info.title += linkify(attr.value, value, "webkit-html-attribute-value", this.nodeName.toLowerCase() == "a"); 693 } else { 694 var value = value.escapeHTML(); 695 value = value.replace(/([\/;:\)\]\}])/g, "$1​"); 696 info.title += "<span class=\"webkit-html-attribute-value\">" + value + "</span>"; 697 } 698 info.title += "\"</span>"; 699 } 700 } 701 info.title += "></span>​"; 702 703 // If this element only has a single child that is a text node, 704 // just show that text and the closing tag inline rather than 705 // create a subtree for them 706 707 var textChild = onlyTextChild.call(this, Preferences.ignoreWhitespace); 708 var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength; 709 710 if (showInlineText) { 711 info.title += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue.escapeHTML() + "</span>​<span class=\"webkit-html-tag\"></" + this.nodeName.toLowerCase().escapeHTML() + "></span>"; 712 info.hasChildren = false; 713 } 714 break; 715 716 case Node.TEXT_NODE: 717 if (isNodeWhitespace.call(this)) 718 info.title = "(whitespace)"; 719 else 720 info.title = "\"<span class=\"webkit-html-text-node\">" + this.nodeValue.escapeHTML() + "</span>\""; 721 break 722 723 case Node.COMMENT_NODE: 724 info.title = "<span class=\"webkit-html-comment\"><!--" + this.nodeValue.escapeHTML() + "--></span>"; 725 break; 726 727 case Node.DOCUMENT_TYPE_NODE: 728 info.title = "<span class=\"webkit-html-doctype\"><!DOCTYPE " + this.nodeName; 729 if (this.publicId) { 730 info.title += " PUBLIC \"" + this.publicId + "\""; 731 if (this.systemId) 732 info.title += " \"" + this.systemId + "\""; 733 } else if (this.systemId) 734 info.title += " SYSTEM \"" + this.systemId + "\""; 735 if (this.internalSubset) 736 info.title += " [" + this.internalSubset + "]"; 737 info.title += "></span>"; 738 break; 739 default: 740 info.title = this.nodeName.toLowerCase().collapseWhitespace().escapeHTML(); 741 } 742 743 return info; 744} 745 746function getDocumentForNode(node) { 747 return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument; 748} 749 750function parentNodeOrFrameElement(node) { 751 var parent = node.parentNode; 752 if (parent) 753 return parent; 754 755 return getDocumentForNode(node).defaultView.frameElement; 756} 757 758function isAncestorIncludingParentFrames(a, b) { 759 if (objectsAreSame(a, b)) 760 return false; 761 for (var node = b; node; node = getDocumentForNode(node).defaultView.frameElement) 762 if (objectsAreSame(a, node) || isAncestorNode.call(a, node)) 763 return true; 764 return false; 765} 766 767Number.secondsToString = function(seconds, formatterFunction, higherResolution) 768{ 769 if (!formatterFunction) 770 formatterFunction = String.sprintf; 771 772 var ms = seconds * 1000; 773 if (higherResolution && ms < 1000) 774 return formatterFunction("%.3fms", ms); 775 else if (ms < 1000) 776 return formatterFunction("%.0fms", ms); 777 778 if (seconds < 60) 779 return formatterFunction("%.2fs", seconds); 780 781 var minutes = seconds / 60; 782 if (minutes < 60) 783 return formatterFunction("%.1fmin", minutes); 784 785 var hours = minutes / 60; 786 if (hours < 24) 787 return formatterFunction("%.1fhrs", hours); 788 789 var days = hours / 24; 790 return formatterFunction("%.1f days", days); 791} 792 793Number.bytesToString = function(bytes, formatterFunction) 794{ 795 if (!formatterFunction) 796 formatterFunction = String.sprintf; 797 798 if (bytes < 1024) 799 return formatterFunction("%.0fB", bytes); 800 801 var kilobytes = bytes / 1024; 802 if (kilobytes < 1024) 803 return formatterFunction("%.2fKB", kilobytes); 804 805 var megabytes = kilobytes / 1024; 806 return formatterFunction("%.3fMB", megabytes); 807} 808 809Number.constrain = function(num, min, max) 810{ 811 if (num < min) 812 num = min; 813 else if (num > max) 814 num = max; 815 return num; 816} 817 818HTMLTextAreaElement.prototype.moveCursorToEnd = function() 819{ 820 var length = this.value.length; 821 this.setSelectionRange(length, length); 822} 823 824Array.prototype.remove = function(value, onlyFirst) 825{ 826 if (onlyFirst) { 827 var index = this.indexOf(value); 828 if (index !== -1) 829 this.splice(index, 1); 830 return; 831 } 832 833 var length = this.length; 834 for (var i = 0; i < length; ++i) { 835 if (this[i] === value) 836 this.splice(i, 1); 837 } 838} 839 840function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction) 841{ 842 // indexOf returns (-lowerBound - 1). Taking (-result - 1) works out to lowerBound. 843 return (-indexOfObjectInListSortedByFunction(anObject, aList, aFunction) - 1); 844} 845 846function indexOfObjectInListSortedByFunction(anObject, aList, aFunction) 847{ 848 var first = 0; 849 var last = aList.length - 1; 850 var floor = Math.floor; 851 var mid, c; 852 853 while (first <= last) { 854 mid = floor((first + last) / 2); 855 c = aFunction(anObject, aList[mid]); 856 857 if (c > 0) 858 first = mid + 1; 859 else if (c < 0) 860 last = mid - 1; 861 else { 862 // Return the first occurance of an item in the list. 863 while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0) 864 mid--; 865 first = mid; 866 break; 867 } 868 } 869 870 // By returning 1 less than the negative lower search bound, we can reuse this function 871 // for both indexOf and insertionIndexFor, with some simple arithmetic. 872 return (-first - 1); 873} 874 875String.sprintf = function(format) 876{ 877 return String.vsprintf(format, Array.prototype.slice.call(arguments, 1)); 878} 879 880String.tokenizeFormatString = function(format) 881{ 882 var tokens = []; 883 var substitutionIndex = 0; 884 885 function addStringToken(str) 886 { 887 tokens.push({ type: "string", value: str }); 888 } 889 890 function addSpecifierToken(specifier, precision, substitutionIndex) 891 { 892 tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex }); 893 } 894 895 var index = 0; 896 for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) { 897 addStringToken(format.substring(index, precentIndex)); 898 index = precentIndex + 1; 899 900 if (format[index] === "%") { 901 addStringToken("%"); 902 ++index; 903 continue; 904 } 905 906 if (!isNaN(format[index])) { 907 // The first character is a number, it might be a substitution index. 908 var number = parseInt(format.substring(index)); 909 while (!isNaN(format[index])) 910 ++index; 911 // If the number is greater than zero and ends with a "$", 912 // then this is a substitution index. 913 if (number > 0 && format[index] === "$") { 914 substitutionIndex = (number - 1); 915 ++index; 916 } 917 } 918 919 var precision = -1; 920 if (format[index] === ".") { 921 // This is a precision specifier. If no digit follows the ".", 922 // then the precision should be zero. 923 ++index; 924 precision = parseInt(format.substring(index)); 925 if (isNaN(precision)) 926 precision = 0; 927 while (!isNaN(format[index])) 928 ++index; 929 } 930 931 addSpecifierToken(format[index], precision, substitutionIndex); 932 933 ++substitutionIndex; 934 ++index; 935 } 936 937 addStringToken(format.substring(index)); 938 939 return tokens; 940} 941 942String.standardFormatters = { 943 d: function(substitution) 944 { 945 substitution = parseInt(substitution); 946 return !isNaN(substitution) ? substitution : 0; 947 }, 948 949 f: function(substitution, token) 950 { 951 substitution = parseFloat(substitution); 952 if (substitution && token.precision > -1) 953 substitution = substitution.toFixed(token.precision); 954 return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0); 955 }, 956 957 s: function(substitution) 958 { 959 return substitution; 960 }, 961}; 962 963String.vsprintf = function(format, substitutions) 964{ 965 return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult; 966} 967 968String.format = function(format, substitutions, formatters, initialValue, append) 969{ 970 if (!format || !substitutions || !substitutions.length) 971 return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions }; 972 973 function prettyFunctionName() 974 { 975 return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")"; 976 } 977 978 function warn(msg) 979 { 980 console.warn(prettyFunctionName() + ": " + msg); 981 } 982 983 function error(msg) 984 { 985 console.error(prettyFunctionName() + ": " + msg); 986 } 987 988 var result = initialValue; 989 var tokens = String.tokenizeFormatString(format); 990 var usedSubstitutionIndexes = {}; 991 992 for (var i = 0; i < tokens.length; ++i) { 993 var token = tokens[i]; 994 995 if (token.type === "string") { 996 result = append(result, token.value); 997 continue; 998 } 999 1000 if (token.type !== "specifier") { 1001 error("Unknown token type \"" + token.type + "\" found."); 1002 continue; 1003 } 1004 1005 if (token.substitutionIndex >= substitutions.length) { 1006 // If there are not enough substitutions for the current substitutionIndex 1007 // just output the format specifier literally and move on. 1008 error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped."); 1009 result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier); 1010 continue; 1011 } 1012 1013 usedSubstitutionIndexes[token.substitutionIndex] = true; 1014 1015 if (!(token.specifier in formatters)) { 1016 // Encountered an unsupported format character, treat as a string. 1017 warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string."); 1018 result = append(result, substitutions[token.substitutionIndex]); 1019 continue; 1020 } 1021 1022 result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token)); 1023 } 1024 1025 var unusedSubstitutions = []; 1026 for (var i = 0; i < substitutions.length; ++i) { 1027 if (i in usedSubstitutionIndexes) 1028 continue; 1029 unusedSubstitutions.push(substitutions[i]); 1030 } 1031 1032 return { formattedResult: result, unusedSubstitutions: unusedSubstitutions }; 1033} 1034