Packages
floki
0.38.3
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.1
0.37.0
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.3
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.0
0.30.1
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.1
0.23.0
0.22.0
0.21.0
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.0
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.0
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
Floki is a simple HTML parser that enables search for nodes using CSS selectors.
Current section
Files
Jump to
Current section
Files
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