mirror of
https://github.com/PuerkitoBio/goquery.git
synced 2024-04-21 12:31:36 +00:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171b719dd6 | ||
|
|
f41c56001f | ||
|
|
8a11cc402f | ||
|
|
70a02e53e3 | ||
|
|
3798c63c0d | ||
|
|
705a0066a5 | ||
|
|
a48bafac4b | ||
|
|
af965a4d72 | ||
|
|
b3706f8903 | ||
|
|
7ebd145bd7 | ||
|
|
4a687a672f | ||
|
|
94c2530dd8 | ||
|
|
5d7b0271f0 | ||
|
|
d89086ead1 | ||
|
|
99c8f1ecd5 | ||
|
|
c942deb265 | ||
|
|
0132bb8147 | ||
|
|
6f0f9d6b87 | ||
|
|
cd12820764 | ||
|
|
51a0e1a4bb | ||
|
|
89946c829f | ||
|
|
8b2a6e25d5 | ||
|
|
538dd4a913 | ||
|
|
844d6bd46b | ||
|
|
1b16b92ddf | ||
|
|
3d11372b35 | ||
|
|
bd23f0c6e0 | ||
|
|
56ba1e6f08 | ||
|
|
b17a2bbb6d | ||
|
|
c70bac62f5 | ||
|
|
8674076365 | ||
|
|
6dcff85b2e | ||
|
|
3dcf72e6c1 | ||
|
|
22fd749627 | ||
|
|
9b813b2309 | ||
|
|
0255f16068 | ||
|
|
2d2796f417 | ||
|
|
80f0084f4c | ||
|
|
68fe358134 | ||
|
|
2af3d16e2b | ||
|
|
6c2cec4567 | ||
|
|
8311f594d7 | ||
|
|
dc2ec5c7ca | ||
|
|
7757e27950 | ||
|
|
ea1bc64a63 | ||
|
|
dd77530fdb | ||
|
|
a86ea07301 | ||
|
|
b4912d23e5 | ||
|
|
106fcbf95a | ||
|
|
61aa1975b1 | ||
|
|
7a8f63580d | ||
|
|
6285ceb1e9 | ||
|
|
b55d492104 | ||
|
|
ef34a70bad | ||
|
|
5819122076 | ||
|
|
09540e5659 | ||
|
|
46f28d87ad | ||
|
|
520f19d599 | ||
|
|
2324bda66f | ||
|
|
3855699cc0 |
@@ -0,0 +1,20 @@
|
||||
image: fedora/latest
|
||||
packages:
|
||||
- git
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/PuerkitoBio/goquery
|
||||
tasks:
|
||||
- go1_2: |
|
||||
cd goquery
|
||||
go get golang.org/dl/go1.2.2
|
||||
go1.2.2 download
|
||||
go1.2.2 version
|
||||
go1.2.2 test -v
|
||||
|
||||
- go1_3: |
|
||||
cd goquery
|
||||
go get golang.org/dl/go1.3.3
|
||||
go1.3.3 download
|
||||
go1.3.3 version
|
||||
go1.3.3 test -v
|
||||
+32
-1
@@ -1,7 +1,9 @@
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1.x
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4.x
|
||||
@@ -10,4 +12,33 @@ go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- tip
|
||||
|
||||
jobs:
|
||||
exclude:
|
||||
- arch: ppc64le
|
||||
go: 1.2.x
|
||||
- arch: ppc64le
|
||||
go: 1.3.x
|
||||
- arch: ppc64le
|
||||
go: 1.4.x
|
||||
- arch: ppc64le
|
||||
go: 1.5.x
|
||||
- arch: ppc64le
|
||||
go: 1.6.x
|
||||
- arch: ppc64le
|
||||
go: 1.7.x
|
||||
- arch: ppc64le
|
||||
go: 1.8.x
|
||||
- arch: ppc64le
|
||||
go: 1.9.x
|
||||
- arch: ppc64le
|
||||
go: 1.10.x
|
||||
- arch: ppc64le
|
||||
go: 1.11.x
|
||||
- arch: ppc64le
|
||||
go: 1.12.x
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# goquery - a little like that j-thing, only in Go
|
||||
[](http://travis-ci.org/PuerkitoBio/goquery) [](http://godoc.org/github.com/PuerkitoBio/goquery) [](https://sourcegraph.com/github.com/PuerkitoBio/goquery?badge)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML. See the [wiki][] for various options to do this.
|
||||
@@ -15,6 +14,7 @@ Syntax-wise, it is as close as possible to jQuery, with the same function names
|
||||
* [API](#api)
|
||||
* [Examples](#examples)
|
||||
* [Related Projects](#related-projects)
|
||||
* [Support](#support)
|
||||
* [License](#license)
|
||||
|
||||
## Installation
|
||||
@@ -37,6 +37,13 @@ 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.**
|
||||
|
||||
* **2021-01-11 (v1.6.1)** : Fix panic when calling `{Prepend,Append,Set}Html` on a `Selection` that contains non-Element nodes.
|
||||
* **2020-10-08 (v1.6.0)** : Parse html in context of the container node for all functions that deal with html strings (`AfterHtml`, `AppendHtml`, etc.). Thanks to [@thiemok][thiemok] and [@davidjwilkins][djw] for their work on this.
|
||||
* **2020-02-04 (v1.5.1)** : Update module dependencies.
|
||||
* **2018-11-15 (v1.5.0)** : Go module support (thanks @Zaba505).
|
||||
* **2018-06-07 (v1.4.1)** : Add `NewDocumentFromReader` examples.
|
||||
* **2018-03-24 (v1.4.0)** : Deprecate `NewDocument(url)` and `NewDocumentFromResponse(response)`.
|
||||
* **2018-01-28 (v1.3.0)** : Add `ToEnd` constant to `Slice` until the end of the selection (thanks to @davidjwilkins for raising the issue).
|
||||
* **2018-01-11 (v1.2.0)** : Add `AddBack*` and deprecate `AndSelf` (thanks to @davidjwilkins).
|
||||
* **2017-02-12 (v1.1.0)** : Add `SetHtml` and `SetText` (thanks to @glebtv).
|
||||
* **2016-12-29 (v1.0.2)** : Optimize allocations for `Selection.Text` (thanks to @radovskyb).
|
||||
@@ -93,12 +100,24 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func ExampleScrape() {
|
||||
doc, err := goquery.NewDocument("http://metalsucks.net")
|
||||
// Request the HTML page.
|
||||
res, err := http.Get("http://metalsucks.net")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
|
||||
}
|
||||
|
||||
// Load the HTML document
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -122,7 +141,30 @@ func main() {
|
||||
- [Goq][goq], an HTML deserialization and scraping library based on goquery and struct tags.
|
||||
- [andybalholm/cascadia][cascadia], the CSS selector library used by goquery.
|
||||
- [suntong/cascadia][cascadiacli], a command-line interface to the cascadia CSS selector library, useful to test selectors.
|
||||
- [asciimoo/colly](https://github.com/asciimoo/colly), a lightning fast and elegant Scraping Framework
|
||||
- [gocolly/colly](https://github.com/gocolly/colly), a lightning fast and elegant Scraping Framework
|
||||
- [gnulnx/goperf](https://github.com/gnulnx/goperf), a website performance test tool that also fetches static assets.
|
||||
- [MontFerret/ferret](https://github.com/MontFerret/ferret), declarative web scraping.
|
||||
- [tacusci/berrycms](https://github.com/tacusci/berrycms), a modern simple to use CMS with easy to write plugins
|
||||
- [Dataflow kit](https://github.com/slotix/dataflowkit), Web Scraping framework for Gophers.
|
||||
- [Geziyor](https://github.com/geziyor/geziyor), a fast web crawling & scraping framework for Go. Supports JS rendering.
|
||||
- [Pagser](https://github.com/foolin/pagser), a simple, easy, extensible, configurable HTML parser to struct based on goquery and struct tags.
|
||||
- [stitcherd](https://github.com/vhodges/stitcherd), A server for doing server side includes using css selectors and DOM updates.
|
||||
|
||||
## Support
|
||||
|
||||
There are a number of ways you can support the project:
|
||||
|
||||
* Use it, star it, build something with it, spread the word!
|
||||
- If you do build something open-source or otherwise publicly-visible, let me know so I can add it to the [Related Projects](#related-projects) section!
|
||||
* Raise issues to improve the project (note: doc typos and clarifications are issues too!)
|
||||
- Please search existing issues before opening a new one - it may have already been adressed.
|
||||
* Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
|
||||
- Make sure new code is tested.
|
||||
- Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.
|
||||
|
||||
If you desperately want to send money my way, I have a BuyMeACoffee.com page:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/mna" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -143,3 +185,5 @@ The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia'
|
||||
[thatguystone]: https://github.com/thatguystone
|
||||
[piotr]: https://github.com/piotrkowalczuk
|
||||
[goq]: https://github.com/andrewstuart/goq
|
||||
[thiemok]: https://github.com/thiemok
|
||||
[djw]: https://github.com/davidjwilkins
|
||||
|
||||
@@ -4,6 +4,16 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
maxUint = ^uint(0)
|
||||
maxInt = int(maxUint >> 1)
|
||||
|
||||
// ToEnd is a special index value that can be used as end index in a call
|
||||
// to Slice so that all elements are selected until the end of the Selection.
|
||||
// It is equivalent to passing (*Selection).Length().
|
||||
ToEnd = maxInt
|
||||
)
|
||||
|
||||
// First reduces the set of matched elements to the first in the set.
|
||||
// It returns a new Selection object, and an empty Selection object if the
|
||||
// the selection is empty.
|
||||
@@ -35,12 +45,23 @@ func (s *Selection) Eq(index int) *Selection {
|
||||
}
|
||||
|
||||
// Slice reduces the set of matched elements to a subset specified by a range
|
||||
// of indices.
|
||||
// of indices. The start index is 0-based and indicates the index of the first
|
||||
// element to select. The end index is 0-based and indicates the index at which
|
||||
// the elements stop being selected (the end index is not selected).
|
||||
//
|
||||
// The indices may be negative, in which case they represent an offset from the
|
||||
// end of the selection.
|
||||
//
|
||||
// The special value ToEnd may be specified as end index, in which case all elements
|
||||
// until the end are selected. This works both for a positive and negative start
|
||||
// index.
|
||||
func (s *Selection) Slice(start, end int) *Selection {
|
||||
if start < 0 {
|
||||
start += len(s.Nodes)
|
||||
}
|
||||
if end < 0 {
|
||||
if end == ToEnd {
|
||||
end = len(s.Nodes)
|
||||
} else if end < 0 {
|
||||
end += len(s.Nodes)
|
||||
}
|
||||
return pushStack(s, s.Nodes[start:end])
|
||||
|
||||
@@ -98,6 +98,17 @@ func TestSlice(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").Slice(0, 2)
|
||||
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
assertSelectionIs(t, sel, "#pc1", "#pc2")
|
||||
}
|
||||
|
||||
func TestSliceToEnd(t *testing.T) {
|
||||
sel := Doc().Find(".pvk-content").Slice(1, ToEnd)
|
||||
|
||||
assertLength(t, sel.Nodes, 2)
|
||||
assertSelectionIs(t, sel.Eq(0), "#pc2")
|
||||
if _, ok := sel.Eq(1).Attr("id"); ok {
|
||||
t.Error("Want no attribute ID, got one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceEmpty(t *testing.T) {
|
||||
@@ -110,6 +121,11 @@ func TestSliceInvalid(t *testing.T) {
|
||||
Doc().Find("").Slice(0, 2)
|
||||
}
|
||||
|
||||
func TestSliceInvalidToEnd(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find("").Slice(2, ToEnd)
|
||||
}
|
||||
|
||||
func TestSliceOutOfBounds(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find(".pvk-content").Slice(2, 12)
|
||||
@@ -135,6 +151,12 @@ func TestNegativeSliceBoth(t *testing.T) {
|
||||
assertSelectionIs(t, sel.Eq(1), "#cf3")
|
||||
}
|
||||
|
||||
func TestNegativeSliceToEnd(t *testing.T) {
|
||||
sel := Doc().Find(".container-fluid").Slice(-3, ToEnd)
|
||||
assertLength(t, sel.Nodes, 3)
|
||||
assertSelectionIs(t, sel, "#cf2", "#cf3", "#cf4")
|
||||
}
|
||||
|
||||
func TestNegativeSliceOutOfBounds(t *testing.T) {
|
||||
defer assertPanic(t)
|
||||
Doc().Find(".container-fluid").Slice(-12, -7)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,7 @@ func BenchmarkMap(b *testing.B) {
|
||||
sel := DocW().Find("td")
|
||||
f := func(i int, s *Selection) string {
|
||||
tmp++
|
||||
return string(tmp)
|
||||
return strconv.Itoa(tmp)
|
||||
}
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
+3
-3
@@ -4,13 +4,13 @@
|
||||
|
||||
The `go.net/html` package used by `goquery` requires that the html document is UTF-8 encoded. When you know the encoding of the html page is not UTF-8, you can use the `iconv` package to convert it to UTF-8 (there are various implementation of the `iconv` API, see [godoc.org][iconv] for other options):
|
||||
|
||||
```
|
||||
```bash
|
||||
$ go get -u github.com/djimenez/iconv-go
|
||||
```
|
||||
|
||||
and then:
|
||||
|
||||
```
|
||||
```golang
|
||||
// Load the URL
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
@@ -51,7 +51,7 @@ You can find a code example using `otto` [in this gist][exotto]. Thanks to githu
|
||||
|
||||
If all you need is a normal `for` loop over all nodes in the current selection, where `Map/Each`-style iteration is not necessary, you can use the following:
|
||||
|
||||
```
|
||||
```golang
|
||||
sel := Doc().Find(".selector")
|
||||
for i := range sel.Nodes {
|
||||
single := sel.Eq(i)
|
||||
|
||||
+53
-1
@@ -3,14 +3,27 @@ package goquery_test
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// This example scrapes the reviews shown on the home page of metalsucks.net.
|
||||
func Example() {
|
||||
// Request the HTML page.
|
||||
res, err := http.Get("http://metalsucks.net")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
|
||||
}
|
||||
|
||||
// Load the HTML document
|
||||
doc, err := goquery.NewDocument("http://metalsucks.net")
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -28,3 +41,42 @@ func Example() {
|
||||
|
||||
// xOutput: voluntarily fail the Example output.
|
||||
}
|
||||
|
||||
// This example shows how to use NewDocumentFromReader from a file.
|
||||
func ExampleNewDocumentFromReader_file() {
|
||||
// create from a file
|
||||
f, err := os.Open("some/file.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
doc, err := goquery.NewDocumentFromReader(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// use the goquery document...
|
||||
_ = doc.Find("h1")
|
||||
}
|
||||
|
||||
// This example shows how to use NewDocumentFromReader from a string.
|
||||
func ExampleNewDocumentFromReader_string() {
|
||||
// create from a string
|
||||
data := `
|
||||
<html>
|
||||
<head>
|
||||
<title>My document</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header</h1>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
header := doc.Find("h1").Text()
|
||||
fmt.Println(header)
|
||||
|
||||
// Output: Header
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
module github.com/PuerkitoBio/goquery
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.1.0
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
|
||||
)
|
||||
|
||||
go 1.13
|
||||
@@ -0,0 +1,8 @@
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
+136
-31
@@ -39,8 +39,15 @@ func (s *Selection) AfterSelection(sel *Selection) *Selection {
|
||||
// AfterHtml parses the html and inserts it after the set of matched elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) AfterHtml(html string) *Selection {
|
||||
return s.AfterNodes(parseHtml(html)...)
|
||||
func (s *Selection) AfterHtml(htmlStr string) *Selection {
|
||||
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||
nextSibling := node.NextSibling
|
||||
for _, n := range nodes {
|
||||
if node.Parent != nil {
|
||||
node.Parent.InsertBefore(n, nextSibling)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// AfterNodes inserts the nodes after each element in the set of matched elements.
|
||||
@@ -85,8 +92,12 @@ func (s *Selection) AppendSelection(sel *Selection) *Selection {
|
||||
}
|
||||
|
||||
// AppendHtml parses the html and appends it to the set of matched elements.
|
||||
func (s *Selection) AppendHtml(html string) *Selection {
|
||||
return s.AppendNodes(parseHtml(html)...)
|
||||
func (s *Selection) AppendHtml(htmlStr string) *Selection {
|
||||
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||
for _, n := range nodes {
|
||||
node.AppendChild(n)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// AppendNodes appends the specified nodes to each node in the set of matched elements.
|
||||
@@ -123,8 +134,14 @@ func (s *Selection) BeforeSelection(sel *Selection) *Selection {
|
||||
// BeforeHtml parses the html and inserts it before the set of matched elements.
|
||||
//
|
||||
// This follows the same rules as Selection.Append.
|
||||
func (s *Selection) BeforeHtml(html string) *Selection {
|
||||
return s.BeforeNodes(parseHtml(html)...)
|
||||
func (s *Selection) BeforeHtml(htmlStr string) *Selection {
|
||||
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||
for _, n := range nodes {
|
||||
if node.Parent != nil {
|
||||
node.Parent.InsertBefore(n, node)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BeforeNodes inserts the nodes before each element in the set of matched elements.
|
||||
@@ -184,8 +201,13 @@ func (s *Selection) PrependSelection(sel *Selection) *Selection {
|
||||
}
|
||||
|
||||
// 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)...)
|
||||
func (s *Selection) PrependHtml(htmlStr string) *Selection {
|
||||
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||
firstChild := node.FirstChild
|
||||
for _, n := range nodes {
|
||||
node.InsertBefore(n, firstChild)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// PrependNodes prepends the specified nodes to each node in the set of
|
||||
@@ -212,14 +234,19 @@ func (s *Selection) Remove() *Selection {
|
||||
return s
|
||||
}
|
||||
|
||||
// RemoveFiltered removes the set of matched elements by selector.
|
||||
// It returns the Selection of removed nodes.
|
||||
// RemoveFiltered removes from the current set of matched elements those that
|
||||
// match the selector filter. It returns the Selection of removed nodes.
|
||||
//
|
||||
// For example if the selection s contains "<h1>", "<h2>" and "<h3>"
|
||||
// and s.RemoveFiltered("h2") is called, only the "<h2>" node is removed
|
||||
// (and returned), while "<h1>" and "<h3>" are kept in the document.
|
||||
func (s *Selection) RemoveFiltered(selector string) *Selection {
|
||||
return s.RemoveMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// RemoveMatcher removes the set of matched elements.
|
||||
// It returns the Selection of removed nodes.
|
||||
// RemoveMatcher removes from the current set of matched elements those that
|
||||
// match the Matcher filter. It returns the Selection of removed nodes.
|
||||
// See RemoveFiltered for additional information.
|
||||
func (s *Selection) RemoveMatcher(m Matcher) *Selection {
|
||||
return s.FilterMatcher(m).Remove()
|
||||
}
|
||||
@@ -256,8 +283,16 @@ func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
|
||||
// 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)...)
|
||||
func (s *Selection) ReplaceWithHtml(htmlStr string) *Selection {
|
||||
s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
|
||||
nextSibling := node.NextSibling
|
||||
for _, n := range nodes {
|
||||
if node.Parent != nil {
|
||||
node.Parent.InsertBefore(n, nextSibling)
|
||||
}
|
||||
}
|
||||
})
|
||||
return s.Remove()
|
||||
}
|
||||
|
||||
// ReplaceWithNodes replaces each element in the set of matched elements with
|
||||
@@ -272,8 +307,17 @@ func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
|
||||
|
||||
// SetHtml sets the html content of each element in the selection to
|
||||
// specified html string.
|
||||
func (s *Selection) SetHtml(html string) *Selection {
|
||||
return setHtmlNodes(s, parseHtml(html)...)
|
||||
func (s *Selection) SetHtml(htmlStr string) *Selection {
|
||||
for _, context := range s.Nodes {
|
||||
for c := context.FirstChild; c != nil; c = context.FirstChild {
|
||||
context.RemoveChild(c)
|
||||
}
|
||||
}
|
||||
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
|
||||
for _, n := range nodes {
|
||||
node.AppendChild(n)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SetText sets the content of each element in the selection to specified content.
|
||||
@@ -329,8 +373,23 @@ func (s *Selection) WrapSelection(sel *Selection) *Selection {
|
||||
// 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)...)
|
||||
func (s *Selection) WrapHtml(htmlStr string) *Selection {
|
||||
nodesMap := make(map[string][]*html.Node)
|
||||
for _, context := range s.Nodes {
|
||||
var parent *html.Node
|
||||
if context.Parent != nil {
|
||||
parent = context.Parent
|
||||
} else {
|
||||
parent = &html.Node{Type: html.ElementNode}
|
||||
}
|
||||
nodes, found := nodesMap[nodeName(parent)]
|
||||
if !found {
|
||||
nodes = parseHtmlWithContext(htmlStr, parent)
|
||||
nodesMap[nodeName(parent)] = nodes
|
||||
}
|
||||
newSingleSelection(context, s.document).wrapAllNodes(cloneNodes(nodes)...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// WrapNode wraps each element in the set of matched elements inside the inner-
|
||||
@@ -382,8 +441,18 @@ func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
|
||||
// document.
|
||||
//
|
||||
// It returns the original set of elements.
|
||||
func (s *Selection) WrapAllHtml(html string) *Selection {
|
||||
return s.wrapAllNodes(parseHtml(html)...)
|
||||
func (s *Selection) WrapAllHtml(htmlStr string) *Selection {
|
||||
var context *html.Node
|
||||
var nodes []*html.Node
|
||||
if len(s.Nodes) > 0 {
|
||||
context = s.Nodes[0]
|
||||
if context.Parent != nil {
|
||||
nodes = parseHtmlWithContext(htmlStr, context)
|
||||
} else {
|
||||
nodes = parseHtml(htmlStr)
|
||||
}
|
||||
}
|
||||
return s.wrapAllNodes(nodes...)
|
||||
}
|
||||
|
||||
func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
|
||||
@@ -452,8 +521,17 @@ func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
|
||||
// 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)...)
|
||||
func (s *Selection) WrapInnerHtml(htmlStr string) *Selection {
|
||||
nodesMap := make(map[string][]*html.Node)
|
||||
for _, context := range s.Nodes {
|
||||
nodes, found := nodesMap[nodeName(context)]
|
||||
if !found {
|
||||
nodes = parseHtmlWithContext(htmlStr, context)
|
||||
nodesMap[nodeName(context)] = nodes
|
||||
}
|
||||
newSingleSelection(context, s.document).wrapInnerNodes(cloneNodes(nodes)...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// WrapInnerNode wraps an HTML structure, matched by the given selector, around
|
||||
@@ -493,16 +571,14 @@ func parseHtml(h string) []*html.Node {
|
||||
return nodes
|
||||
}
|
||||
|
||||
func setHtmlNodes(s *Selection, ns ...*html.Node) *Selection {
|
||||
for _, n := range s.Nodes {
|
||||
for c := n.FirstChild; c != nil; c = n.FirstChild {
|
||||
n.RemoveChild(c)
|
||||
}
|
||||
for _, c := range ns {
|
||||
n.AppendChild(cloneNode(c))
|
||||
}
|
||||
func parseHtmlWithContext(h string, context *html.Node) []*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), context)
|
||||
if err != nil {
|
||||
panic("goquery: failed to parse HTML: " + err.Error())
|
||||
}
|
||||
return s
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Get the first child that is an ElementNode
|
||||
@@ -572,3 +648,32 @@ func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// eachNodeHtml parses the given html string and inserts the resulting nodes in the dom with the mergeFn.
|
||||
// The parsed nodes are inserted for each element of the selection.
|
||||
// isParent can be used to indicate that the elements of the selection should be treated as the parent for the parsed html.
|
||||
// A cache is used to avoid parsing the html multiple times should the elements of the selection result in the same context.
|
||||
func (s *Selection) eachNodeHtml(htmlStr string, isParent bool, mergeFn func(n *html.Node, nodes []*html.Node)) *Selection {
|
||||
// cache to avoid parsing the html for the same context multiple times
|
||||
nodeCache := make(map[string][]*html.Node)
|
||||
var context *html.Node
|
||||
for _, n := range s.Nodes {
|
||||
if isParent {
|
||||
context = n.Parent
|
||||
} else {
|
||||
if n.Type != html.ElementNode {
|
||||
continue
|
||||
}
|
||||
context = n
|
||||
}
|
||||
if context != nil {
|
||||
nodes, found := nodeCache[nodeName(context)]
|
||||
if !found {
|
||||
nodes = parseHtmlWithContext(htmlStr, context)
|
||||
nodeCache[nodeName(context)] = nodes
|
||||
}
|
||||
mergeFn(n, cloneNodes(nodes))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package goquery
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -56,6 +57,26 @@ func TestAfterHtml(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestAfterHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Before1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Before2</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table tr td").AfterHtml("<td class='c1'>Test</td><td class='c2'>Again</td>")
|
||||
assertLength(t, doc.Find("table tr td").Nodes, 6)
|
||||
assertClass(t, doc.Find("table tr td").Last(), "c2")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").Append("#nf6")
|
||||
@@ -113,6 +134,27 @@ func TestAppendHtml(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestAppendHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Before1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Before2</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table tr").AppendHtml("<td class='c1'>new1</td><td class='c2'>new2</td>")
|
||||
|
||||
assertLength(t, doc.Find("table td").Nodes, 6)
|
||||
assertClass(t, doc.Find("table td").Last(), "c2")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestBefore(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#main").Before("#nf6")
|
||||
@@ -151,6 +193,27 @@ func TestBeforeHtml(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestBeforeHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Before1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Before2</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table tr td:first-child").BeforeHtml("<td class='c1'>new1</td><td class='c2'>new2</td>")
|
||||
|
||||
assertLength(t, doc.Find("table td").Nodes, 6)
|
||||
assertClass(t, doc.Find("table td").First(), "c1")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
s := doc.Find("#main").Empty()
|
||||
@@ -218,6 +281,27 @@ func TestPrependHtml(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestPrependHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Before1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Before2</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table tr").PrependHtml("<td class='c1'>new node</td><td class='c2'>other new node</td>")
|
||||
|
||||
assertLength(t, doc.Find("table td").Nodes, 6)
|
||||
assertClass(t, doc.Find("table tr td").First(), "c1")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
doc.Find("#nf1").Remove()
|
||||
@@ -278,6 +362,28 @@ func TestReplaceWithHtml(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestReplaceWithHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Before1</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Before2</th>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table th").ReplaceWithHtml("<td class='c1'>Test</td><td class='c2'>Replace</td>")
|
||||
|
||||
assertLength(t, doc.Find("table th").Nodes, 0)
|
||||
assertLength(t, doc.Find("table tr td").Nodes, 4)
|
||||
assertClass(t, doc.Find("table tr td").First(), "c1")
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestSetHtml(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
q := doc.Find("#main, #foot")
|
||||
@@ -313,6 +419,28 @@ func TestSetHtmlEmpty(t *testing.T) {
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestSetHtmlContext(t *testing.T) {
|
||||
doc := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Before1</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Before2</th>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
doc.Find("table tr").SetHtml("<td class='c1'>Test</td><td class='c2'>Again</td>")
|
||||
|
||||
assertLength(t, doc.Find("table th").Nodes, 0)
|
||||
assertLength(t, doc.Find("table td").Nodes, 4)
|
||||
assertLength(t, doc.Find("table tr").Nodes, 2)
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestSetText(t *testing.T) {
|
||||
doc := Doc2Clone()
|
||||
q := doc.Find("#main, #foot")
|
||||
@@ -511,3 +639,101 @@ func TestWrapInnerHtml(t *testing.T) {
|
||||
|
||||
printSel(t, doc.Selection)
|
||||
}
|
||||
|
||||
func TestParsingRespectsVaryingContext(t *testing.T) {
|
||||
docA := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<a class="x"></a>
|
||||
</body>
|
||||
</html>`)
|
||||
docTable := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table class="x"></table>
|
||||
</body>
|
||||
</html>`)
|
||||
docBoth := loadString(t, `
|
||||
<html>
|
||||
<body>
|
||||
<table class="x"></table>
|
||||
<a class="x"></a>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
sA := docA.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
|
||||
sTable := docTable.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
|
||||
sBoth := docBoth.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
|
||||
|
||||
printSel(t, docA.Selection)
|
||||
printSel(t, docTable.Selection)
|
||||
printSel(t, docBoth.Selection)
|
||||
|
||||
oA, _ := sA.Html()
|
||||
oTable, _ := sTable.Html()
|
||||
|
||||
if oA == oTable {
|
||||
t.Errorf("Expected inner html of <a> and <table> to not be equal, but got %s and %s", oA, oTable)
|
||||
}
|
||||
|
||||
oBothTable, _ := sBoth.First().Html()
|
||||
if oBothTable != oTable {
|
||||
t.Errorf("Expected inner html of <table> and <table> in doc containing both tags to be equal, but got %s and %s",
|
||||
oTable,
|
||||
oBothTable)
|
||||
}
|
||||
|
||||
oBothA, _ := sBoth.Last().Html()
|
||||
if oBothA != oA {
|
||||
t.Errorf("Expected inner html of <a> and <a> in doc containing both tags to be equal, but got %s and %s",
|
||||
oA,
|
||||
oBothA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHtmlWithNonElementNode(t *testing.T) {
|
||||
const data = `
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This is <span>some</span><b>text</b>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
cases := map[string]func(*Selection, string) *Selection{
|
||||
"AfterHtml": (*Selection).AfterHtml,
|
||||
"AppendHtml": (*Selection).AppendHtml,
|
||||
"BeforeHtml": (*Selection).BeforeHtml,
|
||||
"PrependHtml": (*Selection).PrependHtml,
|
||||
"ReplaceWithHtml": (*Selection).ReplaceWithHtml,
|
||||
"SetHtml": (*Selection).SetHtml,
|
||||
}
|
||||
for nm, fn := range cases {
|
||||
// this test is only to make sure that the HTML parsing/manipulation
|
||||
// methods do not raise panics when executed over Selections that contain
|
||||
// non-Element nodes.
|
||||
t.Run(nm, func(t *testing.T) {
|
||||
doc := loadString(t, data)
|
||||
sel := doc.Find("p").Contents()
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
fn(sel, "<div></div>")
|
||||
}()
|
||||
|
||||
// print the resulting document in verbose mode
|
||||
h, err := OuterHtml(doc.Selection)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
t.Log(h)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ 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(compileMatcher(selector))
|
||||
}
|
||||
|
||||
return false
|
||||
return s.IsMatcher(compileMatcher(selector))
|
||||
}
|
||||
|
||||
// IsMatcher checks the current matched set of elements against a matcher and
|
||||
|
||||
@@ -31,6 +31,10 @@ func NewDocumentFromNode(root *html.Node) *Document {
|
||||
// NewDocument is a Document constructor that takes a string URL as argument.
|
||||
// It loads the specified document, parses it, and stores the root Document
|
||||
// node, ready to be manipulated.
|
||||
//
|
||||
// Deprecated: Use the net/http standard library package to make the request
|
||||
// and validate the response before calling goquery.NewDocumentFromReader
|
||||
// with the response's body.
|
||||
func NewDocument(url string) (*Document, error) {
|
||||
// Load the URL
|
||||
res, e := http.Get(url)
|
||||
@@ -40,10 +44,10 @@ func NewDocument(url string) (*Document, error) {
|
||||
return NewDocumentFromResponse(res)
|
||||
}
|
||||
|
||||
// NewDocumentFromReader returns a Document from a generic reader.
|
||||
// NewDocumentFromReader returns a Document from an io.Reader.
|
||||
// It returns an error as second value if the reader's data cannot be parsed
|
||||
// as html. It does *not* check if the reader is also an io.Closer, so the
|
||||
// provided reader is never closed by this call, it is the responsibility
|
||||
// as html. It does not check if the reader is also an io.Closer, the
|
||||
// provided reader is never closed by this call. It is the responsibility
|
||||
// of the caller to close it if required.
|
||||
func NewDocumentFromReader(r io.Reader) (*Document, error) {
|
||||
root, e := html.Parse(r)
|
||||
@@ -56,6 +60,8 @@ func NewDocumentFromReader(r io.Reader) (*Document, error) {
|
||||
// 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.
|
||||
//
|
||||
// Deprecated: Use goquery.NewDocumentFromReader with the response's body.
|
||||
func NewDocumentFromResponse(res *http.Response) (*Document, error) {
|
||||
if res == nil {
|
||||
return nil, errors.New("Response is nil")
|
||||
|
||||
@@ -121,6 +121,14 @@ func loadDoc(page string) *Document {
|
||||
return NewDocumentFromNode(node)
|
||||
}
|
||||
|
||||
func loadString(t *testing.T, doc string) *Document {
|
||||
d, err := NewDocumentFromReader(strings.NewReader(doc))
|
||||
if err != nil {
|
||||
t.Error("Failed to parse test document")
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func TestNewDocument(t *testing.T) {
|
||||
if f, e := os.Open("./testdata/page.html"); e != nil {
|
||||
t.Error(e.Error())
|
||||
|
||||
+14
-4
@@ -36,12 +36,22 @@ func NodeName(s *Selection) string {
|
||||
if s.Length() == 0 {
|
||||
return ""
|
||||
}
|
||||
switch n := s.Get(0); n.Type {
|
||||
return nodeName(s.Get(0))
|
||||
}
|
||||
|
||||
// nodeName returns the node name of the given html node.
|
||||
// See NodeName for additional details on behaviour.
|
||||
func nodeName(node *html.Node) string {
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch node.Type {
|
||||
case html.ElementNode, html.DoctypeNode:
|
||||
return n.Data
|
||||
return node.Data
|
||||
default:
|
||||
if n.Type >= 0 && int(n.Type) < len(nodeNames) {
|
||||
return nodeNames[n.Type]
|
||||
if node.Type >= 0 && int(node.Type) < len(nodeNames) {
|
||||
return nodeNames[node.Type]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user