blob: 5e16b63945949e7b342b12e560c2405f62baa052 [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 A step expression.
* @author [email protected] (Michael Zhou)
*/
goog.provide('wgxpath.Step');
goog.require('goog.array');
goog.require('goog.dom.NodeType');
goog.require('wgxpath.DataType');
goog.require('wgxpath.Expr');
goog.require('wgxpath.KindTest');
goog.require('wgxpath.Node');
goog.require('wgxpath.NodeSet');
goog.require('wgxpath.Predicates');
goog.require('wgxpath.userAgent');
/**
* Class for a step in a path expression
* http://www.w3.org/TR/xpath20/#id-steps.
*
* @extends {wgxpath.Expr}
* @constructor
* @param {!wgxpath.Step.Axis} axis The axis for this Step.
* @param {!wgxpath.NodeTest} test The test for this Step.
* @param {!wgxpath.Predicates=} opt_predicates The predicates for this
* Step.
* @param {boolean=} opt_descendants Whether descendants are to be included in
* this step ('//' vs '/').
*/
wgxpath.Step = function(axis, test, opt_predicates, opt_descendants) {
var axisCast = /** @type {!wgxpath.Step.Axis_} */ (axis);
wgxpath.Expr.call(this, wgxpath.DataType.NODESET);
/**
* @type {!wgxpath.Step.Axis_}
* @private
*/
this.axis_ = axisCast;
/**
* @type {!wgxpath.NodeTest}
* @private
*/
this.test_ = test;
/**
* @type {!wgxpath.Predicates}
* @private
*/
this.predicates_ = opt_predicates || new wgxpath.Predicates([]);
/**
* Whether decendants are included in this step
*
* @private
* @type {boolean}
*/
this.descendants_ = !!opt_descendants;
var quickAttrInfo = this.predicates_.getQuickAttr();
if (axis.supportsQuickAttr_ && quickAttrInfo) {
var attrName = quickAttrInfo.name;
attrName = wgxpath.userAgent.IE_DOC_PRE_9 ?
attrName.toLowerCase() : attrName;
var attrValueExpr = quickAttrInfo.valueExpr;
this.setQuickAttr({
name: attrName,
valueExpr: attrValueExpr
});
}
this.setNeedContextPosition(this.predicates_.doesNeedContextPosition());
};
goog.inherits(wgxpath.Step, wgxpath.Expr);
/**
* @override
* @return {!wgxpath.NodeSet} The nodeset result.
*/
wgxpath.Step.prototype.evaluate = function(ctx) {
var node = ctx.getNode();
var nodeset = null;
var quickAttr = this.getQuickAttr();
var attrName = null;
var attrValue = null;
var pstart = 0;
if (quickAttr) {
attrName = quickAttr.name;
attrValue = quickAttr.valueExpr ?
quickAttr.valueExpr.asString(ctx) : null;
pstart = 1;
}
if (this.descendants_) {
if (!this.doesNeedContextPosition() &&
this.axis_ == wgxpath.Step.Axis.CHILD) {
nodeset = wgxpath.Node.getDescendantNodes(this.test_, node,
attrName, attrValue);
nodeset = this.predicates_.evaluatePredicates(nodeset, pstart);
} else {
var step = new wgxpath.Step(wgxpath.Step.Axis.DESCENDANT_OR_SELF,
new wgxpath.KindTest('node'));
var iter = step.evaluate(ctx).iterator();
var n = iter.next();
if (!n) {
nodeset = new wgxpath.NodeSet();
} else {
nodeset = this.evaluate_(/** @type {!wgxpath.Node} */ (n),
attrName, attrValue, pstart);
while ((n = iter.next()) != null) {
nodeset = wgxpath.NodeSet.merge(nodeset,
this.evaluate_(/** @type {!wgxpath.Node} */ (n), attrName,
attrValue, pstart));
}
}
}
} else {
nodeset = this.evaluate_(ctx.getNode(), attrName, attrValue, pstart);
}
return nodeset;
};
/**
* Evaluates this step on the given context to a nodeset.
* (assumes this.descendants_ = false)
*
* @private
* @param {!wgxpath.Node} node The context node.
* @param {?string} attrName The name of the attribute.
* @param {?string} attrValue The value of the attribute.
* @param {number} pstart The first predicate to evaluate.
* @return {!wgxpath.NodeSet} The nodeset from evaluating this Step.
*/
wgxpath.Step.prototype.evaluate_ = function(
node, attrName, attrValue, pstart) {
var nodeset = this.axis_.func_(this.test_, node, attrName, attrValue);
nodeset = this.predicates_.evaluatePredicates(nodeset, pstart);
return nodeset;
};
/**
* Returns whether the step evaluation should include descendants.
*
* @return {boolean} Whether descendants are included.
*/
wgxpath.Step.prototype.doesIncludeDescendants = function() {
return this.descendants_;
};
/**
* Returns the step's axis.
*
* @return {!wgxpath.Step.Axis} The axis.
*/
wgxpath.Step.prototype.getAxis = function() {
return /** @type {!wgxpath.Step.Axis} */ (this.axis_);
};
/**
* Returns the test for this step.
*
* @return {!wgxpath.NodeTest} The test for this step.
*/
wgxpath.Step.prototype.getTest = function() {
return this.test_;
};
/**
* @override
*/
wgxpath.Step.prototype.toString = function() {
var text = 'Step:';
text += wgxpath.Expr.indent('Operator: ' + (this.descendants_ ? '//' : '/'));
if (this.axis_.name_) {
text += wgxpath.Expr.indent('Axis: ' + this.axis_);
}
text += wgxpath.Expr.indent(this.test_);
if (this.predicates_.getLength()) {
var predicates = goog.array.reduce(this.predicates_.getPredicates(),
function(prev, curr) {
return prev + wgxpath.Expr.indent(curr);
}, 'Predicates:');
text += wgxpath.Expr.indent(predicates);
}
return text;
};
/**
* A step axis.
*
* @constructor
* @param {string} name The axis name.
* @param {function(!wgxpath.NodeTest, wgxpath.Node, ?string, ?string):
* !wgxpath.NodeSet} func The function for this axis.
* @param {boolean} reverse Whether to iterate over the nodeset in reverse.
* @param {boolean} supportsQuickAttr Whether quickAttr should be enabled for
* this axis.
* @private
*/
wgxpath.Step.Axis_ = function(name, func, reverse, supportsQuickAttr) {
/**
* @private
* @type {string}
*/
this.name_ = name;
/**
* @private
* @type {function(!wgxpath.NodeTest, wgxpath.Node, ?string, ?string):
* !wgxpath.NodeSet}
*/
this.func_ = func;
/**
* @private
* @type {boolean}
*/
this.reverse_ = reverse;
/**
* @private
* @type {boolean}
*/
this.supportsQuickAttr_ = supportsQuickAttr;
};
/**
* Returns whether the nodes in the step should be iterated over in reverse.
*
* @return {boolean} Whether the nodes should be iterated over in reverse.
*/
wgxpath.Step.Axis_.prototype.isReverse = function() {
return this.reverse_;
};
/**
* @override
*/
wgxpath.Step.Axis_.prototype.toString = function() {
return this.name_;
};
/**
* A map from axis name to Axis.
*
* @type {!Object.<string, !wgxpath.Step.Axis>}
* @private
*/
wgxpath.Step.nameToAxisMap_ = {};
/**
* Creates an axis and maps the axis's name to that axis.
*
* @param {string} name The axis name.
* @param {function(!wgxpath.NodeTest, wgxpath.Node, ?string, ?string):
* !wgxpath.NodeSet} func The function for this axis.
* @param {boolean} reverse Whether to iterate over nodesets in reverse.
* @param {boolean=} opt_supportsQuickAttr Whether quickAttr can be enabled
* for this axis.
* @return {!wgxpath.Step.Axis} The axis.
* @private
*/
wgxpath.Step.createAxis_ =
function(name, func, reverse, opt_supportsQuickAttr) {
if (wgxpath.Step.nameToAxisMap_.hasOwnProperty(name)) {
throw Error('Axis already created: ' + name);
}
// The upcast and then downcast for the JSCompiler.
var axis = /** @type {!Object} */ (new wgxpath.Step.Axis_(
name, func, reverse, !!opt_supportsQuickAttr));
axis = /** @type {!wgxpath.Step.Axis} */ (axis);
wgxpath.Step.nameToAxisMap_[name] = axis;
return axis;
};
/**
* Returns the axis for this axisname or null if none.
*
* @param {string} name The axis name.
* @return {wgxpath.Step.Axis} The axis.
*/
wgxpath.Step.getAxis = function(name) {
return wgxpath.Step.nameToAxisMap_[name] || null;
};
/**
* Axis enumeration.
*
* @enum {{isReverse: function(): boolean}}
*/
wgxpath.Step.Axis = {
ANCESTOR: wgxpath.Step.createAxis_('ancestor',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
var parent = node;
while (parent = parent.parentNode) {
if (test.matches(parent)) {
nodeset.unshift(parent);
}
}
return nodeset;
}, true),
ANCESTOR_OR_SELF: wgxpath.Step.createAxis_('ancestor-or-self',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
var toMatch = node;
do {
if (test.matches(toMatch)) {
nodeset.unshift(toMatch);
}
} while (toMatch = toMatch.parentNode);
return nodeset;
}, true),
ATTRIBUTE: wgxpath.Step.createAxis_('attribute',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
var testName = test.getName();
// IE8 doesn't allow access to the style attribute using getNamedItem.
// It returns an object with nodeValue = null. Even worse, ".style" on
// IE8 can mutate the DOM, adding an empty string attribute. Therefore
// we check it last.
if (testName == 'style' &&
wgxpath.userAgent.IE_DOC_PRE_9 && node.style) {
nodeset.add(wgxpath.IEAttrWrapper.forStyleOf(
/** @type {!Node} */ (node), node.sourceIndex));
return nodeset;
}
var attrs = node.attributes;
if (attrs) {
if ((test instanceof wgxpath.KindTest &&
goog.isNull(test.getType())) || testName == '*') {
var sourceIndex = node.sourceIndex;
for (var i = 0, attr; attr = attrs[i]; i++) {
if (wgxpath.userAgent.IE_DOC_PRE_9) {
if (attr.nodeValue) {
nodeset.add(wgxpath.IEAttrWrapper.forAttrOf(
/** @type {!Node} */ (node), attr, sourceIndex));
}
} else {
nodeset.add(attr);
}
}
} else {
var attr = attrs.getNamedItem(testName);
if (attr) {
if (wgxpath.userAgent.IE_DOC_PRE_9) {
if (attr.nodeValue) {
nodeset.add(wgxpath.IEAttrWrapper.forAttrOf(
/** @type {!Node} */ (node), attr, node.sourceIndex));
}
} else {
nodeset.add(attr);
}
}
}
}
return nodeset;
}, false),
CHILD: wgxpath.Step.createAxis_('child',
wgxpath.Node.getChildNodes, false, true),
DESCENDANT: wgxpath.Step.createAxis_('descendant',
wgxpath.Node.getDescendantNodes, false, true),
DESCENDANT_OR_SELF: wgxpath.Step.createAxis_('descendant-or-self',
function(test, node, attrName, attrValue) {
var nodeset = new wgxpath.NodeSet();
if (wgxpath.Node.attrMatches(node, attrName, attrValue)) {
if (test.matches(node)) {
nodeset.add(node);
}
}
return wgxpath.Node.getDescendantNodes(test, node,
attrName, attrValue, nodeset);
}, false, true),
FOLLOWING: wgxpath.Step.createAxis_('following',
function(test, node, attrName, attrValue) {
var nodeset = new wgxpath.NodeSet();
var parent = node;
do {
var child = parent;
while (child = child.nextSibling) {
if (wgxpath.Node.attrMatches(child, attrName, attrValue)) {
if (test.matches(child)) {
nodeset.add(child);
}
}
nodeset = wgxpath.Node.getDescendantNodes(test, child,
attrName, attrValue, nodeset);
}
} while (parent = parent.parentNode);
return nodeset;
}, false, true),
FOLLOWING_SIBLING: wgxpath.Step.createAxis_('following-sibling',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
var toMatch = node;
while (toMatch = toMatch.nextSibling) {
if (test.matches(toMatch)) {
nodeset.add(toMatch);
}
}
return nodeset;
}, false),
NAMESPACE: wgxpath.Step.createAxis_('namespace',
function(test, node) {
// not implemented
return new wgxpath.NodeSet();
}, false),
PARENT: wgxpath.Step.createAxis_('parent',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
if (node.nodeType == goog.dom.NodeType.DOCUMENT) {
return nodeset;
} else if (node.nodeType == goog.dom.NodeType.ATTRIBUTE) {
nodeset.add(node.ownerElement);
return nodeset;
}
var parent = /** @type {!Node} */ (node.parentNode);
if (test.matches(parent)) {
nodeset.add(parent);
}
return nodeset;
}, false),
PRECEDING: wgxpath.Step.createAxis_('preceding',
function(test, node, attrName, attrValue) {
var nodeset = new wgxpath.NodeSet();
var parents = [];
var parent = node;
do {
parents.unshift(parent);
} while (parent = parent.parentNode);
for (var i = 1, l0 = parents.length; i < l0; i++) {
var siblings = [];
node = parents[i];
while (node = node.previousSibling) {
siblings.unshift(node);
}
for (var j = 0, l1 = siblings.length; j < l1; j++) {
node = siblings[j];
if (wgxpath.Node.attrMatches(node, attrName, attrValue)) {
if (test.matches(node)) nodeset.add(node);
}
nodeset = wgxpath.Node.getDescendantNodes(test, node,
attrName, attrValue, nodeset);
}
}
return nodeset;
}, true, true),
PRECEDING_SIBLING: wgxpath.Step.createAxis_('preceding-sibling',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
var toMatch = node;
while (toMatch = toMatch.previousSibling) {
if (test.matches(toMatch)) {
nodeset.unshift(toMatch);
}
}
return nodeset;
}, true),
SELF: wgxpath.Step.createAxis_('self',
function(test, node) {
var nodeset = new wgxpath.NodeSet();
if (test.matches(node)) {
nodeset.add(node);
}
return nodeset;
}, false)
};