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