Packages
floki
0.37.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
6
files changed
+132
additions
-24
deletions
| @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. | |
| 7 7 | |
| 8 8 | ## [Unreleased][unreleased] |
| 9 9 | |
| 10 | + ## [0.37.0] - 2024-12-06 |
| 11 | + |
| 12 | + ### Added |
| 13 | + |
| 14 | + - Add `Floki.css_escape/1` - thanks [@SteffenDE](https://github.com/SteffenDE). |
| 15 | + |
| 16 | + ### Fixed |
| 17 | + |
| 18 | + - Fix bug propagating identity encoder in `raw_html/2` - thanks [@andyleclair](https://github.com/andyleclair). |
| 19 | + |
| 20 | + ### Removed |
| 21 | + |
| 22 | + - Remove support for Elixir 1.13 and OTP 22. |
| 23 | + |
| 10 24 | ## [0.36.3] - 2024-10-21 |
| 11 25 | |
| 12 26 | This release contains some performance improvements, thanks to [@ypconstante](https://github.com/ypconstante). |
| @@ -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.36.3">>}. |
| 6 | + {<<"version">>,<<"0.37.0">>}. |
| 7 7 | {<<"description">>, |
| 8 8 | <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}. |
| 9 | - {<<"elixir">>,<<"~> 1.13">>}. |
| 9 | + {<<"elixir">>,<<"~> 1.14">>}. |
| 10 10 | {<<"app">>,<<"floki">>}. |
| 11 11 | {<<"licenses">>,[<<"MIT">>]}. |
| 12 12 | {<<"files">>, |
| @@ -24,14 +24,14 @@ | |
| 24 24 | <<"lib/floki/html_tree/id_seeder.ex">>,<<"lib/floki/html_tree/comment.ex">>, |
| 25 25 | <<"lib/floki/html_tree/html_node.ex">>,<<"lib/floki/html_tree/text.ex">>, |
| 26 26 | <<"lib/floki/finder.ex">>,<<"lib/floki/filter_out.ex">>, |
| 27 | - <<"lib/floki/html_tree.ex">>,<<"lib/floki/html">>, |
| 28 | - <<"lib/floki/html/numeric_charref.ex">>,<<"lib/floki/html/tokenizer.ex">>, |
| 29 | - <<"lib/floki/html_parser.ex">>,<<"lib/floki/deep_text.ex">>, |
| 30 | - <<"lib/floki/entities.ex">>,<<"lib/floki/flat_text.ex">>, |
| 31 | - <<"lib/floki/parse_error.ex">>,<<"lib/floki/entities">>, |
| 32 | - <<"lib/floki/entities/codepoints.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">>]}. |
| 27 | + <<"lib/floki/html_tree.ex">>,<<"lib/floki/css_escape.ex">>, |
| 28 | + <<"lib/floki/html">>,<<"lib/floki/html/numeric_charref.ex">>, |
| 29 | + <<"lib/floki/html/tokenizer.ex">>,<<"lib/floki/html_parser.ex">>, |
| 30 | + <<"lib/floki/deep_text.ex">>,<<"lib/floki/entities.ex">>, |
| 31 | + <<"lib/floki/flat_text.ex">>,<<"lib/floki/parse_error.ex">>, |
| 32 | + <<"lib/floki/entities">>,<<"lib/floki/entities/codepoints.ex">>, |
| 33 | + <<"lib/floki.ex">>,<<"src/floki_selector_lexer.xrl">>, |
| 34 | + <<"src/floki_mochi_html.erl">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>, |
| 35 | + <<"CODE_OF_CONDUCT.md">>,<<"CONTRIBUTING.md">>,<<"CHANGELOG.md">>]}. |
| 36 36 | {<<"requirements">>,[]}. |
| 37 37 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -790,4 +790,19 @@ defmodule Floki do | |
| 790 790 | def filter_out(elements, selector) do |
| 791 791 | FilterOut.filter_out(elements, selector) |
| 792 792 | end |
| 793 | + |
| 794 | + @doc """ |
| 795 | + Escapes a string for use as a CSS identifier. |
| 796 | + |
| 797 | + ## Examples |
| 798 | + |
| 799 | + iex> Floki.css_escape("hello world") |
| 800 | + "hello\\\\ world" |
| 801 | + |
| 802 | + iex> Floki.css_escape("-123") |
| 803 | + "-\\\\31 23" |
| 804 | + |
| 805 | + """ |
| 806 | + @spec css_escape(String.t()) :: String.t() |
| 807 | + def css_escape(value), do: Floki.CSSEscape.escape(value) |
| 793 808 | end |
| @@ -0,0 +1,77 @@ | |
| 1 | + defmodule Floki.CSSEscape do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + # This is a direct translation of |
| 5 | + # https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js |
| 6 | + # into Elixir. |
| 7 | + |
| 8 | + @doc """ |
| 9 | + Escapes a string for use as a CSS identifier. |
| 10 | + |
| 11 | + ## Examples |
| 12 | + |
| 13 | + iex> Floki.CSSEscape.escape("hello world") |
| 14 | + "hello\\\\ world" |
| 15 | + |
| 16 | + iex> Floki.CSSEscape.escape("-123") |
| 17 | + "-\\\\31 23" |
| 18 | + |
| 19 | + """ |
| 20 | + @spec escape(String.t()) :: String.t() |
| 21 | + def escape(value) when is_binary(value) do |
| 22 | + value |
| 23 | + |> String.to_charlist() |
| 24 | + |> escape_chars() |
| 25 | + |> IO.iodata_to_binary() |
| 26 | + end |
| 27 | + |
| 28 | + def escape(_), do: raise(ArgumentError, "CSS.escape requires a string argument") |
| 29 | + |
| 30 | + defp escape_chars(chars) do |
| 31 | + case chars do |
| 32 | + # If the character is the first character and is a `-` (U+002D), and |
| 33 | + # there is no second character, […] |
| 34 | + [?- | []] -> ["\\-"] |
| 35 | + _ -> do_escape_chars(chars, 0, []) |
| 36 | + end |
| 37 | + end |
| 38 | + |
| 39 | + defp do_escape_chars([], _, acc), do: Enum.reverse(acc) |
| 40 | + |
| 41 | + defp do_escape_chars([char | rest], index, acc) do |
| 42 | + escaped = |
| 43 | + cond do |
| 44 | + # If the character is NULL (U+0000), then the REPLACEMENT CHARACTER |
| 45 | + # (U+FFFD). |
| 46 | + char == 0 -> |
| 47 | + <<0xFFFD::utf8>> |
| 48 | + |
| 49 | + # If the character is in the range [\1-\1F] (U+0001 to U+001F) or is |
| 50 | + # U+007F, |
| 51 | + # if the character is the first character and is in the range [0-9] |
| 52 | + # (U+0030 to U+0039), |
| 53 | + # if the character is the second character and is in the range [0-9] |
| 54 | + # (U+0030 to U+0039) and the first character is a `-` (U+002D), |
| 55 | + char in 0x0001..0x001F or char == 0x007F or |
| 56 | + (index == 0 and char in ?0..?9) or |
| 57 | + (index == 1 and char in ?0..?9 and hd(acc) == "-") -> |
| 58 | + # https://drafts.csswg.org/cssom/#escape-a-character-as-code-point |
| 59 | + ["\\", Integer.to_string(char, 16), " "] |
| 60 | + |
| 61 | + # If the character is not handled by one of the above rules and is |
| 62 | + # greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or |
| 63 | + # is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to |
| 64 | + # U+005A), or [a-z] (U+0061 to U+007A), […] |
| 65 | + char >= 0x0080 or char in [?-, ?_] or char in ?0..?9 or char in ?A..?Z or char in ?a..?z -> |
| 66 | + # the character itself |
| 67 | + <<char::utf8>> |
| 68 | + |
| 69 | + true -> |
| 70 | + # Otherwise, the escaped character. |
| 71 | + # https://drafts.csswg.org/cssom/#escape-a-character |
| 72 | + ["\\", <<char::utf8>>] |
| 73 | + end |
| 74 | + |
| 75 | + do_escape_chars(rest, index + 1, [escaped | acc]) |
| 76 | + end |
| 77 | + end |
| @@ -133,14 +133,6 @@ defmodule Floki.RawHTML do | |
| 133 133 | self_closing_tags, |
| 134 134 | line_ending |
| 135 135 | ) do |
| 136 | - encoder = |
| 137 | - case type do |
| 138 | - "script" -> @no_encoder |
| 139 | - "style" -> @no_encoder |
| 140 | - "title" -> @no_encoder |
| 141 | - _ -> encoder |
| 142 | - end |
| 143 | - |
| 144 136 | open_tag_content = [ |
| 145 137 | tag_with_attrs(type, attrs, children, pad, encoder, self_closing_tags), |
| 146 138 | line_ending |
| @@ -156,10 +148,19 @@ defmodule Floki.RawHTML do | |
| 156 148 | _ -> |
| 157 149 | children = List.wrap(children) |
| 158 150 | |
| 151 | + curr_encoder = |
| 152 | + case type do |
| 153 | + "script" -> @no_encoder |
| 154 | + "style" -> @no_encoder |
| 155 | + "title" -> @no_encoder |
| 156 | + _ -> encoder |
| 157 | + end |
| 158 | + |
| 159 159 | build_raw_html( |
| 160 160 | children, |
| 161 161 | acc, |
| 162 | - encoder, |
| 162 | + # Need to make sure to pass the encoder for the current node |
| 163 | + curr_encoder, |
| 163 164 | pad_increase(pad), |
| 164 165 | self_closing_tags, |
| 165 166 | line_ending |
| @@ -168,6 +169,7 @@ defmodule Floki.RawHTML do | |
| 168 169 | |
| 169 170 | close_tag_content = close_end_tag(type, children, pad, self_closing_tags, line_ending) |
| 170 171 | acc = [close_tag_content | acc] |
| 172 | + # Return the original encoder here, we don't want to propagate that |
| 171 173 | build_raw_html(tail, acc, encoder, pad, self_closing_tags, line_ending) |
| 172 174 | end |
Loading more files…