Current section

86 Versions

Jump to

Compare versions

5 files changed
+842 additions
-17 deletions
  @@ -70,7 +70,7 @@ Add Floki to your `mix.exs`:
70 70 ```elixir
71 71 defp deps do
72 72 [
73 - {:floki, "~> 0.22.0"}
73 + {:floki, "~> 0.23.0"}
74 74 ]
75 75 end
76 76 ```
  @@ -105,7 +105,7 @@ After setup Rust, you need to add `html5ever` NIF to your dependency list:
105 105 ```elixir
106 106 defp deps do
107 107 [
108 - {:floki, "~> 0.22.0"},
108 + {:floki, "~> 0.23.0"},
109 109 {:html5ever, "~> 0.7.0"}
110 110 ]
111 111 end
  @@ -2,7 +2,7 @@
2 2 {<<"build_tools">>,[<<"mix">>]}.
3 3 {<<"description">>,
4 4 <<"Floki is a simple HTML parser that enables search for nodes using CSS selectors.">>}.
5 - {<<"elixir">>,<<">= 1.5.0">>}.
5 + {<<"elixir">>,<<"~> 1.5">>}.
6 6 {<<"files">>,
7 7 [<<"lib">>,<<"lib/floki">>,<<"lib/floki/html_parser.ex">>,
8 8 <<"lib/floki/html_parser">>,<<"lib/floki/html_parser/mochiweb.ex">>,
  @@ -18,20 +18,16 @@
18 18 <<"lib/floki/html_tree/id_seeder.ex">>,<<"lib/floki/html_tree/comment.ex">>,
19 19 <<"lib/floki/html_tree/text.ex">>,<<"lib/floki/html_tree/html_node.ex">>,
20 20 <<"lib/floki/selector.ex">>,<<"lib/floki/html_tree.ex">>,<<"lib/floki.ex">>,
21 - <<"src/floki_selector_lexer.xrl">>,<<"mix.exs">>,<<"README.md">>,
22 - <<"LICENSE">>,<<"CODE_OF_CONDUCT.md">>,<<"CONTRIBUTING.md">>]}.
21 + <<"src/floki_selector_lexer.xrl">>,<<"src/floki_mochi_html.erl">>,
22 + <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,<<"CODE_OF_CONDUCT.md">>,
23 + <<"CONTRIBUTING.md">>]}.
23 24 {<<"licenses">>,[<<"MIT">>]}.
24 25 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/philss/floki">>}]}.
25 26 {<<"name">>,<<"floki">>}.
26 27 {<<"requirements">>,
27 - [[{<<"app">>,<<"mochiweb">>},
28 - {<<"name">>,<<"mochiweb">>},
29 - {<<"optional">>,false},
30 - {<<"repository">>,<<"hexpm">>},
31 - {<<"requirement">>,<<"~> 2.15">>}],
32 - [{<<"app">>,<<"html_entities">>},
28 + [[{<<"app">>,<<"html_entities">>},
33 29 {<<"name">>,<<"html_entities">>},
34 30 {<<"optional">>,false},
35 31 {<<"repository">>,<<"hexpm">>},
36 32 {<<"requirement">>,<<"~> 0.4.0">>}]]}.
37 - {<<"version">>,<<"0.22.0">>}.
33 + {<<"version">>,<<"0.23.0">>}.
  @@ -4,7 +4,7 @@ defmodule Floki.HTMLParser.Mochiweb do
4 4
5 5 def parse(html) do
6 6 html = "<#{@root_node}>#{html}</#{@root_node}>"
7 - {@root_node, [], parsed} = :mochiweb_html.parse(html)
7 + {@root_node, [], parsed} = :floki_mochi_html.parse(html)
8 8
9 9 if length(parsed) == 1, do: hd(parsed), else: parsed
10 10 end
  @@ -2,7 +2,7 @@ defmodule Floki.Mixfile do
2 2 use Mix.Project
3 3
4 4 @description "Floki is a simple HTML parser that enables search for nodes using CSS selectors."
5 - @version "0.22.0"
5 + @version "0.23.0"
6 6
7 7 def project do
8 8 [
  @@ -10,7 +10,7 @@ defmodule Floki.Mixfile do
10 10 name: "Floki",
11 11 version: @version,
12 12 description: @description,
13 - elixir: ">= 1.5.0",
13 + elixir: "~> 1.5",
14 14 package: package(),
15 15 deps: deps(),
16 16 source_url: "https://github.com/philss/floki",
  @@ -19,12 +19,11 @@ defmodule Floki.Mixfile do
19 19 end
20 20
21 21 def application do
22 - [applications: [:logger, :mochiweb, :html_entities]]
22 + [extra_applications: [:logger]]
23 23 end
24 24
25 25 defp deps do
26 26 [
27 - {:mochiweb, "~> 2.15"},
28 27 {:html_entities, "~> 0.4.0"},
29 28 {:earmark, "~> 1.2", only: :dev},
30 29 {:ex_doc, "~> 0.18", only: :dev},
  @@ -40,6 +39,7 @@ defmodule Floki.Mixfile do
40 39 files: [
41 40 "lib",
42 41 "src/*.xrl",
42 + "src/floki_mochi_html.erl",
43 43 "mix.exs",
44 44 "README.md",
45 45 "LICENSE",
  @@ -0,0 +1,829 @@
1 + %% @author Bob Ippolito <bob@mochimedia.com>
2 + %% @copyright 2007 Mochi Media, Inc.
3 + %%
4 + %% Permission is hereby granted, free of charge, to any person obtaining a
5 + %% copy of this software and associated documentation files (the "Software"),
6 + %% to deal in the Software without restriction, including without limitation
7 + %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 + %% and/or sell copies of the Software, and to permit persons to whom the
9 + %% Software is furnished to do so, subject to the following conditions:
10 + %%
11 + %% The above copyright notice and this permission notice shall be included in
12 + %% all copies or substantial portions of the Software.
13 + %%
14 + %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 + %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 + %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17 + %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 + %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 + %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 + %% DEALINGS IN THE SOFTWARE.
21 +
22 + %% @doc Loosely tokenizes and generates parse trees for HTML 4.
23 + -module(floki_mochi_html).
24 + -export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1,
25 + escape_attr/1, to_html/1]).
26 + -ifdef(TEST).
27 + -export([destack/1, destack/2, is_singleton/1]).
28 + -endif.
29 +
30 + %% This is a macro to placate syntax highlighters..
31 + -define(QUOTE, $\"). %% $\"
32 + -define(SQUOTE, $\'). %% $\'
33 + -define(ADV_COL(S, N),
34 + S#decoder{column=N+S#decoder.column,
35 + offset=N+S#decoder.offset}).
36 + -define(INC_COL(S),
37 + S#decoder{column=1+S#decoder.column,
38 + offset=1+S#decoder.offset}).
39 + -define(INC_LINE(S),
40 + S#decoder{column=1,
41 + line=1+S#decoder.line,
42 + offset=1+S#decoder.offset}).
43 + -define(INC_CHAR(S, C),
44 + case C of
45 + $\n ->
46 + S#decoder{column=1,
47 + line=1+S#decoder.line,
48 + offset=1+S#decoder.offset};
49 + _ ->
50 + S#decoder{column=1+S#decoder.column,
51 + offset=1+S#decoder.offset}
52 + end).
53 +
54 + -define(IS_WHITESPACE(C),
55 + (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
56 + -define(IS_LETTER(C),
57 + ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z))).
58 + -define(IS_LITERAL_SAFE(C),
59 + ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z)
60 + orelse (C >= $0 andalso C =< $9))).
61 + -define(PROBABLE_CLOSE(C),
62 + (C =:= $> orelse ?IS_WHITESPACE(C))).
63 +
64 + -record(decoder, {line=1,
65 + column=1,
66 + offset=0}).
67 +
68 + %% @type html_node() = {string(), [html_attr()], [html_node() | string()]}
69 + %% @type html_attr() = {string(), string()}
70 + %% @type html_token() = html_data() | start_tag() | end_tag() | inline_html() | html_comment() | html_doctype()
71 + %% @type html_data() = {data, string(), Whitespace::boolean()}
72 + %% @type start_tag() = {start_tag, Name, [html_attr()], Singleton::boolean()}
73 + %% @type end_tag() = {end_tag, Name}
74 + %% @type html_comment() = {comment, Comment}
75 + %% @type html_doctype() = {doctype, [Doctype]}
76 + %% @type inline_html() = {'=', iolist()}
77 +
78 + %% External API.
79 +
80 + %% @spec parse(string() | binary()) -> html_node()
81 + %% @doc tokenize and then transform the token stream into a HTML tree.
82 + parse(Input) ->
83 + parse_tokens(tokens(Input)).
84 +
85 + %% @spec parse_tokens([html_token()]) -> html_node()
86 + %% @doc Transform the output of tokens(Doc) into a HTML tree.
87 + parse_tokens(Tokens) when is_list(Tokens) ->
88 + %% Skip over doctype, processing instructions
89 + [{start_tag, Tag, Attrs, false} | Rest] = find_document(Tokens, normal),
90 + {Tree, _} = tree(Rest, [norm({Tag, Attrs})]),
91 + Tree.
92 +
93 + find_document(Tokens=[{start_tag, _Tag, _Attrs, false} | _Rest], Mode) ->
94 + maybe_add_html_tag(Tokens, Mode);
95 + find_document([{doctype, [<<"html">>]} | Rest], _Mode) ->
96 + find_document(Rest, html5);
97 + find_document([_T | Rest], Mode) ->
98 + find_document(Rest, Mode);
99 + find_document([], _Mode) ->
100 + [].
101 +
102 + maybe_add_html_tag(Tokens=[{start_tag, Tag, _Attrs, false} | _], html5)
103 + when Tag =/= <<"html">> ->
104 + [{start_tag, <<"html">>, [], false} | Tokens];
105 + maybe_add_html_tag(Tokens, _Mode) ->
106 + Tokens.
107 +
108 + %% @spec tokens(StringOrBinary) -> [html_token()]
109 + %% @doc Transform the input UTF-8 HTML into a token stream.
110 + tokens(Input) ->
111 + tokens(iolist_to_binary(Input), #decoder{}, []).
112 +
113 + %% @spec to_tokens(html_node()) -> [html_token()]
114 + %% @doc Convert a html_node() tree to a list of tokens.
115 + to_tokens({Tag0}) ->
116 + to_tokens({Tag0, [], []});
117 + to_tokens(T={'=', _}) ->
118 + [T];
119 + to_tokens(T={doctype, _}) ->
120 + [T];
121 + to_tokens(T={comment, _}) ->
122 + [T];
123 + to_tokens({Tag0, Acc}) ->
124 + %% This is only allowed in sub-tags: {p, [{"class", "foo"}]}
125 + to_tokens({Tag0, [], Acc});
126 + to_tokens({Tag0, Attrs, Acc}) ->
127 + Tag = to_tag(Tag0),
128 + case is_singleton(Tag) of
129 + true ->
130 + to_tokens([], [{start_tag, Tag, Attrs, true}]);
131 + false ->
132 + to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}])
133 + end.
134 +
135 + %% @spec to_html([html_token()] | html_node()) -> iolist()
136 + %% @doc Convert a list of html_token() to a HTML document.
137 + to_html(Node) when is_tuple(Node) ->
138 + to_html(to_tokens(Node));
139 + to_html(Tokens) when is_list(Tokens) ->
140 + to_html(Tokens, []).
141 +
142 + %% @spec escape(string() | atom() | binary()) -> binary()
143 + %% @doc Escape a string such that it's safe for HTML (amp; lt; gt;).
144 + escape(B) when is_binary(B) ->
145 + escape(binary_to_list(B), []);
146 + escape(A) when is_atom(A) ->
147 + escape(atom_to_list(A), []);
148 + escape(S) when is_list(S) ->
149 + escape(S, []).
150 +
151 + %% @spec escape_attr(string() | binary() | atom() | integer() | float()) -> binary()
152 + %% @doc Escape a string such that it's safe for HTML attrs
153 + %% (amp; lt; gt; quot;).
154 + escape_attr(B) when is_binary(B) ->
155 + escape_attr(binary_to_list(B), []);
156 + escape_attr(A) when is_atom(A) ->
157 + escape_attr(atom_to_list(A), []);
158 + escape_attr(S) when is_list(S) ->
159 + escape_attr(S, []);
160 + escape_attr(I) when is_integer(I) ->
161 + escape_attr(integer_to_list(I), []);
162 + %% CHANGED: Previously it used mochinum:digits/1 but float_to_list/1 is
163 + %% good enough generally. In case it isn't, a similar alternative would
164 + %% be lists:flatten(io_lib:format("~p", [F])).
165 + escape_attr(F) when is_float(F) ->
166 + escape_attr(erlang:float_to_list(F), []).
167 +
168 + to_html([], Acc) ->
169 + lists:reverse(Acc);
170 + to_html([{'=', Content} | Rest], Acc) ->
171 + to_html(Rest, [Content | Acc]);
172 + to_html([{pi, Bin} | Rest], Acc) ->
173 + Open = [<<"<?">>,
174 + Bin,
175 + <<"?>">>],
176 + to_html(Rest, [Open | Acc]);
177 + to_html([{pi, Tag, Attrs} | Rest], Acc) ->
178 + Open = [<<"<?">>,
179 + Tag,
180 + attrs_to_html(Attrs, []),
181 + <<"?>">>],
182 + to_html(Rest, [Open | Acc]);
183 + to_html([{comment, Comment} | Rest], Acc) ->
184 + to_html(Rest, [[<<"<!--">>, Comment, <<"-->">>] | Acc]);
185 + to_html([{doctype, Parts} | Rest], Acc) ->
186 + Inside = doctype_to_html(Parts, Acc),
187 + to_html(Rest, [[<<"<!DOCTYPE">>, Inside, <<">">>] | Acc]);
188 + to_html([{data, Data, _Whitespace} | Rest], Acc) ->
189 + to_html(Rest, [escape(Data) | Acc]);
190 + to_html([{start_tag, Tag, Attrs, Singleton} | Rest], Acc) ->
191 + Open = [<<"<">>,
192 + Tag,
193 + attrs_to_html(Attrs, []),
194 + case Singleton of
195 + true -> <<" />">>;
196 + false -> <<">">>
197 + end],
198 + to_html(Rest, [Open | Acc]);
199 + to_html([{end_tag, Tag} | Rest], Acc) ->
200 + to_html(Rest, [[<<"</">>, Tag, <<">">>] | Acc]).
201 +
202 + doctype_to_html([], Acc) ->
203 + lists:reverse(Acc);
204 + doctype_to_html([Word | Rest], Acc) ->
205 + case lists:all(fun (C) -> ?IS_LITERAL_SAFE(C) end,
206 + binary_to_list(iolist_to_binary(Word))) of
207 + true ->
208 + doctype_to_html(Rest, [[<<" ">>, Word] | Acc]);
209 + false ->
210 + doctype_to_html(Rest, [[<<" \"">>, escape_attr(Word), ?QUOTE] | Acc])
211 + end.
212 +
213 + attrs_to_html([], Acc) ->
214 + lists:reverse(Acc);
215 + attrs_to_html([{K, V} | Rest], Acc) ->
216 + attrs_to_html(Rest,
217 + [[<<" ">>, escape(K), <<"=\"">>,
218 + escape_attr(V), <<"\"">>] | Acc]).
219 +
220 + escape([], Acc) ->
221 + list_to_binary(lists:reverse(Acc));
222 + escape("<" ++ Rest, Acc) ->
223 + escape(Rest, lists:reverse("&lt;", Acc));
224 + escape(">" ++ Rest, Acc) ->
225 + escape(Rest, lists:reverse("&gt;", Acc));
226 + escape("&" ++ Rest, Acc) ->
227 + escape(Rest, lists:reverse("&amp;", Acc));
228 + escape([C | Rest], Acc) ->
229 + escape(Rest, [C | Acc]).
230 +
231 + escape_attr([], Acc) ->
232 + list_to_binary(lists:reverse(Acc));
233 + escape_attr("<" ++ Rest, Acc) ->
234 + escape_attr(Rest, lists:reverse("&lt;", Acc));
235 + escape_attr(">" ++ Rest, Acc) ->
236 + escape_attr(Rest, lists:reverse("&gt;", Acc));
237 + escape_attr("&" ++ Rest, Acc) ->
238 + escape_attr(Rest, lists:reverse("&amp;", Acc));
239 + escape_attr([?QUOTE | Rest], Acc) ->
240 + escape_attr(Rest, lists:reverse("&quot;", Acc));
241 + escape_attr([C | Rest], Acc) ->
242 + escape_attr(Rest, [C | Acc]).
243 +
244 + to_tag(A) when is_atom(A) ->
245 + norm(atom_to_list(A));
246 + to_tag(L) ->
247 + norm(L).
248 +
249 + to_tokens([], Acc) ->
250 + lists:reverse(Acc);
251 + to_tokens([{Tag, []} | Rest], Acc) ->
252 + to_tokens(Rest, [{end_tag, to_tag(Tag)} | Acc]);
253 + to_tokens([{Tag0, [{T0} | R1]} | Rest], Acc) ->
254 + %% Allow {br}
255 + to_tokens([{Tag0, [{T0, [], []} | R1]} | Rest], Acc);
256 + to_tokens([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc) ->
257 + %% Allow {'=', iolist()}
258 + to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
259 + to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) ->
260 + %% Allow {comment, iolist()}
261 + to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
262 + to_tokens([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc) ->
263 + %% Allow {pi, binary()}
264 + to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
265 + to_tokens([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc) ->
266 + %% Allow {pi, binary(), list()}
267 + to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
268 + to_tokens([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc) ->
269 + %% Allow {p, [{"class", "foo"}]}
270 + to_tokens([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc);
271 + to_tokens([{Tag0, [{T0, C0} | R1]} | Rest], Acc) ->
272 + %% Allow {p, "content"} and {p, <<"content">>}
273 + to_tokens([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc);
274 + to_tokens([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc) when is_binary(C0) ->
275 + %% Allow {"p", [{"class", "foo"}], <<"content">>}
276 + to_tokens([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc);
277 + to_tokens([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc)
278 + when is_integer(C) ->
279 + %% Allow {"p", [{"class", "foo"}], "content"}
280 + to_tokens([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc);
281 + to_tokens([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc) ->
282 + %% Native {"p", [{"class", "foo"}], ["content"]}
283 + Tag = to_tag(Tag0),
284 + T1 = to_tag(T0),
285 + case is_singleton(norm(T1)) of
286 + true ->
287 + to_tokens([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc]);
288 + false ->
289 + to_tokens([{T1, C1}, {Tag, R1} | Rest],
290 + [{start_tag, T1, A1, false} | Acc])
291 + end;
292 + to_tokens([{Tag0, [L | R1]} | Rest], Acc) when is_list(L) ->
293 + %% List text
294 + Tag = to_tag(Tag0),
295 + to_tokens([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc]);
296 + to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) ->
297 + %% Binary text
298 + Tag = to_tag(Tag0),
299 + to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]).
300 +
301 + tokens(B, S=#decoder{offset=O}, Acc) ->
302 + case B of
303 + <<_:O/binary>> ->
304 + lists:reverse(Acc);
305 + _ ->
306 + {Tag, S1} = tokenize(B, S),
307 + case parse_flag(Tag) of
308 + script ->
309 + {Tag2, S2} = tokenize_script(B, S1),
310 + tokens(B, S2, [Tag2, Tag | Acc]);
311 + textarea ->
312 + {Tag2, S2} = tokenize_textarea(B, S1),
313 + tokens(B, S2, [Tag2, Tag | Acc]);
314 + none ->
315 + tokens(B, S1, [Tag | Acc])
316 + end
317 + end.
318 +
319 + parse_flag({start_tag, B, _, false}) ->
320 + case string:to_lower(binary_to_list(B)) of
321 + "script" ->
322 + script;
323 + "textarea" ->
324 + textarea;
325 + _ ->
326 + none
327 + end;
328 + parse_flag(_) ->
329 + none.
330 +
331 + tokenize(B, S=#decoder{offset=O}) ->
332 + case B of
333 + <<_:O/binary, "<!--", _/binary>> ->
334 + tokenize_comment(B, ?ADV_COL(S, 4));
335 + <<_:O/binary, "<!doctype", _/binary>> ->
336 + tokenize_doctype(B, ?ADV_COL(S, 10));
337 + <<_:O/binary, "<!DOCTYPE", _/binary>> ->
338 + tokenize_doctype(B, ?ADV_COL(S, 10));
339 + <<_:O/binary, "<![CDATA[", _/binary>> ->
340 + tokenize_cdata(B, ?ADV_COL(S, 9));
341 + <<_:O/binary, "<?php", _/binary>> ->
342 + {Body, S1} = raw_qgt(B, ?ADV_COL(S, 2)),
343 + {{pi, Body}, S1};
344 + <<_:O/binary, "<?", _/binary>> ->
345 + {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)),
346 + {Attrs, S2} = tokenize_attributes(B, S1),
347 + S3 = find_qgt(B, S2),
348 + {{pi, Tag, Attrs}, S3};
349 + <<_:O/binary, "&", _/binary>> ->
350 + tokenize_charref(B, ?INC_COL(S));
351 + <<_:O/binary, "</", _/binary>> ->
352 + {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)),
353 + {S2, _} = find_gt(B, S1),
354 + {{end_tag, Tag}, S2};
355 + <<_:O/binary, "<", C, _/binary>>
356 + when ?IS_WHITESPACE(C); not ?IS_LETTER(C) ->
357 + %% This isn't really strict HTML
358 + {{data, Data, _Whitespace}, S1} = tokenize_data(B, ?INC_COL(S)),
359 + {{data, <<$<, Data/binary>>, false}, S1};
360 + <<_:O/binary, "<", _/binary>> ->
361 + {Tag, S1} = tokenize_literal(B, ?INC_COL(S)),
362 + {Attrs, S2} = tokenize_attributes(B, S1),
363 + {S3, HasSlash} = find_gt(B, S2),
364 + Singleton = HasSlash orelse is_singleton(Tag),
365 + {{start_tag, Tag, Attrs, Singleton}, S3};
366 + _ ->
367 + tokenize_data(B, S)
368 + end.
369 +
370 + tree_data([{data, Data, Whitespace} | Rest], AllWhitespace, Acc) ->
371 + tree_data(Rest, (Whitespace andalso AllWhitespace), [Data | Acc]);
372 + tree_data(Rest, AllWhitespace, Acc) ->
373 + {iolist_to_binary(lists:reverse(Acc)), AllWhitespace, Rest}.
374 +
375 + tree([], Stack) ->
376 + {destack(Stack), []};
377 + tree([{end_tag, Tag} | Rest], Stack) ->
378 + case destack(norm(Tag), Stack) of
379 + S when is_list(S) ->
380 + tree(Rest, S);
381 + Result ->
382 + {Result, []}
383 + end;
384 + tree([{start_tag, Tag, Attrs, true} | Rest], S) ->
385 + tree(Rest, append_stack_child(norm({Tag, Attrs}), S));
386 + tree([{start_tag, Tag, Attrs, false} | Rest], S) ->
387 + tree(Rest, stack(norm({Tag, Attrs}), S));
388 + tree([T={pi, _Raw} | Rest], S) ->
389 + tree(Rest, append_stack_child(T, S));
390 + tree([T={pi, _Tag, _Attrs} | Rest], S) ->
391 + tree(Rest, append_stack_child(T, S));
392 + tree([T={comment, _Comment} | Rest], S) ->
393 + tree(Rest, append_stack_child(T, S));
394 + tree(L=[{data, _Data, _Whitespace} | _], S) ->
395 + case tree_data(L, true, []) of
396 + {_, true, Rest} ->
397 + tree(Rest, S);
398 + {Data, false, Rest} ->
399 + tree(Rest, append_stack_child(Data, S))
400 + end;
401 + tree([{doctype, _} | Rest], Stack) ->
402 + tree(Rest, Stack).
403 +
404 + norm({Tag, Attrs}) ->
405 + {norm(Tag), [{norm(K), iolist_to_binary(V)} || {K, V} <- Attrs], []};
406 + norm(Tag) when is_binary(Tag) ->
407 + Tag;
408 + norm(Tag) ->
409 + list_to_binary(string:to_lower(Tag)).
410 +
411 + stack(T1={TN, _, _}, Stack=[{TN, _, _} | _Rest])
412 + when TN =:= <<"li">> orelse TN =:= <<"option">> ->
413 + [T1 | destack(TN, Stack)];
414 + stack(T1={TN0, _, _}, Stack=[{TN1, _, _} | _Rest])
415 + when (TN0 =:= <<"dd">> orelse TN0 =:= <<"dt">>) andalso
416 + (TN1 =:= <<"dd">> orelse TN1 =:= <<"dt">>) ->
417 + [T1 | destack(TN1, Stack)];
418 + stack(T1, Stack) ->
419 + [T1 | Stack].
420 +
421 + append_stack_child(StartTag, [{Name, Attrs, Acc} | Stack]) ->
422 + [{Name, Attrs, [StartTag | Acc]} | Stack].
423 +
424 + destack(<<"br">>, Stack) ->
425 + %% This is an ugly hack to make dumb_br_test() pass,
426 + %% this makes it such that br can never have children.
427 + Stack;
428 + destack(TagName, Stack) when is_list(Stack) ->
429 + F = fun (X) ->
430 + case X of
431 + {TagName, _, _} ->
432 + false;
433 + _ ->
434 + true
435 + end
436 + end,
437 + case lists:splitwith(F, Stack) of
438 + {_, []} ->
439 + %% If we're parsing something like XML we might find
440 + %% a <link>tag</link> that is normally a singleton
441 + %% in HTML but isn't here
442 + case {is_singleton(TagName), Stack} of
443 + {true, [{T0, A0, Acc0} | Post0]} ->
444 + case lists:splitwith(F, Acc0) of
445 + {_, []} ->
446 + %% Actually was a singleton
447 + Stack;
448 + {Pre, [{T1, A1, Acc1} | Post1]} ->
449 + [{T0, A0, [{T1, A1, Acc1 ++ lists:reverse(Pre)} | Post1]}
450 + | Post0]
451 + end;
452 + _ ->
453 + %% No match, no state change
454 + Stack
455 + end;
456 + {_Pre, [_T]} ->
457 + %% Unfurl the whole stack, we're done
458 + destack(Stack);
459 + {Pre, [T, {T0, A0, Acc0} | Post]} ->
460 + %% Unfurl up to the tag, then accumulate it
461 + [{T0, A0, [destack(Pre ++ [T]) | Acc0]} | Post]
462 + end.
463 +
464 + destack([{Tag, Attrs, Acc}]) ->
465 + {Tag, Attrs, lists:reverse(Acc)};
466 + destack([{T1, A1, Acc1}, {T0, A0, Acc0} | Rest]) ->
467 + destack([{T0, A0, [{T1, A1, lists:reverse(Acc1)} | Acc0]} | Rest]).
468 +
469 + is_singleton(<<"area">>) -> true;
470 + is_singleton(<<"base">>) -> true;
471 + is_singleton(<<"br">>) -> true;
472 + is_singleton(<<"col">>) -> true;
473 + is_singleton(<<"embed">>) -> true;
474 + is_singleton(<<"hr">>) -> true;
475 + is_singleton(<<"img">>) -> true;
476 + is_singleton(<<"input">>) -> true;
477 + is_singleton(<<"keygen">>) -> true;
478 + is_singleton(<<"link">>) -> true;
479 + is_singleton(<<"meta">>) -> true;
480 + is_singleton(<<"param">>) -> true;
481 + is_singleton(<<"source">>) -> true;
482 + is_singleton(<<"track">>) -> true;
483 + is_singleton(<<"wbr">>) -> true;
484 + is_singleton(_) -> false.
485 +
486 + tokenize_data(B, S=#decoder{offset=O}) ->
487 + tokenize_data(B, S, O, true).
488 +
489 + tokenize_data(B, S=#decoder{offset=O}, Start, Whitespace) ->
490 + case B of
491 + <<_:O/binary, C, _/binary>> when (C =/= $< andalso C =/= $&) ->
492 + tokenize_data(B, ?INC_CHAR(S, C), Start,
493 + (Whitespace andalso ?IS_WHITESPACE(C)));
494 + _ ->
495 + Len = O - Start,
496 + <<_:Start/binary, Data:Len/binary, _/binary>> = B,
497 + {{data, Data, Whitespace}, S}
498 + end.
499 +
500 + tokenize_attributes(B, S) ->
501 + tokenize_attributes(B, S, []).
502 +
503 + tokenize_attributes(B, S=#decoder{offset=O}, Acc) ->
504 + case B of
505 + <<_:O/binary>> ->
506 + {lists:reverse(Acc), S};
507 + <<_:O/binary, C, _/binary>> when (C =:= $> orelse C =:= $/) ->
508 + {lists:reverse(Acc), S};
509 + <<_:O/binary, "?>", _/binary>> ->
510 + {lists:reverse(Acc), S};
511 + <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
512 + tokenize_attributes(B, ?INC_CHAR(S, C), Acc);
513 + _ ->
514 + {Attr, S1} = tokenize_literal(B, S),
515 + {Value, S2} = tokenize_attr_value(Attr, B, S1),
516 + tokenize_attributes(B, S2, [{Attr, Value} | Acc])
517 + end.
518 +
519 + tokenize_attr_value(Attr, B, S) ->
520 + S1 = skip_whitespace(B, S),
521 + O = S1#decoder.offset,
522 + case B of
523 + <<_:O/binary, "=", _/binary>> ->
524 + S2 = skip_whitespace(B, ?INC_COL(S1)),
525 + tokenize_quoted_or_unquoted_attr_value(B, S2);
526 + _ ->
527 + {Attr, S1}
528 + end.
529 +
530 + tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) ->
531 + case B of
532 + <<_:O/binary>> ->
533 + { [], S };
534 + <<_:O/binary, Q, _/binary>> when Q =:= ?QUOTE orelse
535 + Q =:= ?SQUOTE ->
536 + tokenize_quoted_attr_value(B, ?INC_COL(S), [], Q);
537 + <<_:O/binary, _/binary>> ->
538 + tokenize_unquoted_attr_value(B, S, [])
539 + end.
540 +
541 + tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) ->
542 + case B of
543 + <<_:O/binary>> ->
544 + { iolist_to_binary(lists:reverse(Acc)), S };
545 + <<_:O/binary, $&, _/binary>> ->
546 + {{data, Data, false}, S1} = tokenize_charref(B, ?INC_COL(S)),
547 + tokenize_quoted_attr_value(B, S1, [Data|Acc], Q);
548 + <<_:O/binary, Q, _/binary>> ->
549 + { iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S) };
550 + <<_:O/binary, C, _/binary>> ->
551 + tokenize_quoted_attr_value(B, ?INC_COL(S), [C|Acc], Q)
552 + end.
553 +
554 + tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) ->
555 + case B of
556 + <<_:O/binary>> ->
557 + { iolist_to_binary(lists:reverse(Acc)), S };
558 + <<_:O/binary, $&, _/binary>> ->
559 + {{data, Data, false}, S1} = tokenize_charref(B, ?INC_COL(S)),
560 + tokenize_unquoted_attr_value(B, S1, [Data|Acc]);
561 + <<_:O/binary, $/, $>, _/binary>> ->
562 + { iolist_to_binary(lists:reverse(Acc)), S };
563 + <<_:O/binary, C, _/binary>> when ?PROBABLE_CLOSE(C) ->
564 + { iolist_to_binary(lists:reverse(Acc)), S };
565 + <<_:O/binary, C, _/binary>> ->
566 + tokenize_unquoted_attr_value(B, ?INC_COL(S), [C|Acc])
567 + end.
568 +
569 + skip_whitespace(B, S=#decoder{offset=O}) ->
570 + case B of
571 + <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
572 + skip_whitespace(B, ?INC_CHAR(S, C));
573 + _ ->
574 + S
575 + end.
576 +
577 + tokenize_literal(Bin, S=#decoder{offset=O}) ->
578 + case Bin of
579 + <<_:O/binary, C, _/binary>> when C =:= $>
580 + orelse C =:= $/
581 + orelse C =:= $= ->
582 + %% Handle case where tokenize_literal would consume
583 + %% 0 chars. http://github.com/mochi/mochiweb/pull/13
584 + {[C], ?INC_COL(S)};
585 + _ ->
586 + tokenize_literal(Bin, S, [])
587 + end.
588 +
589 + tokenize_literal(Bin, S=#decoder{offset=O}, Acc) ->
590 + case Bin of
591 + <<_:O/binary, $&, _/binary>> ->
592 + {{data, Data, false}, S1} = tokenize_charref(Bin, ?INC_COL(S)),
593 + tokenize_literal(Bin, S1, [Data | Acc]);
594 + <<_:O/binary, C, _/binary>> when not (?IS_WHITESPACE(C)
595 + orelse C =:= $>
596 + orelse C =:= $/
597 + orelse C =:= $=) ->
598 + tokenize_literal(Bin, ?INC_COL(S), [C | Acc]);
599 + _ ->
600 + {iolist_to_binary(string:to_lower(lists:reverse(Acc))), S}
601 + end.
602 +
603 + raw_qgt(Bin, S=#decoder{offset=O}) ->
604 + raw_qgt(Bin, S, O).
605 +
606 + raw_qgt(Bin, S=#decoder{offset=O}, Start) ->
607 + case Bin of
608 + <<_:O/binary, "?>", _/binary>> ->
609 + Len = O - Start,
610 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
611 + {Raw, ?ADV_COL(S, 2)};
612 + <<_:O/binary, C, _/binary>> ->
613 + raw_qgt(Bin, ?INC_CHAR(S, C), Start);
614 + <<_:O/binary>> ->
615 + <<_:Start/binary, Raw/binary>> = Bin,
616 + {Raw, S}
617 + end.
618 +
619 + find_qgt(Bin, S=#decoder{offset=O}) ->
620 + case Bin of
621 + <<_:O/binary, "?>", _/binary>> ->
622 + ?ADV_COL(S, 2);
623 + <<_:O/binary, ">", _/binary>> ->
624 + ?ADV_COL(S, 1);
625 + <<_:O/binary, "/>", _/binary>> ->
626 + ?ADV_COL(S, 2);
627 + %% tokenize_attributes takes care of this state:
628 + %% <<_:O/binary, C, _/binary>> ->
629 + %% find_qgt(Bin, ?INC_CHAR(S, C));
630 + <<_:O/binary>> ->
631 + S
632 + end.
633 +
634 + find_gt(Bin, S) ->
635 + find_gt(Bin, S, false).
636 +
637 + find_gt(Bin, S=#decoder{offset=O}, HasSlash) ->
638 + case Bin of
639 + <<_:O/binary, $/, _/binary>> ->
640 + find_gt(Bin, ?INC_COL(S), true);
641 + <<_:O/binary, $>, _/binary>> ->
642 + {?INC_COL(S), HasSlash};
643 + <<_:O/binary, C, _/binary>> ->
644 + find_gt(Bin, ?INC_CHAR(S, C), HasSlash);
645 + _ ->
646 + {S, HasSlash}
647 + end.
648 +
649 + tokenize_charref(Bin, S=#decoder{offset=O}) ->
650 + try
651 + case tokenize_charref_raw(Bin, S, O) of
652 + {C1, S1} when C1 >= 16#D800 andalso C1 =< 16#DFFF ->
653 + %% Surrogate pair
654 + tokeninize_charref_surrogate_pair(Bin, S1, C1);
655 + {Unichar, S1} when is_integer(Unichar) ->
656 + %% CHANGED: Previously this was mochiutf8:codepoint_to_bytes(Unichar)
657 + %% but that is equivalent to the below.
658 + {{data, <<Unichar/utf8>>, false},
659 + S1};
660 + {Unichars, S1} when is_list(Unichars) ->
661 + {{data, unicode:characters_to_binary(Unichars), false},
662 + S1};
663 + {undefined, _} ->
664 + throw(invalid_charref)
665 + end
666 + catch
667 + throw:invalid_charref ->
668 + {{data, <<"&">>, false}, S}
669 + end.
670 +
671 + tokeninize_charref_surrogate_pair(Bin, S=#decoder{offset=O}, C1) ->
672 + case Bin of
673 + <<_:O/binary, $&, _/binary>> ->
674 + case tokenize_charref_raw(Bin, ?INC_COL(S), O + 1) of
675 + {C2, S1} when C2 >= 16#D800 andalso C1 =< 16#DFFF ->
676 + {{data,
677 + unicode:characters_to_binary(
678 + <<C1:16, C2:16>>,
679 + utf16,
680 + utf8),
681 + false},
682 + S1};
683 + _ ->
684 + throw(invalid_charref)
685 + end;
686 + _ ->
687 + throw(invalid_charref)
688 + end.
689 +
690 + tokenize_charref_raw(Bin, S=#decoder{offset=O}, Start) ->
691 + case Bin of
692 + <<_:O/binary>> ->
693 + throw(invalid_charref);
694 + <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C)
695 + orelse C =:= ?SQUOTE
696 + orelse C =:= ?QUOTE
697 + orelse C =:= $/
698 + orelse C =:= $> ->
699 + throw(invalid_charref);
700 + <<_:O/binary, $;, _/binary>> ->
701 + Len = O - Start,
702 + %% CHANGED: Previously this was mochiweb_charref:charref/1
703 + %% but the functionality below is equivalent;
704 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
705 + <<CP/utf8>> = 'Elixir.HtmlEntities':decode(<<$&, Raw/binary, $;>>),
706 + {CP, ?INC_COL(S)};
707 + _ ->
708 + tokenize_charref_raw(Bin, ?INC_COL(S), Start)
709 + end.
710 +
711 + tokenize_doctype(Bin, S) ->
712 + tokenize_doctype(Bin, S, []).
713 +
714 + tokenize_doctype(Bin, S=#decoder{offset=O}, Acc) ->
715 + case Bin of
716 + <<_:O/binary>> ->
717 + {{doctype, lists:reverse(Acc)}, S};
718 + <<_:O/binary, $>, _/binary>> ->
719 + {{doctype, lists:reverse(Acc)}, ?INC_COL(S)};
720 + <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
721 + tokenize_doctype(Bin, ?INC_CHAR(S, C), Acc);
722 + _ ->
723 + {Word, S1} = tokenize_word_or_literal(Bin, S),
724 + tokenize_doctype(Bin, S1, [Word | Acc])
725 + end.
726 +
727 + tokenize_word_or_literal(Bin, S=#decoder{offset=O}) ->
728 + case Bin of
729 + <<_:O/binary, C, _/binary>> when C =:= ?QUOTE orelse C =:= ?SQUOTE ->
730 + tokenize_word(Bin, ?INC_COL(S), C);
731 + <<_:O/binary, C, _/binary>> when not ?IS_WHITESPACE(C) ->
732 + %% Sanity check for whitespace
733 + tokenize_literal(Bin, S)
734 + end.
735 +
736 + tokenize_word(Bin, S, Quote) ->
737 + tokenize_word(Bin, S, Quote, []).
738 +
739 + tokenize_word(Bin, S=#decoder{offset=O}, Quote, Acc) ->
740 + case Bin of
741 + <<_:O/binary>> ->
742 + {iolist_to_binary(lists:reverse(Acc)), S};
743 + <<_:O/binary, Quote, _/binary>> ->
744 + {iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S)};
745 + <<_:O/binary, $&, _/binary>> ->
746 + {{data, Data, false}, S1} = tokenize_charref(Bin, ?INC_COL(S)),
747 + tokenize_word(Bin, S1, Quote, [Data | Acc]);
748 + <<_:O/binary, C, _/binary>> ->
749 + tokenize_word(Bin, ?INC_CHAR(S, C), Quote, [C | Acc])
750 + end.
751 +
752 + tokenize_cdata(Bin, S=#decoder{offset=O}) ->
753 + tokenize_cdata(Bin, S, O).
754 +
755 + tokenize_cdata(Bin, S=#decoder{offset=O}, Start) ->
756 + case Bin of
757 + <<_:O/binary, "]]>", _/binary>> ->
758 + Len = O - Start,
759 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
760 + {{data, Raw, false}, ?ADV_COL(S, 3)};
761 + <<_:O/binary, C, _/binary>> ->
762 + tokenize_cdata(Bin, ?INC_CHAR(S, C), Start);
763 + _ ->
764 + <<_:O/binary, Raw/binary>> = Bin,
765 + {{data, Raw, false}, S}
766 + end.
767 +
768 + tokenize_comment(Bin, S=#decoder{offset=O}) ->
769 + tokenize_comment(Bin, S, O).
770 +
771 + tokenize_comment(Bin, S=#decoder{offset=O}, Start) ->
772 + case Bin of
773 + <<_:O/binary, "-->", _/binary>> ->
774 + Len = O - Start,
775 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
776 + {{comment, Raw}, ?ADV_COL(S, 3)};
777 + <<_:O/binary, C, _/binary>> ->
778 + tokenize_comment(Bin, ?INC_CHAR(S, C), Start);
779 + <<_:Start/binary, Raw/binary>> ->
780 + {{comment, Raw}, S}
781 + end.
782 +
783 + tokenize_script(Bin, S=#decoder{offset=O}) ->
784 + tokenize_script(Bin, S, O).
785 +
786 + tokenize_script(Bin, S=#decoder{offset=O}, Start) ->
787 + case Bin of
788 + %% Just a look-ahead, we want the end_tag separately
789 + <<_:O/binary, $<, $/, SS, CC, RR, II, PP, TT, ZZ, _/binary>>
790 + when (SS =:= $s orelse SS =:= $S) andalso
791 + (CC =:= $c orelse CC =:= $C) andalso
792 + (RR =:= $r orelse RR =:= $R) andalso
793 + (II =:= $i orelse II =:= $I) andalso
794 + (PP =:= $p orelse PP =:= $P) andalso
795 + (TT=:= $t orelse TT =:= $T) andalso
796 + ?PROBABLE_CLOSE(ZZ) ->
797 + Len = O - Start,
798 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
799 + {{data, Raw, false}, S};
800 + <<_:O/binary, C, _/binary>> ->
801 + tokenize_script(Bin, ?INC_CHAR(S, C), Start);
802 + <<_:Start/binary, Raw/binary>> ->
803 + {{data, Raw, false}, S}
804 + end.
805 +
806 + tokenize_textarea(Bin, S=#decoder{offset=O}) ->
807 + tokenize_textarea(Bin, S, O).
808 +
809 + tokenize_textarea(Bin, S=#decoder{offset=O}, Start) ->
810 + case Bin of
811 + %% Just a look-ahead, we want the end_tag separately
812 + <<_:O/binary, $<, $/, TT, EE, XX, TT2, AA, RR, EE2, AA2, ZZ, _/binary>>
813 + when (TT =:= $t orelse TT =:= $T) andalso
814 + (EE =:= $e orelse EE =:= $E) andalso
815 + (XX =:= $x orelse XX =:= $X) andalso
816 + (TT2 =:= $t orelse TT2 =:= $T) andalso
817 + (AA =:= $a orelse AA =:= $A) andalso
818 + (RR =:= $r orelse RR =:= $R) andalso
819 + (EE2 =:= $e orelse EE2 =:= $E) andalso
820 + (AA2 =:= $a orelse AA2 =:= $A) andalso
821 + ?PROBABLE_CLOSE(ZZ) ->
822 + Len = O - Start,
823 + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
824 + {{data, Raw, false}, S};
825 + <<_:O/binary, C, _/binary>> ->
826 + tokenize_textarea(Bin, ?INC_CHAR(S, C), Start);
827 + <<_:Start/binary, Raw/binary>> ->
828 + {{data, Raw, false}, S}
829 + end.