defmodule SimpleXmlParser.Utils.PureXmlParser do @moduledoc """ The module can parse pure xml strings """ @typedoc """ A node object from parser, contains: node name, content, attributes. """ @type xml_node() :: {Atom.t(), List.t() | String.t(), List.t()} @doc ~S""" Generate a structure that preserve xml hierarchy and help to parse it to another types ## Examples Parse a primitive object: iex> SimpleXmlParser.Utils.PureXmlParser.parse("Hello!") {:return, "Hello!", []} Parse an object with attributes: iex> SimpleXmlParser.Utils.PureXmlParser.parse("") {:return, [], [id: "1"]} Parse a complex object: iex> "1" ...> |> SimpleXmlParser.Utils.PureXmlParser.parse() {:return, [{:childs, [{:child, "1", []}], []}], []} iex> "12" ...> |> SimpleXmlParser.Utils.PureXmlParser.parse() {:return, [{:childs, [{:child, "1", []}, {:child, "2", []}], []}], []} """ @spec parse(String.t()) :: xml_node() def parse(xml_string) do xml_string |> :erlsom.parse_sax(nil, &sax_handler/2) |> get_sax_response() end defp sax_handler({:characters, value}, [{name, _, attrs} | tail]) do [{name, to_string(value), attrs} | tail] end # Put a new element as a first of the working stack defp sax_handler({:startElement, _, name, _, attrs}, acc) do [{List.to_atom(name), [], map_sax_attributes(attrs)} | acc] end # Put a head node as a child of the forward node defp sax_handler({:endElement, _, _, _}, [element | [head | tail]]) do {name, container, attrs} = head [{name, container ++ [element], attrs} | tail] end # Set a root element as root tuple defp sax_handler({:endElement, _, _, _}, [response]), do: response # Start a document with the working stack defp sax_handler(:startDocument, _), do: [] # Skip another situations defp sax_handler(_, acc), do: acc defp map_sax_attributes(attrs) do attrs |> Enum.filter(&match?({:attribute, _, _, _, _}, &1)) |> Enum.map(fn {:attribute, name, _, _, value} -> {List.to_atom(name), to_string(value)} end) end defp get_sax_response({:ok, response, _}), do: response end