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