Skip to content

Commit

Permalink
Use recursion instead of Enum.flat_map on find_selectors (#508)
Browse files Browse the repository at this point in the history
* Use Enum.reduce instead of Enum.flat_map on find_selectors

* Use recursion instead of Enum.reduce
  • Loading branch information
ypconstante authored Dec 27, 2023
1 parent 3fe2bc3 commit ff1d247
Showing 1 changed file with 57 additions and 77 deletions.
134 changes: 57 additions & 77 deletions lib/floki/finder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand Down

0 comments on commit ff1d247

Please sign in to comment.