Modify GetPath and GetRelativePath.
GetRelativePath now handles relative paths between any two nodes
in an element tree, even if one is not a direct descendant of the
other.
diff --git a/etree.go b/etree.go
index 9c04ecb..cfcc909 100644
--- a/etree.go
+++ b/etree.go
@@ -135,25 +135,92 @@
}
}
-// GetPath returns the absolute path of the element
+// GetPath returns the absolute path of the element.
func (e *Element) GetPath() string {
- path := e.GetRelativePath(nil)
-
- return path[1:]
-}
-
-// GetPath returns the path of the element corresponding to the source element or document root
-func (e *Element) GetRelativePath(source *Element) string {
- parts := []string{}
-
- for element := e; element != nil && element.Tag != "" && element != source; element = element.Parent() {
- parts = append(parts, "")
- copy(parts[1:], parts)
- parts[0] = element.Tag
+ path := []string{}
+ for seg := e; seg != nil; seg = seg.Parent() {
+ if seg.Tag != "" {
+ path = append(path, seg.Tag)
+ }
}
- path := strings.Join(parts, "/")
- return "./" + path
+ // Reverse the path.
+ for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
+ path[i], path[j] = path[j], path[i]
+ }
+
+ return "/" + strings.Join(path, "/")
+}
+
+// GetRelativePath returns the path of the element relative to the source
+// element. If the two elements are not part of the same element tree, then
+// GetRelativePath returns the empty string.
+func (e *Element) GetRelativePath(source *Element) string {
+ var path []*Element
+
+ if source == nil {
+ return ""
+ }
+
+ // Build a reverse path from the element toward the root. Stop if the
+ // source element is encountered.
+ var seg *Element
+ for seg = e; seg != nil && seg != source; seg = seg.Parent() {
+ path = append(path, seg)
+ }
+
+ // If we found the source element, reverse the path and compose the
+ // string.
+ if seg == source {
+ if len(path) == 0 {
+ return "."
+ }
+ parts := []string{}
+ for i := len(path) - 1; i >= 0; i-- {
+ parts = append(parts, path[i].Tag)
+ }
+ return "./" + strings.Join(parts, "/")
+ }
+
+ // The source wasn't encountered, so climb from the source element toward
+ // the root of the tree until an element in the reversed path is
+ // encountered.
+
+ findPathIndex := func(e *Element, path []*Element) int {
+ for i, ee := range path {
+ if e.Tag == ee.Tag {
+ return i
+ }
+ }
+ return -1
+ }
+
+ climb := 0
+ for seg = source; seg != nil; seg = seg.Parent() {
+ i := findPathIndex(seg, path)
+ if i >= 0 {
+ path = path[:i] // truncate at found segment
+ break
+ }
+ climb++
+ }
+
+ // No element in the reversed path was encountered, so the two elements
+ // must not be part of the same tree.
+ if seg == nil {
+ return ""
+ }
+
+ // Reverse the (possibly truncated) path and prepend ".." segments to
+ // climb.
+ parts := []string{}
+ for i := 0; i < climb; i++ {
+ parts = append(parts, "..")
+ }
+ for i := len(path) - 1; i >= 0; i-- {
+ parts = append(parts, path[i].Tag)
+ }
+ return strings.Join(parts, "/")
}
// Copy returns a recursive, deep copy of the document.
@@ -445,7 +512,8 @@
}
// SelectAttr finds an element attribute matching the requested key and
-// returns it if found. The key may be prefixed by a namespace and a colon.
+// returns it if found. Returns nil if no matching attribute is found. The key
+// may be prefixed by a namespace and a colon.
func (e *Element) SelectAttr(key string) *Attr {
space, skey := spaceDecompose(key)
for i, a := range e.Attr {
@@ -481,7 +549,8 @@
}
// SelectElement returns the first child element with the given tag. The tag
-// may be prefixed by a namespace and a colon.
+// may be prefixed by a namespace and a colon. Returns nil if no element with
+// a matching tag was found.
func (e *Element) SelectElement(tag string) *Element {
space, stag := spaceDecompose(tag)
for _, t := range e.Child {
@@ -506,13 +575,14 @@
}
// FindElement returns the first element matched by the XPath-like path
-// string. Panics if an invalid path string is supplied.
+// string. Returns nil if no element is found using the path. Panics if an
+// invalid path string is supplied.
func (e *Element) FindElement(path string) *Element {
return e.FindElementPath(MustCompilePath(path))
}
// FindElementPath returns the first element matched by the XPath-like path
-// string.
+// string. Returns nil if no element is found using the path.
func (e *Element) FindElementPath(path Path) *Element {
p := newPather()
elements := p.traverse(e, path)
diff --git a/etree_test.go b/etree_test.go
index 3cc059a..e20d28b 100644
--- a/etree_test.go
+++ b/etree_test.go
@@ -10,12 +10,9 @@
)
func checkEq(t *testing.T, got, want string) {
- if got == want {
- return
+ if got != want {
+ t.Errorf("etree: unexpected result.\nGot:\n%s\nWanted:\n%s\n", got, want)
}
- t.Errorf(
- "etree: unexpected result.\nGot:\n%s\nWanted:\n%s\n",
- got, want)
}
func TestDocument(t *testing.T) {
@@ -257,26 +254,70 @@
if s1 == s2 {
t.Error("etree: incorrect result after RemoveElement")
}
+}
- pa1 := e1.GetPath()
- checkEq(t, pa1, "/title")
+func TestGetPath(t *testing.T) {
+ testdoc := `<a>
+ <b1>
+ <c1>
+ <d1>
+ </d1>
+ </c1>
+ </b1>
+ <b2>
+ <c2>
+ <d2>
+ </d2>
+ </c2>
+ </b2>
+</a>`
- pa2 := e2.GetPath()
- checkEq(t, pa2, "/store/book/title")
+ doc := NewDocument()
+ err := doc.ReadFromString(testdoc)
+ if err != nil {
+ t.Fatalf("etree ReadFromString: %v\n", err)
+ }
- p1 := e1.GetRelativePath(nil)
- checkEq(t, p1, "./title")
+ cases := []struct {
+ from string
+ to string
+ relpath string
+ topath string
+ }{
+ {"a", ".", "..", "/"},
+ {".", "a", "./a", "/a"},
+ {"a/b1/c1/d1", ".", "../../../..", "/"},
+ {".", "a/b1/c1/d1", "./a/b1/c1/d1", "/a/b1/c1/d1"},
+ {"a", "a", ".", "/a"},
+ {"a/b1", "a/b1/c1", "./c1", "/a/b1/c1"},
+ {"a/b1/c1", "a/b1", "..", "/a/b1"},
+ {"a/b1/c1", "a/b1/c1", ".", "/a/b1/c1"},
+ {"a", "a/b1", "./b1", "/a/b1"},
+ {"a/b1", "a", "..", "/a"},
+ {"a", "a/b1/c1", "./b1/c1", "/a/b1/c1"},
+ {"a/b1/c1", "a", "../..", "/a"},
+ {"a/b1/c1/d1", "a", "../../..", "/a"},
+ {"a", "a/b1/c1/d1", "./b1/c1/d1", "/a/b1/c1/d1"},
+ {"a/b1", "a/b2", "../b2", "/a/b2"},
+ {"a/b2", "a/b1", "../b1", "/a/b1"},
+ {"a/b1/c1/d1", "a/b2/c2/d2", "../../../b2/c2/d2", "/a/b2/c2/d2"},
+ {"a/b2/c2/d2", "a/b1/c1/d1", "../../../b1/c1/d1", "/a/b1/c1/d1"},
+ }
- p2 := e1.GetRelativePath(e1)
- checkEq(t, p2, "./")
+ for _, c := range cases {
+ fe := doc.FindElement(c.from)
+ te := doc.FindElement(c.to)
- e3 := doc.FindElement("./store")
- e4 := e3.FindElement("./book/author")
- p3 := e4.GetRelativePath(e3)
- checkEq(t, p3, "./book/author")
+ rp := te.GetRelativePath(fe)
+ if rp != c.relpath {
+ t.Errorf("GetRelativePath from '%s' to '%s'. Expected '%s', got '%s'.\n", c.from, c.to, c.relpath, rp)
+ }
- p4 := e4.GetRelativePath(nil)
- checkEq(t, p4, "./store/book/author")
+ p := te.GetPath()
+ if p != c.topath {
+ t.Errorf("GetPath for '%s'. Expected '/%s', got '%s'.\n", c.to, c.topath, p)
+ }
+ }
}
func TestInsertChild(t *testing.T) {