From 8592be9b0e4f7941a14202f0ecdafb1668ca88bd Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 07:54:24 +0200 Subject: [PATCH] Upstream PuerkitoBio/goquery state of the touched files before 81f35db25a --- type.go | 212 ++++++++++++++++++++++++++++++++++++++++++ type_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+) create mode 100644 type.go create mode 100644 type_test.go diff --git a/type.go b/type.go new file mode 100644 index 0000000..dd3778d --- /dev/null +++ b/type.go @@ -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 } diff --git a/type_test.go b/type_test.go new file mode 100644 index 0000000..84080a9 --- /dev/null +++ b/type_test.go @@ -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: ` + + +Test + +

Hi

+ +`, + sel: "h1", + cnt: 1, + }, + 1: { + // Actually pretty hard to make html.Parse return an error + // based on content... + src: `>>qq>`, + }, + } + 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("Scientists Stored These Images in DNA—Then Flawlessly Retrieved Them")) + 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 := ` + + +
1
+
2
+
3
+

4

+ + +` + 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) + } +}