/testing/selenium-core/xpath/xpath.js

http://datanucleus-appengine.googlecode.com/ · JavaScript · 2450 lines · 1918 code · 283 blank · 249 comment · 400 complexity · 380ca59ea83ca2115ebf3101c89cd4b5 MD5 · raw file

Large files are truncated click here to view the full file

  1. // Copyright 2005 Google Inc.
  2. // All Rights Reserved
  3. //
  4. // An XPath parser and evaluator written in JavaScript. The
  5. // implementation is complete except for functions handling
  6. // namespaces.
  7. //
  8. // Reference: [XPATH] XPath Specification
  9. // <http://www.w3.org/TR/1999/REC-xpath-19991116>.
  10. //
  11. //
  12. // The API of the parser has several parts:
  13. //
  14. // 1. The parser function xpathParse() that takes a string and returns
  15. // an expession object.
  16. //
  17. // 2. The expression object that has an evaluate() method to evaluate the
  18. // XPath expression it represents. (It is actually a hierarchy of
  19. // objects that resembles the parse tree, but an application will call
  20. // evaluate() only on the top node of this hierarchy.)
  21. //
  22. // 3. The context object that is passed as an argument to the evaluate()
  23. // method, which represents the DOM context in which the expression is
  24. // evaluated.
  25. //
  26. // 4. The value object that is returned from evaluate() and represents
  27. // values of the different types that are defined by XPath (number,
  28. // string, boolean, and node-set), and allows to convert between them.
  29. //
  30. // These parts are near the top of the file, the functions and data
  31. // that are used internally follow after them.
  32. //
  33. //
  34. // Author: Steffen Meschkat <mesch@google.com>
  35. // The entry point for the parser.
  36. //
  37. // @param expr a string that contains an XPath expression.
  38. // @return an expression object that can be evaluated with an
  39. // expression context.
  40. function xpathParse(expr) {
  41. xpathLog('parse ' + expr);
  42. xpathParseInit();
  43. var cached = xpathCacheLookup(expr);
  44. if (cached) {
  45. xpathLog(' ... cached');
  46. return cached;
  47. }
  48. // Optimize for a few common cases: simple attribute node tests
  49. // (@id), simple element node tests (page), variable references
  50. // ($address), numbers (4), multi-step path expressions where each
  51. // step is a plain element node test
  52. // (page/overlay/locations/location).
  53. if (expr.match(/^(\$|@)?\w+$/i)) {
  54. var ret = makeSimpleExpr(expr);
  55. xpathParseCache[expr] = ret;
  56. xpathLog(' ... simple');
  57. return ret;
  58. }
  59. if (expr.match(/^\w+(\/\w+)*$/i)) {
  60. var ret = makeSimpleExpr2(expr);
  61. xpathParseCache[expr] = ret;
  62. xpathLog(' ... simple 2');
  63. return ret;
  64. }
  65. var cachekey = expr; // expr is modified during parse
  66. var stack = [];
  67. var ahead = null;
  68. var previous = null;
  69. var done = false;
  70. var parse_count = 0;
  71. var lexer_count = 0;
  72. var reduce_count = 0;
  73. while (!done) {
  74. parse_count++;
  75. expr = expr.replace(/^\s*/, '');
  76. previous = ahead;
  77. ahead = null;
  78. var rule = null;
  79. var match = '';
  80. for (var i = 0; i < xpathTokenRules.length; ++i) {
  81. var result = xpathTokenRules[i].re.exec(expr);
  82. lexer_count++;
  83. if (result && result.length > 0 && result[0].length > match.length) {
  84. rule = xpathTokenRules[i];
  85. match = result[0];
  86. break;
  87. }
  88. }
  89. // Special case: allow operator keywords to be element and
  90. // variable names.
  91. // NOTE(mesch): The parser resolves conflicts by looking ahead,
  92. // and this is the only case where we look back to
  93. // disambiguate. So this is indeed something different, and
  94. // looking back is usually done in the lexer (via states in the
  95. // general case, called "start conditions" in flex(1)). Also,the
  96. // conflict resolution in the parser is not as robust as it could
  97. // be, so I'd like to keep as much off the parser as possible (all
  98. // these precedence values should be computed from the grammar
  99. // rules and possibly associativity declarations, as in bison(1),
  100. // and not explicitly set.
  101. if (rule &&
  102. (rule == TOK_DIV ||
  103. rule == TOK_MOD ||
  104. rule == TOK_AND ||
  105. rule == TOK_OR) &&
  106. (!previous ||
  107. previous.tag == TOK_AT ||
  108. previous.tag == TOK_DSLASH ||
  109. previous.tag == TOK_SLASH ||
  110. previous.tag == TOK_AXIS ||
  111. previous.tag == TOK_DOLLAR)) {
  112. rule = TOK_QNAME;
  113. }
  114. if (rule) {
  115. expr = expr.substr(match.length);
  116. xpathLog('token: ' + match + ' -- ' + rule.label);
  117. ahead = {
  118. tag: rule,
  119. match: match,
  120. prec: rule.prec ? rule.prec : 0, // || 0 is removed by the compiler
  121. expr: makeTokenExpr(match)
  122. };
  123. } else {
  124. xpathLog('DONE');
  125. done = true;
  126. }
  127. while (xpathReduce(stack, ahead)) {
  128. reduce_count++;
  129. xpathLog('stack: ' + stackToString(stack));
  130. }
  131. }
  132. xpathLog('stack: ' + stackToString(stack));
  133. // DGF any valid XPath should "reduce" to a single Expr token
  134. if (stack.length != 1) {
  135. throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack);
  136. }
  137. var result = stack[0].expr;
  138. xpathParseCache[cachekey] = result;
  139. xpathLog('XPath parse: ' + parse_count + ' / ' +
  140. lexer_count + ' / ' + reduce_count);
  141. return result;
  142. }
  143. var xpathParseCache = {};
  144. function xpathCacheLookup(expr) {
  145. return xpathParseCache[expr];
  146. }
  147. /*DGF xpathReduce is where the magic happens in this parser.
  148. Skim down to the bottom of this file to find the table of
  149. grammatical rules and precedence numbers, "The productions of the grammar".
  150. The idea here
  151. is that we want to take a stack of tokens and apply
  152. grammatical rules to them, "reducing" them to higher-level
  153. tokens. Ultimately, any valid XPath should reduce to exactly one
  154. "Expr" token.
  155. Reduce too early or too late and you'll have two tokens that can't reduce
  156. to single Expr. For example, you may hastily reduce a qname that
  157. should name a function, incorrectly treating it as a tag name.
  158. Or you may reduce too late, accidentally reducing the last part of the
  159. XPath into a top-level "Expr" that won't reduce with earlier parts of
  160. the XPath.
  161. A "cand" is a grammatical rule candidate, with a given precedence
  162. number. "ahead" is the upcoming token, which also has a precedence
  163. number. If the token has a higher precedence number than
  164. the rule candidate, we'll "shift" the token onto the token stack,
  165. instead of immediately applying the rule candidate.
  166. Some tokens have left associativity, in which case we shift when they
  167. have LOWER precedence than the candidate.
  168. */
  169. function xpathReduce(stack, ahead) {
  170. var cand = null;
  171. if (stack.length > 0) {
  172. var top = stack[stack.length-1];
  173. var ruleset = xpathRules[top.tag.key];
  174. if (ruleset) {
  175. for (var i = 0; i < ruleset.length; ++i) {
  176. var rule = ruleset[i];
  177. var match = xpathMatchStack(stack, rule[1]);
  178. if (match.length) {
  179. cand = {
  180. tag: rule[0],
  181. rule: rule,
  182. match: match
  183. };
  184. cand.prec = xpathGrammarPrecedence(cand);
  185. break;
  186. }
  187. }
  188. }
  189. }
  190. var ret;
  191. if (cand && (!ahead || cand.prec > ahead.prec ||
  192. (ahead.tag.left && cand.prec >= ahead.prec))) {
  193. for (var i = 0; i < cand.match.matchlength; ++i) {
  194. stack.pop();
  195. }
  196. xpathLog('reduce ' + cand.tag.label + ' ' + cand.prec +
  197. ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec +
  198. (ahead.tag.left ? ' left' : '')
  199. : ' none '));
  200. var matchexpr = mapExpr(cand.match, function(m) { return m.expr; });
  201. xpathLog('going to apply ' + cand.rule[3].toString());
  202. cand.expr = cand.rule[3].apply(null, matchexpr);
  203. stack.push(cand);
  204. ret = true;
  205. } else {
  206. if (ahead) {
  207. xpathLog('shift ' + ahead.tag.label + ' ' + ahead.prec +
  208. (ahead.tag.left ? ' left' : '') +
  209. ' over ' + (cand ? cand.tag.label + ' ' +
  210. cand.prec : ' none'));
  211. stack.push(ahead);
  212. }
  213. ret = false;
  214. }
  215. return ret;
  216. }
  217. function xpathMatchStack(stack, pattern) {
  218. // NOTE(mesch): The stack matches for variable cardinality are
  219. // greedy but don't do backtracking. This would be an issue only
  220. // with rules of the form A* A, i.e. with an element with variable
  221. // cardinality followed by the same element. Since that doesn't
  222. // occur in the grammar at hand, all matches on the stack are
  223. // unambiguous.
  224. var S = stack.length;
  225. var P = pattern.length;
  226. var p, s;
  227. var match = [];
  228. match.matchlength = 0;
  229. var ds = 0;
  230. for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) {
  231. ds = 0;
  232. var qmatch = [];
  233. if (pattern[p] == Q_MM) {
  234. p -= 1;
  235. match.push(qmatch);
  236. while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
  237. qmatch.push(stack[s - ds]);
  238. ds += 1;
  239. match.matchlength += 1;
  240. }
  241. } else if (pattern[p] == Q_01) {
  242. p -= 1;
  243. match.push(qmatch);
  244. while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) {
  245. qmatch.push(stack[s - ds]);
  246. ds += 1;
  247. match.matchlength += 1;
  248. }
  249. } else if (pattern[p] == Q_1M) {
  250. p -= 1;
  251. match.push(qmatch);
  252. if (stack[s].tag == pattern[p]) {
  253. while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
  254. qmatch.push(stack[s - ds]);
  255. ds += 1;
  256. match.matchlength += 1;
  257. }
  258. } else {
  259. return [];
  260. }
  261. } else if (stack[s].tag == pattern[p]) {
  262. match.push(stack[s]);
  263. ds += 1;
  264. match.matchlength += 1;
  265. } else {
  266. return [];
  267. }
  268. reverseInplace(qmatch);
  269. qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; });
  270. }
  271. reverseInplace(match);
  272. if (p == -1) {
  273. return match;
  274. } else {
  275. return [];
  276. }
  277. }
  278. function xpathTokenPrecedence(tag) {
  279. return tag.prec || 2;
  280. }
  281. function xpathGrammarPrecedence(frame) {
  282. var ret = 0;
  283. if (frame.rule) { /* normal reduce */
  284. if (frame.rule.length >= 3 && frame.rule[2] >= 0) {
  285. ret = frame.rule[2];
  286. } else {
  287. for (var i = 0; i < frame.rule[1].length; ++i) {
  288. var p = xpathTokenPrecedence(frame.rule[1][i]);
  289. ret = Math.max(ret, p);
  290. }
  291. }
  292. } else if (frame.tag) { /* TOKEN match */
  293. ret = xpathTokenPrecedence(frame.tag);
  294. } else if (frame.length) { /* Q_ match */
  295. for (var j = 0; j < frame.length; ++j) {
  296. var p = xpathGrammarPrecedence(frame[j]);
  297. ret = Math.max(ret, p);
  298. }
  299. }
  300. return ret;
  301. }
  302. function stackToString(stack) {
  303. var ret = '';
  304. for (var i = 0; i < stack.length; ++i) {
  305. if (ret) {
  306. ret += '\n';
  307. }
  308. ret += stack[i].tag.label;
  309. }
  310. return ret;
  311. }
  312. // XPath expression evaluation context. An XPath context consists of a
  313. // DOM node, a list of DOM nodes that contains this node, a number
  314. // that represents the position of the single node in the list, and a
  315. // current set of variable bindings. (See XPath spec.)
  316. //
  317. // The interface of the expression context:
  318. //
  319. // Constructor -- gets the node, its position, the node set it
  320. // belongs to, and a parent context as arguments. The parent context
  321. // is used to implement scoping rules for variables: if a variable
  322. // is not found in the current context, it is looked for in the
  323. // parent context, recursively. Except for node, all arguments have
  324. // default values: default position is 0, default node set is the
  325. // set that contains only the node, and the default parent is null.
  326. //
  327. // Notice that position starts at 0 at the outside interface;
  328. // inside XPath expressions this shows up as position()=1.
  329. //
  330. // clone() -- creates a new context with the current context as
  331. // parent. If passed as argument to clone(), the new context has a
  332. // different node, position, or node set. What is not passed is
  333. // inherited from the cloned context.
  334. //
  335. // setVariable(name, expr) -- binds given XPath expression to the
  336. // name.
  337. //
  338. // getVariable(name) -- what the name says.
  339. //
  340. // setNode(position) -- sets the context to the node at the given
  341. // position. Needed to implement scoping rules for variables in
  342. // XPath. (A variable is visible to all subsequent siblings, not
  343. // only to its children.)
  344. //
  345. // set/isCaseInsensitive -- specifies whether node name tests should
  346. // be case sensitive. If you're executing xpaths against a regular
  347. // HTML DOM, you probably don't want case-sensitivity, because
  348. // browsers tend to disagree about whether elements & attributes
  349. // should be upper/lower case. If you're running xpaths in an
  350. // XSLT instance, you probably DO want case sensitivity, as per the
  351. // XSL spec.
  352. function ExprContext(node, opt_position, opt_nodelist, opt_parent,
  353. opt_caseInsensitive, opt_ignoreAttributesWithoutValue,
  354. opt_returnOnFirstMatch)
  355. {
  356. this.node = node;
  357. this.position = opt_position || 0;
  358. this.nodelist = opt_nodelist || [ node ];
  359. this.variables = {};
  360. this.parent = opt_parent || null;
  361. this.caseInsensitive = opt_caseInsensitive || false;
  362. this.ignoreAttributesWithoutValue = opt_ignoreAttributesWithoutValue || false;
  363. this.returnOnFirstMatch = opt_returnOnFirstMatch || false;
  364. if (opt_parent) {
  365. this.root = opt_parent.root;
  366. } else if (this.node.nodeType == DOM_DOCUMENT_NODE) {
  367. // NOTE(mesch): DOM Spec stipulates that the ownerDocument of a
  368. // document is null. Our root, however is the document that we are
  369. // processing, so the initial context is created from its document
  370. // node, which case we must handle here explcitly.
  371. this.root = node;
  372. } else {
  373. this.root = node.ownerDocument;
  374. }
  375. }
  376. ExprContext.prototype.clone = function(opt_node, opt_position, opt_nodelist) {
  377. return new ExprContext(
  378. opt_node || this.node,
  379. typeof opt_position != 'undefined' ? opt_position : this.position,
  380. opt_nodelist || this.nodelist, this, this.caseInsensitive,
  381. this.ignoreAttributesWithoutValue, this.returnOnFirstMatch);
  382. };
  383. ExprContext.prototype.setVariable = function(name, value) {
  384. if (value instanceof StringValue || value instanceof BooleanValue ||
  385. value instanceof NumberValue || value instanceof NodeSetValue) {
  386. this.variables[name] = value;
  387. return;
  388. }
  389. if ('true' === value) {
  390. this.variables[name] = new BooleanValue(true);
  391. } else if ('false' === value) {
  392. this.variables[name] = new BooleanValue(false);
  393. } else if (TOK_NUMBER.re.test(value)) {
  394. this.variables[name] = new NumberValue(value);
  395. } else {
  396. // DGF What if it's null?
  397. this.variables[name] = new StringValue(value);
  398. }
  399. };
  400. ExprContext.prototype.getVariable = function(name) {
  401. if (typeof this.variables[name] != 'undefined') {
  402. return this.variables[name];
  403. } else if (this.parent) {
  404. return this.parent.getVariable(name);
  405. } else {
  406. return null;
  407. }
  408. };
  409. ExprContext.prototype.setNode = function(position) {
  410. this.node = this.nodelist[position];
  411. this.position = position;
  412. };
  413. ExprContext.prototype.contextSize = function() {
  414. return this.nodelist.length;
  415. };
  416. ExprContext.prototype.isCaseInsensitive = function() {
  417. return this.caseInsensitive;
  418. };
  419. ExprContext.prototype.setCaseInsensitive = function(caseInsensitive) {
  420. return this.caseInsensitive = caseInsensitive;
  421. };
  422. ExprContext.prototype.isIgnoreAttributesWithoutValue = function() {
  423. return this.ignoreAttributesWithoutValue;
  424. };
  425. ExprContext.prototype.setIgnoreAttributesWithoutValue = function(ignore) {
  426. return this.ignoreAttributesWithoutValue = ignore;
  427. };
  428. ExprContext.prototype.isReturnOnFirstMatch = function() {
  429. return this.returnOnFirstMatch;
  430. };
  431. ExprContext.prototype.setReturnOnFirstMatch = function(returnOnFirstMatch) {
  432. return this.returnOnFirstMatch = returnOnFirstMatch;
  433. };
  434. // XPath expression values. They are what XPath expressions evaluate
  435. // to. Strangely, the different value types are not specified in the
  436. // XPath syntax, but only in the semantics, so they don't show up as
  437. // nonterminals in the grammar. Yet, some expressions are required to
  438. // evaluate to particular types, and not every type can be coerced
  439. // into every other type. Although the types of XPath values are
  440. // similar to the types present in JavaScript, the type coercion rules
  441. // are a bit peculiar, so we explicitly model XPath types instead of
  442. // mapping them onto JavaScript types. (See XPath spec.)
  443. //
  444. // The four types are:
  445. //
  446. // StringValue
  447. //
  448. // NumberValue
  449. //
  450. // BooleanValue
  451. //
  452. // NodeSetValue
  453. //
  454. // The common interface of the value classes consists of methods that
  455. // implement the XPath type coercion rules:
  456. //
  457. // stringValue() -- returns the value as a JavaScript String,
  458. //
  459. // numberValue() -- returns the value as a JavaScript Number,
  460. //
  461. // booleanValue() -- returns the value as a JavaScript Boolean,
  462. //
  463. // nodeSetValue() -- returns the value as a JavaScript Array of DOM
  464. // Node objects.
  465. //
  466. function StringValue(value) {
  467. this.value = value;
  468. this.type = 'string';
  469. }
  470. StringValue.prototype.stringValue = function() {
  471. return this.value;
  472. }
  473. StringValue.prototype.booleanValue = function() {
  474. return this.value.length > 0;
  475. }
  476. StringValue.prototype.numberValue = function() {
  477. return this.value - 0;
  478. }
  479. StringValue.prototype.nodeSetValue = function() {
  480. throw this;
  481. }
  482. function BooleanValue(value) {
  483. this.value = value;
  484. this.type = 'boolean';
  485. }
  486. BooleanValue.prototype.stringValue = function() {
  487. return '' + this.value;
  488. }
  489. BooleanValue.prototype.booleanValue = function() {
  490. return this.value;
  491. }
  492. BooleanValue.prototype.numberValue = function() {
  493. return this.value ? 1 : 0;
  494. }
  495. BooleanValue.prototype.nodeSetValue = function() {
  496. throw this;
  497. }
  498. function NumberValue(value) {
  499. this.value = value;
  500. this.type = 'number';
  501. }
  502. NumberValue.prototype.stringValue = function() {
  503. return '' + this.value;
  504. }
  505. NumberValue.prototype.booleanValue = function() {
  506. return !!this.value;
  507. }
  508. NumberValue.prototype.numberValue = function() {
  509. return this.value - 0;
  510. }
  511. NumberValue.prototype.nodeSetValue = function() {
  512. throw this;
  513. }
  514. function NodeSetValue(value) {
  515. this.value = value;
  516. this.type = 'node-set';
  517. }
  518. NodeSetValue.prototype.stringValue = function() {
  519. if (this.value.length == 0) {
  520. return '';
  521. } else {
  522. return xmlValue(this.value[0]);
  523. }
  524. }
  525. NodeSetValue.prototype.booleanValue = function() {
  526. return this.value.length > 0;
  527. }
  528. NodeSetValue.prototype.numberValue = function() {
  529. return this.stringValue() - 0;
  530. }
  531. NodeSetValue.prototype.nodeSetValue = function() {
  532. return this.value;
  533. };
  534. // XPath expressions. They are used as nodes in the parse tree and
  535. // possess an evaluate() method to compute an XPath value given an XPath
  536. // context. Expressions are returned from the parser. Teh set of
  537. // expression classes closely mirrors the set of non terminal symbols
  538. // in the grammar. Every non trivial nonterminal symbol has a
  539. // corresponding expression class.
  540. //
  541. // The common expression interface consists of the following methods:
  542. //
  543. // evaluate(context) -- evaluates the expression, returns a value.
  544. //
  545. // toString() -- returns the XPath text representation of the
  546. // expression (defined in xsltdebug.js).
  547. //
  548. // parseTree(indent) -- returns a parse tree representation of the
  549. // expression (defined in xsltdebug.js).
  550. function TokenExpr(m) {
  551. this.value = m;
  552. }
  553. TokenExpr.prototype.evaluate = function() {
  554. return new StringValue(this.value);
  555. };
  556. function LocationExpr() {
  557. this.absolute = false;
  558. this.steps = [];
  559. }
  560. LocationExpr.prototype.appendStep = function(s) {
  561. var combinedStep = this._combineSteps(this.steps[this.steps.length-1], s);
  562. if (combinedStep) {
  563. this.steps[this.steps.length-1] = combinedStep;
  564. } else {
  565. this.steps.push(s);
  566. }
  567. }
  568. LocationExpr.prototype.prependStep = function(s) {
  569. var combinedStep = this._combineSteps(s, this.steps[0]);
  570. if (combinedStep) {
  571. this.steps[0] = combinedStep;
  572. } else {
  573. this.steps.unshift(s);
  574. }
  575. };
  576. // DGF try to combine two steps into one step (perf enhancement)
  577. LocationExpr.prototype._combineSteps = function(prevStep, nextStep) {
  578. if (!prevStep) return null;
  579. if (!nextStep) return null;
  580. var hasPredicates = (prevStep.predicates && prevStep.predicates.length > 0);
  581. if (prevStep.nodetest instanceof NodeTestAny && !hasPredicates) {
  582. // maybe suitable to be combined
  583. if (prevStep.axis == xpathAxis.DESCENDANT_OR_SELF) {
  584. if (nextStep.axis == xpathAxis.CHILD) {
  585. // HBC - commenting out, because this is not a valid reduction
  586. //nextStep.axis = xpathAxis.DESCENDANT;
  587. //return nextStep;
  588. } else if (nextStep.axis == xpathAxis.SELF) {
  589. nextStep.axis = xpathAxis.DESCENDANT_OR_SELF;
  590. return nextStep;
  591. }
  592. } else if (prevStep.axis == xpathAxis.DESCENDANT) {
  593. if (nextStep.axis == xpathAxis.SELF) {
  594. nextStep.axis = xpathAxis.DESCENDANT;
  595. return nextStep;
  596. }
  597. }
  598. }
  599. return null;
  600. }
  601. LocationExpr.prototype.evaluate = function(ctx) {
  602. var start;
  603. if (this.absolute) {
  604. start = ctx.root;
  605. } else {
  606. start = ctx.node;
  607. }
  608. var nodes = [];
  609. xPathStep(nodes, this.steps, 0, start, ctx);
  610. return new NodeSetValue(nodes);
  611. };
  612. function xPathStep(nodes, steps, step, input, ctx) {
  613. var s = steps[step];
  614. var ctx2 = ctx.clone(input);
  615. if (ctx.returnOnFirstMatch && !s.hasPositionalPredicate) {
  616. var nodelist = s.evaluate(ctx2).nodeSetValue();
  617. // the predicates were not processed in the last evaluate(), so that we can
  618. // process them here with the returnOnFirstMatch optimization. We do a
  619. // depth-first grab at any nodes that pass the predicate tests. There is no
  620. // way to optimize when predicates contain positional selectors, including
  621. // indexes or uses of the last() or position() functions, because they
  622. // typically require the entire nodelist for context. Process without
  623. // optimization if we encounter such selectors.
  624. var nLength = nodelist.length;
  625. var pLength = s.predicate.length;
  626. nodelistLoop:
  627. for (var i = 0; i < nLength; ++i) {
  628. var n = nodelist[i];
  629. for (var j = 0; j < pLength; ++j) {
  630. if (!s.predicate[j].evaluate(ctx.clone(n, i, nodelist)).booleanValue()) {
  631. continue nodelistLoop;
  632. }
  633. }
  634. // n survived the predicate tests!
  635. if (step == steps.length - 1) {
  636. nodes.push(n);
  637. }
  638. else {
  639. xPathStep(nodes, steps, step + 1, n, ctx);
  640. }
  641. if (nodes.length > 0) {
  642. break;
  643. }
  644. }
  645. }
  646. else {
  647. // set returnOnFirstMatch to false for the cloned ExprContext, because
  648. // behavior in StepExpr.prototype.evaluate is driven off its value. Note
  649. // that the original context may still have true for this value.
  650. ctx2.returnOnFirstMatch = false;
  651. var nodelist = s.evaluate(ctx2).nodeSetValue();
  652. for (var i = 0; i < nodelist.length; ++i) {
  653. if (step == steps.length - 1) {
  654. nodes.push(nodelist[i]);
  655. } else {
  656. xPathStep(nodes, steps, step + 1, nodelist[i], ctx);
  657. }
  658. }
  659. }
  660. }
  661. function StepExpr(axis, nodetest, opt_predicate) {
  662. this.axis = axis;
  663. this.nodetest = nodetest;
  664. this.predicate = opt_predicate || [];
  665. this.hasPositionalPredicate = false;
  666. for (var i = 0; i < this.predicate.length; ++i) {
  667. if (predicateExprHasPositionalSelector(this.predicate[i].expr)) {
  668. this.hasPositionalPredicate = true;
  669. break;
  670. }
  671. }
  672. }
  673. StepExpr.prototype.appendPredicate = function(p) {
  674. this.predicate.push(p);
  675. if (!this.hasPositionalPredicate) {
  676. this.hasPositionalPredicate = predicateExprHasPositionalSelector(p.expr);
  677. }
  678. }
  679. StepExpr.prototype.evaluate = function(ctx) {
  680. var input = ctx.node;
  681. var nodelist = [];
  682. var skipNodeTest = false;
  683. if (this.nodetest instanceof NodeTestAny) {
  684. skipNodeTest = true;
  685. }
  686. // NOTE(mesch): When this was a switch() statement, it didn't work
  687. // in Safari/2.0. Not sure why though; it resulted in the JavaScript
  688. // console output "undefined" (without any line number or so).
  689. if (this.axis == xpathAxis.ANCESTOR_OR_SELF) {
  690. nodelist.push(input);
  691. for (var n = input.parentNode; n; n = n.parentNode) {
  692. nodelist.push(n);
  693. }
  694. } else if (this.axis == xpathAxis.ANCESTOR) {
  695. for (var n = input.parentNode; n; n = n.parentNode) {
  696. nodelist.push(n);
  697. }
  698. } else if (this.axis == xpathAxis.ATTRIBUTE) {
  699. if (this.nodetest.name != undefined) {
  700. // single-attribute step
  701. if (input.attributes) {
  702. if (input.attributes instanceof Array) {
  703. // probably evaluating on document created by xmlParse()
  704. copyArray(nodelist, input.attributes);
  705. }
  706. else {
  707. if (this.nodetest.name == 'style') {
  708. var value = input.getAttribute('style');
  709. if (value && typeof(value) != 'string') {
  710. // this is the case where indexing into the attributes array
  711. // doesn't give us the attribute node in IE - we create our own
  712. // node instead
  713. nodelist.push(XNode.create(DOM_ATTRIBUTE_NODE, 'style',
  714. value.cssText, document));
  715. }
  716. else {
  717. nodelist.push(input.attributes[this.nodetest.name]);
  718. }
  719. }
  720. else {
  721. nodelist.push(input.attributes[this.nodetest.name]);
  722. }
  723. }
  724. }
  725. }
  726. else {
  727. // all-attributes step
  728. if (ctx.ignoreAttributesWithoutValue) {
  729. copyArrayIgnoringAttributesWithoutValue(nodelist, input.attributes);
  730. }
  731. else {
  732. copyArray(nodelist, input.attributes);
  733. }
  734. }
  735. } else if (this.axis == xpathAxis.CHILD) {
  736. copyArray(nodelist, input.childNodes);
  737. } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) {
  738. if (this.nodetest.evaluate(ctx).booleanValue()) {
  739. nodelist.push(input);
  740. }
  741. var tagName = xpathExtractTagNameFromNodeTest(this.nodetest);
  742. xpathCollectDescendants(nodelist, input, tagName);
  743. if (tagName) skipNodeTest = true;
  744. } else if (this.axis == xpathAxis.DESCENDANT) {
  745. var tagName = xpathExtractTagNameFromNodeTest(this.nodetest);
  746. xpathCollectDescendants(nodelist, input, tagName);
  747. if (tagName) skipNodeTest = true;
  748. } else if (this.axis == xpathAxis.FOLLOWING) {
  749. for (var n = input; n; n = n.parentNode) {
  750. for (var nn = n.nextSibling; nn; nn = nn.nextSibling) {
  751. nodelist.push(nn);
  752. xpathCollectDescendants(nodelist, nn);
  753. }
  754. }
  755. } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) {
  756. for (var n = input.nextSibling; n; n = n.nextSibling) {
  757. nodelist.push(n);
  758. }
  759. } else if (this.axis == xpathAxis.NAMESPACE) {
  760. alert('not implemented: axis namespace');
  761. } else if (this.axis == xpathAxis.PARENT) {
  762. if (input.parentNode) {
  763. nodelist.push(input.parentNode);
  764. }
  765. } else if (this.axis == xpathAxis.PRECEDING) {
  766. for (var n = input; n; n = n.parentNode) {
  767. for (var nn = n.previousSibling; nn; nn = nn.previousSibling) {
  768. nodelist.push(nn);
  769. xpathCollectDescendantsReverse(nodelist, nn);
  770. }
  771. }
  772. } else if (this.axis == xpathAxis.PRECEDING_SIBLING) {
  773. for (var n = input.previousSibling; n; n = n.previousSibling) {
  774. nodelist.push(n);
  775. }
  776. } else if (this.axis == xpathAxis.SELF) {
  777. nodelist.push(input);
  778. } else {
  779. throw 'ERROR -- NO SUCH AXIS: ' + this.axis;
  780. }
  781. if (!skipNodeTest) {
  782. // process node test
  783. var nodelist0 = nodelist;
  784. nodelist = [];
  785. for (var i = 0; i < nodelist0.length; ++i) {
  786. var n = nodelist0[i];
  787. if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) {
  788. nodelist.push(n);
  789. }
  790. }
  791. }
  792. // process predicates
  793. if (!ctx.returnOnFirstMatch) {
  794. for (var i = 0; i < this.predicate.length; ++i) {
  795. var nodelist0 = nodelist;
  796. nodelist = [];
  797. for (var ii = 0; ii < nodelist0.length; ++ii) {
  798. var n = nodelist0[ii];
  799. if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) {
  800. nodelist.push(n);
  801. }
  802. }
  803. }
  804. }
  805. return new NodeSetValue(nodelist);
  806. };
  807. function NodeTestAny() {
  808. this.value = new BooleanValue(true);
  809. }
  810. NodeTestAny.prototype.evaluate = function(ctx) {
  811. return this.value;
  812. };
  813. function NodeTestElementOrAttribute() {}
  814. NodeTestElementOrAttribute.prototype.evaluate = function(ctx) {
  815. return new BooleanValue(
  816. ctx.node.nodeType == DOM_ELEMENT_NODE ||
  817. ctx.node.nodeType == DOM_ATTRIBUTE_NODE);
  818. }
  819. function NodeTestText() {}
  820. NodeTestText.prototype.evaluate = function(ctx) {
  821. return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE);
  822. }
  823. function NodeTestComment() {}
  824. NodeTestComment.prototype.evaluate = function(ctx) {
  825. return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE);
  826. }
  827. function NodeTestPI(target) {
  828. this.target = target;
  829. }
  830. NodeTestPI.prototype.evaluate = function(ctx) {
  831. return new
  832. BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE &&
  833. (!this.target || ctx.node.nodeName == this.target));
  834. }
  835. function NodeTestNC(nsprefix) {
  836. this.regex = new RegExp("^" + nsprefix + ":");
  837. this.nsprefix = nsprefix;
  838. }
  839. NodeTestNC.prototype.evaluate = function(ctx) {
  840. var n = ctx.node;
  841. return new BooleanValue(this.regex.match(n.nodeName));
  842. }
  843. function NodeTestName(name) {
  844. this.name = name;
  845. this.re = new RegExp('^' + name + '$', "i");
  846. }
  847. NodeTestName.prototype.evaluate = function(ctx) {
  848. var n = ctx.node;
  849. if (ctx.caseInsensitive) {
  850. if (n.nodeName.length != this.name.length) return new BooleanValue(false);
  851. return new BooleanValue(this.re.test(n.nodeName));
  852. } else {
  853. return new BooleanValue(n.nodeName == this.name);
  854. }
  855. }
  856. function PredicateExpr(expr) {
  857. this.expr = expr;
  858. }
  859. PredicateExpr.prototype.evaluate = function(ctx) {
  860. var v = this.expr.evaluate(ctx);
  861. if (v.type == 'number') {
  862. // NOTE(mesch): Internally, position is represented starting with
  863. // 0, however in XPath position starts with 1. See functions
  864. // position() and last().
  865. return new BooleanValue(ctx.position == v.numberValue() - 1);
  866. } else {
  867. return new BooleanValue(v.booleanValue());
  868. }
  869. };
  870. function FunctionCallExpr(name) {
  871. this.name = name;
  872. this.args = [];
  873. }
  874. FunctionCallExpr.prototype.appendArg = function(arg) {
  875. this.args.push(arg);
  876. };
  877. FunctionCallExpr.prototype.evaluate = function(ctx) {
  878. var fn = '' + this.name.value;
  879. var f = this.xpathfunctions[fn];
  880. if (f) {
  881. return f.call(this, ctx);
  882. } else {
  883. xpathLog('XPath NO SUCH FUNCTION ' + fn);
  884. return new BooleanValue(false);
  885. }
  886. };
  887. FunctionCallExpr.prototype.xpathfunctions = {
  888. 'last': function(ctx) {
  889. assert(this.args.length == 0);
  890. // NOTE(mesch): XPath position starts at 1.
  891. return new NumberValue(ctx.contextSize());
  892. },
  893. 'position': function(ctx) {
  894. assert(this.args.length == 0);
  895. // NOTE(mesch): XPath position starts at 1.
  896. return new NumberValue(ctx.position + 1);
  897. },
  898. 'count': function(ctx) {
  899. assert(this.args.length == 1);
  900. var v = this.args[0].evaluate(ctx);
  901. return new NumberValue(v.nodeSetValue().length);
  902. },
  903. 'id': function(ctx) {
  904. assert(this.args.length == 1);
  905. var e = this.args[0].evaluate(ctx);
  906. var ret = [];
  907. var ids;
  908. if (e.type == 'node-set') {
  909. ids = [];
  910. var en = e.nodeSetValue();
  911. for (var i = 0; i < en.length; ++i) {
  912. var v = xmlValue(en[i]).split(/\s+/);
  913. for (var ii = 0; ii < v.length; ++ii) {
  914. ids.push(v[ii]);
  915. }
  916. }
  917. } else {
  918. ids = e.stringValue().split(/\s+/);
  919. }
  920. var d = ctx.root;
  921. for (var i = 0; i < ids.length; ++i) {
  922. var n = d.getElementById(ids[i]);
  923. if (n) {
  924. ret.push(n);
  925. }
  926. }
  927. return new NodeSetValue(ret);
  928. },
  929. 'local-name': function(ctx) {
  930. alert('not implmented yet: XPath function local-name()');
  931. },
  932. 'namespace-uri': function(ctx) {
  933. alert('not implmented yet: XPath function namespace-uri()');
  934. },
  935. 'name': function(ctx) {
  936. assert(this.args.length == 1 || this.args.length == 0);
  937. var n;
  938. if (this.args.length == 0) {
  939. n = [ ctx.node ];
  940. } else {
  941. n = this.args[0].evaluate(ctx).nodeSetValue();
  942. }
  943. if (n.length == 0) {
  944. return new StringValue('');
  945. } else {
  946. return new StringValue(n[0].nodeName);
  947. }
  948. },
  949. 'string': function(ctx) {
  950. assert(this.args.length == 1 || this.args.length == 0);
  951. if (this.args.length == 0) {
  952. return new StringValue(new NodeSetValue([ ctx.node ]).stringValue());
  953. } else {
  954. return new StringValue(this.args[0].evaluate(ctx).stringValue());
  955. }
  956. },
  957. 'concat': function(ctx) {
  958. var ret = '';
  959. for (var i = 0; i < this.args.length; ++i) {
  960. ret += this.args[i].evaluate(ctx).stringValue();
  961. }
  962. return new StringValue(ret);
  963. },
  964. 'starts-with': function(ctx) {
  965. assert(this.args.length == 2);
  966. var s0 = this.args[0].evaluate(ctx).stringValue();
  967. var s1 = this.args[1].evaluate(ctx).stringValue();
  968. return new BooleanValue(s0.indexOf(s1) == 0);
  969. },
  970. 'ends-with': function(ctx) {
  971. assert(this.args.length == 2);
  972. var s0 = this.args[0].evaluate(ctx).stringValue();
  973. var s1 = this.args[1].evaluate(ctx).stringValue();
  974. var re = new RegExp(RegExp.escape(s1) + '$');
  975. return new BooleanValue(re.test(s0));
  976. },
  977. 'contains': function(ctx) {
  978. assert(this.args.length == 2);
  979. var s0 = this.args[0].evaluate(ctx).stringValue();
  980. var s1 = this.args[1].evaluate(ctx).stringValue();
  981. return new BooleanValue(s0.indexOf(s1) != -1);
  982. },
  983. 'substring-before': function(ctx) {
  984. assert(this.args.length == 2);
  985. var s0 = this.args[0].evaluate(ctx).stringValue();
  986. var s1 = this.args[1].evaluate(ctx).stringValue();
  987. var i = s0.indexOf(s1);
  988. var ret;
  989. if (i == -1) {
  990. ret = '';
  991. } else {
  992. ret = s0.substr(0,i);
  993. }
  994. return new StringValue(ret);
  995. },
  996. 'substring-after': function(ctx) {
  997. assert(this.args.length == 2);
  998. var s0 = this.args[0].evaluate(ctx).stringValue();
  999. var s1 = this.args[1].evaluate(ctx).stringValue();
  1000. var i = s0.indexOf(s1);
  1001. var ret;
  1002. if (i == -1) {
  1003. ret = '';
  1004. } else {
  1005. ret = s0.substr(i + s1.length);
  1006. }
  1007. return new StringValue(ret);
  1008. },
  1009. 'substring': function(ctx) {
  1010. // NOTE: XPath defines the position of the first character in a
  1011. // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2).
  1012. assert(this.args.length == 2 || this.args.length == 3);
  1013. var s0 = this.args[0].evaluate(ctx).stringValue();
  1014. var s1 = this.args[1].evaluate(ctx).numberValue();
  1015. var ret;
  1016. if (this.args.length == 2) {
  1017. var i1 = Math.max(0, Math.round(s1) - 1);
  1018. ret = s0.substr(i1);
  1019. } else {
  1020. var s2 = this.args[2].evaluate(ctx).numberValue();
  1021. var i0 = Math.round(s1) - 1;
  1022. var i1 = Math.max(0, i0);
  1023. var i2 = Math.round(s2) - Math.max(0, -i0);
  1024. ret = s0.substr(i1, i2);
  1025. }
  1026. return new StringValue(ret);
  1027. },
  1028. 'string-length': function(ctx) {
  1029. var s;
  1030. if (this.args.length > 0) {
  1031. s = this.args[0].evaluate(ctx).stringValue();
  1032. } else {
  1033. s = new NodeSetValue([ ctx.node ]).stringValue();
  1034. }
  1035. return new NumberValue(s.length);
  1036. },
  1037. 'normalize-space': function(ctx) {
  1038. var s;
  1039. if (this.args.length > 0) {
  1040. s = this.args[0].evaluate(ctx).stringValue();
  1041. } else {
  1042. s = new NodeSetValue([ ctx.node ]).stringValue();
  1043. }
  1044. s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' ');
  1045. return new StringValue(s);
  1046. },
  1047. 'translate': function(ctx) {
  1048. assert(this.args.length == 3);
  1049. var s0 = this.args[0].evaluate(ctx).stringValue();
  1050. var s1 = this.args[1].evaluate(ctx).stringValue();
  1051. var s2 = this.args[2].evaluate(ctx).stringValue();
  1052. for (var i = 0; i < s1.length; ++i) {
  1053. s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i));
  1054. }
  1055. return new StringValue(s0);
  1056. },
  1057. 'matches': function(ctx) {
  1058. assert(this.args.length >= 2);
  1059. var s0 = this.args[0].evaluate(ctx).stringValue();
  1060. var s1 = this.args[1].evaluate(ctx).stringValue();
  1061. if (this.args.length > 2) {
  1062. var s2 = this.args[2].evaluate(ctx).stringValue();
  1063. if (/[^mi]/.test(s2)) {
  1064. throw 'Invalid regular expression syntax: ' + s2;
  1065. }
  1066. }
  1067. try {
  1068. var re = new RegExp(s1, s2);
  1069. }
  1070. catch (e) {
  1071. throw 'Invalid matches argument: ' + s1;
  1072. }
  1073. return new BooleanValue(re.test(s0));
  1074. },
  1075. 'boolean': function(ctx) {
  1076. assert(this.args.length == 1);
  1077. return new BooleanValue(this.args[0].evaluate(ctx).booleanValue());
  1078. },
  1079. 'not': function(ctx) {
  1080. assert(this.args.length == 1);
  1081. var ret = !this.args[0].evaluate(ctx).booleanValue();
  1082. return new BooleanValue(ret);
  1083. },
  1084. 'true': function(ctx) {
  1085. assert(this.args.length == 0);
  1086. return new BooleanValue(true);
  1087. },
  1088. 'false': function(ctx) {
  1089. assert(this.args.length == 0);
  1090. return new BooleanValue(false);
  1091. },
  1092. 'lang': function(ctx) {
  1093. assert(this.args.length == 1);
  1094. var lang = this.args[0].evaluate(ctx).stringValue();
  1095. var xmllang;
  1096. var n = ctx.node;
  1097. while (n && n != n.parentNode /* just in case ... */) {
  1098. xmllang = n.getAttribute('xml:lang');
  1099. if (xmllang) {
  1100. break;
  1101. }
  1102. n = n.parentNode;
  1103. }
  1104. if (!xmllang) {
  1105. return new BooleanValue(false);
  1106. } else {
  1107. var re = new RegExp('^' + lang + '$', 'i');
  1108. return new BooleanValue(xmllang.match(re) ||
  1109. xmllang.replace(/_.*$/,'').match(re));
  1110. }
  1111. },
  1112. 'number': function(ctx) {
  1113. assert(this.args.length == 1 || this.args.length == 0);
  1114. if (this.args.length == 1) {
  1115. return new NumberValue(this.args[0].evaluate(ctx).numberValue());
  1116. } else {
  1117. return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue());
  1118. }
  1119. },
  1120. 'sum': function(ctx) {
  1121. assert(this.args.length == 1);
  1122. var n = this.args[0].evaluate(ctx).nodeSetValue();
  1123. var sum = 0;
  1124. for (var i = 0; i < n.length; ++i) {
  1125. sum += xmlValue(n[i]) - 0;
  1126. }
  1127. return new NumberValue(sum);
  1128. },
  1129. 'floor': function(ctx) {
  1130. assert(this.args.length == 1);
  1131. var num = this.args[0].evaluate(ctx).numberValue();
  1132. return new NumberValue(Math.floor(num));
  1133. },
  1134. 'ceiling': function(ctx) {
  1135. assert(this.args.length == 1);
  1136. var num = this.args[0].evaluate(ctx).numberValue();
  1137. return new NumberValue(Math.ceil(num));
  1138. },
  1139. 'round': function(ctx) {
  1140. assert(this.args.length == 1);
  1141. var num = this.args[0].evaluate(ctx).numberValue();
  1142. return new NumberValue(Math.round(num));
  1143. },
  1144. // TODO(mesch): The following functions are custom. There is a
  1145. // standard that defines how to add functions, which should be
  1146. // applied here.
  1147. 'ext-join': function(ctx) {
  1148. assert(this.args.length == 2);
  1149. var nodes = this.args[0].evaluate(ctx).nodeSetValue();
  1150. var delim = this.args[1].evaluate(ctx).stringValue();
  1151. var ret = '';
  1152. for (var i = 0; i < nodes.length; ++i) {
  1153. if (ret) {
  1154. ret += delim;
  1155. }
  1156. ret += xmlValue(nodes[i]);
  1157. }
  1158. return new StringValue(ret);
  1159. },
  1160. // ext-if() evaluates and returns its second argument, if the
  1161. // boolean value of its first argument is true, otherwise it
  1162. // evaluates and returns its third argument.
  1163. 'ext-if': function(ctx) {
  1164. assert(this.args.length == 3);
  1165. if (this.args[0].evaluate(ctx).booleanValue()) {
  1166. return this.args[1].evaluate(ctx);
  1167. } else {
  1168. return this.args[2].evaluate(ctx);
  1169. }
  1170. },
  1171. // ext-cardinal() evaluates its single argument as a number, and
  1172. // returns the current node that many times. It can be used in the
  1173. // select attribute to iterate over an integer range.
  1174. 'ext-cardinal': function(ctx) {
  1175. assert(this.args.length >= 1);
  1176. var c = this.args[0].evaluate(ctx).numberValue();
  1177. var ret = [];
  1178. for (var i = 0; i < c; ++i) {
  1179. ret.push(ctx.node);
  1180. }
  1181. return new NodeSetValue(ret);
  1182. }
  1183. };
  1184. function UnionExpr(expr1, expr2) {
  1185. this.expr1 = expr1;
  1186. this.expr2 = expr2;
  1187. }
  1188. UnionExpr.prototype.evaluate = function(ctx) {
  1189. var nodes1 = this.expr1.evaluate(ctx).nodeSetValue();
  1190. var nodes2 = this.expr2.evaluate(ctx).nodeSetValue();
  1191. var I1 = nodes1.length;
  1192. for (var i2 = 0; i2 < nodes2.length; ++i2) {
  1193. var n = nodes2[i2];
  1194. var inBoth = false;
  1195. for (var i1 = 0; i1 < I1; ++i1) {
  1196. if (nodes1[i1] == n) {
  1197. inBoth = true;
  1198. i1 = I1; // break inner loop
  1199. }
  1200. }
  1201. if (!inBoth) {
  1202. nodes1.push(n);
  1203. }
  1204. }
  1205. return new NodeSetValue(nodes1);
  1206. };
  1207. function PathExpr(filter, rel) {
  1208. this.filter = filter;
  1209. this.rel = rel;
  1210. }
  1211. PathExpr.prototype.evaluate = function(ctx) {
  1212. // the filter expression should be evaluated in its entirety with no
  1213. // optimization, as we can't backtrack to it after having moved on to
  1214. // evaluating the relative location path
  1215. var flag = ctx.returnOnFirstMatch;
  1216. ctx.setReturnOnFirstMatch(false);
  1217. var nodes = this.filter.evaluate(ctx).nodeSetValue();
  1218. ctx.setReturnOnFirstMatch(flag);
  1219. var nodes1 = [];
  1220. if (ctx.returnOnFirstMatch) {
  1221. for (var i = 0; i < nodes.length; ++i) {
  1222. nodes1 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue();
  1223. if (nodes1.length > 0) {
  1224. break;
  1225. }
  1226. }
  1227. return new NodeSetValue(nodes1);
  1228. }
  1229. else {
  1230. for (var i = 0; i < nodes.length; ++i) {
  1231. var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue();
  1232. for (var ii = 0; ii < nodes0.length; ++ii) {
  1233. nodes1.push(nodes0[ii]);
  1234. }
  1235. }
  1236. return new NodeSetValue(nodes1);
  1237. }
  1238. };
  1239. function FilterExpr(expr, predicate) {
  1240. this.expr = expr;
  1241. this.predicate = predicate;
  1242. }
  1243. FilterExpr.prototype.evaluate = function(ctx) {
  1244. var nodes = this.expr.evaluate(ctx).nodeSetValue();
  1245. for (var i = 0; i < this.predicate.length; ++i) {
  1246. var nodes0 = nodes;
  1247. nodes = [];
  1248. for (var j = 0; j < nodes0.length; ++j) {
  1249. var n = nodes0[j];
  1250. if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) {
  1251. nodes.push(n);
  1252. }
  1253. }
  1254. }
  1255. return new NodeSetValue(nodes);
  1256. }
  1257. function UnaryMinusExpr(expr) {
  1258. this.expr = expr;
  1259. }
  1260. UnaryMinusExpr.prototype.evaluate = function(ctx) {
  1261. return new NumberValue(-this.expr.evaluate(ctx).numberValue());
  1262. };
  1263. function BinaryExpr(expr1, op, expr2) {
  1264. this.expr1 = expr1;
  1265. this.expr2 = expr2;
  1266. this.op = op;
  1267. }
  1268. BinaryExpr.prototype.evaluate = function(ctx) {
  1269. var ret;
  1270. switch (this.op.value) {
  1271. case 'or':
  1272. ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() ||
  1273. this.expr2.evaluate(ctx).booleanValue());
  1274. break;
  1275. case 'and':
  1276. ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() &&
  1277. this.expr2.evaluate(ctx).booleanValue());
  1278. break;
  1279. case '+':
  1280. ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() +
  1281. this.expr2.evaluate(ctx).numberValue());
  1282. break;
  1283. case '-':
  1284. ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() -
  1285. this.expr2.evaluate(ctx).numberValue());
  1286. break;
  1287. case '*':
  1288. ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() *
  1289. this.expr2.evaluate(ctx).numberValue());
  1290. break;
  1291. case 'mod':
  1292. ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() %
  1293. this.expr2.evaluate(ctx).numberValue());
  1294. break;
  1295. case 'div':
  1296. ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() /
  1297. this.expr2.evaluate(ctx).numberValue());
  1298. break;
  1299. case '=':
  1300. ret = this.compare(ctx, function(x1, x2) { return x1 == x2; });
  1301. break;
  1302. case '!=':
  1303. ret = this.compare(ctx, function(x1, x2) { return x1 != x2; });
  1304. break;
  1305. case '<':
  1306. ret = this.compare(ctx, function(x1, x2) { return x1 < x2; });
  1307. break;
  1308. case '<=':
  1309. ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; });
  1310. break;
  1311. case '>':
  1312. ret = this.compare(ctx, function(x1, x2) { return x1 > x2; });
  1313. break;
  1314. case '>=':
  1315. ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; });
  1316. break;
  1317. default:
  1318. alert('BinaryExpr.evaluate: ' + this.op.value);
  1319. }
  1320. return ret;
  1321. };
  1322. BinaryExpr.prototype.compare = function(ctx, cmp) {
  1323. var v1 = this.expr1.evaluate(ctx);
  1324. var v2 = this.expr2.evaluate(ctx);
  1325. var ret;
  1326. if (v1.type == 'node-set' && v2.type == 'node-set') {
  1327. var n1 = v1.nodeSetValue();
  1328. var n2 = v2.nodeSetValue();
  1329. ret = false;
  1330. for (var i1 = 0; i1 < n1.length; ++i1) {
  1331. for (var i2 = 0; i2 < n2.length; ++i2) {
  1332. if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) {
  1333. ret = true;
  1334. // Break outer loop. Labels confuse the jscompiler and we
  1335. // don't use them.
  1336. i2 = n2.length;
  1337. i1 = n1.length;
  1338. }
  1339. }
  1340. }
  1341. } else if (v1.type == 'node-set' || v2.type == 'node-set') {
  1342. if (v1.type == 'number') {
  1343. var s = v1.numberValue();
  1344. var n = v2.nodeSetValue();
  1345. ret = false;
  1346. for (var i = 0; i < n.length; ++i) {
  1347. var nn = xmlValue(n[i]) - 0;
  1348. if (cmp(s, nn)) {
  1349. ret = true;
  1350. break;
  1351. }
  1352. }
  1353. } else if (v2.type == 'number') {
  1354. var n = v1.nodeSetValue();
  1355. var s = v2.numberValue();
  1356. ret = false;
  1357. for (var i = 0; i < n.length; ++i) {
  1358. var nn = xmlValue(n[i]) - 0;
  1359. if (cmp(nn, s)) {
  1360. ret = true;
  1361. break;
  1362. }
  1363. }
  1364. } else if (v1.type == 'string') {
  1365. var s = v1.stringValue();
  1366. var n = v2.nodeSetValue();
  1367. ret = false;
  1368. for (var i = 0; i < n.length; ++i) {
  1369. var nn = xmlValue(n[i]);
  1370. if (cmp(s, nn)) {
  1371. ret = true;
  1372. break;
  1373. }
  1374. }
  1375. } else if (v2.type == 'string') {
  1376. var n = v1.nodeSetValue();
  1377. var s = v2.stringValue();
  1378. ret = false;
  1379. for (var i = 0; i < n.length; ++i) {
  1380. var nn = xmlValue(n[i]);
  1381. if (cmp(nn, s)) {
  1382. ret = true;
  1383. break;
  1384. }
  1385. }
  1386. } else {
  1387. ret = cmp(v1.booleanValue(), v2.booleanValue());
  1388. }
  1389. } else if (v1.type == 'boolean' || v2.type == 'boolean') {
  1390. ret = cmp(v1.booleanValue(), v2.booleanValue());
  1391. } else if (v1.type == 'number' || v2.type == 'number') {
  1392. ret = cmp(v1.numberValue(), v2.numberValue());
  1393. } else {
  1394. ret = cmp(v1.stringValue(), v2.stringValue());
  1395. }
  1396. return new BooleanValue(ret);
  1397. }
  1398. function LiteralExpr(value) {
  1399. this.value = value;
  1400. }
  1401. LiteralExpr.prototype.evaluate = function(ctx) {
  1402. return new StringValue(this.value);
  1403. };
  1404. function NumberExpr(value) {
  1405. this.value = value;
  1406. }
  1407. NumberExpr.prototype.evaluate = function(ctx) {
  1408. return new NumberValue(this.value);
  1409. };
  1410. function VariableExpr(name) {
  1411. this.name = name;
  1412. }
  1413. VariableExpr.prototype.evaluate = function(ctx) {
  1414. return ctx.getVariable(this.name);
  1415. }
  1416. // Factory functions for semantic values (i.e. Expressions) of the
  1417. // productions in the grammar. When a production is matched to reduce
  1418. // the current parse state stack, the function is called with the
  1419. // semantic values of the matched elements as arguments, and returns
  1420. // another semantic value. The semantic value is a node of the parse
  1421. // tree, an expression object with an evaluate() method that evaluates the
  1422. // expression in an actual context. These factory functions are used
  1423. // in the specification of the grammar rules, below.
  1424. function makeTokenExpr(m) {
  1425. return new TokenExpr(m);
  1426. }
  1427. function passExpr(e) {
  1428. return e;
  1429. }
  1430. function makeLocationExpr1(slash, rel) {
  1431. rel.absolute = true;
  1432. return rel;
  1433. }
  1434. function makeLocationExpr2(dslash, rel) {
  1435. rel.absolute = true;
  1436. rel.prependStep(makeAbbrevStep(dslash.value));
  1437. return rel;
  1438. }
  1439. function makeLocationExpr3(slash) {
  1440. var ret = new LocationExpr();
  1441. ret.appendStep(makeAbbrevStep('.'));
  1442. ret.absolute = true;
  1443. return ret;
  1444. }
  1445. function makeLocationExpr4(dslash) {
  1446. var ret = new LocationExpr();
  1447. ret.absolute = true;
  1448. ret.appendStep(makeAbbrevStep(dslash.value));
  1449. return ret;
  1450. }
  1451. function makeLocationExpr5(step) {
  1452. var ret = new LocationExpr();
  1453. ret.appendStep(step);
  1454. return ret;
  1455. }
  1456. function makeLocationExpr6(rel, slash, step) {
  1457. rel.appendStep(step);
  1458. return rel;
  1459. }
  1460. function makeLocationExpr7(rel, dslash, step) {
  1461. rel.appendStep(makeAbbrevStep(dslash.value));
  1462. rel.appendStep(step);
  1463. return rel;
  1464. }
  1465. function makeStepExpr1(dot) {
  1466. return makeAbbrevStep(dot.value);
  1467. }
  1468. function makeStepExpr2(ddot) {
  1469. return makeAbbrevStep(ddot.value);
  1470. }
  1471. function makeStepExpr3(axisname, axis, nodetest) {
  1472. return new StepExpr(axisname.value, nodetest);
  1473. }
  1474. function makeStepExpr4(at, nodetest) {
  1475. return new StepExpr('attribute', nodetest);
  1476. }
  1477. function makeStepExpr5(nodetest) {
  1478. return new StepExpr('child', nodetest);
  1479. }
  1480. function makeStepExpr6(step, predicate) {
  1481. step.appendPredicate(predicate);
  1482. return step;
  1483. }
  1484. function makeAbbrevStep(abbrev) {
  1485. switch (abbrev) {
  1486. case '//':
  1487. return new StepExpr('descendant-or-self', new NodeTestAny);
  1488. case '.':
  1489. return new StepExpr('self', new NodeTestAny);
  1490. case '..':
  1491. return new StepExpr('parent', new NodeTestAny);
  1492. }
  1493. }
  1494. function makeNodeTestExpr1(asterisk) {
  1495. return new NodeTestElementOrAttribute;
  1496. }
  1497. function makeNodeTestExpr2(ncname, colon, asterisk) {
  1498. return new NodeTestNC(ncname.value);
  1499. }
  1500. function makeNodeTestExpr3(qname) {
  1501. return new NodeTestName(qname.value);
  1502. }
  1503. function makeNodeTestExpr4(typeo, parenc) {
  1504. var type = typeo.value.replace(/\s*\($/, '');
  1505. switch(type) {
  1506. case 'node':
  1507. return new NodeTestAny;
  1508. case 'text':
  1509. return new NodeTestText;
  1510. case 'comment':
  1511. return new NodeTestComment;
  1512. case 'processing-instruction':
  1513. return new NodeTestPI('');
  1514. }
  1515. }
  1516. function makeNodeTestExpr5(typeo, target, parenc) {
  1517. var type = typeo.replace(/\s*\($/, '');
  1518. if (type != 'processing-instruction') {
  1519. throw type;
  1520. }
  1521. return new NodeTestPI(target.value);
  1522. }
  1523. function makePredicateExpr(pareno, expr, parenc) {
  1524. return new PredicateExpr(expr);
  1525. }
  1526. function makePrimaryExpr(pareno, expr, parenc) {
  1527. return expr;
  1528. }
  1529. function makeFunctionCallExpr1(name, pareno, parenc) {
  1530. return new FunctionCallExpr(name);
  1531. }
  1532. function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) {
  1533. var ret = new FunctionCallExpr(name);
  1534. ret.appendArg(arg1);
  1535. for (var i = 0; i < args.length; ++i) {
  1536. ret.appendArg(args[i]);
  1537. }
  1538. return ret;
  1539. }
  1540. function makeArg