mirror of
https://github.com/PuerkitoBio/goquery.git
synced 2026-10-19 16:15:33 +00:00
Refactor first-match optimization into singleMatcher
Move the "stop at first match" optimization out of HasMatcher and into singleMatcher, where it can be reused elsewhere. singleMatcher now implements MatchFirst, which calls the underlying Matcher's MatchFirst if it provides one (cascadia-compiled matchers all do), otherwise falls back to MatchAll and returns its first element. singleMatcher.MatchAll wraps MatchFirst's result in a single-item slice to respect the Matcher interface, so external SingleMatcher() users get the slice signature while internal callers can avoid that allocation. HasMatcher now constructs a singleMatcher directly to call MatchFirst without a type assertion, removing the duplicated probe branch.
This commit is contained in:
@@ -88,27 +88,15 @@ func (s *Selection) Has(selector string) *Selection {
|
||||
func (s *Selection) HasMatcher(m Matcher) *Selection {
|
||||
result := make([]*html.Node, 0, len(s.Nodes))
|
||||
|
||||
// If the matcher can probe for the first match in a subtree without
|
||||
// building a result slice, use that: it avoids a throwaway one-element
|
||||
// slice allocation for every matching child subtree.
|
||||
if mf, ok := m.(interface {
|
||||
MatchFirst(*html.Node) *html.Node
|
||||
}); ok {
|
||||
for _, n := range s.Nodes {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if c.Type == html.ElementNode && mf.MatchFirst(c) != nil {
|
||||
result = append(result, n)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return pushStack(s, result)
|
||||
}
|
||||
|
||||
m = SingleMatcher(m)
|
||||
// Manually create a singleMatcher (rather than using SingleMatcher, which
|
||||
// returns a Matcher interface) so we can call its MatchFirst method
|
||||
// directly. MatchFirst probes for the first match in a subtree without
|
||||
// building a result slice, avoiding a throwaway one-element slice
|
||||
// allocation for every matching child subtree.
|
||||
sm := singleMatcher{m}
|
||||
for _, n := range s.Nodes {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if c.Type == html.ElementNode && len(m.MatchAll(c)) > 0 {
|
||||
if c.Type == html.ElementNode && sm.MatchFirst(c) != nil {
|
||||
result = append(result, n)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -175,22 +175,31 @@ type singleMatcher struct {
|
||||
Matcher
|
||||
}
|
||||
|
||||
func (m singleMatcher) MatchAll(n *html.Node) []*html.Node {
|
||||
// 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 {
|
||||
node := mm.MatchFirst(n)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return []*html.Node{node}
|
||||
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[:1:1]
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user