Current section

86 Versions

Jump to

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…