blob: 98e3a7d5db82251949afb6a3bfe5758f911a07d6 [file] [log] [blame]
/** Here is yet another implementation of XPath 1.0 in Javascript.
*
* My goal was to make it relatively compact, but as I fixed all the axis bugs
* the axes became more and more complicated. :-(.
*
* I have not implemented namespaces or case-sensitive axes for XML yet.
*
* How to test it in Chrome: You can make a Chrome extension that replaces
* the WebKit XPath parser with this one. But it takes a bit of effort to
* get around isolated world and same-origin restrictions:
* manifest.json:
{
"name": "XPathTest",
"version": "0.1",
"content_scripts": [{
"matches": ["http://localhost/*"], // or wildcard host
"js": ["xpath.js", "injection.js"],
"all_frames": true, "run_at": "document_start"
}]
}
* injection.js:
// goal: give my xpath object to the website's JS context.
var script = document.createElement('script');
script.textContent =
"document.addEventListener('xpathextend', function(e) {\n" +
" console.log('extending document with xpath...');\n" +
" e.detail(window);" +
"});";
document.documentElement.appendChild(script);
document.documentElement.removeChild(script);
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('xpathextend', true, true, this.xpath.extend);
document.dispatchEvent(evt);
*/
(function() {
var xpath;
var core;
if ('function' === typeof require) {
xpath = exports; // the tests go through this
core = require("../level1/core");
} else {
xpath = {};
core = this;
}
/***************************************************************************
* Tokenization *
***************************************************************************/
/**
* The XPath lexer is basically a single regular expression, along with
* some helper functions to pop different types.
*/
var Stream = xpath.Stream = function Stream(str) {
this.original = this.str = str;
this.peeked = null;
// TODO: not really needed, but supposedly tokenizer also disambiguates
// a * b vs. node test *
this.prev = null; // for debugging
this.prevprev = null;
}
Stream.prototype = {
peek: function() {
if (this.peeked) return this.peeked;
var m = this.re.exec(this.str);
if (!m) return null;
this.str = this.str.substr(m[0].length);
return this.peeked = m[1];
},
/** Peek 2 tokens ahead. */
peek2: function() {
this.peek(); // make sure this.peeked is set
var m = this.re.exec(this.str);
if (!m) return null;
return m[1];
},
pop: function() {
var r = this.peek();
this.peeked = null;
this.prevprev = this.prev;
this.prev = r;
return r;
},
trypop: function(tokens) {
var tok = this.peek();
if (tok === tokens) return this.pop();
if (Array.isArray(tokens)) {
for (var i = 0; i < tokens.length; ++i) {
var t = tokens[i];
if (t == tok) return this.pop();;
}
}
},
trypopfuncname: function() {
var tok = this.peek();
if (!this.isQnameRe.test(tok))
return null;
switch (tok) {
case 'comment': case 'text': case 'processing-instruction': case 'node':
return null;
}
if ('(' != this.peek2()) return null;
return this.pop();
},
trypopaxisname: function() {
var tok = this.peek();
switch (tok) {
case 'ancestor': case 'ancestor-or-self': case 'attribute':
case 'child': case 'descendant': case 'descendant-or-self':
case 'following': case 'following-sibling': case 'namespace':
case 'parent': case 'preceding': case 'preceding-sibling': case 'self':
if ('::' == this.peek2()) return this.pop();
}
return null;
},
trypopnametest: function() {
var tok = this.peek();
if ('*' === tok || this.startsWithNcNameRe.test(tok)) return this.pop();
return null;
},
trypopliteral: function() {
var tok = this.peek();
if (null == tok) return null;
var first = tok.charAt(0);
var last = tok.charAt(tok.length - 1);
if ('"' === first && '"' === last ||
"'" === first && "'" === last) {
this.pop();
return tok.substr(1, tok.length - 2);
}
},
trypopnumber: function() {
var tok = this.peek();
if (this.isNumberRe.test(tok)) return parseFloat(this.pop());
else return null;
},
trypopvarref: function() {
var tok = this.peek();
if (null == tok) return null;
if ('$' === tok.charAt(0)) return this.pop().substr(1);
else return null;
},
position: function() {
return this.original.length - this.str.length;
}
};
(function() {
// http://www.w3.org/TR/REC-xml-names/#NT-NCName
var nameStartCharsExceptColon =
'A-Z_a-z\xc0-\xd6\xd8-\xf6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF' +
'\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF' +
'\uFDF0-\uFFFD'; // JS doesn't support [#x10000-#xEFFFF]
var nameCharExceptColon = nameStartCharsExceptColon +
'\\-\\.0-9\xb7\u0300-\u036F\u203F-\u2040';
var ncNameChars = '[' + nameStartCharsExceptColon +
'][' + nameCharExceptColon + ']*'
// http://www.w3.org/TR/REC-xml-names/#NT-QName
var qNameChars = ncNameChars + '(?::' + ncNameChars + ')?';
var otherChars = '\\.\\.|[\\(\\)\\[\\].@,]|::'; // .. must come before [.]
var operatorChars =
'and|or|mod|div|' +
'//|!=|<=|>=|[*/|+\\-=<>]'; // //, !=, <=, >= before individual ones.
var literal = '"[^"]*"|' + "'[^']*'";
var numberChars = '[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+';
var variableReference = '\\$' + qNameChars;
var nameTestChars = '\\*|' + ncNameChars + ':\\*|' + qNameChars;
var optionalSpace = '[ \t\r\n]*'; // stricter than regexp \s.
var nodeType = 'comment|text|processing-instruction|node';
var re = new RegExp(
// numberChars before otherChars so that leading-decimal doesn't become .
'^' + optionalSpace + '(' + numberChars + '|' + otherChars + '|' +
nameTestChars + '|' + operatorChars + '|' + literal + '|' +
variableReference + ')'
// operatorName | nodeType | functionName | axisName are lumped into
// qName for now; we'll check them on pop.
);
Stream.prototype.re = re;
Stream.prototype.startsWithNcNameRe = new RegExp('^' + ncNameChars);
Stream.prototype.isQnameRe = new RegExp('^' + qNameChars + '$');
Stream.prototype.isNumberRe = new RegExp('^' + numberChars + '$');
})();
/***************************************************************************
* Parsing *
***************************************************************************/
var parse = xpath.parse = function parse(stream, a) {
var r = orExpr(stream,a);
var x, unparsed = [];
while (x = stream.pop()) {
unparsed.push(x);
}
if (unparsed.length)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Unparsed tokens: ' + unparsed.join(' '));
return r;
}
/**
* binaryL ::= subExpr
* | binaryL op subExpr
* so a op b op c becomes ((a op b) op c)
*/
function binaryL(subExpr, stream, a, ops) {
var lhs = subExpr(stream, a);
if (lhs == null) return null;
var op;
while (op = stream.trypop(ops)) {
var rhs = subExpr(stream, a);
if (rhs == null)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected something after ' + op);
lhs = a.node(op, lhs, rhs);
}
return lhs;
}
/**
* Too bad this is never used. If they made a ** operator (raise to power),
( we would use it.
* binaryR ::= subExpr
* | subExpr op binaryR
* so a op b op c becomes (a op (b op c))
*/
function binaryR(subExpr, stream, a, ops) {
var lhs = subExpr(stream, a);
if (lhs == null) return null;
var op = stream.trypop(ops);
if (op) {
var rhs = binaryR(stream, a);
if (rhs == null)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected something after ' + op);
return a.node(op, lhs, rhs);
} else {
return lhs;// TODO
}
}
/** [1] LocationPath::= RelativeLocationPath | AbsoluteLocationPath
* e.g. a, a/b, //a/b
*/
function locationPath(stream, a) {
return absoluteLocationPath(stream, a) ||
relativeLocationPath(null, stream, a);
}
/** [2] AbsoluteLocationPath::= '/' RelativeLocationPath? | AbbreviatedAbsoluteLocationPath
* [10] AbbreviatedAbsoluteLocationPath::= '//' RelativeLocationPath
*/
function absoluteLocationPath(stream, a) {
var op = stream.peek();
if ('/' === op || '//' === op) {
var lhs = a.node('Root');
return relativeLocationPath(lhs, stream, a, true);
} else {
return null;
}
}
/** [3] RelativeLocationPath::= Step | RelativeLocationPath '/' Step |
* | AbbreviatedRelativeLocationPath
* [11] AbbreviatedRelativeLocationPath::= RelativeLocationPath '//' Step
* e.g. p/a, etc.
*/
function relativeLocationPath(lhs, stream, a, isOnlyRootOk) {
if (null == lhs) {
lhs = step(stream, a);
if (null == lhs) return lhs;
}
var op;
while (op = stream.trypop(['/', '//'])) {
if ('//' === op) {
lhs = a.node('/', lhs,
a.node('Axis', 'descendant-or-self', 'node', undefined));
}
var rhs = step(stream, a);
if (null == rhs && '/' === op && isOnlyRootOk) return lhs;
else isOnlyRootOk = false;
if (null == rhs)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected step after ' + op);
lhs = a.node('/', lhs, rhs);
}
return lhs;
}
/** [4] Step::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
* [12] AbbreviatedStep::= '.' | '..'
* e.g. @href, self::p, p, a[@href], ., ..
*/
function step(stream, a) {
var abbrStep = stream.trypop(['.', '..']);
if ('.' === abbrStep) // A location step of . is short for self::node().
return a.node('Axis', 'self', 'node');
if ('..' === abbrStep) // A location step of .. is short for parent::node()
return a.node('Axis', 'parent', 'node');
var axis = axisSpecifier(stream, a);
var nodeType = nodeTypeTest(stream, a);
var nodeName;
if (null == nodeType) nodeName = nodeNameTest(stream, a);
if (null == axis && null == nodeType && null == nodeName) return null;
if (null == nodeType && null == nodeName)
throw new XPathException(
XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected nodeTest after axisSpecifier ' + axis);
if (null == axis) axis = 'child';
if (null == nodeType) {
// When there's only a node name, then the node type is forced to be the
// principal node type of the axis.
// see http://www.w3.org/TR/xpath/#dt-principal-node-type
if ('attribute' === axis) nodeType = 'attribute';
else if ('namespace' === axis) nodeType = 'namespace';
else nodeType = 'element';
}
var lhs = a.node('Axis', axis, nodeType, nodeName);
var pred;
while (null != (pred = predicate(lhs, stream, a))) {
lhs = pred;
}
return lhs;
}
/** [5] AxisSpecifier::= AxisName '::' | AbbreviatedAxisSpecifier
* [6] AxisName::= 'ancestor' | 'ancestor-or-self' | 'attribute' | 'child'
* | 'descendant' | 'descendant-or-self' | 'following'
* | 'following-sibling' | 'namespace' | 'parent' |
* | 'preceding' | 'preceding-sibling' | 'self'
* [13] AbbreviatedAxisSpecifier::= '@'?
*/
function axisSpecifier(stream, a) {
var attr = stream.trypop('@');
if (null != attr) return 'attribute';
var axisName = stream.trypopaxisname();
if (null != axisName) {
var coloncolon = stream.trypop('::');
if (null == coloncolon)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Should not happen. Should be ::.');
return axisName;
}
}
/** [7] NodeTest::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
* [38] NodeType::= 'comment' | 'text' | 'processing-instruction' | 'node'
* I've split nodeTypeTest from nodeNameTest for convenience.
*/
function nodeTypeTest(stream, a) {
if ('(' !== stream.peek2()) {
return null;
}
var type = stream.trypop(['comment', 'text', 'processing-instruction', 'node']);
if (null != type) {
if (null == stream.trypop('('))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Should not happen.');
var param = undefined;
if (type == 'processing-instruction') {
param = stream.trypopliteral();
}
if (null == stream.trypop(')'))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected close parens.');
return type
}
}
function nodeNameTest(stream, a) {
var name = stream.trypopnametest();
if (name != null) return name;
else return null;
}
/** [8] Predicate::= '[' PredicateExpr ']'
* [9] PredicateExpr::= Expr
*/
function predicate(lhs, stream, a) {
if (null == stream.trypop('[')) return null;
var expr = orExpr(stream, a);
if (null == expr)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected expression after [');
if (null == stream.trypop(']'))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected ] after expression.');
return a.node('Predicate', lhs, expr);
}
/** [14] Expr::= OrExpr
*/
/** [15] PrimaryExpr::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
* e.g. $x, (3+4), "hi", 32, f(x)
*/
function primaryExpr(stream, a) {
var x = stream.trypopliteral();
if (null == x)
x = stream.trypopnumber();
if (null != x) {
return x;
}
var varRef = stream.trypopvarref();
if (null != varRef) return a.node('VariableReference', varRef);
var funCall = functionCall(stream, a);
if (null != funCall) {
return funCall;
}
if (stream.trypop('(')) {
var e = orExpr(stream, a);
if (null == e)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected expression after (.');
if (null == stream.trypop(')'))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected ) after expression.');
return e;
}
return null;
}
/** [16] FunctionCall::= FunctionName '(' ( Argument ( ',' Argument )* )? ')'
* [17] Argument::= Expr
*/
function functionCall(stream, a) {
var name = stream.trypopfuncname(stream, a);
if (null == name) return null;
if (null == stream.trypop('('))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected ( ) after function name.');
var params = [];
var first = true;
while (null == stream.trypop(')')) {
if (!first && null == stream.trypop(','))
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected , between arguments of the function.');
first = false;
var param = orExpr(stream, a);
if (param == null)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected expression as argument of function.');
params.push(param);
}
return a.node('FunctionCall', name, params);
}
/** [18] UnionExpr::= PathExpr | UnionExpr '|' PathExpr
*/
function unionExpr(stream, a) { return binaryL(pathExpr, stream, a, '|'); }
/** [19] PathExpr ::= LocationPath
* | FilterExpr
* | FilterExpr '/' RelativeLocationPath
* | FilterExpr '//' RelativeLocationPath
* Unlike most other nodes, this one always generates a node because
* at this point all reverse nodesets must turn into a forward nodeset
*/
function pathExpr(stream, a) {
// We have to do FilterExpr before LocationPath because otherwise
// LocationPath will eat up the name from a function call.
var filter = filterExpr(stream, a);
if (null == filter) {
var loc = locationPath(stream, a);
if (null == loc) {
throw new Error
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': The expression shouldn\'t be empty...');
}
return a.node('PathExpr', loc);
}
var rel = relativeLocationPath(filter, stream, a, false);
if (filter === rel) return rel;
else return a.node('PathExpr', rel);
}
/** [20] FilterExpr::= PrimaryExpr | FilterExpr Predicate
* aka. FilterExpr ::= PrimaryExpr Predicate*
*/
function filterExpr(stream, a) {
var primary = primaryExpr(stream, a);
if (primary == null) return null;
var pred, lhs = primary;
while (null != (pred = predicate(lhs, stream, a))) {
lhs = pred;
}
return lhs;
}
/** [21] OrExpr::= AndExpr | OrExpr 'or' AndExpr
*/
function orExpr(stream, a) {
var orig = (stream.peeked || '') + stream.str
var r = binaryL(andExpr, stream, a, 'or');
var now = (stream.peeked || '') + stream.str;
return r;
}
/** [22] AndExpr::= EqualityExpr | AndExpr 'and' EqualityExpr
*/
function andExpr(stream, a) { return binaryL(equalityExpr, stream, a, 'and'); }
/** [23] EqualityExpr::= RelationalExpr | EqualityExpr '=' RelationalExpr
* | EqualityExpr '!=' RelationalExpr
*/
function equalityExpr(stream, a) { return binaryL(relationalExpr, stream, a, ['=','!=']); }
/** [24] RelationalExpr::= AdditiveExpr | RelationalExpr '<' AdditiveExpr
* | RelationalExpr '>' AdditiveExpr
* | RelationalExpr '<=' AdditiveExpr
* | RelationalExpr '>=' AdditiveExpr
*/
function relationalExpr(stream, a) { return binaryL(additiveExpr, stream, a, ['<','>','<=','>=']); }
/** [25] AdditiveExpr::= MultiplicativeExpr
* | AdditiveExpr '+' MultiplicativeExpr
* | AdditiveExpr '-' MultiplicativeExpr
*/
function additiveExpr(stream, a) { return binaryL(multiplicativeExpr, stream, a, ['+','-']); }
/** [26] MultiplicativeExpr::= UnaryExpr
* | MultiplicativeExpr MultiplyOperator UnaryExpr
* | MultiplicativeExpr 'div' UnaryExpr
* | MultiplicativeExpr 'mod' UnaryExpr
*/
function multiplicativeExpr(stream, a) { return binaryL(unaryExpr, stream, a, ['*','div','mod']); }
/** [27] UnaryExpr::= UnionExpr | '-' UnaryExpr
*/
function unaryExpr(stream, a) {
if (stream.trypop('-')) {
var e = unaryExpr(stream, a);
if (null == e)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Expected unary expression after -');
return a.node('UnaryMinus', e);
}
else return unionExpr(stream, a);
}
var astFactory = {
node: function() {return Array.prototype.slice.call(arguments);}
};
/***************************************************************************
* Optimizations (TODO) *
***************************************************************************/
/**
* Some things I've been considering:
* 1) a//b becomes a/descendant::b if there's no predicate that uses
* position() or last()
* 2) axis[pred]: when pred doesn't use position, evaluate it just once per
* node in the node-set rather than once per (node, position, last).
* For more optimizations, look up Gecko's optimizer:
* http://mxr.mozilla.org/mozilla-central/source/content/xslt/src/xpath/txXPathOptimizer.cpp
*/
// TODO
function optimize(ast) {
}
/***************************************************************************
* Evaluation: axes *
***************************************************************************/
/**
* Data types: For string, number, boolean, we just use Javascript types.
* Node-sets have the form
* {nodes: [node, ...]}
* or {nodes: [node, ...], pos: [[1], [2], ...], lasts: [[1], [2], ...]}
*
* Most of the time, only the node is used and the position information is
* discarded. But if you use a predicate, we need to try every value of
* position and last in case the predicate calls position() or last().
*/
/**
* The NodeMultiSet is a helper class to help generate
* {nodes:[], pos:[], lasts:[]} structures. It is useful for the
* descendant, descendant-or-self, following-sibling, and
* preceding-sibling axes for which we can use a stack to organize things.
*/
function NodeMultiSet(isReverseAxis) {
this.nodes = [];
this.pos = [];
this.lasts = [];
this.nextPos = [];
this.seriesIndexes = []; // index within nodes that each series begins.
this.isReverseAxis = isReverseAxis;
this._pushToNodes = isReverseAxis ? Array.prototype.unshift : Array.prototype.push;
}
NodeMultiSet.prototype = {
pushSeries: function pushSeries() {
this.nextPos.push(1);
this.seriesIndexes.push(this.nodes.length);
},
popSeries: function popSeries() {
console.assert(0 < this.nextPos.length, this.nextPos);
var last = this.nextPos.pop() - 1,
indexInPos = this.nextPos.length,
seriesBeginIndex = this.seriesIndexes.pop(),
seriesEndIndex = this.nodes.length;
for (var i = seriesBeginIndex; i < seriesEndIndex; ++i) {
console.assert(indexInPos < this.lasts[i].length);
console.assert(undefined === this.lasts[i][indexInPos]);
this.lasts[i][indexInPos] = last;
}
},
finalize: function() {
if (null == this.nextPos) return this;
console.assert(0 === this.nextPos.length);
for (var i = 0; i < this.lasts.length; ++i) {
for (var j = 0; j < this.lasts[i].length; ++j) {
console.assert(null != this.lasts[i][j], i + ',' + j + ':' + JSON.stringify(this.lasts));
}
}
this.pushSeries = this.popSeries = this.addNode = function() {
throw new Error('Already finalized.');
};
return this;
},
addNode: function addNode(node) {
console.assert(node);
this._pushToNodes.call(this.nodes, node)
this._pushToNodes.call(this.pos, this.nextPos.slice());
this._pushToNodes.call(this.lasts, new Array(this.nextPos.length));
for (var i = 0; i < this.nextPos.length; ++i) this.nextPos[i]++;
},
simplify: function() {
this.finalize();
return {nodes:this.nodes, pos:this.pos, lasts:this.lasts};
}
};
function eachContext(nodeMultiSet) {
var r = [];
for (var i = 0; i < nodeMultiSet.nodes.length; i++) {
var node = nodeMultiSet.nodes[i];
if (!nodeMultiSet.pos) {
r.push({nodes:[node], pos: [[i + 1]], lasts: [[nodeMultiSet.nodes.length]]});
} else {
for (var j = 0; j < nodeMultiSet.pos[i].length; ++j) {
r.push({nodes:[node], pos: [[nodeMultiSet.pos[i][j]]], lasts: [[nodeMultiSet.lasts[i][j]]]});
}
}
}
return r;
}
/** Matcher used in the axes.
*/
function NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase) {
this.nodeTypeNum = nodeTypeNum;
this.nodeName = nodeName;
this.shouldLowerCase = shouldLowerCase;
this.nodeNameTest =
null == nodeName ? this._alwaysTrue :
shouldLowerCase ? this._nodeNameLowerCaseEquals :
this._nodeNameEquals;
}
NodeMatcher.prototype = {
matches: function matches(node) {
return (0 === this.nodeTypeNum || node.nodeType === this.nodeTypeNum) &&
this.nodeNameTest(node.nodeName);
},
_alwaysTrue: function(name) {return true;},
_nodeNameEquals: function _nodeNameEquals(name) {
return this.nodeName === name;
},
_nodeNameLowerCaseEquals: function _nodeNameLowerCaseEquals(name) {
return this.nodeName === name.toLowerCase();
}
};
function followingSiblingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, shift, peek, followingNode, andSelf, isReverseAxis) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodeMultiSet = new NodeMultiSet(isReverseAxis);
while (0 < nodeList.length) { // can be if for following, preceding
var node = shift.call(nodeList);
console.assert(node != null);
node = followingNode(node);
nodeMultiSet.pushSeries();
var numPushed = 1;
while (null != node) {
if (! andSelf && matcher.matches(node))
nodeMultiSet.addNode(node);
if (node === peek.call(nodeList)) {
shift.call(nodeList);
nodeMultiSet.pushSeries();
numPushed++;
}
if (andSelf && matcher.matches(node))
nodeMultiSet.addNode(node);
node = followingNode(node);
}
while (0 < numPushed--)
nodeMultiSet.popSeries();
}
return nodeMultiSet;
}
/** Returns the next non-descendant node in document order.
* This is the first node in following::node(), if node is the context.
*/
function followingNonDescendantNode(node) {
if (node.ownerElement) {
if (node.ownerElement.firstChild)
return node.ownerElement.firstChild;
node = node.ownerElement;
}
do {
if (node.nextSibling) return node.nextSibling;
} while (node = node.parentNode);
return null;
}
/** Returns the next node in a document-order depth-first search.
* See the definition of document order[1]:
* 1) element
* 2) namespace nodes
* 3) attributes
* 4) children
* [1]: http://www.w3.org/TR/xpath/#dt-document-order
*/
function followingNode(node) {
if (node.ownerElement) // attributes: following node of element.
node = node.ownerElement;
if (null != node.firstChild)
return node.firstChild;
do {
if (null != node.nextSibling) {
return node.nextSibling;
}
node = node.parentNode;
} while (node);
return null;
}
/** Returns the previous node in document order (excluding attributes
* and namespace nodes).
*/
function precedingNode(node) {
if (node.ownerElement)
return node.ownerElement;
if (null != node.previousSibling) {
node = node.previousSibling;
while (null != node.lastChild) {
node = node.lastChild;
}
return node;
}
if (null != node.parentNode) {
return node.parentNode;
}
return null;
}
/** This axis is inefficient if there are many nodes in the nodeList.
* But I think it's a pretty useless axis so it's ok. */
function followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodeMultiSet = new NodeMultiSet(false);
var cursor = nodeList[0];
var unorderedFollowingStarts = [];
for (var i = 0; i < nodeList.length; i++) {
var node = nodeList[i];
var start = followingNonDescendantNode(node);
if (start)
unorderedFollowingStarts.push(start);
}
if (0 === unorderedFollowingStarts.length)
return {nodes:[]};
var pos = [], nextPos = [];
var started = 0;
while (cursor = followingNode(cursor)) {
for (var i = unorderedFollowingStarts.length - 1; i >= 0; i--){
if (cursor === unorderedFollowingStarts[i]) {
nodeMultiSet.pushSeries();
unorderedFollowingStarts.splice(i,i+1);
started++;
}
}
if (started && matcher.matches(cursor)) {
nodeMultiSet.addNode(cursor);
}
}
console.assert(0 === unorderedFollowingStarts.length);
for (var i = 0; i < started; i++)
nodeMultiSet.popSeries();
return nodeMultiSet.finalize();
}
function precedingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var cursor = nodeList.pop();
if (null == cursor) return {nodes:{}};
var r = {nodes:[], pos:[], lasts:[]};
var nextParents = [cursor.parentNode || cursor.ownerElement], nextPos = [1];
while (cursor = precedingNode(cursor)) {
if (cursor === nodeList[nodeList.length - 1]) {
nextParents.push(nodeList.pop());
nextPos.push(1);
}
var matches = matcher.matches(cursor);
var pos, someoneUsed = false;
if (matches)
pos = nextPos.slice();
for (var i = 0; i < nextParents.length; ++i) {
if (cursor === nextParents[i]) {
nextParents[i] = cursor.parentNode || cursor.ownerElement;
if (matches) {
pos[i] = null;
}
} else {
if (matches) {
pos[i] = nextPos[i]++;
someoneUsed = true;
}
}
}
if (someoneUsed) {
r.nodes.unshift(cursor);
r.pos.unshift(pos);
}
}
for (var i = 0; i < r.pos.length; ++i) {
var lasts = [];
r.lasts.push(lasts);
for (var j = r.pos[i].length - 1; j >= 0; j--) {
if (null == r.pos[i][j]) {
r.pos[i].splice(j, j+1);
} else {
lasts.unshift(nextPos[j] - 1);
}
}
}
return r;
}
/** node-set, axis -> node-set */
function descendantDfs(nodeMultiSet, node, remaining, matcher, andSelf, attrIndices, attrNodes) {
while (0 < remaining.length && null != remaining[0].ownerElement) {
var attr = remaining.shift();
if (andSelf && matcher.matches(attr)) {
attrNodes.push(attr);
attrIndices.push(nodeMultiSet.nodes.length);
}
}
if (null != node && !andSelf) {
if (matcher.matches(node))
nodeMultiSet.addNode(node);
}
var pushed = false;
if (null == node) {
if (0 === remaining.length) return;
node = remaining.shift();
nodeMultiSet.pushSeries();
pushed = true;
} else if (0 < remaining.length && node === remaining[0]) {
nodeMultiSet.pushSeries();
pushed = true;
remaining.shift();
}
if (andSelf) {
if (matcher.matches(node))
nodeMultiSet.addNode(node);
}
// TODO: use optimization. Also try element.getElementsByTagName
// var nodeList = 1 === nodeTypeNum && null != node.children ? node.children : node.childNodes;
var nodeList = node.childNodes;
for (var j = 0; j < nodeList.length; ++j) {
var child = nodeList[j];
descendantDfs(nodeMultiSet, child, remaining, matcher, andSelf, attrIndices, attrNodes);
}
if (pushed) {
nodeMultiSet.popSeries();
}
}
function descenantHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodeMultiSet = new NodeMultiSet(false);
var attrIndices = [], attrNodes = [];
while (0 < nodeList.length) {
// var node = nodeList.shift();
descendantDfs(nodeMultiSet, null, nodeList, matcher, andSelf, attrIndices, attrNodes);
}
nodeMultiSet.finalize();
for (var i = attrNodes.length-1; i >= 0; --i) {
nodeMultiSet.nodes.splice(attrIndices[i], attrIndices[i], attrNodes[i]);
nodeMultiSet.pos.splice(attrIndices[i], attrIndices[i], [1]);
nodeMultiSet.lasts.splice(attrIndices[i], attrIndices[i], [1]);
}
return nodeMultiSet;
}
/**
*/
function ancestorHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var ancestors = []; // array of non-empty arrays of matching ancestors
for (var i = 0; i < nodeList.length; ++i) {
var node = nodeList[i];
var isFirst = true;
var a = [];
while (null != node) {
if (!isFirst || andSelf) {
if (matcher.matches(node))
a.push(node);
}
isFirst = false;
node = node.parentNode || node.ownerElement;
}
if (0 < a.length)
ancestors.push(a);
}
var lasts = [];
for (var i = 0; i < ancestors.length; ++i) lasts.push(ancestors[i].length);
var nodeMultiSet = new NodeMultiSet(true);
var newCtx = {nodes:[], pos:[], lasts:[]};
while (0 < ancestors.length) {
var pos = [ancestors[0].length];
var last = [lasts[0]];
var node = ancestors[0].pop();
for (var i = ancestors.length - 1; i > 0; --i) {
if (node === ancestors[i][ancestors[i].length - 1]) {
pos.push(ancestors[i].length);
last.push(lasts[i]);
ancestors[i].pop();
if (0 === ancestors[i].length) {
ancestors.splice(i, i+1);
lasts.splice(i, i+1);
}
}
}
if (0 === ancestors[0].length) {
ancestors.shift();
lasts.shift();
}
newCtx.nodes.push(node);
newCtx.pos.push(pos);
newCtx.lasts.push(last);
}
return newCtx;
}
/** Helper function for sortDocumentOrder. Returns a list of indices, from the
* node to the root, of positions within parent.
* For convenience, the node is the first element of the array.
*/
function addressVector(node) {
var r = [node];
if (null != node.ownerElement) {
node = node.ownerElement;
r.push(-1);
}
while (null != node) {
var i = 0;
while (null != node.previousSibling) {
node = node.previousSibling;
i++;
}
r.push(i);
node = node.parentNode
}
return r;
}
function addressComparator(a, b) {
var minlen = Math.min(a.length - 1, b.length - 1), // not including [0]=node
alen = a.length,
blen = b.length;
if (a[0] === b[0]) return 0;
var c;
for (var i = 0; i < minlen; ++i) {
c = a[alen - i - 1] - b[blen - i - 1];
if (0 !== c)
break;
}
if (null == c || 0 === c) {
// All equal until one of the nodes. The longer one is the descendant.
c = alen - blen;
}
if (0 === c)
c = a.nodeName - b.nodeName;
if (0 === c)
c = 1;
return c;
}
var sortUniqDocumentOrder = xpath.sortUniqDocumentOrder = function(nodes) {
var a = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var v = addressVector(node);
a.push(v);
}
a.sort(addressComparator);
var b = [];
for (var i = 0; i < a.length; i++) {
if (0 < i && a[i][0] === a[i - 1][0])
continue;
b.push(a[i][0]);
}
return b;
}
/** Sort node multiset. Does not do any de-duping. */
function sortNodeMultiSet(nodeMultiSet) {
var a = [];
for (var i = 0; i < nodeMultiSet.nodes.length; i++) {
var v = addressVector(nodeMultiSet.nodes[i]);
a.push({v:v, n:nodeMultiSet.nodes[i],
p:nodeMultiSet.pos[i], l:nodeMultiSet.lasts[i]});
}
a.sort(compare);
var r = {nodes:[], pos:[], lasts:[]};
for (var i = 0; i < a.length; ++i) {
r.nodes.push(a[i].n);
r.pos.push(a[i].p);
r.lasts.push(a[i].l);
}
function compare(x, y) {
return addressComparator(x.v, y.v);
}
return r;
}
/** Returns an array containing all the ancestors down to a node.
* The array starts with document.
*/
function nodeAndAncestors(node) {
var ancestors = [node];
var p = node;
while (p = p.parentNode || p.ownerElement) {
ancestors.unshift(p);
}
return ancestors;
}
function compareSiblings(a, b) {
if (a === b) return 0;
var c = a;
while (c = c.previousSibling) {
if (c === b)
return 1; // b < a
}
c = b;
while (c = c.previousSibling) {
if (c === a)
return -1; // a < b
}
throw new Error('a and b are not siblings: ' + xpath.stringifyObject(a) + ' vs ' + xpath.stringifyObject(b));
}
/** The merge in merge-sort.*/
function mergeNodeLists(x, y) {
var a, b, aanc, banc, r = [];
if ('object' !== typeof x)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Invalid LHS for | operator ' +
'(expected node-set): ' + x);
if ('object' !== typeof y)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Invalid LHS for | operator ' +
'(expected node-set): ' + y);
while (true) {
if (null == a) {
a = x.shift();
if (null != a)
aanc = addressVector(a);
}
if (null == b) {
b = y.shift();
if (null != b)
banc = addressVector(b);
}
if (null == a || null == b) break;
var c = addressComparator(aanc, banc);
if (c < 0) {
r.push(a);
a = null;
aanc = null;
} else if (c > 0) {
r.push(b);
b = null;
banc = null;
} else if (a.nodeName < b.nodeName) { // attributes
r.push(a);
a = null;
aanc = null;
} else if (a.nodeName > b.nodeName) { // attributes
r.push(b);
b = null;
banc = null;
} else if (a !== b) {
// choose b arbitrarily
r.push(b);
b = null;
banc = null;
} else {
console.assert(a === b, c);
// just skip b without pushing it.
b = null;
banc = null;
}
}
while (a) {
r.push(a);
a = x.shift();
}
while (b) {
r.push(b);
b = y.shift();
}
return r;
}
function comparisonHelper(test, x, y, isNumericComparison) {
var coersion;
if (isNumericComparison)
coersion = fn.number;
else coersion =
'boolean' === typeof x || 'boolean' === typeof y ? fn['boolean'] :
'number' === typeof x || 'number' === typeof y ? fn.number :
fn.string;
if ('object' === typeof x && 'object' === typeof y) {
var aMap = {};
for (var i = 0; i < x.nodes.length; ++i) {
var xi = coersion({nodes:[x.nodes[i]]});
for (var j = 0; j < y.nodes.length; ++j) {
var yj = coersion({nodes:[y.nodes[j]]});
if (test(xi, yj)) return true;
}
}
return false;
} else if ('object' === typeof x && x.nodes && x.nodes.length) {
for (var i = 0; i < x.nodes.length; ++i) {
var xi = coersion({nodes:[x.nodes[i]]}), yc = coersion(y);
if (test(xi, yc))
return true;
}
return false;
} else if ('object' === typeof y && x.nodes && x.nodes.length) {
for (var i = 0; i < x.nodes.length; ++i) {
var yi = coersion({nodes:[y.nodes[i]]}), xc = coersion(x);
if (test(xc, yi))
return true;
}
return false;
} else {
var xc = coersion(x), yc = coersion(y);
return test(xc, yc);
}
}
var axes = xpath.axes = {
'ancestor':
function ancestor(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return ancestorHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false);
},
'ancestor-or-self':
function ancestorOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return ancestorHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true);
},
'attribute':
function attribute(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
// TODO: figure out whether positions should be undefined here.
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodeMultiSet = new NodeMultiSet(false);
if (null != nodeName) {
// TODO: with namespace
for (var i = 0; i < nodeList.length; ++i) {
var node = nodeList[i];
if (null == node.getAttributeNode)
continue; // only Element has .getAttributeNode
var attr = node.getAttributeNode(nodeName);
if (null != attr && matcher.matches(attr)) {
nodeMultiSet.pushSeries();
nodeMultiSet.addNode(attr);
nodeMultiSet.popSeries();
}
}
} else {
for (var i = 0; i < nodeList.length; ++i) {
var node = nodeList[i];
if (null != node.attributes) {
nodeMultiSet.pushSeries();
for (var j = 0; j < node.attributes.length; j++) { // all nodes have .attributes
var attr = node.attributes[j];
if (matcher.matches(attr)) // TODO: I think this check is unnecessary
nodeMultiSet.addNode(attr);
}
nodeMultiSet.popSeries();
}
}
}
return nodeMultiSet.finalize();
},
'child':
function child(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodeMultiSet = new NodeMultiSet(false);
for (var i = 0; i < nodeList.length; ++i) {
var n = nodeList[i];
if (n.ownerElement) // skip attribute nodes' text child.
continue;
if (n.childNodes) {
nodeMultiSet.pushSeries();
var childList = 1 === nodeTypeNum && null != n.children ?
n.children : n.childNodes;
for (var j = 0; j < childList.length; ++j) {
var child = childList[j];
if (matcher.matches(child)) {
nodeMultiSet.addNode(child);
}
// don't have to do de-duping because children have parent,
// which are current context.
}
nodeMultiSet.popSeries();
}
}
nodeMultiSet.finalize();
return sortNodeMultiSet(nodeMultiSet);
},
'descendant':
function descenant(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return descenantHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false);
},
'descendant-or-self':
function descenantOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return descenantHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true);
},
'following':
function following(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
},
'following-sibling':
function followingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return followingSiblingHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase,
Array.prototype.shift, function() {return this[0];},
function(node) {return node.nextSibling;});
},
'namespace':
function namespace(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
// TODO
},
'parent':
function parent(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
var nodes = [], pos = [];
for (var i = 0; i < nodeList.length; ++i) {
var parent = nodeList[i].parentNode || nodeList[i].ownerElement;
if (null == parent)
continue;
if (!matcher.matches(parent))
continue;
if (nodes.length > 0 && parent === nodes[nodes.length-1])
continue;
nodes.push(parent);
pos.push([1]);
}
return {nodes:nodes, pos:pos, lasts:pos};
},
'preceding':
function preceding(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return precedingHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
},
'preceding-sibling':
function precedingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
return followingSiblingHelper(
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase,
Array.prototype.pop, function() {return this[this.length-1];},
function(node) {return node.previousSibling},
false, true);
},
'self':
function self(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
var nodes = [], pos = [];
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
for (var i = 0; i < nodeList.length; ++i) {
if (matcher.matches(nodeList[i])) {
nodes.push(nodeList[i]);
pos.push([1]);
}
}
return {nodes: nodes, pos: pos, lasts: pos}
}
};
/***************************************************************************
* Evaluation: functions *
***************************************************************************/
var fn = {
'number': function number(optObject) {
if ('number' === typeof optObject)
return optObject;
if ('string' === typeof optObject)
return parseFloat(optObject); // note: parseFloat(' ') -> NaN, unlike +' ' -> 0.
if ('boolean' === typeof optObject)
return +optObject;
return fn.number(fn.string.call(this, optObject)); // for node-sets
},
'string': function string(optObject) {
if (null == optObject)
return fn.string(this);
if ('string' === typeof optObject || 'boolean' === typeof optObject ||
'number' === typeof optObject)
return '' + optObject;
if (0 == optObject.nodes.length) return '';
if (null != optObject.nodes[0].textContent)
return optObject.nodes[0].textContent;
return optObject.nodes[0].nodeValue;
},
'boolean': function booleanVal(x) {
return 'object' === typeof x ? x.nodes.length > 0 : !!x;
},
'last': function last() {
console.assert(Array.isArray(this.pos));
console.assert(Array.isArray(this.lasts));
console.assert(1 === this.pos.length);
console.assert(1 === this.lasts.length);
console.assert(1 === this.lasts[0].length);
return this.lasts[0][0];
},
'position': function position() {
console.assert(Array.isArray(this.pos));
console.assert(Array.isArray(this.lasts));
console.assert(1 === this.pos.length);
console.assert(1 === this.lasts.length);
console.assert(1 === this.pos[0].length);
return this.pos[0][0];
},
'count': function count(nodeSet) {
if ('object' !== typeof nodeSet)
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Position ' + stream.position() +
': Function count(node-set) ' +
'got wrong argument type: ' + nodeSet);
return nodeSet.nodes.length;
},
'id': function id(object) {
var r = {nodes: []};
var doc = this.nodes[0].ownerDocument || this.nodes[0];
console.assert(doc);
var ids;
if ('object' === typeof object) {
// for node-sets, map id over each node value.
ids = [];
for (var i = 0; i < object.nodes.length; ++i) {
var idNode = object.nodes[i];
var idsString = fn.string({nodes:[idNode]});
var a = idsString.split(/[ \t\r\n]+/g);
Array.prototype.push.apply(ids, a);
}
} else {
var idsString = fn.string(object);
var a = idsString.split(/[ \t\r\n]+/g);
ids = a;
}
for (var i = 0; i < ids.length; ++i) {
var id = ids[i];
if (0 === id.length)
continue;
var node = doc.getElementById(id);
if (null != node)
r.nodes.push(node);
}
r.nodes = sortUniqDocumentOrder(r.nodes);
return r;
},
'local-name': function(nodeSet) {
if (null == nodeSet)
return fn.name(this);
if (null == nodeSet.nodes) {
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'argument to name() must be a node-set. got ' + nodeSet);
}
// TODO: namespaced version
return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml
},
'namespace-uri': function(nodeSet) {
// TODO
throw new Error('not implemented yet');
},
'name': function(nodeSet) {
if (null == nodeSet)
return fn.name(this);
if (null == nodeSet.nodes) {
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'argument to name() must be a node-set. got ' + nodeSet);
}
return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml
},
'concat': function concat(x) {
var l = [];
for (var i = 0; i < arguments.length; ++i) {
l.push(fn.string(arguments[i]));
}
return l.join('');
},
'starts-with': function startsWith(a, b) {
var as = fn.string(a), bs = fn.string(b);
return as.substr(0, bs.length) === bs;
},
'contains': function contains(a, b) {
var as = fn.string(a), bs = fn.string(b);
var i = as.indexOf(bs);
if (-1 === i) return false;
return true;
},
'substring-before': function substringBefore(a, b) {
var as = fn.string(a), bs = fn.string(b);
var i = as.indexOf(bs);
if (-1 === i) return '';
return as.substr(0, i);
},
'substring-after': function substringBefore(a, b) {
var as = fn.string(a), bs = fn.string(b);
var i = as.indexOf(bs);
if (-1 === i) return '';
return as.substr(i + bs.length);
},
'substring': function substring(string, start, optEnd) {
if (null == string || null == start) {
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Must be at least 2 arguments to string()');
}
var sString = fn.string(string),
iStart = fn.round(start),
iEnd = optEnd == null ? null : fn.round(optEnd);
// Note that xpath string positions user 1-based index
if (iEnd == null)
return sString.substr(iStart - 1);
else
return sString.substr(iStart - 1, iEnd);
},
'string-length': function stringLength(optString) {
return fn.string.call(this, optString).length;
},
'normalize-space': function normalizeSpace(optString) {
var s = fn.string.call(this, optString);
return s.replace(/[ \t\r\n]+/g, ' ').replace(/^ | $/g, '');
},
'translate': function translate(string, from, to) {
var sString = fn.string.call(this, string),
sFrom = fn.string(from),
sTo = fn.string(to);
var eachCharRe = [];
var map = {};
for (var i = 0; i < sFrom.length; ++i) {
var c = sFrom.charAt(i);
map[c] = sTo.charAt(i); // returns '' if beyond length of sTo.
// copied from goog.string.regExpEscape in the Closure library.
eachCharRe.push(
c.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
replace(/\x08/g, '\\x08'));
}
var re = new RegExp(eachCharRe.join('|'), 'g');
return sString.replace(re, function(c) {return map[c];});
},
/// Boolean functions
'not': function not(x) {
var bx = fn['boolean'](x);
return !bx;
},
'true': function trueVal() { return true; },
'false': function falseVal() { return false; },
// TODO
'lang': function lang(string) { throw new Error('Not implemented');},
'sum': function sum(optNodeSet) {
if (null == optNodeSet) return fn.sum(this);
// for node-sets, map id over each node value.
var sum = 0;
for (var i = 0; i < optNodeSet.nodes.length; ++i) {
var node = optNodeSet.nodes[i];
var x = fn.number({nodes:[node]});
sum += x;
}
return sum;
},
'floor': function floor(number) {
return Math.floor(fn.number(number));
},
'ceiling': function ceiling(number) {
return Math.ceil(fn.number(number));
},
'round': function round(number) {
return Math.round(fn.number(number));
}
};
/***************************************************************************
* Evaluation: operators *
***************************************************************************/
var more = {
UnaryMinus: function(x) { return -fn.number(x); },
'+': function(x, y) { return fn.number(x) + fn.number(y); },
'-': function(x, y) { return fn.number(x) - fn.number(y); },
'*': function(x, y) { return fn.number(x) * fn.number(y); },
'div': function(x, y) { return fn.number(x) / fn.number(y); },
'mod': function(x, y) { return fn.number(x) % fn.number(y); },
'<': function(x, y) {
return comparisonHelper(function(x, y) { return fn.number(x) < fn.number(y);}, x, y, true);
},
'<=': function(x, y) {
return comparisonHelper(function(x, y) { return fn.number(x) <= fn.number(y);}, x, y, true);
},
'>': function(x, y) {
return comparisonHelper(function(x, y) { return fn.number(x) > fn.number(y);}, x, y, true);
},
'>=': function(x, y) {
return comparisonHelper(function(x, y) { return fn.number(x) >= fn.number(y);}, x, y, true);
},
'and': function(x, y) { return fn['boolean'](x) && fn['boolean'](y); },
'or': function(x, y) { return fn['boolean'](x) || fn['boolean'](y); },
'|': function(x, y) { return {nodes: mergeNodeLists(x.nodes, y.nodes)}; },
'=': function(x, y) {
// optimization for two node-sets case: avoid n^2 comparisons.
if ('object' === typeof x && 'object' === typeof y) {
var aMap = {};
for (var i = 0; i < x.nodes.length; ++i) {
var s = fn.string({nodes:[x.nodes[i]]});
aMap[s] = true;
}
for (var i = 0; i < y.nodes.length; ++i) {
var s = fn.string({nodes:[y.nodes[i]]});
if (aMap[s]) return true;
}
return false;
} else {
return comparisonHelper(function(x, y) {return x === y;}, x, y);
}
},
'!=': function(x, y) {
// optimization for two node-sets case: avoid n^2 comparisons.
if ('object' === typeof x && 'object' === typeof y) {
if (0 === x.nodes.length || 0 === y.nodes.length) return false;
var aMap = {};
for (var i = 0; i < x.nodes.length; ++i) {
var s = fn.string({nodes:[x.nodes[i]]});
aMap[s] = true;
}
for (var i = 0; i < y.nodes.length; ++i) {
var s = fn.string({nodes:[y.nodes[i]]});
if (!aMap[s]) return true;
}
return false;
} else {
return comparisonHelper(function(x, y) {return x !== y;}, x, y);
}
}
};
var nodeTypes = xpath.nodeTypes = {
'node': 0,
'attribute': 2,
'comment': 8, // this.doc.COMMENT_NODE,
'text': 3, // this.doc.TEXT_NODE,
'processing-instruction': 7, // this.doc.PROCESSING_INSTRUCTION_NODE,
'element': 1 //this.doc.ELEMENT_NODE
};
/** For debugging and unit tests: returnjs a stringified version of the
* argument. */
var stringifyObject = xpath.stringifyObject = function stringifyObject(ctx) {
var seenKey = 'seen' + Math.floor(Math.random()*1000000000);
return JSON.stringify(helper(ctx));
function helper(ctx) {
if (Array.isArray(ctx)) {
return ctx.map(function(x) {return helper(x);});
}
if ('object' !== typeof ctx) return ctx;
if (null == ctx) return ctx;
// if (ctx.toString) return ctx.toString();
if (null != ctx.outerHTML) return ctx.outerHTML;
if (null != ctx.nodeValue) return ctx.nodeName + '=' + ctx.nodeValue;
if (ctx[seenKey]) return '[circular]';
ctx[seenKey] = true;
var nicer = {};
for (var key in ctx) {
if (seenKey === key)
continue;
try {
nicer[key] = helper(ctx[key]);
} catch (e) {
nicer[key] = '[exception: ' + e.message + ']';
}
}
delete ctx[seenKey];
return nicer;
}
}
var Evaluator = xpath.Evaluator = function Evaluator(doc) {
this.doc = doc;
}
Evaluator.prototype = {
val: function val(ast, ctx) {
console.assert(ctx.nodes);
if ('number' === typeof ast || 'string' === typeof ast) return ast;
if (more[ast[0]]) {
var evaluatedParams = [];
for (var i = 1; i < ast.length; ++i) {
evaluatedParams.push(this.val(ast[i], ctx));
}
var r = more[ast[0]].apply(ctx, evaluatedParams);
return r;
}
switch (ast[0]) {
case 'Root': return {nodes: [this.doc]};
case 'FunctionCall':
var functionName = ast[1], functionParams = ast[2];
if (null == fn[functionName])
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
'Unknown function: ' + functionName);
var evaluatedParams = [];
for (var i = 0; i < functionParams.length; ++i) {
evaluatedParams.push(this.val(functionParams[i], ctx));
}
var r = fn[functionName].apply(ctx, evaluatedParams);
return r;
case 'Predicate':
var lhs = this.val(ast[1], ctx);
var ret = {nodes: []};
var contexts = eachContext(lhs);
for (var i = 0; i < contexts.length; ++i) {
var singleNodeSet = contexts[i];
var rhs = this.val(ast[2], singleNodeSet);
var success;
if ('number' === typeof rhs) {
success = rhs === singleNodeSet.pos[0][0];
} else {
success = fn['boolean'](rhs);
}
if (success) {
var node = singleNodeSet.nodes[0];
ret.nodes.push(node);
// skip over all the rest of the same node.
while (i+1 < contexts.length && node === contexts[i+1].nodes[0]) {
i++;
}
}
}
return ret;
case 'PathExpr':
// turn the path into an expressoin; i.e., remove the position
// information of the last axis.
var x = this.val(ast[1], ctx);
// Make the nodeset a forward-direction-only one.
if (x.finalize) { // it is a NodeMultiSet
for (var i = 0; i < x.nodes.length; ++i) {
console.assert(null != x.nodes[i].nodeType);
}
return {nodes: x.nodes};
} else {
return x;
}
case '/':
// TODO: don't generate '/' nodes, just Axis nodes.
var lhs = this.val(ast[1], ctx);
console.assert(null != lhs);
var r = this.val(ast[2], lhs);
console.assert(null != r);
return r;
case 'Axis':
// All the axis tests from Step. We only get AxisSpecifier NodeTest,
// not the predicate (which is applied later)
var axis = ast[1],
nodeType = ast[2],
nodeTypeNum = nodeTypes[nodeType],
shouldLowerCase = true, // TODO: give option
nodeName = ast[3] && shouldLowerCase ? ast[3].toLowerCase() : ast[3];
nodeName = nodeName === '*' ? null : nodeName;
if ('object' !== typeof ctx) return {nodes:[], pos:[]};
var nodeList = ctx.nodes.slice(); // TODO: is copy needed?
var r = axes[axis](nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
return r;
}
}
};
var evaluate = xpath.evaluate = function evaluate(expr, doc, context) {
//var astFactory = new AstEvaluatorFactory(doc, context);
var stream = new Stream(expr);
var ast = parse(stream, astFactory);
var val = new Evaluator(doc).val(ast, {nodes: [context]});
return val;
}
/***************************************************************************
* DOM interface *
***************************************************************************/
var XPathException = xpath.XPathException = function XPathException(code, message) {
var e = new Error(message);
e.name = 'XPathException';
e.code = code;
return e;
}
XPathException.INVALID_EXPRESSION_ERR = 51;
XPathException.TYPE_ERR = 52;
var XPathEvaluator = xpath.XPathEvaluator = function XPathEvaluator() {}
XPathEvaluator.prototype = {
createExpression: function(expression, resolver) {
return new XPathExpression(expression, resolver);
},
createNSResolver: function(nodeResolver) {
// TODO
},
evaluate: function evaluate(expression, contextNode, resolver, type, result) {
var expr = new XPathExpression(expression, resolver);
return expr.evaluate(contextNode, type, result);
}
};
var XPathExpression = xpath.XPathExpression = function XPathExpression(expression, resolver, optDoc) {
var stream = new Stream(expression);
this._ast = parse(stream, astFactory);
this._doc = optDoc;
}
XPathExpression.prototype = {
evaluate: function evaluate(contextNode, type, result) {
if (null == contextNode.nodeType)
throw new Error('bad argument (expected context node): ' + contextNode);
var doc = contextNode.ownerDocument || contextNode;
if (null != this._doc && this._doc !== doc) {
throw new core.DOMException(
core.WRONG_DOCUMENT_ERR,
'The document must be the same as the context node\'s document.');
}
var evaluator = new Evaluator(doc);
var value = evaluator.val(this._ast, {nodes: [contextNode]});
if (XPathResult.NUMBER_TYPE === type)
value = fn.number(value);
else if (XPathResult.STRING_TYPE === type)
value = fn.string(value);
else if (XPathResult.BOOLEAN_TYPE === type)
value = fn['boolean'](value);
else if (XPathResult.ANY_TYPE !== type &&
XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== type &&
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== type &&
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== type &&
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== type &&
XPathResult.ANY_UNORDERED_NODE_TYPE !== type &&
XPathResult.FIRST_ORDERED_NODE_TYPE !== type)
throw new core.DOMException(
core.NOT_SUPPORTED_ERR,
'You must provide an XPath result type (0=any).');
else if (XPathResult.ANY_TYPE !== type &&
'object' !== typeof value)
throw new XPathException(
XPathException.TYPE_ERR,
'Value should be a node-set: ' + value);
return new XPathResult(doc, value, type);
}
}
var XPathResult = xpath.XPathResult = function XPathResult(doc, value, resultType) {
this._value = value;
this._resultType = resultType;
this._i = 0;
this._invalidated = false;
if (this.resultType === XPathResult.UNORDERED_NODE_ITERATOR_TYPE ||
this.resultType === XPathResult.ORDERED_NODE_ITERATOR_TYPE) {
doc.addEventListener('DOMSubtreeModified', invalidate, true);
var self = this;
function invalidate() {
self._invalidated = true;
doc.removeEventListener('DOMSubtreeModified', invalidate, true);
}
}
}
XPathResult.ANY_TYPE = 0;
XPathResult.NUMBER_TYPE = 1;
XPathResult.STRING_TYPE = 2;
XPathResult.BOOLEAN_TYPE = 3;
XPathResult.UNORDERED_NODE_ITERATOR_TYPE = 4;
XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5;
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE = 6;
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7;
XPathResult.ANY_UNORDERED_NODE_TYPE = 8;
XPathResult.FIRST_ORDERED_NODE_TYPE = 9;
var proto = {
// XPathResultType
get resultType() {
if (this._resultType) return this._resultType;
switch (typeof this._value) {
case 'number': return XPathResult.NUMBER_TYPE;
case 'string': return XPathResult.STRING_TYPE;
case 'boolean': return XPathResult.BOOLEAN_TYPE;
default: return XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
}
},
get numberValue() {
if (XPathResult.NUMBER_TYPE !== this.resultType)
throw new XPathException(XPathException.TYPE_ERR,
'You should have asked for a NUMBER_TYPE.');
return this._value;
},
get stringValue() {
if (XPathResult.STRING_TYPE !== this.resultType)
throw new XPathException(XPathException.TYPE_ERR,
'You should have asked for a STRING_TYPE.');
return this._value;
},
get booleanValue() {
if (XPathResult.BOOLEAN_TYPE !== this.resultType)
throw new XPathException(XPathException.TYPE_ERR,
'You should have asked for a BOOLEAN_TYPE.');
return this._value;
},
get singleNodeValue() {
if (XPathResult.ANY_UNORDERED_NODE_TYPE !== this.resultType &&
XPathResult.FIRST_ORDERED_NODE_TYPE !== this.resultType)
throw new XPathException(
XPathException.TYPE_ERR,
'You should have asked for a FIRST_ORDERED_NODE_TYPE.');
return this._value.nodes[0] || null;
},
get invalidIteratorState() {
if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType &&
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType)
return false;
return !!this._invalidated;
},
get snapshotLength() {
if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType &&
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType)
throw new XPathException(
XPathException.TYPE_ERR,
'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.');
return this._value.nodes.length;
},
iterateNext: function iterateNext() {
if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType &&
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType)
throw new XPathException(
XPathException.TYPE_ERR,
'You should have asked for a ORDERED_NODE_ITERATOR_TYPE.');
if (this.invalidIteratorState)
throw new core.DOMException(
core.INVALID_STATE_ERR,
'The document has been mutated since the result was returned');
return this._value.nodes[this._i++] || null;
},
snapshotItem: function snapshotItem(index) {
if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType &&
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType)
throw new XPathException(
XPathException.TYPE_ERR,
'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.');
return this._value.nodes[index] || null;
}
};
// so you can access ANY_TYPE etc. from the instances:
XPathResult.prototype = Object.create(XPathResult,
Object.keys(proto).reduce(function (descriptors, name) {
descriptors[name] = Object.getOwnPropertyDescriptor(proto, name);
return descriptors;
}, {
constructor: {
value: XPathResult,
writable: true,
configurable: true
}
}));
core.XPathException = XPathException;
core.XPathExpression = XPathExpression;
core.XPathResult = XPathResult;
core.XPathEvaluator = XPathEvaluator;
core.Document.prototype.createExpression =
XPathEvaluator.prototype.createExpression;
core.Document.prototype.createNSResolver =
XPathEvaluator.prototype.createNSResolver;
core.Document.prototype.evaluate = XPathEvaluator.prototype.evaluate;
})();