Compare commits

...
93 Commits
Author SHA1 Message Date
Martin Angers 372d5bdaa0 Add changelog 2021-07-11 09:51:06 -04:00
Martin Angers 5dfda0e354 Merge branch 'jauderho-update-gomod' 2021-07-11 09:49:15 -04:00
Jauder Ho d6951d24bb Update go modules 2021-07-11 05:44:15 +00:00
Jauder HoandGitHub c09491e08b Update dependabot.yml 2021-07-11 05:37:44 +00:00
Jauder HoandGitHub 02e233c33a Create dependabot.yml 2021-07-11 05:36:11 +00:00
Martin Angers e11510b114 Update changelog for 1.7.0 2021-06-14 09:16:48 -04:00
Martin Angers a9cfd6779b Merge branch 'wip-i255-single-match' 2021-06-14 09:09:43 -04:00
Martin Angers b3953ef42a Finalize documentation 2021-06-14 09:06:47 -04:00
Martin Angers ee8a7e0a3a Test and benchmark the Single[Matcher] funcs 2021-06-13 15:02:43 -04:00
Martin Angers 0126a1fd88 Implement Single and SingleMatcher 2021-06-13 14:34:32 -04:00
Martin AngersandGitHub e1f2d60505 Merge pull request #381 from j0nimost/readme-fix
Fixed ExampleScrape README Sample
2021-06-04 14:51:04 -04:00
John Nyingi 1ba0ef7e9b Fixed ExampleScrape Sample 2021-06-04 21:37:05 +03:00
Martin Angers 795f67a092 Update doc badge 2021-05-19 11:06:14 -04:00
Martin Angers b67d192c91 Add funding file 2021-05-19 11:03:18 -04:00
Martin Angers bedb4e7a88 Resubmit build 2021-01-18 18:13:21 -05:00
Martin Angers a29eef7e71 Update go versions in travis tests 2021-01-18 17:11:31 -05:00
Martin Angers aa734f3932 add sourcehut build badge 2021-01-11 20:02:20 -05:00
Martin Angers 74649be271 fix go versions 2021-01-11 19:56:54 -05:00
Martin Angers 9d962837e1 try to fix build manifest 2021-01-11 19:24:59 -05:00
Martin Angers 7fc894a0ba add other go versions to build manifest 2021-01-11 19:17:56 -05:00
Martin Angers 171b719dd6 add fedora build manifest 2021-01-11 19:07:57 -05:00
Martin Angers f41c56001f Cut new release 2021-01-11 18:44:58 -05:00
Martin Angers 8a11cc402f Ignore non-element nodes when inserting html in a selection 2021-01-11 18:43:18 -05:00
Martin AngersandGitHub 70a02e53e3 Merge pull request #364 from santosh653/master
AddingPowerSupport_CI/Testing
2020-12-08 08:09:26 -05:00
santosh653andGitHub 3798c63c0d Update .travis.yml
Excluding unsupported versions for Power., ie go <=1.12.x
2020-12-07 23:31:38 -05:00
santosh653andGitHub 705a0066a5 Update .travis.yml
Adding power support & updating the go versions to >=1.13 as lower versions are not supported.,
2020-12-07 06:53:59 -05:00
Martin AngersandGitHub a48bafac4b Merge pull request #362 from vhodges/master
Update README.md, closes #361
2020-11-30 18:58:28 -05:00
Vince HodgesandGitHub af965a4d72 Update README.md 2020-11-30 15:54:55 -08:00
Martin Angers b3706f8903 update readme to prepare for new release 2020-10-08 09:16:43 -04:00
Martin AngersandGitHub 7ebd145bd7 Merge pull request #348 from thiemok/context
Send context to parseHtml. Closes #178 .
2020-10-08 09:10:18 -04:00
Martin Angers 4a687a672f add test cases to check html insertion order 2020-10-08 09:02:13 -04:00
thiemok 94c2530dd8 fix(manipulation): fix node insertion and caching issues
Node caching was still using the node type instead of the node name in a
few places. Updated those to also use the node name as the cache key.

ReplaceWithHtml and PrependHtml inserted parsed nodes in the wrong
order. Kept track of the correct parent to fix this.

Use correct parent flag for SetHtml
2020-10-07 20:04:45 +02:00
Martin Angers 5d7b0271f0 add tests to confirm erroneous behaviour 2020-10-07 10:54:01 -04:00
thiemok d89086ead1 fix(manipulation): address issues from code review of #235
* SetHml remains unchanged, see comment on the isParent param of
eachNodeHtml
* Removed unused functions
* Added documentation on eachNodeHtml
* Fixed node parsing cache keys and added test
* Refactored some tests for clarity
2020-09-28 15:37:51 +02:00
David Wilkinsandthiemok 99c8f1ecd5 Fixes #178 - Send context to parseHtml 2020-09-25 18:45:19 +02:00
Martin AngersandGitHub c942deb265 Merge pull request #347 from AbhigyanKumar/patch-1
Go syntax added
2020-09-24 09:53:53 -04:00
Abhigyan KumarandGitHub 0132bb8147 Go syntax
Go syntax colouring added.
2020-09-24 19:10:37 +05:30
Martin Angers 6f0f9d6b87 Merge branch 'eclipseo-fix_conversion_int_to_string' 2020-08-01 19:32:31 -04:00
Martin Angers cd12820764 use strconv.Itoa instead of fmt.Sprintf 2020-08-01 19:32:09 -04:00
Robert-André Mauchin 51a0e1a4bb Convert int to string using fmt.Sprintf
See https://github.com/golang/go/issues/32479

Fix #341.

Signed-off-by: Robert-André Mauchin <zebob.m@gmail.com>
2020-07-31 23:58:32 +02:00
Martin AngersandGitHub 89946c829f Merge pull request #330 from foolin/master
add [Pagser] to Related Projects
2020-04-19 11:21:48 -04:00
foolin 8b2a6e25d5 fix an to a 2020-04-19 23:18:31 +08:00
foolin 538dd4a913 add [Pagser] to Related Projects 2020-04-19 23:04:27 +08:00
Martin Angers 844d6bd46b doc: clarify how RemoveFiltered works 2020-02-24 18:34:32 -05:00
Martin Angers 1b16b92ddf doc: add 1.5.1 2020-02-04 18:10:22 -05:00
Martin AngersandGitHub 3d11372b35 Merge pull request #321 from akkumar/gomodupgrade
feat: upgrade gomod
2020-02-04 18:08:31 -05:00
Karthik K bd23f0c6e0 feat: upgrade gomod 2020-02-04 13:00:44 +05:30
Martin Angers 56ba1e6f08 add Geziyor to related projects 2019-12-09 15:00:45 -05:00
Martin Angers b17a2bbb6d remove go1.1 from travis, consistently fails 2019-10-15 18:09:08 -04:00
Martin Angers c70bac62f5 add Go versions to travis 2019-10-15 18:07:18 -04:00
Martin AngersandGitHub 8674076365 Merge pull request #310 from theophanous/colly-readme-patch
Updated colly project reference in README.md
2019-10-15 18:05:36 -04:00
Peter TheophanousandGitHub 6dcff85b2e Updated colly project reference in README.md 2019-10-14 21:58:31 +01:00
Martin AngersandGitHub 3dcf72e6c1 Merge pull request #280 from slotix/master
README.md: Add "Dataflow kit" to related projects.
2019-01-09 18:07:04 -05:00
Dmitry NarizhnykhandGitHub 22fd749627 README.md: Add "Dataflow kit" to related projects. 2019-01-09 22:44:10 +01:00
Martin AngersandGitHub 9b813b2309 Merge pull request #277 from tauraamui/tauraamui-update-readme
Update README.md to include BerryCMS in related
2019-01-08 07:15:36 -05:00
Adam Prakash LewisandGitHub 0255f16068 Update README.md to include BerryCMS in related 2019-01-08 09:36:23 +00:00
Martin Angers 2d2796f417 doc: 1.5 release with go modules 2018-11-15 15:42:34 -05:00
Martin AngersandGitHub 80f0084f4c Merge pull request #267 from Zaba505/master
Added Go Module support
2018-11-15 15:40:51 -05:00
Carson Derr 68fe358134 Add Go Module support 2018-11-14 19:30:47 -05:00
Martin AngersandGitHub 2af3d16e2b Merge pull request #261 from msoap/patch-1
Added Go 1.11 on travis CI
2018-10-14 13:58:06 -04:00
Sergey MudrikandGitHub 6c2cec4567 Added Go 1.11 on travis CI 2018-10-14 20:07:07 +03:00
Martin Angers 8311f594d7 readme: add ferret in projects using goquery 2018-10-02 20:21:05 -04:00
Martin Angers dc2ec5c7ca release 1.4.1 2018-06-07 11:06:10 -04:00
Martin AngersandGitHub 7757e27950 add examples for NewDocumentFromReader (#254) 2018-06-07 11:04:59 -04:00
Martin Angers ea1bc64a63 fix example indentation 2018-03-24 12:22:12 -04:00
Martin Angers dd77530fdb update examples to use the preferred NewDocumentFromReader approach 2018-03-24 12:12:09 -04:00
Martin AngersandGitHub a86ea07301 Merge pull request #247 from PuerkitoBio/wip-deprecate
Deprecate NewDocument and NewDocumentFromResponse. Closes #173 .
2018-03-24 12:02:17 -04:00
Martin Angers b4912d23e5 document the new release 2018-03-24 11:52:59 -04:00
Martin Angers 106fcbf95a deprecate NewDocument and NewDocumentFromResponse 2018-03-24 11:50:21 -04:00
Martin AngersandGitHub 61aa1975b1 Merge pull request #243 from gnulnx/master
Added gnulnx/goperf to Related Projects
2018-03-13 18:36:54 -04:00
John Furr 7a8f63580d Added gnulnx/goperf to Related Projects 2018-03-13 16:31:29 -04:00
Martin Angers 6285ceb1e9 last attempt to get go1.1 with travis 2018-03-10 15:43:14 -05:00
Martin Angers b55d492104 travis go1.1 gets 1.7 for some reason 2018-03-10 15:37:49 -05:00
Martin Angers ef34a70bad add 1.10 in travis config 2018-03-10 15:30:08 -05:00
Martin AngersandGitHub 5819122076 add Support section to readme (#241) 2018-03-10 15:26:54 -05:00
Martin AngersandGitHub 09540e5659 Merge pull request #234 from PuerkitoBio/wip-slice-end
Add ToEnd constant to use as special end index in Slice, closes #232.
2018-01-28 14:56:50 -05:00
Martin Angers 46f28d87ad document addition of ToEnd 2018-01-28 14:43:53 -05:00
Martin Angers 520f19d599 add ToEnd marker to Slice until the end of the Selection 2018-01-28 14:41:06 -05:00
Martin AngersandGitHub 2324bda66f Merge pull request #231 from Strum355/patch-1
Removed unnecessary conditional check
2018-01-18 09:26:56 -05:00
Noah Santschi-CooneyandGitHub 3855699cc0 Removed unnecessary if
IsMatcher returns false if len(s.Nodes) !> 0, so Is was double checking if len(s.Nodes) > 0
2018-01-17 13:49:09 +00:00
Martin Angers a41cf096e6 update changelog 2018-01-11 08:54:59 -05:00
Martin Angers 7d9aa5c1e6 misc/git: remove excessively verbose lint tools 2018-01-11 08:49:46 -05:00
Martin Angers c56812c109 fix godoc comment to comply with golint 2018-01-11 08:48:49 -05:00
Martin AngersandGitHub cab5e2c9d6 Merge pull request #229 from davidjwilkins/feature-226
Fixes #226 - Added the AddBack, AddBackFiltered, and AddBackMatcher methods.
2018-01-11 08:45:45 -05:00
David Wilkins 802bece6d0 Move deprecated notice to end of comment 2018-01-10 21:36:54 -08:00
David Wilkins 499bd47957 Fixes #226 - Added the AddBack, AddBackFiltered, and AddBackMatcher methods. Changed AndSelf to simply be an alias for AddBack, and added a deprecation notice to AddBack (as with https://api.jquery.com/andself/) 2018-01-09 20:34:18 -08:00
Martin AngersandGitHub bc4e06eb07 Merge pull request #221 from y-yagi/patch-1
Add go1.9 to travis
2017-12-06 07:16:06 -05:00
Yuji YaginumaandGitHub d8ab9abbf4 Add go1.9 to travis 2017-12-06 18:46:13 +09:00
Martin AngersandGitHub 1a71cc719d Merge pull request #215 from kataras/patch-1
README.md: Add colly - Scraping Framework to the Related Projects
2017-10-07 10:12:28 -04:00
Gerasimos (Makis) MaropoulosandGitHub 4b3a6d701e README.md: Add colly - Scraping Framework to the Related Projects 2017-10-07 07:57:40 +03:00
Martin AngersandGitHub 63e7f870c2 Merge pull request #211 from 38elements/fix-typo
Fix typo
2017-09-29 06:56:09 -04:00
38elementsandGitHub 109d3a6a56 Fix typo 2017-09-29 18:51:07 +09:00
Martin Angers ce645ea5e6 use svg travis badge, master branch 2017-08-16 09:38:31 -04:00
24 changed files with 926 additions and 90 deletions
+104
View File
@@ -0,0 +1,104 @@
image: fedora/latest
packages:
- tar
environment:
GOROOT: /tmp/go
GOPATH: /home/build
GOBIN: /home/build/bin
sources:
- https://github.com/PuerkitoBio/goquery
tasks:
- setup: |
mkdir bin
mkdir -p src/github.com/PuerkitoBio
mv goquery src/github.com/PuerkitoBio/
- go1_7: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.7.6.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_8: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.8.7.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_9: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.9.7.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_10: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.10.8.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_11: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.11.13.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_12: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.12.17.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_13: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.13.15.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_14: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.14.12.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_15: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.15.5.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
- go1_16: |
cd /tmp
rm -rf go
curl -Lo golang.tar.gz https://golang.org/dl/go1.16beta1.linux-amd64.tar.gz
tar xvf golang.tar.gz
cd /home/build/src/github.com/PuerkitoBio/goquery
/tmp/go/bin/go version
/tmp/go/bin/go get -t .
/tmp/go/bin/go test -v
+2
View File
@@ -0,0 +1,2 @@
github: [mna]
custom: ["https://www.buymeacoffee.com/mna"]
+7
View File
@@ -0,0 +1,7 @@
version: 2
updates:
# Maintain dependencies for Go
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
+25 -6
View File
@@ -1,12 +1,31 @@
arch:
- amd64
- ppc64le
language: go
go:
- 1.1.x
- 1.2.x
- 1.3.x
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
- tip
jobs:
exclude:
- 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 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2012-2016, Martin Angers & Contributors
Copyright (c) 2012-2021, 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:
+65 -13
View File
@@ -1,6 +1,9 @@
# goquery - a little like that j-thing, only in Go
[![build status](https://secure.travis-ci.org/PuerkitoBio/goquery.png)](http://travis-ci.org/PuerkitoBio/goquery) [![GoDoc](https://godoc.org/github.com/PuerkitoBio/goquery?status.png)](http://godoc.org/github.com/PuerkitoBio/goquery) [![Sourcegraph Badge](https://sourcegraph.com/github.com/PuerkitoBio/goquery/-/badge.svg)](https://sourcegraph.com/github.com/PuerkitoBio/goquery?badge)
[![builds.sr.ht status](https://builds.sr.ht/~mna/goquery/commits/fedora.yml.svg)](https://builds.sr.ht/~mna/goquery/commits/fedora.yml?)
[![build status](https://secure.travis-ci.org/PuerkitoBio/goquery.svg?branch=master)](http://travis-ci.org/PuerkitoBio/goquery)
[![Go Reference](https://pkg.go.dev/badge/github.com/PuerkitoBio/goquery.svg)](https://pkg.go.dev/github.com/PuerkitoBio/goquery)
[![Sourcegraph Badge](https://sourcegraph.com/github.com/PuerkitoBio/goquery/-/badge.svg)](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.
@@ -15,11 +18,12 @@ 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
Please note that because of the net/html dependency, goquery requires Go1.1+.
Please note that because of the net/html dependency, goquery requires Go1.1+ and is tested on Go1.7+.
$ go get github.com/PuerkitoBio/goquery
@@ -37,12 +41,22 @@ 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-07-11 (v1.7.1)** : Update go.mod dependencies and add dependabot config (thanks [@jauderho](https://github.com/jauderho)).
* **2021-06-14 (v1.7.0)** : Add `Single` and `SingleMatcher` functions to optimize first-match selection (thanks [@gdollardollar](https://github.com/gdollardollar)).
* **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).
* **2016-08-28 (v1.0.1)** : Optimize performance for large documents.
* **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`).
* **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 [doc][] 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].
@@ -71,7 +85,7 @@ jQuery often has many variants for the same function (no argument, a selector st
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].
The complete [package 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. 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):
@@ -92,23 +106,34 @@ 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)
}
// 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)
})
doc.Find(".left-content article .post-title").Each(func(i int, s *goquery.Selection) {
// For each item found, get the title
title := s.Find("a").Text()
fmt.Printf("Review %d: %s\n", i, title)
})
}
func main() {
@@ -121,6 +146,31 @@ 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.
- [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.
* Sponsor the developer
- See the Github Sponsor button at the top of the repo on github
- or via BuyMeACoffee.com, below
<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
@@ -133,11 +183,13 @@ The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia'
[bsd]: http://opensource.org/licenses/BSD-3-Clause
[golic]: http://golang.org/LICENSE
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
[doc]: http://godoc.org/github.com/PuerkitoBio/goquery
[doc]: https://pkg.go.dev/github.com/PuerkitoBio/goquery
[index]: http://api.jquery.com/index/
[gonet]: https://github.com/golang/net/
[html]: http://godoc.org/golang.org/x/net/html
[html]: https://pkg.go.dev/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
[goq]: https://github.com/andrewstuart/goq
[thiemok]: https://github.com/thiemok
[djw]: https://github.com/davidjwilkins
+23 -2
View File
@@ -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])
+22
View File
@@ -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 -1
View File
@@ -231,6 +231,6 @@ func BenchmarkEnd(b *testing.B) {
}
}
if n != 373 {
b.Fatalf("wnat 373, got %d", n)
b.Fatalf("want 373, got %d", n)
}
}
+2 -1
View File
@@ -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++ {
+20
View File
@@ -2,6 +2,8 @@ package goquery
import (
"testing"
"github.com/andybalholm/cascadia"
)
func BenchmarkFind(b *testing.B) {
@@ -800,3 +802,21 @@ func BenchmarkClosestNodes(b *testing.B) {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkSingleMatcher(b *testing.B) {
doc := Doc()
multi := cascadia.MustCompile(`div`)
single := SingleMatcher(multi)
b.ResetTimer()
b.Run("multi", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = doc.FindMatcher(multi)
}
})
b.Run("single", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = doc.FindMatcher(single)
}
})
}
+3 -3
View File
@@ -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)
+81 -1
View File
@@ -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,70 @@ 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
}
func ExampleSingle() {
html := `
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>
`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
log.Fatal(err)
}
// By default, the selector string selects all matching nodes
multiSel := doc.Find("div")
fmt.Println(multiSel.Text())
// Using goquery.Single, only the first match is selected
singleSel := doc.FindMatcher(goquery.Single("div"))
fmt.Println(singleSel.Text())
// Output:
// 123
// 1
}
+24
View File
@@ -41,6 +41,30 @@ func (s *Selection) AddNodes(nodes ...*html.Node) *Selection {
// AndSelf adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
// Deprecated: This function has been deprecated and is now an alias for AddBack().
func (s *Selection) AndSelf() *Selection {
return s.AddBack()
}
// AddBack adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
func (s *Selection) AddBack() *Selection {
return s.AddSelection(s.prevSel)
}
// AddBackFiltered reduces the previous set of elements on the stack to those that
// match the selector string, and adds them to the current set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackFiltered(selector string) *Selection {
return s.AddSelection(s.prevSel.Filter(selector))
}
// AddBackMatcher reduces the previous set of elements on the stack to those that match
// the mateher, and adds them to the curernt set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackMatcher(m Matcher) *Selection {
return s.AddSelection(s.prevSel.FilterMatcher(m))
}
+22
View File
@@ -94,3 +94,25 @@ func TestAndSelfRollback(t *testing.T) {
sel2 := sel.Find("a").AndSelf().End().End()
assertEqual(t, sel, sel2)
}
func TestAddBack(t *testing.T) {
sel := Doc().Find(".span12").Last().AddBack()
assertLength(t, sel.Nodes, 2)
}
func TestAddBackRollback(t *testing.T) {
sel := Doc().Find(".pvk-content")
sel2 := sel.Find("a").AddBack().End().End()
assertEqual(t, sel, sel2)
}
func TestAddBackFiltered(t *testing.T) {
sel := Doc().Find(".span12, .footer").Find("h1").AddBackFiltered(".footer")
assertLength(t, sel.Nodes, 2)
}
func TestAddBackFilteredRollback(t *testing.T) {
sel := Doc().Find(".span12, .footer")
sel2 := sel.Find("h1").AddBackFiltered(".footer").End().End()
assertEqual(t, sel, sel2)
}
+8
View File
@@ -0,0 +1,8 @@
module github.com/PuerkitoBio/goquery
require (
github.com/andybalholm/cascadia v1.2.0
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
)
go 1.13
+10
View File
@@ -0,0 +1,10 @@
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+140 -34
View File
@@ -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
@@ -270,13 +305,23 @@ func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
return s.Remove()
}
// Set 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)...)
// SetHtml sets the html content of each element in the selection to
// specified html string.
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)
}
})
}
// Set the content of each element in the selection to specified content. The
// provided text string is escaped.
// SetText sets the content of each element in the selection to specified content.
// The provided text string is escaped.
func (s *Selection) SetText(text string) *Selection {
return s.SetHtml(html.EscapeString(text))
}
@@ -328,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-
@@ -381,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 {
@@ -451,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
@@ -492,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
@@ -571,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
}
+226
View File
@@ -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)
})
}
}
-15
View File
@@ -18,21 +18,6 @@ gosimple $(go list ./... | grep -v /vendor/)
echo "<<< gosimple"
echo
echo ">>> staticcheck"
staticcheck $(go list ./... | grep -v /vendor/)
echo "<<< staticcheck"
echo
echo ">>> unused"
unused $(go list ./... | grep -v /vendor/)
echo "<<< unused"
echo
echo ">>> gas"
gas $(find . -name "*.go" | grep -v /vendor/ | grep -v '_test.go$')
echo "<<< gas"
echo
# Check for gofmt problems and report if any.
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$' | grep -v /vendor/)
[ -z "$gofiles" ] && echo "EXIT $vetres" && exit $vetres
+1 -5
View File
@@ -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
+72 -4
View File
@@ -7,7 +7,6 @@ import (
"net/url"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
@@ -31,6 +30,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 +43,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 +59,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")
@@ -116,6 +121,45 @@ type Matcher interface {
Filter([]*html.Node) []*html.Node
}
// Single compiles a selector string to a Matcher that stops after the first
// match is found.
//
// By default, Selection.Find and other functions that accept a selector string
// to select nodes will use all matches corresponding to that selector. By
// using the Matcher returned by Single, at most the first match will be
// selected.
//
// For example, those two statements are semantically equivalent:
//
// sel1 := doc.Find("a").First()
// sel2 := doc.FindMatcher(goquery.Single("a"))
//
// The one using Single is optimized to be potentially much faster on large
// documents.
//
// Only the behaviour of the MatchAll method of the Matcher interface is
// altered compared to standard Matchers. This means that the single-selection
// property of the Matcher only applies for Selection methods where the Matcher
// is used to select nodes, not to filter or check if a node matches the
// Matcher - in those cases, the behaviour of the Matcher is unchanged (e.g.
// FilterMatcher(Single("div")) will still result in a Selection with multiple
// "div"s if there were many "div"s in the Selection to begin with).
func Single(selector string) Matcher {
return singleMatcher{compileMatcher(selector)}
}
// SingleMatcher returns a Matcher matches the same nodes as m, but that stops
// after the first match is found.
//
// See the documentation of function Single for more details.
func SingleMatcher(m Matcher) Matcher {
if _, ok := m.(singleMatcher); ok {
// m is already a singleMatcher
return m
}
return singleMatcher{m}
}
// 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.
@@ -127,6 +171,30 @@ func compileMatcher(s string) Matcher {
return cs
}
type singleMatcher struct {
Matcher
}
func (m singleMatcher) MatchAll(n *html.Node) []*html.Node {
// Optimized version - stops finding at the first match (cascadia-compiled
// matchers all use this code path).
if mm, ok := m.Matcher.(interface{ MatchFirst(*html.Node) *html.Node }); ok {
node := mm.MatchFirst(n)
if node == nil {
return nil
}
return []*html.Node{node}
}
// Fallback version, for e.g. test mocks that don't provide the MatchFirst
// method.
nodes := m.Matcher.MatchAll(n)
if len(nodes) > 0 {
return nodes[:1:1]
}
return nil
}
// invalidMatcher is a Matcher that always fails to match.
type invalidMatcher struct{}
+53
View File
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
@@ -121,6 +122,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())
@@ -200,3 +209,47 @@ func TestIssue103(t *testing.T) {
}
t.Log(text)
}
func TestSingle(t *testing.T) {
data := `
<html>
<body>
<div class="b">1</div>
<div class="a">2</div>
<div class="a">3</div>
<p class="b">4</p>
</body>
</html>
`
doc, err := NewDocumentFromReader(strings.NewReader(data))
if err != nil {
t.Fatal(err)
}
text := doc.FindMatcher(Single("div")).Text()
if text != "1" {
t.Fatalf("want %q, got %q", "1", text)
}
// Verify semantic equivalence
sel1 := doc.Find("div").First()
sel2 := doc.FindMatcher(Single("div"))
if sel1.Text() != sel2.Text() {
t.Fatalf("want sel1 to equal sel2")
}
// Here, the Single has no effect as the selector is used to filter
// from the existing selection, not to find nodes in the document.
divs := doc.Find("div")
text = divs.FilterMatcher(Single(".a")).Text()
if text != "23" {
t.Fatalf("want %q, got %q", "23", text)
}
classA := cascadia.MustCompile(".a")
classB := cascadia.MustCompile(".b")
text = doc.FindMatcher(classB).AddMatcher(SingleMatcher(classA)).Text()
if text != "142" {
t.Fatalf("want %q, got %q", "142", text)
}
}
+14 -4
View File
@@ -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 ""
}