diff --git a/manipulation.go b/manipulation.go index b7f2fe5..2c31770 100644 --- a/manipulation.go +++ b/manipulation.go @@ -39,8 +39,15 @@ func (s *Selection) AfterSelection(sel *Selection) *Selection { // AfterHtml parses the html and inserts it after the set of matched elements. // // This follows the same rules as Selection.Append. -func (s *Selection) AfterHtml(html string) *Selection { - return s.AfterNodes(parseHtml(html)...) +func (s *Selection) AfterHtml(htmlStr string) *Selection { + return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) { + nextSibling := node.NextSibling + for _, n := range nodes { + if node.Parent != nil { + node.Parent.InsertBefore(n, nextSibling) + } + } + }) } // AfterNodes inserts the nodes after each element in the set of matched elements. @@ -85,8 +92,12 @@ func (s *Selection) AppendSelection(sel *Selection) *Selection { } // AppendHtml parses the html and appends it to the set of matched elements. -func (s *Selection) AppendHtml(html string) *Selection { - return s.AppendNodes(parseHtml(html)...) +func (s *Selection) AppendHtml(htmlStr string) *Selection { + return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) { + for _, n := range nodes { + node.AppendChild(n) + } + }) } // AppendNodes appends the specified nodes to each node in the set of matched elements. @@ -123,8 +134,14 @@ func (s *Selection) BeforeSelection(sel *Selection) *Selection { // BeforeHtml parses the html and inserts it before the set of matched elements. // // This follows the same rules as Selection.Append. -func (s *Selection) BeforeHtml(html string) *Selection { - return s.BeforeNodes(parseHtml(html)...) +func (s *Selection) BeforeHtml(htmlStr string) *Selection { + return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) { + for _, n := range nodes { + if node.Parent != nil { + node.Parent.InsertBefore(n, node) + } + } + }) } // BeforeNodes inserts the nodes before each element in the set of matched elements. @@ -184,8 +201,13 @@ func (s *Selection) PrependSelection(sel *Selection) *Selection { } // PrependHtml parses the html and prepends it to the set of matched elements. -func (s *Selection) PrependHtml(html string) *Selection { - return s.PrependNodes(parseHtml(html)...) +func (s *Selection) PrependHtml(htmlStr string) *Selection { + return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) { + firstChild := node.FirstChild + for _, n := range nodes { + node.InsertBefore(n, firstChild) + } + }) } // PrependNodes prepends the specified nodes to each node in the set of @@ -261,8 +283,16 @@ func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection { // It returns the removed elements. // // This follows the same rules as Selection.Append. -func (s *Selection) ReplaceWithHtml(html string) *Selection { - return s.ReplaceWithNodes(parseHtml(html)...) +func (s *Selection) ReplaceWithHtml(htmlStr string) *Selection { + s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) { + nextSibling := node.NextSibling + for _, n := range nodes { + if node.Parent != nil { + node.Parent.InsertBefore(n, nextSibling) + } + } + }) + return s.Remove() } // ReplaceWithNodes replaces each element in the set of matched elements with @@ -277,8 +307,17 @@ func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection { // SetHtml sets the html content of each element in the selection to // specified html string. -func (s *Selection) SetHtml(html string) *Selection { - return setHtmlNodes(s, parseHtml(html)...) +func (s *Selection) SetHtml(htmlStr string) *Selection { + for _, context := range s.Nodes { + for c := context.FirstChild; c != nil; c = context.FirstChild { + context.RemoveChild(c) + } + } + return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) { + for _, n := range nodes { + node.AppendChild(n) + } + }) } // SetText sets the content of each element in the selection to specified content. @@ -334,8 +373,23 @@ func (s *Selection) WrapSelection(sel *Selection) *Selection { // most child of the given HTML. // // It returns the original set of elements. -func (s *Selection) WrapHtml(html string) *Selection { - return s.wrapNodes(parseHtml(html)...) +func (s *Selection) WrapHtml(htmlStr string) *Selection { + nodesMap := make(map[string][]*html.Node) + for _, context := range s.Nodes { + var parent *html.Node + if context.Parent != nil { + parent = context.Parent + } else { + parent = &html.Node{Type: html.ElementNode} + } + nodes, found := nodesMap[nodeName(parent)] + if !found { + nodes = parseHtmlWithContext(htmlStr, parent) + nodesMap[nodeName(parent)] = nodes + } + newSingleSelection(context, s.document).wrapAllNodes(cloneNodes(nodes)...) + } + return s } // WrapNode wraps each element in the set of matched elements inside the inner- @@ -387,8 +441,18 @@ func (s *Selection) WrapAllSelection(sel *Selection) *Selection { // document. // // It returns the original set of elements. -func (s *Selection) WrapAllHtml(html string) *Selection { - return s.wrapAllNodes(parseHtml(html)...) +func (s *Selection) WrapAllHtml(htmlStr string) *Selection { + var context *html.Node + var nodes []*html.Node + if len(s.Nodes) > 0 { + context = s.Nodes[0] + if context.Parent != nil { + nodes = parseHtmlWithContext(htmlStr, context) + } else { + nodes = parseHtml(htmlStr) + } + } + return s.wrapAllNodes(nodes...) } func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection { @@ -457,8 +521,17 @@ func (s *Selection) WrapInnerSelection(sel *Selection) *Selection { // cloned before being inserted into the document. // // It returns the original set of elements. -func (s *Selection) WrapInnerHtml(html string) *Selection { - return s.wrapInnerNodes(parseHtml(html)...) +func (s *Selection) WrapInnerHtml(htmlStr string) *Selection { + nodesMap := make(map[string][]*html.Node) + for _, context := range s.Nodes { + nodes, found := nodesMap[nodeName(context)] + if !found { + nodes = parseHtmlWithContext(htmlStr, context) + nodesMap[nodeName(context)] = nodes + } + newSingleSelection(context, s.document).wrapInnerNodes(cloneNodes(nodes)...) + } + return s } // WrapInnerNode wraps an HTML structure, matched by the given selector, around @@ -498,16 +571,14 @@ func parseHtml(h string) []*html.Node { return nodes } -func setHtmlNodes(s *Selection, ns ...*html.Node) *Selection { - for _, n := range s.Nodes { - for c := n.FirstChild; c != nil; c = n.FirstChild { - n.RemoveChild(c) - } - for _, c := range ns { - n.AppendChild(cloneNode(c)) - } +func parseHtmlWithContext(h string, context *html.Node) []*html.Node { + // Errors are only returned when the io.Reader returns any error besides + // EOF, but strings.Reader never will + nodes, err := html.ParseFragment(strings.NewReader(h), context) + if err != nil { + panic("goquery: failed to parse HTML: " + err.Error()) } - return s + return nodes } // Get the first child that is an ElementNode @@ -577,3 +648,29 @@ func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool, return s } + +// eachNodeHtml parses the given html string and inserts the resulting nodes in the dom with the mergeFn. +// The parsed nodes are inserted for each element of the selection. +// isParent can be used to indicate that the elements of the selection should be treated as the parent for the parsed html. +// A cache is used to avoid parsing the html multiple times should the elements of the selection result in the same context. +func (s *Selection) eachNodeHtml(htmlStr string, isParent bool, mergeFn func(n *html.Node, nodes []*html.Node)) *Selection { + // cache to avoid parsing the html for the same context multiple times + nodeCache := make(map[string][]*html.Node) + var context *html.Node + for _, n := range s.Nodes { + if isParent { + context = n.Parent + } else { + context = n + } + if context != nil { + nodes, found := nodeCache[nodeName(context)] + if !found { + nodes = parseHtmlWithContext(htmlStr, context) + nodeCache[nodeName(context)] = nodes + } + mergeFn(n, cloneNodes(nodes)) + } + } + return s +} diff --git a/manipulation_test.go b/manipulation_test.go index c5f5022..74dda7c 100644 --- a/manipulation_test.go +++ b/manipulation_test.go @@ -56,6 +56,26 @@ func TestAfterHtml(t *testing.T) { printSel(t, doc.Selection) } +func TestAfterHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table tr td").AfterHtml("TestAgain") + assertLength(t, doc.Find("table tr td").Nodes, 6) + assertClass(t, doc.Find("table tr td").Last(), "c2") + printSel(t, doc.Selection) +} + func TestAppend(t *testing.T) { doc := Doc2Clone() doc.Find("#main").Append("#nf6") @@ -113,6 +133,27 @@ func TestAppendHtml(t *testing.T) { printSel(t, doc.Selection) } +func TestAppendHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table tr").AppendHtml("new1new2") + + assertLength(t, doc.Find("table td").Nodes, 6) + assertClass(t, doc.Find("table td").Last(), "c2") + printSel(t, doc.Selection) +} + func TestBefore(t *testing.T) { doc := Doc2Clone() doc.Find("#main").Before("#nf6") @@ -151,6 +192,27 @@ func TestBeforeHtml(t *testing.T) { printSel(t, doc.Selection) } +func TestBeforeHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table tr td:first-child").BeforeHtml("new1new2") + + assertLength(t, doc.Find("table td").Nodes, 6) + assertClass(t, doc.Find("table td").First(), "c1") + printSel(t, doc.Selection) +} + func TestEmpty(t *testing.T) { doc := Doc2Clone() s := doc.Find("#main").Empty() @@ -218,6 +280,27 @@ func TestPrependHtml(t *testing.T) { printSel(t, doc.Selection) } +func TestPrependHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table tr").PrependHtml("new nodeother new node") + + assertLength(t, doc.Find("table td").Nodes, 6) + assertClass(t, doc.Find("table tr td").First(), "c1") + printSel(t, doc.Selection) +} + func TestRemove(t *testing.T) { doc := Doc2Clone() doc.Find("#nf1").Remove() @@ -278,6 +361,28 @@ func TestReplaceWithHtml(t *testing.T) { printSel(t, doc.Selection) } +func TestReplaceWithHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table th").ReplaceWithHtml("TestReplace") + + assertLength(t, doc.Find("table th").Nodes, 0) + assertLength(t, doc.Find("table tr td").Nodes, 4) + assertClass(t, doc.Find("table tr td").First(), "c1") + printSel(t, doc.Selection) +} + func TestSetHtml(t *testing.T) { doc := Doc2Clone() q := doc.Find("#main, #foot") @@ -313,6 +418,28 @@ func TestSetHtmlEmpty(t *testing.T) { printSel(t, doc.Selection) } +func TestSetHtmlContext(t *testing.T) { + doc := loadString(t, ` + + + + + + + + + +
Before1
Before2
+ + `) + doc.Find("table tr").SetHtml("TestAgain") + + assertLength(t, doc.Find("table th").Nodes, 0) + assertLength(t, doc.Find("table td").Nodes, 4) + assertLength(t, doc.Find("table tr").Nodes, 2) + printSel(t, doc.Selection) +} + func TestSetText(t *testing.T) { doc := Doc2Clone() q := doc.Find("#main, #foot") @@ -511,3 +638,54 @@ func TestWrapInnerHtml(t *testing.T) { printSel(t, doc.Selection) } + +func TestParsingRespectsVaryingContext(t *testing.T) { + docA := loadString(t, ` + + + + + `) + docTable := loadString(t, ` + + +
+ + `) + docBoth := loadString(t, ` + + +
+ + + `) + + sA := docA.Find(".x").AppendHtml("Hello") + sTable := docTable.Find(".x").AppendHtml("Hello") + sBoth := docBoth.Find(".x").AppendHtml("Hello") + + printSel(t, docA.Selection) + printSel(t, docTable.Selection) + printSel(t, docBoth.Selection) + + oA, _ := sA.Html() + oTable, _ := sTable.Html() + + if oA == oTable { + t.Errorf("Expected inner html of and to not be equal, but got %s and %s", oA, oTable) + } + + oBothTable, _ := sBoth.First().Html() + if oBothTable != oTable { + t.Errorf("Expected inner html of
and
in doc containing both tags to be equal, but got %s and %s", + oTable, + oBothTable) + } + + oBothA, _ := sBoth.Last().Html() + if oBothA != oA { + t.Errorf("Expected inner html of and in doc containing both tags to be equal, but got %s and %s", + oA, + oBothA) + } +} diff --git a/type_test.go b/type_test.go index 1e82d5e..798d4ea 100644 --- a/type_test.go +++ b/type_test.go @@ -121,6 +121,14 @@ func loadDoc(page string) *Document { return NewDocumentFromNode(node) } +func loadString(t *testing.T, doc string) *Document { + d, err := NewDocumentFromReader(strings.NewReader(doc)) + if err != nil { + t.Error("Failed to parse test document") + } + return d +} + func TestNewDocument(t *testing.T) { if f, e := os.Open("./testdata/page.html"); e != nil { t.Error(e.Error()) diff --git a/utilities.go b/utilities.go index b4c061a..3e11b1d 100644 --- a/utilities.go +++ b/utilities.go @@ -36,12 +36,22 @@ func NodeName(s *Selection) string { if s.Length() == 0 { return "" } - switch n := s.Get(0); n.Type { + return nodeName(s.Get(0)) +} + +// nodeName returns the node name of the given html node. +// See NodeName for additional details on behaviour. +func nodeName(node *html.Node) string { + if node == nil { + return "" + } + + switch node.Type { case html.ElementNode, html.DoctypeNode: - return n.Data + return node.Data default: - if n.Type >= 0 && int(n.Type) < len(nodeNames) { - return nodeNames[n.Type] + if node.Type >= 0 && int(node.Type) < len(nodeNames) { + return nodeNames[node.Type] } return "" }