Current section

Files

Jump to
floki lib floki selector pseudo_class.ex
Raw

lib/floki/selector/pseudo_class.ex

defmodule Floki.Selector.PseudoClass do
@moduledoc false
require Logger
# Represents a pseudo-class selector
defstruct name: "", value: nil
alias Floki.HTMLTree.{HTMLNode, Text}
alias Floki.Selector.Functional
@type t :: %__MODULE__{
name: String.t(),
value: String.t() | [Floki.Selector.t()]
}
defimpl String.Chars do
def to_string(%{name: name, value: nil}) do
":#{name}"
end
def to_string(%{name: name, value: selectors}) when is_list(selectors) do
":#{name}(#{Enum.join(selectors)})"
end
def to_string(pseudo_class) do
":#{pseudo_class.name}(#{pseudo_class.value})"
end
end
def match_nth_child?(tree, html_node, %__MODULE__{value: value}) do
tree
|> pseudo_nodes(html_node)
|> Enum.reverse()
|> node_position(html_node)
|> match_position?(value, "nth-child")
end
def match_nth_of_type?(tree, html_node, %__MODULE__{value: value}) do
tree
|> pseudo_nodes(html_node)
|> filter_nodes_by_type(tree.nodes, html_node.type)
|> Enum.reverse()
|> node_position(html_node)
|> match_position?(value, "nth-of-type")
end
def match_nth_last_child?(tree, html_node, %__MODULE__{value: value}) do
tree
|> pseudo_nodes(html_node)
|> node_position(html_node)
|> match_position?(value, "nth-last-child")
end
def match_nth_last_of_type?(tree, html_node, %__MODULE__{value: value}) do
tree
|> pseudo_nodes(html_node)
|> filter_nodes_by_type(tree.nodes, html_node.type)
|> node_position(html_node)
|> match_position?(value, "nth-last-of-type")
end
def match_contains?(tree, html_node, %__MODULE__{value: value}) do
res =
Enum.find(html_node.children_nodes_ids, fn id ->
case Map.get(tree.nodes, id) do
%Text{content: content} -> content =~ value
_ -> false
end
end)
res != nil
end
# Case insensitive contains
def match_icontains?(tree, html_node, %__MODULE__{value: value}) do
downcase_value = String.downcase(value)
res =
Enum.find(html_node.children_nodes_ids, fn id ->
case Map.get(tree.nodes, id) do
%Text{content: content} -> String.downcase(content) =~ downcase_value
_ -> false
end
end)
res != nil
end
defp match_position?(relative_position, value, name) do
case value do
position when is_integer(position) ->
relative_position == position
"even" ->
rem(relative_position, 2) == 0
"odd" ->
rem(relative_position, 2) == 1
%Functional{stream: s} ->
relative_position in s
expression ->
Logger.debug(fn ->
"Pseudo-class #{name} with expressions like #{inspect(expression)} are not supported yet. Ignoring."
end)
false
end
end
def match_checked?(%{type: "input"} = html_node) do
attribute_present?(html_node.attributes, "checked")
end
def match_checked?(%{type: "option"} = html_node) do
attribute_present?(html_node.attributes, "selected")
end
def match_checked?({"input", attributes, _children}) do
attribute_present?(attributes, "checked")
end
def match_checked?({"option", attributes, _children}) do
attribute_present?(attributes, "selected")
end
def match_checked?(_), do: false
defp attribute_present?([{attr_name, _} | _], attr_name), do: true
defp attribute_present?([_ | rest], attr_name), do: attribute_present?(rest, attr_name)
defp attribute_present?(attrs, attr_name) when is_map(attrs), do: Map.has_key?(attrs, attr_name)
defp attribute_present?(_, _), do: false
@disableable_html_nodes ~w[button input select option textarea]
def match_disabled?(%{type: type} = html_node) when type in @disableable_html_nodes do
attribute_present?(html_node.attributes, "disabled")
end
def match_disabled?({type, attributes, _children}) when type in @disableable_html_nodes do
attribute_present?(attributes, "disabled")
end
def match_disabled?(_html_node), do: false
def match_root?(html_node, tree) do
html_node.node_id in tree.root_nodes_ids
end
def match_has?(tree, html_node, %__MODULE__{value: value} = pseudo_class) do
Enum.any?(value, fn inner_selector ->
Enum.any?(html_node.children_nodes_ids, fn id ->
child = Map.fetch!(tree.nodes, id)
is_struct(child, HTMLNode) and
(Floki.Selector.match?(child, inner_selector, tree) or
match_has?(tree, child, pseudo_class))
end)
end)
end
defp node_position(ids, %HTMLNode{node_id: node_id}) do
position = Enum.find_index(ids, fn id -> id == node_id end)
position + 1
end
defp pseudo_nodes(tree, %HTMLNode{parent_node_id: nil}) do
tree.root_nodes_ids
|> filter_only_html_nodes(tree.nodes)
end
defp pseudo_nodes(tree, %HTMLNode{parent_node_id: parent_node_id}) do
parent_node = Map.fetch!(tree.nodes, parent_node_id)
parent_node.children_nodes_ids
|> filter_only_html_nodes(tree.nodes)
end
defp filter_only_html_nodes(ids, nodes) do
Enum.filter(ids, fn id ->
case nodes do
%{^id => %HTMLNode{}} -> true
_ -> false
end
end)
end
defp filter_nodes_by_type(ids, nodes, type) do
Enum.filter(ids, fn id ->
case nodes do
%{^id => %HTMLNode{type: ^type}} -> true
_ -> false
end
end)
end
end