fix class manipulation issue

This commit is contained in:
Martin Angers
2014-11-07 11:22:08 -05:00
parent ac1e87b973
commit c0464bb4c9
4 changed files with 256 additions and 223 deletions
+184 -180
View File
@@ -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()
}
+50 -39
View File
@@ -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
}
+21 -3
View File
@@ -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)
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
<div id="nf2" class="two odd row"></div>
<div id="nf3" class="three even row"></div>
<div id="nf4" class="four odd row"></div>
<div id="nf5" class="five even row"></div>
<div id="nf5" class="five even row odder"></div>
<div id="nf6" class="six odd row"></div>
</div>
</body>