Upstream PuerkitoBio/goquery state of the touched files before 0f91a3810f
This commit is contained in:
commit
445cd7eabe
2 files changed
+518
No files matched your search
+266
@@ -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
|
||||
}
|
||||
@@ -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*<a href="/">Provok<span class="green">\.</span><span class="red">i</span>n</a>\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 := `<p>Some text</p>`
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user