From df88dce547921d4dbdb0eeecd1a600e98f34ddc9 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Tue, 2 Jun 2026 14:15:45 +0200 Subject: [PATCH] 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. --- filter.go | 26 +++++++------------------- type.go | 23 ++++++++++++++++------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/filter.go b/filter.go index d72179e..7d2e8f9 100644 --- a/filter.go +++ b/filter.go @@ -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 } diff --git a/type.go b/type.go index 6646c14..dd3778d 100644 --- a/type.go +++ b/type.go @@ -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 }