Packages
floki
0.38.2
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
86 Versions
Jump to
Current section
86 Versions
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…