Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions namespace_scope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package xmlquery

import (
"encoding/xml"
"io"
"strings"
"testing"
)

func TestOutputRestoresNamespaceAfterChild(t *testing.T) {
input := `<root xmlns:p="urn:test"><child xmlns:q="urn:test"><q:inside/></child><p:after p:id="v"/></root>`
doc, err := Parse(strings.NewReader(input))
if err != nil {
t.Fatal(err)
}
d := xml.NewDecoder(strings.NewReader(doc.OutputXML(false)))
found := false
for {
tok, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
if x, ok := tok.(xml.StartElement); ok && x.Name.Local == "after" {
found = true
if x.Name.Space != "urn:test" {
t.Errorf("namespace lost: %#v output%s", x.Name, doc.OutputXML(false))
}
if len(x.Attr) != 1 || x.Attr[0].Name != (xml.Name{Space: "urn:test", Local: "id"}) || x.Attr[0].Value != "v" {
t.Errorf("attribute namespace/value lost: %#v", x.Attr)
}
}
}
if !found {
t.Fatal("after missing")
}
}

func TestNamespaceScopeAfterDefaultNamespaceChild(t *testing.T) {
doc, err := Parse(strings.NewReader(`<root xmlns:p="urn:test"><child xmlns="urn:test"><inside/></child><p:after/></root>`))
if err != nil {
t.Fatal(err)
}
after := FindOne(doc, "//*[local-name()='after']")
if after == nil {
t.Fatal("after missing")
}
if after.Prefix != "p" || after.NamespaceURI != "urn:test" {
t.Errorf("after=%#v", after)
}
}

func TestStreamNamespaceScopeBetweenRecords(t *testing.T) {
sp, err := CreateStreamParser(strings.NewReader(`<root xmlns:p="urn:test"><child xmlns:q="urn:test"><q:inside/></child><p:after/></root>`), "/root/*")
if err != nil {
t.Fatal(err)
}
if _, err = sp.Read(); err != nil {
t.Fatal(err)
}
n, err := sp.Read()
if err != nil {
t.Fatal(err)
}
if n.Data != "after" || n.Prefix != "p" || n.NamespaceURI != "urn:test" {
t.Errorf("unexpected streamed node: %#v", n)
}
if _, err = sp.Read(); err != io.EOF {
t.Errorf("want EOF, got %v", err)
}
}

func TestNestedNamespaceAliasScope(t *testing.T) {
doc, err := Parse(strings.NewReader(`<root xmlns:p="urn:test"><child xmlns:q="urn:test"><grandchild xmlns:r="urn:test"><r:inside/></grandchild><q:middle q:id="inner"/></child><p:after p:id="outer"/></root>`))
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct{ name, prefix, value string }{{"middle", "q", "inner"}, {"after", "p", "outer"}} {
n := FindOne(doc, "//*[local-name()='"+tc.name+"']")
if n == nil {
t.Fatal(tc.name)
}
if n.Prefix != tc.prefix || n.NamespaceURI != "urn:test" || n.SelectAttr(tc.prefix+":id") != tc.value {
t.Errorf("namespace scope not restored for%s: %#v", tc.name, n)
}
}
decoder := xml.NewDecoder(strings.NewReader(doc.OutputXML(false)))
for {
tok, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
if n, ok := tok.(xml.StartElement); ok && (n.Name.Local == "middle" || n.Name.Local == "after") {
if n.Name.Space != "urn:test" || len(n.Attr) != 1 || n.Attr[0].Name.Space != "urn:test" {
t.Errorf("invalid roundtrip expanded names: %#v", n)
}
}
}
}
26 changes: 26 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type parser struct {
reader *cachedReader // Need to maintain a reference to the reader, so we can determine whether a node contains CDATA.
once sync.Once
space2prefix map[string]*xmlnsPrefix
prefixScopes []map[string]*xmlnsPrefix
lineStarts []int
}

Expand Down Expand Up @@ -174,18 +175,32 @@ func (p *parser) parse() (*Node, error) {
p.prev = node
}

// Namespace aliases declared by this element apply only in its subtree.
var previous map[string]*xmlnsPrefix
remember := func(space string) {
if previous == nil {
previous = make(map[string]*xmlnsPrefix)
}
if _, recorded := previous[space]; !recorded {
previous[space] = p.space2prefix[space]
}
}
for _, att := range tok.Attr {
if att.Name.Local == "xmlns" {
// https://github.com/antchfx/xmlquery/issues/67
if prefix, ok := p.space2prefix[att.Value]; !ok || (ok && prefix.level >= p.level) {
remember(att.Value)
p.space2prefix[att.Value] = &xmlnsPrefix{name: "", level: p.level} // reset empty if exist the default namespace
}
} else if att.Name.Space == "xmlns" {
// maybe there are have duplicate NamespaceURL?
remember(att.Value)
p.space2prefix[att.Value] = &xmlnsPrefix{name: att.Name.Local, level: p.level}
}
}

p.prefixScopes = append(p.prefixScopes, previous)

if space := tok.Name.Space; space != "" {
if _, found := p.space2prefix[space]; !found && p.decoder.Strict {
return nil, fmt.Errorf("xmlquery: invalid XML document, namespace %s is missing", space)
Expand Down Expand Up @@ -252,6 +267,17 @@ func (p *parser) parse() (*Node, error) {
p.level++
case xml.EndElement:
p.level--
if last := len(p.prefixScopes) - 1; last >= 0 {
for space, prefix := range p.prefixScopes[last] {
if prefix == nil {
delete(p.space2prefix, space)
} else {
p.space2prefix[space] = prefix
}
}
p.prefixScopes[last] = nil
p.prefixScopes = p.prefixScopes[:last]
}
// If we're in streaming mode, and we already have a potential streaming
// target node identified (p.streamNode != nil) then we need to check if
// this is the real one we want to return to caller.
Expand Down