diff --git a/manipulation.go b/manipulation.go index c91da9b..baf5c0c 100644 --- a/manipulation.go +++ b/manipulation.go @@ -8,14 +8,193 @@ import ( "golang.org/x/net/html" ) -func parseHtml(html string) *Selection { +// After applies the selector from the root document and inserts the matched elements +// after the elements in the set of matched elements. +// +// If one of the matched elements in the selection is not currently in the +// document, it's impossible to insert nodes after it, so it will be ignored. +// +// This follows the same rules outlined in Selection.Append. +func (s *Selection) After(selector string) *Selection { + return s.AfterMatcher(cascadia.MustCompile(selector)) +} + +// AfterMatcher applies the matcher from the root document and inserts the matched elements +// after the elements in the set of matched elements. +// +// If one of the matched elements in the selection is not currently in the +// document, it's impossible to insert nodes after it, so it will be ignored. +// +// This follows the same rules outlined in Selection.Append. +func (s *Selection) AfterMatcher(m Matcher) *Selection { + return s.AfterNodes(m.MatchAll(s.document.rootNode)...) +} + +// AfterSelection inserts the elements in the selection after each element in the set of matched +// elements. +// +// This follows the same rules outlined in Selection.Append. +func (s *Selection) AfterSelection(sel *Selection) *Selection { + return s.AfterNodes(sel.Nodes...) +} + +// AfterHtml parses the html and inserts it after the set of matched elements. +// +// This follows the same rules outlined in Selection.Append. +func (s *Selection) AfterHtml(html string) *Selection { + return s.AfterNodes(parseHtml(html)...) +} + +// AfterNodes inserts the nodes after each element in the set of matched elements. +// +// This follows the same rules outlined in Selection.Append. +func (s *Selection) AfterNodes(ns ...*html.Node) *Selection { + return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) { + if sn.Parent != nil { + sn.Parent.InsertBefore(n, sn.NextSibling) + } + }) +} + +// Append appends the elements specified by the selector to the end of each element +// in the set of matched elements, following those rules: +// +// 1) The selector is applied to the root document. +// +// 2) Elements that are part of the document 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 one, which will be moved +// as noted in (2). +func (s *Selection) Append(selector string) *Selection { + return s.AppendMatcher(cascadia.MustCompile(selector)) +} + +// AppendMatcher appends the elements specified by the matcher to the end of each element +// in the set of matched elements. +// +// This follows the same rules as Selection.Append. +func (s *Selection) AppendMatcher(m Matcher) *Selection { + return s.AppendNodes(m.MatchAll(s.document.rootNode)...) +} + +// AppendSelection appends 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...) +} + +// 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)...) +} + +// AppendNodes appends 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) + }) +} + +// Before inserts the matched elements before each element in the set of matched elements. +// +// This follows the same rules as Selection.Append. +func (s *Selection) Before(selector string) *Selection { + return s.BeforeMatcher(cascadia.MustCompile(selector)) +} + +// BeforeMatcher inserts the matched elements before each element in the set of matched elements. +// +// This follows the same rules as Selection.Append. +func (s *Selection) BeforeMatcher(m Matcher) *Selection { + return s.BeforeNodes(m.MatchAll(s.document.rootNode)...) +} + +// BeforeSelection inserts the elements in the selection before each element in the set of matched +// elements. +// +// This follows the same rules as Selection.Append. +func (s *Selection) BeforeSelection(sel *Selection) *Selection { + return s.BeforeNodes(sel.Nodes...) +} + +// 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)...) +} + +// BeforeNodes inserts the nodes before each element in the set of matched elements. +// +// This follows the same rules as Selection.Append. +func (s *Selection) BeforeNodes(ns ...*html.Node) *Selection { + return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) { + if sn.Parent != nil { + sn.Parent.InsertBefore(n, sn) + } + }) +} + +// Clone creates 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 +} + +// Empty removes all children nodes from the set of matched elements. +// It returns the children nodes in a new Selection. +func (s *Selection) Empty() *Selection { + var nodes []*html.Node + + for _, n := range s.Nodes { + for c := n.FirstChild; c != nil; c = n.FirstChild { + n.RemoveChild(c) + nodes = append(nodes, c) + } + } + + return pushStack(s, nodes) +} + +// Remove removes the set of matched elements from the document. +// It returns the same selection, now consisting of nodes not in the document. +func (s *Selection) Remove() *Selection { + for _, n := range s.Nodes { + if n.Parent != nil { + n.Parent.RemoveChild(n) + } + } + + return s +} + +// RemoveFiltered removes the set of matched elements by selector. +// It returns the Selection of removed nodes. +func (s *Selection) RemoveFiltered(selector string) *Selection { + return s.RemoveMatcher(cascadia.MustCompile(selector)) +} + +// RemoveMatcher removes the set of matched elements. +// It returns the Selection of removed nodes. +func (s *Selection) RemoveMatcher(m Matcher) *Selection { + return s.FilterMatcher(m).Remove() +} + +func parseHtml(h string) []*html.Node { // Errors are only returned when the io.Reader returns any error besides // EOF, but strings.Reader never will - doc, err := NewDocumentFromReader(strings.NewReader(html)) + nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode}) if err != nil { - panic(fmt.Sprintf("Could not parse HTML: %s", err)) + panic(fmt.Sprintf("goquery: failed to parse HTML: %s", err)) } - return doc.Find("body").Children() + return nodes } // Deep copy a slice of nodes. @@ -47,9 +226,7 @@ func cloneNode(n *html.Node) *html.Node { return nn } -func (s *Selection) manipulateNodes( - ns []*html.Node, - reverse bool, +func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool, f func(sn *html.Node, n *html.Node)) *Selection { lasti := s.Size() - 1 @@ -78,176 +255,3 @@ func (s *Selection) manipulateNodes( return s } - -// After applies the selector from the root document and inserts the matched elements -// after the elements in the set of matched elements. -// -// If one of the matched elements in the selection is not currently in the -// document, it's impossible to insert nodes after it, so it will be ignored. -// -// This follows the same rules as Selection.Append. -func (s *Selection) After(selector string) *Selection { - return s.AfterMatcher(cascadia.MustCompile(selector)) -} - -// AfterMatcher applies the matcher from the root document and inserts the matched elements -// after the elements in the set of matched elements. -// -// If one of the matched elements in the selection is not currently in the -// document, it's impossible to insert nodes after it, so it will be ignored. -// -// This follows the same rules as Selection.Append. -func (s *Selection) AfterMatcher(m Matcher) *Selection { - return s.AfterNodes(m.MatchAll(s.document.rootNode)...) -} - -// AfterSelection inserts the elements in the selection after each element in the set of matched -// elements. -// This follows the same rules as Selection.After. -func (s *Selection) AfterSelection(sel *Selection) *Selection { - return s.AfterNodes(sel.Nodes...) -} - -// AfterHtml parses the html and inserts it after the set of matched elements -// This follows the same rules as Selection.After. -func (s *Selection) AfterHtml(html string) *Selection { - return s.AfterSelection(parseHtml(html)) -} - -// AfterNodes inserts the nodes after each element in the set of matched elements. -// This follows the same rules as Selection.After. -func (s *Selection) AfterNodes(ns ...*html.Node) *Selection { - return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) { - if sn.Parent != nil { - sn.Parent.InsertBefore(n, sn.NextSibling) - } - }) -} - -// 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.AppendMatcher(cascadia.MustCompile(selector)) -} - -// AppendMatcher applies the matcher from the root document, and append those nodes -// to the set of matched elements. -// This follows the same rules as Selection.Append. -func (s *Selection) AppendMatcher(m Matcher) *Selection { - return s.AppendNodes(m.MatchAll(s.document.rootNode)...) -} - -// AppendSelection appends 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...) -} - -// AppendHtml parses the html and appends it to the set of matched elements. -func (s *Selection) AppendHtml(html string) *Selection { - return s.AppendSelection(parseHtml(html)) -} - -// AppendNodes appends 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) - }) -} - -// Before applies the selector from the root document, and inserts the matched elements -// before each element in the set of matched elements. -// This follows the same rules as Selection.After. -func (s *Selection) Before(selector string) *Selection { - return s.BeforeMatcher(cascadia.MustCompile(selector)) -} - -// BeforeMatcher applies the matcher from the root document, and inserts the matched -// elements before each element in the set of matched elements. -// This follows the same rules as Selection.After. -func (s *Selection) BeforeMatcher(m Matcher) *Selection { - return s.BeforeNodes(m.MatchAll(s.document.rootNode)...) -} - -// BeforeSelection inserts the elements in the selection before each element in the set of matched -// elements. -// This follows the same rules as Selection.After. -func (s *Selection) BeforeSelection(sel *Selection) *Selection { - return s.BeforeNodes(sel.Nodes...) -} - -// BeforeHtml parses the html and inserts it before the set of matched elements. -// This follows the same rules as Selection.After. -func (s *Selection) BeforeHtml(html string) *Selection { - return s.BeforeSelection(parseHtml(html)) -} - -// BeforeNodes inserts the nodes before each element in the set of matched elements. -// This follows the same rules as Selection.After. -func (s *Selection) BeforeNodes(ns ...*html.Node) *Selection { - return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) { - if sn.Parent != nil { - sn.Parent.InsertBefore(n, sn) - } - }) -} - -// Clone creates 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 -} - -// Empty removes all children nodes from the set of matched elements. -// Returns the children nodes in a new Selection. -func (s *Selection) Empty() *Selection { - var nodes []*html.Node - - for _, n := range s.Nodes { - for c := n.FirstChild; c != nil; c = n.FirstChild { - n.RemoveChild(c) - nodes = append(nodes, c) - } - } - - return pushStack(s, nodes) -} - -// Remove removes the set of matched elements from the document. -// Returns the same selection, now consisting of nodes not in the document. -func (s *Selection) Remove() *Selection { - for _, n := range s.Nodes { - if n.Parent != nil { - n.Parent.RemoveChild(n) - } - } - - return s -} - -// RemoveFiltered removes the set of matched elements by selector. -// Returns the Selection of removed nodes. -func (s *Selection) RemoveFiltered(selector string) *Selection { - return s.RemoveMatcher(cascadia.MustCompile(selector)) -} - -// RemoveMatcher removes the set of matched elements. -// Returns the Selection of removed nodes. -func (s *Selection) RemoveMatcher(m Matcher) *Selection { - return s.FilterMatcher(m).Remove() -} diff --git a/property.go b/property.go index 779f1ae..b52b58c 100644 --- a/property.go +++ b/property.go @@ -32,7 +32,7 @@ func (s *Selection) RemoveAttr(attrName string) *Selection { // SetAttr sets the given attribute on each element in the set of matched elements. func (s *Selection) SetAttr(attrName string, val string) *Selection { for _, n := range s.Nodes { - if attr, ok := getAttribute(attrName, n); ok { + if attr := getAttributePtr(attrName, n); attr != nil { attr.Val = val } } @@ -83,18 +83,22 @@ func (s *Selection) Html() (ret string, e error) { } // AddClass adds the given class(es) to each element in the set of matched elements. +// Multiple class names can be specified, separated by a space. func (s *Selection) AddClass(class string) *Selection { - rclasses := getClassesSlice(class) + if class == "" { + return s + } + slClasses := getClassesSlice(class) for _, n := range s.Nodes { - classes, attr := getClassesAndAttr(n, true) - for _, rcl := range rclasses { - if strings.Index(classes, " "+rcl+" ") == -1 { - classes += rcl + " " + curClasses, attr := getClassesAndAttr(n, true) + for _, newClass := range slClasses { + if strings.Index(curClasses, " "+newClass+" ") == -1 { + curClasses += newClass + " " } } - setClasses(n, attr, classes) + setClasses(n, attr, curClasses) } return s @@ -114,40 +118,50 @@ func (s *Selection) HasClass(class string) bool { } // RemoveClass removes the given class(es) from each element in the set of matched elements. -func (s *Selection) RemoveClass(class string) *Selection { - rclasses := getClassesSlice(class) +// Multiple class names can be specified, separated by a space or via multiple arguments. +// If no class name is provided, all classes are removed. +func (s *Selection) RemoveClass(class ...string) *Selection { + var rclasses []string - for _, n := range s.Nodes { - classes, attr := getClassesAndAttr(n, true) - for _, rcl := range rclasses { - classes = strings.Replace(classes, rcl, "", -1) - } + classStr := strings.TrimSpace(strings.Join(class, " ")) + remove := classStr == "" - setClasses(n, attr, classes) + if !remove { + rclasses = getClassesSlice(classStr) } - return s -} - -// Remove all classes from each element in the set of matched elements. -func (s *Selection) RemoveClasses() *Selection { for _, n := range s.Nodes { - _, attr := getClassesAndAttr(n, false) - setClasses(n, attr, "") + if remove { + removeAttr(n, "class") + } else { + classes, attr := getClassesAndAttr(n, true) + for _, rcl := range rclasses { + classes = strings.Replace(classes, " "+rcl+" ", " ", -1) + } + + setClasses(n, attr, classes) + } } return s } // ToggleClass adds or removes the given class(es) for each element in the set of matched elements. -func (s *Selection) ToggleClass(class string) *Selection { - tcls := getClassesSlice(class) +// Multiple class names can be specified, separated by a space or via multiple arguments. +func (s *Selection) ToggleClass(class ...string) *Selection { + classStr := strings.TrimSpace(strings.Join(class, " ")) + + if classStr == "" { + return s + } + + tcls := getClassesSlice(classStr) for _, n := range s.Nodes { classes, attr := getClassesAndAttr(n, true) for _, tcl := range tcls { - if strings.Index(classes, tcl) != -1 { - classes = strings.Replace(classes, tcl, "", -1) + if strings.Index(classes, " "+tcl+" ") != -1 { + classes = strings.Replace(classes, " "+tcl+" ", " ", -1) } else { classes += tcl + " " } @@ -175,25 +189,22 @@ func getNodeText(node *html.Node) string { return "" } -func getAttribute(attrName string, n *html.Node) (attr *html.Attribute, exists bool) { +func getAttributePtr(attrName string, n *html.Node) *html.Attribute { if n == nil { - return + return nil } for i, a := range n.Attr { if a.Key == attrName { - attr = &n.Attr[i] - exists = true - return + return &n.Attr[i] } } - - return + return nil } // Private function to get the specified attribute's value from a node. func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) { - if a, ok := getAttribute(attrName, n); ok { + if a := getAttributePtr(attrName, n); a != nil { val = a.Val exists = true } @@ -204,13 +215,13 @@ func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) { // Applies only to element nodes if n.Type == html.ElementNode { - attr, _ = getAttribute("class", n) + attr = getAttributePtr("class", n) if attr == nil && create { n.Attr = append(n.Attr, html.Attribute{ Key: "class", Val: "", }) - attr, _ = getAttribute("class", n) + attr = &n.Attr[len(n.Attr)-1] } } @@ -239,10 +250,10 @@ func removeAttr(n *html.Node, attrName string) { func setClasses(n *html.Node, attr *html.Attribute, classes string) { classes = strings.TrimSpace(classes) - if classes == "" { removeAttr(n, "class") - } else { - attr.Val = classes + return } + + attr.Val = classes } diff --git a/property_test.go b/property_test.go index 2f30dfc..0d12916 100644 --- a/property_test.go +++ b/property_test.go @@ -120,6 +120,15 @@ func TestAddClass(t *testing.T) { } } +func TestAddClassSimilar(t *testing.T) { + sel := Doc2Clone().Find("#nf5") + sel.AddClass("odd") + + assertClass(t, sel, "odd") + assertClass(t, sel, "odder") + printSel(t, sel.Parent()) +} + func TestAddEmptyClass(t *testing.T) { sel := Doc2Clone().Find("#main") sel.AddClass("") @@ -171,16 +180,25 @@ func TestRemoveClass(t *testing.T) { } } +func TestRemoveClassSimilar(t *testing.T) { + sel := Doc2Clone().Find("#nf5, #nf6") + assertLength(t, sel.Nodes, 2) + + sel.RemoveClass("odd") + assertClass(t, sel.Eq(0), "odder") + printSel(t, sel) +} + func TestRemoveAllClasses(t *testing.T) { sel := Doc2Clone().Find("#nf1") - sel.RemoveClasses() + sel.RemoveClass() if a, ok := sel.Attr("class"); ok { t.Error("All classes were not removed, has ", a) } sel = Doc2Clone().Find("#main") - sel.RemoveClasses() + sel.RemoveClass() if a, ok := sel.Attr("class"); ok { t.Error("All classes were not removed, has ", a) } @@ -201,6 +219,6 @@ func TestToggleClass(t *testing.T) { sel.ToggleClass("one even row") if a, ok := sel.Attr("class"); ok { - t.Error("Expected #nf1 to have no classes, have ", a) + t.Errorf("Expected #nf1 to have no classes, have %q", a) } } diff --git a/testdata/page2.html b/testdata/page2.html index 249209b..2136963 100644 --- a/testdata/page2.html +++ b/testdata/page2.html @@ -17,7 +17,7 @@
- +