Current section

Files

Jump to
floki lib floki selector attribute_selector.ex
Raw

lib/floki/selector/attribute_selector.ex

defmodule Floki.Selector.AttributeSelector do
@moduledoc false
# It is very similar to the `Selector` module, but is specialized in attributes
# and attribute selectors.
alias Floki.Selector.AttributeSelector
defstruct match_type: nil, attribute: nil, value: nil, flag: nil
@type match_type ::
nil
| :equal
| :includes
| :dash_match
| :prefix_match
| :suffix_match
| :substring_match
@type t :: %__MODULE__{
match_type: match_type(),
attribute: String.t(),
value: String.t() | nil,
flag: String.t() | nil
}
defimpl String.Chars do
def to_string(selector) do
"[#{selector.attribute}#{type(selector.match_type)}'#{selector.value}'#{flag(selector.flag)}]"
end
defp type(match_type) do
case match_type do
:equal -> "="
:includes -> "~="
:dash_match -> "|="
:prefix_match -> "^="
:suffix_match -> "$="
:substring_match -> "*="
_ -> ""
end
end
defp flag(nil), do: ""
defp flag(flag), do: " " <> flag
end
# Returns if attributes of a node matches with a given attribute selector.
def match?(attributes, s = %AttributeSelector{match_type: nil, value: nil})
when is_list(attributes) or is_map(attributes) do
attribute_present?(s.attribute, attributes)
end
# Case-insensitive matches
def match?(attributes, s = %AttributeSelector{match_type: :equal, flag: "i"}) do
String.downcase(get_value(s.attribute, attributes)) == String.downcase(s.value)
end
def match?(attributes, s = %AttributeSelector{match_type: :includes, flag: "i"}) do
selector_value = String.downcase(s.value)
s.attribute
|> get_value(attributes)
# Splits by whitespaces ("a b c" -> ["a", "b", "c"])
|> String.split([" ", "\t", "\n"], trim: true)
|> Enum.any?(fn v -> String.downcase(v) == selector_value end)
end
def match?(attributes, s = %AttributeSelector{match_type: :dash_match, flag: "i"}) do
selector_value = String.downcase(s.value)
value = String.downcase(get_value(s.attribute, attributes))
value == selector_value || String.starts_with?(value, "#{selector_value}-")
end
def match?(attributes, s = %AttributeSelector{match_type: :prefix_match, flag: "i"}) do
s.attribute
|> get_value(attributes)
|> String.downcase()
|> String.starts_with?(String.downcase(s.value))
end
def match?(attributes, s = %AttributeSelector{match_type: :suffix_match, flag: "i"}) do
s.attribute
|> get_value(attributes)
|> String.downcase()
|> String.ends_with?(String.downcase(s.value))
end
def match?(attributes, s = %AttributeSelector{match_type: :substring_match, flag: "i"}) do
s.attribute
|> get_value(attributes)
|> String.downcase()
|> String.contains?(String.downcase(s.value))
end
# Case-sensitive matches
def match?(attributes, s = %AttributeSelector{match_type: :equal}) do
get_value(s.attribute, attributes) == s.value
end
def match?(attributes, s = %AttributeSelector{match_type: :includes, value: value}) do
get_value(s.attribute, attributes)
|> String.split([" ", "\t", "\n"], trim: true)
|> Enum.member?(value)
end
def match?(attributes, s = %AttributeSelector{match_type: :dash_match}) do
value = get_value(s.attribute, attributes)
value == s.value || String.starts_with?(value, "#{s.value}-")
end
def match?(attributes, s = %AttributeSelector{match_type: :prefix_match}) do
s.attribute |> get_value(attributes) |> String.starts_with?(s.value)
end
def match?(attributes, s = %AttributeSelector{match_type: :suffix_match}) do
s.attribute |> get_value(attributes) |> String.ends_with?(s.value)
end
def match?(attributes, s = %AttributeSelector{match_type: :substring_match}) do
s.attribute |> get_value(attributes) |> String.contains?(s.value)
end
defp get_value(attr_name, attributes) do
Enum.find_value(attributes, "", fn
{^attr_name, value} -> value
_ -> false
end)
end
defp attribute_present?(name, attributes) do
Enum.any?(attributes, fn
{^name, _v} -> true
_ -> false
end)
end
end