diff options
Diffstat (limited to 'node_modules/jsdom/lib/jsdom/level3/xpath.js')
-rw-r--r-- | node_modules/jsdom/lib/jsdom/level3/xpath.js | 1874 |
1 files changed, 1874 insertions, 0 deletions
diff --git a/node_modules/jsdom/lib/jsdom/level3/xpath.js b/node_modules/jsdom/lib/jsdom/level3/xpath.js new file mode 100644 index 0000000..28648c4 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level3/xpath.js @@ -0,0 +1,1874 @@ +/** 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); + */ +module.exports = core => { + var xpath = {}; + + // Helper function to deal with the migration of Attr to no longer have a nodeName property despite this codebase + // assuming it does. + function getNodeName(nodeOrAttr) { + return nodeOrAttr.constructor.name === 'Attr' ? nodeOrAttr.name : nodeOrAttr.nodeName; + } + + /*************************************************************************** + * 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); + var lastsJSON = JSON.stringify(this.lasts); + 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 + ':' + lastsJSON); + } + } + 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) { + if (0 === this.nodeTypeNum || this._nodeTypeMatches(node)) { + return this.nodeNameTest(getNodeName(node)); + } + + return false; + }, + _nodeTypeMatches(nodeOrAttr) { + if (nodeOrAttr.constructor.name === 'Attr' && this.nodeTypeNum === 2) { + return true; + } + return nodeOrAttr.nodeType === this.nodeTypeNum; + }, + _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 = getNodeName(a) - getNodeName(b); + 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 (getNodeName(a) < getNodeName(b)) { // attributes + r.push(a); + a = null; + aanc = null; + } else if (getNodeName(a) > getNodeName(b)) { // 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].localName; + }, + '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].name; + }, + '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 + 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.DOMException.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.DOMException.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; + + // TODO: we removed mutation events but didn't take care of this. No tests fail, so that's nice, but eventually we + // should fix this, preferably by entirely replacing our XPath implementation. + // 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.DOMException.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; + + return xpath; // for tests +}; |