Skip to content

Commit

Permalink
Merge PR '#38'. Expose functions:AddAttr, AddChild, AddSibling,…
Browse files Browse the repository at this point in the history
… `RemoveFromTree`
  • Loading branch information
zhengchun committed Aug 27, 2020
2 parents 8049e7d + 2958a82 commit 64ca73d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 43 deletions.
22 changes: 16 additions & 6 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ func (n *Node) OutputXML(self bool) string {
return buf.String()
}

func addAttr(n *Node, key, val string) {
// AddAttr adds a new attribute specified by 'key' and 'val' to a node 'n'.
func AddAttr(n *Node, key, val string) {
var attr xml.Attr
if i := strings.Index(key, ":"); i > 0 {
attr = xml.Attr{
Expand All @@ -158,10 +159,13 @@ func addAttr(n *Node, key, val string) {
n.Attr = append(n.Attr, attr)
}

func addChild(parent, n *Node) {
// AddChild adds a new node 'n' to a node 'parent' as its last child.
func AddChild(parent, n *Node) {
n.Parent = parent
n.NextSibling = nil
if parent.FirstChild == nil {
parent.FirstChild = n
n.PrevSibling = nil
} else {
parent.LastChild.NextSibling = n
n.PrevSibling = parent.LastChild
Expand All @@ -170,21 +174,27 @@ func addChild(parent, n *Node) {
parent.LastChild = n
}

func addSibling(sibling, n *Node) {
// AddSibling adds a new node 'n' as a sibling of a given node 'sibling'.
// Note it is not necessarily true that the new node 'n' would be added
// immediately after 'sibling'. If 'sibling' isn't the last child of its
// parent, then the new node 'n' will be added at the end of the sibling
// chain of their parent.
func AddSibling(sibling, n *Node) {
for t := sibling.NextSibling; t != nil; t = t.NextSibling {
sibling = t
}
n.Parent = sibling.Parent
sibling.NextSibling = n
n.PrevSibling = sibling
n.NextSibling = nil
if sibling.Parent != nil {
sibling.Parent.LastChild = n
}
}

// removes a node and its subtree from the tree it is in.
// If the node is the root of the tree, then it's no-op.
func removeFromTree(n *Node) {
// RemoveFromTree removes a node and its subtree from the document
// tree it is in. If the node is the root of the tree, then it's no-op.
func RemoveFromTree(n *Node) {
if n.Parent == nil {
return
}
Expand Down
54 changes: 47 additions & 7 deletions node_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package xmlquery

import (
"encoding/xml"
"html"
"reflect"
"strings"
"testing"
)

func findRoot(n *Node) *Node {
if n == nil {
return nil
}
for ; n.Parent != nil; n = n.Parent {
}
return n
}

func findNode(root *Node, name string) *Node {
node := root.FirstChild
for {
Expand Down Expand Up @@ -107,6 +117,36 @@ func verifyNodePointers(t *testing.T, n *Node) {
testTrue(t, parent == nil || parent.LastChild == cur)
}

func TestAddAttr(t *testing.T) {
for _, test := range []struct {
name string
n *Node
key string
val string
expected string
}{
{
name: "node has no existing attr",
n: &Node{Type: AttributeNode},
key: "ns:k1",
val: "v1",
expected: `< ns:k1="v1"></>`,
},
{
name: "node has existing attrs",
n: &Node{Type: AttributeNode, Attr: []xml.Attr{{Name: xml.Name{Local: "k1"}, Value: "v1"}}},
key: "k2",
val: "v2",
expected: `< k1="v1" k2="v2"></>`,
},
} {
t.Run(test.name, func(t *testing.T) {
AddAttr(test.n, test.key, test.val)
testValue(t, test.n.OutputXML(true), test.expected)
})
}
}

func TestRemoveFromTree(t *testing.T) {
xml := `<?procinst?>
<!--comment-->
Expand All @@ -123,7 +163,7 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
n := FindOne(doc, "//aaa/ddd/eee")
testTrue(t, n != nil)
removeFromTree(n)
RemoveFromTree(n)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><!--comment--><aaa><bbb></bbb><ddd></ddd><ggg></ggg></aaa>`)
Expand All @@ -133,7 +173,7 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
n := FindOne(doc, "//aaa/bbb")
testTrue(t, n != nil)
removeFromTree(n)
RemoveFromTree(n)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><!--comment--><aaa><ddd><eee><fff></fff></eee></ddd><ggg></ggg></aaa>`)
Expand All @@ -143,7 +183,7 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
n := FindOne(doc, "//aaa/ddd")
testTrue(t, n != nil)
removeFromTree(n)
RemoveFromTree(n)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><!--comment--><aaa><bbb></bbb><ggg></ggg></aaa>`)
Expand All @@ -153,7 +193,7 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
n := FindOne(doc, "//aaa/ggg")
testTrue(t, n != nil)
removeFromTree(n)
RemoveFromTree(n)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><!--comment--><aaa><bbb></bbb><ddd><eee><fff></fff></eee></ddd></aaa>`)
Expand All @@ -163,7 +203,7 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
procInst := doc.FirstChild
testValue(t, procInst.Type, DeclarationNode)
removeFromTree(procInst)
RemoveFromTree(procInst)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<!--comment--><aaa><bbb></bbb><ddd><eee><fff></fff></eee></ddd><ggg></ggg></aaa>`)
Expand All @@ -173,15 +213,15 @@ func TestRemoveFromTree(t *testing.T) {
doc := parseXML()
commentNode := doc.FirstChild.NextSibling.NextSibling // First .NextSibling is an empty text node.
testValue(t, commentNode.Type, CommentNode)
removeFromTree(commentNode)
RemoveFromTree(commentNode)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><aaa><bbb></bbb><ddd><eee><fff></fff></eee></ddd><ggg></ggg></aaa>`)
})

t.Run("remove call on root does nothing", func(t *testing.T) {
doc := parseXML()
removeFromTree(doc)
RemoveFromTree(doc)
verifyNodePointers(t, doc)
testValue(t, doc.OutputXML(false),
`<?procinst?><!--comment--><aaa><bbb></bbb><ddd><eee><fff></fff></eee></ddd><ggg></ggg></aaa>`)
Expand Down
28 changes: 14 additions & 14 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (p *parser) parse() (*Node, error) {
if p.level == 0 {
// mising XML declaration
node := &Node{Type: DeclarationNode, Data: "xml", level: 1}
addChild(p.prev, node)
AddChild(p.prev, node)
p.level = 1
p.prev = node
}
Expand Down Expand Up @@ -112,14 +112,14 @@ func (p *parser) parse() (*Node, error) {
}
//fmt.Println(fmt.Sprintf("start > %s : %d", node.Data, node.level))
if p.level == p.prev.level {
addSibling(p.prev, node)
AddSibling(p.prev, node)
} else if p.level > p.prev.level {
addChild(p.prev, node)
AddChild(p.prev, node)
} else if p.level < p.prev.level {
for i := p.prev.level - p.level; i > 1; i-- {
p.prev = p.prev.Parent
}
addSibling(p.prev.Parent, node)
AddSibling(p.prev.Parent, node)
}
// If we're in the streaming mode, we need to remember the node if it is the target node
// so that when we finish processing the node's EndElement, we know how/what to return to
Expand Down Expand Up @@ -172,26 +172,26 @@ func (p *parser) parse() (*Node, error) {
case xml.CharData:
node := &Node{Type: CharDataNode, Data: string(tok), level: p.level}
if p.level == p.prev.level {
addSibling(p.prev, node)
AddSibling(p.prev, node)
} else if p.level > p.prev.level {
addChild(p.prev, node)
AddChild(p.prev, node)
} else if p.level < p.prev.level {
for i := p.prev.level - p.level; i > 1; i-- {
p.prev = p.prev.Parent
}
addSibling(p.prev.Parent, node)
AddSibling(p.prev.Parent, node)
}
case xml.Comment:
node := &Node{Type: CommentNode, Data: string(tok), level: p.level}
if p.level == p.prev.level {
addSibling(p.prev, node)
AddSibling(p.prev, node)
} else if p.level > p.prev.level {
addChild(p.prev, node)
AddChild(p.prev, node)
} else if p.level < p.prev.level {
for i := p.prev.level - p.level; i > 1; i-- {
p.prev = p.prev.Parent
}
addSibling(p.prev.Parent, node)
AddSibling(p.prev.Parent, node)
}
case xml.ProcInst: // Processing Instruction
if p.prev.Type != DeclarationNode {
Expand All @@ -202,13 +202,13 @@ func (p *parser) parse() (*Node, error) {
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if i := strings.Index(pair, "="); i > 0 {
addAttr(node, pair[:i], strings.Trim(pair[i+1:], `"`))
AddAttr(node, pair[:i], strings.Trim(pair[i+1:], `"`))
}
}
if p.level == p.prev.level {
addSibling(p.prev, node)
AddSibling(p.prev, node)
} else if p.level > p.prev.level {
addChild(p.prev, node)
AddChild(p.prev, node)
}
p.prev = node
case xml.Directive:
Expand Down Expand Up @@ -293,7 +293,7 @@ func (sp *StreamParser) Read() (*Node, error) {
// Because this is a streaming read, we need to release/remove last
// target node from the node tree to free up memory.
if sp.p.streamNode != nil {
removeFromTree(sp.p.streamNode)
RemoveFromTree(sp.p.streamNode)
sp.p.prev = sp.p.streamNodePrev
sp.p.streamNode = nil
sp.p.streamNodePrev = nil
Expand Down
23 changes: 7 additions & 16 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,6 @@ func TestStreamParser_InvalidXPath(t *testing.T) {
}
}

func root(n *Node) *Node {
if n == nil {
return nil
}
for ; n.Parent != nil; n = n.Parent {
}
return n
}

func testOutputXML(t *testing.T, msg string, expectedXML string, n *Node) {
if n.OutputXML(true) != expectedXML {
t.Fatalf("%s, expected XML: '%s', actual: '%s'", msg, expectedXML, n.OutputXML(true))
Expand Down Expand Up @@ -309,7 +300,7 @@ func TestStreamParser_Success1(t *testing.T) {
t.Fatal(err.Error())
}
testOutputXML(t, "first call result", `<BBB>b1</BBB>`, n)
testOutputXML(t, "doc after first call", `<><?xml?><AAA><CCC>c1</CCC><BBB>b1</BBB></AAA></>`, root(n))
testOutputXML(t, "doc after first call", `<><?xml?><AAA><CCC>c1</CCC><BBB>b1</BBB></AAA></>`, findRoot(n))

// Second `<BBB>` read
n, err = sp.Read()
Expand All @@ -318,7 +309,7 @@ func TestStreamParser_Success1(t *testing.T) {
}
testOutputXML(t, "second call result", `<BBB>b2<ZZZ z="1">z1</ZZZ></BBB>`, n)
testOutputXML(t, "doc after second call",
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b2<ZZZ z="1">z1</ZZZ></BBB></AAA></>`, root(n))
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b2<ZZZ z="1">z1</ZZZ></BBB></AAA></>`, findRoot(n))

// Third `<BBB>` read (Note we will skip 'b3' since the streamElementFilter excludes it)
n, err = sp.Read()
Expand All @@ -330,7 +321,7 @@ func TestStreamParser_Success1(t *testing.T) {
// been filtered out and is not our target node, thus it is considered just like any other
// non target nodes such as `<CCC>`` or `<DDD>`
testOutputXML(t, "doc after third call",
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b3</BBB><BBB>b4</BBB></AAA></>`, root(n))
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b3</BBB><BBB>b4</BBB></AAA></>`, findRoot(n))

// Fourth `<BBB>` read
n, err = sp.Read()
Expand All @@ -340,7 +331,7 @@ func TestStreamParser_Success1(t *testing.T) {
testOutputXML(t, "fourth call result", `<BBB>b5</BBB>`, n)
// Note the inclusion of `<BBB>b3</BBB>` in the document.
testOutputXML(t, "doc after fourth call",
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b3</BBB><BBB>b5</BBB></AAA></>`, root(n))
`<><?xml?><AAA><CCC>c1</CCC><DDD>d1</DDD><BBB>b3</BBB><BBB>b5</BBB></AAA></>`, findRoot(n))

_, err = sp.Read()
if err != io.EOF {
Expand Down Expand Up @@ -369,7 +360,7 @@ func TestStreamParser_Success2(t *testing.T) {
t.Fatal(err.Error())
}
testOutputXML(t, "first call result", `<CCC>c1</CCC>`, n)
testOutputXML(t, "doc after first call", `<><?xml?><AAA><CCC>c1</CCC></AAA></>`, root(n))
testOutputXML(t, "doc after first call", `<><?xml?><AAA><CCC>c1</CCC></AAA></>`, findRoot(n))

// Second Read() should return d1
n, err = sp.Read()
Expand All @@ -378,7 +369,7 @@ func TestStreamParser_Success2(t *testing.T) {
}
testOutputXML(t, "second call result", `<DDD>d1</DDD>`, n)
testOutputXML(t, "doc after second call",
`<><?xml?><AAA><BBB>b1</BBB><DDD>d1</DDD></AAA></>`, root(n))
`<><?xml?><AAA><BBB>b1</BBB><DDD>d1</DDD></AAA></>`, findRoot(n))

// Third call should return c2
n, err = sp.Read()
Expand All @@ -387,7 +378,7 @@ func TestStreamParser_Success2(t *testing.T) {
}
testOutputXML(t, "third call result", `<CCC>c2</CCC>`, n)
testOutputXML(t, "doc after third call",
`<><?xml?><AAA><BBB>b1</BBB><BBB>b2</BBB><CCC>c2</CCC></AAA></>`, root(n))
`<><?xml?><AAA><BBB>b1</BBB><BBB>b2</BBB><CCC>c2</CCC></AAA></>`, findRoot(n))

_, err = sp.Read()
if err != io.EOF {
Expand Down

0 comments on commit 64ca73d

Please sign in to comment.