Current section

Files

Jump to
floki lib floki selector.ex
Raw

lib/floki/selector.ex

defmodule Floki.Selector do
require Logger
@moduledoc false
# Represents a CSS selector. It also have functions to match nodes with a given selector.
alias Floki.{Selector, HTMLTree}
alias Selector.{AttributeSelector, PseudoClass}
alias HTMLTree.{HTMLNode, Text, Comment}
defstruct id: nil,
type: nil,
classes: [],
attributes: [],
namespace: nil,
pseudo_classes: [],
combinator: nil
@type t :: %__MODULE__{
id: String.t() | nil,
type: String.t() | nil,
classes: [String.t()],
attributes: [AttributeSelector.t()],
namespace: String.t() | nil,
pseudo_classes: [PseudoClass.t()],
combinator: Selector.Combinator.t() | nil
}
defimpl String.Chars do
def to_string(selector) do
Enum.join([
namespace(selector),
selector.type,
id(selector),
classes(selector),
Enum.join(selector.attributes),
Enum.join(selector.pseudo_classes),
selector.combinator
])
end
defp namespace(%{namespace: nil}), do: ""
defp namespace(%{namespace: ns}), do: "#{ns} | "
defp id(%{id: nil}), do: ""
defp id(%{id: id}), do: "##{id}"
defp classes(%{classes: []}), do: ""
defp classes(%{classes: classes}), do: ".#{Enum.join(classes, ".")}"
end
@wildcards [nil, "*"]
defguardp is_wildcard(x) when x in @wildcards
@doc false
# Returns if a given node matches with a given selector.
def match?(
_node,
%Selector{
id: nil,
type: nil,
classes: [],
attributes: [],
namespace: nil,
pseudo_classes: [],
combinator: nil
},
_tree
) do
false
end
def match?(nil, _selector, _tree), do: false
def match?({:comment, _comment}, _selector, _tree), do: false
def match?({:pi, _xml, _xml_attrs}, _selector, _tree), do: false
def match?(%Text{}, _selector, _tree), do: false
def match?(%Comment{}, _selector, _tree), do: false
def match?(html_node, selector, tree) do
can_match_combinator?(html_node, selector.combinator) &&
id_match?(html_node, selector.id) &&
namespace_match?(html_node, selector.namespace) &&
type_match?(html_node, selector.type) &&
classes_matches?(html_node, selector.classes) &&
attributes_matches?(html_node, selector.attributes) &&
pseudo_classes_match?(html_node, selector.pseudo_classes, tree)
end
defp can_match_combinator?(_node, nil), do: true
defp can_match_combinator?(
%HTMLNode{children_nodes_ids: []},
%Selector.Combinator{match_type: match_type}
)
when match_type in [:child, :descendant] do
false
end
defp can_match_combinator?(_node, _combinator), do: true
def id_match?(_node, nil), do: true
def id_match?(node, id), do: attribute_value(node, "id") == id
defp namespace_match?(_node, namespace) when is_wildcard(namespace), do: true
defp namespace_match?(node, namespace) do
namespace_size = byte_size(namespace)
case type_maybe_with_namespace(node) do
<<^namespace::binary-size(^namespace_size), ":", _::binary>> -> true
_ -> false
end
end
defp type_match?(_node, type) when is_wildcard(type), do: true
defp type_match?(node, type) do
case type_maybe_with_namespace(node) do
^type ->
true
type_maybe_with_namespace when byte_size(type_maybe_with_namespace) > byte_size(type) ->
expected_namespace_size = byte_size(type_maybe_with_namespace) - byte_size(type) - 1
Kernel.match?(
<<
_ns::binary-size(^expected_namespace_size),
":",
^type::binary
>>,
type_maybe_with_namespace
)
_ ->
false
end
end
defp classes_matches?(_node, []), do: true
defp classes_matches?(node, classes) do
class_attr_value = attribute_value(node, "class")
do_classes_matches?(class_attr_value, classes)
end
defp do_classes_matches?(nil, _classes), do: false
defp do_classes_matches?(class_attr_value, [class | _])
when bit_size(class_attr_value) < bit_size(class) do
false
end
defp do_classes_matches?(class_attr_value, [class])
when bit_size(class_attr_value) == bit_size(class) do
class == class_attr_value
end
defp do_classes_matches?(class_attr_value, [class]) do
if String.contains?(class_attr_value, class) do
class_attr_value
|> String.split([" ", "\t", "\n"], trim: true)
|> Enum.member?(class)
else
false
end
end
defp do_classes_matches?(class_attr_value, classes) do
if Enum.all?(classes, &String.contains?(class_attr_value, &1)) do
class_list = String.split(class_attr_value, [" ", "\t", "\n"], trim: true)
Enum.all?(classes, &(&1 in class_list))
else
false
end
end
defp attributes_matches?(_node, []), do: true
defp attributes_matches?(node, attributes_selectors) do
attributes = attributes(node)
not Enum.empty?(attributes) and
Enum.all?(attributes_selectors, fn attribute_selector ->
AttributeSelector.match?(attributes, attribute_selector)
end)
end
defp pseudo_classes_match?(_html_node, [], _tree), do: true
defp pseudo_classes_match?(html_node, pseudo_classes, tree) do
Enum.all?(pseudo_classes, &pseudo_class_match?(html_node, &1, tree))
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "nth-child"}, tree) do
PseudoClass.match_nth_child?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(html_node, %{name: "first-child"}, tree) do
PseudoClass.match_nth_child?(tree, html_node, %PseudoClass{name: "nth-child", value: 1})
end
defp pseudo_class_match?(html_node, %{name: "last-child"}, tree) do
PseudoClass.match_nth_last_child?(tree, html_node, %PseudoClass{
name: "nth-last-child",
value: 1
})
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "nth-last-child"}, tree) do
PseudoClass.match_nth_last_child?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "nth-of-type"}, tree) do
PseudoClass.match_nth_of_type?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(html_node, %{name: "first-of-type"}, tree) do
PseudoClass.match_nth_of_type?(tree, html_node, %PseudoClass{
name: "nth-of-type",
value: 1
})
end
defp pseudo_class_match?(html_node, %{name: "last-of-type"}, tree) do
PseudoClass.match_nth_last_of_type?(tree, html_node, %PseudoClass{
name: "nth-last-of-type",
value: 1
})
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "nth-last-of-type"}, tree) do
PseudoClass.match_nth_last_of_type?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "not"}, tree) do
Enum.all?(pseudo_class.value, &(!Selector.match?(html_node, &1, tree)))
end
defp pseudo_class_match?(html_node, %{name: "checked"}, _tree) do
PseudoClass.match_checked?(html_node)
end
defp pseudo_class_match?(html_node, %{name: "disabled"}, _tree) do
PseudoClass.match_disabled?(html_node)
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "fl-contains"}, tree) do
PseudoClass.match_contains?(tree, html_node, pseudo_class)
end
# Case insensitive contains
defp pseudo_class_match?(html_node, pseudo_class = %{name: "fl-icontains"}, tree) do
PseudoClass.match_icontains?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(html_node, %{name: "root"}, tree) do
PseudoClass.match_root?(html_node, tree)
end
defp pseudo_class_match?(html_node, pseudo_class = %{name: "has"}, tree) do
PseudoClass.match_has?(tree, html_node, pseudo_class)
end
defp pseudo_class_match?(_html_node, %{name: unknown_pseudo_class}, _tree) do
Logger.debug(fn ->
"Pseudo-class #{inspect(unknown_pseudo_class)} is not implemented. Ignoring."
end)
false
end
defp type_maybe_with_namespace({type, _attributes, _children}) when is_binary(type), do: type
defp type_maybe_with_namespace(%HTMLNode{type: type}) when is_binary(type), do: type
defp type_maybe_with_namespace(_), do: nil
defp attribute_value(node, attribute_name) do
attributes = attributes(node)
get_attribute_value(attributes, attribute_name)
end
defp attributes({_type, attributes, _children}), do: attributes
defp attributes(%HTMLNode{type: :pi}), do: []
defp attributes(%HTMLNode{attributes: attributes}), do: attributes
defp get_attribute_value([{attr_name, value} | _], attr_name), do: value
defp get_attribute_value([_ | rest], attr_name), do: get_attribute_value(rest, attr_name)
defp get_attribute_value(attrs, attr_name) when is_map(attrs), do: Map.get(attrs, attr_name)
defp get_attribute_value(_, _), do: nil
end