diff --git a/lib/floki/finder.ex b/lib/floki/finder.ex index 3c893019..39df3871 100644 --- a/lib/floki/finder.ex +++ b/lib/floki/finder.ex @@ -45,8 +45,10 @@ defmodule Floki.Finder do results = tree.node_ids |> Enum.reverse() - |> get_nodes(tree) - |> Enum.flat_map(fn html_node -> get_matches_for_selectors(tree, html_node, selectors) end) + |> Enum.reduce([], fn node_id, acc -> + get_matches_for_selectors(tree, node_id, selectors, acc) + end) + |> Enum.reverse() |> Enum.uniq() {tree, results} @@ -58,95 +60,73 @@ defmodule Floki.Finder do |> Selector.Parser.parse() end - defp get_matches_for_selectors(tree, html_node, selectors) do - Enum.flat_map(selectors, fn selector -> get_matches(tree, html_node, selector) end) - end - - defp get_matches(tree, html_node, selector = %Selector{combinator: nil}) do - if selector_match?(tree, html_node, selector) do - [html_node] - else - [] - end - end - - defp get_matches(tree, html_node, selector = %Selector{combinator: combinator}) do - if selector_match?(tree, html_node, selector) do - traverse_with(combinator, tree, [html_node]) - else - [] - end - end - - defp selector_match?(tree, html_node, selector) do - Selector.match?(html_node, selector, tree) + defp get_matches_for_selectors(tree, node_id, selectors, acc) do + Enum.reduce(selectors, acc, fn selector, acc -> + traverse_with(selector, tree, node_id, acc) + end) end # The stack serves as accumulator when there is another combinator to traverse. # So the scope of one combinator is the stack (or acc) or the parent one. - defp traverse_with(_, _, []), do: [] - defp traverse_with(nil, _, results), do: results - - defp traverse_with(%Selector.Combinator{match_type: :child, selector: s}, tree, stack) do - results = - Enum.flat_map(stack, fn html_node -> - nodes = - html_node.children_nodes_ids - |> Enum.reverse() - |> get_nodes(tree) - - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) - end) + defp traverse_with(nil, _, html_node, acc), do: [html_node | acc] + defp traverse_with(_, _, [], acc), do: acc - traverse_with(s.combinator, tree, results) + defp traverse_with(selector, tree, [node_id | rest], acc) do + traverse_with( + selector, + tree, + rest, + traverse_with(selector, tree, node_id, acc) + ) end - defp traverse_with(%Selector.Combinator{match_type: :sibling, selector: s}, tree, stack) do - results = - Enum.flat_map(stack, fn html_node -> - # It treats sibling as list to easily ignores those that didn't match - sibling_id = - html_node - |> get_siblings(tree) - |> Enum.take(1) - - nodes = get_nodes(sibling_id, tree) - - # Finally, try to match those siblings with the selector - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) - end) + defp traverse_with(%Selector{combinator: combinator} = selector, tree, node_id, acc) do + html_node = get_node(node_id, tree) - traverse_with(s.combinator, tree, results) + if Selector.match?(html_node, selector, tree) do + traverse_with(combinator, tree, html_node, acc) + else + acc + end end - defp traverse_with(%Selector.Combinator{match_type: :general_sibling, selector: s}, tree, stack) do - results = - Enum.flat_map(stack, fn html_node -> - sibling_ids = get_siblings(html_node, tree) - - nodes = get_nodes(sibling_ids, tree) - - # Finally, try to match those siblings with the selector - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) - end) - - traverse_with(s.combinator, tree, results) + defp traverse_with( + %Selector.Combinator{match_type: :child, selector: s}, + tree, + html_node, + acc + ) do + traverse_with(s, tree, Enum.reverse(html_node.children_nodes_ids), acc) + end + + defp traverse_with( + %Selector.Combinator{match_type: :sibling, selector: s}, + tree, + html_node, + acc + ) do + case get_siblings(html_node, tree) do + [sibling_id | _] -> traverse_with(s, tree, sibling_id, acc) + _ -> acc + end end - defp traverse_with(%Selector.Combinator{match_type: :descendant, selector: s}, tree, stack) do - results = - Enum.flat_map(stack, fn html_node -> - ids_to_match = get_descendant_ids(html_node.node_id, tree) - nodes = get_nodes(ids_to_match, tree) - - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) - end) - - traverse_with(s.combinator, tree, results) + defp traverse_with( + %Selector.Combinator{match_type: :general_sibling, selector: s}, + tree, + html_node, + acc + ) do + traverse_with(s, tree, get_siblings(html_node, tree), acc) end - defp get_nodes(ids, tree) do - Enum.map(ids, fn id -> Map.get(tree.nodes, id) end) + defp traverse_with( + %Selector.Combinator{match_type: :descendant, selector: s}, + tree, + html_node, + acc + ) do + traverse_with(s, tree, get_descendant_ids(html_node.node_id, tree), acc) end defp get_node(id, tree) do