diff options
Diffstat (limited to 'node_modules/jsdom/lib/jsdom/level3/xpath.js')
| -rw-r--r-- | node_modules/jsdom/lib/jsdom/level3/xpath.js | 1866 |
1 files changed, 0 insertions, 1866 deletions
diff --git a/node_modules/jsdom/lib/jsdom/level3/xpath.js b/node_modules/jsdom/lib/jsdom/level3/xpath.js deleted file mode 100644 index 98e3a7d..0000000 --- a/node_modules/jsdom/lib/jsdom/level3/xpath.js +++ /dev/null @@ -1,1866 +0,0 @@ -/** 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; - -})(); |
