defmodule Pealist.XML do require Record @moduledoc false Record.defrecordp( :element_node, :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl") ) Record.defrecordp( :text_node, :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl") ) def decode(xml) do {doc, _} = xml |> :binary.bin_to_list() |> :xmerl_scan.string([{:comments, false}, {:space, :normalize}]) root = doc |> element_node(:content) |> Enum.reject(&empty?/1) |> Enum.at(0) parse_value(root) end def encode(term) do """ #{encode_plist_data(term, 0)} """ end defp parse_value(element_node() = element) do parse_value(element_node(element, :name), element_node(element, :content)) end defp parse_value(:string, list) do do_parse_text_nodes(list, "") end defp parse_value(:date, nodes) do str = parse_value(:string, nodes) {:ok, dt, _} = DateTime.from_iso8601(str) dt end defp parse_value(:data, nodes) do {:ok, data} = parse_value(:string, nodes) |> Base.decode64(ignore: :whitespace) data end defp parse_value(true, []), do: true defp parse_value(false, []), do: false defp parse_value(:integer, nodes) do parse_value(:string, nodes) |> String.to_integer() end defp parse_value(:real, nodes) do {value, ""} = parse_value(:string, nodes) |> Float.parse() value end defp parse_value(:array, contents) do contents |> Enum.reject(&empty?/1) |> Enum.map(&parse_value/1) end defp parse_value(:dict, contents) do {keys, values} = contents |> Enum.reject(&empty?/1) |> Enum.split_with(fn element -> element_node(element, :name) == :key end) unless length(keys) == length(values), do: raise("Key/value pair mismatch") keys |> Enum.map(&element_text_value/1) |> Enum.zip(values) |> Enum.into(%{}, fn {key, element} -> {key, parse_value(element)} end) end defp element_text_value(element) do case element_node(element, :content) do [content_node] -> content_node |> text_node(:value) |> :unicode.characters_to_binary() [] -> "" end end defp do_parse_text_nodes([], result), do: result defp do_parse_text_nodes([node | list], result) do text = node |> text_node(:value) |> :unicode.characters_to_binary() do_parse_text_nodes(list, result <> text) end defp empty?({:xmlText, _, _, [], ~c" ", :text}), do: true defp empty?(_), do: false defp encode_plist_data(%NaiveDateTime{} = dt, level) do dt |> DateTime.from_naive!("Etc/UTC") |> encode_plist_data(level) end defp encode_plist_data(%DateTime{time_zone: "Etc/UTC"} = dt, level) do indent(["", DateTime.to_iso8601(dt), "\n"], level) end defp encode_plist_data(%DateTime{} = dt, _level) do raise "Unable to encode DateTime #{dt}, please use Etc/UTC timezone" end defp encode_plist_data(val, level) when is_map(val) do [ indent("\n", level), val |> Enum.sort_by(fn {k, _v} -> k end) |> Enum.map(fn {k, v} -> [ indent(["", xml_escape(k), "\n"], level + 1), encode_plist_data(v, level + 1) ] end), indent("\n", level) ] end defp encode_plist_data(array, level) when is_list(array) do [ indent("\n", level), Enum.map(array, &encode_plist_data(&1, level + 1)), indent("\n", level) ] end defp encode_plist_data(true, level), do: indent("\n", level) defp encode_plist_data(false, level), do: indent("\n", level) defp encode_plist_data(int, level) when is_integer(int) do indent(["", to_string(int), "\n"], level) end defp encode_plist_data(real, level) when is_float(real) do indent(["", to_string(real), "\n"], level) end defp encode_plist_data(str, level) when is_binary(str) do if String.printable?(str) do indent(["", xml_escape(str), "\n"], level) else indent(["", Base.encode64(str), "\n"], level) end end defp indent(str, level) when is_binary(str), do: indent([str], level) defp indent(io_list, level) when is_list(io_list) do [String.duplicate(" ", level) | io_list] end defp xml_escape(str) do str |> String.replace("&", "&") |> String.replace(~s("), """) |> String.replace("'", "'") |> String.replace("<", "<") |> String.replace(">", ">") end end