summaryrefslogtreecommitdiff
path: root/tests/test_tools/selenium/core/xpath/xpath.js
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_tools/selenium/core/xpath/xpath.js')
-rw-r--r--tests/test_tools/selenium/core/xpath/xpath.js2182
1 files changed, 2182 insertions, 0 deletions
diff --git a/tests/test_tools/selenium/core/xpath/xpath.js b/tests/test_tools/selenium/core/xpath/xpath.js
new file mode 100644
index 00000000..b93469f2
--- /dev/null
+++ b/tests/test_tools/selenium/core/xpath/xpath.js
@@ -0,0 +1,2182 @@
+// Copyright 2005 Google Inc.
+// All Rights Reserved
+//
+// An XPath parser and evaluator written in JavaScript. The
+// implementation is complete except for functions handling
+// namespaces.
+//
+// Reference: [XPATH] XPath Specification
+// <http://www.w3.org/TR/1999/REC-xpath-19991116>.
+//
+//
+// The API of the parser has several parts:
+//
+// 1. The parser function xpathParse() that takes a string and returns
+// an expession object.
+//
+// 2. The expression object that has an evaluate() method to evaluate the
+// XPath expression it represents. (It is actually a hierarchy of
+// objects that resembles the parse tree, but an application will call
+// evaluate() only on the top node of this hierarchy.)
+//
+// 3. The context object that is passed as an argument to the evaluate()
+// method, which represents the DOM context in which the expression is
+// evaluated.
+//
+// 4. The value object that is returned from evaluate() and represents
+// values of the different types that are defined by XPath (number,
+// string, boolean, and node-set), and allows to convert between them.
+//
+// These parts are near the top of the file, the functions and data
+// that are used internally follow after them.
+//
+//
+// TODO(mesch): add jsdoc comments. Use more coherent naming.
+//
+//
+// Author: Steffen Meschkat <mesch@google.com>
+
+
+// The entry point for the parser.
+//
+// @param expr a string that contains an XPath expression.
+// @return an expression object that can be evaluated with an
+// expression context.
+
+function xpathParse(expr) {
+ if (xpathdebug) {
+ Log.write('XPath parse ' + expr);
+ }
+ xpathParseInit();
+
+ var cached = xpathCacheLookup(expr);
+ if (cached) {
+ if (xpathdebug) {
+ Log.write(' ... cached');
+ }
+ return cached;
+ }
+
+ // Optimize for a few common cases: simple attribute node tests
+ // (@id), simple element node tests (page), variable references
+ // ($address), numbers (4), multi-step path expressions where each
+ // step is a plain element node test
+ // (page/overlay/locations/location).
+
+ if (expr.match(/^(\$|@)?\w+$/i)) {
+ var ret = makeSimpleExpr(expr);
+ xpathParseCache[expr] = ret;
+ if (xpathdebug) {
+ Log.write(' ... simple');
+ }
+ return ret;
+ }
+
+ if (expr.match(/^\w+(\/\w+)*$/i)) {
+ var ret = makeSimpleExpr2(expr);
+ xpathParseCache[expr] = ret;
+ if (xpathdebug) {
+ Log.write(' ... simple 2');
+ }
+ return ret;
+ }
+
+ var cachekey = expr; // expr is modified during parse
+ if (xpathdebug) {
+ Timer.start('XPath parse', cachekey);
+ }
+
+ var stack = [];
+ var ahead = null;
+ var previous = null;
+ var done = false;
+
+ var parse_count = 0;
+ var lexer_count = 0;
+ var reduce_count = 0;
+
+ while (!done) {
+ parse_count++;
+ expr = expr.replace(/^\s*/, '');
+ previous = ahead;
+ ahead = null;
+
+ var rule = null;
+ var match = '';
+ for (var i = 0; i < xpathTokenRules.length; ++i) {
+ var result = xpathTokenRules[i].re.exec(expr);
+ lexer_count++;
+ if (result && result.length > 0 && result[0].length > match.length) {
+ rule = xpathTokenRules[i];
+ match = result[0];
+ break;
+ }
+ }
+
+ // Special case: allow operator keywords to be element and
+ // variable names.
+
+ // NOTE(mesch): The parser resolves conflicts by looking ahead,
+ // and this is the only case where we look back to
+ // disambiguate. So this is indeed something different, and
+ // looking back is usually done in the lexer (via states in the
+ // general case, called "start conditions" in flex(1)). Also,the
+ // conflict resolution in the parser is not as robust as it could
+ // be, so I'd like to keep as much off the parser as possible (all
+ // these precedence values should be computed from the grammar
+ // rules and possibly associativity declarations, as in bison(1),
+ // and not explicitly set.
+
+ if (rule &&
+ (rule == TOK_DIV ||
+ rule == TOK_MOD ||
+ rule == TOK_AND ||
+ rule == TOK_OR) &&
+ (!previous ||
+ previous.tag == TOK_AT ||
+ previous.tag == TOK_DSLASH ||
+ previous.tag == TOK_SLASH ||
+ previous.tag == TOK_AXIS ||
+ previous.tag == TOK_DOLLAR)) {
+ rule = TOK_QNAME;
+ }
+
+ if (rule) {
+ expr = expr.substr(match.length);
+ if (xpathdebug) {
+ Log.write('token: ' + match + ' -- ' + rule.label);
+ }
+ ahead = {
+ tag: rule,
+ match: match,
+ prec: rule.prec ? rule.prec : 0, // || 0 is removed by the compiler
+ expr: makeTokenExpr(match)
+ };
+
+ } else {
+ if (xpathdebug) {
+ Log.write('DONE');
+ }
+ done = true;
+ }
+
+ while (xpathReduce(stack, ahead)) {
+ reduce_count++;
+ if (xpathdebug) {
+ Log.write('stack: ' + stackToString(stack));
+ }
+ }
+ }
+
+ if (xpathdebug) {
+ Log.write(stackToString(stack));
+ }
+
+ if (stack.length != 1) {
+ throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack);
+ }
+
+ var result = stack[0].expr;
+ xpathParseCache[cachekey] = result;
+
+ if (xpathdebug) {
+ Timer.end('XPath parse', cachekey);
+ }
+
+ if (xpathdebug) {
+ Log.write('XPath parse: ' + parse_count + ' / ' +
+ lexer_count + ' / ' + reduce_count);
+ }
+
+ return result;
+}
+
+var xpathParseCache = {};
+
+function xpathCacheLookup(expr) {
+ return xpathParseCache[expr];
+}
+
+function xpathReduce(stack, ahead) {
+ var cand = null;
+
+ if (stack.length > 0) {
+ var top = stack[stack.length-1];
+ var ruleset = xpathRules[top.tag.key];
+
+ if (ruleset) {
+ for (var i = 0; i < ruleset.length; ++i) {
+ var rule = ruleset[i];
+ var match = xpathMatchStack(stack, rule[1]);
+ if (match.length) {
+ cand = {
+ tag: rule[0],
+ rule: rule,
+ match: match
+ };
+ cand.prec = xpathGrammarPrecedence(cand);
+ break;
+ }
+ }
+ }
+ }
+
+ var ret;
+ if (cand && (!ahead || cand.prec > ahead.prec ||
+ (ahead.tag.left && cand.prec >= ahead.prec))) {
+ for (var i = 0; i < cand.match.matchlength; ++i) {
+ stack.pop();
+ }
+
+ if (xpathdebug) {
+ Log.write('reduce ' + cand.tag.label + ' ' + cand.prec +
+ ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec +
+ (ahead.tag.left ? ' left' : '')
+ : ' none '));
+ }
+
+ var matchexpr = mapExpr(cand.match, function(m) { return m.expr; });
+ cand.expr = cand.rule[3].apply(null, matchexpr);
+
+ stack.push(cand);
+ ret = true;
+
+ } else {
+ if (ahead) {
+ if (xpathdebug) {
+ Log.write('shift ' + ahead.tag.label + ' ' + ahead.prec +
+ (ahead.tag.left ? ' left' : '') +
+ ' over ' + (cand ? cand.tag.label + ' ' +
+ cand.prec : ' none'));
+ }
+ stack.push(ahead);
+ }
+ ret = false;
+ }
+ return ret;
+}
+
+function xpathMatchStack(stack, pattern) {
+
+ // NOTE(mesch): The stack matches for variable cardinality are
+ // greedy but don't do backtracking. This would be an issue only
+ // with rules of the form A* A, i.e. with an element with variable
+ // cardinality followed by the same element. Since that doesn't
+ // occur in the grammar at hand, all matches on the stack are
+ // unambiguous.
+
+ var S = stack.length;
+ var P = pattern.length;
+ var p, s;
+ var match = [];
+ match.matchlength = 0;
+ var ds = 0;
+ for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) {
+ ds = 0;
+ var qmatch = [];
+ if (pattern[p] == Q_MM) {
+ p -= 1;
+ match.push(qmatch);
+ while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
+ qmatch.push(stack[s - ds]);
+ ds += 1;
+ match.matchlength += 1;
+ }
+
+ } else if (pattern[p] == Q_01) {
+ p -= 1;
+ match.push(qmatch);
+ while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) {
+ qmatch.push(stack[s - ds]);
+ ds += 1;
+ match.matchlength += 1;
+ }
+
+ } else if (pattern[p] == Q_1M) {
+ p -= 1;
+ match.push(qmatch);
+ if (stack[s].tag == pattern[p]) {
+ while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
+ qmatch.push(stack[s - ds]);
+ ds += 1;
+ match.matchlength += 1;
+ }
+ } else {
+ return [];
+ }
+
+ } else if (stack[s].tag == pattern[p]) {
+ match.push(stack[s]);
+ ds += 1;
+ match.matchlength += 1;
+
+ } else {
+ return [];
+ }
+
+ reverseInplace(qmatch);
+ qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; });
+ }
+
+ reverseInplace(match);
+
+ if (p == -1) {
+ return match;
+
+ } else {
+ return [];
+ }
+}
+
+function xpathTokenPrecedence(tag) {
+ return tag.prec || 2;
+}
+
+function xpathGrammarPrecedence(frame) {
+ var ret = 0;
+
+ if (frame.rule) { /* normal reduce */
+ if (frame.rule.length >= 3 && frame.rule[2] >= 0) {
+ ret = frame.rule[2];
+
+ } else {
+ for (var i = 0; i < frame.rule[1].length; ++i) {
+ var p = xpathTokenPrecedence(frame.rule[1][i]);
+ ret = Math.max(ret, p);
+ }
+ }
+ } else if (frame.tag) { /* TOKEN match */
+ ret = xpathTokenPrecedence(frame.tag);
+
+ } else if (frame.length) { /* Q_ match */
+ for (var j = 0; j < frame.length; ++j) {
+ var p = xpathGrammarPrecedence(frame[j]);
+ ret = Math.max(ret, p);
+ }
+ }
+
+ return ret;
+}
+
+function stackToString(stack) {
+ var ret = '';
+ for (var i = 0; i < stack.length; ++i) {
+ if (ret) {
+ ret += '\n';
+ }
+ ret += stack[i].tag.label;
+ }
+ return ret;
+}
+
+
+// XPath expression evaluation context. An XPath context consists of a
+// DOM node, a list of DOM nodes that contains this node, a number
+// that represents the position of the single node in the list, and a
+// current set of variable bindings. (See XPath spec.)
+//
+// The interface of the expression context:
+//
+// Constructor -- gets the node, its position, the node set it
+// belongs to, and a parent context as arguments. The parent context
+// is used to implement scoping rules for variables: if a variable
+// is not found in the current context, it is looked for in the
+// parent context, recursively. Except for node, all arguments have
+// default values: default position is 0, default node set is the
+// set that contains only the node, and the default parent is null.
+//
+// Notice that position starts at 0 at the outside interface;
+// inside XPath expressions this shows up as position()=1.
+//
+// clone() -- creates a new context with the current context as
+// parent. If passed as argument to clone(), the new context has a
+// different node, position, or node set. What is not passed is
+// inherited from the cloned context.
+//
+// setVariable(name, expr) -- binds given XPath expression to the
+// name.
+//
+// getVariable(name) -- what the name says.
+//
+// setNode(node, position) -- sets the context to the new node and
+// its corresponding position. Needed to implement scoping rules for
+// variables in XPath. (A variable is visible to all subsequent
+// siblings, not only to its children.)
+
+function ExprContext(node, position, nodelist, parent) {
+ this.node = node;
+ this.position = position || 0;
+ this.nodelist = nodelist || [ node ];
+ this.variables = {};
+ this.parent = parent || null;
+ this.root = parent ? parent.root : node.ownerDocument;
+}
+
+ExprContext.prototype.clone = function(node, position, nodelist) {
+ return new
+ ExprContext(node || this.node,
+ typeof position != 'undefined' ? position : this.position,
+ nodelist || this.nodelist, this);
+};
+
+ExprContext.prototype.setVariable = function(name, value) {
+ this.variables[name] = value;
+};
+
+ExprContext.prototype.getVariable = function(name) {
+ if (typeof this.variables[name] != 'undefined') {
+ return this.variables[name];
+
+ } else if (this.parent) {
+ return this.parent.getVariable(name);
+
+ } else {
+ return null;
+ }
+}
+
+ExprContext.prototype.setNode = function(node, position) {
+ this.node = node;
+ this.position = position;
+}
+
+
+// XPath expression values. They are what XPath expressions evaluate
+// to. Strangely, the different value types are not specified in the
+// XPath syntax, but only in the semantics, so they don't show up as
+// nonterminals in the grammar. Yet, some expressions are required to
+// evaluate to particular types, and not every type can be coerced
+// into every other type. Although the types of XPath values are
+// similar to the types present in JavaScript, the type coercion rules
+// are a bit peculiar, so we explicitly model XPath types instead of
+// mapping them onto JavaScript types. (See XPath spec.)
+//
+// The four types are:
+//
+// StringValue
+//
+// NumberValue
+//
+// BooleanValue
+//
+// NodeSetValue
+//
+// The common interface of the value classes consists of methods that
+// implement the XPath type coercion rules:
+//
+// stringValue() -- returns the value as a JavaScript String,
+//
+// numberValue() -- returns the value as a JavaScript Number,
+//
+// booleanValue() -- returns the value as a JavaScript Boolean,
+//
+// nodeSetValue() -- returns the value as a JavaScript Array of DOM
+// Node objects.
+//
+
+function StringValue(value) {
+ this.value = value;
+ this.type = 'string';
+}
+
+StringValue.prototype.stringValue = function() {
+ return this.value;
+}
+
+StringValue.prototype.booleanValue = function() {
+ return this.value.length > 0;
+}
+
+StringValue.prototype.numberValue = function() {
+ return this.value - 0;
+}
+
+StringValue.prototype.nodeSetValue = function() {
+ throw this + ' ' + Error().stack;
+}
+
+function BooleanValue(value) {
+ this.value = value;
+ this.type = 'boolean';
+}
+
+BooleanValue.prototype.stringValue = function() {
+ return '' + this.value;
+}
+
+BooleanValue.prototype.booleanValue = function() {
+ return this.value;
+}
+
+BooleanValue.prototype.numberValue = function() {
+ return this.value ? 1 : 0;
+}
+
+BooleanValue.prototype.nodeSetValue = function() {
+ throw this + ' ' + Error().stack;
+}
+
+function NumberValue(value) {
+ this.value = value;
+ this.type = 'number';
+}
+
+NumberValue.prototype.stringValue = function() {
+ return '' + this.value;
+}
+
+NumberValue.prototype.booleanValue = function() {
+ return !!this.value;
+}
+
+NumberValue.prototype.numberValue = function() {
+ return this.value - 0;
+}
+
+NumberValue.prototype.nodeSetValue = function() {
+ throw this + ' ' + Error().stack;
+}
+
+function NodeSetValue(value) {
+ this.value = value;
+ this.type = 'node-set';
+}
+
+NodeSetValue.prototype.stringValue = function() {
+ if (this.value.length == 0) {
+ return '';
+ } else {
+ return xmlValue(this.value[0]);
+ }
+}
+
+NodeSetValue.prototype.booleanValue = function() {
+ return this.value.length > 0;
+}
+
+NodeSetValue.prototype.numberValue = function() {
+ return this.stringValue() - 0;
+}
+
+NodeSetValue.prototype.nodeSetValue = function() {
+ return this.value;
+};
+
+// XPath expressions. They are used as nodes in the parse tree and
+// possess an evaluate() method to compute an XPath value given an XPath
+// context. Expressions are returned from the parser. Teh set of
+// expression classes closely mirrors the set of non terminal symbols
+// in the grammar. Every non trivial nonterminal symbol has a
+// corresponding expression class.
+//
+// The common expression interface consists of the following methods:
+//
+// evaluate(context) -- evaluates the expression, returns a value.
+//
+// toString() -- returns the XPath text representation of the
+// expression (defined in xsltdebug.js).
+//
+// parseTree(indent) -- returns a parse tree representation of the
+// expression (defined in xsltdebug.js).
+
+function TokenExpr(m) {
+ this.value = m;
+}
+
+TokenExpr.prototype.evaluate = function() {
+ return new StringValue(this.value);
+};
+
+function LocationExpr() {
+ this.absolute = false;
+ this.steps = [];
+}
+
+LocationExpr.prototype.appendStep = function(s) {
+ this.steps.push(s);
+}
+
+LocationExpr.prototype.prependStep = function(s) {
+ var steps0 = this.steps;
+ this.steps = [ s ];
+ for (var i = 0; i < steps0.length; ++i) {
+ this.steps.push(steps0[i]);
+ }
+};
+
+LocationExpr.prototype.evaluate = function(ctx) {
+ var start;
+ if (this.absolute) {
+ start = ctx.root;
+
+ } else {
+ start = ctx.node;
+ }
+
+ var nodes = [];
+ xPathStep(nodes, this.steps, 0, start, ctx);
+ return new NodeSetValue(nodes);
+};
+
+function xPathStep(nodes, steps, step, input, ctx) {
+ var s = steps[step];
+ var ctx2 = ctx.clone(input);
+ var nodelist = s.evaluate(ctx2).nodeSetValue();
+
+ for (var i = 0; i < nodelist.length; ++i) {
+ if (step == steps.length - 1) {
+ nodes.push(nodelist[i]);
+ } else {
+ xPathStep(nodes, steps, step + 1, nodelist[i], ctx);
+ }
+ }
+}
+
+function StepExpr(axis, nodetest, predicate) {
+ this.axis = axis;
+ this.nodetest = nodetest;
+ this.predicate = predicate || [];
+}
+
+StepExpr.prototype.appendPredicate = function(p) {
+ this.predicate.push(p);
+}
+
+StepExpr.prototype.evaluate = function(ctx) {
+ var input = ctx.node;
+ var nodelist = [];
+
+ // NOTE(mesch): When this was a switch() statement, it didn't work
+ // in Safari/2.0. Not sure why though; it resulted in the JavaScript
+ // console output "undefined" (without any line number or so).
+
+ if (this.axis == xpathAxis.ANCESTOR_OR_SELF) {
+ nodelist.push(input);
+ for (var n = input.parentNode; n; n = input.parentNode) {
+ nodelist.push(n);
+ }
+
+ } else if (this.axis == xpathAxis.ANCESTOR) {
+ for (var n = input.parentNode; n; n = input.parentNode) {
+ nodelist.push(n);
+ }
+
+ } else if (this.axis == xpathAxis.ATTRIBUTE) {
+ copyArray(nodelist, input.attributes);
+
+ } else if (this.axis == xpathAxis.CHILD) {
+ copyArray(nodelist, input.childNodes);
+
+ } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) {
+ nodelist.push(input);
+ xpathCollectDescendants(nodelist, input);
+
+ } else if (this.axis == xpathAxis.DESCENDANT) {
+ xpathCollectDescendants(nodelist, input);
+
+ } else if (this.axis == xpathAxis.FOLLOWING) {
+ for (var n = input.parentNode; n; n = n.parentNode) {
+ for (var nn = n.nextSibling; nn; nn = nn.nextSibling) {
+ nodelist.push(nn);
+ xpathCollectDescendants(nodelist, nn);
+ }
+ }
+
+ } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) {
+ for (var n = input.nextSibling; n; n = input.nextSibling) {
+ nodelist.push(n);
+ }
+
+ } else if (this.axis == xpathAxis.NAMESPACE) {
+ alert('not implemented: axis namespace');
+
+ } else if (this.axis == xpathAxis.PARENT) {
+ if (input.parentNode) {
+ nodelist.push(input.parentNode);
+ }
+
+ } else if (this.axis == xpathAxis.PRECEDING) {
+ for (var n = input.parentNode; n; n = n.parentNode) {
+ for (var nn = n.previousSibling; nn; nn = nn.previousSibling) {
+ nodelist.push(nn);
+ xpathCollectDescendantsReverse(nodelist, nn);
+ }
+ }
+
+ } else if (this.axis == xpathAxis.PRECEDING_SIBLING) {
+ for (var n = input.previousSibling; n; n = input.previousSibling) {
+ nodelist.push(n);
+ }
+
+ } else if (this.axis == xpathAxis.SELF) {
+ nodelist.push(input);
+
+ } else {
+ throw 'ERROR -- NO SUCH AXIS: ' + this.axis;
+ }
+
+ // process node test
+ var nodelist0 = nodelist;
+ nodelist = [];
+ for (var i = 0; i < nodelist0.length; ++i) {
+ var n = nodelist0[i];
+ if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) {
+ nodelist.push(n);
+ }
+ }
+
+ // process predicates
+ for (var i = 0; i < this.predicate.length; ++i) {
+ var nodelist0 = nodelist;
+ nodelist = [];
+ for (var ii = 0; ii < nodelist0.length; ++ii) {
+ var n = nodelist0[ii];
+ if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) {
+ nodelist.push(n);
+ }
+ }
+ }
+
+ return new NodeSetValue(nodelist);
+};
+
+function NodeTestAny() {
+ this.value = new BooleanValue(true);
+}
+
+NodeTestAny.prototype.evaluate = function(ctx) {
+ return this.value;
+};
+
+function NodeTestElement() {}
+
+NodeTestElement.prototype.evaluate = function(ctx) {
+ return new BooleanValue(ctx.node.nodeType == DOM_ELEMENT_NODE);
+}
+
+function NodeTestText() {}
+
+NodeTestText.prototype.evaluate = function(ctx) {
+ return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE);
+}
+
+function NodeTestComment() {}
+
+NodeTestComment.prototype.evaluate = function(ctx) {
+ return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE);
+}
+
+function NodeTestPI(target) {
+ this.target = target;
+}
+
+NodeTestPI.prototype.evaluate = function(ctx) {
+ return new
+ BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE &&
+ (!this.target || ctx.node.nodeName == this.target));
+}
+
+function NodeTestNC(nsprefix) {
+ this.regex = new RegExp("^" + nsprefix + ":");
+ this.nsprefix = nsprefix;
+}
+
+NodeTestNC.prototype.evaluate = function(ctx) {
+ var n = ctx.node;
+ return new BooleanValue(this.regex.match(n.nodeName));
+}
+
+function NodeTestName(name) {
+ this.name = name;
+}
+
+NodeTestName.prototype.evaluate = function(ctx) {
+ var n = ctx.node;
+ // NOTE (Patrick Lightbody): this change allows node selection to be case-insensitive
+ return new BooleanValue(n.nodeName.toUpperCase() == this.name.toUpperCase());
+}
+
+function PredicateExpr(expr) {
+ this.expr = expr;
+}
+
+PredicateExpr.prototype.evaluate = function(ctx) {
+ var v = this.expr.evaluate(ctx);
+ if (v.type == 'number') {
+ // NOTE(mesch): Internally, position is represented starting with
+ // 0, however in XPath position starts with 1. See functions
+ // position() and last().
+ return new BooleanValue(ctx.position == v.numberValue() - 1);
+ } else {
+ return new BooleanValue(v.booleanValue());
+ }
+};
+
+function FunctionCallExpr(name) {
+ this.name = name;
+ this.args = [];
+}
+
+FunctionCallExpr.prototype.appendArg = function(arg) {
+ this.args.push(arg);
+};
+
+FunctionCallExpr.prototype.evaluate = function(ctx) {
+ var fn = '' + this.name.value;
+ var f = this.xpathfunctions[fn];
+ if (f) {
+ return f.call(this, ctx);
+ } else {
+ Log.write('XPath NO SUCH FUNCTION ' + fn);
+ return new BooleanValue(false);
+ }
+};
+
+FunctionCallExpr.prototype.xpathfunctions = {
+ 'last': function(ctx) {
+ assert(this.args.length == 0);
+ // NOTE(mesch): XPath position starts at 1.
+ return new NumberValue(ctx.nodelist.length);
+ },
+
+ 'position': function(ctx) {
+ assert(this.args.length == 0);
+ // NOTE(mesch): XPath position starts at 1.
+ return new NumberValue(ctx.position + 1);
+ },
+
+ 'count': function(ctx) {
+ assert(this.args.length == 1);
+ var v = this.args[0].evaluate(ctx);
+ return new NumberValue(v.nodeSetValue().length);
+ },
+
+ 'id': function(ctx) {
+ assert(this.args.length == 1);
+ var e = this.args.evaluate(ctx);
+ var ret = [];
+ var ids;
+ if (e.type == 'node-set') {
+ ids = [];
+ for (var i = 0; i < e.length; ++i) {
+ var v = xmlValue(e[i]).split(/\s+/);
+ for (var ii = 0; ii < v.length; ++ii) {
+ ids.push(v[ii]);
+ }
+ }
+ } else {
+ ids = e.split(/\s+/);
+ }
+ var d = ctx.node.ownerDocument;
+ for (var i = 0; i < ids.length; ++i) {
+ var n = d.getElementById(ids[i]);
+ if (n) {
+ ret.push(n);
+ }
+ }
+ return new NodeSetValue(ret);
+ },
+
+ 'local-name': function(ctx) {
+ alert('not implmented yet: XPath function local-name()');
+ },
+
+ 'namespace-uri': function(ctx) {
+ alert('not implmented yet: XPath function namespace-uri()');
+ },
+
+ 'name': function(ctx) {
+ assert(this.args.length == 1 || this.args.length == 0);
+ var n;
+ if (this.args.length == 0) {
+ n = [ ctx.node ];
+ } else {
+ n = this.args[0].evaluate(ctx).nodeSetValue();
+ }
+
+ if (n.length == 0) {
+ return new StringValue('');
+ } else {
+ return new StringValue(n[0].nodeName);
+ }
+ },
+
+ 'string': function(ctx) {
+ assert(this.args.length == 1 || this.args.length == 0);
+ if (this.args.length == 0) {
+ return new StringValue(new NodeSetValue([ ctx.node ]).stringValue());
+ } else {
+ return new StringValue(this.args[0].evaluate(ctx).stringValue());
+ }
+ },
+
+ 'concat': function(ctx) {
+ var ret = '';
+ for (var i = 0; i < this.args.length; ++i) {
+ ret += this.args[i].evaluate(ctx).stringValue();
+ }
+ return new StringValue(ret);
+ },
+
+ 'starts-with': function(ctx) {
+ assert(this.args.length == 2);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).stringValue();
+ return new BooleanValue(s0.indexOf(s1) == 0);
+ },
+
+ 'contains': function(ctx) {
+ assert(this.args.length == 2);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).stringValue();
+ return new BooleanValue(s0.indexOf(s1) != -1);
+ },
+
+ 'substring-before': function(ctx) {
+ assert(this.args.length == 2);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).stringValue();
+ var i = s0.indexOf(s1);
+ var ret;
+ if (i == -1) {
+ ret = '';
+ } else {
+ ret = s0.substr(0,i);
+ }
+ return new StringValue(ret);
+ },
+
+ 'substring-after': function(ctx) {
+ assert(this.args.length == 2);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).stringValue();
+ var i = s0.indexOf(s1);
+ var ret;
+ if (i == -1) {
+ ret = '';
+ } else {
+ ret = s0.substr(i + s1.length);
+ }
+ return new StringValue(ret);
+ },
+
+ 'substring': function(ctx) {
+ // NOTE: XPath defines the position of the first character in a
+ // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2).
+ assert(this.args.length == 2 || this.args.length == 3);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).numberValue();
+ var ret;
+ if (this.args.length == 2) {
+ var i1 = Math.max(0, Math.round(s1) - 1);
+ ret = s0.substr(i1);
+
+ } else {
+ var s2 = this.args[2].evaluate(ctx).numberValue();
+ var i0 = Math.round(s1) - 1;
+ var i1 = Math.max(0, i0);
+ var i2 = Math.round(s2) - Math.max(0, -i0);
+ ret = s0.substr(i1, i2);
+ }
+ return new StringValue(ret);
+ },
+
+ 'string-length': function(ctx) {
+ var s;
+ if (this.args.length > 0) {
+ s = this.args[0].evaluate(ctx).stringValue();
+ } else {
+ s = new NodeSetValue([ ctx.node ]).stringValue();
+ }
+ return new NumberValue(s.length);
+ },
+
+ 'normalize-space': function(ctx) {
+ var s;
+ if (this.args.length > 0) {
+ s = this.args[0].evaluate(ctx).stringValue();
+ } else {
+ s = new NodeSetValue([ ctx.node ]).stringValue();
+ }
+ s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' ');
+ return new StringValue(s);
+ },
+
+ 'translate': function(ctx) {
+ assert(this.args.length == 3);
+ var s0 = this.args[0].evaluate(ctx).stringValue();
+ var s1 = this.args[1].evaluate(ctx).stringValue();
+ var s2 = this.args[2].evaluate(ctx).stringValue();
+
+ for (var i = 0; i < s1.length; ++i) {
+ s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i));
+ }
+ return new StringValue(s0);
+ },
+
+ 'boolean': function(ctx) {
+ assert(this.args.length == 1);
+ return new BooleanValue(this.args[0].evaluate(ctx).booleanValue());
+ },
+
+ 'not': function(ctx) {
+ assert(this.args.length == 1);
+ var ret = !this.args[0].evaluate(ctx).booleanValue();
+ return new BooleanValue(ret);
+ },
+
+ 'true': function(ctx) {
+ assert(this.args.length == 0);
+ return new BooleanValue(true);
+ },
+
+ 'false': function(ctx) {
+ assert(this.args.length == 0);
+ return new BooleanValue(false);
+ },
+
+ 'lang': function(ctx) {
+ assert(this.args.length == 1);
+ var lang = this.args[0].evaluate(ctx).stringValue();
+ var xmllang;
+ var n = ctx.node;
+ while (n && n != n.parentNode /* just in case ... */) {
+ xmllang = n.getAttribute('xml:lang');
+ if (xmllang) {
+ break;
+ }
+ n = n.parentNode;
+ }
+ if (!xmllang) {
+ return new BooleanValue(false);
+ } else {
+ var re = new RegExp('^' + lang + '$', 'i');
+ return new BooleanValue(xmllang.match(re) ||
+ xmllang.replace(/_.*$/,'').match(re));
+ }
+ },
+
+ 'number': function(ctx) {
+ assert(this.args.length == 1 || this.args.length == 0);
+
+ if (this.args.length == 1) {
+ return new NumberValue(this.args[0].evaluate(ctx).numberValue());
+ } else {
+ return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue());
+ }
+ },
+
+ 'sum': function(ctx) {
+ assert(this.args.length == 1);
+ var n = this.args[0].evaluate(ctx).nodeSetValue();
+ var sum = 0;
+ for (var i = 0; i < n.length; ++i) {
+ sum += xmlValue(n[i]) - 0;
+ }
+ return new NumberValue(sum);
+ },
+
+ 'floor': function(ctx) {
+ assert(this.args.length == 1);
+ var num = this.args[0].evaluate(ctx).numberValue();
+ return new NumberValue(Math.floor(num));
+ },
+
+ 'ceiling': function(ctx) {
+ assert(this.args.length == 1);
+ var num = this.args[0].evaluate(ctx).numberValue();
+ return new NumberValue(Math.ceil(num));
+ },
+
+ 'round': function(ctx) {
+ assert(this.args.length == 1);
+ var num = this.args[0].evaluate(ctx).numberValue();
+ return new NumberValue(Math.round(num));
+ },
+
+ // TODO(mesch): The following functions are custom. There is a
+ // standard that defines how to add functions, which should be
+ // applied here.
+
+ 'ext-join': function(ctx) {
+ assert(this.args.length == 2);
+ var nodes = this.args[0].evaluate(ctx).nodeSetValue();
+ var delim = this.args[1].evaluate(ctx).stringValue();
+ var ret = '';
+ for (var i = 0; i < nodes.length; ++i) {
+ if (ret) {
+ ret += delim;
+ }
+ ret += xmlValue(nodes[i]);
+ }
+ return new StringValue(ret);
+ },
+
+ // ext-if() evaluates and returns its second argument, if the
+ // boolean value of its first argument is true, otherwise it
+ // evaluates and returns its third argument.
+
+ 'ext-if': function(ctx) {
+ assert(this.args.length == 3);
+ if (this.args[0].evaluate(ctx).booleanValue()) {
+ return this.args[1].evaluate(ctx);
+ } else {
+ return this.args[2].evaluate(ctx);
+ }
+ },
+
+ 'ext-sprintf': function(ctx) {
+ assert(this.args.length >= 1);
+ var args = [];
+ for (var i = 0; i < this.args.length; ++i) {
+ args.push(this.args[i].evaluate(ctx).stringValue());
+ }
+ return new StringValue(sprintf.apply(null, args));
+ },
+
+ // ext-cardinal() evaluates its single argument as a number, and
+ // returns the current node that many times. It can be used in the
+ // select attribute to iterate over an integer range.
+
+ 'ext-cardinal': function(ctx) {
+ assert(this.args.length >= 1);
+ var c = this.args[0].evaluate(ctx).numberValue();
+ var ret = [];
+ for (var i = 0; i < c; ++i) {
+ ret.push(ctx.node);
+ }
+ return new NodeSetValue(ret);
+ }
+};
+
+function UnionExpr(expr1, expr2) {
+ this.expr1 = expr1;
+ this.expr2 = expr2;
+}
+
+UnionExpr.prototype.evaluate = function(ctx) {
+ var nodes1 = this.expr1.evaluate(ctx).nodeSetValue();
+ var nodes2 = this.expr2.evaluate(ctx).nodeSetValue();
+ var I1 = nodes1.length;
+ for (var i2 = 0; i2 < nodes2.length; ++i2) {
+ for (var i1 = 0; i1 < I1; ++i1) {
+ if (nodes1[i1] == nodes2[i2]) {
+ // break inner loop and continue outer loop, labels confuse
+ // the js compiler, so we don't use them here.
+ i1 = I1;
+ }
+ }
+ nodes1.push(nodes2[i2]);
+ }
+ return new NodeSetValue(nodes2);
+};
+
+function PathExpr(filter, rel) {
+ this.filter = filter;
+ this.rel = rel;
+}
+
+PathExpr.prototype.evaluate = function(ctx) {
+ var nodes = this.filter.evaluate(ctx).nodeSetValue();
+ var nodes1 = [];
+ for (var i = 0; i < nodes.length; ++i) {
+ var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue();
+ for (var ii = 0; ii < nodes0.length; ++ii) {
+ nodes1.push(nodes0[ii]);
+ }
+ }
+ return new NodeSetValue(nodes1);
+};
+
+function FilterExpr(expr, predicate) {
+ this.expr = expr;
+ this.predicate = predicate;
+}
+
+FilterExpr.prototype.evaluate = function(ctx) {
+ var nodes = this.expr.evaluate(ctx).nodeSetValue();
+ for (var i = 0; i < this.predicate.length; ++i) {
+ var nodes0 = nodes;
+ nodes = [];
+ for (var j = 0; j < nodes0.length; ++j) {
+ var n = nodes0[j];
+ if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) {
+ nodes.push(n);
+ }
+ }
+ }
+
+ return new NodeSetValue(nodes);
+}
+
+function UnaryMinusExpr(expr) {
+ this.expr = expr;
+}
+
+UnaryMinusExpr.prototype.evaluate = function(ctx) {
+ return new NumberValue(-this.expr.evaluate(ctx).numberValue());
+};
+
+function BinaryExpr(expr1, op, expr2) {
+ this.expr1 = expr1;
+ this.expr2 = expr2;
+ this.op = op;
+}
+
+BinaryExpr.prototype.evaluate = function(ctx) {
+ var ret;
+ switch (this.op.value) {
+ case 'or':
+ ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() ||
+ this.expr2.evaluate(ctx).booleanValue());
+ break;
+
+ case 'and':
+ ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() &&
+ this.expr2.evaluate(ctx).booleanValue());
+ break;
+
+ case '+':
+ ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() +
+ this.expr2.evaluate(ctx).numberValue());
+ break;
+
+ case '-':
+ ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() -
+ this.expr2.evaluate(ctx).numberValue());
+ break;
+
+ case '*':
+ ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() *
+ this.expr2.evaluate(ctx).numberValue());
+ break;
+
+ case 'mod':
+ ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() %
+ this.expr2.evaluate(ctx).numberValue());
+ break;
+
+ case 'div':
+ ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() /
+ this.expr2.evaluate(ctx).numberValue());
+ break;
+
+ case '=':
+ ret = this.compare(ctx, function(x1, x2) { return x1 == x2; });
+ break;
+
+ case '!=':
+ ret = this.compare(ctx, function(x1, x2) { return x1 != x2; });
+ break;
+
+ case '<':
+ ret = this.compare(ctx, function(x1, x2) { return x1 < x2; });
+ break;
+
+ case '<=':
+ ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; });
+ break;
+
+ case '>':
+ ret = this.compare(ctx, function(x1, x2) { return x1 > x2; });
+ break;
+
+ case '>=':
+ ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; });
+ break;
+
+ default:
+ alert('BinaryExpr.evaluate: ' + this.op.value);
+ }
+ return ret;
+};
+
+BinaryExpr.prototype.compare = function(ctx, cmp) {
+ var v1 = this.expr1.evaluate(ctx);
+ var v2 = this.expr2.evaluate(ctx);
+
+ var ret;
+ if (v1.type == 'node-set' && v2.type == 'node-set') {
+ var n1 = v1.nodeSetValue();
+ var n2 = v2.nodeSetValue();
+ ret = false;
+ for (var i1 = 0; i1 < n1.length; ++i1) {
+ for (var i2 = 0; i2 < n2.length; ++i2) {
+ if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) {
+ ret = true;
+ // Break outer loop. Labels confuse the jscompiler and we
+ // don't use them.
+ i2 = n2.length;
+ i1 = n1.length;
+ }
+ }
+ }
+
+ } else if (v1.type == 'node-set' || v2.type == 'node-set') {
+
+ if (v1.type == 'number') {
+ var s = v1.numberValue();
+ var n = v2.nodeSetValue();
+
+ ret = false;
+ for (var i = 0; i < n.length; ++i) {
+ var nn = xmlValue(n[i]) - 0;
+ if (cmp(s, nn)) {
+ ret = true;
+ break;
+ }
+ }
+
+ } else if (v2.type == 'number') {
+ var n = v1.nodeSetValue();
+ var s = v2.numberValue();
+
+ ret = false;
+ for (var i = 0; i < n.length; ++i) {
+ var nn = xmlValue(n[i]) - 0;
+ if (cmp(nn, s)) {
+ ret = true;
+ break;
+ }
+ }
+
+ } else if (v1.type == 'string') {
+ var s = v1.stringValue();
+ var n = v2.nodeSetValue();
+
+ ret = false;
+ for (var i = 0; i < n.length; ++i) {
+ var nn = xmlValue(n[i]);
+ if (cmp(s, nn)) {
+ ret = true;
+ break;
+ }
+ }
+
+ } else if (v2.type == 'string') {
+ var n = v1.nodeSetValue();
+ var s = v2.stringValue();
+
+ ret = false;
+ for (var i = 0; i < n.length; ++i) {
+ var nn = xmlValue(n[i]);
+ if (cmp(nn, s)) {
+ ret = true;
+ break;
+ }
+ }
+
+ } else {
+ ret = cmp(v1.booleanValue(), v2.booleanValue());
+ }
+
+ } else if (v1.type == 'boolean' || v2.type == 'boolean') {
+ ret = cmp(v1.booleanValue(), v2.booleanValue());
+
+ } else if (v1.type == 'number' || v2.type == 'number') {
+ ret = cmp(v1.numberValue(), v2.numberValue());
+
+ } else {
+ ret = cmp(v1.stringValue(), v2.stringValue());
+ }
+
+ return new BooleanValue(ret);
+}
+
+function LiteralExpr(value) {
+ this.value = value;
+}
+
+LiteralExpr.prototype.evaluate = function(ctx) {
+ return new StringValue(this.value);
+};
+
+function NumberExpr(value) {
+ this.value = value;
+}
+
+NumberExpr.prototype.evaluate = function(ctx) {
+ return new NumberValue(this.value);
+};
+
+function VariableExpr(name) {
+ this.name = name;
+}
+
+VariableExpr.prototype.evaluate = function(ctx) {
+ return ctx.getVariable(this.name);
+}
+
+// Factory functions for semantic values (i.e. Expressions) of the
+// productions in the grammar. When a production is matched to reduce
+// the current parse state stack, the function is called with the
+// semantic values of the matched elements as arguments, and returns
+// another semantic value. The semantic value is a node of the parse
+// tree, an expression object with an evaluate() method that evaluates the
+// expression in an actual context. These factory functions are used
+// in the specification of the grammar rules, below.
+
+function makeTokenExpr(m) {
+ return new TokenExpr(m);
+}
+
+function passExpr(e) {
+ return e;
+}
+
+function makeLocationExpr1(slash, rel) {
+ rel.absolute = true;
+ return rel;
+}
+
+function makeLocationExpr2(dslash, rel) {
+ rel.absolute = true;
+ rel.prependStep(makeAbbrevStep(dslash.value));
+ return rel;
+}
+
+function makeLocationExpr3(slash) {
+ var ret = new LocationExpr();
+ ret.appendStep(makeAbbrevStep('.'));
+ ret.absolute = true;
+ return ret;
+}
+
+function makeLocationExpr4(dslash) {
+ var ret = new LocationExpr();
+ ret.absolute = true;
+ ret.appendStep(makeAbbrevStep(dslash.value));
+ return ret;
+}
+
+function makeLocationExpr5(step) {
+ var ret = new LocationExpr();
+ ret.appendStep(step);
+ return ret;
+}
+
+function makeLocationExpr6(rel, slash, step) {
+ rel.appendStep(step);
+ return rel;
+}
+
+function makeLocationExpr7(rel, dslash, step) {
+ rel.appendStep(makeAbbrevStep(dslash.value));
+ return rel;
+}
+
+function makeStepExpr1(dot) {
+ return makeAbbrevStep(dot.value);
+}
+
+function makeStepExpr2(ddot) {
+ return makeAbbrevStep(ddot.value);
+}
+
+function makeStepExpr3(axisname, axis, nodetest) {
+ return new StepExpr(axisname.value, nodetest);
+}
+
+function makeStepExpr4(at, nodetest) {
+ return new StepExpr('attribute', nodetest);
+}
+
+function makeStepExpr5(nodetest) {
+ return new StepExpr('child', nodetest);
+}
+
+function makeStepExpr6(step, predicate) {
+ step.appendPredicate(predicate);
+ return step;
+}
+
+function makeAbbrevStep(abbrev) {
+ switch (abbrev) {
+ case '//':
+ return new StepExpr('descendant-or-self', new NodeTestAny);
+
+ case '.':
+ return new StepExpr('self', new NodeTestAny);
+
+ case '..':
+ return new StepExpr('parent', new NodeTestAny);
+ }
+}
+
+function makeNodeTestExpr1(asterisk) {
+ return new NodeTestElement;
+}
+
+function makeNodeTestExpr2(ncname, colon, asterisk) {
+ return new NodeTestNC(ncname.value);
+}
+
+function makeNodeTestExpr3(qname) {
+ return new NodeTestName(qname.value);
+}
+
+function makeNodeTestExpr4(typeo, parenc) {
+ var type = typeo.value.replace(/\s*\($/, '');
+ switch(type) {
+ case 'node':
+ return new NodeTestAny;
+
+ case 'text':
+ return new NodeTestText;
+
+ case 'comment':
+ return new NodeTestComment;
+
+ case 'processing-instruction':
+ return new NodeTestPI;
+ }
+}
+
+function makeNodeTestExpr5(typeo, target, parenc) {
+ var type = typeo.replace(/\s*\($/, '');
+ if (type != 'processing-instruction') {
+ throw type + ' ' + Error().stack;
+ }
+ return new NodeTestPI(target.value);
+}
+
+function makePredicateExpr(pareno, expr, parenc) {
+ return new PredicateExpr(expr);
+}
+
+function makePrimaryExpr(pareno, expr, parenc) {
+ return expr;
+}
+
+function makeFunctionCallExpr1(name, pareno, parenc) {
+ return new FunctionCallExpr(name);
+}
+
+function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) {
+ var ret = new FunctionCallExpr(name);
+ ret.appendArg(arg1);
+ for (var i = 0; i < args.length; ++i) {
+ ret.appendArg(args[i]);
+ }
+ return ret;
+}
+
+function makeArgumentExpr(comma, expr) {
+ return expr;
+}
+
+function makeUnionExpr(expr1, pipe, expr2) {
+ return new UnionExpr(expr1, expr2);
+}
+
+function makePathExpr1(filter, slash, rel) {
+ return new PathExpr(filter, rel);
+}
+
+function makePathExpr2(filter, dslash, rel) {
+ rel.prependStep(makeAbbrevStep(dslash.value));
+ return new PathExpr(filter, rel);
+}
+
+function makeFilterExpr(expr, predicates) {
+ if (predicates.length > 0) {
+ return new FilterExpr(expr, predicates);
+ } else {
+ return expr;
+ }
+}
+
+function makeUnaryMinusExpr(minus, expr) {
+ return new UnaryMinusExpr(expr);
+}
+
+function makeBinaryExpr(expr1, op, expr2) {
+ return new BinaryExpr(expr1, op, expr2);
+}
+
+function makeLiteralExpr(token) {
+ // remove quotes from the parsed value:
+ var value = token.value.substring(1, token.value.length - 1);
+ return new LiteralExpr(value);
+}
+
+function makeNumberExpr(token) {
+ return new NumberExpr(token.value);
+}
+
+function makeVariableReference(dollar, name) {
+ return new VariableExpr(name.value);
+}
+
+// Used before parsing for optimization of common simple cases. See
+// the begin of xpathParse() for which they are.
+function makeSimpleExpr(expr) {
+ if (expr.charAt(0) == '$') {
+ return new VariableExpr(expr.substr(1));
+ } else if (expr.charAt(0) == '@') {
+ var a = new NodeTestName(expr.substr(1));
+ var b = new StepExpr('attribute', a);
+ var c = new LocationExpr();
+ c.appendStep(b);
+ return c;
+ } else if (expr.match(/^[0-9]+$/)) {
+ return new NumberExpr(expr);
+ } else {
+ var a = new NodeTestName(expr);
+ var b = new StepExpr('child', a);
+ var c = new LocationExpr();
+ c.appendStep(b);
+ return c;
+ }
+}
+
+function makeSimpleExpr2(expr) {
+ var steps = expr.split('/');
+ var c = new LocationExpr();
+ for (var i in steps) {
+ var a = new NodeTestName(steps[i]);
+ var b = new StepExpr('child', a);
+ c.appendStep(b);
+ }
+ return c;
+}
+
+// The axes of XPath expressions.
+
+var xpathAxis = {
+ ANCESTOR_OR_SELF: 'ancestor-or-self',
+ ANCESTOR: 'ancestor',
+ ATTRIBUTE: 'attribute',
+ CHILD: 'child',
+ DESCENDANT_OR_SELF: 'descendant-or-self',
+ DESCENDANT: 'descendant',
+ FOLLOWING_SIBLING: 'following-sibling',
+ FOLLOWING: 'following',
+ NAMESPACE: 'namespace',
+ PARENT: 'parent',
+ PRECEDING_SIBLING: 'preceding-sibling',
+ PRECEDING: 'preceding',
+ SELF: 'self'
+};
+
+var xpathAxesRe = [
+ xpathAxis.ANCESTOR_OR_SELF,
+ xpathAxis.ANCESTOR,
+ xpathAxis.ATTRIBUTE,
+ xpathAxis.CHILD,
+ xpathAxis.DESCENDANT_OR_SELF,
+ xpathAxis.DESCENDANT,
+ xpathAxis.FOLLOWING_SIBLING,
+ xpathAxis.FOLLOWING,
+ xpathAxis.NAMESPACE,
+ xpathAxis.PARENT,
+ xpathAxis.PRECEDING_SIBLING,
+ xpathAxis.PRECEDING,
+ xpathAxis.SELF
+].join('|');
+
+
+// The tokens of the language. The label property is just used for
+// generating debug output. The prec property is the precedence used
+// for shift/reduce resolution. Default precedence is 0 as a lookahead
+// token and 2 on the stack. TODO(mesch): this is certainly not
+// necessary and too complicated. Simplify this!
+
+// NOTE: tabular formatting is the big exception, but here it should
+// be OK.
+
+var TOK_PIPE = { label: "|", prec: 17, re: new RegExp("^\\|") };
+var TOK_DSLASH = { label: "//", prec: 19, re: new RegExp("^//") };
+var TOK_SLASH = { label: "/", prec: 30, re: new RegExp("^/") };
+var TOK_AXIS = { label: "::", prec: 20, re: new RegExp("^::") };
+var TOK_COLON = { label: ":", prec: 1000, re: new RegExp("^:") };
+var TOK_AXISNAME = { label: "[axis]", re: new RegExp('^(' + xpathAxesRe + ')') };
+var TOK_PARENO = { label: "(", prec: 34, re: new RegExp("^\\(") };
+var TOK_PARENC = { label: ")", re: new RegExp("^\\)") };
+var TOK_DDOT = { label: "..", prec: 34, re: new RegExp("^\\.\\.") };
+var TOK_DOT = { label: ".", prec: 34, re: new RegExp("^\\.") };
+var TOK_AT = { label: "@", prec: 34, re: new RegExp("^@") };
+
+var TOK_COMMA = { label: ",", re: new RegExp("^,") };
+
+var TOK_OR = { label: "or", prec: 10, re: new RegExp("^or\\b") };
+var TOK_AND = { label: "and", prec: 11, re: new RegExp("^and\\b") };
+var TOK_EQ = { label: "=", prec: 12, re: new RegExp("^=") };
+var TOK_NEQ = { label: "!=", prec: 12, re: new RegExp("^!=") };
+var TOK_GE = { label: ">=", prec: 13, re: new RegExp("^>=") };
+var TOK_GT = { label: ">", prec: 13, re: new RegExp("^>") };
+var TOK_LE = { label: "<=", prec: 13, re: new RegExp("^<=") };
+var TOK_LT = { label: "<", prec: 13, re: new RegExp("^<") };
+var TOK_PLUS = { label: "+", prec: 14, re: new RegExp("^\\+"), left: true };
+var TOK_MINUS = { label: "-", prec: 14, re: new RegExp("^\\-"), left: true };
+var TOK_DIV = { label: "div", prec: 15, re: new RegExp("^div\\b"), left: true };
+var TOK_MOD = { label: "mod", prec: 15, re: new RegExp("^mod\\b"), left: true };
+
+var TOK_BRACKO = { label: "[", prec: 32, re: new RegExp("^\\[") };
+var TOK_BRACKC = { label: "]", re: new RegExp("^\\]") };
+var TOK_DOLLAR = { label: "$", re: new RegExp("^\\$") };
+
+var TOK_NCNAME = { label: "[ncname]", re: new RegExp('^[a-z][-\\w]*','i') };
+
+var TOK_ASTERISK = { label: "*", prec: 15, re: new RegExp("^\\*"), left: true };
+var TOK_LITERALQ = { label: "[litq]", prec: 20, re: new RegExp("^'[^\\']*'") };
+var TOK_LITERALQQ = {
+ label: "[litqq]",
+ prec: 20,
+ re: new RegExp('^"[^\\"]*"')
+};
+
+var TOK_NUMBER = {
+ label: "[number]",
+ prec: 35,
+ re: new RegExp('^\\d+(\\.\\d*)?') };
+
+var TOK_QNAME = {
+ label: "[qname]",
+ re: new RegExp('^([a-z][-\\w]*:)?[a-z][-\\w]*','i')
+};
+
+var TOK_NODEO = {
+ label: "[nodetest-start]",
+ re: new RegExp('^(processing-instruction|comment|text|node)\\(')
+};
+
+// The table of the tokens of our grammar, used by the lexer: first
+// column the tag, second column a regexp to recognize it in the
+// input, third column the precedence of the token, fourth column a
+// factory function for the semantic value of the token.
+//
+// NOTE: order of this list is important, because the first match
+// counts. Cf. DDOT and DOT, and AXIS and COLON.
+
+var xpathTokenRules = [
+ TOK_DSLASH,
+ TOK_SLASH,
+ TOK_DDOT,
+ TOK_DOT,
+ TOK_AXIS,
+ TOK_COLON,
+ TOK_AXISNAME,
+ TOK_NODEO,
+ TOK_PARENO,
+ TOK_PARENC,
+ TOK_BRACKO,
+ TOK_BRACKC,
+ TOK_AT,
+ TOK_COMMA,
+ TOK_OR,
+ TOK_AND,
+ TOK_NEQ,
+ TOK_EQ,
+ TOK_GE,
+ TOK_GT,
+ TOK_LE,
+ TOK_LT,
+ TOK_PLUS,
+ TOK_MINUS,
+ TOK_ASTERISK,
+ TOK_PIPE,
+ TOK_MOD,
+ TOK_DIV,
+ TOK_LITERALQ,
+ TOK_LITERALQQ,
+ TOK_NUMBER,
+ TOK_QNAME,
+ TOK_NCNAME,
+ TOK_DOLLAR
+];
+
+// All the nonterminals of the grammar. The nonterminal objects are
+// identified by object identity; the labels are used in the debug
+// output only.
+var XPathLocationPath = { label: "LocationPath" };
+var XPathRelativeLocationPath = { label: "RelativeLocationPath" };
+var XPathAbsoluteLocationPath = { label: "AbsoluteLocationPath" };
+var XPathStep = { label: "Step" };
+var XPathNodeTest = { label: "NodeTest" };
+var XPathPredicate = { label: "Predicate" };
+var XPathLiteral = { label: "Literal" };
+var XPathExpr = { label: "Expr" };
+var XPathPrimaryExpr = { label: "PrimaryExpr" };
+var XPathVariableReference = { label: "Variablereference" };
+var XPathNumber = { label: "Number" };
+var XPathFunctionCall = { label: "FunctionCall" };
+var XPathArgumentRemainder = { label: "ArgumentRemainder" };
+var XPathPathExpr = { label: "PathExpr" };
+var XPathUnionExpr = { label: "UnionExpr" };
+var XPathFilterExpr = { label: "FilterExpr" };
+var XPathDigits = { label: "Digits" };
+
+var xpathNonTerminals = [
+ XPathLocationPath,
+ XPathRelativeLocationPath,
+ XPathAbsoluteLocationPath,
+ XPathStep,
+ XPathNodeTest,
+ XPathPredicate,
+ XPathLiteral,
+ XPathExpr,
+ XPathPrimaryExpr,
+ XPathVariableReference,
+ XPathNumber,
+ XPathFunctionCall,
+ XPathArgumentRemainder,
+ XPathPathExpr,
+ XPathUnionExpr,
+ XPathFilterExpr,
+ XPathDigits
+];
+
+// Quantifiers that are used in the productions of the grammar.
+var Q_01 = { label: "?" };
+var Q_MM = { label: "*" };
+var Q_1M = { label: "+" };
+
+// Tag for left associativity (right assoc is implied by undefined).
+var ASSOC_LEFT = true;
+
+// The productions of the grammar. Columns of the table:
+//
+// - target nonterminal,
+// - pattern,
+// - precedence,
+// - semantic value factory
+//
+// The semantic value factory is a function that receives parse tree
+// nodes from the stack frames of the matched symbols as arguments and
+// returns an a node of the parse tree. The node is stored in the top
+// stack frame along with the target object of the rule. The node in
+// the parse tree is an expression object that has an evaluate() method
+// and thus evaluates XPath expressions.
+//
+// The precedence is used to decide between reducing and shifting by
+// comparing the precendence of the rule that is candidate for
+// reducing with the precedence of the look ahead token. Precedence of
+// -1 means that the precedence of the tokens in the pattern is used
+// instead. TODO: It shouldn't be necessary to explicitly assign
+// precedences to rules.
+
+var xpathGrammarRules =
+ [
+ [ XPathLocationPath, [ XPathRelativeLocationPath ], 18,
+ passExpr ],
+ [ XPathLocationPath, [ XPathAbsoluteLocationPath ], 18,
+ passExpr ],
+
+ [ XPathAbsoluteLocationPath, [ TOK_SLASH, XPathRelativeLocationPath ], 18,
+ makeLocationExpr1 ],
+ [ XPathAbsoluteLocationPath, [ TOK_DSLASH, XPathRelativeLocationPath ], 18,
+ makeLocationExpr2 ],
+
+ [ XPathAbsoluteLocationPath, [ TOK_SLASH ], 0,
+ makeLocationExpr3 ],
+ [ XPathAbsoluteLocationPath, [ TOK_DSLASH ], 0,
+ makeLocationExpr4 ],
+
+ [ XPathRelativeLocationPath, [ XPathStep ], 31,
+ makeLocationExpr5 ],
+ [ XPathRelativeLocationPath,
+ [ XPathRelativeLocationPath, TOK_SLASH, XPathStep ], 31,
+ makeLocationExpr6 ],
+ [ XPathRelativeLocationPath,
+ [ XPathRelativeLocationPath, TOK_DSLASH, XPathStep ], 31,
+ makeLocationExpr7 ],
+
+ [ XPathStep, [ TOK_DOT ], 33,
+ makeStepExpr1 ],
+ [ XPathStep, [ TOK_DDOT ], 33,
+ makeStepExpr2 ],
+ [ XPathStep,
+ [ TOK_AXISNAME, TOK_AXIS, XPathNodeTest ], 33,
+ makeStepExpr3 ],
+ [ XPathStep, [ TOK_AT, XPathNodeTest ], 33,
+ makeStepExpr4 ],
+ [ XPathStep, [ XPathNodeTest ], 33,
+ makeStepExpr5 ],
+ [ XPathStep, [ XPathStep, XPathPredicate ], 33,
+ makeStepExpr6 ],
+
+ [ XPathNodeTest, [ TOK_ASTERISK ], 33,
+ makeNodeTestExpr1 ],
+ [ XPathNodeTest, [ TOK_NCNAME, TOK_COLON, TOK_ASTERISK ], 33,
+ makeNodeTestExpr2 ],
+ [ XPathNodeTest, [ TOK_QNAME ], 33,
+ makeNodeTestExpr3 ],
+ [ XPathNodeTest, [ TOK_NODEO, TOK_PARENC ], 33,
+ makeNodeTestExpr4 ],
+ [ XPathNodeTest, [ TOK_NODEO, XPathLiteral, TOK_PARENC ], 33,
+ makeNodeTestExpr5 ],
+
+ [ XPathPredicate, [ TOK_BRACKO, XPathExpr, TOK_BRACKC ], 33,
+ makePredicateExpr ],
+
+ [ XPathPrimaryExpr, [ XPathVariableReference ], 33,
+ passExpr ],
+ [ XPathPrimaryExpr, [ TOK_PARENO, XPathExpr, TOK_PARENC ], 33,
+ makePrimaryExpr ],
+ [ XPathPrimaryExpr, [ XPathLiteral ], 30,
+ passExpr ],
+ [ XPathPrimaryExpr, [ XPathNumber ], 30,
+ passExpr ],
+ [ XPathPrimaryExpr, [ XPathFunctionCall ], 30,
+ passExpr ],
+
+ [ XPathFunctionCall, [ TOK_QNAME, TOK_PARENO, TOK_PARENC ], -1,
+ makeFunctionCallExpr1 ],
+ [ XPathFunctionCall,
+ [ TOK_QNAME, TOK_PARENO, XPathExpr, XPathArgumentRemainder, Q_MM,
+ TOK_PARENC ], -1,
+ makeFunctionCallExpr2 ],
+ [ XPathArgumentRemainder, [ TOK_COMMA, XPathExpr ], -1,
+ makeArgumentExpr ],
+
+ [ XPathUnionExpr, [ XPathPathExpr ], 20,
+ passExpr ],
+ [ XPathUnionExpr, [ XPathUnionExpr, TOK_PIPE, XPathPathExpr ], 20,
+ makeUnionExpr ],
+
+ [ XPathPathExpr, [ XPathLocationPath ], 20,
+ passExpr ],
+ [ XPathPathExpr, [ XPathFilterExpr ], 19,
+ passExpr ],
+ [ XPathPathExpr,
+ [ XPathFilterExpr, TOK_SLASH, XPathRelativeLocationPath ], 20,
+ makePathExpr1 ],
+ [ XPathPathExpr,
+ [ XPathFilterExpr, TOK_DSLASH, XPathRelativeLocationPath ], 20,
+ makePathExpr2 ],
+
+ [ XPathFilterExpr, [ XPathPrimaryExpr, XPathPredicate, Q_MM ], 20,
+ makeFilterExpr ],
+
+ [ XPathExpr, [ XPathPrimaryExpr ], 16,
+ passExpr ],
+ [ XPathExpr, [ XPathUnionExpr ], 16,
+ passExpr ],
+
+ [ XPathExpr, [ TOK_MINUS, XPathExpr ], -1,
+ makeUnaryMinusExpr ],
+
+ [ XPathExpr, [ XPathExpr, TOK_OR, XPathExpr ], -1,
+ makeBinaryExpr ],
+ [ XPathExpr, [ XPathExpr, TOK_AND, XPathExpr ], -1,
+ makeBinaryExpr ],
+
+ [ XPathExpr, [ XPathExpr, TOK_EQ, XPathExpr ], -1,
+ makeBinaryExpr ],
+ [ XPathExpr, [ XPathExpr, TOK_NEQ, XPathExpr ], -1,
+ makeBinaryExpr ],
+
+ [ XPathExpr, [ XPathExpr, TOK_LT, XPathExpr ], -1,
+ makeBinaryExpr ],
+ [ XPathExpr, [ XPathExpr, TOK_LE, XPathExpr ], -1,
+ makeBinaryExpr ],
+ [ XPathExpr, [ XPathExpr, TOK_GT, XPathExpr ], -1,
+ makeBinaryExpr ],
+ [ XPathExpr, [ XPathExpr, TOK_GE, XPathExpr ], -1,
+ makeBinaryExpr ],
+
+ [ XPathExpr, [ XPathExpr, TOK_PLUS, XPathExpr ], -1,
+ makeBinaryExpr, ASSOC_LEFT ],
+ [ XPathExpr, [ XPathExpr, TOK_MINUS, XPathExpr ], -1,
+ makeBinaryExpr, ASSOC_LEFT ],
+
+ [ XPathExpr, [ XPathExpr, TOK_ASTERISK, XPathExpr ], -1,
+ makeBinaryExpr, ASSOC_LEFT ],
+ [ XPathExpr, [ XPathExpr, TOK_DIV, XPathExpr ], -1,
+ makeBinaryExpr, ASSOC_LEFT ],
+ [ XPathExpr, [ XPathExpr, TOK_MOD, XPathExpr ], -1,
+ makeBinaryExpr, ASSOC_LEFT ],
+
+ [ XPathLiteral, [ TOK_LITERALQ ], -1,
+ makeLiteralExpr ],
+ [ XPathLiteral, [ TOK_LITERALQQ ], -1,
+ makeLiteralExpr ],
+
+ [ XPathNumber, [ TOK_NUMBER ], -1,
+ makeNumberExpr ],
+
+ [ XPathVariableReference, [ TOK_DOLLAR, TOK_QNAME ], 200,
+ makeVariableReference ]
+ ];
+
+// That function computes some optimizations of the above data
+// structures and will be called right here. It merely takes the
+// counter variables out of the global scope.
+
+var xpathRules = [];
+
+function xpathParseInit() {
+ if (xpathRules.length) {
+ return;
+ }
+
+ // Some simple optimizations for the xpath expression parser: sort
+ // grammar rules descending by length, so that the longest match is
+ // first found.
+
+ xpathGrammarRules.sort(function(a,b) {
+ var la = a[1].length;
+ var lb = b[1].length;
+ if (la < lb) {
+ return 1;
+ } else if (la > lb) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+
+ var k = 1;
+ for (var i = 0; i < xpathNonTerminals.length; ++i) {
+ xpathNonTerminals[i].key = k++;
+ }
+
+ for (i = 0; i < xpathTokenRules.length; ++i) {
+ xpathTokenRules[i].key = k++;
+ }
+
+ Log.write('XPath parse INIT: ' + k + ' rules');
+
+ // Another slight optimization: sort the rules into bins according
+ // to the last element (observing quantifiers), so we can restrict
+ // the match against the stack to the subest of rules that match the
+ // top of the stack.
+ //
+ // TODO(mesch): What we actually want is to compute states as in
+ // bison, so that we don't have to do any explicit and iterated
+ // match against the stack.
+
+ function push_(array, position, element) {
+ if (!array[position]) {
+ array[position] = [];
+ }
+ array[position].push(element);
+ }
+
+ for (i = 0; i < xpathGrammarRules.length; ++i) {
+ var rule = xpathGrammarRules[i];
+ var pattern = rule[1];
+
+ for (var j = pattern.length - 1; j >= 0; --j) {
+ if (pattern[j] == Q_1M) {
+ push_(xpathRules, pattern[j-1].key, rule);
+ break;
+
+ } else if (pattern[j] == Q_MM || pattern[j] == Q_01) {
+ push_(xpathRules, pattern[j-1].key, rule);
+ --j;
+
+ } else {
+ push_(xpathRules, pattern[j].key, rule);
+ break;
+ }
+ }
+ }
+
+ Log.write('XPath parse INIT: ' + xpathRules.length + ' rule bins');
+
+ var sum = 0;
+ mapExec(xpathRules, function(i) {
+ if (i) {
+ sum += i.length;
+ }
+ });
+
+ Log.write('XPath parse INIT: ' + (sum / xpathRules.length) + ' average bin size');
+}
+
+// Local utility functions that are used by the lexer or parser.
+
+function xpathCollectDescendants(nodelist, node) {
+ for (var n = node.firstChild; n; n = n.nextSibling) {
+ nodelist.push(n);
+ arguments.callee(nodelist, n);
+ }
+}
+
+function xpathCollectDescendantsReverse(nodelist, node) {
+ for (var n = node.lastChild; n; n = n.previousSibling) {
+ nodelist.push(n);
+ arguments.callee(nodelist, n);
+ }
+}
+
+
+// The entry point for the library: match an expression against a DOM
+// node. Returns an XPath value.
+function xpathDomEval(expr, node) {
+ var expr1 = xpathParse(expr);
+ var ret = expr1.evaluate(new ExprContext(node));
+ return ret;
+}
+
+// Utility function to sort a list of nodes. Used by xsltSort() and
+// nxslSelect().
+function xpathSort(input, sort) {
+ if (sort.length == 0) {
+ return;
+ }
+
+ var sortlist = [];
+
+ for (var i = 0; i < input.nodelist.length; ++i) {
+ var node = input.nodelist[i];
+ var sortitem = { node: node, key: [] };
+ var context = input.clone(node, 0, [ node ]);
+
+ for (var j = 0; j < sort.length; ++j) {
+ var s = sort[j];
+ var value = s.expr.evaluate(context);
+
+ var evalue;
+ if (s.type == 'text') {
+ evalue = value.stringValue();
+ } else if (s.type == 'number') {
+ evalue = value.numberValue();
+ }
+ sortitem.key.push({ value: evalue, order: s.order });
+ }
+
+ // Make the sort stable by adding a lowest priority sort by
+ // id. This is very convenient and furthermore required by the
+ // spec ([XSLT] - Section 10 Sorting).
+ sortitem.key.push({ value: i, order: 'ascending' });
+
+ sortlist.push(sortitem);
+ }
+
+ sortlist.sort(xpathSortByKey);
+
+ var nodes = [];
+ for (var i = 0; i < sortlist.length; ++i) {
+ nodes.push(sortlist[i].node);
+ }
+ input.nodelist = nodes;
+ input.setNode(nodes[0], 0);
+}
+
+
+// Sorts by all order criteria defined. According to the JavaScript
+// spec ([ECMA] Section 11.8.5), the compare operators compare strings
+// as strings and numbers as numbers.
+//
+// NOTE: In browsers which do not follow the spec, this breaks only in
+// the case that numbers should be sorted as strings, which is very
+// uncommon.
+
+function xpathSortByKey(v1, v2) {
+ // NOTE: Sort key vectors of different length never occur in
+ // xsltSort.
+
+ for (var i = 0; i < v1.key.length; ++i) {
+ var o = v1.key[i].order == 'descending' ? -1 : 1;
+ if (v1.key[i].value > v2.key[i].value) {
+ return +1 * o;
+ } else if (v1.key[i].value < v2.key[i].value) {
+ return -1 * o;
+ }
+ }
+
+ return 0;
+}