Current section

86 Versions

Jump to

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…