Compare commits

..
8 Commits
Author SHA1 Message Date
Martin AngersandGitHub 5ee0843df1 Merge pull request #604 from PuerkitoBio/dependabot/go_modules/github.com/andybalholm/cascadia-1.3.5
build(deps): bump github.com/andybalholm/cascadia from 1.3.4 to 1.3.5
2026-09-08 11:15:24 -04:00
dependabot[bot]andGitHub dd800704d1 build(deps): bump github.com/andybalholm/cascadia from 1.3.4 to 1.3.5
Bumps [github.com/andybalholm/cascadia](https://github.com/andybalholm/cascadia) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/andybalholm/cascadia/releases)
- [Commits](https://github.com/andybalholm/cascadia/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: github.com/andybalholm/cascadia
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 05:52:20 +00:00
Martin AngersandGitHub ef5f2ddf71 Merge pull request #600 from udf2457/manipulateNodes
Clone and reverse nodes slice for safe manipulation
2026-09-07 11:13:08 -04:00
Martin AngersandGitHub 3d22a4dd10 Merge pull request #603 from dualfroz/dualfroz/fix-removeclass-duplicates
RemoveClass leaves a class behind when the source repeats it
2026-09-07 10:29:11 -04:00
dualfroz b4ef68dec8 fix: remove every occurrence of a repeated class
RemoveClass deleted each class with one strings.ReplaceAll pass over the
space-wrapped class list. Two adjacent occurrences share the space
between them, so " a a " holds only one non-overlapping " a " and the
second survived: RemoveClass("a") on class="a a" returned with
HasClass("a") still true. ToggleClass used the same expression and so
toggled such a class the wrong way.

Replace one occurrence at a time, re-scanning from the start, in a helper
shared by both call sites. Whitespace between the remaining classes is
untouched.
2026-09-06 00:44:19 +02:00
Martin AngersandGitHub 738783cbc3 Merge pull request #597 from udf2457/master
Refactor IsMatcher to use slices.ContainsFunc
2026-09-04 11:11:08 -04:00
udf2457andGitHub a80a922806 Clone and reverse nodes slice for safe manipulation
Clone the slice before reversing to avoid modifying the caller's slice.
2026-09-04 16:08:58 +01:00
udf2457andGitHub 8cb3ddf6e3 Refactor IsMatcher to use slices.ContainsFunc 2026-09-04 15:38:40 +01:00
6 changed files with 90 additions and 18 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
module github.com/PuerkitoBio/goquery
require (
github.com/andybalholm/cascadia v1.3.4
github.com/andybalholm/cascadia v1.3.5
golang.org/x/net v0.58.0
)
+2 -2
View File
@@ -1,4 +1,4 @@
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
github.com/andybalholm/cascadia v1.3.5 h1:RLjq12WJy58dN6eCIQrz0bAGZkztHWsEPFxP53Y7Ms8=
github.com/andybalholm/cascadia v1.3.5/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
+5 -4
View File
@@ -1,6 +1,7 @@
package goquery
import (
"slices"
"strings"
"golang.org/x/net/html"
@@ -618,11 +619,11 @@ func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
// net.Html doesn't provide document fragments for insertion, so to get
// things in the correct order with After() and Prepend(), the callback
// needs to be called on the reverse of the nodes.
// needs to be called on the reverse of the nodes. Clone the slice before
// reversing so the caller's slice is not modified.
if reverse {
for i, j := 0, len(ns)-1; i < j; i, j = i+1, j-1 {
ns[i], ns[j] = ns[j], ns[i]
}
ns = slices.Clone(ns)
slices.Reverse(ns)
}
for i, sn := range s.Nodes {
+17 -4
View File
@@ -167,7 +167,7 @@ func (s *Selection) RemoveClass(class ...string) *Selection {
} else {
classes, attr := getClassesAndAttr(n)
for _, rcl := range rclasses {
classes = strings.ReplaceAll(classes, " "+rcl+" ", " ")
classes = removeClassFromClasses(classes, rcl)
}
setClasses(n, attr, classes)
@@ -191,9 +191,8 @@ func (s *Selection) ToggleClass(class ...string) *Selection {
for _, n := range s.Nodes {
classes, attr := getClassesAndAttr(n)
for _, tcl := range tcls {
spaceAroundTcl := " " + tcl + " "
if strings.Contains(classes, spaceAroundTcl) {
classes = strings.ReplaceAll(classes, spaceAroundTcl, " ")
if strings.Contains(classes, " "+tcl+" ") {
classes = removeClassFromClasses(classes, tcl)
} else {
classes += tcl + " "
}
@@ -205,6 +204,20 @@ func (s *Selection) ToggleClass(class ...string) *Selection {
return s
}
// removeClassFromClasses removes every occurrence of cl from the normalized
// class string. One ReplaceAll pass is not enough: adjacent occurrences
// share the space between them, so " a a " holds only one " a ".
func removeClassFromClasses(classes, cl string) string {
target := " " + cl + " "
for {
replaced := strings.Replace(classes, target, " ", 1)
if replaced == classes {
return classes
}
classes = replaced
}
}
func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
if n == nil {
return nil
+59
View File
@@ -250,3 +250,62 @@ func TestToggleClass(t *testing.T) {
t.Errorf("Expected #nf1 to have no classes, have %q", a)
}
}
func TestRemoveClassRepeatedInSource(t *testing.T) {
cases := []struct {
class string
remove string
expected string
}{
{"a a", "a", ""},
{"a a a", "a", ""},
{"a b a", "a", "b"},
{"a b a b", "a", "b b"},
{"a b a b", "a b", ""},
{"a a b", "a", "b"},
{"b a a", "a", "b"},
}
for _, c := range cases {
doc, err := NewDocumentFromReader(strings.NewReader(`<div id="t" class="` + c.class + `"></div>`))
if err != nil {
t.Fatal(err)
}
sel := doc.Find("#t")
sel.RemoveClass(c.remove)
got, _ := sel.Attr("class")
if got != c.expected {
t.Errorf("class=%q RemoveClass(%q): got class %q, want %q", c.class, c.remove, got, c.expected)
}
for _, removed := range strings.Fields(c.remove) {
if sel.HasClass(removed) {
t.Errorf("class=%q RemoveClass(%q): still has class %q", c.class, c.remove, removed)
}
}
}
}
func TestToggleClassRepeatedInSource(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(`<div id="t" class="a a b"></div>`))
if err != nil {
t.Fatal(err)
}
sel := doc.Find("#t")
sel.ToggleClass("a")
if sel.HasClass("a") {
t.Error("expected #t to not have class a after toggling it off")
}
if !sel.HasClass("b") {
t.Error("expected #t to keep class b")
}
sel.ToggleClass("a")
if !sel.HasClass("a") {
t.Error("expected #t to have class a after toggling it back on")
}
}
+6 -7
View File
@@ -1,6 +1,10 @@
package goquery
import "golang.org/x/net/html"
import (
"slices"
"golang.org/x/net/html"
)
// Is checks the current matched set of elements against a selector and
// returns true if at least one of these elements matches.
@@ -11,12 +15,7 @@ func (s *Selection) Is(selector string) bool {
// IsMatcher checks the current matched set of elements against a matcher and
// returns true if at least one of these elements matches.
func (s *Selection) IsMatcher(m Matcher) bool {
for _, n := range s.Nodes {
if m.Match(n) {
return true
}
}
return false
return slices.ContainsFunc(s.Nodes, m.Match)
}
// IsFunction checks the current matched set of elements against a predicate and