repo_description: A little like that j-thing, only in Go.
repo_topics: goquery html-parsing jquery selector-strings
code: package goquery
  
  import (
  	"regexp"
  	"strings"
  
  	"golang.org/x/net/html"
  )
  
  var rxClassTrim = regexp.MustCompile("[\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) (val string, exists bool) {
  	if len(s.Nodes) == 0 {
  		return
  	}
  	return getAttributeValue(attrName, s.Nodes[0])
  }
  
  // AttrOr works like Attr but returns default value if attribute is not present.
  func (s *Selection) AttrOr(attrName, defaultValue string) string {
  	if len(s.Nodes) == 0 {
  		return defaultValue
  	}
  
  	val, exists := getAttributeValue(attrName, s.Nodes[0])
  	if !exists {
  		return defaultValue
  	}
  
  	return val
  }
  
  // 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
  
  	// Slightly optimized vs calling Each: no single selection object created
  	var f func(*html.Node)
  	f = func(n *html.Node) {
  		if n.Type == html.TextNode {
  			// Keep newlines and spaces, like jQuery
  			builder.WriteString(n.Data)
  		}
  		if n.FirstChild != nil {
  			for c := n.FirstChild; c != nil; c = c.NextSibling {
  				f(c)
  			}
  		}
  	}
  	for _, n := range s.Nodes {
  		f(n)
  	}
  
  	return builder.String()
  }
  
  // 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, true)
  		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 {
  	class = " " + class + " "
  	for _, n := range s.Nodes {
  		classes, _ := getClassesAndAttr(n, false)
  		if strings.Contains(classes, 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, true)
  			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, true)
  		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
  }
  
  // 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 := getAttributePtr(attrName, n); a != nil {
  		val = a.Val
  		exists = true
  	}
  	return
  }
  
  // Get and normalize the "class" attribute from the node.
  func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
  	// Applies only to element nodes
  	if n.Type == html.ElementNode {
  		attr = getAttributePtr("class", n)
  		if attr == nil && create {
  			n.Attr = append(n.Attr, html.Attribute{
  				Key: "class",
  				Val: "",
  			})
  			attr = &n.Attr[len(n.Attr)-1]
  		}
  	}
  
  	if attr == nil {
  		classes = " "
  	} else {
  		classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
  	}
  
  	return
  }
  
  func getClassesSlice(classes string) []string {
  	return strings.Split(rxClassTrim.ReplaceAllString(" "+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
  }
  