Current section

86 Versions

Jump to

Compare versions

14 files changed
+202 additions
-215 deletions
  @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7 7
8 8 ## [Unreleased][unreleased]
9 9
10 + ## [0.38.2] - 2026-05-18
11 +
12 + ### Performance
13 +
14 + This is another juicy patch version with performance improvements made by [@preciz](https://github.com/preciz).
15 +
16 + * [Optimize multiple selectors deduping - #667](https://github.com/philss/floki/pull/667)
17 + * [Optimize DeepText by using plain recursion - #668](https://github.com/philss/floki/pull/668)
18 + * [Optimize get_descendant_ids by removing Enum.reverse calls - #669](https://github.com/philss/floki/pull/669)
19 + * [Optimize HTMLTree.to_tuple_list/1 using tail recursion - #670](https://github.com/philss/floki/pull/670)
20 + * [Optimize Floki.TextExtractor by removing Enum iterations - #675](https://github.com/philss/floki/pull/675)
21 + * [Simplify class attribute matching - #677](https://github.com/philss/floki/pull/677)
22 + * [Refactor attribute_values to use for comprehension with pattern matching - #679](https://github.com/philss/floki/pull/679)
23 + * [Optimize CSS class matching to avoid list subtraction - #680](https://github.com/philss/floki/pull/680)
24 + * [Optimize iodata by avoiding empty separators in DeepText and FlatText - #682](https://github.com/philss/floki/pull/682)
25 + * [Simplify HTMLTree.build/1 by avoiding redundant build_tree calls - #683](https://github.com/philss/floki/pull/683)
26 + * [Optimize Floki.text/2 by extracting text in a single pass - #684](https://github.com/philss/floki/pull/684)
27 + * [Optimize HTMLTree updates and deletions - #687](https://github.com/philss/floki/pull/687)
28 + * [Refactor attribute retrieval functions for a 1.2-1.5x speedup - #689](https://github.com/philss/floki/pull/689)
29 + * [Optimize case-insensitive attribute includes match - #690](https://github.com/philss/floki/pull/690)
30 + * [Optimize selector traversal - #611](https://github.com/philss/floki/pull/691)
31 +
32 + Please check the pull requests to see the improvements.
33 +
34 + ### Fixed
35 +
36 + - Fix compiler warnings for the upcoming Elixir v1.20.
37 + - Fix typespecs of `Floki.attribute/3`.
38 + - Fix documentation for some functions.
39 +
10 40 ## [0.38.1] - 2026-03-17
11 41
12 42 ### Performance
  @@ -920,7 +950,8 @@ of the parent element inside HTML.
920 950
921 951 - Elixir version requirement from "~> 1.0.0" to ">= 1.0.0".
922 952
923 - [unreleased]: https://github.com/philss/floki/compare/v0.38.1...HEAD
953 + [unreleased]: https://github.com/philss/floki/compare/v0.38.2...HEAD
954 + [0.38.2]: https://github.com/philss/floki/compare/v0.38.1...v0.38.2
924 955 [0.38.1]: https://github.com/philss/floki/compare/v0.38.0...v0.38.1
925 956 [0.38.0]: https://github.com/philss/floki/compare/v0.37.1...v0.38.0
926 957 [0.37.1]: https://github.com/philss/floki/compare/v0.37.0...v0.37.1
  @@ -3,7 +3,7 @@
3 3 {<<"GitHub">>,<<"https://github.com/philss/floki">>},
4 4 {<<"Sponsor">>,<<"https://github.com/sponsors/philss">>}]}.
5 5 {<<"name">>,<<"floki">>}.
6 - {<<"version">>,<<"0.38.1">>}.
6 + {<<"version">>,<<"0.38.2">>}.
7 7 {<<"description">>,
8 8 <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}.
9 9 {<<"elixir">>,<<"~> 1.15">>}.
  @@ -230,7 +230,7 @@ defmodule Floki do
230 230 defdelegate raw_html(html_tree, options \\ []), to: Floki.RawHTML
231 231
232 232 @doc """
233 - Find elements inside an HTML tree or string.
233 + Find elements inside an HTML tree.
234 234
235 235 ## Examples
236 236
  @@ -488,7 +488,7 @@ defmodule Floki do
488 488 should be considered as text. Defaults to `false`.
489 489
490 490 * `:style` - A boolean to control if the contents of `<style>` tags
491 - should be considered as text. Defaults to `false`.
491 + should be considered as text. Defaults to `true`.
492 492
493 493 * `:sep` - A separator string that is added between text nodes.
494 494 Defaults to `""`.
  @@ -497,9 +497,6 @@ defmodule Floki do
497 497 values should be included in the resultant string.
498 498 Defaults to `false`.
499 499
500 - * `:html_parser` - The module of the backend that is responsible for parsing
501 - the HTML string. By default it is set to `Floki.HTMLParser.Mochiweb`.
502 -
503 500 ## Examples
504 501
505 502 iex> Floki.text({"div", [], [{"span", [], ["hello"]}, " world"]})
  @@ -541,14 +538,9 @@ defmodule Floki do
541 538
542 539 opts = Keyword.validate!(opts, defaults)
543 540
544 - cleaned_html_tree =
545 - html
546 - |> clean_html_tree(:js, opts[:js])
547 - |> clean_html_tree(:style, opts[:style])
548 -
549 541 search_strategy = if opts[:deep], do: Floki.DeepText, else: Floki.FlatText
550 542
551 - search_strategy.get(cleaned_html_tree, opts[:sep], opts[:include_inputs])
543 + search_strategy.get(html, opts)
552 544 end
553 545
554 546 @doc """
  @@ -614,7 +606,7 @@ defmodule Floki do
614 606 ["e.corp"]
615 607 """
616 608
617 - @spec attribute(binary | html_tree | html_node, binary, binary) :: list
609 + @spec attribute(html_tree | html_node, binary, binary) :: list
618 610
619 611 def attribute(html, selector, attribute_name) do
620 612 html
  @@ -644,42 +636,19 @@ defmodule Floki do
644 636 end
645 637
646 638 defp attribute_values(elements, attr_name) do
647 - values =
648 - Enum.reduce(
649 - elements,
650 - [],
651 - fn
652 - {_, attributes, _}, acc ->
653 - case attribute_match?(attributes, attr_name) do
654 - {_attr_name, value} ->
655 - [value | acc]
656 -
657 - _ ->
658 - acc
659 - end
660 -
661 - _, acc ->
662 - acc
663 - end
664 - )
665 -
666 - Enum.reverse(values)
639 + for {_, attributes, _} <- elements,
640 + value = get_attribute_value(attributes, attr_name),
641 + not is_nil(value),
642 + do: value
667 643 end
668 644
669 - defp attribute_match?(attributes, attribute_name) do
670 - Enum.find(
671 - attributes,
672 - fn {attr_name, _} ->
673 - attr_name == attribute_name
674 - end
675 - )
645 + defp get_attribute_value(attributes, attr_name) when is_map(attributes) do
646 + Map.get(attributes, attr_name)
676 647 end
677 648
678 - defp clean_html_tree(html_tree, :js, true), do: html_tree
679 - defp clean_html_tree(html_tree, :js, _), do: filter_out(html_tree, "script")
680 -
681 - defp clean_html_tree(html_tree, :style, true), do: html_tree
682 - defp clean_html_tree(html_tree, :style, _), do: filter_out(html_tree, "style")
649 + defp get_attribute_value([{attr_name, value} | _], attr_name), do: value
650 + defp get_attribute_value([_ | rest], attr_name), do: get_attribute_value(rest, attr_name)
651 + defp get_attribute_value([], _attr_name), do: nil
683 652
684 653 @doc """
685 654 Returns the nodes from a HTML tree that don't match the filter selector.
  @@ -8,35 +8,51 @@ defmodule Floki.DeepText do
8 8
9 9 @spec get(html_tree, binary, boolean) :: binary
10 10
11 - def get(html_tree, sep \\ "", include_inputs? \\ false)
11 + def get(html_tree, sep_or_opts \\ "", include_inputs? \\ false)
12 +
13 + def get(html_tree, opts, _) when is_list(opts) do
14 + sep = Keyword.get(opts, :sep, "")
15 + include_inputs? = Keyword.get(opts, :include_inputs, false)
16 + js? = Keyword.get(opts, :js, false)
17 + style? = Keyword.get(opts, :style, true)
12 18
13 - def get(html_tree, sep, include_inputs?) do
14 19 html_tree
15 - |> get_text([], sep, include_inputs?)
20 + |> get_text([], sep, include_inputs?, js?, style?)
16 21 |> IO.iodata_to_binary()
17 22 end
18 23
19 - defp get_text(text, [], _sep, _) when is_binary(text), do: text
20 - defp get_text(text, acc, sep, _) when is_binary(text), do: [acc, sep, text]
21 -
22 - defp get_text(nodes, acc, sep, include_inputs?) when is_list(nodes) do
23 - Enum.reduce(nodes, acc, fn child, istr ->
24 - get_text(child, istr, sep, include_inputs?)
25 - end)
24 + def get(html_tree, sep, include_inputs?) do
25 + html_tree
26 + |> get_text([], sep, include_inputs?, true, true)
27 + |> IO.iodata_to_binary()
26 28 end
27 29
28 - defp get_text({:comment, _}, acc, _, _), do: acc
29 - defp get_text({"br", _, _}, acc, _, _), do: [acc, "\n"]
30 + defp get_text(text, [], _sep, _, _, _) when is_binary(text), do: text
31 + defp get_text(text, acc, "", _, _, _) when is_binary(text), do: [acc, text]
32 + defp get_text(text, acc, sep, _, _, _) when is_binary(text), do: [acc, sep, text]
30 33
31 - defp get_text({"input", attrs, _}, acc, _, true) do
34 + defp get_text([], acc, _sep, _, _, _), do: acc
35 +
36 + defp get_text([child | rest], acc, sep, include_inputs?, js?, style?) do
37 + acc = get_text(child, acc, sep, include_inputs?, js?, style?)
38 + get_text(rest, acc, sep, include_inputs?, js?, style?)
39 + end
40 +
41 + defp get_text({:comment, _}, acc, _, _, _, _), do: acc
42 + defp get_text({"br", _, _}, acc, _, _, _, _), do: [acc, "\n"]
43 +
44 + defp get_text({"script", _, _}, acc, _, _, false, _), do: acc
45 + defp get_text({"style", _, _}, acc, _, _, _, false), do: acc
46 +
47 + defp get_text({"input", attrs, _}, acc, _, true, _, _) do
32 48 [acc, Floki.TextExtractor.extract_input_value(attrs)]
33 49 end
34 50
35 - defp get_text({"textarea", attrs, _}, acc, _, true) do
51 + defp get_text({"textarea", attrs, _}, acc, _, true, _, _) do
36 52 [acc, Floki.TextExtractor.extract_input_value(attrs)]
37 53 end
38 54
39 - defp get_text({_, _, nodes}, acc, sep, include_inputs?) do
40 - get_text(nodes, acc, sep, include_inputs?)
55 + defp get_text({_, _, nodes}, acc, sep, include_inputs?, js?, style?) do
56 + get_text(nodes, acc, sep, include_inputs?, js?, style?)
41 57 end
42 58 end
  @@ -76,8 +76,8 @@ defmodule Floki.Finder do
76 76
77 77 def find(%HTMLTree{} = tree, selectors) when is_list(selectors) do
78 78 Enum.flat_map(selectors, fn s -> traverse_html_tree(tree.node_ids, s, tree, []) end)
79 + |> Enum.uniq_by(& &1.node_id)
79 80 |> Enum.sort_by(& &1.node_id)
80 - |> Enum.uniq()
81 81 end
82 82
83 83 # some selectors can be applied with the raw html tree tuples instead of
  @@ -319,7 +319,7 @@ defmodule Floki.Finder do
319 319 end
320 320
321 321 defp get_selector_nodes(%Selector.Combinator{match_type: :child}, html_node, _tree) do
322 - Enum.reverse(html_node.children_nodes_ids)
322 + html_node.children_nodes_ids
323 323 end
324 324
325 325 defp get_selector_nodes(%Selector.Combinator{match_type: :adjacent_sibling}, html_node, tree) do
  @@ -372,8 +372,7 @@ defmodule Floki.Finder do
372 372 defp get_descendant_ids(node_id, tree) do
373 373 case get_node(node_id, tree) do
374 374 %{children_nodes_ids: children} when children != [] ->
375 - do_get_descendant_ids(Enum.reverse(children), tree, [])
376 - |> Enum.reverse()
375 + do_get_descendant_ids(children, tree, [])
377 376
378 377 _ ->
379 378 []
  @@ -383,17 +382,16 @@ defmodule Floki.Finder do
383 382 defp do_get_descendant_ids([], _tree, acc), do: acc
384 383
385 384 defp do_get_descendant_ids([node_id | rest], tree, acc) do
386 - acc = [node_id | acc]
387 -
388 385 acc =
389 386 case get_node(node_id, tree) do
390 387 %{children_nodes_ids: children} when children != [] ->
391 - do_get_descendant_ids(Enum.reverse(children), tree, acc)
388 + do_get_descendant_ids(children, tree, acc)
392 389
393 390 _ ->
394 391 acc
395 392 end
396 393
394 + acc = [node_id | acc]
397 395 do_get_descendant_ids(rest, tree, acc)
398 396 end
Loading more files…