Skip to content

Commit 39bb40d

Browse files
- applyPredicates now computes position = isReverseAxis ? size - i : i + 1 for the 4 reverse axes.
- `getAncestors`/`getPreceding` rebuilt to yield strict document order (was closest-first / scrambled) so the reversed-position math lines up, and so parenthesized forms like `(preceding-sibling::*)[1]` — which per spec use plain document-order indexing since parens break axis-based numbering (resolves #3).
1 parent 5bf9406 commit 39bb40d

2 files changed

Lines changed: 122 additions & 15 deletions

File tree

src/expressions/step-expression.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,12 @@ export class XPathStep extends XPathExpression {
180180
}
181181

182182
private getAncestors(node: any, includeSelf: boolean): any[] {
183+
// Returned in document order (root first, closest ancestor last).
184+
// Proximity numbering for the reverse axis is handled in applyPredicates().
183185
const result: any[] = [];
184-
if (includeSelf) result.push(node);
185-
186-
let current = node.parentNode;
186+
let current = includeSelf ? node : node.parentNode;
187187
while (current) {
188-
result.push(current);
188+
result.unshift(current);
189189
current = current.parentNode;
190190
}
191191
return result;
@@ -238,25 +238,29 @@ export class XPathStep extends XPathExpression {
238238
}
239239

240240
private getPreceding(node: any): any[] {
241+
// Returned in document order. Proximity numbering for the reverse
242+
// axis is handled in applyPredicates().
241243
const result: any[] = [];
242244

243-
// Preceding siblings and their descendants (in reverse document order)
245+
const unshiftSubtree = (n: any) => {
246+
// n followed by its descendants is already document order;
247+
// unshifting the whole block keeps farther subtrees ahead of closer ones.
248+
result.unshift(n, ...this.getDescendants(n, false));
249+
};
250+
251+
// Preceding siblings and their descendants
244252
let sibling = node.previousSibling;
245253
while (sibling) {
246-
result.unshift(sibling);
247-
const descendants = this.getDescendants(sibling, false);
248-
result.unshift(...descendants);
254+
unshiftSubtree(sibling);
249255
sibling = sibling.previousSibling;
250256
}
251257

252-
// Ancestors' preceding siblings
258+
// Ancestors' preceding siblings and their descendants
253259
let ancestor = node.parentNode;
254260
while (ancestor) {
255261
sibling = ancestor.previousSibling;
256262
while (sibling) {
257-
result.unshift(sibling);
258-
const descendants = this.getDescendants(sibling, false);
259-
result.unshift(...descendants);
263+
unshiftSubtree(sibling);
260264
sibling = sibling.previousSibling;
261265
}
262266
ancestor = ancestor.parentNode;
@@ -421,26 +425,39 @@ export class XPathStep extends XPathExpression {
421425
}
422426
}
423427

428+
/** Axes whose proximity position is counted in reverse document order (XPath 3.1 §2.5.1). */
429+
private static readonly REVERSE_AXES = new Set<AxisType>([
430+
'ancestor',
431+
'ancestor-or-self',
432+
'preceding',
433+
'preceding-sibling',
434+
]);
435+
424436
private applyPredicates(nodes: any[], context: any): any[] {
425437
let result = nodes;
438+
// getNodesByAxis() always returns candidates in document order; for reverse
439+
// axes the closest node to the context node is the *last* one in that order,
440+
// so its proximity position (used by position()/numeric predicates) is 1.
441+
const isReverseAxis = XPathStep.REVERSE_AXES.has(this.axis);
426442

427443
for (const predicate of this.predicates) {
428444
const filtered: any[] = [];
429445
const size = result.length;
430446

431447
for (let i = 0; i < result.length; i++) {
448+
const position = isReverseAxis ? size - i : i + 1;
432449
const predicateContext = {
433450
...context,
434451
node: result[i],
435-
position: i + 1,
436-
size: size,
452+
position,
453+
size,
437454
};
438455

439456
const predicateResult = predicate.evaluate(predicateContext);
440457

441458
// If predicate result is a number, it's a position test
442459
if (typeof predicateResult === 'number') {
443-
if (predicateResult === i + 1) {
460+
if (predicateResult === position) {
444461
filtered.push(result[i]);
445462
}
446463
} else if (this.toBoolean(predicateResult)) {

tests/location-path.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,96 @@ describe('Location Path and Step Expression Evaluation', () => {
284284
});
285285
});
286286

287+
describe('Reverse axis proximity position (GitHub issue #3)', () => {
288+
// <fruit><banana><lady-finger-banana/></banana><apple/><pear/><kiwi/></fruit>
289+
function buildFruitTree() {
290+
const fruit = createNode('fruit');
291+
const banana = createNode('banana', 1, fruit);
292+
const ladyFinger = createNode('lady-finger-banana', 1, banana);
293+
const apple = createNode('apple', 1, fruit);
294+
const pear = createNode('pear', 1, fruit);
295+
const kiwi = createNode('kiwi', 1, fruit);
296+
297+
banana.childNodes = [ladyFinger];
298+
fruit.childNodes = [banana, apple, pear, kiwi];
299+
300+
banana.nextSibling = apple;
301+
apple.previousSibling = banana;
302+
apple.nextSibling = pear;
303+
pear.previousSibling = apple;
304+
pear.nextSibling = kiwi;
305+
kiwi.previousSibling = pear;
306+
307+
return { fruit, banana, ladyFinger, apple, pear, kiwi };
308+
}
309+
310+
it('preceding-sibling::*[1] returns the closest preceding sibling, not the first in document order', () => {
311+
const { apple, pear } = buildFruitTree();
312+
313+
const ast = parse('preceding-sibling::*[1]') as XPathLocationPath;
314+
const context: XPathContext = { node: pear, position: 1, size: 1 };
315+
const result = ast.evaluate(context) as any[];
316+
317+
expect(result).toEqual([apple]);
318+
});
319+
320+
it('(preceding-sibling::*)[1] still uses document order, since parenthesizing breaks axis-based numbering', () => {
321+
const { banana, pear } = buildFruitTree();
322+
323+
const ast = parse('(preceding-sibling::*)[1]') as XPathLocationPath;
324+
const context: XPathContext = { node: pear, position: 1, size: 1 };
325+
const result = ast.evaluate(context) as any[];
326+
327+
expect(result).toEqual([banana]);
328+
});
329+
330+
it('preceding::*[1] returns the closest preceding node in reverse document order', () => {
331+
const { apple, pear } = buildFruitTree();
332+
333+
const ast = parse('preceding::*[1]') as XPathLocationPath;
334+
const context: XPathContext = { node: pear, position: 1, size: 1 };
335+
const result = ast.evaluate(context) as any[];
336+
337+
expect(result).toEqual([apple]);
338+
});
339+
340+
it('preceding::* is returned in document order', () => {
341+
const { banana, ladyFinger, apple, pear } = buildFruitTree();
342+
343+
const ast = parse('preceding::*') as XPathLocationPath;
344+
const context: XPathContext = { node: pear, position: 1, size: 1 };
345+
const result = ast.evaluate(context) as any[];
346+
347+
expect(result).toEqual([banana, ladyFinger, apple]);
348+
});
349+
350+
it('ancestor::*[1] returns the closest ancestor', () => {
351+
const grandparent = createNode('root');
352+
const { fruit, pear } = buildFruitTree();
353+
grandparent.childNodes = [fruit];
354+
fruit.parentNode = grandparent;
355+
356+
const ast = parse('ancestor::*[1]') as XPathLocationPath;
357+
const context: XPathContext = { node: pear, position: 1, size: 1 };
358+
const result = ast.evaluate(context) as any[];
359+
360+
expect(result).toEqual([fruit]);
361+
});
362+
363+
it('ancestor::* is returned in document order', () => {
364+
const grandparent = createNode('root');
365+
const { fruit, pear } = buildFruitTree();
366+
grandparent.childNodes = [fruit];
367+
fruit.parentNode = grandparent;
368+
369+
const ast = parse('ancestor::*') as XPathLocationPath;
370+
const context: XPathContext = { node: pear, position: 1, size: 1 };
371+
const result = ast.evaluate(context) as any[];
372+
373+
expect(result).toEqual([grandparent, fruit]);
374+
});
375+
});
376+
287377
describe('Step Expression - Node Tests', () => {
288378
it('should match name node test', () => {
289379
const root = createNode('root');

0 commit comments

Comments
 (0)