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("Test | Again | ")
+ 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("new1 | new2 | ")
+
+ 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("new1 | new2 | ")
+
+ 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 node | other 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("Test | Replace | ")
+
+ 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("Test | Again | ")
+
+ 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 ""
}