%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% @doc Loosely tokenizes and generates parse trees for HTML 4. %% Adapted by Maas-Maarten Zeeman -module(z_html_parse). -export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1, escape_attr/1, to_html/1]). -type html_node() :: {binary(), [html_attr()], [ html_element() ]}. -type html_attr() :: {binary(), binary()}. -type html_element() :: html_node() | html_comment() | pi_tag() | binary(). -type html_comment() :: {comment, Comment::binary()}. -type pi_tag() :: {pi, binary()} | {pi, Tag::binary(), [html_attr()]}. -type html_data() :: {data, binary(), Whitespace::boolean()}. -type start_tag() :: {start_tag, Name::binary(), [ html_attr() ], Singleton::boolean()}. -type end_tag() :: {end_tag, Name::binary()}. -type html_doctype() :: {doctype, [ Doctype::any() ]}. -type inline_html() :: {'=', binary()}. -type html_token() :: html_data() | start_tag() | end_tag() | pi_tag() | inline_html() | html_comment() | html_doctype(). -export_type([ html_node/0, html_element/0, html_attr/0, html_data/0, html_comment/0, html_doctype/0, start_tag/0, end_tag/0, html_token/0 ]). %% This is a macro to placate syntax highlighters.. -define(QUOTE, $\"). -define(SQUOTE, $\'). -define(ADV_COL(S, N), S#decoder{column=N+S#decoder.column, offset=N+S#decoder.offset}). -define(INC_COL(S), S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset}). -define(INC_LINE(S), S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}). -define(INC_CHAR(S, C), case C of $\n -> S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}; _ -> S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset} end). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). -define(IS_LITERAL_SAFE(C), ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C >= $0 andalso C =< $9))). -define(IS_START_LITERAL_SAFE(C), ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C == $_))). -define(PROBABLE_CLOSE(C), (C =:= $> orelse ?IS_WHITESPACE(C))). -record(decoder, {line=1, column=1, offset=0}). %% External API. %% @doc tokenize and then transform the token stream into a HTML tree. -spec parse( iodata() ) -> {ok, html_node()} | {error, nohtml}. parse(Input) -> parse_tokens(tokens(Input)). %% @doc Transform the output of tokens(Doc) into a HTML tree. -spec parse_tokens( [ html_token() ] ) -> {ok, html_node()} | {error, nohtml}. parse_tokens(Tokens) when is_list(Tokens) -> %% Skip over doctype, processing instructions F = fun (X) -> case X of {start_tag, _, _, false} -> false; _ -> true end end, case lists:dropwhile(F, Tokens) of [{start_tag, Tag, Attrs, false} | Rest] -> {Tree, _} = tree(Rest, [norm({Tag, Attrs})]), {ok, Tree}; [] -> {error, nohtml} end. %% @spec tokens(StringOrBinary) -> [html_token()] %% @doc Transform the input UTF-8 HTML into a token stream. tokens(Input) -> tokens(iolist_to_binary(Input), #decoder{}, []). %% @spec to_tokens(html_node()) -> [html_token()] %% @doc Convert a html_node() tree to a list of tokens. to_tokens({Tag0}) -> to_tokens({Tag0, [], []}); to_tokens(T={'=', _}) -> [T]; to_tokens(T={doctype, _}) -> [T]; to_tokens(T={comment, _}) -> [T]; to_tokens({Tag0, Acc}) -> %% This is only allowed in sub-tags: {p, [{"class", "foo"}]} to_tokens({Tag0, [], Acc}); to_tokens({Tag0, Attrs, Acc}) -> Tag = to_tag(Tag0), case is_singleton(Tag) of true -> to_tokens([], [{start_tag, Tag, Attrs, true}]); false -> to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}]) end. %% @spec to_html([html_token()] | html_node()) -> iolist() %% @doc Convert a list of html_token() to a HTML document. to_html(Node) when is_tuple(Node) -> to_html(to_tokens(Node)); to_html(Tokens) when is_list(Tokens) -> to_html(Tokens, []). %% @spec escape(string() | atom() | binary()) -> binary() %% @doc Escape a string such that it's safe for HTML (amp; lt; gt;). escape(B) when is_binary(B) -> escape(binary_to_list(B), []); escape(A) when is_atom(A) -> escape(atom_to_list(A), []); escape(S) when is_list(S) -> escape(S, []). %% @spec escape_attr(string() | binary() | atom() | integer() | float()) -> binary() %% @doc Escape a string such that it's safe for HTML attrs %% (amp; lt; gt; quot;). escape_attr(B) when is_binary(B) -> escape_attr(binary_to_list(B), []); escape_attr(A) when is_atom(A) -> escape_attr(atom_to_list(A), []); escape_attr(S) when is_list(S) -> escape_attr(S, []); escape_attr(I) when is_integer(I) -> escape_attr(integer_to_list(I), []); escape_attr(F) when is_float(F) -> escape_attr(mochinum:digits(F), []). to_html(Tree, Acc) -> to_html(Tree, Acc, true). to_html([], Acc, _Escape) -> lists:reverse(Acc); to_html([{'=', Content} | Rest], Acc, Escape) -> to_html(Rest, [Content | Acc], Escape); to_html([{pi, Bin} | Rest], Acc, Escape) -> Open = [<<">, Bin, <<"?>">>], to_html(Rest, [Open | Acc], Escape); to_html([{pi, Tag, Attrs} | Rest], Acc, Escape) -> Open = [<<">, Tag, attrs_to_html(Attrs, []), <<"?>">>], to_html(Rest, [Open | Acc], Escape); to_html([{comment, Comment} | Rest], Acc, Escape) -> to_html(Rest, [[<<"">>] | Acc], Escape); to_html([{doctype, Parts} | Rest], Acc, Escape) -> Inside = doctype_to_html(Parts, Acc), to_html(Rest, [[<<">, Inside, <<">">>] | Acc], Escape); to_html([{data, Data, _Whitespace} | Rest], Acc, true) -> to_html(Rest, [escape(Data) | Acc], true); to_html([{data, Data, _Whitespace} | Rest], Acc, false) -> to_html(Rest, [Data | Acc], false); to_html([{start_tag, Tag, Attrs, Singleton} | Rest], Acc, _Escape) -> EscapeData = case Tag of <<"script">> -> false; _ -> true end, Open = [<<"<">>, Tag, attrs_to_html(Attrs, []), case Singleton of true -> <<" />">>; false -> <<">">> end], to_html(Rest, [Open | Acc], EscapeData); to_html([{end_tag, Tag} | Rest], Acc, _Escape) -> to_html(Rest, [[<<">, Tag, <<">">>] | Acc], false). doctype_to_html([], Acc) -> lists:reverse(Acc); doctype_to_html([Word | Rest], Acc) -> case lists:all(fun (C) -> ?IS_LITERAL_SAFE(C) end, binary_to_list(iolist_to_binary(Word))) of true -> doctype_to_html(Rest, [[<<" ">>, Word] | Acc]); false -> doctype_to_html(Rest, [[<<" \"">>, escape_attr(Word), ?QUOTE] | Acc]) end. attrs_to_html([], Acc) -> lists:reverse(Acc); attrs_to_html([{K, V} | Rest], Acc) -> attrs_to_html(Rest, [[<<" ">>, escape(K), <<"=\"">>, escape_attr(V), <<"\"">>] | Acc]). escape([], Acc) -> list_to_binary(lists:reverse(Acc)); escape("<" ++ Rest, Acc) -> escape(Rest, lists:reverse("<", Acc)); escape(">" ++ Rest, Acc) -> escape(Rest, lists:reverse(">", Acc)); escape("&" ++ Rest, Acc) -> escape(Rest, lists:reverse("&", Acc)); escape([16#c2, 16#a0] ++ Rest, Acc) -> escape(Rest, lists:reverse(" ", Acc)); escape([C | Rest], Acc) -> escape(Rest, [C | Acc]). escape_attr([], Acc) -> list_to_binary(lists:reverse(Acc)); escape_attr("<" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("<", Acc)); escape_attr(">" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse(">", Acc)); escape_attr("&" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("&", Acc)); escape_attr([?QUOTE | Rest], Acc) -> escape_attr(Rest, lists:reverse(""", Acc)); escape_attr([16#c2, 16#a0] ++ Rest, Acc) -> escape_attr(Rest, lists:reverse(" ", Acc)); escape_attr([C | Rest], Acc) -> escape_attr(Rest, [C | Acc]). to_tag(A) when is_atom(A) -> norm(atom_to_list(A)); to_tag(L) -> norm(L). to_tokens([], Acc) -> lists:reverse(Acc); to_tokens([{Tag, []} | Rest], Acc) -> to_tokens(Rest, [{end_tag, to_tag(Tag)} | Acc]); to_tokens([{Tag0, [{T0} | R1]} | Rest], Acc) -> %% Allow {br} to_tokens([{Tag0, [{T0, [], []} | R1]} | Rest], Acc); to_tokens([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc) -> %% Allow {'=', iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) -> %% Allow {comment, iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc) -> %% Allow {pi, binary()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc) -> %% Allow {pi, binary(), list()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc) -> %% Allow {p, [{"class", "foo"}]} to_tokens([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, C0} | R1]} | Rest], Acc) -> %% Allow {p, "content"} and {p, <<"content">>} to_tokens([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc) when is_binary(C0) -> %% Allow {"p", [{"class", "foo"}], <<"content">>} to_tokens([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc) when is_integer(C) -> %% Allow {"p", [{"class", "foo"}], "content"} to_tokens([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc) -> %% Native {"p", [{"class", "foo"}], ["content"]} Tag = to_tag(Tag0), T1 = to_tag(T0), case is_singleton(norm(T1)) of true -> to_tokens([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc]); false -> to_tokens([{T1, C1}, {Tag, R1} | Rest], [{start_tag, T1, A1, false} | Acc]) end; to_tokens([{Tag0, [L | R1]} | Rest], Acc) when is_list(L) -> %% List text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc]); to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) -> %% Binary text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]). tokens(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary>> -> lists:reverse(Acc); _ -> {Tag, S1} = tokenize(B, S), case parse_flag(Tag) of script -> {Tag2, S2} = tokenize_script(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); textarea -> {Tag2, S2} = tokenize_textarea(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); none -> tokens(B, S1, [Tag | Acc]) end end. parse_flag({start_tag, B, _, false}) -> case z_string:to_lower(B) of <<"script">> -> script; <<"textarea">> -> textarea; _ -> none end; parse_flag(_) -> none. tokenize(B, S=#decoder{offset=O}) -> case B of <<_:O/binary, "", _/binary>> -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{comment, Raw}, ?ADV_COL(S, 3)}; <<_:O/binary, C, _/binary>> -> tokenize_comment(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{comment, Raw}, S} end. tokenize_script(Bin, S=#decoder{offset=O}) -> tokenize_script(Bin, S, O). tokenize_script(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, SS, CC, RR, II, PP, TT, ZZ, _/binary>> when (SS =:= $s orelse SS =:= $S) andalso (CC =:= $c orelse CC =:= $C) andalso (RR =:= $r orelse RR =:= $R) andalso (II =:= $i orelse II =:= $I) andalso (PP =:= $p orelse PP =:= $P) andalso (TT=:= $t orelse TT =:= $T) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_script(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. tokenize_textarea(Bin, S=#decoder{offset=O}) -> tokenize_textarea(Bin, S, O). tokenize_textarea(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, TT, EE, XX, TT2, AA, RR, EE2, AA2, ZZ, _/binary>> when (TT =:= $t orelse TT =:= $T) andalso (EE =:= $e orelse EE =:= $E) andalso (XX =:= $x orelse XX =:= $X) andalso (TT2 =:= $t orelse TT2 =:= $T) andalso (AA =:= $a orelse AA =:= $A) andalso (RR =:= $r orelse RR =:= $R) andalso (EE2 =:= $e orelse EE2 =:= $E) andalso (AA2 =:= $a orelse AA2 =:= $A) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_textarea(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. % @doc Return true when Tag is a html tag. % % A is_html_tag(<<"a">>) -> true; is_html_tag(<<"abbr">>) -> true; is_html_tag(<<"acronym">>) -> true; is_html_tag(<<"address">>) -> true; is_html_tag(<<"applet">>) -> true; is_html_tag(<<"area">>) -> true; is_html_tag(<<"article">>) -> true; is_html_tag(<<"aside">>) -> true; is_html_tag(<<"audio">>) -> true; % B is_html_tag(<<"b">>) -> true; is_html_tag(<<"base">>) -> true; is_html_tag(<<"basefont">>) -> true; is_html_tag(<<"bdi">>) -> true; is_html_tag(<<"bdo">>) -> true; is_html_tag(<<"bgsound">>) -> true; is_html_tag(<<"big">>) -> true; is_html_tag(<<"blink">>) -> true; is_html_tag(<<"blockquote">>) -> true; is_html_tag(<<"body">>) -> true; is_html_tag(<<"br">>) -> true; is_html_tag(<<"button">>) -> true; % C is_html_tag(<<"canvas">>) -> true; is_html_tag(<<"caption">>) -> true; is_html_tag(<<"center">>) -> true; is_html_tag(<<"cite">>) -> true; is_html_tag(<<"code">>) -> true; is_html_tag(<<"col">>) -> true; is_html_tag(<<"colgroup">>) -> true; is_html_tag(<<"content">>) -> true; % D is_html_tag(<<"data">>) -> true; is_html_tag(<<"datalist">>) -> true; is_html_tag(<<"dd">>) -> true; is_html_tag(<<"decorator">>) -> true; is_html_tag(<<"del">>) -> true; is_html_tag(<<"details">>) -> true; is_html_tag(<<"dfn">>) -> true; is_html_tag(<<"dir">>) -> true; is_html_tag(<<"div">>) -> true; is_html_tag(<<"dl">>) -> true; is_html_tag(<<"dt">>) -> true; % E is_html_tag(<<"element">>) -> true; is_html_tag(<<"em">>) -> true; is_html_tag(<<"embed">>) -> true; % F is_html_tag(<<"fieldset">>) -> true; is_html_tag(<<"figcaption">>) -> true; is_html_tag(<<"figure">>) -> true; is_html_tag(<<"font">>) -> true; is_html_tag(<<"footer">>) -> true; is_html_tag(<<"form">>) -> true; is_html_tag(<<"frame">>) -> true; is_html_tag(<<"frameset">>) -> true; % G H is_html_tag(<<"h1">>) -> true; is_html_tag(<<"h2">>) -> true; is_html_tag(<<"h3">>) -> true; is_html_tag(<<"h4">>) -> true; is_html_tag(<<"h5">>) -> true; is_html_tag(<<"h6">>) -> true; is_html_tag(<<"head">>) -> true; is_html_tag(<<"header">>) -> true; is_html_tag(<<"hgroup">>) -> true; is_html_tag(<<"hr">>) -> true; is_html_tag(<<"html">>) -> true; % I is_html_tag(<<"i">>) -> true; is_html_tag(<<"iframe">>) -> true; is_html_tag(<<"img">>) -> true; is_html_tag(<<"input">>) -> true; is_html_tag(<<"ins">>) -> true; is_html_tag(<<"isindex">>) -> true; % J K is_html_tag(<<"kbd">>) -> true; is_html_tag(<<"keygen">>) -> true; % L is_html_tag(<<"label">>) -> true; is_html_tag(<<"legend">>) -> true; is_html_tag(<<"li">>) -> true; is_html_tag(<<"link">>) -> true; is_html_tag(<<"listing">>) -> true; % M is_html_tag(<<"main">>) -> true; is_html_tag(<<"map">>) -> true; is_html_tag(<<"mark">>) -> true; is_html_tag(<<"marquee">>) -> true; is_html_tag(<<"menu">>) -> true; is_html_tag(<<"menuitem">>) -> true; is_html_tag(<<"meta">>) -> true; is_html_tag(<<"meter">>) -> true; % N is_html_tag(<<"nav">>) -> true; is_html_tag(<<"nobr">>) -> true; is_html_tag(<<"noframes">>) -> true; is_html_tag(<<"noscript">>) -> true; % O is_html_tag(<<"object">>) -> true; is_html_tag(<<"ol">>) -> true; is_html_tag(<<"optgroup">>) -> true; is_html_tag(<<"option">>) -> true; is_html_tag(<<"output">>) -> true; % P is_html_tag(<<"p">>) -> true; is_html_tag(<<"param">>) -> true; is_html_tag(<<"plaintext">>) -> true; is_html_tag(<<"pre">>) -> true; is_html_tag(<<"progress">>) -> true; % Q is_html_tag(<<"q">>) -> true; % R is_html_tag(<<"rp">>) -> true; is_html_tag(<<"rt">>) -> true; is_html_tag(<<"ruby">>) -> true; % S is_html_tag(<<"s">>) -> true; is_html_tag(<<"samp">>) -> true; is_html_tag(<<"script">>) -> true; is_html_tag(<<"section">>) -> true; is_html_tag(<<"select">>) -> true; is_html_tag(<<"shadow">>) -> true; is_html_tag(<<"small">>) -> true; is_html_tag(<<"source">>) -> true; is_html_tag(<<"spacer">>) -> true; is_html_tag(<<"span">>) -> true; is_html_tag(<<"strike">>) -> true; is_html_tag(<<"strong">>) -> true; is_html_tag(<<"style">>) -> true; is_html_tag(<<"sub">>) -> true; is_html_tag(<<"summary">>) -> true; is_html_tag(<<"sup">>) -> true; % T is_html_tag(<<"table">>) -> true; is_html_tag(<<"tbody">>) -> true; is_html_tag(<<"td">>) -> true; is_html_tag(<<"template">>) -> true; is_html_tag(<<"textarea">>) -> true; is_html_tag(<<"tfoot">>) -> true; is_html_tag(<<"th">>) -> true; is_html_tag(<<"thead">>) -> true; is_html_tag(<<"time">>) -> true; is_html_tag(<<"title">>) -> true; is_html_tag(<<"tr">>) -> true; is_html_tag(<<"track">>) -> true; is_html_tag(<<"tt">>) -> true; % U is_html_tag(<<"u">>) -> true; is_html_tag(<<"ul">>) -> true; % V is_html_tag(<<"var">>) -> true; is_html_tag(<<"video">>) -> true; % W is_html_tag(<<"wbr">>) -> true; % X Y Z is_html_tag(<<"xmp">>) -> true; % Everything else is_html_tag(_) -> false. % @doc Returns true when Attr is a html attribute. is_html_attr(<<"accept">>) -> true; is_html_attr(<<"accept-charset">>) -> true; is_html_attr(<<"accesskey">>) -> true; is_html_attr(<<"action">>) -> true; is_html_attr(<<"align">>) -> true; is_html_attr(<<"alt">>) -> true; is_html_attr(<<"async">>) -> true; is_html_attr(<<"autocomplete">>) -> true; is_html_attr(<<"autofocus">>) -> true; is_html_attr(<<"autoplay">>) -> true; % B is_html_attr(<<"bgcolor">>) -> true; is_html_attr(<<"border">>) -> true; is_html_attr(<<"buffered">>) -> true; % C is_html_attr(<<"challenge">>) -> true; is_html_attr(<<"charset">>) -> true; is_html_attr(<<"checked">>) -> true; is_html_attr(<<"cite">>) -> true; is_html_attr(<<"class">>) -> true; is_html_attr(<<"code">>) -> true; is_html_attr(<<"codebase">>) -> true; is_html_attr(<<"color">>) -> true; is_html_attr(<<"cols">>) -> true; is_html_attr(<<"colspan">>) -> true; is_html_attr(<<"content">>) -> true; is_html_attr(<<"contenteditable">>) -> true; is_html_attr(<<"contextmenu">>) -> true; is_html_attr(<<"controls">>) -> true; is_html_attr(<<"coords">>) -> true; % D is_html_attr(<<"data">>) -> true; is_html_attr(<<"data-", _Rest/binary>>) -> true; is_html_attr(<<"datetime">>) -> true; is_html_attr(<<"default">>) -> true; is_html_attr(<<"defer">>) -> true; is_html_attr(<<"dir">>) -> true; is_html_attr(<<"dirname">>) -> true; is_html_attr(<<"disabled">>) -> true; is_html_attr(<<"download">>) -> true; is_html_attr(<<"draggable">>) -> true; is_html_attr(<<"dropzone">>) -> true; % E is_html_attr(<<"enctype">>) -> true; % F is_html_attr(<<"for">>) -> true; is_html_attr(<<"form">>) -> true; is_html_attr(<<"headers">>) -> true; is_html_attr(<<"height">>) -> true; is_html_attr(<<"hidden">>) -> true; is_html_attr(<<"high">>) -> true; is_html_attr(<<"href">>) -> true; is_html_attr(<<"hreflang">>) -> true; is_html_attr(<<"http-equiv">>) -> true; % G H I is_html_attr(<<"icon">>) -> true; is_html_attr(<<"id">>) -> true; is_html_attr(<<"ismap">>) -> true; is_html_attr(<<"itemprop">>) -> true; % K is_html_attr(<<"keytype">>) -> true; is_html_attr(<<"kind">>) -> true; is_html_attr(<<"label">>) -> true; % L is_html_attr(<<"lang">>) -> true; is_html_attr(<<"language">>) -> true; is_html_attr(<<"list">>) -> true; is_html_attr(<<"loop">>) -> true; is_html_attr(<<"low">>) -> true; % M is_html_attr(<<"manifest">>) -> true; is_html_attr(<<"max">>) -> true; is_html_attr(<<"maxlength">>) -> true; is_html_attr(<<"media">>) -> true; is_html_attr(<<"method">>) -> true; is_html_attr(<<"min">>) -> true; % N is_html_attr(<<"multiple">>) -> true; is_html_attr(<<"name">>) -> true; is_html_attr(<<"novalidate">>) -> true; % O is_html_attr(<<"open">>) -> true; is_html_attr(<<"optimum">>) -> true; % P is_html_attr(<<"pattern">>) -> true; is_html_attr(<<"ping">>) -> true; is_html_attr(<<"placeholder">>) -> true; is_html_attr(<<"poster">>) -> true; is_html_attr(<<"preload">>) -> true; is_html_attr(<<"pubdate">>) -> true; % Q R is_html_attr(<<"radiogroup">>) -> true; is_html_attr(<<"readonly">>) -> true; is_html_attr(<<"rel">>) -> true; is_html_attr(<<"required">>) -> true; is_html_attr(<<"reversed">>) -> true; is_html_attr(<<"rows">>) -> true; is_html_attr(<<"rowspan">>) -> true; % S is_html_attr(<<"sandbox">>) -> true; is_html_attr(<<"spellcheck">>) -> true; is_html_attr(<<"scope">>) -> true; is_html_attr(<<"scoped">>) -> true; is_html_attr(<<"seamless">>) -> true; is_html_attr(<<"selected">>) -> true; is_html_attr(<<"shape">>) -> true; is_html_attr(<<"size">>) -> true; is_html_attr(<<"sizes">>) -> true; is_html_attr(<<"span">>) -> true; is_html_attr(<<"src">>) -> true; is_html_attr(<<"srcdoc">>) -> true; is_html_attr(<<"srclang">>) -> true; is_html_attr(<<"start">>) -> true; is_html_attr(<<"step">>) -> true; is_html_attr(<<"style">>) -> true; is_html_attr(<<"summary">>) -> true; % T is_html_attr(<<"tabindex">>) -> true; is_html_attr(<<"target">>) -> true; is_html_attr(<<"title">>) -> true; is_html_attr(<<"type">>) -> true; % U is_html_attr(<<"usemap">>) -> true; is_html_attr(<<"value">>) -> true; % W is_html_attr(<<"width">>) -> true; is_html_attr(<<"wrap">>) -> true; is_html_attr(_) -> false. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). to_html_test() -> ?assertEqual( <<"hey!

what's up

sucka
RAW!">>, iolist_to_binary( to_html({html, [], [{<<"head">>, [], [{title, <<"hey!">>}]}, {body, [], [{p, [{class, foo}], [<<"what's">>, <<" up">>, {br}]}, {'div', <<"sucka">>}, {'=', <<"RAW!">>}, {comment, <<" comment! ">>}]}]}))), ?assertEqual( <<"">>, iolist_to_binary( to_html({doctype, [<<"html">>, <<"PUBLIC">>, <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>, <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]}))), ?assertEqual( <<"">>, iolist_to_binary( to_html({<<"html">>,[], [{pi, <<"xml:namespace">>, [{<<"prefix">>,<<"o">>}, {<<"ns">>,<<"urn:schemas-microsoft-com:office:office">>}]}]}))), ok. escape_test() -> ?assertEqual( <<"&quot;\"word ><<up!&quot;">>, escape(<<""\"word ><>)), ?assertEqual( <<"&quot;\"word ><<up!&quot;">>, escape(""\"word ><>, escape('"\"word ><>, escape(<<"pre", 16#c2, 16#a0, "post">>)), ok. escape_attr_test() -> ?assertEqual( <<"&quot;"word ><<up!&quot;">>, escape_attr(<<""\"word ><>)), ?assertEqual( <<"&quot;"word ><<up!&quot;">>, escape_attr(""\"word ><>, escape_attr('"\"word ><>, escape_attr(12345)), ?assertEqual( <<"1.5">>, escape_attr(1.5)), ?assertEqual( <<"pre post">>, escape_attr(<<"pre", 16#c2, 16#a0, "post">>)), ok. tokens_test() -> ?assertEqual( [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>}, {<<"wibble">>, <<"wibble">>}, {<<"alice">>, <<"bob">>}], true}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>}, {<<"wibble">>, <<"wibble">>}, {<<"alice">>, <<"bob">>}], true}], tokens(<<"">>)), ?assertEqual( [{comment, <<"[if lt IE 7]>\n\n>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"textarea">>, [], false}, {data, <<"">>, false}, {end_tag, <<"textarea">>}], tokens(<<"">>)), ?assertEqual( [{start_tag, <<"textarea">>, [], false}, {data, <<"">>, false}], tokens(<<"