blob: cdcdc18de24aff9b8522c5284978c2dc9ddc5747 [file] [log] [blame] [edit]
/**
* @license
* The MIT License
*
* Copyright (c) 2007 Cybozu Labs, Inc.
* Copyright (c) 2012 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/**
* @fileoverview Node utilities.
* @author [email protected] (Michael Zhou)
*/
goog.provide('wgxpath.Node');
goog.require('goog.array');
goog.require('goog.dom.NodeType');
goog.require('goog.userAgent');
goog.require('wgxpath.IEAttrWrapper');
goog.require('wgxpath.userAgent');
/** @typedef {!(Node|wgxpath.IEAttrWrapper)} */
wgxpath.Node = {};
/**
* Returns whether two nodes are equal.
*
* @param {wgxpath.Node} a The first node.
* @param {wgxpath.Node} b The second node.
* @return {boolean} Whether the nodes are equal.
*/
wgxpath.Node.equal = function(a, b) {
return (a == b) || (a instanceof wgxpath.IEAttrWrapper &&
b instanceof wgxpath.IEAttrWrapper && a.getNode() ==
b.getNode());
};
/**
* Returns the string-value of the required type from a node.
*
* @param {!wgxpath.Node} node The node to get value from.
* @return {string} The value required.
*/
wgxpath.Node.getValueAsString = function(node) {
var t = null, type = node.nodeType;
// Old IE title problem.
var needTitleFix = function(node) {
return wgxpath.userAgent.IE_DOC_PRE_9 &&
node.nodeName.toLowerCase() == 'title';
};
// goog.dom.getTextContent doesn't seem to work
if (type == goog.dom.NodeType.ELEMENT) {
t = node.textContent;
t = (t == undefined || t == null) ? node.innerText : t;
t = (t == undefined || t == null) ? '' : t;
}
if (typeof t != 'string') {
if (needTitleFix(node) && type == goog.dom.NodeType.ELEMENT) {
t = node.text;
} else if (type == goog.dom.NodeType.DOCUMENT ||
type == goog.dom.NodeType.ELEMENT) {
node = (type == goog.dom.NodeType.DOCUMENT) ?
node.documentElement : node.firstChild;
var i = 0, stack = [];
for (t = ''; node;) {
do {
if (node.nodeType != goog.dom.NodeType.ELEMENT) {
t += node.nodeValue;
}
if (needTitleFix(node)) {
t += node.text;
}
stack[i++] = node; // push
} while (node = node.firstChild);
while (i && !(node = stack[--i].nextSibling)) {}
}
} else {
t = node.nodeValue;
}
}
return '' + t;
};
/**
* Returns the string-value of the required type from a node, casted to number.
*
* @param {!wgxpath.Node} node The node to get value from.
* @return {number} The value required.
*/
wgxpath.Node.getValueAsNumber = function(node) {
return +wgxpath.Node.getValueAsString(node);
};
/**
* Returns the string-value of the required type from a node, casted to boolean.
*
* @param {!wgxpath.Node} node The node to get value from.
* @return {boolean} The value required.
*/
wgxpath.Node.getValueAsBool = function(node) {
return !!wgxpath.Node.getValueAsString(node);
};
/**
* Returns if the attribute matches the given value.
*
* @param {!wgxpath.Node} node The node to get value from.
* @param {?string} name The attribute name to match, if any.
* @param {?string} value The attribute value to match, if any.
* @return {boolean} Whether the node matches the attribute, if any.
*/
wgxpath.Node.attrMatches = function(node, name, value) {
// No attribute.
if (goog.isNull(name)) {
return true;
}
// TODO: If possible, figure out why this throws an exception in some
// cases on IE < 9.
try {
if (!node.getAttribute) {
return false;
}
} catch (e) {
return false;
}
if (wgxpath.userAgent.IE_DOC_PRE_8 && name == 'class') {
name = 'className';
}
return value == null ? !!node.getAttribute(name) :
(node.getAttribute(name, 2) == value);
};
/**
* Returns the descendants of a node.
*
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get descendants from.
* @param {?string=} opt_attrName The attribute name to match, if any.
* @param {?string=} opt_attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet=} opt_nodeset The node set to add descendants to.
* @return {!wgxpath.NodeSet} The nodeset with descendants.
* @suppress {missingRequire} There's a circular dependency between this file
* and nodeset.js.
*/
wgxpath.Node.getDescendantNodes = function(test, node, opt_attrName,
opt_attrValue, opt_nodeset) {
var nodeset = opt_nodeset || new wgxpath.NodeSet();
var func = wgxpath.userAgent.IE_DOC_PRE_9 ?
wgxpath.Node.getDescendantNodesIEPre9_ :
wgxpath.Node.getDescendantNodesGeneric_;
var attrName = goog.isString(opt_attrName) ? opt_attrName : null;
var attrValue = goog.isString(opt_attrValue) ? opt_attrValue : null;
return func.call(null, test, node, attrName, attrValue, nodeset);
};
/**
* Returns the descendants of a node for IE.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get descendants from.
* @param {?string} attrName The attribute name to match, if any.
* @param {?string} attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet} nodeset The node set to add descendants to.
* @return {!wgxpath.NodeSet} The nodeset with descendants.
*/
wgxpath.Node.getDescendantNodesIEPre9_ = function(test, node, attrName,
attrValue, nodeset) {
if (wgxpath.Node.doesNeedSpecialHandlingIEPre9_(test, attrName)) {
var descendants = node.all;
if (!descendants) {
return nodeset;
}
var name = wgxpath.Node.getNameFromTestIEPre9_(test);
// all.tags not working.
if (name != '*') {
descendants = node.getElementsByTagName(name);
if (!descendants) {
return nodeset;
}
}
if (attrName) {
/**
* The length property of the "all" collection is overwritten
* if there exists an element with id="length", therefore we
* have to iterate without knowing the length.
*/
var result = [];
var i = 0;
while (node = descendants[i++]) {
if (wgxpath.Node.attrMatches(node, attrName, attrValue)) {
result.push(node);
}
}
descendants = result;
}
var i = 0;
while (node = descendants[i++]) {
if (name != '*' || node.tagName != '!') {
nodeset.add(node);
}
}
return nodeset;
}
wgxpath.Node.doRecursiveAttrMatch_(test, node, attrName,
attrValue, nodeset);
return nodeset;
};
/**
* Returns the descendants of a node for browsers other than IE.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get descendants from.
* @param {?string} attrName The attribute name to match, if any.
* @param {?string} attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet} nodeset The node set to add descendants to.
* @return {!wgxpath.NodeSet} The nodeset with descendants.
*/
wgxpath.Node.getDescendantNodesGeneric_ = function(test, node,
attrName, attrValue, nodeset) {
if (node.getElementsByName && attrValue && attrName == 'name' &&
!goog.userAgent.IE) {
var nodes = node.getElementsByName(attrValue);
goog.array.forEach(nodes, function(node) {
if (test.matches(node)) {
nodeset.add(node);
}
});
} else if (node.getElementsByClassName && attrValue && attrName == 'class') {
var nodes = node.getElementsByClassName(attrValue);
goog.array.forEach(nodes, function(node) {
if (node.className == attrValue && test.matches(node)) {
nodeset.add(node);
}
});
} else if (test instanceof wgxpath.KindTest) {
wgxpath.Node.doRecursiveAttrMatch_(test, node, attrName,
attrValue, nodeset);
} else if (node.getElementsByTagName) {
var nodes = node.getElementsByTagName(test.getName());
goog.array.forEach(nodes, function(node) {
if (wgxpath.Node.attrMatches(node, attrName, attrValue)) {
nodeset.add(node);
}
});
}
return nodeset;
};
/**
* Returns the child nodes of a node.
*
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get child nodes from.
* @param {?string=} opt_attrName The attribute name to match, if any.
* @param {?string=} opt_attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet=} opt_nodeset The node set to add child nodes to.
* @return {!wgxpath.NodeSet} The nodeset with child nodes.
* @suppress {missingRequire} There's a circular dependency between this file
* and nodeset.js.
*/
wgxpath.Node.getChildNodes = function(test, node,
opt_attrName, opt_attrValue, opt_nodeset) {
var nodeset = opt_nodeset || new wgxpath.NodeSet();
var func = wgxpath.userAgent.IE_DOC_PRE_9 ?
wgxpath.Node.getChildNodesIEPre9_ : wgxpath.Node.getChildNodesGeneric_;
var attrName = goog.isString(opt_attrName) ? opt_attrName : null;
var attrValue = goog.isString(opt_attrValue) ? opt_attrValue : null;
return func.call(null, test, node, attrName, attrValue, nodeset);
};
/**
* Returns the child nodes of a node for IE browsers.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get child nodes from.
* @param {?string} attrName The attribute name to match, if any.
* @param {?string} attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet} nodeset The node set to add child nodes to.
* @return {!wgxpath.NodeSet} The nodeset with child nodes.
*/
wgxpath.Node.getChildNodesIEPre9_ = function(test, node,
attrName, attrValue, nodeset) {
var children;
if (wgxpath.Node.doesNeedSpecialHandlingIEPre9_(test, attrName) &&
(children = node.childNodes)) { // node.children seems buggy.
var name = wgxpath.Node.getNameFromTestIEPre9_(test);
if (name != '*') {
//children = children.tags(name); // children.tags seems buggy.
children = goog.array.filter(children, function(child) {
return child.tagName && child.tagName.toLowerCase() == name;
});
if (!children) {
return nodeset;
}
}
if (attrName) {
// TODO: See if an optimization is possible.
children = goog.array.filter(children, function(n) {
return wgxpath.Node.attrMatches(n, attrName, attrValue);
});
}
goog.array.forEach(children, function(node) {
if (name != '*' || node.tagName != '!' &&
!(name == '*' && node.nodeType != goog.dom.NodeType.ELEMENT)) {
nodeset.add(node);
}
});
return nodeset;
}
return wgxpath.Node.getChildNodesGeneric_(test, node, attrName,
attrValue, nodeset);
};
/**
* Returns the child nodes of a node genericly.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The node to get child nodes from.
* @param {?string} attrName The attribute name to match, if any.
* @param {?string} attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet} nodeset The node set to add child nodes to.
* @return {!wgxpath.NodeSet} The nodeset with child nodes.
*/
wgxpath.Node.getChildNodesGeneric_ = function(test, node, attrName,
attrValue, nodeset) {
for (var current = node.firstChild; current; current = current.nextSibling) {
if (wgxpath.Node.attrMatches(current, attrName, attrValue)) {
if (test.matches(current)) {
nodeset.add(current);
}
}
}
return nodeset;
};
/**
* Returns whether a getting descendants/children call
* needs special handling on IE browsers.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {!wgxpath.Node} node The root node to start the recursive call on.
* @param {?string} attrName The attribute name to match, if any.
* @param {?string} attrValue The attribute value to match, if any.
* @param {!wgxpath.NodeSet} nodeset The NodeSet to add nodes to.
*/
wgxpath.Node.doRecursiveAttrMatch_ = function(test, node,
attrName, attrValue, nodeset) {
for (var n = node.firstChild; n; n = n.nextSibling) {
if (wgxpath.Node.attrMatches(n, attrName, attrValue) &&
test.matches(n)) {
nodeset.add(n);
}
wgxpath.Node.doRecursiveAttrMatch_(test, n, attrName,
attrValue, nodeset);
}
};
/**
* Returns whether a getting descendants/children call
* needs special handling on IE browsers.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest for matching nodes.
* @param {?string} attrName The attribute name to match, if any.
* @return {boolean} Whether the call needs special handling.
*/
wgxpath.Node.doesNeedSpecialHandlingIEPre9_ = function(test, attrName) {
return test instanceof wgxpath.NameTest ||
test.getType() == goog.dom.NodeType.COMMENT ||
(!!attrName && goog.isNull(test.getType()));
};
/**
* Returns a fixed name of a NodeTest for IE browsers.
*
* @private
* @param {!wgxpath.NodeTest} test A NodeTest.
* @return {string} The name of the NodeTest.
*/
wgxpath.Node.getNameFromTestIEPre9_ = function(test) {
if (test instanceof wgxpath.KindTest) {
if (test.getType() == goog.dom.NodeType.COMMENT) {
return '!';
} else if (goog.isNull(test.getType())) {
return '*';
}
}
return test.getName();
};