Files
goquery/property.go
2026-10-05 10:20:05 -04:00

288 lines
6.9 KiB
Go

package goquery
import (
"strings"
"golang.org/x/net/html"
)
var classTrimReplacer = strings.NewReplacer("\t", " ", "\r", " ", "\n", " ", "\f", " ")
// 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 {
if n.Type != html.ElementNode {
continue
}
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.Trim(strings.Join(class, " "), " \t\r\n\f")
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.Trim(strings.Join(class, " "), " \t\r\n\f")
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 = removeClassFromClasses(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.Trim(strings.Join(class, " "), " \t\r\n\f")
if classStr == "" {
return s
}
tcls := getClassesSlice(classStr)
for _, n := range s.Nodes {
classes, attr := getClassesAndAttr(n)
for _, tcl := range tcls {
if strings.Contains(classes, " "+tcl+" ") {
classes = removeClassFromClasses(classes, tcl)
} else {
classes += tcl + " "
}
}
setClasses(n, attr, classes)
}
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
}
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 {
// 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) {
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.Trim(classes, " \t\r\n\f")
if classes == "" {
removeAttr(n, "class")
return
}
if attr != nil {
attr.Val = classes
}
}