Skip to content

Commit 060b154

Browse files
committed
Fix positional predicate for the "ancestor" axis
The expression `/ancestor::div[1]` should select only the first "div" node while traversing up the document tree, but this wasn't the case. This fixes positional predicates by having ancestorQuery implement the Position interface. The test was written by hand by me and the implementation was helped by GitHub Copilot w/ GPT-5.
1 parent 8d50c25 commit 060b154

File tree

3 files changed

+65
-0
lines changed

3 files changed

+65
-0
lines changed

query.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ type ancestorQuery struct {
147147
name string
148148
iterator func() NodeNavigator
149149
table map[uint64]bool
150+
pos int
150151

151152
Self bool
152153
Input query
@@ -164,6 +165,8 @@ func (a *ancestorQuery) Select(t iterator) NodeNavigator {
164165
if node == nil {
165166
return nil
166167
}
168+
// Reset position for a new input context node
169+
a.pos = 0
167170
first := true
168171
node = node.Copy()
169172
a.iterator = func() NodeNavigator {
@@ -186,6 +189,8 @@ func (a *ancestorQuery) Select(t iterator) NodeNavigator {
186189
node_id := getHashCode(node.Copy())
187190
if _, ok := a.table[node_id]; !ok {
188191
a.table[node_id] = true
192+
// Increase position for each matched node in current input context
193+
a.pos++
189194
return node
190195
}
191196
}
@@ -215,6 +220,13 @@ func (a *ancestorQuery) Properties() queryProp {
215220
return queryProps.Position | queryProps.Count | queryProps.Cached | queryProps.Merge | queryProps.Reverse
216221
}
217222

223+
// position returns the ordinal of the current matched node within the axis
224+
// traversal for the current input context node. This is required so numeric
225+
// predicates like [1] or [2] on the ancestor axis resolve in axis order.
226+
func (a *ancestorQuery) position() int {
227+
return a.pos
228+
}
229+
218230
// attributeQuery is an XPath attribute node query.(@*)
219231
type attributeQuery struct {
220232
name string

xpath_axes_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ func Test_ancestor(t *testing.T) {
3232
//test_xpath_elements(t, employee_example, `//ancestor::name`, 4, 9, 14)
3333
}
3434

35+
func Test_ancestor_predicate(t *testing.T) {
36+
doc := createElement(0, "",
37+
createElement(1, "html",
38+
createElement(2, "body",
39+
createElement(3, "h1"),
40+
createElement(4, "section",
41+
createElement(5, "div",
42+
createElement(6, "section",
43+
createElement(7, "div",
44+
createElement(8, "span"),
45+
),
46+
),
47+
),
48+
),
49+
createElement(9, "section",
50+
createElement(10, "div",
51+
createElement(11, "section",
52+
createElement(12, "div",
53+
createElement(13, "span"),
54+
),
55+
),
56+
),
57+
),
58+
),
59+
),
60+
)
61+
62+
test_xpath_elements(t, doc, `//span/ancestor::*`, 7, 6, 5, 4, 2, 1, 12, 11, 10, 9)
63+
test_xpath_elements(t, doc, `//span/ancestor::section`, 6, 4, 11, 9)
64+
test_xpath_elements(t, doc, `//span/ancestor::section[1]`, 6, 11)
65+
test_xpath_elements(t, doc, `//span/ancestor::section[2]`, 4, 9)
66+
}
67+
3568
func Test_ancestor_or_self(t *testing.T) {
3669
// Expected the value is [2, 3, 8, 13], but got [3, 2, 8, 13]
3770
test_xpath_elements(t, employee_example, `//employee/ancestor-or-self::*`, 3, 2, 8, 13)

xpath_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,26 @@ func (n *TNode) getAttribute(key string) string {
587587
return ""
588588
}
589589

590+
func createElement(line int, name string, children ...*TNode) *TNode {
591+
nodeType := ElementNode
592+
if name == "" {
593+
nodeType = RootNode
594+
}
595+
n := createNode(name, nodeType)
596+
n.lines = line
597+
for _, c := range children {
598+
c.Parent = n
599+
c.PrevSibling = n.LastChild
600+
if c.PrevSibling == nil {
601+
n.FirstChild = c
602+
} else {
603+
c.PrevSibling.NextSibling = c
604+
}
605+
n.LastChild = c
606+
}
607+
return n
608+
}
609+
590610
func createBookExample() *TNode {
591611
/*
592612
<?xml version="1.0" encoding="UTF-8"?>

0 commit comments

Comments
 (0)