Upstream PuerkitoBio/goquery state of the touched files before 81f35db25a
This commit is contained in:
1 parent
d6450709f9
commit
8592be9b0e
2 files changed
+467
No files matched your search
@@ -0,0 +1,212 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/andybalholm/cascadia"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document represents an HTML document to be manipulated. Unlike jQuery, which
|
||||||
|
// is loaded as part of a DOM document, and thus acts upon its containing
|
||||||
|
// document, GoQuery doesn't know which HTML document to act upon. So it needs
|
||||||
|
// to be told, and that's what the Document class is for. It holds the root
|
||||||
|
// document node to manipulate, and can make selections on this document.
|
||||||
|
type Document struct {
|
||||||
|
*Selection
|
||||||
|
Url *url.URL
|
||||||
|
rootNode *html.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocumentFromNode is a Document constructor that takes a root html Node
|
||||||
|
// as argument.
|
||||||
|
func NewDocumentFromNode(root *html.Node) *Document {
|
||||||
|
return newDocument(root, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return NewDocumentFromResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, 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)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return newDocument(root, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.Request == nil {
|
||||||
|
return nil, errors.New("Response.Request is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the HTML into nodes
|
||||||
|
root, e := html.Parse(res.Body)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and fill the document
|
||||||
|
return newDocument(root, res.Request.URL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneDocument creates a deep-clone of a document.
|
||||||
|
func CloneDocument(doc *Document) *Document {
|
||||||
|
return newDocument(cloneNode(doc.rootNode), doc.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private constructor, make sure all fields are correctly filled.
|
||||||
|
func newDocument(root *html.Node, url *url.URL) *Document {
|
||||||
|
// Create and fill the document
|
||||||
|
d := &Document{nil, url, root}
|
||||||
|
d.Selection = newSingleSelection(root, d)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection represents a collection of nodes matching some criteria. The
|
||||||
|
// initial Selection can be created by using Document.Find, and then
|
||||||
|
// manipulated using the jQuery-like chainable syntax and methods.
|
||||||
|
type Selection struct {
|
||||||
|
Nodes []*html.Node
|
||||||
|
document *Document
|
||||||
|
prevSel *Selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper constructor to create an empty selection
|
||||||
|
func newEmptySelection(doc *Document) *Selection {
|
||||||
|
return &Selection{nil, doc, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper constructor to create a selection of only one node
|
||||||
|
func newSingleSelection(node *html.Node, doc *Document) *Selection {
|
||||||
|
return &Selection{[]*html.Node{node}, doc, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matcher is an interface that defines the methods to match
|
||||||
|
// HTML nodes against a compiled selector string. Cascadia's
|
||||||
|
// Selector implements this interface.
|
||||||
|
type Matcher interface {
|
||||||
|
Match(*html.Node) bool
|
||||||
|
MatchAll(*html.Node) []*html.Node
|
||||||
|
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.
|
||||||
|
func compileMatcher(s string) Matcher {
|
||||||
|
cs, err := cascadia.Compile(s)
|
||||||
|
if err != nil {
|
||||||
|
return invalidMatcher{}
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
type singleMatcher struct {
|
||||||
|
Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFirst returns the first node that matches, or nil if none does. It uses
|
||||||
|
// the underlying Matcher's MatchFirst method if it provides one (cascadia-
|
||||||
|
// compiled matchers all do), avoiding building a result slice. Otherwise it
|
||||||
|
// falls back to MatchAll and returns its first element.
|
||||||
|
func (m singleMatcher) MatchFirst(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 {
|
||||||
|
return mm.MatchFirst(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m singleMatcher) MatchAll(n *html.Node) []*html.Node {
|
||||||
|
// Wrap MatchFirst's single result in a slice to respect the Matcher
|
||||||
|
// interface's MatchAll signature.
|
||||||
|
if node := m.MatchFirst(n); node != nil {
|
||||||
|
return []*html.Node{node}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalidMatcher is a Matcher that always fails to match.
|
||||||
|
type invalidMatcher struct{}
|
||||||
|
|
||||||
|
func (invalidMatcher) Match(n *html.Node) bool { return false }
|
||||||
|
func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil }
|
||||||
|
func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil }
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
package goquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/andybalholm/cascadia"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test helper functions and members
|
||||||
|
var doc *Document
|
||||||
|
var doc2 *Document
|
||||||
|
var doc3 *Document
|
||||||
|
var docB *Document
|
||||||
|
var docW *Document
|
||||||
|
|
||||||
|
func Doc() *Document {
|
||||||
|
if doc == nil {
|
||||||
|
doc = loadDoc("page.html")
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doc2() *Document {
|
||||||
|
if doc2 == nil {
|
||||||
|
doc2 = loadDoc("page2.html")
|
||||||
|
}
|
||||||
|
return doc2
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doc2Clone() *Document {
|
||||||
|
return CloneDocument(Doc2())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doc3() *Document {
|
||||||
|
if doc3 == nil {
|
||||||
|
doc3 = loadDoc("page3.html")
|
||||||
|
}
|
||||||
|
return doc3
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doc3Clone() *Document {
|
||||||
|
return CloneDocument(Doc3())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DocB() *Document {
|
||||||
|
if docB == nil {
|
||||||
|
docB = loadDoc("gotesting.html")
|
||||||
|
}
|
||||||
|
return docB
|
||||||
|
}
|
||||||
|
|
||||||
|
func DocW() *Document {
|
||||||
|
if docW == nil {
|
||||||
|
docW = loadDoc("gowiki.html")
|
||||||
|
}
|
||||||
|
return docW
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLength(t *testing.T, nodes []*html.Node, length int) {
|
||||||
|
if len(nodes) != length {
|
||||||
|
t.Errorf("Expected %d nodes, found %d.", length, len(nodes))
|
||||||
|
for i, n := range nodes {
|
||||||
|
t.Logf("Node %d: %+v.", i, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertClass(t *testing.T, sel *Selection, class string) {
|
||||||
|
if !sel.HasClass(class) {
|
||||||
|
t.Errorf("Expected node to have class %s, found %+v.", class, sel.Get(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPanic(t *testing.T) {
|
||||||
|
if e := recover(); e == nil {
|
||||||
|
t.Error("Expected a panic.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqual(t *testing.T, s1 *Selection, s2 *Selection) {
|
||||||
|
if s1 != s2 {
|
||||||
|
t.Error("Expected selection objects to be the same.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSelectionIs(t *testing.T, sel *Selection, is ...string) {
|
||||||
|
for i := 0; i < sel.Length(); i++ {
|
||||||
|
if !sel.Eq(i).Is(is[i]) {
|
||||||
|
t.Errorf("Expected node %d to be %s, found %+v", i, is[i], sel.Get(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSel(t *testing.T, sel *Selection) {
|
||||||
|
if testing.Verbose() {
|
||||||
|
h, err := sel.Html()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDoc(page string) *Document {
|
||||||
|
var f *os.File
|
||||||
|
var e error
|
||||||
|
|
||||||
|
if f, e = os.Open(fmt.Sprintf("./testdata/%s", page)); e != nil {
|
||||||
|
panic(e.Error())
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var node *html.Node
|
||||||
|
if node, e = html.Parse(f); e != nil {
|
||||||
|
panic(e.Error())
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
} else {
|
||||||
|
defer f.Close()
|
||||||
|
if node, e := html.Parse(f); e != nil {
|
||||||
|
t.Error(e.Error())
|
||||||
|
} else {
|
||||||
|
doc = NewDocumentFromNode(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDocumentFromReader(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
src string
|
||||||
|
err bool
|
||||||
|
sel string
|
||||||
|
cnt int
|
||||||
|
}{
|
||||||
|
0: {
|
||||||
|
src: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test</title>
|
||||||
|
<body>
|
||||||
|
<h1>Hi</h1>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
sel: "h1",
|
||||||
|
cnt: 1,
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
// Actually pretty hard to make html.Parse return an error
|
||||||
|
// based on content...
|
||||||
|
src: `<html><body><aef<eqf>>>qq></body></ht>`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
buf.Reset()
|
||||||
|
buf.WriteString(c.src)
|
||||||
|
|
||||||
|
d, e := NewDocumentFromReader(buf)
|
||||||
|
if (e != nil) != c.err {
|
||||||
|
if c.err {
|
||||||
|
t.Errorf("[%d] - expected error, got none", i)
|
||||||
|
} else {
|
||||||
|
t.Errorf("[%d] - expected no error, got %s", i, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.sel != "" {
|
||||||
|
s := d.Find(c.sel)
|
||||||
|
if s.Length() != c.cnt {
|
||||||
|
t.Errorf("[%d] - expected %d nodes, found %d", i, c.cnt, s.Length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDocumentFromResponseNil(t *testing.T) {
|
||||||
|
_, e := NewDocumentFromResponse(nil)
|
||||||
|
if e == nil {
|
||||||
|
t.Error("Expected error, got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue103(t *testing.T) {
|
||||||
|
d, err := NewDocumentFromReader(strings.NewReader("<html><title>Scientists Stored These Images in DNA—Then Flawlessly Retrieved Them</title></html>"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
text := d.Find("title").Text()
|
||||||
|
for i, r := range text {
|
||||||
|
t.Logf("%d: %d - %q\n", i, r, string(r))
|
||||||
|
}
|
||||||
|
t.Log(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user