diff --git a/manipulation.go b/manipulation.go new file mode 100644 index 0000000..7ff5d60 --- /dev/null +++ b/manipulation.go @@ -0,0 +1,132 @@ +package goquery + +import ( + "fmt" + "strings" + + "code.google.com/p/cascadia" + "code.google.com/p/go.net/html" +) + +func parseHtml(html string) *Selection { + // Errors are only returned when the io.Reader returns any error besides + // EOF, but strings.Reader never will + doc, err := NewDocumentFromReader(strings.NewReader(html)) + if err != nil { + panic(fmt.Sprintf("Could not parse HTML: %s", err)) + } + return doc.Find("body").Children() +} + +// Deep copy a slice of nodes. +func cloneNodes(ns []*html.Node) []*html.Node { + cns := make([]*html.Node, 0, len(ns)) + + for _, n := range ns { + cns = append(cns, cloneNode(n)) + } + + return cns +} + +// Deep copy a node. The new node has clones of all the original node's +// children but none of its parents or siblings. +func cloneNode(n *html.Node) *html.Node { + nn := &html.Node{ + Type: n.Type, + DataAtom: n.DataAtom, + Data: n.Data, + Attr: make([]html.Attribute, len(n.Attr)), + } + + copy(nn.Attr, n.Attr) + for c := n.FirstChild; c != nil; c = c.NextSibling { + nn.AppendChild(cloneNode(c)) + } + + return nn +} + +func (s *Selection) manipulateNodes( + ns []*html.Node, + reverse bool, + f func(sn *html.Node, n *html.Node)) *Selection { + + lasti := s.Size() - 1 + + // net.Html doesn't provide document fragments for insertion, so to get + // things in the correct order with After() and Prepend(), the callback + // needs to be called on the reverse of the nodes. + if reverse { + for i, j := 0, len(ns)-1; i < j; i, j = i+1, j-1 { + ns[i], ns[j] = ns[j], ns[i] + } + } + + for i, sn := range s.Nodes { + for _, n := range ns { + if i != lasti { + f(sn, cloneNode(n)) + } else { + if n.Parent != nil { + n.Parent.RemoveChild(n) + } + f(sn, n) + } + } + } + + return s +} + +// Append the elements, specified by the selector, to the end of each element +// in the set of matched elements. +// +// Take note: +// +// 1) The selector is applied to the root document. +// +// 2) If any elements specified in the parameter are still part of the +// document, they will be moved to the new location. +// +// 3) If there are multiple locations to append to, cloned nodes will be +// appended to all target locations except the last, which will be moved +// as noted in (1). +func (s *Selection) Append(selector string) *Selection { + return s.AppendSelector(cascadia.MustCompile(selector)) +} + +// From the root document, apply the cascadia selector, and append those nodes +// to the set of matched elements. +// This follows the same rules as Selection.Append(). +func (s *Selection) AppendSelector(cs cascadia.Selector) *Selection { + return s.AppendNodes(cs.MatchAll(s.document.rootNode)...) +} + +// Append the elements in the selection to the end of each element in the +// set of matched elements. +// This follows the same rules as Selection.Append(). +func (s *Selection) AppendSelection(sel *Selection) *Selection { + return s.AppendNodes(sel.Nodes...) +} + +// Parse the html and append it to the set of matched elements +func (s *Selection) AppendHtml(html string) *Selection { + return s.AppendSelection(parseHtml(html)) +} + +// Append the specified nodes to each node in the set of matched elements. +// This follows the same rules as Selection.Append(). +func (s *Selection) AppendNodes(ns ...*html.Node) *Selection { + return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) { + sn.AppendChild(n) + }) +} + +// Create a deep copy of the set of matched nodes. The new nodes will not be +// attached to the document. +func (s *Selection) Clone() *Selection { + ns := newEmptySelection(s.document) + ns.Nodes = cloneNodes(s.Nodes) + return ns +} diff --git a/manipulation_test.go b/manipulation_test.go new file mode 100644 index 0000000..cfe1a7c --- /dev/null +++ b/manipulation_test.go @@ -0,0 +1,47 @@ +package goquery + +import ( + "testing" +) + +func TestAppend(t *testing.T) { + doc := Doc2Clone() + doc.Find("#main").Append("#nf6") + + AssertLength(t, doc.Find("#foot #nf6").Nodes, 0) + AssertLength(t, doc.Find("#main #nf6").Nodes, 1) +} + +func TestAppendBody(t *testing.T) { + doc := Doc2Clone() + doc.Find("body").Append("#nf6") + + AssertLength(t, doc.Find("#foot #nf6").Nodes, 0) + AssertLength(t, doc.Find("#main #nf6").Nodes, 0) + AssertLength(t, doc.Find("body > #nf6").Nodes, 1) +} + +func TestAppendSelection(t *testing.T) { + doc := Doc2Clone() + doc.Find("#main").AppendSelection(doc.Find("#nf1, #nf2")) + + AssertLength(t, doc.Find("#foot #nf1").Nodes, 0) + AssertLength(t, doc.Find("#foot #nf2").Nodes, 0) + AssertLength(t, doc.Find("#main #nf1").Nodes, 1) + AssertLength(t, doc.Find("#main #nf2").Nodes, 1) +} + +func TestAppendClone(t *testing.T) { + doc := Doc2Clone() + doc.Find("#n1").AppendSelection(doc.Find("#nf1").Clone()) + + AssertLength(t, doc.Find("#foot #nf1").Nodes, 1) + AssertLength(t, doc.Find("#main #nf1").Nodes, 1) +} + +func TestAppendHtml(t *testing.T) { + doc := Doc2Clone() + doc.Find("div").AppendHtml("new node") + + AssertLength(t, doc.Find("strong").Nodes, 14) +} diff --git a/type.go b/type.go index e253a75..e22d00c 100644 --- a/type.go +++ b/type.go @@ -70,6 +70,11 @@ func NewDocumentFromResponse(res *http.Response) (*Document, error) { return newDocument(root, res.Request.URL), nil } +// Create a deep-clone of a document +func NewDocumentFromDocument(doc *Document) *Document { + return newDocument(cloneNode(doc.rootNode), doc.Url) +} + // Private constructor, make sure all fields are correctly filled. func newDocument(root *html.Node, url *url.URL) *Document { // Create and fill the document diff --git a/type_test.go b/type_test.go index 410a095..584fb96 100644 --- a/type_test.go +++ b/type_test.go @@ -21,24 +21,36 @@ func Doc() *Document { } return doc } +func DocClone() *Document { + return NewDocumentFromDocument(Doc()) +} func Doc2() *Document { if doc2 == nil { doc2 = LoadDoc("page2.html") } return doc2 } +func Doc2Clone() *Document { + return NewDocumentFromDocument(Doc2()) +} func DocB() *Document { if docB == nil { docB = LoadDoc("gotesting.html") } return docB } +func DocBClone() *Document { + return NewDocumentFromDocument(DocB()) +} func DocW() *Document { if docW == nil { docW = LoadDoc("gowiki.html") } return docW } +func DocWClone() *Document { + return NewDocumentFromDocument(DocW()) +} func AssertLength(t *testing.T, nodes []*html.Node, length int) { if len(nodes) != length {