Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80684 views
1
/** Here is yet another implementation of XPath 1.0 in Javascript.
2
*
3
* My goal was to make it relatively compact, but as I fixed all the axis bugs
4
* the axes became more and more complicated. :-(.
5
*
6
* I have not implemented namespaces or case-sensitive axes for XML yet.
7
*
8
* How to test it in Chrome: You can make a Chrome extension that replaces
9
* the WebKit XPath parser with this one. But it takes a bit of effort to
10
* get around isolated world and same-origin restrictions:
11
* manifest.json:
12
{
13
"name": "XPathTest",
14
"version": "0.1",
15
"content_scripts": [{
16
"matches": ["http://localhost/*"], // or wildcard host
17
"js": ["xpath.js", "injection.js"],
18
"all_frames": true, "run_at": "document_start"
19
}]
20
}
21
* injection.js:
22
// goal: give my xpath object to the website's JS context.
23
var script = document.createElement('script');
24
script.textContent =
25
"document.addEventListener('xpathextend', function(e) {\n" +
26
" console.log('extending document with xpath...');\n" +
27
" e.detail(window);" +
28
"});";
29
document.documentElement.appendChild(script);
30
document.documentElement.removeChild(script);
31
var evt = document.createEvent('CustomEvent');
32
evt.initCustomEvent('xpathextend', true, true, this.xpath.extend);
33
document.dispatchEvent(evt);
34
*/
35
(function() {
36
var core;
37
var xpath;
38
if ('function' === typeof require) {
39
core = require("../level3/core").dom.level3.core;
40
xpath = exports;
41
} else {
42
core = this;
43
xpath = {};
44
}
45
46
47
/***************************************************************************
48
* Tokenization *
49
***************************************************************************/
50
/**
51
* The XPath lexer is basically a single regular expression, along with
52
* some helper functions to pop different types.
53
*/
54
var Stream = xpath.Stream = function Stream(str) {
55
this.original = this.str = str;
56
this.peeked = null;
57
// TODO: not really needed, but supposedly tokenizer also disambiguates
58
// a * b vs. node test *
59
this.prev = null; // for debugging
60
this.prevprev = null;
61
}
62
Stream.prototype = {
63
peek: function() {
64
if (this.peeked) return this.peeked;
65
var m = this.re.exec(this.str);
66
if (!m) return null;
67
this.str = this.str.substr(m[0].length);
68
return this.peeked = m[1];
69
},
70
/** Peek 2 tokens ahead. */
71
peek2: function() {
72
this.peek(); // make sure this.peeked is set
73
var m = this.re.exec(this.str);
74
if (!m) return null;
75
return m[1];
76
},
77
pop: function() {
78
var r = this.peek();
79
this.peeked = null;
80
this.prevprev = this.prev;
81
this.prev = r;
82
return r;
83
},
84
trypop: function(tokens) {
85
var tok = this.peek();
86
if (tok === tokens) return this.pop();
87
if (Array.isArray(tokens)) {
88
for (var i = 0; i < tokens.length; ++i) {
89
var t = tokens[i];
90
if (t == tok) return this.pop();;
91
}
92
}
93
},
94
trypopfuncname: function() {
95
var tok = this.peek();
96
if (!this.isQnameRe.test(tok))
97
return null;
98
switch (tok) {
99
case 'comment': case 'text': case 'processing-instruction': case 'node':
100
return null;
101
}
102
if ('(' != this.peek2()) return null;
103
return this.pop();
104
},
105
trypopaxisname: function() {
106
var tok = this.peek();
107
switch (tok) {
108
case 'ancestor': case 'ancestor-or-self': case 'attribute':
109
case 'child': case 'descendant': case 'descendant-or-self':
110
case 'following': case 'following-sibling': case 'namespace':
111
case 'parent': case 'preceding': case 'preceding-sibling': case 'self':
112
if ('::' == this.peek2()) return this.pop();
113
}
114
return null;
115
},
116
trypopnametest: function() {
117
var tok = this.peek();
118
if ('*' === tok || this.startsWithNcNameRe.test(tok)) return this.pop();
119
return null;
120
},
121
trypopliteral: function() {
122
var tok = this.peek();
123
if (null == tok) return null;
124
var first = tok.charAt(0);
125
var last = tok.charAt(tok.length - 1);
126
if ('"' === first && '"' === last ||
127
"'" === first && "'" === last) {
128
this.pop();
129
return tok.substr(1, tok.length - 2);
130
}
131
},
132
trypopnumber: function() {
133
var tok = this.peek();
134
if (this.isNumberRe.test(tok)) return parseFloat(this.pop());
135
else return null;
136
},
137
trypopvarref: function() {
138
var tok = this.peek();
139
if (null == tok) return null;
140
if ('$' === tok.charAt(0)) return this.pop().substr(1);
141
else return null;
142
},
143
position: function() {
144
return this.original.length - this.str.length;
145
}
146
};
147
(function() {
148
// http://www.w3.org/TR/REC-xml-names/#NT-NCName
149
var nameStartCharsExceptColon =
150
'A-Z_a-z\xc0-\xd6\xd8-\xf6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF' +
151
'\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF' +
152
'\uFDF0-\uFFFD'; // JS doesn't support [#x10000-#xEFFFF]
153
var nameCharExceptColon = nameStartCharsExceptColon +
154
'\\-\\.0-9\xb7\u0300-\u036F\u203F-\u2040';
155
var ncNameChars = '[' + nameStartCharsExceptColon +
156
'][' + nameCharExceptColon + ']*'
157
// http://www.w3.org/TR/REC-xml-names/#NT-QName
158
var qNameChars = ncNameChars + '(?::' + ncNameChars + ')?';
159
var otherChars = '\\.\\.|[\\(\\)\\[\\].@,]|::'; // .. must come before [.]
160
var operatorChars =
161
'and|or|mod|div|' +
162
'//|!=|<=|>=|[*/|+\\-=<>]'; // //, !=, <=, >= before individual ones.
163
var literal = '"[^"]*"|' + "'[^']*'";
164
var numberChars = '[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+';
165
var variableReference = '\\$' + qNameChars;
166
var nameTestChars = '\\*|' + ncNameChars + ':\\*|' + qNameChars;
167
var optionalSpace = '[ \t\r\n]*'; // stricter than regexp \s.
168
var nodeType = 'comment|text|processing-instruction|node';
169
var re = new RegExp(
170
// numberChars before otherChars so that leading-decimal doesn't become .
171
'^' + optionalSpace + '(' + numberChars + '|' + otherChars + '|' +
172
nameTestChars + '|' + operatorChars + '|' + literal + '|' +
173
variableReference + ')'
174
// operatorName | nodeType | functionName | axisName are lumped into
175
// qName for now; we'll check them on pop.
176
);
177
Stream.prototype.re = re;
178
Stream.prototype.startsWithNcNameRe = new RegExp('^' + ncNameChars);
179
Stream.prototype.isQnameRe = new RegExp('^' + qNameChars + '$');
180
Stream.prototype.isNumberRe = new RegExp('^' + numberChars + '$');
181
})();
182
183
/***************************************************************************
184
* Parsing *
185
***************************************************************************/
186
var parse = xpath.parse = function parse(stream, a) {
187
var r = orExpr(stream,a);
188
var x, unparsed = [];
189
while (x = stream.pop()) {
190
unparsed.push(x);
191
}
192
if (unparsed.length)
193
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
194
'Position ' + stream.position() +
195
': Unparsed tokens: ' + unparsed.join(' '));
196
return r;
197
}
198
199
/**
200
* binaryL ::= subExpr
201
* | binaryL op subExpr
202
* so a op b op c becomes ((a op b) op c)
203
*/
204
function binaryL(subExpr, stream, a, ops) {
205
var lhs = subExpr(stream, a);
206
if (lhs == null) return null;
207
var op;
208
while (op = stream.trypop(ops)) {
209
var rhs = subExpr(stream, a);
210
if (rhs == null)
211
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
212
'Position ' + stream.position() +
213
': Expected something after ' + op);
214
lhs = a.node(op, lhs, rhs);
215
}
216
return lhs;
217
}
218
/**
219
* Too bad this is never used. If they made a ** operator (raise to power),
220
( we would use it.
221
* binaryR ::= subExpr
222
* | subExpr op binaryR
223
* so a op b op c becomes (a op (b op c))
224
*/
225
function binaryR(subExpr, stream, a, ops) {
226
var lhs = subExpr(stream, a);
227
if (lhs == null) return null;
228
var op = stream.trypop(ops);
229
if (op) {
230
var rhs = binaryR(stream, a);
231
if (rhs == null)
232
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
233
'Position ' + stream.position() +
234
': Expected something after ' + op);
235
return a.node(op, lhs, rhs);
236
} else {
237
return lhs;// TODO
238
}
239
}
240
/** [1] LocationPath::= RelativeLocationPath | AbsoluteLocationPath
241
* e.g. a, a/b, //a/b
242
*/
243
function locationPath(stream, a) {
244
return absoluteLocationPath(stream, a) ||
245
relativeLocationPath(null, stream, a);
246
}
247
/** [2] AbsoluteLocationPath::= '/' RelativeLocationPath? | AbbreviatedAbsoluteLocationPath
248
* [10] AbbreviatedAbsoluteLocationPath::= '//' RelativeLocationPath
249
*/
250
function absoluteLocationPath(stream, a) {
251
var op = stream.peek();
252
if ('/' === op || '//' === op) {
253
var lhs = a.node('Root');
254
return relativeLocationPath(lhs, stream, a, true);
255
} else {
256
return null;
257
}
258
}
259
/** [3] RelativeLocationPath::= Step | RelativeLocationPath '/' Step |
260
* | AbbreviatedRelativeLocationPath
261
* [11] AbbreviatedRelativeLocationPath::= RelativeLocationPath '//' Step
262
* e.g. p/a, etc.
263
*/
264
function relativeLocationPath(lhs, stream, a, isOnlyRootOk) {
265
if (null == lhs) {
266
lhs = step(stream, a);
267
if (null == lhs) return lhs;
268
}
269
var op;
270
while (op = stream.trypop(['/', '//'])) {
271
if ('//' === op) {
272
lhs = a.node('/', lhs,
273
a.node('Axis', 'descendant-or-self', 'node', undefined));
274
}
275
var rhs = step(stream, a);
276
if (null == rhs && '/' === op && isOnlyRootOk) return lhs;
277
else isOnlyRootOk = false;
278
if (null == rhs)
279
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
280
'Position ' + stream.position() +
281
': Expected step after ' + op);
282
lhs = a.node('/', lhs, rhs);
283
}
284
return lhs;
285
}
286
/** [4] Step::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
287
* [12] AbbreviatedStep::= '.' | '..'
288
* e.g. @href, self::p, p, a[@href], ., ..
289
*/
290
function step(stream, a) {
291
var abbrStep = stream.trypop(['.', '..']);
292
if ('.' === abbrStep) // A location step of . is short for self::node().
293
return a.node('Axis', 'self', 'node');
294
if ('..' === abbrStep) // A location step of .. is short for parent::node()
295
return a.node('Axis', 'parent', 'node');
296
297
var axis = axisSpecifier(stream, a);
298
var nodeType = nodeTypeTest(stream, a);
299
var nodeName;
300
if (null == nodeType) nodeName = nodeNameTest(stream, a);
301
if (null == axis && null == nodeType && null == nodeName) return null;
302
if (null == nodeType && null == nodeName)
303
throw new XPathException(
304
XPathException.INVALID_EXPRESSION_ERR,
305
'Position ' + stream.position() +
306
': Expected nodeTest after axisSpecifier ' + axis);
307
if (null == axis) axis = 'child';
308
if (null == nodeType) {
309
// When there's only a node name, then the node type is forced to be the
310
// principal node type of the axis.
311
// see http://www.w3.org/TR/xpath/#dt-principal-node-type
312
if ('attribute' === axis) nodeType = 'attribute';
313
else if ('namespace' === axis) nodeType = 'namespace';
314
else nodeType = 'element';
315
}
316
var lhs = a.node('Axis', axis, nodeType, nodeName);
317
var pred;
318
while (null != (pred = predicate(lhs, stream, a))) {
319
lhs = pred;
320
}
321
return lhs;
322
}
323
/** [5] AxisSpecifier::= AxisName '::' | AbbreviatedAxisSpecifier
324
* [6] AxisName::= 'ancestor' | 'ancestor-or-self' | 'attribute' | 'child'
325
* | 'descendant' | 'descendant-or-self' | 'following'
326
* | 'following-sibling' | 'namespace' | 'parent' |
327
* | 'preceding' | 'preceding-sibling' | 'self'
328
* [13] AbbreviatedAxisSpecifier::= '@'?
329
*/
330
function axisSpecifier(stream, a) {
331
var attr = stream.trypop('@');
332
if (null != attr) return 'attribute';
333
var axisName = stream.trypopaxisname();
334
if (null != axisName) {
335
var coloncolon = stream.trypop('::');
336
if (null == coloncolon)
337
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
338
'Position ' + stream.position() +
339
': Should not happen. Should be ::.');
340
return axisName;
341
}
342
}
343
/** [7] NodeTest::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
344
* [38] NodeType::= 'comment' | 'text' | 'processing-instruction' | 'node'
345
* I've split nodeTypeTest from nodeNameTest for convenience.
346
*/
347
function nodeTypeTest(stream, a) {
348
if ('(' !== stream.peek2()) {
349
return null;
350
}
351
var type = stream.trypop(['comment', 'text', 'processing-instruction', 'node']);
352
if (null != type) {
353
if (null == stream.trypop('('))
354
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
355
'Position ' + stream.position() +
356
': Should not happen.');
357
var param = undefined;
358
if (type == 'processing-instruction') {
359
param = stream.trypopliteral();
360
}
361
if (null == stream.trypop(')'))
362
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
363
'Position ' + stream.position() +
364
': Expected close parens.');
365
return type
366
}
367
}
368
function nodeNameTest(stream, a) {
369
var name = stream.trypopnametest();
370
if (name != null) return name;
371
else return null;
372
}
373
/** [8] Predicate::= '[' PredicateExpr ']'
374
* [9] PredicateExpr::= Expr
375
*/
376
function predicate(lhs, stream, a) {
377
if (null == stream.trypop('[')) return null;
378
var expr = orExpr(stream, a);
379
if (null == expr)
380
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
381
'Position ' + stream.position() +
382
': Expected expression after [');
383
if (null == stream.trypop(']'))
384
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
385
'Position ' + stream.position() +
386
': Expected ] after expression.');
387
return a.node('Predicate', lhs, expr);
388
}
389
/** [14] Expr::= OrExpr
390
*/
391
/** [15] PrimaryExpr::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
392
* e.g. $x, (3+4), "hi", 32, f(x)
393
*/
394
function primaryExpr(stream, a) {
395
var x = stream.trypopliteral();
396
if (null == x)
397
x = stream.trypopnumber();
398
if (null != x) {
399
return x;
400
}
401
var varRef = stream.trypopvarref();
402
if (null != varRef) return a.node('VariableReference', varRef);
403
var funCall = functionCall(stream, a);
404
if (null != funCall) {
405
return funCall;
406
}
407
if (stream.trypop('(')) {
408
var e = orExpr(stream, a);
409
if (null == e)
410
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
411
'Position ' + stream.position() +
412
': Expected expression after (.');
413
if (null == stream.trypop(')'))
414
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
415
'Position ' + stream.position() +
416
': Expected ) after expression.');
417
return e;
418
}
419
return null;
420
}
421
/** [16] FunctionCall::= FunctionName '(' ( Argument ( ',' Argument )* )? ')'
422
* [17] Argument::= Expr
423
*/
424
function functionCall(stream, a) {
425
var name = stream.trypopfuncname(stream, a);
426
if (null == name) return null;
427
if (null == stream.trypop('('))
428
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
429
'Position ' + stream.position() +
430
': Expected ( ) after function name.');
431
var params = [];
432
var first = true;
433
while (null == stream.trypop(')')) {
434
if (!first && null == stream.trypop(','))
435
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
436
'Position ' + stream.position() +
437
': Expected , between arguments of the function.');
438
first = false;
439
var param = orExpr(stream, a);
440
if (param == null)
441
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
442
'Position ' + stream.position() +
443
': Expected expression as argument of function.');
444
params.push(param);
445
}
446
return a.node('FunctionCall', name, params);
447
}
448
449
/** [18] UnionExpr::= PathExpr | UnionExpr '|' PathExpr
450
*/
451
function unionExpr(stream, a) { return binaryL(pathExpr, stream, a, '|'); }
452
/** [19] PathExpr ::= LocationPath
453
* | FilterExpr
454
* | FilterExpr '/' RelativeLocationPath
455
* | FilterExpr '//' RelativeLocationPath
456
* Unlike most other nodes, this one always generates a node because
457
* at this point all reverse nodesets must turn into a forward nodeset
458
*/
459
function pathExpr(stream, a) {
460
// We have to do FilterExpr before LocationPath because otherwise
461
// LocationPath will eat up the name from a function call.
462
var filter = filterExpr(stream, a);
463
if (null == filter) {
464
var loc = locationPath(stream, a);
465
if (null == loc) {
466
throw new Error
467
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
468
'Position ' + stream.position() +
469
': The expression shouldn\'t be empty...');
470
}
471
return a.node('PathExpr', loc);
472
}
473
var rel = relativeLocationPath(filter, stream, a, false);
474
if (filter === rel) return rel;
475
else return a.node('PathExpr', rel);
476
}
477
/** [20] FilterExpr::= PrimaryExpr | FilterExpr Predicate
478
* aka. FilterExpr ::= PrimaryExpr Predicate*
479
*/
480
function filterExpr(stream, a) {
481
var primary = primaryExpr(stream, a);
482
if (primary == null) return null;
483
var pred, lhs = primary;
484
while (null != (pred = predicate(lhs, stream, a))) {
485
lhs = pred;
486
}
487
return lhs;
488
}
489
490
/** [21] OrExpr::= AndExpr | OrExpr 'or' AndExpr
491
*/
492
function orExpr(stream, a) {
493
var orig = (stream.peeked || '') + stream.str
494
var r = binaryL(andExpr, stream, a, 'or');
495
var now = (stream.peeked || '') + stream.str;
496
return r;
497
}
498
/** [22] AndExpr::= EqualityExpr | AndExpr 'and' EqualityExpr
499
*/
500
function andExpr(stream, a) { return binaryL(equalityExpr, stream, a, 'and'); }
501
/** [23] EqualityExpr::= RelationalExpr | EqualityExpr '=' RelationalExpr
502
* | EqualityExpr '!=' RelationalExpr
503
*/
504
function equalityExpr(stream, a) { return binaryL(relationalExpr, stream, a, ['=','!=']); }
505
/** [24] RelationalExpr::= AdditiveExpr | RelationalExpr '<' AdditiveExpr
506
* | RelationalExpr '>' AdditiveExpr
507
* | RelationalExpr '<=' AdditiveExpr
508
* | RelationalExpr '>=' AdditiveExpr
509
*/
510
function relationalExpr(stream, a) { return binaryL(additiveExpr, stream, a, ['<','>','<=','>=']); }
511
/** [25] AdditiveExpr::= MultiplicativeExpr
512
* | AdditiveExpr '+' MultiplicativeExpr
513
* | AdditiveExpr '-' MultiplicativeExpr
514
*/
515
function additiveExpr(stream, a) { return binaryL(multiplicativeExpr, stream, a, ['+','-']); }
516
/** [26] MultiplicativeExpr::= UnaryExpr
517
* | MultiplicativeExpr MultiplyOperator UnaryExpr
518
* | MultiplicativeExpr 'div' UnaryExpr
519
* | MultiplicativeExpr 'mod' UnaryExpr
520
*/
521
function multiplicativeExpr(stream, a) { return binaryL(unaryExpr, stream, a, ['*','div','mod']); }
522
/** [27] UnaryExpr::= UnionExpr | '-' UnaryExpr
523
*/
524
function unaryExpr(stream, a) {
525
if (stream.trypop('-')) {
526
var e = unaryExpr(stream, a);
527
if (null == e)
528
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
529
'Position ' + stream.position() +
530
': Expected unary expression after -');
531
return a.node('UnaryMinus', e);
532
}
533
else return unionExpr(stream, a);
534
}
535
var astFactory = {
536
node: function() {return Array.prototype.slice.call(arguments);}
537
};
538
539
540
/***************************************************************************
541
* Optimizations (TODO) *
542
***************************************************************************/
543
/**
544
* Some things I've been considering:
545
* 1) a//b becomes a/descendant::b if there's no predicate that uses
546
* position() or last()
547
* 2) axis[pred]: when pred doesn't use position, evaluate it just once per
548
* node in the node-set rather than once per (node, position, last).
549
* For more optimizations, look up Gecko's optimizer:
550
* http://mxr.mozilla.org/mozilla-central/source/content/xslt/src/xpath/txXPathOptimizer.cpp
551
*/
552
// TODO
553
function optimize(ast) {
554
}
555
556
/***************************************************************************
557
* Evaluation: axes *
558
***************************************************************************/
559
560
/**
561
* Data types: For string, number, boolean, we just use Javascript types.
562
* Node-sets have the form
563
* {nodes: [node, ...]}
564
* or {nodes: [node, ...], pos: [[1], [2], ...], lasts: [[1], [2], ...]}
565
*
566
* Most of the time, only the node is used and the position information is
567
* discarded. But if you use a predicate, we need to try every value of
568
* position and last in case the predicate calls position() or last().
569
*/
570
571
/**
572
* The NodeMultiSet is a helper class to help generate
573
* {nodes:[], pos:[], lasts:[]} structures. It is useful for the
574
* descendant, descendant-or-self, following-sibling, and
575
* preceding-sibling axes for which we can use a stack to organize things.
576
*/
577
function NodeMultiSet(isReverseAxis) {
578
this.nodes = [];
579
this.pos = [];
580
this.lasts = [];
581
this.nextPos = [];
582
this.seriesIndexes = []; // index within nodes that each series begins.
583
this.isReverseAxis = isReverseAxis;
584
this._pushToNodes = isReverseAxis ? Array.prototype.unshift : Array.prototype.push;
585
}
586
NodeMultiSet.prototype = {
587
pushSeries: function pushSeries() {
588
this.nextPos.push(1);
589
this.seriesIndexes.push(this.nodes.length);
590
},
591
popSeries: function popSeries() {
592
console.assert(0 < this.nextPos.length, this.nextPos);
593
var last = this.nextPos.pop() - 1,
594
indexInPos = this.nextPos.length,
595
seriesBeginIndex = this.seriesIndexes.pop(),
596
seriesEndIndex = this.nodes.length;
597
for (var i = seriesBeginIndex; i < seriesEndIndex; ++i) {
598
console.assert(indexInPos < this.lasts[i].length);
599
console.assert(undefined === this.lasts[i][indexInPos]);
600
this.lasts[i][indexInPos] = last;
601
}
602
},
603
finalize: function() {
604
if (null == this.nextPos) return this;
605
console.assert(0 === this.nextPos.length);
606
for (var i = 0; i < this.lasts.length; ++i) {
607
for (var j = 0; j < this.lasts[i].length; ++j) {
608
console.assert(null != this.lasts[i][j], i + ',' + j + ':' + JSON.stringify(this.lasts));
609
}
610
}
611
this.pushSeries = this.popSeries = this.addNode = function() {
612
throw new Error('Already finalized.');
613
};
614
return this;
615
},
616
addNode: function addNode(node) {
617
console.assert(node);
618
this._pushToNodes.call(this.nodes, node)
619
this._pushToNodes.call(this.pos, this.nextPos.slice());
620
this._pushToNodes.call(this.lasts, new Array(this.nextPos.length));
621
for (var i = 0; i < this.nextPos.length; ++i) this.nextPos[i]++;
622
},
623
simplify: function() {
624
this.finalize();
625
return {nodes:this.nodes, pos:this.pos, lasts:this.lasts};
626
}
627
};
628
function eachContext(nodeMultiSet) {
629
var r = [];
630
for (var i = 0; i < nodeMultiSet.nodes.length; i++) {
631
var node = nodeMultiSet.nodes[i];
632
if (!nodeMultiSet.pos) {
633
r.push({nodes:[node], pos: [[i + 1]], lasts: [[nodeMultiSet.nodes.length]]});
634
} else {
635
for (var j = 0; j < nodeMultiSet.pos[i].length; ++j) {
636
r.push({nodes:[node], pos: [[nodeMultiSet.pos[i][j]]], lasts: [[nodeMultiSet.lasts[i][j]]]});
637
}
638
}
639
}
640
return r;
641
}
642
/** Matcher used in the axes.
643
*/
644
function NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase) {
645
this.nodeTypeNum = nodeTypeNum;
646
this.nodeName = nodeName;
647
this.shouldLowerCase = shouldLowerCase;
648
this.nodeNameTest =
649
null == nodeName ? this._alwaysTrue :
650
shouldLowerCase ? this._nodeNameLowerCaseEquals :
651
this._nodeNameEquals;
652
}
653
NodeMatcher.prototype = {
654
matches: function matches(node) {
655
return (0 === this.nodeTypeNum || node.nodeType === this.nodeTypeNum) &&
656
this.nodeNameTest(node.nodeName);
657
},
658
_alwaysTrue: function(name) {return true;},
659
_nodeNameEquals: function _nodeNameEquals(name) {
660
return this.nodeName === name;
661
},
662
_nodeNameLowerCaseEquals: function _nodeNameLowerCaseEquals(name) {
663
return this.nodeName === name.toLowerCase();
664
}
665
};
666
667
function followingSiblingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, shift, peek, followingNode, andSelf, isReverseAxis) {
668
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
669
var nodeMultiSet = new NodeMultiSet(isReverseAxis);
670
while (0 < nodeList.length) { // can be if for following, preceding
671
var node = shift.call(nodeList);
672
console.assert(node != null);
673
node = followingNode(node);
674
nodeMultiSet.pushSeries();
675
var numPushed = 1;
676
while (null != node) {
677
if (! andSelf && matcher.matches(node))
678
nodeMultiSet.addNode(node);
679
if (node === peek.call(nodeList)) {
680
shift.call(nodeList);
681
nodeMultiSet.pushSeries();
682
numPushed++;
683
}
684
if (andSelf && matcher.matches(node))
685
nodeMultiSet.addNode(node);
686
node = followingNode(node);
687
}
688
while (0 < numPushed--)
689
nodeMultiSet.popSeries();
690
}
691
return nodeMultiSet;
692
}
693
694
/** Returns the next non-descendant node in document order.
695
* This is the first node in following::node(), if node is the context.
696
*/
697
function followingNonDescendantNode(node) {
698
if (node.ownerElement) {
699
if (node.ownerElement.firstChild)
700
return node.ownerElement.firstChild;
701
node = node.ownerElement;
702
}
703
do {
704
if (node.nextSibling) return node.nextSibling;
705
} while (node = node.parentNode);
706
return null;
707
}
708
709
/** Returns the next node in a document-order depth-first search.
710
* See the definition of document order[1]:
711
* 1) element
712
* 2) namespace nodes
713
* 3) attributes
714
* 4) children
715
* [1]: http://www.w3.org/TR/xpath/#dt-document-order
716
*/
717
function followingNode(node) {
718
if (node.ownerElement) // attributes: following node of element.
719
node = node.ownerElement;
720
if (null != node.firstChild)
721
return node.firstChild;
722
do {
723
if (null != node.nextSibling) {
724
return node.nextSibling;
725
}
726
node = node.parentNode;
727
} while (node);
728
return null;
729
}
730
/** Returns the previous node in document order (excluding attributes
731
* and namespace nodes).
732
*/
733
function precedingNode(node) {
734
if (node.ownerElement)
735
return node.ownerElement;
736
if (null != node.previousSibling) {
737
node = node.previousSibling;
738
while (null != node.lastChild) {
739
node = node.lastChild;
740
}
741
return node;
742
}
743
if (null != node.parentNode) {
744
return node.parentNode;
745
}
746
return null;
747
}
748
/** This axis is inefficient if there are many nodes in the nodeList.
749
* But I think it's a pretty useless axis so it's ok. */
750
function followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
751
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
752
var nodeMultiSet = new NodeMultiSet(false);
753
var cursor = nodeList[0];
754
var unorderedFollowingStarts = [];
755
for (var i = 0; i < nodeList.length; i++) {
756
var node = nodeList[i];
757
var start = followingNonDescendantNode(node);
758
if (start)
759
unorderedFollowingStarts.push(start);
760
}
761
if (0 === unorderedFollowingStarts.length)
762
return {nodes:[]};
763
var pos = [], nextPos = [];
764
var started = 0;
765
while (cursor = followingNode(cursor)) {
766
for (var i = unorderedFollowingStarts.length - 1; i >= 0; i--){
767
if (cursor === unorderedFollowingStarts[i]) {
768
nodeMultiSet.pushSeries();
769
unorderedFollowingStarts.splice(i,i+1);
770
started++;
771
}
772
}
773
if (started && matcher.matches(cursor)) {
774
nodeMultiSet.addNode(cursor);
775
}
776
}
777
console.assert(0 === unorderedFollowingStarts.length);
778
for (var i = 0; i < started; i++)
779
nodeMultiSet.popSeries();
780
return nodeMultiSet.finalize();
781
}
782
function precedingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
783
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
784
var cursor = nodeList.pop();
785
if (null == cursor) return {nodes:{}};
786
var r = {nodes:[], pos:[], lasts:[]};
787
var nextParents = [cursor.parentNode || cursor.ownerElement], nextPos = [1];
788
while (cursor = precedingNode(cursor)) {
789
if (cursor === nodeList[nodeList.length - 1]) {
790
nextParents.push(nodeList.pop());
791
nextPos.push(1);
792
}
793
var matches = matcher.matches(cursor);
794
var pos, someoneUsed = false;
795
if (matches)
796
pos = nextPos.slice();
797
798
for (var i = 0; i < nextParents.length; ++i) {
799
if (cursor === nextParents[i]) {
800
nextParents[i] = cursor.parentNode || cursor.ownerElement;
801
if (matches) {
802
pos[i] = null;
803
}
804
} else {
805
if (matches) {
806
pos[i] = nextPos[i]++;
807
someoneUsed = true;
808
}
809
}
810
}
811
if (someoneUsed) {
812
r.nodes.unshift(cursor);
813
r.pos.unshift(pos);
814
}
815
}
816
for (var i = 0; i < r.pos.length; ++i) {
817
var lasts = [];
818
r.lasts.push(lasts);
819
for (var j = r.pos[i].length - 1; j >= 0; j--) {
820
if (null == r.pos[i][j]) {
821
r.pos[i].splice(j, j+1);
822
} else {
823
lasts.unshift(nextPos[j] - 1);
824
}
825
}
826
}
827
return r;
828
}
829
830
/** node-set, axis -> node-set */
831
function descendantDfs(nodeMultiSet, node, remaining, matcher, andSelf, attrIndices, attrNodes) {
832
while (0 < remaining.length && null != remaining[0].ownerElement) {
833
var attr = remaining.shift();
834
if (andSelf && matcher.matches(attr)) {
835
attrNodes.push(attr);
836
attrIndices.push(nodeMultiSet.nodes.length);
837
}
838
}
839
if (null != node && !andSelf) {
840
if (matcher.matches(node))
841
nodeMultiSet.addNode(node);
842
}
843
var pushed = false;
844
if (null == node) {
845
if (0 === remaining.length) return;
846
node = remaining.shift();
847
nodeMultiSet.pushSeries();
848
pushed = true;
849
} else if (0 < remaining.length && node === remaining[0]) {
850
nodeMultiSet.pushSeries();
851
pushed = true;
852
remaining.shift();
853
}
854
if (andSelf) {
855
if (matcher.matches(node))
856
nodeMultiSet.addNode(node);
857
}
858
// TODO: use optimization. Also try element.getElementsByTagName
859
// var nodeList = 1 === nodeTypeNum && null != node.children ? node.children : node.childNodes;
860
var nodeList = node.childNodes;
861
for (var j = 0; j < nodeList.length; ++j) {
862
var child = nodeList[j];
863
descendantDfs(nodeMultiSet, child, remaining, matcher, andSelf, attrIndices, attrNodes);
864
}
865
if (pushed) {
866
nodeMultiSet.popSeries();
867
}
868
}
869
function descenantHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) {
870
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
871
var nodeMultiSet = new NodeMultiSet(false);
872
var attrIndices = [], attrNodes = [];
873
while (0 < nodeList.length) {
874
// var node = nodeList.shift();
875
descendantDfs(nodeMultiSet, null, nodeList, matcher, andSelf, attrIndices, attrNodes);
876
}
877
nodeMultiSet.finalize();
878
for (var i = attrNodes.length-1; i >= 0; --i) {
879
nodeMultiSet.nodes.splice(attrIndices[i], attrIndices[i], attrNodes[i]);
880
nodeMultiSet.pos.splice(attrIndices[i], attrIndices[i], [1]);
881
nodeMultiSet.lasts.splice(attrIndices[i], attrIndices[i], [1]);
882
}
883
return nodeMultiSet;
884
}
885
/**
886
*/
887
function ancestorHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) {
888
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
889
var ancestors = []; // array of non-empty arrays of matching ancestors
890
for (var i = 0; i < nodeList.length; ++i) {
891
var node = nodeList[i];
892
var isFirst = true;
893
var a = [];
894
while (null != node) {
895
if (!isFirst || andSelf) {
896
if (matcher.matches(node))
897
a.push(node);
898
}
899
isFirst = false;
900
node = node.parentNode || node.ownerElement;
901
}
902
if (0 < a.length)
903
ancestors.push(a);
904
}
905
var lasts = [];
906
for (var i = 0; i < ancestors.length; ++i) lasts.push(ancestors[i].length);
907
var nodeMultiSet = new NodeMultiSet(true);
908
var newCtx = {nodes:[], pos:[], lasts:[]};
909
while (0 < ancestors.length) {
910
var pos = [ancestors[0].length];
911
var last = [lasts[0]];
912
var node = ancestors[0].pop();
913
for (var i = ancestors.length - 1; i > 0; --i) {
914
if (node === ancestors[i][ancestors[i].length - 1]) {
915
pos.push(ancestors[i].length);
916
last.push(lasts[i]);
917
ancestors[i].pop();
918
if (0 === ancestors[i].length) {
919
ancestors.splice(i, i+1);
920
lasts.splice(i, i+1);
921
}
922
}
923
}
924
if (0 === ancestors[0].length) {
925
ancestors.shift();
926
lasts.shift();
927
}
928
newCtx.nodes.push(node);
929
newCtx.pos.push(pos);
930
newCtx.lasts.push(last);
931
}
932
return newCtx;
933
}
934
/** Helper function for sortDocumentOrder. Returns a list of indices, from the
935
* node to the root, of positions within parent.
936
* For convenience, the node is the first element of the array.
937
*/
938
function addressVector(node) {
939
var r = [node];
940
if (null != node.ownerElement) {
941
node = node.ownerElement;
942
r.push(-1);
943
}
944
while (null != node) {
945
var i = 0;
946
while (null != node.previousSibling) {
947
node = node.previousSibling;
948
i++;
949
}
950
r.push(i);
951
node = node.parentNode
952
}
953
return r;
954
}
955
function addressComparator(a, b) {
956
var minlen = Math.min(a.length - 1, b.length - 1), // not including [0]=node
957
alen = a.length,
958
blen = b.length;
959
if (a[0] === b[0]) return 0;
960
var c;
961
for (var i = 0; i < minlen; ++i) {
962
c = a[alen - i - 1] - b[blen - i - 1];
963
if (0 !== c)
964
break;
965
}
966
if (null == c || 0 === c) {
967
// All equal until one of the nodes. The longer one is the descendant.
968
c = alen - blen;
969
}
970
if (0 === c)
971
c = a.nodeName - b.nodeName;
972
if (0 === c)
973
c = 1;
974
return c;
975
}
976
var sortUniqDocumentOrder = xpath.sortUniqDocumentOrder = function(nodes) {
977
var a = [];
978
for (var i = 0; i < nodes.length; i++) {
979
var node = nodes[i];
980
var v = addressVector(node);
981
a.push(v);
982
}
983
a.sort(addressComparator);
984
var b = [];
985
for (var i = 0; i < a.length; i++) {
986
if (0 < i && a[i][0] === a[i - 1][0])
987
continue;
988
b.push(a[i][0]);
989
}
990
return b;
991
}
992
/** Sort node multiset. Does not do any de-duping. */
993
function sortNodeMultiSet(nodeMultiSet) {
994
var a = [];
995
for (var i = 0; i < nodeMultiSet.nodes.length; i++) {
996
var v = addressVector(nodeMultiSet.nodes[i]);
997
a.push({v:v, n:nodeMultiSet.nodes[i],
998
p:nodeMultiSet.pos[i], l:nodeMultiSet.lasts[i]});
999
}
1000
a.sort(compare);
1001
var r = {nodes:[], pos:[], lasts:[]};
1002
for (var i = 0; i < a.length; ++i) {
1003
r.nodes.push(a[i].n);
1004
r.pos.push(a[i].p);
1005
r.lasts.push(a[i].l);
1006
}
1007
function compare(x, y) {
1008
return addressComparator(x.v, y.v);
1009
}
1010
return r;
1011
}
1012
/** Returns an array containing all the ancestors down to a node.
1013
* The array starts with document.
1014
*/
1015
function nodeAndAncestors(node) {
1016
var ancestors = [node];
1017
var p = node;
1018
while (p = p.parentNode || p.ownerElement) {
1019
ancestors.unshift(p);
1020
}
1021
return ancestors;
1022
}
1023
function compareSiblings(a, b) {
1024
if (a === b) return 0;
1025
var c = a;
1026
while (c = c.previousSibling) {
1027
if (c === b)
1028
return 1; // b < a
1029
}
1030
c = b;
1031
while (c = c.previousSibling) {
1032
if (c === a)
1033
return -1; // a < b
1034
}
1035
throw new Error('a and b are not siblings: ' + xpath.stringifyObject(a) + ' vs ' + xpath.stringifyObject(b));
1036
}
1037
/** The merge in merge-sort.*/
1038
function mergeNodeLists(x, y) {
1039
var a, b, aanc, banc, r = [];
1040
if ('object' !== typeof x)
1041
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1042
'Invalid LHS for | operator ' +
1043
'(expected node-set): ' + x);
1044
if ('object' !== typeof y)
1045
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1046
'Invalid LHS for | operator ' +
1047
'(expected node-set): ' + y);
1048
while (true) {
1049
if (null == a) {
1050
a = x.shift();
1051
if (null != a)
1052
aanc = addressVector(a);
1053
}
1054
if (null == b) {
1055
b = y.shift();
1056
if (null != b)
1057
banc = addressVector(b);
1058
}
1059
if (null == a || null == b) break;
1060
var c = addressComparator(aanc, banc);
1061
if (c < 0) {
1062
r.push(a);
1063
a = null;
1064
aanc = null;
1065
} else if (c > 0) {
1066
r.push(b);
1067
b = null;
1068
banc = null;
1069
} else if (a.nodeName < b.nodeName) { // attributes
1070
r.push(a);
1071
a = null;
1072
aanc = null;
1073
} else if (a.nodeName > b.nodeName) { // attributes
1074
r.push(b);
1075
b = null;
1076
banc = null;
1077
} else if (a !== b) {
1078
// choose b arbitrarily
1079
r.push(b);
1080
b = null;
1081
banc = null;
1082
} else {
1083
console.assert(a === b, c);
1084
// just skip b without pushing it.
1085
b = null;
1086
banc = null;
1087
}
1088
}
1089
while (a) {
1090
r.push(a);
1091
a = x.shift();
1092
}
1093
while (b) {
1094
r.push(b);
1095
b = y.shift();
1096
}
1097
return r;
1098
}
1099
function comparisonHelper(test, x, y, isNumericComparison) {
1100
var coersion;
1101
if (isNumericComparison)
1102
coersion = fn.number;
1103
else coersion =
1104
'boolean' === typeof x || 'boolean' === typeof y ? fn['boolean'] :
1105
'number' === typeof x || 'number' === typeof y ? fn.number :
1106
fn.string;
1107
if ('object' === typeof x && 'object' === typeof y) {
1108
var aMap = {};
1109
for (var i = 0; i < x.nodes.length; ++i) {
1110
var xi = coersion({nodes:[x.nodes[i]]});
1111
for (var j = 0; j < y.nodes.length; ++j) {
1112
var yj = coersion({nodes:[y.nodes[j]]});
1113
if (test(xi, yj)) return true;
1114
}
1115
}
1116
return false;
1117
} else if ('object' === typeof x && x.nodes && x.nodes.length) {
1118
for (var i = 0; i < x.nodes.length; ++i) {
1119
var xi = coersion({nodes:[x.nodes[i]]}), yc = coersion(y);
1120
if (test(xi, yc))
1121
return true;
1122
}
1123
return false;
1124
} else if ('object' === typeof y && x.nodes && x.nodes.length) {
1125
for (var i = 0; i < x.nodes.length; ++i) {
1126
var yi = coersion({nodes:[y.nodes[i]]}), xc = coersion(x);
1127
if (test(xc, yi))
1128
return true;
1129
}
1130
return false;
1131
} else {
1132
var xc = coersion(x), yc = coersion(y);
1133
return test(xc, yc);
1134
}
1135
}
1136
var axes = xpath.axes = {
1137
'ancestor':
1138
function ancestor(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1139
return ancestorHelper(
1140
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false);
1141
},
1142
'ancestor-or-self':
1143
function ancestorOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1144
return ancestorHelper(
1145
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true);
1146
},
1147
'attribute':
1148
function attribute(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1149
// TODO: figure out whether positions should be undefined here.
1150
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
1151
var nodeMultiSet = new NodeMultiSet(false);
1152
if (null != nodeName) {
1153
// TODO: with namespace
1154
for (var i = 0; i < nodeList.length; ++i) {
1155
var node = nodeList[i];
1156
if (null == node.getAttributeNode)
1157
continue; // only Element has .getAttributeNode
1158
var attr = node.getAttributeNode(nodeName);
1159
if (null != attr && matcher.matches(attr)) {
1160
nodeMultiSet.pushSeries();
1161
nodeMultiSet.addNode(attr);
1162
nodeMultiSet.popSeries();
1163
}
1164
}
1165
} else {
1166
for (var i = 0; i < nodeList.length; ++i) {
1167
var node = nodeList[i];
1168
if (null != node.attributes) {
1169
nodeMultiSet.pushSeries();
1170
for (var j = 0; j < node.attributes.length; j++) { // all nodes have .attributes
1171
var attr = node.attributes[j];
1172
if (matcher.matches(attr)) // TODO: I think this check is unnecessary
1173
nodeMultiSet.addNode(attr);
1174
}
1175
nodeMultiSet.popSeries();
1176
}
1177
}
1178
}
1179
return nodeMultiSet.finalize();
1180
},
1181
'child':
1182
function child(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1183
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
1184
var nodeMultiSet = new NodeMultiSet(false);
1185
for (var i = 0; i < nodeList.length; ++i) {
1186
var n = nodeList[i];
1187
if (n.ownerElement) // skip attribute nodes' text child.
1188
continue;
1189
if (n.childNodes) {
1190
nodeMultiSet.pushSeries();
1191
var childList = 1 === nodeTypeNum && null != n.children ?
1192
n.children : n.childNodes;
1193
for (var j = 0; j < childList.length; ++j) {
1194
var child = childList[j];
1195
if (matcher.matches(child)) {
1196
nodeMultiSet.addNode(child);
1197
}
1198
// don't have to do de-duping because children have parent,
1199
// which are current context.
1200
}
1201
nodeMultiSet.popSeries();
1202
}
1203
}
1204
nodeMultiSet.finalize();
1205
return sortNodeMultiSet(nodeMultiSet);
1206
},
1207
'descendant':
1208
function descenant(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1209
return descenantHelper(
1210
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false);
1211
},
1212
'descendant-or-self':
1213
function descenantOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1214
return descenantHelper(
1215
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true);
1216
},
1217
'following':
1218
function following(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1219
return followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
1220
},
1221
'following-sibling':
1222
function followingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1223
return followingSiblingHelper(
1224
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase,
1225
Array.prototype.shift, function() {return this[0];},
1226
function(node) {return node.nextSibling;});
1227
},
1228
'namespace':
1229
function namespace(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1230
// TODO
1231
},
1232
'parent':
1233
function parent(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1234
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
1235
var nodes = [], pos = [];
1236
for (var i = 0; i < nodeList.length; ++i) {
1237
var parent = nodeList[i].parentNode || nodeList[i].ownerElement;
1238
if (null == parent)
1239
continue;
1240
if (!matcher.matches(parent))
1241
continue;
1242
if (nodes.length > 0 && parent === nodes[nodes.length-1])
1243
continue;
1244
nodes.push(parent);
1245
pos.push([1]);
1246
}
1247
return {nodes:nodes, pos:pos, lasts:pos};
1248
},
1249
'preceding':
1250
function preceding(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1251
return precedingHelper(
1252
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
1253
},
1254
'preceding-sibling':
1255
function precedingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1256
return followingSiblingHelper(
1257
nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase,
1258
Array.prototype.pop, function() {return this[this.length-1];},
1259
function(node) {return node.previousSibling},
1260
false, true);
1261
},
1262
'self':
1263
function self(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) {
1264
var nodes = [], pos = [];
1265
var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase);
1266
for (var i = 0; i < nodeList.length; ++i) {
1267
if (matcher.matches(nodeList[i])) {
1268
nodes.push(nodeList[i]);
1269
pos.push([1]);
1270
}
1271
}
1272
return {nodes: nodes, pos: pos, lasts: pos}
1273
}
1274
};
1275
1276
/***************************************************************************
1277
* Evaluation: functions *
1278
***************************************************************************/
1279
var fn = {
1280
'number': function number(optObject) {
1281
if ('number' === typeof optObject)
1282
return optObject;
1283
if ('string' === typeof optObject)
1284
return parseFloat(optObject); // note: parseFloat(' ') -> NaN, unlike +' ' -> 0.
1285
if ('boolean' === typeof optObject)
1286
return +optObject;
1287
return fn.number(fn.string.call(this, optObject)); // for node-sets
1288
},
1289
'string': function string(optObject) {
1290
if (null == optObject)
1291
return fn.string(this);
1292
if ('string' === typeof optObject || 'boolean' === typeof optObject ||
1293
'number' === typeof optObject)
1294
return '' + optObject;
1295
if (0 == optObject.nodes.length) return '';
1296
if (null != optObject.nodes[0].textContent)
1297
return optObject.nodes[0].textContent;
1298
return optObject.nodes[0].nodeValue;
1299
},
1300
'boolean': function booleanVal(x) {
1301
return 'object' === typeof x ? x.nodes.length > 0 : !!x;
1302
},
1303
'last': function last() {
1304
console.assert(Array.isArray(this.pos));
1305
console.assert(Array.isArray(this.lasts));
1306
console.assert(1 === this.pos.length);
1307
console.assert(1 === this.lasts.length);
1308
console.assert(1 === this.lasts[0].length);
1309
return this.lasts[0][0];
1310
},
1311
'position': function position() {
1312
console.assert(Array.isArray(this.pos));
1313
console.assert(Array.isArray(this.lasts));
1314
console.assert(1 === this.pos.length);
1315
console.assert(1 === this.lasts.length);
1316
console.assert(1 === this.pos[0].length);
1317
return this.pos[0][0];
1318
},
1319
'count': function count(nodeSet) {
1320
if ('object' !== typeof nodeSet)
1321
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1322
'Position ' + stream.position() +
1323
': Function count(node-set) ' +
1324
'got wrong argument type: ' + nodeSet);
1325
return nodeSet.nodes.length;
1326
},
1327
'id': function id(object) {
1328
var r = {nodes: []};
1329
var doc = this.nodes[0].ownerDocument || this.nodes[0];
1330
console.assert(doc);
1331
var ids;
1332
if ('object' === typeof object) {
1333
// for node-sets, map id over each node value.
1334
ids = [];
1335
for (var i = 0; i < object.nodes.length; ++i) {
1336
var idNode = object.nodes[i];
1337
var idsString = fn.string({nodes:[idNode]});
1338
var a = idsString.split(/[ \t\r\n]+/g);
1339
Array.prototype.push.apply(ids, a);
1340
}
1341
} else {
1342
var idsString = fn.string(object);
1343
var a = idsString.split(/[ \t\r\n]+/g);
1344
ids = a;
1345
}
1346
for (var i = 0; i < ids.length; ++i) {
1347
var id = ids[i];
1348
if (0 === id.length)
1349
continue;
1350
var node = doc.getElementById(id);
1351
if (null != node)
1352
r.nodes.push(node);
1353
}
1354
r.nodes = sortUniqDocumentOrder(r.nodes);
1355
return r;
1356
},
1357
'local-name': function(nodeSet) {
1358
if (null == nodeSet)
1359
return fn.name(this);
1360
if (null == nodeSet.nodes) {
1361
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1362
'argument to name() must be a node-set. got ' + nodeSet);
1363
}
1364
// TODO: namespaced version
1365
return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml
1366
},
1367
'namespace-uri': function(nodeSet) {
1368
// TODO
1369
throw new Error('not implemented yet');
1370
},
1371
'name': function(nodeSet) {
1372
if (null == nodeSet)
1373
return fn.name(this);
1374
if (null == nodeSet.nodes) {
1375
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1376
'argument to name() must be a node-set. got ' + nodeSet);
1377
}
1378
return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml
1379
},
1380
'concat': function concat(x) {
1381
var l = [];
1382
for (var i = 0; i < arguments.length; ++i) {
1383
l.push(fn.string(arguments[i]));
1384
}
1385
return l.join('');
1386
},
1387
'starts-with': function startsWith(a, b) {
1388
var as = fn.string(a), bs = fn.string(b);
1389
return as.substr(0, bs.length) === bs;
1390
},
1391
'contains': function contains(a, b) {
1392
var as = fn.string(a), bs = fn.string(b);
1393
var i = as.indexOf(bs);
1394
if (-1 === i) return false;
1395
return true;
1396
},
1397
'substring-before': function substringBefore(a, b) {
1398
var as = fn.string(a), bs = fn.string(b);
1399
var i = as.indexOf(bs);
1400
if (-1 === i) return '';
1401
return as.substr(0, i);
1402
},
1403
'substring-after': function substringBefore(a, b) {
1404
var as = fn.string(a), bs = fn.string(b);
1405
var i = as.indexOf(bs);
1406
if (-1 === i) return '';
1407
return as.substr(i + bs.length);
1408
},
1409
'substring': function substring(string, start, optEnd) {
1410
if (null == string || null == start) {
1411
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1412
'Must be at least 2 arguments to string()');
1413
}
1414
var sString = fn.string(string),
1415
iStart = fn.round(start),
1416
iEnd = optEnd == null ? null : fn.round(optEnd);
1417
// Note that xpath string positions user 1-based index
1418
if (iEnd == null)
1419
return sString.substr(iStart - 1);
1420
else
1421
return sString.substr(iStart - 1, iEnd);
1422
},
1423
'string-length': function stringLength(optString) {
1424
return fn.string.call(this, optString).length;
1425
},
1426
'normalize-space': function normalizeSpace(optString) {
1427
var s = fn.string.call(this, optString);
1428
return s.replace(/[ \t\r\n]+/g, ' ').replace(/^ | $/g, '');
1429
},
1430
'translate': function translate(string, from, to) {
1431
var sString = fn.string.call(this, string),
1432
sFrom = fn.string(from),
1433
sTo = fn.string(to);
1434
var eachCharRe = [];
1435
var map = {};
1436
for (var i = 0; i < sFrom.length; ++i) {
1437
var c = sFrom.charAt(i);
1438
map[c] = sTo.charAt(i); // returns '' if beyond length of sTo.
1439
// copied from goog.string.regExpEscape in the Closure library.
1440
eachCharRe.push(
1441
c.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
1442
replace(/\x08/g, '\\x08'));
1443
}
1444
var re = new RegExp(eachCharRe.join('|'), 'g');
1445
return sString.replace(re, function(c) {return map[c];});
1446
},
1447
/// Boolean functions
1448
'not': function not(x) {
1449
var bx = fn['boolean'](x);
1450
return !bx;
1451
},
1452
'true': function trueVal() { return true; },
1453
'false': function falseVal() { return false; },
1454
// TODO
1455
'lang': function lang(string) { throw new Error('Not implemented');},
1456
'sum': function sum(optNodeSet) {
1457
if (null == optNodeSet) return fn.sum(this);
1458
// for node-sets, map id over each node value.
1459
var sum = 0;
1460
for (var i = 0; i < optNodeSet.nodes.length; ++i) {
1461
var node = optNodeSet.nodes[i];
1462
var x = fn.number({nodes:[node]});
1463
sum += x;
1464
}
1465
return sum;
1466
},
1467
'floor': function floor(number) {
1468
return Math.floor(fn.number(number));
1469
},
1470
'ceiling': function ceiling(number) {
1471
return Math.ceil(fn.number(number));
1472
},
1473
'round': function round(number) {
1474
return Math.round(fn.number(number));
1475
}
1476
};
1477
/***************************************************************************
1478
* Evaluation: operators *
1479
***************************************************************************/
1480
var more = {
1481
UnaryMinus: function(x) { return -fn.number(x); },
1482
'+': function(x, y) { return fn.number(x) + fn.number(y); },
1483
'-': function(x, y) { return fn.number(x) - fn.number(y); },
1484
'*': function(x, y) { return fn.number(x) * fn.number(y); },
1485
'div': function(x, y) { return fn.number(x) / fn.number(y); },
1486
'mod': function(x, y) { return fn.number(x) % fn.number(y); },
1487
'<': function(x, y) {
1488
return comparisonHelper(function(x, y) { return fn.number(x) < fn.number(y);}, x, y, true);
1489
},
1490
'<=': function(x, y) {
1491
return comparisonHelper(function(x, y) { return fn.number(x) <= fn.number(y);}, x, y, true);
1492
},
1493
'>': function(x, y) {
1494
return comparisonHelper(function(x, y) { return fn.number(x) > fn.number(y);}, x, y, true);
1495
},
1496
'>=': function(x, y) {
1497
return comparisonHelper(function(x, y) { return fn.number(x) >= fn.number(y);}, x, y, true);
1498
},
1499
'and': function(x, y) { return fn['boolean'](x) && fn['boolean'](y); },
1500
'or': function(x, y) { return fn['boolean'](x) || fn['boolean'](y); },
1501
'|': function(x, y) { return {nodes: mergeNodeLists(x.nodes, y.nodes)}; },
1502
'=': function(x, y) {
1503
// optimization for two node-sets case: avoid n^2 comparisons.
1504
if ('object' === typeof x && 'object' === typeof y) {
1505
var aMap = {};
1506
for (var i = 0; i < x.nodes.length; ++i) {
1507
var s = fn.string({nodes:[x.nodes[i]]});
1508
aMap[s] = true;
1509
}
1510
for (var i = 0; i < y.nodes.length; ++i) {
1511
var s = fn.string({nodes:[y.nodes[i]]});
1512
if (aMap[s]) return true;
1513
}
1514
return false;
1515
} else {
1516
return comparisonHelper(function(x, y) {return x === y;}, x, y);
1517
}
1518
},
1519
'!=': function(x, y) {
1520
// optimization for two node-sets case: avoid n^2 comparisons.
1521
if ('object' === typeof x && 'object' === typeof y) {
1522
if (0 === x.nodes.length || 0 === y.nodes.length) return false;
1523
var aMap = {};
1524
for (var i = 0; i < x.nodes.length; ++i) {
1525
var s = fn.string({nodes:[x.nodes[i]]});
1526
aMap[s] = true;
1527
}
1528
for (var i = 0; i < y.nodes.length; ++i) {
1529
var s = fn.string({nodes:[y.nodes[i]]});
1530
if (!aMap[s]) return true;
1531
}
1532
return false;
1533
} else {
1534
return comparisonHelper(function(x, y) {return x !== y;}, x, y);
1535
}
1536
}
1537
};
1538
var nodeTypes = xpath.nodeTypes = {
1539
'node': 0,
1540
'attribute': 2,
1541
'comment': 8, // this.doc.COMMENT_NODE,
1542
'text': 3, // this.doc.TEXT_NODE,
1543
'processing-instruction': 7, // this.doc.PROCESSING_INSTRUCTION_NODE,
1544
'element': 1 //this.doc.ELEMENT_NODE
1545
};
1546
/** For debugging and unit tests: returnjs a stringified version of the
1547
* argument. */
1548
var stringifyObject = xpath.stringifyObject = function stringifyObject(ctx) {
1549
var seenKey = 'seen' + Math.floor(Math.random()*1000000000);
1550
return JSON.stringify(helper(ctx));
1551
1552
function helper(ctx) {
1553
if (Array.isArray(ctx)) {
1554
return ctx.map(function(x) {return helper(x);});
1555
}
1556
if ('object' !== typeof ctx) return ctx;
1557
if (null == ctx) return ctx;
1558
// if (ctx.toString) return ctx.toString();
1559
if (null != ctx.outerHTML) return ctx.outerHTML;
1560
if (null != ctx.nodeValue) return ctx.nodeName + '=' + ctx.nodeValue;
1561
if (ctx[seenKey]) return '[circular]';
1562
ctx[seenKey] = true;
1563
var nicer = {};
1564
for (var key in ctx) {
1565
if (seenKey === key)
1566
continue;
1567
try {
1568
nicer[key] = helper(ctx[key]);
1569
} catch (e) {
1570
nicer[key] = '[exception: ' + e.message + ']';
1571
}
1572
}
1573
delete ctx[seenKey];
1574
return nicer;
1575
}
1576
}
1577
var Evaluator = xpath.Evaluator = function Evaluator(doc) {
1578
this.doc = doc;
1579
}
1580
Evaluator.prototype = {
1581
val: function val(ast, ctx) {
1582
console.assert(ctx.nodes);
1583
1584
if ('number' === typeof ast || 'string' === typeof ast) return ast;
1585
if (more[ast[0]]) {
1586
var evaluatedParams = [];
1587
for (var i = 1; i < ast.length; ++i) {
1588
evaluatedParams.push(this.val(ast[i], ctx));
1589
}
1590
var r = more[ast[0]].apply(ctx, evaluatedParams);
1591
return r;
1592
}
1593
switch (ast[0]) {
1594
case 'Root': return {nodes: [this.doc]};
1595
case 'FunctionCall':
1596
var functionName = ast[1], functionParams = ast[2];
1597
if (null == fn[functionName])
1598
throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,
1599
'Unknown function: ' + functionName);
1600
var evaluatedParams = [];
1601
for (var i = 0; i < functionParams.length; ++i) {
1602
evaluatedParams.push(this.val(functionParams[i], ctx));
1603
}
1604
var r = fn[functionName].apply(ctx, evaluatedParams);
1605
return r;
1606
case 'Predicate':
1607
var lhs = this.val(ast[1], ctx);
1608
var ret = {nodes: []};
1609
var contexts = eachContext(lhs);
1610
for (var i = 0; i < contexts.length; ++i) {
1611
var singleNodeSet = contexts[i];
1612
var rhs = this.val(ast[2], singleNodeSet);
1613
var success;
1614
if ('number' === typeof rhs) {
1615
success = rhs === singleNodeSet.pos[0][0];
1616
} else {
1617
success = fn['boolean'](rhs);
1618
}
1619
if (success) {
1620
var node = singleNodeSet.nodes[0];
1621
ret.nodes.push(node);
1622
// skip over all the rest of the same node.
1623
while (i+1 < contexts.length && node === contexts[i+1].nodes[0]) {
1624
i++;
1625
}
1626
}
1627
}
1628
return ret;
1629
case 'PathExpr':
1630
// turn the path into an expressoin; i.e., remove the position
1631
// information of the last axis.
1632
var x = this.val(ast[1], ctx);
1633
// Make the nodeset a forward-direction-only one.
1634
if (x.finalize) { // it is a NodeMultiSet
1635
for (var i = 0; i < x.nodes.length; ++i) {
1636
console.assert(null != x.nodes[i].nodeType);
1637
}
1638
return {nodes: x.nodes};
1639
} else {
1640
return x;
1641
}
1642
case '/':
1643
// TODO: don't generate '/' nodes, just Axis nodes.
1644
var lhs = this.val(ast[1], ctx);
1645
console.assert(null != lhs);
1646
var r = this.val(ast[2], lhs);
1647
console.assert(null != r);
1648
return r;
1649
case 'Axis':
1650
// All the axis tests from Step. We only get AxisSpecifier NodeTest,
1651
// not the predicate (which is applied later)
1652
var axis = ast[1],
1653
nodeType = ast[2],
1654
nodeTypeNum = nodeTypes[nodeType],
1655
shouldLowerCase = true, // TODO: give option
1656
nodeName = ast[3] && shouldLowerCase ? ast[3].toLowerCase() : ast[3];
1657
nodeName = nodeName === '*' ? null : nodeName;
1658
if ('object' !== typeof ctx) return {nodes:[], pos:[]};
1659
var nodeList = ctx.nodes.slice(); // TODO: is copy needed?
1660
var r = axes[axis](nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase);
1661
return r;
1662
}
1663
}
1664
};
1665
var evaluate = xpath.evaluate = function evaluate(expr, doc, context) {
1666
//var astFactory = new AstEvaluatorFactory(doc, context);
1667
var stream = new Stream(expr);
1668
var ast = parse(stream, astFactory);
1669
var val = new Evaluator(doc).val(ast, {nodes: [context]});
1670
return val;
1671
}
1672
1673
/***************************************************************************
1674
* DOM interface *
1675
***************************************************************************/
1676
var XPathException = xpath.XPathException = function XPathException(code, message) {
1677
var e = new Error(message);
1678
e.name = 'XPathException';
1679
e.code = code;
1680
return e;
1681
}
1682
XPathException.INVALID_EXPRESSION_ERR = 51;
1683
XPathException.TYPE_ERR = 52;
1684
1685
1686
var XPathEvaluator = xpath.XPathEvaluator = function XPathEvaluator() {}
1687
XPathEvaluator.prototype = {
1688
createExpression: function(expression, resolver) {
1689
return new XPathExpression(expression, resolver);
1690
},
1691
createNSResolver: function(nodeResolver) {
1692
// TODO
1693
},
1694
evaluate: function evaluate(expression, contextNode, resolver, type, result) {
1695
var expr = new XPathExpression(expression, resolver);
1696
return expr.evaluate(contextNode, type, result);
1697
}
1698
};
1699
1700
1701
var XPathExpression = xpath.XPathExpression = function XPathExpression(expression, resolver, optDoc) {
1702
var stream = new Stream(expression);
1703
this._ast = parse(stream, astFactory);
1704
this._doc = optDoc;
1705
}
1706
XPathExpression.prototype = {
1707
evaluate: function evaluate(contextNode, type, result) {
1708
if (null == contextNode.nodeType)
1709
throw new Error('bad argument (expected context node): ' + contextNode);
1710
var doc = contextNode.ownerDocument || contextNode;
1711
if (null != this._doc && this._doc !== doc) {
1712
throw new core.DOMException(
1713
core.WRONG_DOCUMENT_ERR,
1714
'The document must be the same as the context node\'s document.');
1715
}
1716
var evaluator = new Evaluator(doc);
1717
var value = evaluator.val(this._ast, {nodes: [contextNode]});
1718
if (XPathResult.NUMBER_TYPE === type)
1719
value = fn.number(value);
1720
else if (XPathResult.STRING_TYPE === type)
1721
value = fn.string(value);
1722
else if (XPathResult.BOOLEAN_TYPE === type)
1723
value = fn['boolean'](value);
1724
else if (XPathResult.ANY_TYPE !== type &&
1725
XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== type &&
1726
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== type &&
1727
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== type &&
1728
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== type &&
1729
XPathResult.ANY_UNORDERED_NODE_TYPE !== type &&
1730
XPathResult.FIRST_ORDERED_NODE_TYPE !== type)
1731
throw new core.DOMException(
1732
core.NOT_SUPPORTED_ERR,
1733
'You must provide an XPath result type (0=any).');
1734
else if (XPathResult.ANY_TYPE !== type &&
1735
'object' !== typeof value)
1736
throw new XPathException(
1737
XPathException.TYPE_ERR,
1738
'Value should be a node-set: ' + value);
1739
return new XPathResult(doc, value, type);
1740
}
1741
}
1742
1743
var XPathResult = xpath.XPathResult = function XPathResult(doc, value, resultType) {
1744
this._value = value;
1745
this._resultType = resultType;
1746
this._i = 0;
1747
this._invalidated = false;
1748
if (this.resultType === XPathResult.UNORDERED_NODE_ITERATOR_TYPE ||
1749
this.resultType === XPathResult.ORDERED_NODE_ITERATOR_TYPE) {
1750
doc.addEventListener('DOMSubtreeModified', invalidate, true);
1751
var self = this;
1752
function invalidate() {
1753
self._invalidated = true;
1754
doc.removeEventListener('DOMSubtreeModified', invalidate, true);
1755
}
1756
}
1757
}
1758
XPathResult.ANY_TYPE = 0;
1759
XPathResult.NUMBER_TYPE = 1;
1760
XPathResult.STRING_TYPE = 2;
1761
XPathResult.BOOLEAN_TYPE = 3;
1762
XPathResult.UNORDERED_NODE_ITERATOR_TYPE = 4;
1763
XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5;
1764
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE = 6;
1765
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7;
1766
XPathResult.ANY_UNORDERED_NODE_TYPE = 8;
1767
XPathResult.FIRST_ORDERED_NODE_TYPE = 9;
1768
var proto = {
1769
// XPathResultType
1770
get resultType() {
1771
if (this._resultType) return this._resultType;
1772
switch (typeof this._value) {
1773
case 'number': return XPathResult.NUMBER_TYPE;
1774
case 'string': return XPathResult.STRING_TYPE;
1775
case 'boolean': return XPathResult.BOOLEAN_TYPE;
1776
default: return XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
1777
}
1778
},
1779
get numberValue() {
1780
if (XPathResult.NUMBER_TYPE !== this.resultType)
1781
throw new XPathException(XPathException.TYPE_ERR,
1782
'You should have asked for a NUMBER_TYPE.');
1783
return this._value;
1784
},
1785
get stringValue() {
1786
if (XPathResult.STRING_TYPE !== this.resultType)
1787
throw new XPathException(XPathException.TYPE_ERR,
1788
'You should have asked for a STRING_TYPE.');
1789
return this._value;
1790
},
1791
get booleanValue() {
1792
if (XPathResult.BOOLEAN_TYPE !== this.resultType)
1793
throw new XPathException(XPathException.TYPE_ERR,
1794
'You should have asked for a BOOLEAN_TYPE.');
1795
return this._value;
1796
},
1797
get singleNodeValue() {
1798
if (XPathResult.ANY_UNORDERED_NODE_TYPE !== this.resultType &&
1799
XPathResult.FIRST_ORDERED_NODE_TYPE !== this.resultType)
1800
throw new XPathException(
1801
XPathException.TYPE_ERR,
1802
'You should have asked for a FIRST_ORDERED_NODE_TYPE.');
1803
return this._value.nodes[0] || null;
1804
},
1805
get invalidIteratorState() {
1806
if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType &&
1807
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType)
1808
return false;
1809
return !!this._invalidated;
1810
},
1811
get snapshotLength() {
1812
if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType &&
1813
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType)
1814
throw new XPathException(
1815
XPathException.TYPE_ERR,
1816
'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.');
1817
return this._value.nodes.length;
1818
},
1819
iterateNext: function iterateNext() {
1820
if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType &&
1821
XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType)
1822
throw new XPathException(
1823
XPathException.TYPE_ERR,
1824
'You should have asked for a ORDERED_NODE_ITERATOR_TYPE.');
1825
if (this.invalidIteratorState)
1826
throw new core.DOMException(
1827
core.INVALID_STATE_ERR,
1828
'The document has been mutated since the result was returned');
1829
return this._value.nodes[this._i++] || null;
1830
},
1831
snapshotItem: function snapshotItem(index) {
1832
if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType &&
1833
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType)
1834
throw new XPathException(
1835
XPathException.TYPE_ERR,
1836
'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.');
1837
return this._value.nodes[index] || null;
1838
}
1839
};
1840
// so you can access ANY_TYPE etc. from the instances:
1841
XPathResult.prototype = Object.create(XPathResult,
1842
Object.keys(proto).reduce(function (descriptors, name) {
1843
descriptors[name] = Object.getOwnPropertyDescriptor(proto, name);
1844
return descriptors;
1845
}, {
1846
constructor: {
1847
value: XPathResult,
1848
writable: true,
1849
configurable: true
1850
}
1851
}));
1852
1853
core.XPathException = XPathException;
1854
core.XPathExpression = XPathExpression;
1855
core.XPathResult = XPathResult;
1856
core.XPathEvaluator = XPathEvaluator;
1857
1858
core.Document.prototype.createExpression =
1859
XPathEvaluator.prototype.createExpression;
1860
1861
core.Document.prototype.createNSResolver =
1862
XPathEvaluator.prototype.createNSResolver;
1863
1864
core.Document.prototype.evaluate = XPathEvaluator.prototype.evaluate;
1865
1866
})();
1867
1868