From 2958a822a6456bafa570e87112fc53b7a00fb776 Mon Sep 17 00:00:00 2001 From: jf-tech Date: Thu, 27 Aug 2020 14:52:19 +1200 Subject: [PATCH] Expose a number of `*Node` related helper functions: `AddAttr`, `AddChild`, `AddSibling`, `RemoveFromTree` We have a use of those helpers, currently we simply cut and paste the code in our own repo. Also added a bit more tests to make `node.go` coverage to reach 100%. --- node.go | 22 +++++++++++++++------ node_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++------- parse.go | 28 +++++++++++++------------- parse_test.go | 23 +++++++--------------- 4 files changed, 84 insertions(+), 43 deletions(-) diff --git a/node.go b/node.go index fab652e..e053748 100644 --- a/node.go +++ b/node.go @@ -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{ @@ -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 @@ -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 } diff --git a/node_test.go b/node_test.go index 7bf011b..a96a8e0 100644 --- a/node_test.go +++ b/node_test.go @@ -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 { @@ -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 := ` @@ -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), ``) @@ -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), ``) @@ -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), ``) @@ -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), ``) @@ -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), ``) @@ -173,7 +213,7 @@ 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), ``) @@ -181,7 +221,7 @@ func TestRemoveFromTree(t *testing.T) { 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), ``) diff --git a/parse.go b/parse.go index cf7ab39..7b5cd66 100644 --- a/parse.go +++ b/parse.go @@ -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 } @@ -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 @@ -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 { @@ -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: @@ -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 diff --git a/parse_test.go b/parse_test.go index 894ab51..6099741 100644 --- a/parse_test.go +++ b/parse_test.go @@ -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)) @@ -309,7 +300,7 @@ func TestStreamParser_Success1(t *testing.T) { t.Fatal(err.Error()) } testOutputXML(t, "first call result", `b1`, n) - testOutputXML(t, "doc after first call", `<>c1b1`, root(n)) + testOutputXML(t, "doc after first call", `<>c1b1`, findRoot(n)) // Second `` read n, err = sp.Read() @@ -318,7 +309,7 @@ func TestStreamParser_Success1(t *testing.T) { } testOutputXML(t, "second call result", `b2z1`, n) testOutputXML(t, "doc after second call", - `<>c1d1b2z1`, root(n)) + `<>c1d1b2z1`, findRoot(n)) // Third `` read (Note we will skip 'b3' since the streamElementFilter excludes it) n, err = sp.Read() @@ -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 ``` or `` testOutputXML(t, "doc after third call", - `<>c1d1b3b4`, root(n)) + `<>c1d1b3b4`, findRoot(n)) // Fourth `` read n, err = sp.Read() @@ -340,7 +331,7 @@ func TestStreamParser_Success1(t *testing.T) { testOutputXML(t, "fourth call result", `b5`, n) // Note the inclusion of `b3` in the document. testOutputXML(t, "doc after fourth call", - `<>c1d1b3b5`, root(n)) + `<>c1d1b3b5`, findRoot(n)) _, err = sp.Read() if err != io.EOF { @@ -369,7 +360,7 @@ func TestStreamParser_Success2(t *testing.T) { t.Fatal(err.Error()) } testOutputXML(t, "first call result", `c1`, n) - testOutputXML(t, "doc after first call", `<>c1`, root(n)) + testOutputXML(t, "doc after first call", `<>c1`, findRoot(n)) // Second Read() should return d1 n, err = sp.Read() @@ -378,7 +369,7 @@ func TestStreamParser_Success2(t *testing.T) { } testOutputXML(t, "second call result", `d1`, n) testOutputXML(t, "doc after second call", - `<>b1d1`, root(n)) + `<>b1d1`, findRoot(n)) // Third call should return c2 n, err = sp.Read() @@ -387,7 +378,7 @@ func TestStreamParser_Success2(t *testing.T) { } testOutputXML(t, "third call result", `c2`, n) testOutputXML(t, "doc after third call", - `<>b1b2c2`, root(n)) + `<>b1b2c2`, findRoot(n)) _, err = sp.Read() if err != io.EOF {