src/xml_builderl.erl

-module(xml_builderl).

-export([document/1, document/2, document/3, doctype/2, elem/1, elem/2, elem/3]).
-export([generate/1, generate/2, generate_iodata/1, generate_iodata/2, from_map/1]).

-define(IS_BLANK_MAP(M), ((M == undefined) or (M == #{}))).
-define(IS_BLANK_LIST(L), ((L == undefined) or (L == []))).
-define(IS_BLANK_ATTRS(A), (?IS_BLANK_MAP(A) or ?IS_BLANK_LIST(A))).

document(Elems) ->
    [xml_decl | wrap(elems_with_prolog(Elems))].

document(Name, AttrsOrContent) ->
    [xml_decl | [elem(Name, AttrsOrContent)]].

document(Name, Attrs, Content) ->
    [xml_decl | [elem(Name, Attrs, Content)]].

%% Chose elem instead of element since element is a BIF

elem(Name) when is_binary(Name) ->
    elem({undefined, undefined, Name});
elem({iodata, _IoData} = IoData) ->
    elem({undefined, undefined, IoData});
elem(Name) when is_binary(Name) or is_atom(Name) ->
    elem({Name});
elem(List) when is_list(List) ->
    List1 = lists:filter(fun (E) ->
                         E =/= undefined
                 end, List),
    lists:map(fun elem/1, List1);
elem({Name}) ->
    elem(Name, undefined, undefined);
elem({Name, Attrs}) when is_map(Attrs) ->
    elem({Name, Attrs, undefined});
elem({Name, Content}) ->
    elem({Name, undefined, Content});
elem({Name, Attrs, Content}) when is_list(Content) ->
    {Name, Attrs, elem(Content)};
elem({Name, Attrs, Content}) ->
    {Name, Attrs, Content}.

elem(Name, Attrs) when is_map(Attrs) ->
    elem({Name, Attrs, undefined});
elem(Name, Content) ->
    elem({Name, undefined, Content}).

elem(Name, Attrs, Content) ->
    elem({Name, Attrs, Content}).

doctype(Name, [{system, SystemIdentifier}]) ->
    {doctype, {system, Name, SystemIdentifier}};
doctype(Name, [{public, [PublicIdentifier, SystemIdentifier]}]) ->
    {doctype, {public, Name, PublicIdentifier, SystemIdentifier}}.

elems_with_prolog([First | Rest]) when length(Rest) > 0 ->
    [first_elem(First) | elem(Rest)];
elems_with_prolog(ElemSpec) ->
    elem(ElemSpec).

first_elem({doctype, Args} = DoctypeDecl) when is_tuple(Args) ->
    DoctypeDecl;
first_elem(ElemSpec) ->
    elem(ElemSpec).

generate(Any) ->
    generate(Any, #{}).

generate(Any, Options) ->
    list_to_binary(format(Any, 0, Options)).

generate_iodata(Any) ->
    generate_iodata(Any, #{}).

generate_iodata(Any, Options) ->
    format(Any, 0, Options).

format(xml_decl, 0, Options) ->
    Encoding = maps:get(encoding, Options, <<"UTF-8">>),

    Standalone =
    case maps:get(standalone, Options, undefined) of
        true -> " standalone=\"yes\"";
        false -> " standalone=\"no\"";
        undefined -> ""
    end,
    ["<?xml version=\"1.0\" encoding=\"", to_string(Encoding), $", Standalone, "?>"];
format({doctype, {system, Name, System}}, 0, _Options) ->
    ["<!DOCTYPE ",
     to_string(Name),
     " SYSTEM \"",
     to_string(System),
     "\">"];
format({doctype, {public, Name, Public, System}}, 0, _Options) ->
    ["<!DOCTYPE ",
     to_string(Name),
     " PUBLIC \"",
     to_string(Public),
     "\" \"",
     to_string(System),
     "\">"];
format(String, Level, Options) when is_bitstring(String) ->
    format({undefined, undefined, String}, Level, Options);
format(List, Level, Options) when is_list(List) ->
    Formatter = formatter(Options),
    map_intersperse(List, Formatter:line_break(), fun (Arg) ->
                                                          format(Arg, Level, Options)
                                                  end);
format({undefined, undefined, Name}, Level, Options) when is_bitstring(Name) ->
    [indent(Level, Options), to_string(Name)];
format({undefined, undefined, {iodata, IoData}}, _Level, _Options) ->
    IoData;
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and ?IS_BLANK_LIST(Content) ->
    [indent(Level, Options),
     "<",
     to_string(Name),
     "/>"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_LIST(Content) ->
    [indent(Level, Options),
     "<",
     to_string(Name),
     " ",
     format_attributes(Attrs),
     "/>"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and not is_list(Content) ->
    [indent(Level, Options),
     "<",
     to_string(Name),
     ">",
     format_content(Content, Level + 1, Options),
     "</",
     to_string(Name),
     ">"];
format({Name, Attrs, Content}, Level, Options) when ?IS_BLANK_ATTRS(Attrs) and is_list(Content) ->
    Formatter = formatter(Options),
    FormatChar = Formatter:line_break(),
    [indent(Level, Options),
     "<",
     to_string(Name),
     ">",
     format_content(Content, Level + 1, Options),
     FormatChar,
     indent(Level, Options),
     "</",
     to_string(Name),
     ">"];
format({Name, Attrs, Content}, Level, Options) when not ?IS_BLANK_ATTRS(Attrs) and not is_list(Content) ->
    [indent(Level, Options),
     "<",
     to_string(Name),
     " ",
     format_attributes(Attrs),
     ">",
     format_content(Content, Level + 1, Options),
     "</",
     to_string(Name),
     ">"];
format({Name, Attrs, Content}, Level, Options) when not ?IS_BLANK_ATTRS(Attrs) and is_list(Content) ->
    Formatter = formatter(Options),
    FormatChar = Formatter:line_break(),

    [indent(Level, Options),
     "<",
     to_string(Name),
     " ",
     format_attributes(Attrs),
     ">",
     format_content(Content, Level + 1, Options),
     FormatChar,
     indent(Level, Options),
     "</",
     to_string(Name),
     ">"].

format_content(Children, Level, Options) when is_list(Children) ->
    Formatter = formatter(Options),
    FormatChar = Formatter:line_break(),
    [FormatChar, map_intersperse(Children, FormatChar, fun (Arg) ->
                                                               format(Arg, Level, Options)
                                                       end)];
format_content(Content, _Level, _Options) ->
    escape(Content).

format_attributes(Attrs) ->
    map_intersperse(Attrs, <<" ">>, fun ({Name, Value}) ->
                                            [to_string(Name), "=", quote_attribute_value(Value)]
                                    end).

indent(Level, Options) ->
    Formatter = formatter(Options),
    Formatter:indentation(Level, Options).

formatter(Options) ->
    case maps:get(format, Options, undefined) of
        none -> xml_builderl_format_none;
        _ -> xml_builderl_format_indented
    end.

quote_attribute_value(Val) when not is_bitstring(Val) ->
    quote_attribute_value(to_string(Val));
quote_attribute_value(Val) ->
    Escape = string_contains(Val, [<<"\"">>, <<"&">>, <<"<">>]),

    case Escape of
        true -> [$", escape(Val), $"];
        false -> [$", Val, $"]
    end.

escape({iodata, IoData}) ->
    IoData;
escape({safe, Data}) when is_bitstring(Data) ->
    Data;
escape({safe, Data}) ->
    to_string(Data);
escape({cdata, Data}) ->
    ["<![CDATA[", Data, "]]>"];
escape(Data) when is_binary(Data) ->
    to_string(escape_string(Data));
escape(Data) when not is_binary(Data) ->
    to_string(escape_string(to_string(Data))).

escape_string(<<"">>) ->
    <<"">>;
escape_string(<<"&"/utf8, Rest/binary>>) ->
    escape_entity(Rest);
escape_string(<<"<"/utf8, Rest/binary>>) ->
    ["&lt;" | escape_string(Rest)];
escape_string(<<">"/utf8, Rest/binary>>) ->
    ["&gt;" | escape_string(Rest)];
escape_string(<<"\""/utf8, Rest/binary>>) ->
    ["&quot;" | escape_string(Rest)];
escape_string(<<"'"/utf8, Rest/binary>>) ->
    ["&apos;" | escape_string(Rest)];
escape_string(<<C/utf8, Rest/binary>>) ->
    [C | escape_string(Rest)].

escape_entity(<<"amp;"/utf8, Rest/binary>>) ->
    ["&amp;" | escape_string(Rest)];
escape_entity(<<"lt;"/utf8, Rest/binary>>) ->
    ["&lt;" | escape_string(Rest)];
escape_entity(<<"gt;"/utf8, Rest/binary>>) ->
    ["&gt;" | escape_string(Rest)];
escape_entity(<<"quot;"/utf8, Rest/binary>>) ->
    ["&quot;" | escape_string(Rest)];
escape_entity(<<"apos;"/utf8, Rest/binary>>) ->
    ["&apos;" | escape_string(Rest)];
escape_entity(Rest) ->
    ["&amp;" | escape_string(Rest)].

to_string(Term) when is_binary(Term) ->
    Term;
to_string(Term) when is_bitstring(Term) ->
    Term;
to_string(Term) when is_list(Term) ->
    list_to_binary(Term);
to_string(Term) ->
    list_to_binary(lists:flatten(io_lib:format("~p", [Term]))).

wrap(Data) when is_list(Data) ->
    Data;
wrap(Data) ->
    [Data].

string_contains(S, Search) when is_binary(Search) ->
    nomatch =/= string:find(S, Search);
string_contains(_S, []) ->
    false;
string_contains(S, [H | T]) ->
    case string_contains(S, H) of
        true ->
            true;
        false ->
            string_contains(S, T)
    end.

%% Ported from Elixir standard library Enum.map_intersperse
map_intersperse(Enumerable, Separator, Mapper) when is_map(Enumerable) ->
    Reduced = lists:foldl(fun (Entry, Acc) ->
                                  case Acc == first of
                                      true ->
                                          [Mapper(Entry)];
                                      false ->
                                          [Mapper(Entry), Separator | Acc]
                                  end
                          end, first, maps:to_list(Enumerable)),
    case Reduced == first of
        true ->
            [];
        false ->
            lists:reverse(Reduced)
    end;
map_intersperse(Enumerable, Separator, Mapper) when is_list(Enumerable) ->
    map_intersperse_list(Enumerable, Separator, Mapper).

map_intersperse_list([], _, _) ->
    [];
map_intersperse_list([Last], _, Mapper) ->
    [Mapper(Last)];
map_intersperse_list([Head | Rest], Separator, Mapper) ->
    [Mapper(Head), Separator | map_intersperse_list(Rest, Separator, Mapper)].

from_map(Map) when is_map(Map) ->
    Tags = lists:map(fun ({Key, Value}) ->
                             build_tag(Key, Value)
                     end, maps:to_list(Map)),
    generate(document(Tags)).

build_tag(Key, Value) ->
    build_tag(Key, Value, #{}).

build_tag(Key, Map, Attributes) when is_map(Map) ->
    case maps:get(<<"#content">>, Map, undefined) of
        undefined ->
            Tags = lists:map(fun ({K, V}) ->
                                     build_tag(K, V) 
                             end, maps:to_list(Map)),
            elem(Key, Attributes, Tags);
        Value ->
            FAttributes = lists:filter(fun ({K, _V}) ->
                                               string:slice(K, 0, 1) == <<"-">>
                                       end, maps:to_list(Map)),
            Attributes2 = maps:from_list(lists:map(fun ({K, V}) ->
                                                 {string:slice(K, 1), V} 
                                         end, FAttributes)),
            build_tag(Key, Value, Attributes2)
    end;
build_tag(Key, Values, Attributes) when is_list(Values) ->
    lists:map(fun (Value) ->
                      build_tag(Key, Value, Attributes)
              end, Values);
build_tag(Key, Value, Attributes) ->
    elem(Key, Attributes, to_string(Value)).

-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

string_contains_test() ->
    ?assert(string_contains(<<"foo\"">>, <<"\"">>)),
    ?assert(string_contains(<<"foo&">>, <<"&">>)),
    ?assert(string_contains(<<"foo<">>, <<"<">>)),
    ?assert(string_contains(<<"foo\"">>, [<<"\"">>, <<"&">>, <<"<">>])).

element_test() ->
    ?assertEqual({person, undefined, undefined}, elem(person)),
    ?assertEqual({person, undefined, <<"data">>}, elem(person, <<"data">>)),
    ?assertEqual({person, #{id => 1}, undefined}, elem(person, #{id => 1})),
    ?assertEqual({person, #{id => 1}, <<"data">>}, elem(person, #{id => 1}, <<"data">>)).

element_nested_test() ->
    ?assertEqual({person, #{id => 1}, [{first, undefined, <<"Steve">>}, {last, undefined, <<"Jobs">>}]},
                 elem(person, #{id => 1}, [elem(first, <<"Steve">>), elem(last, <<"Jobs">>)])).

document_test() ->
    ?assertEqual([xml_decl, {person, undefined, undefined}], document(person)),

    ?assertEqual([xml_decl, {doctype, {system, <<"greeting">>, <<"hello.dtd">>}}, {greeting, undefined, <<"Hello, World!">>}], document([doctype(<<"greeting">>, [{system, <<"hello.dtd">>}]), {greeting, <<"Hello, World!">>}])),

    ?assertEqual([xml_decl, {doctype, {public, <<"html">>, <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
                 <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>}},
               {html, undefined, <<"Hello, world!">>}],
                 document([
             doctype(
               <<"html">>,
               [{public, [
                 <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
                 <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>
               ]}]
             ),
             {html, <<"Hello, world!">>}
           ])),

    ?assertEqual([xml_decl, {person, undefined, <<"Josh">>}],
                 document(person, <<"Josh">>)),

    ?assertEqual([xml_decl, {person, #{id => 1}, <<"Josh">>}],
                 document(person, #{id => 1}, <<"Josh">>)).

generate_iodata_test() ->
    ?assertEqual([<<"">>, "<", <<"person">>, "/>"],
                 generate_iodata(elem(person))),
    ?assertEqual([<<"">>, "<", <<"person">>, ">", [<<"test">>, $i, <<"ng 123">>], "</", <<"person">>, ">"],
                 generate_iodata(elem(person, {iodata, [<<"test">>, $i, <<"ng 123">>]}))).

generate_test() ->
    ?assertEqual(<<"<person/>">>, generate(elem(person))),
    ?assertEqual(<<"<person id=\"1\">Steve Jobs</person>">>, generate({person, #{id => 1}, <<"Steve Jobs">>})),
    ?assertEqual(<<"<name><first>Steve</first></name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]}, #{format => none})),
    ?assertEqual(<<"<name>\n<first>Steve</first>\n</name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]}, #{whitespace => <<"">>})),
    ?assertEqual(<<"<name>\n  <first>Steve</first>\n</name>">>, generate({name, undefined, [{first, undefined, <<"Steve">>}]})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>">>, generate(xml_decl, #{encoding => <<"ISO-8859-1">>})),

    Input = {level1, undefined, [{level2, undefined, <<"test_value">>}]},
    ?assertEqual(<<"<level1><level2>test_value</level2></level1>">>, generate(Input, #{format => none})),
    ?assertEqual(<<"<level1>\n\t<level2>test_value</level2>\n</level1>">>, generate(Input, #{whitespace => <<"\t">>})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>">>, generate(xml_decl)),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>">>,
                 generate(xml_decl, #{standalone => true})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>">>,
                 generate(xml_decl, #{standalone => false})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>">>,
                 generate(xml_decl)).


document_generate_test() ->
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
                 generate(document(person))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
                 generate(document(person, <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?><person/>">>,
                 generate(document(person), #{format => none})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"1\"/>">>,
                 generate(document(person, #{id => 1}))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"1\">some data</person>">>,
                 generate(document(person, #{id => 1}, <<"some data">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE greeting SYSTEM \"hello.dtd\">\n<greeting>Hello, World!</greeting>">>,
                 generate(document([doctype(<<"greeting">>, [{system, <<"hello.dtd">>}]),
                                    {greeting, <<"Hello, World!">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html>Hello, world!</html>">>,
                generate(document([doctype(<<"html">>, [{public, [<<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
                                                                  <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]}]), {html, <<"Hello, world!">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<oldschool/>">>,
                 generate(document([elem(oldschool, [])]), #{format => indent, encoding => <<"ISO-8859-1">>})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" standalone=\"yes\"?>\n<standaloneOldschool/>">>,
                 generate(document([elem(standaloneOldschool, [])]), #{format => indent, encoding => <<"ISO-8859-1">>, standalone => true})),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
                 generate(document(person, <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\"/>">>,
                 generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\">Josh</person>">>,
                 generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}, <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person city=\"Montreal\" occupation=\"Developer\"/>">>,
                 generate(document(person, #{occupation => <<"Developer">>, city => <<"Montreal">>}, undefined))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
                 generate(document(person, #{}, <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
                 generate(document(person, #{}, undefined))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person occupation=\"Developer\" city=\"Montreal\">Josh</person>">>,
                 generate(document(person, [{occupation, <<"Developer">>}, {city, <<"Montreal">>}], <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person occupation=\"Developer\" city=\"Montreal\"/>">>,
                 generate(document(person, [{occupation, <<"Developer">>}, {city, <<"Montreal">>}], undefined))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>Josh</person>">>,
                 generate(document(person, [], <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person/>">>,
                 generate(document(person, [], undefined))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n  <name id=\"123\">Josh</name>\n</person>">>,
                 generate(document(person, [{name, #{id => 123}, <<"Josh">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n  <first_name>Josh</first_name>\n  <last_name>Nussbaum</last_name>\n</person>">>,
                 generate(document(person, [{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"123\">\n  <name>Josh</name>\n</person>">>,
                 generate(document(person, #{id => 123}, [{name, <<"Josh">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person id=\"123\">\n  <first_name>Josh</first_name>\n  <last_name>Nussbaum</last_name>\n</person>">>,
                 generate(document(person, #{id => 123}, [{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n  TextNode\n  <name id=\"123\">Josh</name>\n  TextNode\n</person>">>,
                 generate(document(person, [<<"TextNode">>, {name, #{id => 123}, <<"Josh">>}, <<"TextNode">>]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<name id=\"123\">Josh</name>">>,
                 generate(document(name, #{id => 123}, <<"Josh">>))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name>Josh</first_name>\n<last_name>Nussbaum</last_name>">>,
                 generate(document([{first_name, <<"Josh">>}, {last_name, <<"Nussbaum">>}]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name/>\n<middle_name/>\n<last_name/>">>,
                 generate(document([first_name, middle_name, last_name]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<first_name/>\n<last_name/>">>,
                 generate(document([first_name, undefined, last_name]))),
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n  <first>Josh</first>\n  <last>Nussbaum</last>\n</person>">>,
                 generate(document(person, [{first, <<"Josh">>}, {last, <<"Nussbaum">>}]))).

element_generate_test() ->
    ?assertEqual(<<"<person>Josh</person>">>, generate(elem(person, <<"Josh">>))),
    ?assertEqual(<<"<person occupation=\"Developer\">Josh</person>">>, generate(elem(person, #{occupation => <<"Developer">>}, <<"Josh">>))),
    ?assertEqual(<<"<person height=\"12\"/>">>, generate(elem(person, #{height => 12}))),
    ?assertEqual(<<"<person height=\"10'\"/>">>, generate(elem(person, #{height => <<"10'">>}))),
    ?assertEqual(<<"<person height=\"10&quot;\"/>">>, generate(elem(person, #{height => <<"10\"">>}))),
    ?assertEqual(<<"<person height=\"&lt;10&apos;5&quot;\"/>">>, generate(elem(person, #{height => <<"<10'5\"">>}))),
    ?assertEqual(<<"<person height=\"&lt;10\"/>">>, generate(elem(person, #{height => <<"<10">>}))),
    ?assertEqual(<<"<person>Josh</person>">>, generate(elem(person, <<"Josh">>))),
    ?assertEqual(<<"<person>&lt;Josh&gt;</person>">>, generate(elem(person, <<"<Josh>">>))),
    ?assertEqual(<<"<data>1 &lt;&gt; 2 &amp; 2 &lt;&gt; 3 &quot;&apos;&quot;&apos;</data>">>,
                 generate(elem(data, <<"1 <> 2 & 2 <> 3 \"'\"'">>))),
    ?assertEqual(<<"<data>&gt;&lt;&quot;&apos;&amp;</data>">>,
                 generate(elem(data, <<"&gt;&lt;&quot;&apos;&amp;">>))),
    ?assertEqual(<<"<person><![CDATA[john & <is ok>]]></person>">>,
                 generate(elem(person, {cdata, "john & <is ok>"}))),
    ?assertEqual(<<"<person>john & <is ok></person>">>,
                 generate(elem(person, {safe, <<"john & <is ok>">>}))),
    ?assertEqual(<<"<person><name>john</name><age>12</age></person>">>,
                 generate(elem(person, {iodata, [generate(elem(name, <<"john">>)), generate(elem(age, <<"12">>))]}))),
    ?assertEqual(<<"<person>testing 123</person>">>,
                 generate(elem(person, {iodata, [<<"test">>, $i, <<"ng 123">>]}))).

map_to_xml_test() ->
    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>Value1</Tag1>">>,
                 from_map(#{<<"Tag1">> => <<"Value1">>})),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>Value1</Tag1>\n<Tag2>Value2</Tag2>\n<Tag3>Value3</Tag3>">>,
                 from_map(#{<<"Tag1">> => <<"Value1">>,
                            <<"Tag2">> => <<"Value2">>,
                            <<"Tag3">> => <<"Value3">>})),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tags>\n  <Tag1>\n    <Sub1>Val1</Sub1>\n  </Tag1>\n  <Tag1>\n    <Sub1>Val2</Sub1>\n  </Tag1>\n  <Tag1>\n    <Sub1>Val3</Sub1>\n  </Tag1>\n</Tags>">>,
                 from_map(#{
               <<"Tags">> => #{
                 <<"Tag1">> => [
                                #{<<"Sub1">> => <<"Val1">>},
                                #{<<"Sub1">> => <<"Val2">>},
                                #{<<"Sub1">> => <<"Val3">>}
                 ]
               }
             })),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>1234</Tag1>\n<Tag2>12.15</Tag2>">>,
                 from_map(#{<<"Tag1">> => 1234, <<"Tag2">> => 12.15})),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1>true</Tag1>\n<Tag2>false</Tag2>">>,
                 from_map(#{<<"Tag1">> => true, <<"Tag2">> => false})),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"123\" something=\"111\">some value</Tag1>">>,
                 from_map(#{<<"Tag1">> => #{<<"#content">> => <<"some value">>,
                                            <<"-id">> => 123,
                                            <<"-something">> => <<"111">>}})),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"123\" something=\"111\">\n  <Tag2>\n    <Tag3>value</Tag3>\n  </Tag2>\n</Tag1>">>,
                 from_map(#{
               <<"Tag1">> => #{
                 <<"#content">> => #{
                   <<"Tag2">> => #{
                     <<"Tag3">> => <<"value">>
                   }
                 },
                 <<"-id">> => 123,
                 <<"-something">> => <<"111">>
               }
             })),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"1234\" something=\"value\">\n  <Tag2 id=\"1234\" something=\"value\">\n    <Tag3 id=\"1234\" something=\"value\">final value</Tag3>\n  </Tag2>\n</Tag1>">>,
                 from_map(#{
               <<"Tag1">> => #{
                 <<"#content">> => #{
                   <<"Tag2">> => #{
                     <<"#content">> => #{
                       <<"Tag3">> => #{
                         <<"#content">> => <<"final value">>,
                         <<"-id">> => <<"1234">>,
                         <<"-something">> => <<"value">>
                       }
                     },
                     <<"-id">> => <<"1234">>,
                     <<"-something">> => <<"value">>
                   }
                 },
                 <<"-id">> => <<"1234">>,
                 <<"-something">> => <<"value">>
               }
             })),

    ?assertEqual(<<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Tag1 id=\"1234\" something=\"value\">value1</Tag1>\n<Tag1 id=\"1234\" something=\"value\">value2</Tag1>">>,
                 from_map(#{
               <<"Tag1">> => #{
                 <<"#content">> => [
                   <<"value1">>,
                   <<"value2">>
                 ],
                 <<"-id">> => <<"1234">>,
                 <<"-something">> => <<"value">>
               }
             })).


-endif.