Packages
floki
0.36.0
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
9
files changed
+232
additions
-34
deletions
| @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. | |
| 7 7 | |
| 8 8 | ## [Unreleased][unreleased] |
| 9 9 | |
| 10 | + ## [0.36.0] - 2024-03-01 |
| 11 | + |
| 12 | + ### Added |
| 13 | + |
| 14 | + - Add `Floki.get_by_id/1` that returns one element by ID or `nil`. |
| 15 | + Thanks [@SteffenDE](https://github.com/SteffenDE). |
| 16 | + |
| 17 | + ### Changed |
| 18 | + |
| 19 | + - Improve options validation with `Keyword.validate!/2`. |
| 20 | + This is not a change in APIs, but the error messages and opts validation |
| 21 | + should be standardized now. |
| 22 | + Thanks [@vittoriabitton](https://github.com/vittoriabitton). |
| 23 | + |
| 24 | + ### Removed |
| 25 | + |
| 26 | + - Drop support for Elixir v1.12. |
| 27 | + |
| 10 28 | ## [0.35.4] - 2024-02-19 |
| 11 29 | |
| 12 30 | Besides the fix described below, this release also contains more performance |
| @@ -746,7 +764,8 @@ of the parent element inside HTML. | |
| 746 764 | |
| 747 765 | - Elixir version requirement from "~> 1.0.0" to ">= 1.0.0". |
| 748 766 | |
| 749 | - [unreleased]: https://github.com/philss/floki/compare/v0.35.4...HEAD |
| 767 | + [unreleased]: https://github.com/philss/floki/compare/v0.36.0...HEAD |
| 768 | + [0.36.0]: https://github.com/philss/floki/compare/v0.35.4...v0.36.0 |
| 750 769 | [0.35.4]: https://github.com/philss/floki/compare/v0.35.3...v0.35.4 |
| 751 770 | [0.35.3]: https://github.com/philss/floki/compare/v0.35.2...v0.35.3 |
| 752 771 | [0.35.2]: https://github.com/philss/floki/compare/v0.35.1...v0.35.2 |
| @@ -61,13 +61,21 @@ Add Floki to your `mix.exs`: | |
| 61 61 | ```elixir |
| 62 62 | defp deps do |
| 63 63 | [ |
| 64 | - {:floki, "~> 0.35.0"} |
| 64 | + {:floki, "~> 0.36.0"} |
| 65 65 | ] |
| 66 66 | end |
| 67 67 | ``` |
| 68 68 | |
| 69 69 | After that, run `mix deps.get`. |
| 70 70 | |
| 71 | + If you are running on [Livebook](https://livebook.dev) or a script, you can install with `Mix.install/2`: |
| 72 | + |
| 73 | + ```elixir |
| 74 | + Mix.install([ |
| 75 | + {:floki, "~> 0.36.0"} |
| 76 | + ]) |
| 77 | + ``` |
| 78 | + |
| 71 79 | You can check the [changelog](CHANGELOG.md) for changes. |
| 72 80 | |
| 73 81 | ## Dependencies |
| @@ -118,7 +126,7 @@ you don't need to install anything to compile it thanks to [RustlerPrecompiled]( | |
| 118 126 | ```elixir |
| 119 127 | defp deps do |
| 120 128 | [ |
| 121 | - {:floki, "~> 0.35.0"}, |
| 129 | + {:floki, "~> 0.36.0"}, |
| 122 130 | {:html5ever, "~> 0.15.0"} |
| 123 131 | ] |
| 124 132 | end |
| @@ -146,7 +154,7 @@ First, add `fast_html` to your dependencies: | |
| 146 154 | ```elixir |
| 147 155 | defp deps do |
| 148 156 | [ |
| 149 | - {:floki, "~> 0.35.0"}, |
| 157 | + {:floki, "~> 0.36.0"}, |
| 150 158 | {:fast_html, "~> 2.0"} |
| 151 159 | ] |
| 152 160 | end |
| @@ -3,10 +3,10 @@ | |
| 3 3 | {<<"GitHub">>,<<"https://github.com/philss/floki">>}, |
| 4 4 | {<<"Sponsor">>,<<"https://github.com/sponsors/philss">>}]}. |
| 5 5 | {<<"name">>,<<"floki">>}. |
| 6 | - {<<"version">>,<<"0.35.4">>}. |
| 6 | + {<<"version">>,<<"0.36.0">>}. |
| 7 7 | {<<"description">>, |
| 8 8 | <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}. |
| 9 | - {<<"elixir">>,<<"~> 1.12">>}. |
| 9 | + {<<"elixir">>,<<"~> 1.13">>}. |
| 10 10 | {<<"app">>,<<"floki">>}. |
| 11 11 | {<<"licenses">>,[<<"MIT">>]}. |
| 12 12 | {<<"files">>, |
| @@ -289,6 +289,32 @@ defmodule Floki do | |
| 289 289 | Finder.find(html_tree_as_tuple, selector) |
| 290 290 | end |
| 291 291 | |
| 292 | + @doc """ |
| 293 | + Finds the first element in an HTML tree by id. |
| 294 | + |
| 295 | + Returns `nil` if no element is found. |
| 296 | + |
| 297 | + This is useful when there are IDs that contain special characters that |
| 298 | + are invalid when passed as is as a CSS selector. |
| 299 | + It is similar to the `getElementById` method in the browser. |
| 300 | + |
| 301 | + ## Examples |
| 302 | + |
| 303 | + iex> {:ok, html} = Floki.parse_fragment(~s[<p><span class="hint" id="id?foo_special:chars">hello</span></p>]) |
| 304 | + iex> Floki.get_by_id(html, "id?foo_special:chars") |
| 305 | + {"span", [{"class", "hint"}, {"id", "id?foo_special:chars"}], ["hello"]} |
| 306 | + iex> Floki.get_by_id(html, "does-not-exist") |
| 307 | + nil |
| 308 | + |
| 309 | + """ |
| 310 | + @spec get_by_id(html_tree() | html_node(), String.t()) :: html_tree |
| 311 | + def get_by_id(html_tree_as_tuple, id) |
| 312 | + when is_list(html_tree_as_tuple) or is_html_node(html_tree_as_tuple) do |
| 313 | + html_tree_as_tuple |
| 314 | + |> Finder.find(%Floki.Selector{id: id}) |
| 315 | + |> List.first() |
| 316 | + end |
| 317 | + |
| 292 318 | @doc """ |
| 293 319 | Changes the attribute values of the elements matched by `selector` |
| 294 320 | with the function `mutation` and returns the whole element tree. |
| @@ -570,8 +596,7 @@ defmodule Floki do | |
| 570 596 | def text(html, opts \\ []) do |
| 571 597 | defaults = [deep: true, js: false, style: true, sep: "", include_inputs: false] |
| 572 598 | |
| 573 | - # We can use `Keyword.validate!` when require Elixir 1.13 |
| 574 | - opts = Keyword.merge(defaults, opts) |
| 599 | + opts = Keyword.validate!(opts, defaults) |
| 575 600 | |
| 576 601 | cleaned_html_tree = |
| 577 602 | html |
| @@ -617,6 +642,8 @@ defmodule Floki do | |
| 617 642 | end |
| 618 643 | |
| 619 644 | def children({_, _, _} = html_node, opts) do |
| 645 | + opts = Keyword.validate!(opts, include_text: true) |
| 646 | + |
| 620 647 | children(html_node, include_text: opts[:include_text]) |
| 621 648 | end |
| @@ -54,7 +54,7 @@ defmodule Floki.Finder do | |
| 54 54 | # some selectors can be applied with the raw html tree tuples instead of |
| 55 55 | # using an intermediate HTMLTree: |
| 56 56 | # - single selector |
| 57 | - # - no composite selector |
| 57 | + # - single child or adjacent sibling combinator, and as the last combinator |
| 58 58 | # - no pseudo classes |
| 59 59 | defp traverse_html_tuples?([selector]), do: traverse_html_tuples?(selector) |
| 60 60 | defp traverse_html_tuples?(selectors) when is_list(selectors), do: false |
| @@ -64,8 +64,16 @@ defmodule Floki.Finder do | |
| 64 64 | defp traverse_html_tuples?(%Selector{combinator: combinator}), |
| 65 65 | do: traverse_html_tuples?(combinator) |
| 66 66 | |
| 67 | - defp traverse_html_tuples?(%Selector.Combinator{match_type: :descendant, selector: selector}), |
| 68 | - do: traverse_html_tuples?(selector) |
| 67 | + defp traverse_html_tuples?(%Selector.Combinator{match_type: match_type, selector: selector}) |
| 68 | + when match_type in [:descendant, :general_sibling], |
| 69 | + do: traverse_html_tuples?(selector) |
| 70 | + |
| 71 | + defp traverse_html_tuples?(%Selector.Combinator{ |
| 72 | + match_type: match_type, |
| 73 | + selector: %Selector{combinator: nil} = selector |
| 74 | + }) |
| 75 | + when match_type in [:child, :adjacent_sibling], |
| 76 | + do: traverse_html_tuples?(selector) |
| 69 77 | |
| 70 78 | defp traverse_html_tuples?(_), do: false |
| 71 79 | |
| @@ -116,17 +124,26 @@ defmodule Floki.Finder do | |
| 116 124 | acc |
| 117 125 | end |
| 118 126 | |
| 127 | + # `stack` is a list of tuples composed of a Selector or Selector.Combinator |
| 128 | + # and html_node tuple. |
| 129 | + # When a selector has a combinator with match type descendant or |
| 130 | + # general_sibling we are able to use the combinator selector directly to add |
| 131 | + # it's siblings or children to the stack when there's a match. |
| 132 | + # For selectors with child and adjacent_sibling combinators we have to make |
| 133 | + # sure we don't propagate the selector to more elements than the combinator |
| 134 | + # specifies. For matches of these combinators we put the Selector.Combinator |
| 135 | + # term to the stack to keep track of this information. |
| 119 136 | defp traverse_html_tuples( |
| 120 137 | [ |
| 121 138 | { |
| 122 139 | %Selector{combinator: nil} = selector, |
| 123 | - [{_type, _attributes, children} = html_tuple | selector_rest] |
| 140 | + [{_type, _attributes, children} = html_tuple | siblings] |
| 124 141 | } |
| 125 142 | | stack |
| 126 143 | ], |
| 127 144 | acc |
| 128 145 | ) do |
| 129 | - stack = [{selector, children}, {selector, selector_rest} | stack] |
| 146 | + stack = [{selector, children}, {selector, siblings} | stack] |
| 130 147 | |
| 131 148 | acc = |
| 132 149 | if Selector.match?(html_tuple, selector, nil) do |
| @@ -147,13 +164,13 @@ defmodule Floki.Finder do | |
| 147 164 | selector: combinator_selector |
| 148 165 | } |
| 149 166 | } = selector, |
| 150 | - [{_type, _attributes, children} = html_tuple | selector_rest] |
| 167 | + [{_type, _attributes, children} = html_tuple | siblings] |
| 151 168 | } |
| 152 169 | | stack |
| 153 170 | ], |
| 154 171 | acc |
| 155 172 | ) do |
| 156 | - stack = [{selector, selector_rest} | stack] |
| 173 | + stack = [{selector, siblings} | stack] |
| 157 174 | |
| 158 175 | stack = |
| 159 176 | if Selector.match?(html_tuple, selector, nil) do |
| @@ -168,14 +185,133 @@ defmodule Floki.Finder do | |
| 168 185 | defp traverse_html_tuples( |
| 169 186 | [ |
| 170 187 | { |
| 171 | - selector, |
| 172 | - [_ | selector_rest] |
| 188 | + %Selector{ |
| 189 | + combinator: %Selector.Combinator{match_type: :child} = combinator |
| 190 | + } = selector, |
| 191 | + [{_type, _attributes, children} = html_tuple | siblings] |
| 173 192 | } |
| 174 193 | | stack |
| 175 194 | ], |
| 176 195 | acc |
| 177 196 | ) do |
| 178 | - stack = [{selector, selector_rest} | stack] |
| 197 | + stack = [{selector, children}, {selector, siblings} | stack] |
| 198 | + |
| 199 | + stack = |
| 200 | + if Selector.match?(html_tuple, selector, nil) do |
| 201 | + [{combinator, children} | stack] |
| 202 | + else |
| 203 | + stack |
| 204 | + end |
| 205 | + |
| 206 | + traverse_html_tuples(stack, acc) |
| 207 | + end |
| 208 | + |
| 209 | + defp traverse_html_tuples( |
| 210 | + [ |
| 211 | + { |
| 212 | + %Selector{ |
| 213 | + combinator: %Selector.Combinator{match_type: :adjacent_sibling} = combinator |
| 214 | + } = selector, |
| 215 | + [{_type, _attributes, children} = html_tuple | siblings] |
| 216 | + } |
| 217 | + | stack |
| 218 | + ], |
| 219 | + acc |
| 220 | + ) do |
| 221 | + stack = |
| 222 | + if Selector.match?(html_tuple, selector, nil) do |
| 223 | + [{combinator, siblings} | stack] |
| 224 | + else |
| 225 | + stack |
| 226 | + end |
| 227 | + |
| 228 | + stack = [{selector, children}, {selector, siblings} | stack] |
| 229 | + |
| 230 | + traverse_html_tuples(stack, acc) |
| 231 | + end |
| 232 | + |
| 233 | + defp traverse_html_tuples( |
| 234 | + [ |
| 235 | + { |
| 236 | + %Selector{ |
| 237 | + combinator: %Selector.Combinator{ |
| 238 | + match_type: :general_sibling, |
| 239 | + selector: combinator_selector |
| 240 | + } |
| 241 | + } = selector, |
| 242 | + [{_type, _attributes, children} = html_tuple | siblings] |
| 243 | + } |
| 244 | + | stack |
| 245 | + ], |
| 246 | + acc |
| 247 | + ) do |
| 248 | + stack = |
| 249 | + if Selector.match?(html_tuple, selector, nil) do |
| 250 | + [{combinator_selector, siblings} | stack] |
| 251 | + else |
| 252 | + [{selector, siblings} | stack] |
| 253 | + end |
| 254 | + |
| 255 | + stack = [{selector, children} | stack] |
| 256 | + |
| 257 | + traverse_html_tuples(stack, acc) |
| 258 | + end |
| 259 | + |
| 260 | + defp traverse_html_tuples( |
| 261 | + [ |
| 262 | + { |
| 263 | + %Selector.Combinator{match_type: :child, selector: selector} = combinator, |
| 264 | + [{_type, _attributes, _children} = html_tuple | siblings] |
| 265 | + } |
| 266 | + | stack |
| 267 | + ], |
| 268 | + acc |
| 269 | + ) do |
| 270 | + stack = [{combinator, siblings} | stack] |
| 271 | + |
| 272 | + acc = |
| 273 | + if Selector.match?(html_tuple, selector, nil) do |
| 274 | + [html_tuple | acc] |
| 275 | + else |
| 276 | + acc |
| 277 | + end |
| 278 | + |
| 279 | + traverse_html_tuples(stack, acc) |
| 280 | + end |
| 281 | + |
| 282 | + defp traverse_html_tuples( |
| 283 | + [ |
| 284 | + { |
| 285 | + %Selector.Combinator{match_type: :adjacent_sibling, selector: selector}, |
| 286 | + [{_type, _attributes, _children} = html_tuple | _siblings] |
| 287 | + } |
| 288 | + | stack |
| 289 | + ], |
| 290 | + acc |
| 291 | + ) do |
| 292 | + # adjacent_sibling combinator targets only the first html_tag, so we don't |
| 293 | + # add the siblings back to the stack |
| 294 | + acc = |
| 295 | + if Selector.match?(html_tuple, selector, nil) do |
| 296 | + [html_tuple | acc] |
| 297 | + else |
| 298 | + acc |
| 299 | + end |
| 300 | + |
| 301 | + traverse_html_tuples(stack, acc) |
| 302 | + end |
| 303 | + |
| 304 | + defp traverse_html_tuples( |
| 305 | + [ |
| 306 | + { |
| 307 | + selector, |
| 308 | + [_ | siblings] |
| 309 | + } |
| 310 | + | stack |
| 311 | + ], |
| 312 | + acc |
| 313 | + ) do |
| 314 | + stack = [{selector, siblings} | stack] |
| 179 315 | traverse_html_tuples(stack, acc) |
| 180 316 | end |
| 181 317 | |
| @@ -191,7 +327,7 @@ defmodule Floki.Finder do | |
| 191 327 | Enum.reverse(html_node.children_nodes_ids) |
| 192 328 | end |
| 193 329 | |
| 194 | - defp get_selector_nodes(%Selector.Combinator{match_type: :sibling}, html_node, tree) do |
| 330 | + defp get_selector_nodes(%Selector.Combinator{match_type: :adjacent_sibling}, html_node, tree) do |
| 195 331 | case get_siblings(html_node, tree) do |
| 196 332 | [sibling_id | _] -> [sibling_id] |
| 197 333 | _ -> [] |
Loading more files…