Current section

Files

Jump to
sax_map lib handler.ex
Raw

lib/handler.ex

defmodule SAXMap.Handler do
@moduledoc false
@behaviour Saxy.Handler
# The key name of internal text content when process XML mixed content
@key_text_content "_#{__MODULE__}.TextContent_"
# The key name of the transfer map result for the text content
# part extracted from the original XML data
@key_content "content"
def handle_event(:start_document, _prolog, opts) do
{:ok, {[], Map.new(opts)}}
end
def handle_event(
:start_element,
{tag_name, _attributes},
{stack, %{ignore_attribute: true} = options}
) do
stack = prepare_stack_text_when_start_element(stack)
tag = {tag_name, nil}
{:ok, {[tag | stack], options}}
end
def handle_event(
:start_element,
{tag_name, attributes},
{stack, %{ignore_attribute: false} = options}
) do
stack = prepare_stack_text_when_start_element(stack)
tag = {tag_name, attributes, nil}
{:ok, {[tag | stack], options}}
end
def handle_event(
:start_element,
{tag_name, attributes},
{stack, %{ignore_attribute: {false, attribute_prefix}} = options}
) do
stack = prepare_stack_text_when_start_element(stack)
attributes =
Enum.map(attributes, fn {key, value} ->
{"#{attribute_prefix}#{key}", value}
end)
tag = {tag_name, attributes, nil}
{:ok, {[tag | stack], options}}
end
def handle_event(:cdata, cdata, {[{tag_name, _} | stack], options}) do
current = {tag_name, cdata}
{:ok, {[current | stack], options}}
end
def handle_event(:cdata, cdata, {[{tag_name, attributes, _} | stack], options}) do
current = {tag_name, attributes, cdata}
{:ok, {[current | stack], options}}
end
def handle_event(:characters, "\r" <> _ = chars, state) do
ignore_or_extract_characters(chars, state)
end
def handle_event(:characters, "\n" <> _ = chars, state) do
ignore_or_extract_characters(chars, state)
end
def handle_event(
:characters,
chars,
{[{_not_end_tag, prepared}] = _stack, _} = state
)
when prepared != nil do
ignore_or_extract_characters(chars, state)
end
def handle_event(:characters, chars, state) do
extract_characters(chars, state)
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, content} | []], %{ignore_attribute: true} = options}
) do
current = {tag_name, content}
{:ok, {current, options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, attributes, content} | []], %{ignore_attribute: false} = options}
) do
current = {tag_name, attributes, content}
{:ok, {current, options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, attributes, content} | []],
%{ignore_attribute: {false, _attribute_prefix}} = options}
) do
current = {tag_name, attributes, content}
{:ok, {current, options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, content} | [{parent_tag_name, nil} | rest]],
%{ignore_attribute: true} = options}
) do
current = {tag_name, format_key_value_pairs(content)}
parent = {parent_tag_name, [current]}
{:ok, {[parent | rest], options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, attributes, content} | [{parent_tag_name, parent_attributes, nil} | rest]],
%{ignore_attribute: false} = options}
) do
formated_content = format_key_value_pairs(content)
current = %{tag_name => format_key_value_pairs([{@key_content, formated_content} | attributes])}
parent = {parent_tag_name, parent_attributes, [current]}
{:ok, {[parent | rest], options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, attributes, content} | [{parent_tag_name, parent_attributes, nil} | rest]],
%{ignore_attribute: {false, _attribute_prefix}} = options}
) do
formated_content = format_key_value_pairs(content)
current = %{tag_name => format_key_value_pairs([{@key_content, formated_content} | attributes])}
parent = {parent_tag_name, parent_attributes, [current]}
{:ok, {[parent | rest], options}}
end
def handle_event(
:end_element,
tag_name,
{[{tag_name, content} | [{parent_tag_name, parent_content} | rest]],
%{ignore_attribute: true} = options}
) do
current = {tag_name, format_key_value_pairs(content)}
parent = {parent_tag_name, [current | parent_content]}
{:ok, {[parent | rest], options}}
end
def handle_event(
:end_element,
tag_name,
{[
{tag_name, attributes, content}
| [{parent_tag_name, parent_attributes, parent_content} | rest]
], %{ignore_attribute: false} = options}
) do
formated_content = format_key_value_pairs(content)
current = %{tag_name => format_key_value_pairs([{@key_content, formated_content} | attributes])}
parent = {parent_tag_name, parent_attributes, [current | parent_content]}
{:ok, {[parent | rest], options}}
end
def handle_event(
:end_element,
tag_name,
{[
{tag_name, attributes, content}
| [{parent_tag_name, parent_attributes, parent_content} | rest]
], %{ignore_attribute: {false, _attribute_prefix}} = options}
) do
formated_content = format_key_value_pairs(content)
current = %{tag_name => format_key_value_pairs([{@key_content, formated_content} | attributes])}
parent = {parent_tag_name, parent_attributes, [current | parent_content]}
{:ok, {[parent | rest], options}}
end
def handle_event(:end_document, _, {{key, nil}, %{ignore_attribute: true}}) do
{:ok, %{key => %{}}}
end
def handle_event(:end_document, _, {{key, attributes, nil}, %{ignore_attribute: false}}) do
{:ok, %{key => format_key_value_pairs(attributes)}}
end
def handle_event(
:end_document,
_,
{{key, attributes, nil}, %{ignore_attribute: {false, _attribute_prefix}}}
) do
{:ok, %{key => format_key_value_pairs(attributes)}}
end
def handle_event(:end_document, _, {{key, value}, %{ignore_attribute: true}}) do
{:ok, %{key => format_key_value_pairs(value)}}
end
def handle_event(:end_document, _, {{key, attributes, value}, %{ignore_attribute: false}}) do
content =
format_key_value_pairs(attributes) |> Map.put(@key_content, format_key_value_pairs(value))
{:ok, %{key => content}}
end
def handle_event(
:end_document,
_,
{{key, attributes, value}, %{ignore_attribute: {false, _attribute_prefix}}}
) do
content =
format_key_value_pairs(attributes) |> Map.put(@key_content, format_key_value_pairs(value))
{:ok, %{key => content}}
end
defp format_key_value_pairs(items) when is_list(items) do
list_to_map(items, %{})
end
defp format_key_value_pairs(item) do
item
end
defp list_to_map([], prepared) do
prepared
end
defp list_to_map([%{@key_text_content => text_items} | rest], prepared) do
# Use "content" instead of @key_text_content as the key name
list_to_map(rest, Map.put(prepared, @key_content, Enum.reverse(text_items)))
end
defp list_to_map([item | rest], prepared) when is_map(item) do
[{key, value}] = Map.to_list(item)
current_value = Map.get(prepared, key)
prepared = put_or_concat_to_map(current_value, prepared, key, value)
list_to_map(rest, prepared)
end
defp list_to_map([{key, value} | rest], prepared) do
current_value = Map.get(prepared, key)
prepared = put_or_concat_to_map(current_value, prepared, key, value)
list_to_map(rest, prepared)
end
defp put_or_concat_to_map(nil, map, key, value) do
Map.put(map, key, value)
end
defp put_or_concat_to_map(current_value, map, key, value) when is_list(current_value) do
Map.put(map, key, [value | current_value])
end
defp put_or_concat_to_map(current_value, map, key, value) do
Map.put(map, key, [value, current_value])
end
defp ignore_or_extract_characters(chars, state) do
if String.trim(chars) == "" do
# ignore
{:ok, state}
else
extract_characters(chars, state)
end
end
defp extract_characters(chars, {stack, %{ignore_attribute: true} = options}) do
[{tag_name, content} | stack] = stack
current = {tag_name, append_characters_text_content(content, chars)}
{:ok, {[current | stack], options}}
end
defp extract_characters(chars, {stack, %{ignore_attribute: false} = options}) do
[{tag_name, attributes, content} | stack] = stack
current = {tag_name, attributes, append_characters_text_content(content, chars)}
{:ok, {[current | stack], options}}
end
defp extract_characters(
chars,
{stack, %{ignore_attribute: {false, _attribute_prefix}} = options}
) do
[{tag_name, attributes, content} | stack] = stack
current = {tag_name, attributes, append_characters_text_content(content, chars)}
{:ok, {[current | stack], options}}
end
defp prepare_stack_text_when_start_element([{key, value} | rest]) when is_bitstring(value) do
[{key, [%{@key_text_content => [value]}]} | rest]
end
defp prepare_stack_text_when_start_element([{key, attributes, value} | rest]) when is_bitstring(value) do
[{key, attributes, [%{@key_text_content => [value]}]} | rest]
end
defp prepare_stack_text_when_start_element(stack), do: stack
defp append_characters_text_content(nil, chars), do: chars
defp append_characters_text_content(content, chars) when is_list(content) do
case Enum.reverse(content) do
[%{@key_text_content => text_items} | rest] ->
Enum.reverse([%{@key_text_content => [chars | text_items]} | rest])
items ->
Enum.reverse([%{@key_text_content => [chars]} | items])
end
end
end