Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions lib/floki/selector/attribute_selector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,16 @@ defmodule Floki.Selector.AttributeSelector do

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)
attribute_value = get_value(s.attribute, attributes)

if String.contains?(String.downcase(attribute_value), selector_value) do
attribute_value
# 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)
else
false
end
end

def match?(attributes, s = %AttributeSelector{match_type: :dash_match, flag: "i"}) do
Expand Down Expand Up @@ -102,9 +106,15 @@ defmodule Floki.Selector.AttributeSelector do
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)
attribute_value = get_value(s.attribute, attributes)

if String.contains?(attribute_value, value) do
attribute_value
|> String.split([" ", "\t", "\n"], trim: true)
|> Enum.member?(value)
else
false
end
end

def match?(attributes, s = %AttributeSelector{match_type: :dash_match}) do
Expand All @@ -125,17 +135,22 @@ defmodule Floki.Selector.AttributeSelector do
s.attribute |> get_value(attributes) |> String.contains?(s.value)
end

defp get_value(attr_name, attributes) do
Enum.find_value(attributes, "", fn
defp get_value(attr_name, attributes) when is_list(attributes) do
case List.keyfind(attributes, attr_name, 0) do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much of the improvements came from this change?

If is not that much, I would prefer to keep the Enum.find_value/3 just because it's easier to read and maintain.

Copy link
Contributor Author

@preciz preciz Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just ran the benchmarks back and forth changing this part only and it's faster in all cases, the biggest win in speedup is that it's ~11% faster in the "exact_attribute" case but most significantly it halves the memory usage in the "exact_attribute" and in the "attribute_includes" cases.

I believe this library is used heavily by a lot of companies where every speedup has a huge effect on throughput.
That is the case for our company.

{^attr_name, value} -> value
_ -> false
end)
nil -> ""
end
end

defp get_value(attr_name, attributes) when is_map(attributes) do
Map.get(attributes, attr_name, "")
end

defp attribute_present?(name, attributes) when is_list(attributes) do
List.keymember?(attributes, name, 0)
end

defp attribute_present?(name, attributes) do
Enum.any?(attributes, fn
{^name, _v} -> true
_ -> false
end)
defp attribute_present?(name, attributes) when is_map(attributes) do
Map.has_key?(attributes, name)
end
end
Loading