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