You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
6.5 KiB

  1. package goquery
  2. import (
  3. "bytes"
  4. "regexp"
  5. "strings"
  6. "golang.org/x/net/html"
  7. )
  8. var rxClassTrim = regexp.MustCompile("[\t\r\n]")
  9. // Attr gets the specified attribute's value for the first element in the
  10. // Selection. To get the value for each element individually, use a looping
  11. // construct such as Each or Map method.
  12. func (s *Selection) Attr(attrName string) (val string, exists bool) {
  13. if len(s.Nodes) == 0 {
  14. return
  15. }
  16. return getAttributeValue(attrName, s.Nodes[0])
  17. }
  18. // AttrOr works like Attr but returns default value if attribute is not present.
  19. func (s *Selection) AttrOr(attrName, defaultValue string) string {
  20. if len(s.Nodes) == 0 {
  21. return defaultValue
  22. }
  23. val, exists := getAttributeValue(attrName, s.Nodes[0])
  24. if !exists {
  25. return defaultValue
  26. }
  27. return val
  28. }
  29. // RemoveAttr removes the named attribute from each element in the set of matched elements.
  30. func (s *Selection) RemoveAttr(attrName string) *Selection {
  31. for _, n := range s.Nodes {
  32. removeAttr(n, attrName)
  33. }
  34. return s
  35. }
  36. // SetAttr sets the given attribute on each element in the set of matched elements.
  37. func (s *Selection) SetAttr(attrName, val string) *Selection {
  38. for _, n := range s.Nodes {
  39. attr := getAttributePtr(attrName, n)
  40. if attr == nil {
  41. n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val})
  42. } else {
  43. attr.Val = val
  44. }
  45. }
  46. return s
  47. }
  48. // Text gets the combined text contents of each element in the set of matched
  49. // elements, including their descendants.
  50. func (s *Selection) Text() string {
  51. var buf bytes.Buffer
  52. // Slightly optimized vs calling Each: no single selection object created
  53. for _, n := range s.Nodes {
  54. buf.WriteString(getNodeText(n))
  55. }
  56. return buf.String()
  57. }
  58. // Size is an alias for Length.
  59. func (s *Selection) Size() int {
  60. return s.Length()
  61. }
  62. // Length returns the number of elements in the Selection object.
  63. func (s *Selection) Length() int {
  64. return len(s.Nodes)
  65. }
  66. // Html gets the HTML contents of the first element in the set of matched
  67. // elements. It includes text and comment nodes.
  68. func (s *Selection) Html() (ret string, e error) {
  69. // Since there is no .innerHtml, the HTML content must be re-created from
  70. // the nodes using html.Render.
  71. var buf bytes.Buffer
  72. if len(s.Nodes) > 0 {
  73. for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
  74. e = html.Render(&buf, c)
  75. if e != nil {
  76. return
  77. }
  78. }
  79. ret = buf.String()
  80. }
  81. return
  82. }
  83. // AddClass adds the given class(es) to each element in the set of matched elements.
  84. // Multiple class names can be specified, separated by a space or via multiple arguments.
  85. func (s *Selection) AddClass(class ...string) *Selection {
  86. classStr := strings.TrimSpace(strings.Join(class, " "))
  87. if classStr == "" {
  88. return s
  89. }
  90. tcls := getClassesSlice(classStr)
  91. for _, n := range s.Nodes {
  92. curClasses, attr := getClassesAndAttr(n, true)
  93. for _, newClass := range tcls {
  94. if !strings.Contains(curClasses, " "+newClass+" ") {
  95. curClasses += newClass + " "
  96. }
  97. }
  98. setClasses(n, attr, curClasses)
  99. }
  100. return s
  101. }
  102. // HasClass determines whether any of the matched elements are assigned the
  103. // given class.
  104. func (s *Selection) HasClass(class string) bool {
  105. class = " " + class + " "
  106. for _, n := range s.Nodes {
  107. classes, _ := getClassesAndAttr(n, false)
  108. if strings.Contains(classes, class) {
  109. return true
  110. }
  111. }
  112. return false
  113. }
  114. // RemoveClass removes the given class(es) from each element in the set of matched elements.
  115. // Multiple class names can be specified, separated by a space or via multiple arguments.
  116. // If no class name is provided, all classes are removed.
  117. func (s *Selection) RemoveClass(class ...string) *Selection {
  118. var rclasses []string
  119. classStr := strings.TrimSpace(strings.Join(class, " "))
  120. remove := classStr == ""
  121. if !remove {
  122. rclasses = getClassesSlice(classStr)
  123. }
  124. for _, n := range s.Nodes {
  125. if remove {
  126. removeAttr(n, "class")
  127. } else {
  128. classes, attr := getClassesAndAttr(n, true)
  129. for _, rcl := range rclasses {
  130. classes = strings.Replace(classes, " "+rcl+" ", " ", -1)
  131. }
  132. setClasses(n, attr, classes)
  133. }
  134. }
  135. return s
  136. }
  137. // ToggleClass adds or removes the given class(es) for each element in the set of matched elements.
  138. // Multiple class names can be specified, separated by a space or via multiple arguments.
  139. func (s *Selection) ToggleClass(class ...string) *Selection {
  140. classStr := strings.TrimSpace(strings.Join(class, " "))
  141. if classStr == "" {
  142. return s
  143. }
  144. tcls := getClassesSlice(classStr)
  145. for _, n := range s.Nodes {
  146. classes, attr := getClassesAndAttr(n, true)
  147. for _, tcl := range tcls {
  148. if strings.Contains(classes, " "+tcl+" ") {
  149. classes = strings.Replace(classes, " "+tcl+" ", " ", -1)
  150. } else {
  151. classes += tcl + " "
  152. }
  153. }
  154. setClasses(n, attr, classes)
  155. }
  156. return s
  157. }
  158. // Get the specified node's text content.
  159. func getNodeText(node *html.Node) string {
  160. if node.Type == html.TextNode {
  161. // Keep newlines and spaces, like jQuery
  162. return node.Data
  163. } else if node.FirstChild != nil {
  164. var buf bytes.Buffer
  165. for c := node.FirstChild; c != nil; c = c.NextSibling {
  166. buf.WriteString(getNodeText(c))
  167. }
  168. return buf.String()
  169. }
  170. return ""
  171. }
  172. func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
  173. if n == nil {
  174. return nil
  175. }
  176. for i, a := range n.Attr {
  177. if a.Key == attrName {
  178. return &n.Attr[i]
  179. }
  180. }
  181. return nil
  182. }
  183. // Private function to get the specified attribute's value from a node.
  184. func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) {
  185. if a := getAttributePtr(attrName, n); a != nil {
  186. val = a.Val
  187. exists = true
  188. }
  189. return
  190. }
  191. // Get and normalize the "class" attribute from the node.
  192. func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
  193. // Applies only to element nodes
  194. if n.Type == html.ElementNode {
  195. attr = getAttributePtr("class", n)
  196. if attr == nil && create {
  197. n.Attr = append(n.Attr, html.Attribute{
  198. Key: "class",
  199. Val: "",
  200. })
  201. attr = &n.Attr[len(n.Attr)-1]
  202. }
  203. }
  204. if attr == nil {
  205. classes = " "
  206. } else {
  207. classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
  208. }
  209. return
  210. }
  211. func getClassesSlice(classes string) []string {
  212. return strings.Split(rxClassTrim.ReplaceAllString(" "+classes+" ", " "), " ")
  213. }
  214. func removeAttr(n *html.Node, attrName string) {
  215. for i, a := range n.Attr {
  216. if a.Key == attrName {
  217. n.Attr[i], n.Attr[len(n.Attr)-1], n.Attr =
  218. n.Attr[len(n.Attr)-1], html.Attribute{}, n.Attr[:len(n.Attr)-1]
  219. return
  220. }
  221. }
  222. }
  223. func setClasses(n *html.Node, attr *html.Attribute, classes string) {
  224. classes = strings.TrimSpace(classes)
  225. if classes == "" {
  226. removeAttr(n, "class")
  227. return
  228. }
  229. attr.Val = classes
  230. }