defmodule SAXMap do
@moduledoc """
XML to Map conversion.
SAXMap uses a SAX parser (built on top of [Saxy](https://hex.pm/packages/saxy)) to transfer an XML string or file stream into a `Map` containing a collection
of pairs where the key is the element name and the value is its content, and it is a optional to process element attribute into the result.
"""
@doc ~S'''
Use `Saxy.parse_string/4` with a custom SAX parse handler to extract a `Map` containing a collection of pairs where the key is the element name
and the value is its content, there can optionally append the key-value pair from the attribute of element.
## Example
Here is an example:
iex> xml = """
...>
...>
...> Hello
...>
...> - item1
...> - item2
...>
...>
...> """
iex> SAXMap.from_string(xml)
{:ok,
%{
"thread" => %{"items" => %{"item" => ["item1", "item2"]}, "title" => "Hello"}
}}
## Options
* `:ignore_attribute`, whether to ignore the attributes of elements in the final map, by default is `true` so
there will not see any attributes in the result; when set this option as `false`, it equals `{false, ""}`,
in this case, there with append the attributes of all elements by the processing order, and put the attributes
key-value pair into the peer child elements, and automatically naming child elements with "content",
we can also set this option as `{false, "@"}` or `{false, "-"}`, any proper naming prefix you perfer should be fine to process.
```
xml = """
Hello
- item1
- item2
- item3
"""
# set ignore_attribute: false
SAXMap.from_string(xml, ignore_attribute: false)
{:ok,
%{
"thread" => %{
"content" => %{
"items" => %{
"content" => %{
"item" => [
%{"content" => "item1", "font" => "12"},
%{"content" => "item2", "font" => "12"},
%{"content" => "item3", "font" => "12"}
]
},
"size" => "3"
},
"title" => %{"color" => "red", "content" => "Hello", "font" => "16"}
},
"version" => "1"
}
}}
# set ignore_attribute: {false, "@"}
SAXMap.from_string(xml, ignore_attribute: {false, "@"})
{:ok,
%{
"thread" => %{
"@version" => "1",
"content" => %{
"items" => %{
"@size" => "3",
"content" => %{
"item" => [
%{"@font" => "12", "content" => "item1"},
%{"@font" => "12", "content" => "item2"},
%{"@font" => "12", "content" => "item3"}
]
}
},
"title" => %{"@color" => "red", "@font" => "16", "content" => "Hello"}
}
}
}}
```
Please notice that the comments of XML are ignored.
'''
@spec from_string(xml :: String.t()) ::
{:ok, map :: map()} | {:error, exception :: Saxy.ParseError.t()}
def from_string(xml, opts \\ []) do
ignore_attribute = Keyword.get(opts, :ignore_attribute, true)
parse_from_string(xml, ignore_attribute)
end
defp parse_from_string(xml, true) do
Saxy.parse_string(xml, SAXMap.Handler, ignore_attribute: true)
end
defp parse_from_string(xml, false) do
Saxy.parse_string(xml, SAXMap.Handler, ignore_attribute: false)
end
defp parse_from_string(xml, {false, attribute_prefix}) do
Saxy.parse_string(xml, SAXMap.Handler, ignore_attribute: {false, attribute_prefix})
end
@doc ~S'''
Use `Saxy.parse_stream/4` with a custom SAX parse handler to extract a `Map` containing a collection of pairs where the key is the element name
and the value is its content, there can optionally append the key-value pair from the attribute of element.
## Options
Please see `from_string/2`
'''
def from_stream(stream, opts \\ []) do
ignore_attribute = Keyword.get(opts, :ignore_attribute, true)
parse_from_stream(stream, ignore_attribute)
end
defp parse_from_stream(stream, true) do
Saxy.parse_stream(stream, SAXMap.Handler, ignore_attribute: true)
end
defp parse_from_stream(stream, false) do
Saxy.parse_stream(stream, SAXMap.Handler, ignore_attribute: false)
end
defp parse_from_stream(stream, {false, attribute_prefix}) do
Saxy.parse_stream(stream, SAXMap.Handler, ignore_attribute: {false, attribute_prefix})
end
end