1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/lib/events", 6 "firebug/lib/css", 7 "firebug/lib/dom", 8 "firebug/lib/search", 9 "firebug/lib/xml", 10 "firebug/lib/string", 11 ], 12 function(Obj, Events, Css, Dom, Search, Xml, Str) { 13 14 // ********************************************************************************************* // 15 // Constants 16 17 const Ci = Components.interfaces; 18 const SHOW_ALL = Ci.nsIDOMNodeFilter.SHOW_ALL; 19 20 // ********************************************************************************************* // 21 22 /** 23 * @class Static utility class. Contains utilities used for displaying and 24 * searching a HTML tree. 25 */ 26 var HTMLLib = 27 { 28 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 29 // Node Search Utilities 30 31 /** 32 * Constructs a NodeSearch instance. 33 * 34 * @class Class used to search a DOM tree for the given text. Will display 35 * the search results in a IO Box. 36 * 37 * @constructor 38 * @param {String} text Text to search for 39 * @param {Object} root Root of search. This may be an element or a document 40 * @param {Object} panelNode Panel node containing the IO Box representing the DOM tree. 41 * @param {Object} ioBox IO Box to display the search results in 42 * @param {Object} walker Optional walker parameter. 43 */ 44 NodeSearch: function(text, root, panelNode, ioBox, walker) 45 { 46 root = root.documentElement || root; 47 walker = walker || new HTMLLib.DOMWalker(root); 48 var re = new Search.ReversibleRegExp(text, "m"); 49 var matchCount = 0; 50 51 /** 52 * Finds the first match within the document. 53 * 54 * @param {boolean} revert true to search backward, false to search forward 55 * @param {boolean} caseSensitive true to match exact case, false to ignore case 56 * @return true if no more matches were found, but matches were found previously. 57 */ 58 this.find = function(reverse, caseSensitive) 59 { 60 var match = this.findNextMatch(reverse, caseSensitive); 61 if (match) 62 { 63 this.lastMatch = match; 64 ++matchCount; 65 66 var node = match.node; 67 var nodeBox = this.openToNode(node, match.isValue); 68 69 this.selectMatched(nodeBox, node, match, reverse); 70 } 71 else if (matchCount) 72 { 73 return true; 74 } 75 else 76 { 77 this.noMatch = true; 78 Events.dispatch([Firebug.A11yModel], "onHTMLSearchNoMatchFound", 79 [panelNode.ownerPanel, text]); 80 } 81 }; 82 83 /** 84 * Resets the search to the beginning of the document. 85 */ 86 this.reset = function() 87 { 88 delete this.lastMatch; 89 }; 90 91 /** 92 * Finds the next match in the document. 93 * 94 * The return value is an object with the fields 95 * - node: Node that contains the match 96 * - isValue: true if the match is a match due to the value of the node, false if it is due to the name 97 * - match: Regular expression result from the match 98 * 99 * @param {boolean} revert true to search backward, false to search forward 100 * @param {boolean} caseSensitive true to match exact case, false to ignore case 101 * @return Match object if found 102 */ 103 this.findNextMatch = function(reverse, caseSensitive) 104 { 105 var innerMatch = this.findNextInnerMatch(reverse, caseSensitive); 106 if (innerMatch) 107 return innerMatch; 108 else 109 this.reset(); 110 111 function walkNode() { return reverse ? walker.previousNode() : walker.nextNode(); } 112 113 var node; 114 while (node = walkNode()) 115 { 116 if (node.nodeType == Node.TEXT_NODE && HTMLLib.isSourceElement(node.parentNode)) 117 continue; 118 119 var m = this.checkNode(node, reverse, caseSensitive); 120 if (m) 121 return m; 122 } 123 }; 124 125 /** 126 * Helper util used to scan the current search result for more results 127 * in the same object. 128 * 129 * @private 130 */ 131 this.findNextInnerMatch = function(reverse, caseSensitive) 132 { 133 if (this.lastMatch) 134 { 135 var lastMatchNode = this.lastMatch.node; 136 var lastReMatch = this.lastMatch.match; 137 var m = re.exec(lastReMatch.input, reverse, lastReMatch.caseSensitive, lastReMatch); 138 if (m) 139 { 140 return { 141 node: lastMatchNode, 142 isValue: this.lastMatch.isValue, 143 match: m 144 }; 145 } 146 147 // May need to check the pair for attributes 148 if (lastMatchNode.nodeType == Node.ATTRIBUTE_NODE && 149 this.lastMatch.isValue == !!reverse) 150 { 151 return this.checkNode(lastMatchNode, reverse, caseSensitive, 1); 152 } 153 } 154 }; 155 156 /** 157 * Checks a given node for a search match. 158 * 159 * @private 160 */ 161 this.checkNode = function(node, reverse, caseSensitive, firstStep) 162 { 163 var checkOrder; 164 if (node.nodeType != Node.TEXT_NODE) 165 { 166 var nameCheck = { name: "nodeName", isValue: false, caseSensitive: caseSensitive }; 167 var valueCheck = { name: "nodeValue", isValue: true, caseSensitive: caseSensitive }; 168 checkOrder = reverse ? [ valueCheck, nameCheck ] : [ nameCheck, valueCheck ]; 169 } 170 else 171 { 172 checkOrder = [{name: "nodeValue", isValue: false, caseSensitive: caseSensitive }]; 173 } 174 175 for (var i = firstStep || 0; i < checkOrder.length; i++) 176 { 177 var m = re.exec(node[checkOrder[i].name], reverse, checkOrder[i].caseSensitive); 178 if (m) { 179 return { 180 node: node, 181 isValue: checkOrder[i].isValue, 182 match: m 183 }; 184 } 185 } 186 }; 187 188 /** 189 * Opens the given node in the associated IO Box. 190 * 191 * @private 192 */ 193 this.openToNode = function(node, isValue) 194 { 195 if (node.nodeType == Node.ELEMENT_NODE) 196 { 197 var nodeBox = ioBox.openToObject(node); 198 return nodeBox.getElementsByClassName("nodeTag")[0]; 199 } 200 else if (node.nodeType == Node.ATTRIBUTE_NODE) 201 { 202 var nodeBox = ioBox.openToObject(node.ownerElement); 203 if (nodeBox) 204 { 205 var attrNodeBox = HTMLLib.findNodeAttrBox(nodeBox, node.name); 206 return Dom.getChildByClass(attrNodeBox, isValue ? "nodeValue" : "nodeName"); 207 } 208 } 209 else if (node.nodeType == Node.TEXT_NODE) 210 { 211 var nodeBox = ioBox.openToObject(node); 212 if (nodeBox) 213 { 214 return nodeBox; 215 } 216 else 217 { 218 var nodeBox = ioBox.openToObject(node.parentNode); 219 if (Css.hasClass(nodeBox, "textNodeBox")) 220 nodeBox = HTMLLib.getTextElementTextBox(nodeBox); 221 return nodeBox; 222 } 223 } 224 }; 225 226 /** 227 * Selects the search results. 228 * 229 * @private 230 */ 231 this.selectMatched = function(nodeBox, node, match, reverse) 232 { 233 setTimeout(Obj.bindFixed(function() 234 { 235 var reMatch = match.match; 236 this.selectNodeText(nodeBox, node, reMatch[0], reMatch.index, reverse, 237 reMatch.caseSensitive); 238 239 Events.dispatch([Firebug.A11yModel], "onHTMLSearchMatchFound", 240 [panelNode.ownerPanel, match]); 241 }, this)); 242 }; 243 244 /** 245 * Select text node search results. 246 * 247 * @private 248 */ 249 this.selectNodeText = function(nodeBox, node, text, index, reverse, caseSensitive) 250 { 251 var row; 252 253 // If we are still inside the same node as the last search, advance the range 254 // to the next substring within that node 255 if (nodeBox == this.lastNodeBox) 256 { 257 row = this.textSearch.findNext(false, true, reverse, caseSensitive); 258 } 259 260 if (!row) 261 { 262 // Search for the first instance of the string inside the node 263 function findRow(node) 264 { 265 return node.nodeType == Node.ELEMENT_NODE ? node : node.parentNode; 266 } 267 268 this.textSearch = new Search.TextSearch(nodeBox, findRow); 269 row = this.textSearch.find(text, reverse, caseSensitive); 270 this.lastNodeBox = nodeBox; 271 } 272 273 if (row) 274 { 275 var trueNodeBox = Dom.getAncestorByClass(nodeBox, "nodeBox"); 276 Css.setClass(trueNodeBox, "search-selection"); 277 278 Dom.scrollIntoCenterView(row, panelNode); 279 var sel = panelNode.ownerDocument.defaultView.getSelection(); 280 sel.removeAllRanges(); 281 sel.addRange(this.textSearch.range); 282 283 Css.removeClass(trueNodeBox, "search-selection"); 284 return true; 285 } 286 }; 287 }, 288 289 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 290 291 /** 292 * XXXjjb this code is no longer called and won't be in 1.5; if FireFinder works out we can delete this. 293 * Constructs a SelectorSearch instance. 294 * 295 * @class Class used to search a DOM tree for elements matching the given 296 * CSS selector. 297 * 298 * @constructor 299 * @param {String} text CSS selector to search for 300 * @param {Document} doc Document to search 301 * @param {Object} panelNode Panel node containing the IO Box representing the DOM tree. 302 * @param {Object} ioBox IO Box to display the search results in 303 */ 304 SelectorSearch: function(text, doc, panelNode, ioBox) 305 { 306 this.parent = new HTMLLib.NodeSearch(text, doc, panelNode, ioBox); 307 308 /** 309 * Finds the first match within the document. 310 * 311 * @param {boolean} revert true to search backward, false to search forward 312 * @param {boolean} caseSensitive true to match exact case, false to ignore case 313 * @return true if no more matches were found, but matches were found previously. 314 */ 315 this.find = this.parent.find; 316 317 /** 318 * Resets the search to the beginning of the document. 319 */ 320 this.reset = this.parent.reset; 321 322 /** 323 * Opens the given node in the associated IO Box. 324 * 325 * @private 326 */ 327 this.openToNode = this.parent.openToNode; 328 329 try 330 { 331 // http://dev.w3.org/2006/webapi/selectors-api/ 332 this.matchingNodes = doc.querySelectorAll(text); 333 this.matchIndex = 0; 334 } 335 catch(exc) 336 { 337 FBTrace.sysout("SelectorSearch FAILS "+exc, exc); 338 } 339 340 /** 341 * Finds the next match in the document. 342 * 343 * The return value is an object with the fields 344 * - node: Node that contains the match 345 * - isValue: true if the match is a match due to the value of the node, false if it is due to the name 346 * - match: Regular expression result from the match 347 * 348 * @param {boolean} revert true to search backward, false to search forward 349 * @param {boolean} caseSensitive true to match exact case, false to ignore case 350 * @return Match object if found 351 */ 352 this.findNextMatch = function(reverse, caseSensitive) 353 { 354 if (!this.matchingNodes || !this.matchingNodes.length) 355 return undefined; 356 357 if (reverse) 358 { 359 if (this.matchIndex > 0) 360 return { node: this.matchingNodes[this.matchIndex--], isValue: false, match: "?XX?"}; 361 else 362 return undefined; 363 } 364 else 365 { 366 if (this.matchIndex < this.matchingNodes.length) 367 return { node: this.matchingNodes[this.matchIndex++], isValue: false, match: "?XX?"}; 368 else 369 return undefined; 370 } 371 }; 372 373 /** 374 * Selects the search results. 375 * 376 * @private 377 */ 378 this.selectMatched = function(nodeBox, node, match, reverse) 379 { 380 setTimeout(Obj.bindFixed(function() 381 { 382 ioBox.select(node, true, true); 383 Events.dispatch([Firebug.A11yModel], "onHTMLSearchMatchFound", [panelNode.ownerPanel, match]); 384 }, this)); 385 }; 386 }, 387 388 389 /** 390 * Constructs a DOMWalker instance. 391 * 392 * @constructor 393 * @class Implements an ordered traveral of the document, including attributes and 394 * iframe contents within the results. 395 * 396 * Note that the order for attributes is not defined. This will follow the 397 * same order as the Element.attributes accessor. 398 * @param {Element} root Element to traverse 399 */ 400 DOMWalker: function(root) 401 { 402 var walker; 403 var currentNode, attrIndex; 404 var pastStart, pastEnd; 405 var doc = root.ownerDocument; 406 407 function createWalker(docElement) 408 { 409 var walk = doc.createTreeWalker(docElement, SHOW_ALL, null, true); 410 walker.unshift(walk); 411 } 412 413 function getLastAncestor() 414 { 415 while (walker[0].lastChild()) {} 416 return walker[0].currentNode; 417 } 418 419 /** 420 * Move to the previous node. 421 * 422 * @return The previous node if one exists, undefined otherwise. 423 */ 424 this.previousNode = function() 425 { 426 if (pastStart) 427 return undefined; 428 429 if (attrIndex) 430 { 431 attrIndex--; 432 } 433 else 434 { 435 var prevNode; 436 if (currentNode == walker[0].root) 437 { 438 if (walker.length > 1) 439 { 440 walker.shift(); 441 prevNode = walker[0].currentNode; 442 } 443 else 444 { 445 prevNode = undefined; 446 } 447 } 448 else 449 { 450 prevNode = !currentNode ? getLastAncestor(): walker[0].previousNode(); 451 452 // Really shouldn't occur, but to be safe 453 if (!prevNode) 454 prevNode = walker[0].root; 455 456 while ((prevNode.nodeName || "").toUpperCase() == "IFRAME") 457 { 458 createWalker(prevNode.contentDocument.documentElement); 459 prevNode = getLastAncestor(); 460 } 461 } 462 currentNode = prevNode; 463 attrIndex = ((prevNode || {}).attributes || []).length; 464 } 465 466 if (!currentNode) 467 pastStart = true; 468 else 469 pastEnd = false; 470 471 return this.currentNode(); 472 }; 473 474 /** 475 * Move to the next node. 476 * 477 * @return The next node if one exists, otherwise undefined. 478 */ 479 this.nextNode = function() 480 { 481 if (pastEnd) 482 return undefined; 483 484 if (!currentNode) 485 { 486 // We are working with a new tree walker 487 currentNode = walker[0].root; 488 attrIndex = 0; 489 } 490 else 491 { 492 // First check attributes 493 var attrs = currentNode.attributes || []; 494 if (attrIndex < attrs.length) 495 { 496 attrIndex++; 497 } 498 else if ((currentNode.nodeName || "").toUpperCase() == "IFRAME") 499 { 500 // Attributes have completed, check for iframe contents 501 createWalker(currentNode.contentDocument.documentElement); 502 currentNode = walker[0].root; 503 attrIndex = 0; 504 } 505 else 506 { 507 // Next node 508 var nextNode = walker[0].nextNode(); 509 while (!nextNode && walker.length > 1) 510 { 511 walker.shift(); 512 nextNode = walker[0].nextNode(); 513 } 514 currentNode = nextNode; 515 attrIndex = 0; 516 } 517 } 518 519 if (!currentNode) 520 pastEnd = true; 521 else 522 pastStart = false; 523 524 return this.currentNode(); 525 }; 526 527 /** 528 * Retrieves the current node. 529 * 530 * @return The current node, if not past the beginning or end of the iteration. 531 */ 532 this.currentNode = function() 533 { 534 return !attrIndex ? currentNode : currentNode.attributes[attrIndex-1]; 535 }; 536 537 /** 538 * Resets the walker position back to the initial position. 539 */ 540 this.reset = function() 541 { 542 pastStart = false; 543 pastEnd = false; 544 walker = []; 545 currentNode = undefined; 546 attrIndex = 0; 547 548 createWalker(root); 549 }; 550 551 this.reset(); 552 }, 553 554 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 555 // Node/Element Utilities 556 557 /** 558 * Determines if the given element is the source for a non-DOM resource such 559 * as Javascript source or CSS definition. 560 * 561 * @param {Element} element Element to test 562 * @return true if the element is a source element 563 */ 564 isSourceElement: function(element) 565 { 566 if (!Xml.isElementHTML(element) && !Xml.isElementXHTML(element)) 567 return false; 568 569 var tag = element.localName ? element.localName.toLowerCase() : ""; 570 return tag == "script" || (tag == "link" && element.getAttribute("rel") == "stylesheet") || 571 tag == "style"; 572 }, 573 574 /** 575 * Retrieves the source URL for any external resource associated with a node. 576 * 577 * @param {Element} element Element to examine 578 * @return URL of the external resouce. 579 */ 580 getSourceHref: function(element) 581 { 582 var tag = element.localName.toLowerCase(); 583 if (tag == "script" && element.src) 584 return element.src; 585 else if (tag == "link") 586 return element.href; 587 else 588 return null; 589 }, 590 591 /** 592 * Retrieves the source text for inline script and style elements. 593 * 594 * @param {Element} element Script or style element 595 * @return Source text 596 */ 597 getSourceText: function(element) 598 { 599 var tag = element.localName.toLowerCase(); 600 if (tag == "script" && !element.src) 601 return element.textContent; 602 else if (tag == "style") 603 return element.textContent; 604 else 605 return null; 606 }, 607 608 /** 609 * Determines if the given element is a container element. 610 * 611 * @param {Element} element Element to test 612 * @return True if the element is a container element. 613 */ 614 isContainerElement: function(element) 615 { 616 var tag = element.localName.toLowerCase(); 617 switch (tag) 618 { 619 case "script": 620 case "style": 621 case "iframe": 622 case "frame": 623 case "tabbrowser": 624 case "browser": 625 return true; 626 case "link": 627 return element.getAttribute("rel") == "stylesheet"; 628 case "embed": 629 return element.getSVGDocument(); 630 } 631 return false; 632 }, 633 634 /** 635 * Determines if the given node has any children which are elements. 636 * 637 * @param {Element} element Element to test. 638 * @return true if immediate children of type Element exist, false otherwise 639 */ 640 hasNoElementChildren: function(element) 641 { 642 if (element.childElementCount != 0) // FF 3.5+ 643 return false; 644 645 // https://developer.mozilla.org/en/XBL/XBL_1.0_Reference/DOM_Interfaces 646 if (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL) 647 { 648 if (FBTrace.DBG_HTML) 649 { 650 FBTrace.sysout("hasNoElementChildren "+Css.getElementCSSSelector(element)+ 651 " (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL) "+ 652 (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL), element); 653 } 654 655 var walker = new HTMLLib.ElementWalker(); 656 var child = walker.getFirstChild(element); 657 658 while (child) 659 { 660 if (child.nodeType === Node.ELEMENT_NODE) 661 return false; 662 child = walker.getNextSibling(child); 663 } 664 } 665 666 if (FBTrace.DBG_HTML) 667 FBTrace.sysout("hasNoElementChildren TRUE "+element.tagName+ 668 " (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL) "+ 669 (element.ownerDocument instanceof Ci.nsIDOMDocumentXBL), element); 670 671 return true; 672 }, 673 674 675 /** 676 * Determines if the given node has any children which are comments. 677 * 678 * @param {Element} element Element to test. 679 * @return true if immediate children of type Comment exist, false otherwise 680 */ 681 hasCommentChildren: function(element) 682 { 683 if (element.hasChildNodes()) 684 { 685 var children = element.childNodes; 686 for (var i = 0; i < children.length; i++) 687 { 688 if (children[i] instanceof Comment) 689 return true; 690 } 691 }; 692 return false; 693 }, 694 695 696 /** 697 * Determines if the given node consists solely of whitespace text. 698 * 699 * @param {Node} node Node to test. 700 * @return true if the node is a whitespace text node 701 */ 702 isWhitespaceText: function(node) 703 { 704 if (node instanceof window.HTMLAppletElement) 705 return false; 706 707 return node.nodeType == window.Node.TEXT_NODE && Str.isWhitespace(node.nodeValue); 708 }, 709 710 /** 711 * Determines if a given element is empty. When the 712 * {@link Firebug#showTextNodesWithWhitespace} parameter is true, an element is 713 * considered empty if it has no child elements and is self closing. When 714 * false, an element is considered empty if the only children are whitespace 715 * nodes. 716 * 717 * @param {Element} element Element to test 718 * @return true if the element is empty, false otherwise 719 */ 720 isEmptyElement: function(element) 721 { 722 // XXXjjb the commented code causes issues 48, 240, and 244. I think the lines should be deleted. 723 // If the DOM has whitespace children, then the element is not empty even if 724 // we decide not to show the whitespace in the UI. 725 726 // XXXsroussey reverted above but added a check for self closing tags 727 if (Firebug.showTextNodesWithWhitespace) 728 { 729 return !element.firstChild && Xml.isSelfClosing(element); 730 } 731 else 732 { 733 for (var child = element.firstChild; child; child = child.nextSibling) 734 { 735 if (!HTMLLib.isWhitespaceText(child)) 736 return false; 737 } 738 } 739 return Xml.isSelfClosing(element); 740 }, 741 742 /** 743 * Finds the next sibling of the given node. If the 744 * {@link Firebug#showTextNodesWithWhitespace} parameter is set to true, the next 745 * sibling may be a whitespace, otherwise the next is the first adjacent 746 * non-whitespace node. 747 * 748 * @param {Node} node Node to analyze. 749 * @return Next sibling node, if one exists 750 */ 751 findNextSibling: function(node) 752 { 753 if (Firebug.showTextNodesWithWhitespace) 754 return node.nextSibling; 755 else 756 { 757 // only return a non-whitespace node 758 for (var child = node.nextSibling; child; child = child.nextSibling) 759 { 760 if (!HTMLLib.isWhitespaceText(child)) 761 return child; 762 } 763 } 764 }, 765 766 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 767 // Domplate Utilities 768 769 /** 770 * Locates the attribute domplate node for a given element domplate. This method will 771 * only examine notes marked with the "nodeAttr" class that are the direct 772 * children of the given element. 773 * 774 * @param {Object} objectNodeBox The domplate element to look up the attribute for. 775 * @param {String} attrName Attribute name 776 * @return Attribute's domplate node 777 */ 778 findNodeAttrBox: function(objectNodeBox, attrName) 779 { 780 var child = objectNodeBox.firstChild.lastChild.firstChild; 781 for (; child; child = child.nextSibling) 782 { 783 if (Css.hasClass(child, "nodeAttr") && child.childNodes[1].firstChild 784 && child.childNodes[1].firstChild.nodeValue == attrName) 785 { 786 return child; 787 } 788 } 789 }, 790 791 /** 792 * Locates the text domplate node for a given text element domplate. 793 * @param {Object} nodeBox Text element domplate 794 * @return Element's domplate text node 795 */ 796 getTextElementTextBox: function(nodeBox) 797 { 798 var nodeLabelBox = nodeBox.firstChild.lastChild; 799 return Dom.getChildByClass(nodeLabelBox, "nodeText"); 800 }, 801 802 // These functions can be copied to add tree walking feature, they allow Chromebug 803 // to reuse the HTML panel 804 ElementWalkerFunctions: 805 { 806 getTreeWalker: function(node) 807 { 808 if (!this.treeWalker || this.treeWalker.currentNode !== node) 809 this.treeWalker = node.ownerDocument.createTreeWalker( 810 node, NodeFilter.SHOW_ALL, null, false); 811 812 return this.treeWalker; 813 }, 814 815 getFirstChild: function(node) 816 { 817 return node.firstChild; 818 }, 819 820 getNextSibling: function(node) 821 { 822 // the Mozilla XBL tree walker fails for nextSibling 823 return node.nextSibling; 824 }, 825 826 getParentNode: function(node) 827 { 828 // the Mozilla XBL tree walker fails for parentNode 829 return node.parentNode; 830 } 831 }, 832 833 ElementWalker: function() // tree walking via new ElementWalker 834 { 835 836 } 837 }; 838 839 // ********************************************************************************************* // 840 // Registration 841 842 HTMLLib.ElementWalker.prototype = HTMLLib.ElementWalkerFunctions; 843 844 return HTMLLib; 845 846 // ********************************************************************************************* // 847 });