diff --git a/traversal.go b/traversal.go index c495eea..948e68a 100644 --- a/traversal.go +++ b/traversal.go @@ -51,12 +51,19 @@ func (s *Selection) FindSelection(sel *Selection) *Selection { // Selection, filtered by some nodes. It returns a new Selection object // containing these matched elements. func (s *Selection) FindNodes(nodes ...*html.Node) *Selection { - return pushStack(s, mapNodes(nodes, func(i int, n *html.Node) []*html.Node { - if sliceContains(s.Nodes, n) { - return []*html.Node{n} + // Each source node maps to at most itself (when it is a descendant of the + // current selection), so there is nothing to deduplicate across distinct + // sources. Skip mapNodes' callback indirection and append matches directly, + // while still discarding duplicate input nodes so the resulting Selection + // stays a proper set. The cheap isInSlice check runs first so duplicate + // inputs short-circuit before the more expensive sliceContains tree walk. + var result []*html.Node + for _, n := range nodes { + if !isInSlice(result, n) && sliceContains(s.Nodes, n) { + result = append(result, n) } - return nil - })) + } + return pushStack(s, result) } // Contents gets the children of each element in the Selection, diff --git a/traversal_test.go b/traversal_test.go index 04383a4..627bcc5 100644 --- a/traversal_test.go +++ b/traversal_test.go @@ -36,6 +36,16 @@ func TestFindBig(t *testing.T) { assertLength(t, sel3.Nodes, 248) } +func TestFindNodesDuplicateInput(t *testing.T) { + doc := DocW() + sel := doc.Find("body") + span := doc.Find("span").Nodes[0] + // Duplicate input nodes must be discarded so the resulting Selection + // stays a proper set. + sel2 := sel.FindNodes(span, span) + assertLength(t, sel2.Nodes, 1) +} + func TestChainedFind(t *testing.T) { sel := Doc().Find("div.hero-unit").Find(".row-fluid") assertLength(t, sel.Nodes, 4)