Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use recursion instead of Enum.flat_map on find_selectors #508

Merged
merged 2 commits into from
Dec 27, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading