mirror of
https://github.com/PuerkitoBio/goquery.git
synced 2024-04-21 12:31:36 +00:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f4291566c | ||
|
|
f0d75731e0 | ||
|
|
77cbaef46a | ||
|
|
2441483128 | ||
|
|
2ab590df05 | ||
|
|
9cc531274b | ||
|
|
2cd8b4e49a | ||
|
|
aeecb06b1e | ||
|
|
2e29ea41f0 | ||
|
|
5cc721859e | ||
|
|
4126223a86 | ||
|
|
81e6517758 | ||
|
|
bb16855614 | ||
|
|
b0437209ef | ||
|
|
417cce822c | ||
|
|
b4440419d8 | ||
|
|
60ec77b003 | ||
|
|
a8c8239eb6 | ||
|
|
c416dcdfae | ||
|
|
23904f6805 | ||
|
|
5203bc06da | ||
|
|
2d28dba982 | ||
|
|
269246d9e7 | ||
|
|
55f7879c98 | ||
|
|
d9b2b5ed51 | ||
|
|
4cf64c51f7 | ||
|
|
fe61b74344 | ||
|
|
edcb3e4b1e | ||
|
|
12a46cd4a1 | ||
|
|
51379dc225 | ||
|
|
0db8a55dc4 | ||
|
|
2922326512 | ||
|
|
a923db6a4b | ||
|
|
2455631714 | ||
|
|
162ad3a767 | ||
|
|
ca48f5f409 | ||
|
|
ffa8a68c9f | ||
|
|
39d99e8fa4 |
@@ -0,0 +1 @@
|
||||
testdata/* linguist-vendored
|
||||
@@ -4,4 +4,7 @@ go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2012-2014, Martin Angers & Contributors
|
||||
Copyright (c) 2012-2016, Martin Angers & Contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
# goquery - a little like that j-thing, only in Go
|
||||
|
||||
[](http://travis-ci.org/PuerkitoBio/goquery)
|
||||
|
||||
[](http://godoc.org/github.com/PuerkitoBio/goquery)
|
||||
# goquery - a little like that j-thing, only in Go [](http://travis-ci.org/PuerkitoBio/goquery) [](http://godoc.org/github.com/PuerkitoBio/goquery)
|
||||
|
||||
goquery brings a syntax and a set of features similar to [jQuery][] to the [Go language][go]. It is based on Go's [net/html package][html] and the CSS Selector library [cascadia][]. Since the net/html parser returns nodes, and not a full-featured DOM tree, jQuery's stateful manipulation functions (like height(), css(), detach()) have been left off.
|
||||
|
||||
@@ -30,6 +26,12 @@ Please note that because of the net/html dependency, goquery requires Go1.1+.
|
||||
|
||||
**Note that goquery's API is now stable, and will not break.**
|
||||
|
||||
* **2016-07-27 (v1.0.0)** : Tag version 1.0.0.
|
||||
* **2016-06-15** : Invalid selector strings internally compile to a `Matcher` implementation that never matches any node (instead of a panic). So for example, `doc.Find("~")` returns an empty `*Selection` object.
|
||||
* **2016-02-02** : Add `NodeName` utility function similar to the DOM's `nodeName` property. It returns the tag name of the first element in a selection, and other relevant values of non-element nodes (see godoc for details). Add `OuterHtml` utility function similar to the DOM's `outerHTML` property (named `OuterHtml` in small caps for consistency with the existing `Html` method on the `Selection`).
|
||||
* **2015-04-20** : Add `AttrOr` helper method to return the attribute's value or a default value if absent. Thanks to [piotrkowalczuk][piotr].
|
||||
* **2015-02-04** : Add more manipulation functions - Prepend* - thanks again to [Andrew Stone][thatguystone].
|
||||
* **2014-11-28** : Add more manipulation functions - ReplaceWith*, Wrap* and Unwrap - thanks again to [Andrew Stone][thatguystone].
|
||||
* **2014-11-07** : Add manipulation functions (thanks to [Andrew Stone][thatguystone]) and `*Matcher` functions, that receive compiled cascadia selectors instead of selector strings, thus avoiding potential panics thrown by goquery via `cascadia.MustCompile` calls. This results in better performance (selectors can be compiled once and reused) and more idiomatic error handling (you can handle cascadia's compilation errors, instead of recovering from panics, which had been bugging me for a long time). Note that the actual type expected is a `Matcher` interface, that `cascadia.Selector` implements. Other matcher implementations could be used.
|
||||
* **2014-11-06** : Change import paths of net/html to golang.org/x/net/html (see https://groups.google.com/forum/#!topic/golang-nuts/eD8dh3T9yyA). Make sure to update your code to use the new import path too when you call goquery with `html.Node`s.
|
||||
* **v0.3.2** : Add `NewDocumentFromReader()` (thanks jweir) which allows creating a goquery document from an io.Reader.
|
||||
@@ -53,15 +55,22 @@ jQuery often has many variants for the same function (no argument, a selector st
|
||||
* The signatures accepting a function as argument in jQuery are defined in goquery as `XxxFunction()` and take a function as argument (e.g.: `FilterFunction()`)
|
||||
* The goquery methods that can be called with a selector string have a corresponding version that take a `Matcher` interface and are defined as `XxxMatcher()` (e.g.: `IsMatcher()`)
|
||||
|
||||
Utility functions that are not in jQuery but are useful in Go are implemented as functions (that take a `*Selection` as parameter), to avoid a potential naming clash on the `*Selection`'s methods (reserved for jQuery-equivalent behaviour).
|
||||
|
||||
The complete [godoc reference documentation can be found here][doc].
|
||||
|
||||
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details.
|
||||
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string):
|
||||
|
||||
* `Find("~")` returns an empty selection because the selector string doesn't match anything.
|
||||
* `Add("~")` returns a new selection that holds the same nodes as the original selection, because it didn't add any node (selector string didn't match anything).
|
||||
* `ParentsFiltered("~")` returns an empty selection because the selector string doesn't match anything.
|
||||
* `ParentsUntil("~")` returns all parents of the selection because the selector string didn't match any element to stop before the top element.
|
||||
|
||||
## Examples
|
||||
|
||||
See some tips and tricks in the [wiki][].
|
||||
|
||||
Taken (and adapted as if executed from outside the goquery package) from example_test.go:
|
||||
Adapted from example_test.go:
|
||||
|
||||
```Go
|
||||
package main
|
||||
@@ -79,8 +88,10 @@ func ExampleScrape() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
doc.Find(".reviews-wrap article .review-rhs").Each(func(i int, s *goquery.Selection) {
|
||||
band := s.Find("h3").Text()
|
||||
// Find the review items
|
||||
doc.Find(".sidebar-reviews article .content-block").Each(func(i int, s *goquery.Selection) {
|
||||
// For each item found, get the band and title
|
||||
band := s.Find("a").Text()
|
||||
title := s.Find("i").Text()
|
||||
fmt.Printf("Review %d: %s - %s\n", i, band, title)
|
||||
})
|
||||
@@ -97,13 +108,14 @@ The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia'
|
||||
|
||||
[jquery]: http://jquery.com/
|
||||
[go]: http://golang.org/
|
||||
[cascadia]: http://code.google.com/p/cascadia/
|
||||
[cascadia]: https://github.com/andybalholm/cascadia
|
||||
[bsd]: http://opensource.org/licenses/BSD-3-Clause
|
||||
[golic]: http://golang.org/LICENSE
|
||||
[caslic]: http://code.google.com/p/cascadia/source/browse/LICENSE
|
||||
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
|
||||
[doc]: http://godoc.org/github.com/PuerkitoBio/goquery
|
||||
[index]: http://api.jquery.com/index/
|
||||
[gonet]: http://code.google.com/p/go/source/detail?r=f7f5159120f51ba0070774d3c5907969b5fe7858&repo=net
|
||||
[html]: http://godoc.org/code.google.com/p/go.net/html
|
||||
[gonet]: https://github.com/golang/net/
|
||||
[html]: http://godoc.org/golang.org/x/net/html
|
||||
[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
|
||||
[thatguystone]: https://github.com/thatguystone
|
||||
[piotr]: https://github.com/piotrkowalczuk
|
||||
|
||||
@@ -14,6 +14,11 @@ func TestFirstEmpty(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestFirstInvalid(t *testing.T) {
|
||||
sel := Doc().Find("").First()
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestFirstRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.First().End()
|
||||
@@ -36,6 +41,11 @@ func TestLastEmpty(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestLastInvalid(t *testing.T) {
|
||||
sel := Doc().Find("").Last()
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestLastRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.Last().End()
|
||||
@@ -63,6 +73,11 @@ func TestEqEmpty(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestEqInvalid(t *testing.T) {
|
||||
sel := Doc().Find("").Eq(0)
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestEqInvalidPositive(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").Eq(3)
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
@@ -85,6 +100,16 @@ func TestSlice(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestSliceEmpty(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find("x").Slice(0, 2)
|
||||
}
|
||||
|
||||
func TestSliceInvalid(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find("").Slice(0, 2)
|
||||
}
|
||||
|
||||
func TestSliceOutOfBounds(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find(".pvk-content").Slice(2, 12)
|
||||
@@ -157,6 +182,13 @@ func TestIndexSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexSelectorInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".hero-unit")
|
||||
if i := sel.IndexSelector(""); i != -1 {
|
||||
t.Errorf("Expected index of -1, got %v.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexOfNode(t *testing.T) {
|
||||
sel := Doc().Find("div.pvk-gutter")
|
||||
if i := sel.IndexOfNode(sel.Nodes[1]); i != 1 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2012-2014, Martin Angers & Contributors
|
||||
// Copyright (c) 2012-2016, Martin Angers & Contributors
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -35,7 +35,7 @@ Also, because the net/html parser requires UTF-8 encoding, so does goquery: it i
|
||||
the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML.
|
||||
See the repository's wiki for various options on how to do this.
|
||||
|
||||
Syntax-wise, it is as close as possible to jQuery, with the same function names when
|
||||
Syntax-wise, it is as close as possible to jQuery, with the same method names when
|
||||
possible, and that warm and fuzzy chainable interface. jQuery being the
|
||||
ultra-popular library that it is, writing a similar HTML-manipulating
|
||||
library was better to follow its API than to start anew (in the same spirit as
|
||||
@@ -81,10 +81,16 @@ The three dots (...) indicate that various "overloads" are available.
|
||||
- Before...()
|
||||
- Clone()
|
||||
- Empty()
|
||||
- Prepend...()
|
||||
- Remove...()
|
||||
- ReplaceWith...()
|
||||
- Unwrap()
|
||||
- Wrap...()
|
||||
- WrapAll...()
|
||||
- WrapInner...()
|
||||
|
||||
* property.go : methods that inspect and get the node's properties values.
|
||||
- Attr(), RemoveAttr(), SetAttr()
|
||||
- Attr*(), RemoveAttr(), SetAttr()
|
||||
- AddClass(), HasClass(), RemoveClass(), ToggleClass()
|
||||
- Html()
|
||||
- Length()
|
||||
@@ -108,5 +114,10 @@ The three dots (...) indicate that various "overloads" are available.
|
||||
- Document
|
||||
- Selection
|
||||
- Matcher
|
||||
|
||||
* utilities.go : definition of helper functions (and not methods on a *Selection)
|
||||
that are not part of jQuery, but are useful to goquery.
|
||||
- NodeName
|
||||
- OuterHtml
|
||||
*/
|
||||
package goquery
|
||||
|
||||
+8
-10
@@ -1,26 +1,24 @@
|
||||
package goquery
|
||||
package goquery_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
// In real use, this import would be required (not in this example, since it
|
||||
// is part of the goquery package)
|
||||
//"github.com/PuerkitoBio/goquery"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// This example scrapes the reviews shown on the home page of metalsucks.net.
|
||||
func ExampleScrape_MetalSucks() {
|
||||
// Load the HTML document (in real use, the type would be *goquery.Document)
|
||||
doc, err := NewDocument("http://metalsucks.net")
|
||||
func Example() {
|
||||
// Load the HTML document
|
||||
doc, err := goquery.NewDocument("http://metalsucks.net")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Find the review items (the type of the Selection would be *goquery.Selection)
|
||||
doc.Find(".reviews-wrap article .review-rhs").Each(func(i int, s *Selection) {
|
||||
// Find the review items
|
||||
doc.Find(".sidebar-reviews article .content-block").Each(func(i int, s *goquery.Selection) {
|
||||
// For each item found, get the band and title
|
||||
band := s.Find("h3").Text()
|
||||
band := s.Find("a").Text()
|
||||
title := s.Find("i").Text()
|
||||
fmt.Printf("Review %d: %s - %s\n", i, band, title)
|
||||
})
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"code.google.com/p/cascadia"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
import "golang.org/x/net/html"
|
||||
|
||||
// Add adds the selector string's matching nodes to those in the current
|
||||
// selection and returns a new Selection object.
|
||||
// The selector string is run in the context of the document of the current
|
||||
// Selection object.
|
||||
func (s *Selection) Add(selector string) *Selection {
|
||||
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, cascadia.MustCompile(selector))...)
|
||||
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, compileMatcher(selector))...)
|
||||
}
|
||||
|
||||
// AddMatcher adds the matcher's matching nodes to those in the current
|
||||
|
||||
@@ -9,6 +9,16 @@ func TestAdd(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 19)
|
||||
}
|
||||
|
||||
func TestAddInvalid(t *testing.T) {
|
||||
sel1 := Doc().Find("div.row-fluid")
|
||||
sel2 := sel1.Add("")
|
||||
assertLength(t, sel1.Nodes, 9)
|
||||
assertLength(t, sel2.Nodes, 9)
|
||||
if sel1 == sel2 {
|
||||
t.Errorf("selections should not be the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.Add("a").End()
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"code.google.com/p/cascadia"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
import "golang.org/x/net/html"
|
||||
|
||||
// Filter reduces the set of matched elements to those that match the selector string.
|
||||
// It returns a new Selection object for this subset of matching elements.
|
||||
func (s *Selection) Filter(selector string) *Selection {
|
||||
return s.FilterMatcher(cascadia.MustCompile(selector))
|
||||
return s.FilterMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// FilterMatcher reduces the set of matched elements to those that match
|
||||
// the given matcher.
|
||||
// It returns a new Selection object for this subset of matching elements.
|
||||
// the given matcher. It returns a new Selection object for this subset
|
||||
// of matching elements.
|
||||
func (s *Selection) FilterMatcher(m Matcher) *Selection {
|
||||
return pushStack(s, winnow(s, m, true))
|
||||
}
|
||||
@@ -21,7 +18,7 @@ func (s *Selection) FilterMatcher(m Matcher) *Selection {
|
||||
// Not removes elements from the Selection that match the selector string.
|
||||
// It returns a new Selection object with the matching elements removed.
|
||||
func (s *Selection) Not(selector string) *Selection {
|
||||
return s.NotMatcher(cascadia.MustCompile(selector))
|
||||
return s.NotMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// NotMatcher removes elements from the Selection that match the given matcher.
|
||||
|
||||
@@ -14,6 +14,11 @@ func TestFilterNone(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestFilterInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".span12").Filter("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestFilterRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.Filter(".alert").End()
|
||||
@@ -74,6 +79,11 @@ func TestNot(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
}
|
||||
|
||||
func TestNotInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".span12").Not("")
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestNotRollback(t *testing.T) {
|
||||
sel := Doc().Find(".span12")
|
||||
sel2 := sel.Not(".alert").End()
|
||||
@@ -145,6 +155,11 @@ func TestHas(t *testing.T) {
|
||||
// Has() returns the high-level .container-fluid div, and the one that is the immediate parent of center-content
|
||||
}
|
||||
|
||||
func TestHasInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").Has("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestHasRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.Has(".center-content").End()
|
||||
|
||||
+8
-2
@@ -1,7 +1,10 @@
|
||||
package goquery
|
||||
|
||||
// Each iterates over a Selection object, executing a function for each
|
||||
// matched element. It returns the current Selection object.
|
||||
// matched element. It returns the current Selection object. The function
|
||||
// f is called for each element in the selection with the index of the
|
||||
// element in that selection starting at 0, and a *Selection that contains
|
||||
// only that element.
|
||||
func (s *Selection) Each(f func(int, *Selection)) *Selection {
|
||||
for i, n := range s.Nodes {
|
||||
f(i, newSingleSelection(n, s.document))
|
||||
@@ -23,7 +26,10 @@ func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
|
||||
}
|
||||
|
||||
// Map passes each element in the current matched set through a function,
|
||||
// producing a slice of string holding the returned values.
|
||||
// producing a slice of string holding the returned values. The function
|
||||
// f is called for each element in the selection with the index of the
|
||||
// element in that selection starting at 0, and a *Selection that contains
|
||||
// only that element.
|
||||
func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
|
||||
for i, n := range s.Nodes {
|
||||
result = append(result, f(i, newSingleSelection(n, s.document)))
|
||||
|
||||
+305
-12
@@ -1,10 +1,8 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/cascadia"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@@ -14,9 +12,9 @@ import (
|
||||
// If one of the matched elements in the selection is not currently in the
|
||||
// document, it's impossible to insert nodes after it, so it will be ignored.
|
||||
//
|
||||
// This follows the same rules outlined in Selection.Append.
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) After(selector string) *Selection {
|
||||
return s.AfterMatcher(cascadia.MustCompile(selector))
|
||||
return s.AfterMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// AfterMatcher applies the matcher from the root document and inserts the matched elements
|
||||
@@ -25,7 +23,7 @@ func (s *Selection) After(selector string) *Selection {
|
||||
// If one of the matched elements in the selection is not currently in the
|
||||
// document, it's impossible to insert nodes after it, so it will be ignored.
|
||||
//
|
||||
// This follows the same rules outlined in Selection.Append.
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) AfterMatcher(m Matcher) *Selection {
|
||||
return s.AfterNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
@@ -33,21 +31,21 @@ func (s *Selection) AfterMatcher(m Matcher) *Selection {
|
||||
// AfterSelection inserts the elements in the selection after each element in the set of matched
|
||||
// elements.
|
||||
//
|
||||
// This follows the same rules outlined in Selection.Append.
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) AfterSelection(sel *Selection) *Selection {
|
||||
return s.AfterNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// AfterHtml parses the html and inserts it after the set of matched elements.
|
||||
//
|
||||
// This follows the same rules outlined in Selection.Append.
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) AfterHtml(html string) *Selection {
|
||||
return s.AfterNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
// AfterNodes inserts the nodes after each element in the set of matched elements.
|
||||
//
|
||||
// This follows the same rules outlined in Selection.Append.
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) AfterNodes(ns ...*html.Node) *Selection {
|
||||
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
|
||||
if sn.Parent != nil {
|
||||
@@ -67,7 +65,7 @@ func (s *Selection) AfterNodes(ns ...*html.Node) *Selection {
|
||||
// appended to all target locations except the last one, which will be moved
|
||||
// as noted in (2).
|
||||
func (s *Selection) Append(selector string) *Selection {
|
||||
return s.AppendMatcher(cascadia.MustCompile(selector))
|
||||
return s.AppendMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// AppendMatcher appends the elements specified by the matcher to the end of each element
|
||||
@@ -104,7 +102,7 @@ func (s *Selection) AppendNodes(ns ...*html.Node) *Selection {
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) Before(selector string) *Selection {
|
||||
return s.BeforeMatcher(cascadia.MustCompile(selector))
|
||||
return s.BeforeMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// BeforeMatcher inserts the matched elements before each element in the set of matched elements.
|
||||
@@ -163,6 +161,45 @@ func (s *Selection) Empty() *Selection {
|
||||
return pushStack(s, nodes)
|
||||
}
|
||||
|
||||
// Prepend prepends the elements specified by the selector to each element in
|
||||
// the set of matched elements, following the same rules as Append.
|
||||
func (s *Selection) Prepend(selector string) *Selection {
|
||||
return s.PrependMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// PrependMatcher prepends the elements specified by the matcher to each
|
||||
// element in the set of matched elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) PrependMatcher(m Matcher) *Selection {
|
||||
return s.PrependNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
|
||||
// PrependSelection prepends the elements in the selection to each element in
|
||||
// the set of matched elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) PrependSelection(sel *Selection) *Selection {
|
||||
return s.PrependNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// PrependHtml parses the html and prepends it to the set of matched elements.
|
||||
func (s *Selection) PrependHtml(html string) *Selection {
|
||||
return s.PrependNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
// PrependNodes prepends the specified nodes to each node in the set of
|
||||
// matched elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) PrependNodes(ns ...*html.Node) *Selection {
|
||||
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
|
||||
// sn.FirstChild may be nil, in which case this functions like
|
||||
// sn.AppendChild()
|
||||
sn.InsertBefore(n, sn.FirstChild)
|
||||
})
|
||||
}
|
||||
|
||||
// Remove removes the set of matched elements from the document.
|
||||
// It returns the same selection, now consisting of nodes not in the document.
|
||||
func (s *Selection) Remove() *Selection {
|
||||
@@ -178,7 +215,7 @@ func (s *Selection) Remove() *Selection {
|
||||
// RemoveFiltered removes the set of matched elements by selector.
|
||||
// It returns the Selection of removed nodes.
|
||||
func (s *Selection) RemoveFiltered(selector string) *Selection {
|
||||
return s.RemoveMatcher(cascadia.MustCompile(selector))
|
||||
return s.RemoveMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// RemoveMatcher removes the set of matched elements.
|
||||
@@ -187,16 +224,272 @@ func (s *Selection) RemoveMatcher(m Matcher) *Selection {
|
||||
return s.FilterMatcher(m).Remove()
|
||||
}
|
||||
|
||||
// ReplaceWith replaces each element in the set of matched elements with the
|
||||
// nodes matched by the given selector.
|
||||
// It returns the removed elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) ReplaceWith(selector string) *Selection {
|
||||
return s.ReplaceWithMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// ReplaceWithMatcher replaces each element in the set of matched elements with
|
||||
// the nodes matched by the given Matcher.
|
||||
// It returns the removed elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) ReplaceWithMatcher(m Matcher) *Selection {
|
||||
return s.ReplaceWithNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
|
||||
// ReplaceWithSelection replaces each element in the set of matched elements with
|
||||
// the nodes from the given Selection.
|
||||
// It returns the removed elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
|
||||
return s.ReplaceWithNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// ReplaceWithHtml replaces each element in the set of matched elements with
|
||||
// the parsed HTML.
|
||||
// It returns the removed elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) ReplaceWithHtml(html string) *Selection {
|
||||
return s.ReplaceWithNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
// ReplaceWithNodes replaces each element in the set of matched elements with
|
||||
// the given nodes.
|
||||
// It returns the removed elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
|
||||
s.AfterNodes(ns...)
|
||||
return s.Remove()
|
||||
}
|
||||
|
||||
// Unwrap removes the parents of the set of matched elements, leaving the matched
|
||||
// elements (and their siblings, if any) in their place.
|
||||
// It returns the original selection.
|
||||
func (s *Selection) Unwrap() *Selection {
|
||||
s.Parent().Each(func(i int, ss *Selection) {
|
||||
// For some reason, jquery allows unwrap to remove the <head> element, so
|
||||
// allowing it here too. Same for <html>. Why it allows those elements to
|
||||
// be unwrapped while not allowing body is a mystery to me.
|
||||
if ss.Nodes[0].Data != "body" {
|
||||
ss.ReplaceWithSelection(ss.Contents())
|
||||
}
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Wrap wraps each element in the set of matched elements inside the first
|
||||
// element matched by the given selector. The matched child is cloned before
|
||||
// being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) Wrap(selector string) *Selection {
|
||||
return s.WrapMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// WrapMatcher wraps each element in the set of matched elements inside the
|
||||
// first element matched by the given matcher. The matched child is cloned
|
||||
// before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapMatcher(m Matcher) *Selection {
|
||||
return s.wrapNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
|
||||
// WrapSelection wraps each element in the set of matched elements inside the
|
||||
// first element in the given Selection. The element is cloned before being
|
||||
// inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapSelection(sel *Selection) *Selection {
|
||||
return s.wrapNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// WrapHtml wraps each element in the set of matched elements inside the inner-
|
||||
// most child of the given HTML.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapHtml(html string) *Selection {
|
||||
return s.wrapNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
// WrapNode wraps each element in the set of matched elements inside the inner-
|
||||
// most child of the given node. The given node is copied before being inserted
|
||||
// into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapNode(n *html.Node) *Selection {
|
||||
return s.wrapNodes(n)
|
||||
}
|
||||
|
||||
func (s *Selection) wrapNodes(ns ...*html.Node) *Selection {
|
||||
s.Each(func(i int, ss *Selection) {
|
||||
ss.wrapAllNodes(ns...)
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// WrapAll wraps a single HTML structure, matched by the given selector, around
|
||||
// all elements in the set of matched elements. The matched child is cloned
|
||||
// before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAll(selector string) *Selection {
|
||||
return s.WrapAllMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// WrapAllMatcher wraps a single HTML structure, matched by the given Matcher,
|
||||
// around all elements in the set of matched elements. The matched child is
|
||||
// cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAllMatcher(m Matcher) *Selection {
|
||||
return s.wrapAllNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
|
||||
// WrapAllSelection wraps a single HTML structure, the first node of the given
|
||||
// Selection, around all elements in the set of matched elements. The matched
|
||||
// child is cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
|
||||
return s.wrapAllNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// WrapAllHtml wraps the given HTML structure around all elements in the set of
|
||||
// matched elements. The matched child is cloned before being inserted into the
|
||||
// document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAllHtml(html string) *Selection {
|
||||
return s.wrapAllNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
|
||||
if len(ns) > 0 {
|
||||
return s.WrapAllNode(ns[0])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// WrapAllNode wraps the given node around the first element in the Selection,
|
||||
// making all other nodes in the Selection children of the given node. The node
|
||||
// is cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAllNode(n *html.Node) *Selection {
|
||||
if s.Size() == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
wrap := cloneNode(n)
|
||||
|
||||
first := s.Nodes[0]
|
||||
if first.Parent != nil {
|
||||
first.Parent.InsertBefore(wrap, first)
|
||||
first.Parent.RemoveChild(first)
|
||||
}
|
||||
|
||||
for c := getFirstChildEl(wrap); c != nil; c = getFirstChildEl(wrap) {
|
||||
wrap = c
|
||||
}
|
||||
|
||||
newSingleSelection(wrap, s.document).AppendSelection(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// WrapInner wraps an HTML structure, matched by the given selector, around the
|
||||
// content of element in the set of matched elements. The matched child is
|
||||
// cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapInner(selector string) *Selection {
|
||||
return s.WrapInnerMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// WrapInnerMatcher wraps an HTML structure, matched by the given selector,
|
||||
// around the content of element in the set of matched elements. The matched
|
||||
// child is cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapInnerMatcher(m Matcher) *Selection {
|
||||
return s.wrapInnerNodes(m.MatchAll(s.document.rootNode)...)
|
||||
}
|
||||
|
||||
// WrapInnerSelection wraps an HTML structure, matched by the given selector,
|
||||
// around the content of element in the set of matched elements. The matched
|
||||
// child is cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
|
||||
return s.wrapInnerNodes(sel.Nodes...)
|
||||
}
|
||||
|
||||
// WrapInnerHtml wraps an HTML structure, matched by the given selector, around
|
||||
// the content of element in the set of matched elements. The matched child is
|
||||
// cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapInnerHtml(html string) *Selection {
|
||||
return s.wrapInnerNodes(parseHtml(html)...)
|
||||
}
|
||||
|
||||
// WrapInnerNode wraps an HTML structure, matched by the given selector, around
|
||||
// the content of element in the set of matched elements. The matched child is
|
||||
// cloned before being inserted into the document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapInnerNode(n *html.Node) *Selection {
|
||||
return s.wrapInnerNodes(n)
|
||||
}
|
||||
|
||||
func (s *Selection) wrapInnerNodes(ns ...*html.Node) *Selection {
|
||||
if len(ns) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
s.Each(func(i int, s *Selection) {
|
||||
contents := s.Contents()
|
||||
|
||||
if contents.Size() > 0 {
|
||||
contents.wrapAllNodes(ns...)
|
||||
} else {
|
||||
s.AppendNodes(cloneNode(ns[0]))
|
||||
}
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func parseHtml(h string) []*html.Node {
|
||||
// Errors are only returned when the io.Reader returns any error besides
|
||||
// EOF, but strings.Reader never will
|
||||
nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("goquery: failed to parse HTML: %s", err))
|
||||
panic("goquery: failed to parse HTML: " + err.Error())
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Get the first child that is an ElementNode
|
||||
func getFirstChildEl(n *html.Node) *html.Node {
|
||||
c := n.FirstChild
|
||||
for c != nil && c.Type != html.ElementNode {
|
||||
c = c.NextSibling
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Deep copy a slice of nodes.
|
||||
func cloneNodes(ns []*html.Node) []*html.Node {
|
||||
cns := make([]*html.Node, 0, len(ns))
|
||||
|
||||
@@ -4,6 +4,10 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
wrapHtml = "<div id=\"ins\">test string<div><p><em><b></b></em></p></div></div>"
|
||||
)
|
||||
|
||||
func TestAfter(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").After("#nf6")
|
||||
@@ -156,6 +160,64 @@ func TestEmpty(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrepend(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").Prepend("#nf6")
|
||||
|
||||
assertLength(t, doc.Find("#foot #nf6").Nodes, 0)
|
||||
assertLength(t, doc.Find("#main #nf6:first-child").Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependBody(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("body").Prepend("#nf6")
|
||||
|
||||
assertLength(t, doc.Find("#foot #nf6").Nodes, 0)
|
||||
assertLength(t, doc.Find("#main #nf6").Nodes, 0)
|
||||
assertLength(t, doc.Find("body > #nf6:first-child").Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependSelection(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").PrependSelection(doc.Find("#nf1, #nf2"))
|
||||
|
||||
assertLength(t, doc.Find("#foot #nf1").Nodes, 0)
|
||||
assertLength(t, doc.Find("#foot #nf2").Nodes, 0)
|
||||
assertLength(t, doc.Find("#main #nf1:first-child").Nodes, 1)
|
||||
assertLength(t, doc.Find("#main #nf2:nth-child(2)").Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependSelectionExisting(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").PrependSelection(doc.Find("#n5, #n6"))
|
||||
|
||||
assertClass(t, doc.Find("#main :nth-child(1)"), "five")
|
||||
assertClass(t, doc.Find("#main :nth-child(2)"), "six")
|
||||
assertClass(t, doc.Find("#main :nth-child(5)"), "three")
|
||||
assertClass(t, doc.Find("#main :nth-child(6)"), "four")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependClone(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#n1").PrependSelection(doc.Find("#nf1").Clone())
|
||||
|
||||
assertLength(t, doc.Find("#foot #nf1:first-child").Nodes, 1)
|
||||
assertLength(t, doc.Find("#main #nf1:first-child").Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("div").PrependHtml("<strong>new node</strong>")
|
||||
|
||||
assertLength(t, doc.Find("strong:first-child").Nodes, 14)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#nf1").Remove()
|
||||
@@ -192,3 +254,200 @@ func TestRemoveFiltered(t *testing.T) {
|
||||
}
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestReplaceWith(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
|
||||
doc.Find("#nf6").ReplaceWith("#main")
|
||||
assertLength(t, doc.Find("#foot #main:last-child").Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
|
||||
doc.Find("#foot").ReplaceWith("#main")
|
||||
assertLength(t, doc.Find("#foot").Nodes, 0)
|
||||
assertLength(t, doc.Find("#main").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestReplaceWithHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main, #foot").ReplaceWithHtml("<div id=\"replace\"></div>")
|
||||
|
||||
assertLength(t, doc.Find("#replace").Nodes, 2)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestReplaceWithSelection(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
sel := doc.Find("#nf6").ReplaceWithSelection(doc.Find("#nf5"))
|
||||
|
||||
assertSelectionIs(t, sel, "#nf6")
|
||||
assertLength(t, doc.Find("#nf6").Nodes, 0)
|
||||
assertLength(t, doc.Find("#nf5").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestUnwrap(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
|
||||
doc.Find("#nf5").Unwrap()
|
||||
assertLength(t, doc.Find("#foot").Nodes, 0)
|
||||
assertLength(t, doc.Find("body > #nf1").Nodes, 1)
|
||||
assertLength(t, doc.Find("body > #nf5").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
|
||||
doc = Doc2Clone()
|
||||
|
||||
doc.Find("#nf5, #n1").Unwrap()
|
||||
assertLength(t, doc.Find("#foot").Nodes, 0)
|
||||
assertLength(t, doc.Find("#main").Nodes, 0)
|
||||
assertLength(t, doc.Find("body > #n1").Nodes, 1)
|
||||
assertLength(t, doc.Find("body > #nf5").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestUnwrapBody(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
|
||||
doc.Find("#main").Unwrap()
|
||||
assertLength(t, doc.Find("body").Nodes, 1)
|
||||
assertLength(t, doc.Find("body > #main").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestUnwrapHead(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
|
||||
doc.Find("title").Unwrap()
|
||||
assertLength(t, doc.Find("head").Nodes, 0)
|
||||
assertLength(t, doc.Find("head > title").Nodes, 0)
|
||||
assertLength(t, doc.Find("title").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestUnwrapHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
|
||||
doc.Find("head").Unwrap()
|
||||
assertLength(t, doc.Find("html").Nodes, 0)
|
||||
assertLength(t, doc.Find("html head").Nodes, 0)
|
||||
assertLength(t, doc.Find("head").Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#nf1").Wrap("#nf2")
|
||||
nf1 := doc.Find("#foot #nf2 #nf1")
|
||||
assertLength(t, nf1.Nodes, 1)
|
||||
|
||||
nf2 := doc.Find("#nf2")
|
||||
assertLength(t, nf2.Nodes, 2)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapEmpty(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#nf1").Wrap("#doesnt-exist")
|
||||
|
||||
origHtml, _ := Doc2().Html()
|
||||
newHtml, _ := doc.Html()
|
||||
|
||||
if origHtml != newHtml {
|
||||
t.Error("Expected the two documents to be identical.")
|
||||
}
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find(".odd").WrapHtml(wrapHtml)
|
||||
nf2 := doc.Find("#ins #nf2")
|
||||
assertLength(t, nf2.Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapSelection(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#nf1").WrapSelection(doc.Find("#nf2"))
|
||||
nf1 := doc.Find("#foot #nf2 #nf1")
|
||||
assertLength(t, nf1.Nodes, 1)
|
||||
|
||||
nf2 := doc.Find("#nf2")
|
||||
assertLength(t, nf2.Nodes, 2)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapAll(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find(".odd").WrapAll("#nf1")
|
||||
nf1 := doc.Find("#main #nf1")
|
||||
assertLength(t, nf1.Nodes, 1)
|
||||
|
||||
sel := nf1.Find("#n2 ~ #n4 ~ #n6 ~ #nf2 ~ #nf4 ~ #nf6")
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapAllHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find(".odd").WrapAllHtml(wrapHtml)
|
||||
nf1 := doc.Find("#main div#ins div p em b #n2 ~ #n4 ~ #n6 ~ #nf2 ~ #nf4 ~ #nf6")
|
||||
assertLength(t, nf1.Nodes, 1)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapInnerNoContent(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find(".one").WrapInner(".two")
|
||||
|
||||
twos := doc.Find(".two")
|
||||
assertLength(t, twos.Nodes, 4)
|
||||
assertLength(t, doc.Find(".one .two").Nodes, 2)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapInnerWithContent(t *testing.T) {
|
||||
doc := Doc3Clone()
|
||||
doc.Find(".one").WrapInner(".two")
|
||||
|
||||
twos := doc.Find(".two")
|
||||
assertLength(t, twos.Nodes, 4)
|
||||
assertLength(t, doc.Find(".one .two").Nodes, 2)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapInnerNoWrapper(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find(".one").WrapInner(".not-exist")
|
||||
|
||||
twos := doc.Find(".two")
|
||||
assertLength(t, twos.Nodes, 2)
|
||||
assertLength(t, doc.Find(".one").Nodes, 2)
|
||||
assertLength(t, doc.Find(".one .two").Nodes, 0)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestWrapInnerHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#foot").WrapInnerHtml(wrapHtml)
|
||||
|
||||
foot := doc.Find("#foot div#ins div p em b #nf1 ~ #nf2 ~ #nf3")
|
||||
assertLength(t, foot.Nodes, 1)
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
+25
-6
@@ -20,6 +20,20 @@ func (s *Selection) Attr(attrName string) (val string, exists bool) {
|
||||
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 {
|
||||
@@ -32,7 +46,10 @@ func (s *Selection) RemoveAttr(attrName string) *Selection {
|
||||
// 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 attr := getAttributePtr(attrName, n); attr != nil {
|
||||
attr := getAttributePtr(attrName, n)
|
||||
if attr == nil {
|
||||
n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val})
|
||||
} else {
|
||||
attr.Val = val
|
||||
}
|
||||
}
|
||||
@@ -83,16 +100,18 @@ func (s *Selection) Html() (ret string, e error) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *Selection) AddClass(class string) *Selection {
|
||||
if class == "" {
|
||||
// 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
|
||||
}
|
||||
|
||||
slClasses := getClassesSlice(class)
|
||||
tcls := getClassesSlice(classStr)
|
||||
for _, n := range s.Nodes {
|
||||
curClasses, attr := getClassesAndAttr(n, true)
|
||||
for _, newClass := range slClasses {
|
||||
for _, newClass := range tcls {
|
||||
if strings.Index(curClasses, " "+newClass+" ") == -1 {
|
||||
curClasses += newClass + " "
|
||||
}
|
||||
|
||||
@@ -14,6 +14,19 @@ func TestAttrExists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -46,6 +59,21 @@ func TestSetAttr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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" {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"code.google.com/p/cascadia"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
import "golang.org/x/net/html"
|
||||
|
||||
// Is checks the current matched set of elements against a selector and
|
||||
// returns true if at least one of these elements matches.
|
||||
func (s *Selection) Is(selector string) bool {
|
||||
if len(s.Nodes) > 0 {
|
||||
return s.IsMatcher(cascadia.MustCompile(selector))
|
||||
return s.IsMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -11,6 +11,13 @@ func TestIs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".footer p:nth-child(1)")
|
||||
if sel.Is("") {
|
||||
t.Error("Is should not succeed with invalid selector string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPositional(t *testing.T) {
|
||||
sel := Doc().Find(".footer p:nth-child(2)")
|
||||
if !sel.Is("p:nth-child(2)") {
|
||||
|
||||
Vendored
+2
-2
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<title>Tests for siblings</title>
|
||||
</head>
|
||||
<body>
|
||||
<BODY>
|
||||
<div id="main">
|
||||
<div id="n1" class="one even row"></div>
|
||||
<div id="n2" class="two odd row"></div>
|
||||
@@ -20,5 +20,5 @@
|
||||
<div id="nf5" class="five even row odder"></div>
|
||||
<div id="nf6" class="six odd row"></div>
|
||||
</div>
|
||||
</body>
|
||||
</BODY>
|
||||
</html>
|
||||
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tests for siblings</title>
|
||||
</head>
|
||||
<BODY>
|
||||
<div id="main">
|
||||
<div id="n1" class="one even row">hello</div>
|
||||
<div id="n2" class="two odd row"></div>
|
||||
<div id="n3" class="three even row"></div>
|
||||
<div id="n4" class="four odd row"></div>
|
||||
<div id="n5" class="five even row"></div>
|
||||
<div id="n6" class="six odd row"></div>
|
||||
</div>
|
||||
<div id="foot">
|
||||
<div id="nf1" class="one even row">text</div>
|
||||
<div id="nf2" class="two odd row"></div>
|
||||
<div id="nf3" class="three even row"></div>
|
||||
<div id="nf4" class="four odd row"></div>
|
||||
<div id="nf5" class="five even row odder"></div>
|
||||
<div id="nf6" class="six odd row"></div>
|
||||
</div>
|
||||
</BODY>
|
||||
</html>
|
||||
+23
-26
@@ -1,9 +1,6 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"code.google.com/p/cascadia"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
import "golang.org/x/net/html"
|
||||
|
||||
type siblingType int
|
||||
|
||||
@@ -24,7 +21,7 @@ const (
|
||||
// elements, filtered by a selector. It returns a new Selection object
|
||||
// containing these matched elements.
|
||||
func (s *Selection) Find(selector string) *Selection {
|
||||
return pushStack(s, findWithMatcher(s.Nodes, cascadia.MustCompile(selector)))
|
||||
return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
|
||||
}
|
||||
|
||||
// FindMatcher gets the descendants of each element in the current set of matched
|
||||
@@ -93,7 +90,7 @@ func (s *Selection) Children() *Selection {
|
||||
// filtered by the specified selector. It returns a new
|
||||
// Selection object containing these elements.
|
||||
func (s *Selection) ChildrenFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// ChildrenMatcher gets the child elements of each element in the Selection,
|
||||
@@ -112,7 +109,7 @@ func (s *Selection) Parent() *Selection {
|
||||
// ParentFiltered gets the parent of each element in the Selection filtered by a
|
||||
// selector. It returns a new Selection object containing the matched elements.
|
||||
func (s *Selection) ParentFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getParentNodes(s.Nodes), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getParentNodes(s.Nodes), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// ParentMatcher gets the parent of each element in the Selection filtered by a
|
||||
@@ -124,7 +121,7 @@ func (s *Selection) ParentMatcher(m Matcher) *Selection {
|
||||
// Closest gets the first element that matches the selector by testing the
|
||||
// element itself and traversing up through its ancestors in the DOM tree.
|
||||
func (s *Selection) Closest(selector string) *Selection {
|
||||
cs := cascadia.MustCompile(selector)
|
||||
cs := compileMatcher(selector)
|
||||
return s.ClosestMatcher(cs)
|
||||
}
|
||||
|
||||
@@ -177,7 +174,7 @@ func (s *Selection) Parents() *Selection {
|
||||
// ParentsFiltered gets the ancestors of each element in the current
|
||||
// Selection. It returns a new Selection object with the matched elements.
|
||||
func (s *Selection) ParentsFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// ParentsMatcher gets the ancestors of each element in the current
|
||||
@@ -190,7 +187,7 @@ func (s *Selection) ParentsMatcher(m Matcher) *Selection {
|
||||
// not including the element matched by the selector. It returns a new Selection
|
||||
// object containing the matched elements.
|
||||
func (s *Selection) ParentsUntil(selector string) *Selection {
|
||||
return pushStack(s, getParentsNodes(s.Nodes, cascadia.MustCompile(selector), nil))
|
||||
return pushStack(s, getParentsNodes(s.Nodes, compileMatcher(selector), nil))
|
||||
}
|
||||
|
||||
// ParentsUntilMatcher gets the ancestors of each element in the Selection, up to but
|
||||
@@ -221,7 +218,7 @@ func (s *Selection) ParentsUntilNodes(nodes ...*html.Node) *Selection {
|
||||
// results based on a selector string. It returns a new Selection
|
||||
// object containing the matched elements.
|
||||
func (s *Selection) ParentsFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector))
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// ParentsFilteredUntilMatcher is like ParentsUntilMatcher, with the option to filter the
|
||||
@@ -234,7 +231,7 @@ func (s *Selection) ParentsFilteredUntilMatcher(filter, until Matcher) *Selectio
|
||||
// option to filter the results based on a selector string. It returns a new
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) ParentsFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||
return s.ParentsMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel)
|
||||
return s.ParentsMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||
}
|
||||
|
||||
// ParentsMatcherUntilSelection is like ParentsUntilSelection, with the
|
||||
@@ -251,7 +248,7 @@ func (s *Selection) ParentsMatcherUntilSelection(filter Matcher, sel *Selection)
|
||||
// option to filter the results based on a selector string. It returns a new
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) ParentsFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), cascadia.MustCompile(filterSelector))
|
||||
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// ParentsMatcherUntilNodes is like ParentsUntilNodes, with the
|
||||
@@ -271,7 +268,7 @@ func (s *Selection) Siblings() *Selection {
|
||||
// filtered by a selector. It returns a new Selection object containing the
|
||||
// matched elements.
|
||||
func (s *Selection) SiblingsFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// SiblingsMatcher gets the siblings of each element in the Selection
|
||||
@@ -291,7 +288,7 @@ func (s *Selection) Next() *Selection {
|
||||
// Selection filtered by a selector. It returns a new Selection object
|
||||
// containing the matched elements.
|
||||
func (s *Selection) NextFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// NextMatcher gets the immediately following sibling of each element in the
|
||||
@@ -311,7 +308,7 @@ func (s *Selection) NextAll() *Selection {
|
||||
// Selection filtered by a selector. It returns a new Selection object
|
||||
// containing the matched elements.
|
||||
func (s *Selection) NextAllFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// NextAllMatcher gets all the following siblings of each element in the
|
||||
@@ -331,7 +328,7 @@ func (s *Selection) Prev() *Selection {
|
||||
// Selection filtered by a selector. It returns a new Selection object
|
||||
// containing the matched elements.
|
||||
func (s *Selection) PrevFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// PrevMatcher gets the immediately preceding sibling of each element in the
|
||||
@@ -351,7 +348,7 @@ func (s *Selection) PrevAll() *Selection {
|
||||
// Selection filtered by a selector. It returns a new Selection object
|
||||
// containing the matched elements.
|
||||
func (s *Selection) PrevAllFiltered(selector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), cascadia.MustCompile(selector))
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), compileMatcher(selector))
|
||||
}
|
||||
|
||||
// PrevAllMatcher gets all the preceding siblings of each element in the
|
||||
@@ -366,7 +363,7 @@ func (s *Selection) PrevAllMatcher(m Matcher) *Selection {
|
||||
// object containing the matched elements.
|
||||
func (s *Selection) NextUntil(selector string) *Selection {
|
||||
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||
cascadia.MustCompile(selector), nil))
|
||||
compileMatcher(selector), nil))
|
||||
}
|
||||
|
||||
// NextUntilMatcher gets all following siblings of each element up to but not
|
||||
@@ -400,7 +397,7 @@ func (s *Selection) NextUntilNodes(nodes ...*html.Node) *Selection {
|
||||
// object containing the matched elements.
|
||||
func (s *Selection) PrevUntil(selector string) *Selection {
|
||||
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||
cascadia.MustCompile(selector), nil))
|
||||
compileMatcher(selector), nil))
|
||||
}
|
||||
|
||||
// PrevUntilMatcher gets all preceding siblings of each element up to but not
|
||||
@@ -434,7 +431,7 @@ func (s *Selection) PrevUntilNodes(nodes ...*html.Node) *Selection {
|
||||
// It returns a new Selection object containing the matched elements.
|
||||
func (s *Selection) NextFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||
cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector))
|
||||
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// NextFilteredUntilMatcher is like NextUntilMatcher, with the option to filter
|
||||
@@ -449,7 +446,7 @@ func (s *Selection) NextFilteredUntilMatcher(filter, until Matcher) *Selection {
|
||||
// option to filter the results based on a selector string. It returns a new
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) NextFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||
return s.NextMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel)
|
||||
return s.NextMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||
}
|
||||
|
||||
// NextMatcherUntilSelection is like NextUntilSelection, with the
|
||||
@@ -467,7 +464,7 @@ func (s *Selection) NextMatcherUntilSelection(filter Matcher, sel *Selection) *S
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) NextFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
|
||||
nil, nodes), cascadia.MustCompile(filterSelector))
|
||||
nil, nodes), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// NextMatcherUntilNodes is like NextUntilNodes, with the
|
||||
@@ -483,7 +480,7 @@ func (s *Selection) NextMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *
|
||||
// It returns a new Selection object containing the matched elements.
|
||||
func (s *Selection) PrevFilteredUntil(filterSelector, untilSelector string) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||
cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector))
|
||||
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// PrevFilteredUntilMatcher is like PrevUntilMatcher, with the option to filter
|
||||
@@ -498,7 +495,7 @@ func (s *Selection) PrevFilteredUntilMatcher(filter, until Matcher) *Selection {
|
||||
// option to filter the results based on a selector string. It returns a new
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) PrevFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
|
||||
return s.PrevMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel)
|
||||
return s.PrevMatcherUntilSelection(compileMatcher(filterSelector), sel)
|
||||
}
|
||||
|
||||
// PrevMatcherUntilSelection is like PrevUntilSelection, with the
|
||||
@@ -516,7 +513,7 @@ func (s *Selection) PrevMatcherUntilSelection(filter Matcher, sel *Selection) *S
|
||||
// Selection object containing the matched elements.
|
||||
func (s *Selection) PrevFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
|
||||
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
|
||||
nil, nodes), cascadia.MustCompile(filterSelector))
|
||||
nil, nodes), compileMatcher(filterSelector))
|
||||
}
|
||||
|
||||
// PrevMatcherUntilNodes is like PrevUntilNodes, with the
|
||||
|
||||
+89
-3
@@ -21,9 +21,9 @@ func TestFindNotSelf(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestFindInvalidSelector(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find(":+ ^")
|
||||
func TestFindInvalid(t *testing.T) {
|
||||
sel := Doc().Find(":+ ^")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestChainedFind(t *testing.T) {
|
||||
@@ -31,6 +31,11 @@ func TestChainedFind(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 4)
|
||||
}
|
||||
|
||||
func TestChainedFindInvalid(t *testing.T) {
|
||||
sel := Doc().Find("div.hero-unit").Find("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestChildren(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").Children()
|
||||
assertLength(t, sel.Nodes, 5)
|
||||
@@ -58,6 +63,11 @@ func TestChildrenFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
}
|
||||
|
||||
func TestChildrenFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").ChildrenFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestChildrenFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.ChildrenFiltered(".hero-unit").End()
|
||||
@@ -69,6 +79,11 @@ func TestContentsFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
}
|
||||
|
||||
func TestContentsFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").ContentsFiltered("~")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestContentsFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content")
|
||||
sel2 := sel.ContentsFiltered(".hero-unit").End()
|
||||
@@ -102,6 +117,11 @@ func TestParentFiltered(t *testing.T) {
|
||||
assertClass(t, sel, "hero-unit")
|
||||
}
|
||||
|
||||
func TestParentFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").ParentFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestParentFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.ParentFiltered(".hero-unit").End()
|
||||
@@ -130,6 +150,11 @@ func TestParentsFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
}
|
||||
|
||||
func TestParentsFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").ParentsFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestParentsFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.ParentsFiltered("body").End()
|
||||
@@ -141,6 +166,11 @@ func TestParentsUntil(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 6)
|
||||
}
|
||||
|
||||
func TestParentsUntilInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").ParentsUntil("")
|
||||
assertLength(t, sel.Nodes, 8)
|
||||
}
|
||||
|
||||
func TestParentsUntilRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.ParentsUntil("body").End()
|
||||
@@ -180,6 +210,11 @@ func TestParentsFilteredUntil(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestParentsFilteredUntilInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").ParentsFilteredUntil("", "")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestParentsFilteredUntilRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.ParentsFilteredUntil(".pvk-content", "body").End()
|
||||
@@ -240,6 +275,11 @@ func TestSiblingsFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 3)
|
||||
}
|
||||
|
||||
func TestSiblingsFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-gutter").SiblingsFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestSiblingsFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-gutter")
|
||||
sel2 := sel.SiblingsFiltered(".pvk-content").End()
|
||||
@@ -272,6 +312,11 @@ func TestNextFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestNextFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").NextFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestNextFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.NextFiltered("div").End()
|
||||
@@ -310,6 +355,11 @@ func TestPrevFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 5)
|
||||
}
|
||||
|
||||
func TestPrevFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".row-fluid").PrevFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestPrevFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".row-fluid")
|
||||
sel2 := sel.PrevFiltered(".row-fluid").End()
|
||||
@@ -342,6 +392,11 @@ func TestNextAllFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestNextAllFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find("#cf2 .row-fluid").NextAllFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestNextAllFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find("#cf2 .row-fluid")
|
||||
sel2 := sel.NextAllFiltered("[ng-cloak]").End()
|
||||
@@ -380,6 +435,11 @@ func TestPrevAllFiltered(t *testing.T) {
|
||||
assertLength(t, sel.Nodes, 3)
|
||||
}
|
||||
|
||||
func TestPrevAllFilteredInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-gutter").PrevAllFiltered("")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestPrevAllFilteredRollback(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-gutter")
|
||||
sel2 := sel.PrevAllFiltered(".pvk-content").End()
|
||||
@@ -392,6 +452,11 @@ func TestNextUntil(t *testing.T) {
|
||||
assertSelectionIs(t, sel, "h4")
|
||||
}
|
||||
|
||||
func TestNextUntilInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".alert a").NextUntil("")
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestNextUntil2(t *testing.T) {
|
||||
sel := Doc().Find("#cf2-1").NextUntil("[ng-cloak]")
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
@@ -446,6 +511,11 @@ func TestPrevUntil(t *testing.T) {
|
||||
assertSelectionIs(t, sel, "h4")
|
||||
}
|
||||
|
||||
func TestPrevUntilInvalid(t *testing.T) {
|
||||
sel := Doc().Find(".alert p").PrevUntil("")
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestPrevUntil2(t *testing.T) {
|
||||
sel := Doc().Find("[ng-cloak]").PrevUntil(":not([ng-cloak])")
|
||||
assertLength(t, sel.Nodes, 1)
|
||||
@@ -500,6 +570,11 @@ func TestNextFilteredUntil(t *testing.T) {
|
||||
assertSelectionIs(t, sel, "#n3", "#n5", "#nf3", "#nf5")
|
||||
}
|
||||
|
||||
func TestNextFilteredUntilInvalid(t *testing.T) {
|
||||
sel := Doc2().Find(".two").NextFilteredUntil("", "")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestNextFilteredUntilRollback(t *testing.T) {
|
||||
sel := Doc2().Find(".two")
|
||||
sel2 := sel.NextFilteredUntil(".even", ".six").End()
|
||||
@@ -542,6 +617,11 @@ func TestPrevFilteredUntil(t *testing.T) {
|
||||
assertSelectionIs(t, sel, "#n4", "#n2", "#nf4", "#nf2")
|
||||
}
|
||||
|
||||
func TestPrevFilteredUntilInvalid(t *testing.T) {
|
||||
sel := Doc2().Find(".five").PrevFilteredUntil("", "")
|
||||
assertLength(t, sel.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestPrevFilteredUntilRollback(t *testing.T) {
|
||||
sel := Doc2().Find(".four")
|
||||
sel2 := sel.PrevFilteredUntil(".odd", ".one").End()
|
||||
@@ -598,6 +678,12 @@ func TestClosestNone(t *testing.T) {
|
||||
assertLength(t, sel2.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestClosestInvalid(t *testing.T) {
|
||||
sel := Doc().Find("h4")
|
||||
sel2 := sel.Closest("")
|
||||
assertLength(t, sel2.Nodes, 0)
|
||||
}
|
||||
|
||||
func TestClosestMany(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid")
|
||||
sel2 := sel.Closest(".pvk-content")
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/andybalholm/cascadia"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@@ -51,15 +53,17 @@ func NewDocumentFromReader(r io.Reader) (*Document, error) {
|
||||
return newDocument(root, nil), nil
|
||||
}
|
||||
|
||||
// NewDocumentFromResponse is another Document constructor that takes an http resonse as argument.
|
||||
// NewDocumentFromResponse is another Document constructor that takes an http response as argument.
|
||||
// It loads the specified response's document, parses it, and stores the root Document
|
||||
// node, ready to be manipulated. The response's body is closed on return.
|
||||
func NewDocumentFromResponse(res *http.Response) (*Document, error) {
|
||||
if res == nil {
|
||||
return nil, errors.New("Response is nil pointer")
|
||||
return nil, errors.New("Response is nil")
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
if res.Request == nil {
|
||||
return nil, errors.New("Response.Request is nil")
|
||||
}
|
||||
|
||||
// Parse the HTML into nodes
|
||||
root, e := html.Parse(res.Body)
|
||||
@@ -111,3 +115,21 @@ type Matcher interface {
|
||||
MatchAll(*html.Node) []*html.Node
|
||||
Filter([]*html.Node) []*html.Node
|
||||
}
|
||||
|
||||
// compileMatcher compiles the selector string s and returns
|
||||
// the corresponding Matcher. If s is an invalid selector string,
|
||||
// it returns a Matcher that fails all matches.
|
||||
func compileMatcher(s string) Matcher {
|
||||
cs, err := cascadia.Compile(s)
|
||||
if err != nil {
|
||||
return invalidMatcher{}
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
// invalidMatcher is a Matcher that always fails to match.
|
||||
type invalidMatcher struct{}
|
||||
|
||||
func (invalidMatcher) Match(n *html.Node) bool { return false }
|
||||
func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil }
|
||||
func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil }
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
// Test helper functions and members
|
||||
var doc *Document
|
||||
var doc2 *Document
|
||||
var doc3 *Document
|
||||
var docB *Document
|
||||
var docW *Document
|
||||
|
||||
@@ -33,6 +35,15 @@ func Doc2() *Document {
|
||||
func Doc2Clone() *Document {
|
||||
return CloneDocument(Doc2())
|
||||
}
|
||||
func Doc3() *Document {
|
||||
if doc3 == nil {
|
||||
doc3 = loadDoc("page3.html")
|
||||
}
|
||||
return doc3
|
||||
}
|
||||
func Doc3Clone() *Document {
|
||||
return CloneDocument(Doc3())
|
||||
}
|
||||
func DocB() *Document {
|
||||
if docB == nil {
|
||||
docB = loadDoc("gotesting.html")
|
||||
@@ -97,6 +108,19 @@ func printSel(t *testing.T, sel *Selection) {
|
||||
}
|
||||
}
|
||||
|
||||
func shortPrintSel(t *testing.T, sel *Selection) {
|
||||
sel.Each(func(i int, s *Selection) {
|
||||
h, _ := OuterHtml(s)
|
||||
if ix := strings.Index(h, "\n"); ix >= 0 {
|
||||
h = h[:ix]
|
||||
}
|
||||
if len(h) > 100 {
|
||||
h = h[:100]
|
||||
}
|
||||
fmt.Println(i, ">>> ", h)
|
||||
})
|
||||
}
|
||||
|
||||
func loadDoc(page string) *Document {
|
||||
var f *os.File
|
||||
var e error
|
||||
@@ -180,3 +204,15 @@ func TestNewDocumentFromResponseNil(t *testing.T) {
|
||||
t.Error("Expected error, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue103(t *testing.T) {
|
||||
d, err := NewDocumentFromReader(strings.NewReader("<html><title>Scientists Stored These Images in DNA—Then Flawlessly Retrieved Them</title></html>"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
text := d.Find("title").Text()
|
||||
for i, r := range text {
|
||||
t.Logf("%d: %d - %q\n", i, r, string(r))
|
||||
}
|
||||
t.Log(text)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var nodeNames = []string{
|
||||
html.ErrorNode: "#error",
|
||||
html.TextNode: "#text",
|
||||
html.DocumentNode: "#document",
|
||||
html.CommentNode: "#comment",
|
||||
}
|
||||
|
||||
// NodeName returns the node name of the first element in the selection.
|
||||
// It tries to behave in a similar way as the DOM's nodeName property
|
||||
// (https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName).
|
||||
//
|
||||
// Go's net/html package defines the following node types, listed with
|
||||
// the corresponding returned value from this function:
|
||||
//
|
||||
// ErrorNode : #error
|
||||
// TextNode : #text
|
||||
// DocumentNode : #document
|
||||
// ElementNode : the element's tag name
|
||||
// CommentNode : #comment
|
||||
// DoctypeNode : the name of the document type
|
||||
//
|
||||
func NodeName(s *Selection) string {
|
||||
if s.Length() == 0 {
|
||||
return ""
|
||||
}
|
||||
switch n := s.Get(0); n.Type {
|
||||
case html.ElementNode, html.DoctypeNode:
|
||||
return n.Data
|
||||
default:
|
||||
if n.Type >= 0 && int(n.Type) < len(nodeNames) {
|
||||
return nodeNames[n.Type]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// OuterHtml returns the outer HTML rendering of the first item in
|
||||
// the selection - that is, the HTML including the first element's
|
||||
// tag and attributes.
|
||||
//
|
||||
// Unlike InnerHtml, this is a function and not a method on the Selection,
|
||||
// because this is not a jQuery method (in javascript-land, this is
|
||||
// a property provided by the DOM).
|
||||
func OuterHtml(s *Selection) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if s.Length() == 0 {
|
||||
return "", nil
|
||||
}
|
||||
n := s.Get(0)
|
||||
if err := html.Render(&buf, n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func getChildren(n *html.Node) (result []*html.Node) {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
result = append(result, c)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var allNodes = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta a="b">
|
||||
</head>
|
||||
<body>
|
||||
<p><!-- this is a comment -->
|
||||
This is some text.
|
||||
</p>
|
||||
<div></div>
|
||||
<h1 class="header"></h1>
|
||||
<h2 class="header"></h2>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
func TestNodeName(t *testing.T) {
|
||||
doc, err := NewDocumentFromReader(strings.NewReader(allNodes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n0 := doc.Nodes[0]
|
||||
nDT := n0.FirstChild
|
||||
sMeta := doc.Find("meta")
|
||||
nMeta := sMeta.Get(0)
|
||||
sP := doc.Find("p")
|
||||
nP := sP.Get(0)
|
||||
nComment := nP.FirstChild
|
||||
nText := nComment.NextSibling
|
||||
|
||||
cases := []struct {
|
||||
node *html.Node
|
||||
typ html.NodeType
|
||||
want string
|
||||
}{
|
||||
{n0, html.DocumentNode, nodeNames[html.DocumentNode]},
|
||||
{nDT, html.DoctypeNode, "html"},
|
||||
{nMeta, html.ElementNode, "meta"},
|
||||
{nP, html.ElementNode, "p"},
|
||||
{nComment, html.CommentNode, nodeNames[html.CommentNode]},
|
||||
{nText, html.TextNode, nodeNames[html.TextNode]},
|
||||
}
|
||||
for i, c := range cases {
|
||||
got := NodeName(newSingleSelection(c.node, doc))
|
||||
if c.node.Type != c.typ {
|
||||
t.Errorf("%d: want type %v, got %v", i, c.typ, c.node.Type)
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("%d: want %q, got %q", i, c.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeNameMultiSel(t *testing.T) {
|
||||
doc, err := NewDocumentFromReader(strings.NewReader(allNodes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
in := []string{"p", "h1", "div"}
|
||||
var out []string
|
||||
doc.Find(strings.Join(in, ", ")).Each(func(i int, s *Selection) {
|
||||
got := NodeName(s)
|
||||
out = append(out, got)
|
||||
})
|
||||
sort.Strings(in)
|
||||
sort.Strings(out)
|
||||
if !reflect.DeepEqual(in, out) {
|
||||
t.Errorf("want %v, got %v", in, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOuterHtml(t *testing.T) {
|
||||
doc, err := NewDocumentFromReader(strings.NewReader(allNodes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n0 := doc.Nodes[0]
|
||||
nDT := n0.FirstChild
|
||||
sMeta := doc.Find("meta")
|
||||
sP := doc.Find("p")
|
||||
nP := sP.Get(0)
|
||||
nComment := nP.FirstChild
|
||||
nText := nComment.NextSibling
|
||||
sHeaders := doc.Find(".header")
|
||||
|
||||
cases := []struct {
|
||||
node *html.Node
|
||||
sel *Selection
|
||||
want string
|
||||
}{
|
||||
{nDT, nil, "<!DOCTYPE html>"}, // render makes DOCTYPE all caps
|
||||
{nil, sMeta, `<meta a="b"/>`}, // and auto-closes the meta
|
||||
{nil, sP, `<p><!-- this is a comment -->
|
||||
This is some text.
|
||||
</p>`},
|
||||
{nComment, nil, "<!-- this is a comment -->"},
|
||||
{nText, nil, `
|
||||
This is some text.
|
||||
`},
|
||||
{nil, sHeaders, `<h1 class="header"></h1>`},
|
||||
}
|
||||
for i, c := range cases {
|
||||
if c.sel == nil {
|
||||
c.sel = newSingleSelection(c.node, doc)
|
||||
}
|
||||
got, err := OuterHtml(c.sel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got != c.want {
|
||||
t.Errorf("%d: want %q, got %q", i, c.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user