Merge pull request #612 from sergioperezcheco/fix/class-ascii-token-boundaries

Fix class helpers splitting non-ASCII whitespace class names
This commit is contained in:
Martin Angers
2026-10-05 10:17:27 -04:00
committed by GitHub
2 changed files with 83 additions and 5 deletions
+8 -5
View File
@@ -108,7 +108,7 @@ 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 or via multiple arguments.
func (s *Selection) AddClass(class ...string) *Selection {
classStr := strings.TrimSpace(strings.Join(class, " "))
classStr := strings.Trim(strings.Join(class, " "), " \t\r\n\f")
if classStr == "" {
return s
@@ -157,7 +157,7 @@ func (s *Selection) HasClass(class string) bool {
func (s *Selection) RemoveClass(class ...string) *Selection {
var rclasses []string
classStr := strings.TrimSpace(strings.Join(class, " "))
classStr := strings.Trim(strings.Join(class, " "), " \t\r\n\f")
remove := classStr == ""
if !remove {
@@ -183,7 +183,7 @@ func (s *Selection) RemoveClass(class ...string) *Selection {
// ToggleClass adds or removes the given class(es) for each element in the set of matched elements.
// 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, " "))
classStr := strings.Trim(strings.Join(class, " "), " \t\r\n\f")
if classStr == "" {
return s
@@ -258,7 +258,10 @@ func getClassesAndAttr(n *html.Node) (classes string, attr *html.Attribute) {
}
func getClassesSlice(classes string) []string {
return strings.Fields(classes)
// HTML class tokens are separated by ASCII whitespace, not Unicode whitespace.
return strings.FieldsFunc(classes, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\r' || r == '\n' || r == '\f'
})
}
func removeAttr(n *html.Node, attrName string) {
@@ -272,7 +275,7 @@ func removeAttr(n *html.Node, attrName string) {
}
func setClasses(n *html.Node, attr *html.Attribute, classes string) {
classes = strings.TrimSpace(classes)
classes = strings.Trim(classes, " \t\r\n\f")
if classes == "" {
removeAttr(n, "class")
return
+75
View File
@@ -0,0 +1,75 @@
package goquery
import (
"strings"
"testing"
)
func TestClassOperationsPreserveNonASCIIWhitespace(t *testing.T) {
for _, whitespace := range []string{"\u00a0", "\u2003", "\u202f", "\v"} {
for _, class := range []string{"a" + whitespace + "b", whitespace + "a", "a" + whitespace, whitespace} {
for _, op := range []struct {
name string
run func(*Selection, string) *Selection
want string
}{
{"add", func(s *Selection, c string) *Selection { return s.AddClass(c) }, class + " keep"},
{"remove", func(s *Selection, c string) *Selection { return s.RemoveClass(c) }, "keep"},
{"toggle", func(s *Selection, c string) *Selection { return s.ToggleClass(c) }, "keep"},
} {
t.Run(op.name+"/"+class, func(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader("<div></div>"))
if err != nil {
t.Fatal(err)
}
sel := doc.Find("div").SetAttr("class", class+" keep")
if !sel.HasClass(class) {
t.Fatal("existing class not recognized")
}
if op.run(sel, "\t"+class+"\r\n") != sel {
t.Fatal("lost chaining")
}
if got := sel.AttrOr("class", ""); got != op.want {
t.Fatalf("class = %q, want %q", got, op.want)
}
markup, err := OuterHtml(sel)
if err != nil {
t.Fatal(err)
}
reopened, err := NewDocumentFromReader(strings.NewReader(markup))
if err != nil {
t.Fatal(err)
}
if got := reopened.Find("div").AttrOr("class", ""); got != op.want {
t.Fatalf("round trip = %q, want %q", got, op.want)
}
})
}
}
}
}
func TestClassTokensASCIIWhitespace(t *testing.T) {
for _, sep := range []string{" ", "\t", "\r", "\n", "\f"} {
got := getClassesSlice(sep + "a\u00a0b" + sep + sep + "keep" + sep)
if len(got) != 2 || got[0] != "a\u00a0b" || got[1] != "keep" {
t.Errorf("separator %q: %q", sep, got)
}
}
}
func TestClassOperationsKeepUnicodeAtAttributeEdges(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader("<div></div>"))
if err != nil {
t.Fatal(err)
}
sel := doc.Find("div")
sel.AddClass("\u00a0a\u00a0").AddClass("keep").RemoveClass("keep")
if got := sel.AttrOr("class", ""); got != "\u00a0a\u00a0" {
t.Fatalf("class = %q", got)
}
sel.RemoveClass()
if _, exists := sel.Attr("class"); exists {
t.Fatal("RemoveClass() did not clear attribute")
}
}