Packages
floki
0.35.3
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
16
files changed
+519
additions
-399
deletions
| @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. | |
| 7 7 | |
| 8 8 | ## [Unreleased][unreleased] |
| 9 9 | |
| 10 | + ## [0.35.3] - 2024-01-25 |
| 11 | + |
| 12 | + This release has great performance improvements, thanks to the PRs |
| 13 | + from [@ypconstante](https://github.com/ypconstante)! |
| 14 | + |
| 15 | + Most of the main functions, such as `Floki.raw_html/2` and `Floki.find/2` are |
| 16 | + faster and are using less memory. It's something like twice as fast, and half |
| 17 | + usage of memory for `find/2`, for example. |
| 18 | + |
| 19 | + ### Fixed |
| 20 | + |
| 21 | + - Add `:leex` to Mix compilers. Fixes the build when running with dev version of Elixir. |
| 22 | + Thanks [@wojtekmach](https://github.com/wojtekmach). |
| 23 | + |
| 24 | + - Fix `Floki.raw_html/2` when a tree using attributes as maps is given. |
| 25 | + Thanks [@SupaMic](https://github.com/SupaMic). |
| 26 | + |
| 27 | + - Add a guard to `Floki.find/2` so people can have a better error message when an |
| 28 | + invalid input is given. Thanks [@Hajto](https://github.com/Hajto). |
| 29 | + |
| 30 | + - Fix parsers to consider IO data as inputs. This may change in the next version |
| 31 | + of Floki, as I plan to drop support for IO data. |
| 32 | + Thanks [@ypconstante](https://github.com/ypconstante). |
| 33 | + |
| 34 | + ### Removed |
| 35 | + |
| 36 | + - Remove outdated Gleam wrapper code. The external functions syntax in Gleam |
| 37 | + has [changed](https://gleam.run/book/tour/external-functions.html). So now |
| 38 | + the wrapper is not needed anymore. |
| 39 | + Thanks [@michallepicki](https://github.com/michallepicki). |
| 40 | + |
| 10 41 | ## [0.35.2] - 2023-10-25 |
| 11 42 | |
| 12 43 | ### Fixed |
| @@ -705,7 +736,8 @@ of the parent element inside HTML. | |
| 705 736 | |
| 706 737 | - Elixir version requirement from "~> 1.0.0" to ">= 1.0.0". |
| 707 738 | |
| 708 | - [unreleased]: https://github.com/philss/floki/compare/v0.35.2...HEAD |
| 739 | + [unreleased]: https://github.com/philss/floki/compare/v0.35.3...HEAD |
| 740 | + [0.35.3]: https://github.com/philss/floki/compare/v0.35.2...v0.35.3 |
| 709 741 | [0.35.2]: https://github.com/philss/floki/compare/v0.35.1...v0.35.2 |
| 710 742 | [0.35.1]: https://github.com/philss/floki/compare/v0.35.0...v0.35.1 |
| 711 743 | [0.35.0]: https://github.com/philss/floki/compare/v0.34.3...v0.35.0 |
| @@ -1,36 +1,37 @@ | |
| 1 | - {<<"app">>,<<"floki">>}. |
| 2 | - {<<"build_tools">>,[<<"mix">>]}. |
| 3 | - {<<"description">>, |
| 4 | - <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}. |
| 5 | - {<<"elixir">>,<<"~> 1.12">>}. |
| 6 | - {<<"files">>, |
| 7 | - [<<"lib/floki">>,<<"lib/floki/html_tree.ex">>,<<"lib/floki/entities.ex">>, |
| 8 | - <<"lib/floki/parse_error.ex">>,<<"lib/floki/deep_text.ex">>, |
| 9 | - <<"lib/floki/finder.ex">>,<<"lib/floki/flat_text.ex">>, |
| 10 | - <<"lib/floki/selector">>,<<"lib/floki/selector/attribute_selector.ex">>, |
| 11 | - <<"lib/floki/selector/pseudo_class.ex">>,<<"lib/floki/selector/parser.ex">>, |
| 12 | - <<"lib/floki/selector/functional.ex">>, |
| 13 | - <<"lib/floki/selector/combinator.ex">>, |
| 14 | - <<"lib/floki/selector/tokenizer.ex">>,<<"lib/floki/raw_html.ex">>, |
| 15 | - <<"lib/floki/filter_out.ex">>,<<"lib/floki/html">>, |
| 16 | - <<"lib/floki/html/numeric_charref.ex">>,<<"lib/floki/html/tokenizer.ex">>, |
| 17 | - <<"lib/floki/html_tree">>,<<"lib/floki/html_tree/comment.ex">>, |
| 18 | - <<"lib/floki/html_tree/id_seeder.ex">>,<<"lib/floki/html_tree/text.ex">>, |
| 19 | - <<"lib/floki/html_tree/html_node.ex">>,<<"lib/floki/html_parser.ex">>, |
| 20 | - <<"lib/floki/entities">>,<<"lib/floki/entities/codepoints.ex">>, |
| 21 | - <<"lib/floki/text_extractor.ex">>,<<"lib/floki/selector.ex">>, |
| 22 | - <<"lib/floki/traversal.ex">>,<<"lib/floki/html_parser">>, |
| 23 | - <<"lib/floki/html_parser/html5ever.ex">>, |
| 24 | - <<"lib/floki/html_parser/fast_html.ex">>, |
| 25 | - <<"lib/floki/html_parser/mochiweb.ex">>,<<"lib/floki.ex">>, |
| 26 | - <<"src/floki_selector_lexer.xrl">>,<<"src/floki_mochi_html.erl">>, |
| 27 | - <<"src/floki.gleam">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>, |
| 28 | - <<"CODE_OF_CONDUCT.md">>,<<"CONTRIBUTING.md">>,<<"CHANGELOG.md">>]}. |
| 29 | - {<<"licenses">>,[<<"MIT">>]}. |
| 30 1 | {<<"links">>, |
| 31 2 | [{<<"Changelog">>,<<"https://hexdocs.pm/floki/changelog.html">>}, |
| 32 3 | {<<"GitHub">>,<<"https://github.com/philss/floki">>}, |
| 33 4 | {<<"Sponsor">>,<<"https://github.com/sponsors/philss">>}]}. |
| 34 5 | {<<"name">>,<<"floki">>}. |
| 6 | + {<<"version">>,<<"0.35.3">>}. |
| 7 | + {<<"description">>, |
| 8 | + <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}. |
| 9 | + {<<"elixir">>,<<"~> 1.12">>}. |
| 10 | + {<<"app">>,<<"floki">>}. |
| 11 | + {<<"licenses">>,[<<"MIT">>]}. |
| 12 | + {<<"files">>, |
| 13 | + [<<"lib/floki">>,<<"lib/floki/deep_text.ex">>,<<"lib/floki/entities">>, |
| 14 | + <<"lib/floki/entities/codepoints.ex">>,<<"lib/floki/filter_out.ex">>, |
| 15 | + <<"lib/floki/flat_text.ex">>,<<"lib/floki/html">>, |
| 16 | + <<"lib/floki/html/numeric_charref.ex">>,<<"lib/floki/html/tokenizer.ex">>, |
| 17 | + <<"lib/floki/html_parser">>,<<"lib/floki/html_parser/fast_html.ex">>, |
| 18 | + <<"lib/floki/html_parser/html5ever.ex">>, |
| 19 | + <<"lib/floki/html_parser/mochiweb.ex">>,<<"lib/floki/html_tree">>, |
| 20 | + <<"lib/floki/html_tree/comment.ex">>,<<"lib/floki/html_tree/html_node.ex">>, |
| 21 | + <<"lib/floki/html_tree/id_seeder.ex">>,<<"lib/floki/html_tree/text.ex">>, |
| 22 | + <<"lib/floki/parse_error.ex">>,<<"lib/floki/selector">>, |
| 23 | + <<"lib/floki/selector/combinator.ex">>, |
| 24 | + <<"lib/floki/selector/functional.ex">>, |
| 25 | + <<"lib/floki/selector/pseudo_class.ex">>, |
| 26 | + <<"lib/floki/selector/tokenizer.ex">>, |
| 27 | + <<"lib/floki/selector/attribute_selector.ex">>, |
| 28 | + <<"lib/floki/selector/parser.ex">>,<<"lib/floki/text_extractor.ex">>, |
| 29 | + <<"lib/floki/traversal.ex">>,<<"lib/floki/html_parser.ex">>, |
| 30 | + <<"lib/floki/entities.ex">>,<<"lib/floki/finder.ex">>, |
| 31 | + <<"lib/floki/html_tree.ex">>,<<"lib/floki/raw_html.ex">>, |
| 32 | + <<"lib/floki/selector.ex">>,<<"lib/floki.ex">>, |
| 33 | + <<"src/floki_selector_lexer.xrl">>,<<"src/floki_mochi_html.erl">>, |
| 34 | + <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,<<"CODE_OF_CONDUCT.md">>, |
| 35 | + <<"CONTRIBUTING.md">>,<<"CHANGELOG.md">>]}. |
| 35 36 | {<<"requirements">>,[]}. |
| 36 | - {<<"version">>,<<"0.35.2">>}. |
| 37 | + {<<"build_tools">>,[<<"mix">>]}. |
| @@ -77,6 +77,11 @@ defmodule Floki do | |
| 77 77 | |
| 78 78 | @type css_selector :: String.t() | %Floki.Selector{} | [%Floki.Selector{}] |
| 79 79 | |
| 80 | + defguard is_html_node(value) |
| 81 | + when is_binary(value) or tuple_size(value) == 3 or |
| 82 | + (tuple_size(value) == 2 and elem(value, 0) in [:pi, :comment]) or |
| 83 | + (tuple_size(value) == 4 and elem(value, 0) == :doctype) |
| 84 | + |
| 80 85 | @doc """ |
| 81 86 | Parses a HTML Document from a String. |
| 82 87 | |
| @@ -168,7 +173,7 @@ defmodule Floki do | |
| 168 173 | ## Options |
| 169 174 | |
| 170 175 | * `:attributes_as_maps` - Change the behaviour of the parser to return the attributes |
| 171 | - as maps, instead of a list of `{"key", "value"}`. Remember that maps are no longer |
| 176 | + as maps, instead of a list of `{"key", "value"}`. Remember that maps are no longer |
| 172 177 | ordered since OTP 26. Default to `false`. |
| 173 178 | |
| 174 179 | * `:html_parser` - The module of the backend that is responsible for parsing |
| @@ -212,7 +217,7 @@ defmodule Floki do | |
| 212 217 | ## Options |
| 213 218 | |
| 214 219 | * `:encode` - A boolean option to control if special HTML characters |
| 215 | - should be encoded as HTML entities. Defaults to `true`. |
| 220 | + should be encoded as HTML entities. Defaults to `true`. |
| 216 221 | |
| 217 222 | You can also control the encoding behaviour at the application level via |
| 218 223 | `config :floki, :encode_raw_html, false` |
| @@ -269,7 +274,6 @@ defmodule Floki do | |
| 269 274 | """ |
| 270 275 | |
| 271 276 | @spec find(binary() | html_tree() | html_node(), css_selector()) :: html_tree |
| 272 | - |
| 273 277 | def find(html, selector) when is_binary(html) do |
| 274 278 | Logger.info( |
| 275 279 | "deprecation: parse the HTML with parse_document or parse_fragment before using find/2" |
| @@ -282,7 +286,8 @@ defmodule Floki do | |
| 282 286 | end |
| 283 287 | end |
| 284 288 | |
| 285 | - def find(html_tree_as_tuple, selector) do |
| 289 | + def find(html_tree_as_tuple, selector) |
| 290 | + when is_list(html_tree_as_tuple) or is_html_node(html_tree_as_tuple) do |
| 286 291 | {tree, results} = Finder.find(html_tree_as_tuple, selector) |
| 287 292 | |
| 288 293 | Enum.map(results, fn html_node -> HTMLTree.to_tuple(tree, html_node) end) |
| @@ -511,7 +516,7 @@ defmodule Floki do | |
| 511 516 | |
| 512 517 | ## Options |
| 513 518 | |
| 514 | - * `:deep` - A boolean option to control how deep the search for |
| 519 | + * `:deep` - A boolean option to control how deep the search for |
| 515 520 | text is going to be. If `false`, only the level of the HTML node |
| 516 521 | or the first level of the HTML document is going to be considered. |
| 517 522 | Defaults to `true`. |
| @@ -626,11 +631,19 @@ defmodule Floki do | |
| 626 631 | iex> Floki.attribute([{"a", [{"href", "https://google.com"}], ["Google"]}], "a", "href") |
| 627 632 | ["https://google.com"] |
| 628 633 | |
| 629 | - iex> Floki.attribute([{"a", [{"class", "foo"}, {"href", "https://google.com"}], ["Google"]}], "a", "class") |
| 634 | + iex> Floki.attribute( |
| 635 | + iex> [{"a", [{"class", "foo"}, {"href", "https://google.com"}], ["Google"]}], |
| 636 | + iex> "a", |
| 637 | + iex> "class" |
| 638 | + iex> ) |
| 630 639 | ["foo"] |
| 631 640 | |
| 632 | - iex> Floki.attribute([{"a", [{"href", "https://google.com"}, {"data-name", "google"}], ["Google"]}], "a[data-name]", "data-name") |
| 633 | - ["google"] |
| 641 | + iex> Floki.attribute( |
| 642 | + iex> [{"a", [{"href", "https://e.corp.com"}, {"data-name", "e.corp"}], ["E.Corp"]}], |
| 643 | + iex> "a[data-name]", |
| 644 | + iex> "data-name" |
| 645 | + iex> ) |
| 646 | + ["e.corp"] |
| 634 647 | """ |
| 635 648 | |
| 636 649 | @spec attribute(binary | html_tree | html_node, binary, binary) :: list |
| @@ -62,15 +62,51 @@ defmodule Floki.Entities do | |
| 62 62 | * greater-than sign - > - is replaced by ">". |
| 63 63 | |
| 64 64 | All other simbols are going to remain the same. |
| 65 | + |
| 66 | + Optimized IO data implementation from Plug.HTML |
| 65 67 | """ |
| 66 | - @spec encode(String.t()) :: String.t() |
| 67 | - def encode(string) when is_binary(string) do |
| 68 | - String.replace(string, ["'", "\"", "&", "<", ">"], fn |
| 69 | - "'" -> "'" |
| 70 | - "\"" -> """ |
| 71 | - "&" -> "&" |
| 72 | - "<" -> "<" |
| 73 | - ">" -> ">" |
| 74 | - end) |
| 68 | + @spec encode(iodata()) :: iodata() |
| 69 | + def encode(string) when is_binary(string), do: encode(string, 0, string, []) |
| 70 | + def encode(data), do: encode(IO.iodata_to_binary(data)) |
| 71 | + |
| 72 | + escapes = [ |
| 73 | + {?<, "<"}, |
| 74 | + {?>, ">"}, |
| 75 | + {?&, "&"}, |
| 76 | + {?", """}, |
| 77 | + {?', "'"} |
| 78 | + ] |
| 79 | + |
| 80 | + for {match, insert} <- escapes do |
| 81 | + defp encode(<<unquote(match), rest::bits>>, skip, original, acc) do |
| 82 | + encode(rest, skip + 1, original, [acc | unquote(insert)]) |
| 83 | + end |
| 84 | + end |
| 85 | + |
| 86 | + defp encode(<<_char, rest::bits>>, skip, original, acc) do |
| 87 | + encode(rest, skip, original, acc, 1) |
| 88 | + end |
| 89 | + |
| 90 | + defp encode(<<>>, _skip, _original, acc) do |
| 91 | + acc |
| 92 | + end |
| 93 | + |
| 94 | + for {match, insert} <- escapes do |
| 95 | + defp encode(<<unquote(match), rest::bits>>, skip, original, acc, len) do |
| 96 | + part = binary_part(original, skip, len) |
| 97 | + encode(rest, skip + len + 1, original, [acc, part | unquote(insert)]) |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + defp encode(<<_char, rest::bits>>, skip, original, acc, len) do |
| 102 | + encode(rest, skip, original, acc, len + 1) |
| 103 | + end |
| 104 | + |
| 105 | + defp encode(<<>>, 0, original, _acc, _len) do |
| 106 | + original |
| 107 | + end |
| 108 | + |
| 109 | + defp encode(<<>>, skip, original, acc, len) do |
| 110 | + [acc | binary_part(original, skip, len)] |
| 75 111 | end |
| 76 112 | end |
| @@ -16,137 +16,91 @@ defmodule Floki.Finder do | |
| 16 16 | def find(html_as_string, _) when is_binary(html_as_string), do: {%HTMLTree{}, []} |
| 17 17 | |
| 18 18 | def find(html_tree, selector_as_string) when is_binary(selector_as_string) do |
| 19 | - selectors = get_selectors(selector_as_string) |
| 20 | - find_selectors(html_tree, selectors) |
| 21 | - end |
| 22 | - |
| 23 | - def find(html_tree, selectors) when is_list(selectors) do |
| 24 | - find_selectors(html_tree, selectors) |
| 19 | + selectors = Selector.Parser.parse(selector_as_string) |
| 20 | + find(html_tree, selectors) |
| 25 21 | end |
| 26 22 | |
| 27 23 | def find(html_tree, selector = %Selector{}) do |
| 28 | - find_selectors(html_tree, [selector]) |
| 24 | + find(html_tree, [selector]) |
| 29 25 | end |
| 30 26 | |
| 31 | - @spec map(Floki.html_tree() | Floki.html_node(), function()) :: |
| 32 | - Floki.html_tree() | Floki.html_node() |
| 27 | + def find(html_tree, selectors) when is_list(selectors) do |
| 28 | + tree = HTMLTree.build(html_tree) |
| 33 29 | |
| 34 | - def map({name, attrs, rest}, fun) do |
| 35 | - {new_name, new_attrs} = fun.({name, attrs}) |
| 36 | - |
| 37 | - {new_name, new_attrs, Enum.map(rest, &map(&1, fun))} |
| 38 | - end |
| 39 | - |
| 40 | - def map(other, _fun), do: other |
| 41 | - |
| 42 | - defp find_selectors(html_tuple_or_list, selectors) do |
| 43 | - tree = HTMLTree.build(html_tuple_or_list) |
| 30 | + node_ids = Enum.reverse(tree.node_ids) |
| 31 | + stack = Enum.map(selectors, fn s -> {s, node_ids} end) |
| 44 32 | |
| 45 33 | results = |
| 46 | - tree.node_ids |
| 34 | + traverse_with(:cont, tree, [], stack) |
| 47 35 | |> Enum.reverse() |
| 48 | - |> get_nodes(tree) |
| 49 | - |> Enum.flat_map(fn html_node -> get_matches_for_selectors(tree, html_node, selectors) end) |
| 50 36 | |> Enum.uniq() |
| 51 37 | |
| 52 38 | {tree, results} |
| 53 39 | end |
| 54 40 | |
| 55 | - defp get_selectors(selector_as_string) do |
| 56 | - selector_as_string |
| 57 | - |> Selector.Tokenizer.tokenize() |
| 58 | - |> Selector.Parser.parse() |
| 59 | - end |
| 60 | - |
| 61 | - defp get_matches_for_selectors(tree, html_node, selectors) do |
| 62 | - Enum.flat_map(selectors, fn selector -> get_matches(tree, html_node, selector) end) |
| 63 | - end |
| 64 | - |
| 65 | - defp get_matches(tree, html_node, selector = %Selector{combinator: nil}) do |
| 66 | - if selector_match?(tree, html_node, selector) do |
| 67 | - [html_node] |
| 68 | - else |
| 69 | - [] |
| 70 | - end |
| 71 | - end |
| 72 | - |
| 73 | - defp get_matches(tree, html_node, selector = %Selector{combinator: combinator}) do |
| 74 | - if selector_match?(tree, html_node, selector) do |
| 75 | - traverse_with(combinator, tree, [html_node]) |
| 76 | - else |
| 77 | - [] |
| 78 | - end |
| 79 | - end |
| 80 | - |
| 81 | - defp selector_match?(tree, html_node, selector) do |
| 82 | - Selector.match?(html_node, selector, tree) |
| 83 | - end |
| 84 | - |
| 85 41 | # The stack serves as accumulator when there is another combinator to traverse. |
| 86 42 | # So the scope of one combinator is the stack (or acc) or the parent one. |
| 87 | - defp traverse_with(_, _, []), do: [] |
| 88 | - defp traverse_with(nil, _, results), do: results |
| 89 | - |
| 90 | - defp traverse_with(%Selector.Combinator{match_type: :child, selector: s}, tree, stack) do |
| 91 | - results = |
| 92 | - Enum.flat_map(stack, fn html_node -> |
| 93 | - nodes = |
| 94 | - html_node.children_nodes_ids |
| 95 | - |> Enum.reverse() |
| 96 | - |> get_nodes(tree) |
| 97 | - |
| 98 | - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) |
| 99 | - end) |
| 100 | - |
| 101 | - traverse_with(s.combinator, tree, results) |
| 43 | + defp traverse_with(:cont, _, acc, []) do |
| 44 | + acc |
| 102 45 | end |
| 103 46 | |
| 104 | - defp traverse_with(%Selector.Combinator{match_type: :sibling, selector: s}, tree, stack) do |
| 105 | - results = |
| 106 | - Enum.flat_map(stack, fn html_node -> |
| 107 | - # It treats sibling as list to easily ignores those that didn't match |
| 108 | - sibling_id = |
| 109 | - html_node |
| 110 | - |> get_siblings(tree) |
| 111 | - |> Enum.take(1) |
| 112 | - |
| 113 | - nodes = get_nodes(sibling_id, tree) |
| 114 | - |
| 115 | - # Finally, try to match those siblings with the selector |
| 116 | - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) |
| 117 | - end) |
| 118 | - |
| 119 | - traverse_with(s.combinator, tree, results) |
| 47 | + defp traverse_with(:cont, tree, acc, [next | rest]) do |
| 48 | + traverse_with(next, tree, acc, rest) |
| 120 49 | end |
| 121 50 | |
| 122 | - defp traverse_with(%Selector.Combinator{match_type: :general_sibling, selector: s}, tree, stack) do |
| 123 | - results = |
| 124 | - Enum.flat_map(stack, fn html_node -> |
| 125 | - sibling_ids = get_siblings(html_node, tree) |
| 126 | - |
| 127 | - nodes = get_nodes(sibling_ids, tree) |
| 128 | - |
| 129 | - # Finally, try to match those siblings with the selector |
| 130 | - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) |
| 131 | - end) |
| 132 | - |
| 133 | - traverse_with(s.combinator, tree, results) |
| 51 | + defp traverse_with({selector, [node_id]}, tree, acc, stack) do |
| 52 | + traverse_with({selector, node_id}, tree, acc, stack) |
| 134 53 | end |
| 135 54 | |
| 136 | - defp traverse_with(%Selector.Combinator{match_type: :descendant, selector: s}, tree, stack) do |
| 137 | - results = |
| 138 | - Enum.flat_map(stack, fn html_node -> |
| 139 | - ids_to_match = get_descendant_ids(html_node.node_id, tree) |
| 140 | - nodes = get_nodes(ids_to_match, tree) |
| 141 | - |
| 142 | - Enum.filter(nodes, fn html_node -> selector_match?(tree, html_node, s) end) |
| 143 | - end) |
| 144 | - |
| 145 | - traverse_with(s.combinator, tree, results) |
| 55 | + defp traverse_with({selector, [next | rest]}, tree, acc, stack) do |
| 56 | + stack = [{selector, rest} | stack] |
| 57 | + traverse_with({selector, next}, tree, acc, stack) |
| 146 58 | end |
| 147 59 | |
| 148 | - defp get_nodes(ids, tree) do |
| 149 | - Enum.map(ids, fn id -> Map.get(tree.nodes, id) end) |
| 60 | + defp traverse_with({%Selector{combinator: nil} = selector, node_id}, tree, acc, stack) do |
| 61 | + html_node = get_node(node_id, tree) |
| 62 | + |
| 63 | + acc = |
| 64 | + if Selector.match?(html_node, selector, tree) do |
| 65 | + [html_node | acc] |
| 66 | + else |
| 67 | + acc |
| 68 | + end |
| 69 | + |
| 70 | + traverse_with(:cont, tree, acc, stack) |
| 71 | + end |
| 72 | + |
| 73 | + defp traverse_with({%Selector{combinator: combinator} = selector, node_id}, tree, acc, stack) do |
| 74 | + html_node = get_node(node_id, tree) |
| 75 | + |
| 76 | + stack = |
| 77 | + if Selector.match?(html_node, selector, tree) do |
| 78 | + nodes = get_selector_nodes(combinator, html_node, tree) |
| 79 | + [{combinator.selector, nodes} | stack] |
| 80 | + else |
| 81 | + stack |
| 82 | + end |
| 83 | + |
| 84 | + traverse_with(:cont, tree, acc, stack) |
| 85 | + end |
| 86 | + |
| 87 | + defp get_selector_nodes(%Selector.Combinator{match_type: :child}, html_node, _tree) do |
| 88 | + Enum.reverse(html_node.children_nodes_ids) |
| 89 | + end |
| 90 | + |
| 91 | + defp get_selector_nodes(%Selector.Combinator{match_type: :sibling}, html_node, tree) do |
| 92 | + case get_siblings(html_node, tree) do |
| 93 | + [sibling_id | _] -> sibling_id |
| 94 | + _ -> nil |
| 95 | + end |
| 96 | + end |
| 97 | + |
| 98 | + defp get_selector_nodes(%Selector.Combinator{match_type: :general_sibling}, html_node, tree) do |
| 99 | + get_siblings(html_node, tree) |
| 100 | + end |
| 101 | + |
| 102 | + defp get_selector_nodes(%Selector.Combinator{match_type: :descendant}, html_node, tree) do |
| 103 | + get_descendant_ids(html_node.node_id, tree) |
| 150 104 | end |
| 151 105 | |
| 152 106 | defp get_node(id, tree) do |
| @@ -191,4 +145,15 @@ defmodule Floki.Finder do | |
| 191 145 | [] |
| 192 146 | end |
| 193 147 | end |
| 148 | + |
| 149 | + @spec map(Floki.html_tree() | Floki.html_node(), function()) :: |
| 150 | + Floki.html_tree() | Floki.html_node() |
| 151 | + |
| 152 | + def map({name, attrs, rest}, fun) do |
| 153 | + {new_name, new_attrs} = fun.({name, attrs}) |
| 154 | + |
| 155 | + {new_name, new_attrs, Enum.map(rest, &map(&1, fun))} |
| 156 | + end |
| 157 | + |
| 158 | + def map(other, _fun), do: other |
| 194 159 | end |
Loading more files…