Current section

Files

Jump to
meeseeks lib meeseeks select.ex
Raw

lib/meeseeks/select.ex

defmodule Meeseeks.Select do
@moduledoc false
alias Meeseeks.{Accumulator, Context, Document, Error, Result, Selector}
@return? Context.return_key()
@matches Context.matches_key()
@nodes Context.nodes_key()
@type queryable :: Document.t() | Result.t()
@type selectors :: Selector.t() | [Selector.t()]
# All
@spec fetch_all(queryable, selectors, Context.t()) :: {:ok, [Result.t()]} | {:error, Error.t()}
def fetch_all(queryable, selectors, context) do
case all(queryable, selectors, context) do
[] -> {:error, Error.new(:select, :no_match)}
results -> {:ok, results}
end
end
@spec all(queryable, selectors, Context.t()) :: [Result.t()]
def all(queryable, selectors, context) do
context = Context.add_accumulator(context, %Accumulator.All{})
select(queryable, selectors, context)
end
# One
@spec fetch_one(queryable, selectors, Context.t()) :: {:ok, Result.t()} | {:error, Error.t()}
def fetch_one(queryable, selectors, context) do
case one(queryable, selectors, context) do
nil -> {:error, Error.new(:select, :no_match)}
result -> {:ok, result}
end
end
@spec one(queryable, selectors, Context.t()) :: Result.t() | nil
def one(queryable, selectors, context) do
context = Context.add_accumulator(context, %Accumulator.One{})
select(queryable, selectors, context)
end
# Select
@spec select(queryable, selectors, Context.t()) :: any
def select(queryable, selectors, context)
def select(_queryable, string, _context) when is_binary(string) do
raise Error.new(:select, :invalid_selectors, %{
description:
"Expected selectors, received a string- did you mean to wrap the string in the `css` or `xpath` macro?",
string: string
})
end
def select(queryable, selectors, context) do
context =
context
|> Context.prepare_for_selection()
|> Context.ensure_accumulator!()
walk(queryable, selectors, context)
end
# Walk
defp walk(%Document{} = document, selectors, context) do
document
|> Document.get_nodes()
|> walk_nodes(document, selectors, context)
|> Context.return_accumulator()
end
defp walk(%Result{id: id, document: document}, selectors, context) do
ids = [id | Document.descendants(document, id)]
document
|> Document.get_nodes(ids)
|> walk_nodes(document, selectors, context)
|> Context.return_accumulator()
end
# Walk Nodes
defp walk_nodes(_, _, _, %{@return? => true} = context) do
context
end
defp walk_nodes([], document, _, %{@matches => matches} = context) when map_size(matches) > 0 do
filter_and_walk(matches, document, context)
end
defp walk_nodes([], _, _, context) do
context
end
defp walk_nodes(_, document, [], %{@matches => matches} = context) when map_size(matches) > 0 do
filter_and_walk(matches, document, context)
end
defp walk_nodes(_, _, [], context) do
context
end
defp walk_nodes(nodes, document, selectors, context) do
context =
Enum.reduce(nodes, Context.clear_matches(context), fn node, context ->
walk_node(node, document, selectors, context)
end)
walk_nodes([], document, [], context)
end
# Walk Node
defp walk_node(_, _, _, %{@return? => true} = context) do
context
end
defp walk_node(nil, _, _, context) do
context
end
defp walk_node(_, _, [], context) do
context
end
defp walk_node(node, document, [selector | selectors], context) do
context = walk_node(node, document, selector, context)
walk_node(node, document, selectors, context)
end
defp walk_node(%Document.Element{} = node, document, %Selector.Element{} = selector, context) do
case Selector.match(selector, node, document, context) do
false -> context
{false, context} -> context
true -> handle_match(node, document, selector, context)
{true, context} -> handle_match(node, document, selector, context)
end
end
defp walk_node(_node, _document, %Selector.Element{} = _selector, context) do
context
end
defp walk_node(node, document, selector, context) do
case Selector.match(selector, node, document, context) do
false -> context
{false, context} -> context
true -> handle_match(node, document, selector, context)
{true, context} -> handle_match(node, document, selector, context)
end
end
# Handle Match
defp handle_match(node, document, selector, context) do
combinator = Selector.combinator(selector)
filters = Selector.filters(selector)
case {combinator, filters} do
# Add to accumulator if there is no combinator and no filters
{nil, nil} ->
Context.add_to_accumulator(context, document, node.id)
# Add to accumulator if there is no combinator and empty filters
{nil, []} ->
Context.add_to_accumulator(context, document, node.id)
# Add to @matches so all matching nodes can be filtered before to
# continuing
_ ->
Context.add_to_matches(context, selector, node)
end
end
# Filter and Walk
defp filter_and_walk(matching, document, context) do
# For each set of nodes matching a selector
Enum.reduce(matching, context, fn {selector, nodes}, context ->
filters = Selector.filters(selector)
nodes = Enum.reverse(nodes)
# Filter the nodes based on the selector's filters
{nodes, context} = filter_nodes(filters, nodes, document, context)
walk_filtered(nodes, document, selector, context)
end)
end
defp walk_filtered(nodes, document, selector, context) do
# For each remaining node either
Enum.reduce(nodes, context, fn node, context ->
case Selector.combinator(selector) do
# Add the node to the accumulator if there is no combinator
nil ->
Context.add_to_accumulator(context, document, node.id)
# Or walk the combinator
combinator ->
walk_combinator(combinator, node, document, context)
end
end)
end
defp filter_nodes(nil, nodes, _, context) do
{nodes, context}
end
defp filter_nodes([], nodes, _, context) do
{nodes, context}
end
defp filter_nodes(filters, nodes, document, context) when is_list(filters) do
Enum.reduce(filters, {nodes, context}, fn filter, {nodes, context} ->
filter_nodes(filter, nodes, document, context)
|> reverse_filtered_nodes()
end)
end
defp filter_nodes(filter, nodes, document, context) do
context = Map.put(context, @nodes, nodes)
Enum.reduce(nodes, {[], context}, fn node, {nodes, context} ->
case Selector.match(filter, node, document, context) do
false -> {nodes, context}
{false, context} -> {nodes, context}
true -> {[node | nodes], context}
{true, context} -> {[node | nodes], context}
end
end)
end
defp reverse_filtered_nodes({nodes, context}) do
{Enum.reverse(nodes), context}
end
# Walk Combinator
defp walk_combinator(combinator, node, document, context) do
case Selector.Combinator.next(combinator, node, document) do
nil ->
context
nodes when is_list(nodes) ->
selector = Selector.Combinator.selector(combinator)
walk_nodes(nodes, document, selector, context)
node ->
selector = Selector.Combinator.selector(combinator)
walk_nodes([node], document, selector, context)
end
end
end