commit 445cd7eabe85fcc51ddd79b3d9b531e10fd8958a Author: upstream Date: Fri Sep 4 07:52:26 2026 +0200 Upstream PuerkitoBio/goquery state of the touched files before 0f91a3810f diff --git a/property.go b/property.go new file mode 100644 index 0000000..bda736a --- /dev/null +++ b/property.go @@ -0,0 +1,266 @@ +package goquery + +import ( + "strings" + + "golang.org/x/net/html" +) + +var classTrimReplacer = strings.NewReplacer("\t", " ", "\r", " ", "\n", " ") + +// Attr gets the specified attribute's value for the first element in the +// Selection. To get the value for each element individually, use a looping +// construct such as Each or Map method. +func (s *Selection) Attr(attrName string) (string, bool) { + if len(s.Nodes) != 0 { + if attr := getAttributePtr(attrName, s.Nodes[0]); attr != nil { + return attr.Val, true + } + } + return "", false +} + +// AttrOr works like Attr but returns default value if attribute is not present. +func (s *Selection) AttrOr(attrName, defaultValue string) string { + if val, exists := s.Attr(attrName); exists { + return val + } + return defaultValue +} + +// RemoveAttr removes the named attribute from each element in the set of matched elements. +func (s *Selection) RemoveAttr(attrName string) *Selection { + for _, n := range s.Nodes { + removeAttr(n, attrName) + } + + return s +} + +// SetAttr sets the given attribute on each element in the set of matched elements. +func (s *Selection) SetAttr(attrName, val string) *Selection { + for _, n := range s.Nodes { + attr := getAttributePtr(attrName, n) + if attr == nil { + n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val}) + } else { + attr.Val = val + } + } + + return s +} + +// Text gets the combined text contents of each element in the set of matched +// elements, including their descendants. +func (s *Selection) Text() string { + var builder strings.Builder + for _, n := range s.Nodes { + s.textHelper(n, &builder) + } + + return builder.String() +} + +func (s *Selection) textHelper(n *html.Node, builder *strings.Builder) { + if n.Type == html.TextNode { + // Keep newlines and spaces, like jQuery + builder.WriteString(n.Data) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + s.textHelper(c, builder) + } +} + +// Size is an alias for Length. +func (s *Selection) Size() int { + return s.Length() +} + +// Length returns the number of elements in the Selection object. +func (s *Selection) Length() int { + return len(s.Nodes) +} + +// Html gets the HTML contents of the first element in the set of matched +// elements. It includes text and comment nodes. +func (s *Selection) Html() (ret string, e error) { + // Since there is no .innerHtml, the HTML content must be re-created from + // the nodes using html.Render. + var builder strings.Builder + + if len(s.Nodes) > 0 { + for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling { + e = html.Render(&builder, c) + if e != nil { + return + } + } + ret = builder.String() + } + + return +} + +// 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, " ")) + + if classStr == "" { + return s + } + + tcls := getClassesSlice(classStr) + for _, n := range s.Nodes { + curClasses, attr := getClassesAndAttr(n) + for _, newClass := range tcls { + if !strings.Contains(curClasses, " "+newClass+" ") { + curClasses += newClass + " " + } + } + + setClasses(n, attr, curClasses) + } + + return s +} + +// HasClass determines whether any of the matched elements are assigned the +// given class. +func (s *Selection) HasClass(class string) bool { + rawClass := class + class = " " + class + " " + for _, n := range s.Nodes { + if n.Type != html.ElementNode { + continue + } + if attr := getAttributePtr("class", n); attr != nil { + if !strings.Contains(attr.Val, rawClass) { + continue + } + val := classTrimReplacer.Replace(attr.Val) + if strings.Contains(" "+val+" ", class) { + return true + } + } + } + return false +} + +// RemoveClass removes the given class(es) from each element in the set of matched elements. +// 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 + + classStr := strings.TrimSpace(strings.Join(class, " ")) + remove := classStr == "" + + if !remove { + rclasses = getClassesSlice(classStr) + } + + for _, n := range s.Nodes { + if remove { + removeAttr(n, "class") + } else { + classes, attr := getClassesAndAttr(n) + for _, rcl := range rclasses { + classes = strings.ReplaceAll(classes, " "+rcl+" ", " ") + } + + setClasses(n, attr, classes) + } + } + + return s +} + +// 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, " ")) + + if classStr == "" { + return s + } + + tcls := getClassesSlice(classStr) + + 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, " ") + } else { + classes += tcl + " " + } + } + + setClasses(n, attr, classes) + } + + return s +} + +func getAttributePtr(attrName string, n *html.Node) *html.Attribute { + if n == nil { + return nil + } + + for i, a := range n.Attr { + if a.Key == attrName { + return &n.Attr[i] + } + } + return nil +} + +// Get and normalize the "class" attribute from the node. +func getClassesAndAttr(n *html.Node) (classes string, attr *html.Attribute) { + // Applies only to element nodes + if n.Type == html.ElementNode { + attr = getAttributePtr("class", n) + if attr == nil { + n.Attr = append(n.Attr, html.Attribute{ + Key: "class", + Val: "", + }) + attr = &n.Attr[len(n.Attr)-1] + } + } + + if attr == nil { + classes = " " + } else { + classes = classTrimReplacer.Replace(" " + attr.Val + " ") + } + + return +} + +func getClassesSlice(classes string) []string { + return strings.Fields(classes) +} + +func removeAttr(n *html.Node, attrName string) { + for i, a := range n.Attr { + if a.Key == attrName { + n.Attr[i], n.Attr[len(n.Attr)-1], n.Attr = + n.Attr[len(n.Attr)-1], html.Attribute{}, n.Attr[:len(n.Attr)-1] + return + } + } +} + +func setClasses(n *html.Node, attr *html.Attribute, classes string) { + classes = strings.TrimSpace(classes) + if classes == "" { + removeAttr(n, "class") + return + } + + attr.Val = classes +} diff --git a/property_test.go b/property_test.go new file mode 100644 index 0000000..1095dcc --- /dev/null +++ b/property_test.go @@ -0,0 +1,252 @@ +package goquery + +import ( + "regexp" + "strings" + "testing" +) + +func TestAttrExists(t *testing.T) { + if val, ok := Doc().Find("a").Attr("href"); !ok { + t.Error("Expected a value for the href attribute.") + } else { + t.Logf("Href of first anchor: %v.", val) + } +} + +func TestAttrOr(t *testing.T) { + if val := Doc().Find("a").AttrOr("fake-attribute", "alternative"); val != "alternative" { + t.Error("Expected an alternative value for 'fake-attribute' attribute.") + } else { + t.Logf("Value returned for not existing attribute: %v.", val) + } + if val := Doc().Find("zz").AttrOr("fake-attribute", "alternative"); val != "alternative" { + t.Error("Expected an alternative value for 'fake-attribute' on an empty selection.") + } else { + t.Logf("Value returned for empty selection: %v.", val) + } +} + +func TestAttrNotExist(t *testing.T) { + if val, ok := Doc().Find("div.row-fluid").Attr("href"); ok { + t.Errorf("Expected no value for the href attribute, got %v.", val) + } +} + +func TestRemoveAttr(t *testing.T) { + sel := Doc2Clone().Find("div") + + sel.RemoveAttr("id") + + _, ok := sel.Attr("id") + if ok { + t.Error("Expected there to be no id attributes set") + } +} + +func TestSetAttr(t *testing.T) { + sel := Doc2Clone().Find("#main") + + sel.SetAttr("id", "not-main") + + val, ok := sel.Attr("id") + if !ok { + t.Error("Expected an id attribute on main") + } + + if val != "not-main" { + t.Errorf("Expected an attribute id to be not-main, got %s", val) + } +} + +func TestSetAttr2(t *testing.T) { + sel := Doc2Clone().Find("#main") + + sel.SetAttr("foo", "bar") + + val, ok := sel.Attr("foo") + if !ok { + t.Error("Expected an 'foo' attribute on main") + } + + if val != "bar" { + t.Errorf("Expected an attribute 'foo' to be 'bar', got '%s'", val) + } +} + +func TestText(t *testing.T) { + txt := Doc().Find("h1").Text() + if strings.Trim(txt, " \n\r\t") != "Provok.in" { + t.Errorf("Expected text to be Provok.in, found %s.", txt) + } +} + +func TestText2(t *testing.T) { + txt := Doc().Find(".hero-unit .container-fluid .row-fluid:nth-child(1)").Text() + if ok, e := regexp.MatchString(`^\s+Provok\.in\s+Prove your point.\s+$`, txt); !ok || e != nil { + t.Errorf("Expected text to be Provok.in Prove your point., found %s.", txt) + if e != nil { + t.Logf("Error: %s.", e.Error()) + } + } +} + +func TestText3(t *testing.T) { + txt := Doc().Find(".pvk-gutter").First().Text() + // There's an   character in there... + if ok, e := regexp.MatchString(`^[\s\x{00A0}]+$`, txt); !ok || e != nil { + t.Errorf("Expected spaces, found <%v>.", txt) + if e != nil { + t.Logf("Error: %s.", e.Error()) + } + } +} + +func TestHtml(t *testing.T) { + txt, e := Doc().Find("h1").Html() + if e != nil { + t.Errorf("Error: %s.", e) + } + + if ok, e := regexp.MatchString(`^\s*Provok\.in\s*$`, txt); !ok || e != nil { + t.Errorf("Unexpected HTML content, found %s.", txt) + if e != nil { + t.Logf("Error: %s.", e.Error()) + } + } +} + +func TestNbsp(t *testing.T) { + src := `

Some text

` + d, err := NewDocumentFromReader(strings.NewReader(src)) + if err != nil { + t.Fatal(err) + } + txt := d.Find("p").Text() + ix := strings.Index(txt, "\u00a0") + if ix != 4 { + t.Errorf("Text: expected a non-breaking space at index 4, got %d", ix) + } + + h, err := d.Find("p").Html() + if err != nil { + t.Fatal(err) + } + ix = strings.Index(h, "\u00a0") + if ix != 4 { + t.Errorf("Html: expected a non-breaking space at index 4, got %d", ix) + } +} + +func TestAddClass(t *testing.T) { + sel := Doc2Clone().Find("#main") + sel.AddClass("main main main") + + // Make sure that class was only added once + if a, ok := sel.Attr("class"); !ok || a != "main" { + t.Error("Expected #main to have class main") + } +} + +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("") + + // Make sure that class was only added once + if a, ok := sel.Attr("class"); ok { + t.Errorf("Expected #main to not to have a class, have: %s", a) + } +} + +func TestAddClasses(t *testing.T) { + sel := Doc2Clone().Find("#main") + sel.AddClass("a b") + + // Make sure that class was only added once + if !sel.HasClass("a") || !sel.HasClass("b") { + t.Errorf("#main does not have classes") + } +} + +func TestHasClass(t *testing.T) { + sel := Doc().Find("div") + if !sel.HasClass("span12") { + t.Error("Expected at least one div to have class span12.") + } +} + +func TestHasClassNone(t *testing.T) { + sel := Doc().Find("h2") + if sel.HasClass("toto") { + t.Error("Expected h1 to have no class.") + } +} + +func TestHasClassNotFirst(t *testing.T) { + sel := Doc().Find(".alert") + if !sel.HasClass("alert-error") { + t.Error("Expected .alert to also have class .alert-error.") + } +} + +func TestRemoveClass(t *testing.T) { + sel := Doc2Clone().Find("#nf1") + sel.RemoveClass("one row") + + if !sel.HasClass("even") || sel.HasClass("one") || sel.HasClass("row") { + classes, _ := sel.Attr("class") + t.Error("Expected #nf1 to have class even, has ", classes) + } +} + +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.RemoveClass() + + if a, ok := sel.Attr("class"); ok { + t.Error("All classes were not removed, has ", a) + } + + sel = Doc2Clone().Find("#main") + sel.RemoveClass() + if a, ok := sel.Attr("class"); ok { + t.Error("All classes were not removed, has ", a) + } +} + +func TestToggleClass(t *testing.T) { + sel := Doc2Clone().Find("#nf1") + + sel.ToggleClass("one") + if sel.HasClass("one") { + t.Error("Expected #nf1 to not have class one") + } + + sel.ToggleClass("one") + if !sel.HasClass("one") { + t.Error("Expected #nf1 to have class one") + } + + sel.ToggleClass("one even row") + if a, ok := sel.Attr("class"); ok { + t.Errorf("Expected #nf1 to have no classes, have %q", a) + } +}