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:
jvoisin
2026-06-02 14:15:45 +02:00
parent 021fb11910
commit df88dce547
2 changed files with 23 additions and 26 deletions
+7 -19
View File
@@ -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
}
+16 -7
View File
@@ -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
}