%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc.; copyright 2018-2021 Maas-Maarten Zeeman %% @doc Loosely tokenizes and generates parse trees for (X)HTML and XML. %% Adapted by Maas-Maarten Zeeman %% Extended for basic XML parsing by Marc Worrell -module(z_html_parse). -export([ tokens/1, tokens/2, parse/1, parse/2, parse_to_map/1, parse_to_map/2, parse_tokens/1, to_tokens/1, to_tokens/2, escape/1, escape_attr/1, to_html/1, to_html/2]). %% Exports for tests in test/z_html_parse_test.tpl -ifdef(TEST). -export([ is_singleton/2, destack/1, destack/3 ]). -endif. -type html_node() :: {html_tag(), [html_attr()], [ html_element() ]}. -type html_attr() :: {html_attr_name(), html_attr_value()}. -type html_tag() :: binary() | string() | atom(). -type html_attr_name() :: binary() | string() | atom(). -type html_attr_value() :: binary() | string() | atom() | number(). -type html_element() :: html_node() | html_comment() | html_nop() | pi_tag() | inline_html() | {html_tag()} | {html_tag(), [ html_element() ]} | binary(). -type html_comment() :: {comment, Comment::binary()}. -type html_nop() :: {nop, [ html_element() ]}. % Special node used by sanitizer for unwanted elements -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(). -type html_tree() :: html_doctype() | html_node() | html_comment() | inline_html() | {html_tag()} | {html_tag(), [ html_element() ]} | pi_tag(). -type options() :: #{ mode => xml | html, escape => boolean(), lowercase => boolean() }. -export_type([ html_tree/0, 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, html_tag/0, html_attr_name/0, html_attr_value/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(Input, #{ mode => html, escape => true }). -spec parse( iodata(), options() ) -> {ok, html_node()} | {error, nohtml}. parse(Input, Options) -> Options1 = opts(Options), parse_tokens(tokens(Input, Options1), Options1). %% @doc Parse an HTML/XML document to a JSON compatible map. Attributes will be added %% as keys in an @attributes key. Elements will be mapped to keys with value lists. %% all keys are lowercased. -spec parse_to_map( Input :: iodata() | {binary, list(), list()} ) -> {ok, map()} | {error, term()}. parse_to_map(Input) -> parse_to_map(Input, #{ mode => html, escape => true }). %% @doc Parse an HTML/XML document to a JSON compatible map. Attributes will be added %% as keys in an @attributes key. Elements will be mapped to keys with value lists. %% all keys are lowercased. -spec parse_to_map( Input :: iodata() | {binary, list(), list()}, options() ) -> {ok, map()} | {error, term()}. parse_to_map({_, _, _} = Tree, _Options) -> {ok, tree_to_map(Tree, #{})}; parse_to_map(Input, Options) -> Options1 = opts(Options), case parse(Input, Options1) of {ok, Tree} -> {ok, tree_to_map(Tree, #{})}; {error, _} = Error -> Error end. opts(Options) -> Options#{ mode => maps:get(mode, Options, html), escape => maps:get(escape, Options, true) }. %% @doc Transform the output of tokens(Doc) into a HTML tree. -spec parse_tokens( [ html_token() ] ) -> {ok, html_node()} | {error, nohtml}. parse_tokens(Tokens) -> parse_tokens(Tokens, #{ mode => html, escape => true }). -spec parse_tokens( [ html_token() ], options() ) -> {ok, html_node()} | {error, nohtml}. parse_tokens(Tokens, #{ mode := Mode } = Options) when is_list(Tokens) -> %% Skip over doctype, processing instructions F = fun (X) -> case X of {start_tag, _, _, false} -> false; {start_tag, _, _, true} when Mode =:= xml -> false; _ -> true end end, case lists:dropwhile(F, Tokens) of [{start_tag, Tag, Attrs, false} | Rest] -> {Tree, _} = tree(Rest, [ norm({Tag, Attrs}, Options) ], Options), {ok, Tree}; [{start_tag, Tag, Attrs, true} ] when Mode =:= xml -> {Tree, _} = tree([], [ norm({Tag, Attrs}, Options) ], Options), {ok, Tree}; [] -> {error, nohtml} end. %% @doc Transform the input UTF-8 HTML into a token stream. -spec tokens( iodata() ) -> [ html_token() ]. tokens(Input) -> tokens(Input, #{ mode => html, escape => true }). -spec tokens( iodata(), options() ) -> [ html_token() ]. tokens(Input, Options) -> tokens(iolist_to_binary(Input), #decoder{}, [], Options). %% @doc Convert a html_node() tree to a list of tokens. -spec to_tokens( html_tree() ) -> [ html_token() ]. to_tokens(HtmlNode) -> to_tokens(HtmlNode, #{ mode => html, escape => true }). -spec to_tokens( html_tree(), options() ) -> [ html_token() ]. to_tokens({Tag0}, Options) -> to_tokens({Tag0, [], []}, Options); to_tokens(T={'=', _}, _Options) -> [T]; to_tokens(T={doctype, _}, _Options) -> [T]; to_tokens(T={comment, _}, _Options) -> [T]; to_tokens({Tag0, Acc}, Options) -> %% This is only allowed in sub-tags: {p, [{"class", "foo"}]} to_tokens({Tag0, [], Acc}, Options); to_tokens({Tag0, Attrs, []}, #{ mode := xml } = Options) -> Tag = to_tag(Tag0, Options), to_tokens_1([], [{start_tag, Tag, Attrs, true}], Options); to_tokens({Tag0, Attrs, Acc}, Options) -> Tag = to_tag(Tag0, Options), case is_singleton(Tag, Options) of true -> to_tokens_1([], [{start_tag, Tag, Attrs, true}], Options); false -> to_tokens_1([{Tag, Acc}], [{start_tag, Tag, Attrs, false}], Options) end. %% @doc Convert a list of html_token() to a HTML document. -spec to_html([ html_token() ] | html_tree() ) -> iodata(). to_html(Node) -> to_html(Node, #{ mode => html, escape => true }). -spec to_html([ html_token() ] | html_tree(), options() ) -> iodata(). to_html(Node, Options) when is_tuple(Node) -> Options1 = opts(Options), to_html(to_tokens(Node, Options1), Options1); to_html(Tokens, Options) when is_list(Tokens) -> Options1 = opts(Options), to_html_1(Tokens, [], Options1). %% @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(z_mochinum:digits(F), []). to_html_1([], Acc, _Options) -> lists:reverse(Acc); to_html_1([{'=', Content} | Rest], Acc, Options) -> to_html_1(Rest, [Content | Acc], Options); to_html_1([{pi, Bin} | Rest], Acc, Options) -> Open = [<<">, Bin, <<"?>">>], to_html_1(Rest, [Open | Acc], Options); to_html_1([{pi, Tag, Attrs} | Rest], Acc, Options) -> Open = [<<">, Tag, attrs_to_html(Attrs, []), <<"?>">>], to_html_1(Rest, [Open | Acc], Options); to_html_1([{comment, Comment} | Rest], Acc, Options) -> to_html_1(Rest, [[<<"">>] | Acc], Options); to_html_1([{doctype, Parts} | Rest], Acc, Options) -> Inside = doctype_to_html(Parts, Acc), to_html_1(Rest, [[<<">, Inside, <<">">>] | Acc], Options); to_html_1([{data, Data, _Whitespace} | Rest], Acc, #{ escape := true } = Options) -> to_html_1(Rest, [escape(Data) | Acc], Options); to_html_1([{data, Data, _Whitespace} | Rest], Acc, #{ escape := false } = Options) -> to_html_1(Rest, [Data | Acc], Options); to_html_1([{start_tag, Tag, Attrs, Singleton} | Rest], Acc, #{ mode := html } = Options) -> EscapeData = case Tag of <<"script">> -> false; _ -> true end, Open = [<<"<">>, Tag, attrs_to_html(Attrs, []), case Singleton of true -> <<" />">>; false -> <<">">> end], to_html_1(Rest, [Open | Acc], Options#{ escape := EscapeData }); to_html_1([{start_tag, Tag, Attrs, Singleton} | Rest], Acc, #{ mode := xml } = Options) -> Open = [<<"<">>, Tag, attrs_to_html(Attrs, []), case Singleton of true -> <<" />">>; false -> <<">">> end], to_html_1(Rest, [Open | Acc], Options#{ escape := true }); to_html_1([{end_tag, Tag} | Rest], Acc, Options) -> to_html_1(Rest, [[<<">, Tag, <<">">>] | Acc], Options#{ escape => 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, Options) when is_atom(A) -> norm(atom_to_binary(A, utf8), Options); to_tag(L, Options) -> norm(L, Options). to_tokens_1([], Acc, _Options) -> lists:reverse(Acc); to_tokens_1([{Tag, []} | Rest], Acc, Options) -> to_tokens_1(Rest, [{end_tag, to_tag(Tag, Options)} | Acc], Options); to_tokens_1([{Tag0, [{T0} | R1]} | Rest], Acc, Options) -> %% Allow {br} to_tokens_1([{Tag0, [{T0, [], []} | R1]} | Rest], Acc, Options); to_tokens_1([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc, Options) -> %% Allow {'=', iolist()} to_tokens_1([{Tag0, R1} | Rest], [T0 | Acc], Options); to_tokens_1([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc, Options) -> %% Allow {comment, iolist()} to_tokens_1([{Tag0, R1} | Rest], [T0 | Acc], Options); to_tokens_1([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc, Options) -> %% Allow {pi, binary()} to_tokens_1([{Tag0, R1} | Rest], [T0 | Acc], Options); to_tokens_1([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc, Options) -> %% Allow {pi, binary(), list()} to_tokens_1([{Tag0, R1} | Rest], [T0 | Acc], Options); to_tokens_1([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc, Options) -> %% Allow {p, [{"class", "foo"}]} to_tokens_1([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc, Options); to_tokens_1([{Tag0, [{T0, C0} | R1]} | Rest], Acc, Options) -> %% Allow {p, "content"} and {p, <<"content">>} to_tokens_1([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc, Options); to_tokens_1([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc, Options) when is_binary(C0) -> %% Allow {"p", [{"class", "foo"}], <<"content">>} to_tokens_1([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc, Options); to_tokens_1([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc, Options) when is_integer(C) -> %% Allow {"p", [{"class", "foo"}], "content"} to_tokens_1([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc, Options); to_tokens_1([{Tag0, [{T0, A1, []} | R1]} | Rest], Acc, #{ mode := xml } = Options) -> Tag = to_tag(Tag0, Options), T1 = to_tag(T0, Options), to_tokens_1([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc], Options); to_tokens_1([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc, Options) -> %% Native {"p", [{"class", "foo"}], ["content"]} Tag = to_tag(Tag0, Options), T1 = to_tag(T0, Options), case is_singleton(norm(T1, Options), Options) of true -> to_tokens_1([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc], Options); false -> to_tokens_1([{T1, C1}, {Tag, R1} | Rest], [{start_tag, T1, A1, false} | Acc], Options) end; to_tokens_1([{Tag0, [L | R1]} | Rest], Acc, Options) when is_list(L) -> %% List text Tag = to_tag(Tag0, Options), to_tokens_1([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc], Options); to_tokens_1([{Tag0, [B | R1]} | Rest], Acc, Options) when is_binary(B) -> %% Binary text Tag = to_tag(Tag0, Options), to_tokens_1([{Tag, R1} | Rest], [{data, B, false} | Acc], Options). tokens(B, S=#decoder{offset=O}, Acc, #{ mode := Mode } = Options) -> case B of <<_:O/binary>> -> lists:reverse(Acc); _ when Mode =:= xml -> {Tag, S1} = tokenize(B, S, Options), tokens(B, S1, [Tag | Acc], Options); _ when Mode =:= html -> {Tag, S1} = tokenize(B, S, Options), case parse_flag(Tag) of script -> {Tag2, S2} = tokenize_script(B, S1), tokens(B, S2, [Tag2, Tag | Acc], Options); textarea -> {Tag2, S2} = tokenize_textarea(B, S1), tokens(B, S2, [Tag2, Tag | Acc], Options); none -> tokens(B, S1, [Tag | Acc], Options) 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}, Options) -> 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. tree_to_map({Tag, [], Elts}, TagMap) when is_list(Elts) -> case lists:any(fun is_element/1, Elts) of true -> EltMap = lists:foldr( fun tree_to_map/2, #{}, Elts), TagMap#{ Tag => [ EltMap | maps:get(Tag, TagMap, []) ] }; false -> % Leave node, sub elements are values Vs = map_values(Elts), TagMap#{ Tag => lists:flatten([ Vs | maps:get(Tag, TagMap, []) ]) } end; tree_to_map({Tag, Attrs, Elts}, TagMap) when is_list(Elts) -> case lists:any(fun is_element/1, Elts) of true -> EltMap = lists:foldr( fun tree_to_map/2, #{}, Elts), EltMap1 = EltMap#{ <<"@attributes">> => Attrs }, TagMap#{ Tag => [ EltMap1 | maps:get(Tag, TagMap, []) ] }; false -> % Leave node, but with attributes V = #{ <<"@attributes">> => Attrs, <<"value">> => map_values(Elts) }, TagMap#{ Tag => [ V | maps:get(Tag, TagMap, []) ] } end; tree_to_map(_, TagMap) -> TagMap. map_values(Vs) -> lists:filtermap(fun map_value/1, Vs). map_value(B) when is_binary(B) -> {true, B}; map_value(_) -> false. is_element({Tag, Attrs, Elts}) when is_binary(Tag), is_list(Attrs), is_list(Elts) -> true; is_element(_) -> false. % @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.