defmodule XmerlXmlIndent do
@moduledoc """
Erlang OTP's built-in `xmerl` library lacks functionality to print XML with indent.
This module fills the gap by providing a custom callback to print XML with indent.
This module is taken from https://github.com/erlang/otp/blob/master/lib/xmerl/src/xmerl_xml.erl,
converted to Elixir and modified for indentation.
This module is used in conjunction with Erlang's `xmerl` library. See the project documentation for details.
"""
def unquote(:"#xml-inheritance#")() do
[]
end
def unquote(:"#text#")(text) do
:xmerl_lib.export_text(text)
end
def unquote(:"#root#")(data, [%{name: _, value: v}], [], _e) do
[v, data]
end
def unquote(:"#root#")(data, _attrs, [], _e) do
["<?xml version=\"1.0\"?>\n", data]
end
def unquote(:"#element#")(tag, [], attrs, _parents, _e) do
:xmerl_lib.empty_tag(tag, attrs)
end
def unquote(:"#element#")(tag, data, attrs, parents, _e) do
data =
cond do
is_a_tag?(data) ->
level = Enum.count(parents)
data
|> clean_up_tag()
|> indent_tag_lines(level)
true ->
data
end
:xmerl_lib.markup(tag, attrs, data)
end
@doc """
This function distinguishes an XML tag from an XML value.
Let's say there's an XML string `<Outer><Inner>Value</Inner></Outer>`,
there will be two calls to this function:
1. The first call has `data` parameter `['Value']`
2. The second call has `data` parameter
`[[['<', 'Inner', '>'], ['Value'], ['</', 'Inner', '>']]]`
The first one is an XML value, not an XML tag.
The second one is an XML tag.
"""
defp is_a_tag?(data) do
is_all_chars =
Enum.reduce(
data,
true,
fn d, acc ->
is_char = is_integer(Enum.at(d, 0))
acc && is_char
end)
!is_all_chars
end
@doc """
This function cleans up a tag data contaminated by characters outside the tag.
If the tag data is indented, this function removes the new lines
```
[
'\\n ',
[['<', 'Tag', '>'], ['Value'], ['</', 'Tag', '>']],
'\\n '
]
```
After the cleanup, the tag data looks like this:
```
[[['<', 'Tag', '>'], ['Value'], ['</', 'Tag', '>']]]
```
"""
defp clean_up_tag(data) do
Enum.filter(
data,
fn d -> !is_integer(Enum.at(d, 0)) end)
end
@doc """
This function indents all tag lines in the data.
Example clean tag data:
```
[
[['<', 'Tag1', '>'], ['Value 1'], ['</', 'Tag1', '>']],
[['<', 'Tag2', '>'], ['Value 2'], ['</', 'Tag2', '>']],
]
```
This function interleaves the tag data with indented new lines and appends
a new line with one lower level indent:
```
[
['\\n ']
['<', 'Tag1', '>'],
['Value 1'],
['</', 'Tag1', '>'],
['\\n ']
['<', 'Tag2', '>'],
['Value 2'],
['</', 'Tag2', '>'],
['\\n']
]
```
"""
defp indent_tag_lines(data, level) do
indented =
Enum.reduce(
data,
[],
fn d, acc ->
acc ++ [prepend_indent(level + 1)] ++ d
end)
indented ++ [prepend_indent(level)]
end
defp prepend_indent(level) do
("\n" <> String.duplicate(" ", level)) |> to_charlist()
end
end