1 /**
  2  * @fileoverview Node utilities.
  3  */
  4 
  5 goog.provide('xrx.xpath.Node');
  6 
  7 goog.require('goog.array');
  8 goog.require('goog.dom.NodeType');
  9 goog.require('goog.userAgent');
 10 
 11 
 12 /** @typedef {!xrx.xpath.Node} */
 13 xrx.xpath.Node = {};
 14 
 15 
 16 /**
 17  * Returns whether two nodes are equal.
 18  *
 19  * @param {xrx.xpath.Node} a The first node.
 20  * @param {xrx.xpath.Node} b The second node.
 21  * @return {boolean} Whether the nodes are equal.
 22  */
 23 xrx.xpath.Node.equal = function(a, b) {
 24   return (a == b) || (a.getNode() == b.getNode());
 25 };
 26 
 27 
 28 /**
 29  * Returns the string-value of the required type from a node.
 30  *
 31  * @param {!xrx.xpath.Node} node The node to get value from.
 32  * @return {string} The value required.
 33  */
 34 xrx.xpath.Node.getValueAsString = function(node) {
 35   var t = null, type = node.nodeType;
 36   // Old IE title problem.
 37   var needTitleFix = function(node) {
 38     return false;
 39   };
 40   // goog.dom.getTextContent doesn't seem to work
 41   if (type == goog.dom.NodeType.ELEMENT) {
 42     t = node.textContent;
 43     t = (t == undefined || t == null) ? node.innerText : t;
 44     t = (t == undefined || t == null) ? '' : t;
 45   }
 46   if (typeof t != 'string') {
 47     if (needTitleFix(node) && type == goog.dom.NodeType.ELEMENT) {
 48       t = node.text;
 49     } else if (type == goog.dom.NodeType.DOCUMENT ||
 50         type == goog.dom.NodeType.ELEMENT) {
 51       node = (type == goog.dom.NodeType.DOCUMENT) ?
 52           node.documentElement : node.firstChild;
 53       var i = 0, stack = [];
 54       for (t = ''; node;) {
 55         do {
 56           if (node.nodeType != goog.dom.NodeType.ELEMENT) {
 57             t += node.nodeValue;
 58           }
 59           if (needTitleFix(node)) {
 60             t += node.text;
 61           }
 62           stack[i++] = node; // push
 63         } while (node = node.firstChild);
 64         while (i && !(node = stack[--i].nextSibling)) {}
 65       }
 66     } else {
 67       t = node.nodeValue;
 68     }
 69   }
 70   return '' + t;
 71 };
 72 
 73 
 74 /**
 75  * Returns the string-value of the required type from a node, casted to number.
 76  *
 77  * @param {!xrx.xpath.Node} node The node to get value from.
 78  * @return {number} The value required.
 79  */
 80 xrx.xpath.Node.getValueAsNumber = function(node) {
 81   return +xrx.xpath.Node.getValueAsString(node);
 82 };
 83 
 84 
 85 /**
 86  * Returns the string-value of the required type from a node, casted to boolean.
 87  *
 88  * @param {!xrx.xpath.Node} node The node to get value from.
 89  * @return {boolean} The value required.
 90  */
 91 xrx.xpath.Node.getValueAsBool = function(node) {
 92   return !!xrx.xpath.Node.getValueAsString(node);
 93 };
 94 
 95 
 96 /**
 97  * Returns if the attribute matches the given value.
 98  *
 99  * @param {!xrx.xpath.Node} node The node to get value from.
100  * @param {?string} name The attribute name to match, if any.
101  * @param {?string} value The attribute value to match, if any.
102  * @return {boolean} Whether the node matches the attribute, if any.
103  */
104 xrx.xpath.Node.attrMatches = function(node, name, value) {
105   // No attribute.
106   if (goog.isNull(name)) {
107     return true;
108   }
109   try {
110     if (!node.getAttribute) {
111       return false;
112     }
113   } catch (e) {
114     return false;
115   }
116   return value == null ? !!node.getAttribute(name) :
117       (node.getAttribute(name, 2) == value);
118 };
119 
120 
121 /**
122  * Returns the descendants of a node.
123  *
124  * @param {!xrx.xpath.NodeTest} test A NodeTest for matching nodes.
125  * @param {!xrx.xpath.Node} node The node to get descendants from.
126  * @param {?string=} opt_attrName The attribute name to match, if any.
127  * @param {?string=} opt_attrValue The attribute value to match, if any.
128  * @param {!xrx.xpath.NodeSet=} opt_nodeset The node set to add descendants to.
129  * @return {!xrx.xpath.NodeSet} The nodeset with descendants.
130  */
131 xrx.xpath.Node.getDescendantNodes = function(test, node, opt_attrName,
132     opt_attrValue, opt_nodeset) {
133   var nodeset = opt_nodeset || new xrx.xpath.NodeSet();
134   var func = xrx.xpath.Node.getDescendantNodesGeneric_;
135   var attrName = goog.isString(opt_attrName) ? opt_attrName : null;
136   var attrValue = goog.isString(opt_attrValue) ? opt_attrValue : null;
137   return func.call(null, test, node, attrName, attrValue, nodeset);
138 };
139 
140 
141 
142 /**
143  * Returns the descendants of a node for browsers other than IE.
144  *
145  * @private
146  * @param {!xrx.xpath.NodeTest} test A NodeTest for matching nodes.
147  * @param {!xrx.xpath.Node} node The node to get descendants from.
148  * @param {?string} attrName The attribute name to match, if any.
149  * @param {?string} attrValue The attribute value to match, if any.
150  * @param {!xrx.xpath.NodeSet} nodeset The node set to add descendants to.
151  * @return {!xrx.xpath.NodeSet} The nodeset with descendants.
152  */
153 xrx.xpath.Node.getDescendantNodesGeneric_ = function(test, node,
154     attrName, attrValue, nodeset) {
155   if (node.getElementsByName && attrValue && attrName == 'name' &&
156       !goog.userAgent.IE) {
157     var nodes = node.getElementsByName(attrValue);
158     goog.array.forEach(nodes, function(node) {
159       if (test.matches(node)) {
160         nodeset.add(node);
161       }
162     });
163   } else if (node.getElementsByClassName && attrValue && attrName == 'class') {
164     var nodes = node.getElementsByClassName(attrValue);
165     goog.array.forEach(nodes, function(node) {
166       if (node.className == attrValue && test.matches(node)) {
167         nodeset.add(node);
168       }
169     });
170   } else if (test instanceof xrx.xpath.KindTest) {
171     xrx.xpath.Node.doRecursiveAttrMatch_(test, node, attrName,
172         attrValue, nodeset);
173   } else if (node.getElementsByTagName) {
174     var nodes = node.getElementsByTagName(test.getName());
175     goog.array.forEach(nodes, function(node) {
176       if (xrx.xpath.Node.attrMatches(node, attrName, attrValue)) {
177         nodeset.add(node);
178       }
179     });
180   }
181   return nodeset;
182 };
183 
184 
185 /**
186  * Returns the child nodes of a node.
187  *
188  * @param {!xrx.xpath.NodeTest} test A NodeTest for matching nodes.
189  * @param {!xrx.xpath.Node} node The node to get child nodes from.
190  * @param {?string=} opt_attrName The attribute name to match, if any.
191  * @param {?string=} opt_attrValue The attribute value to match, if any.
192  * @param {!xrx.xpath.NodeSet=} opt_nodeset The node set to add child nodes to.
193  * @return {!xrx.xpath.NodeSet} The nodeset with child nodes.
194  */
195 xrx.xpath.Node.getChildNodes = function(test, node,
196     opt_attrName, opt_attrValue, opt_nodeset) {
197   var nodeset = opt_nodeset || new xrx.xpath.NodeSet();
198   var func = xrx.xpath.Node.getChildNodesGeneric_;
199   var attrName = goog.isString(opt_attrName) ? opt_attrName : null;
200   var attrValue = goog.isString(opt_attrValue) ? opt_attrValue : null;
201   return func.call(null, test, node, attrName, attrValue, nodeset);
202 };
203 
204 
205 
206 /**
207  * Returns the child nodes of a node genericly.
208  *
209  * @private
210  * @param {!xrx.xpath.NodeTest} test A NodeTest for matching nodes.
211  * @param {!xrx.xpath.Node} node The node to get child nodes from.
212  * @param {?string} attrName The attribute name to match, if any.
213  * @param {?string} attrValue The attribute value to match, if any.
214  * @param {!xrx.xpath.NodeSet} nodeset The node set to add child nodes to.
215  * @return {!xrx.xpath.NodeSet} The nodeset with child nodes.
216  */
217 xrx.xpath.Node.getChildNodesGeneric_ = function(test, node, attrName,
218     attrValue, nodeset) {
219   for (var current = node.firstChild; current; current = current.nextSibling) {
220     if (xrx.xpath.Node.attrMatches(current, attrName, attrValue)) {
221       if (test.matches(current)) {
222         nodeset.add(current);
223       }
224     }
225   }
226   return nodeset;
227 };
228 
229 
230 /**
231  * Returns whether a getting descendants/children call
232  * needs special handling on IE browsers.
233  *
234  * @private
235  * @param {!xrx.xpath.NodeTest} test A NodeTest for matching nodes.
236  * @param {!xrx.xpath.Node} node The root node to start the recursive call on.
237  * @param {?string} attrName The attribute name to match, if any.
238  * @param {?string} attrValue The attribute value to match, if any.
239  * @param {!xrx.xpath.NodeSet} nodeset The NodeSet to add nodes to.
240  */
241 xrx.xpath.Node.doRecursiveAttrMatch_ = function(test, node,
242     attrName, attrValue, nodeset) {
243   for (var n = node.firstChild; n; n = n.nextSibling) {
244     if (xrx.xpath.Node.attrMatches(n, attrName, attrValue) &&
245         test.matches(n)) {
246       nodeset.add(n);
247     }
248     xrx.xpath.Node.doRecursiveAttrMatch_(test, n, attrName,
249         attrValue, nodeset);
250   }
251 };
252