|
1 | 1 | defmodule QuickBEAM.BeamAPI do |
2 | 2 | @moduledoc false |
3 | 3 | import Bitwise |
| 4 | + require Record |
| 5 | + |
| 6 | + Record.defrecord(:xml_element, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")) |
| 7 | + Record.defrecord(:xml_text, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")) |
| 8 | + Record.defrecord(:xml_attribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl")) |
4 | 9 |
|
5 | 10 | @version Mix.Project.config()[:version] |
6 | 11 |
|
@@ -191,6 +196,15 @@ defmodule QuickBEAM.BeamAPI do |
191 | 196 | Kernel.inspect(value, pretty: true, width: 80) |
192 | 197 | end |
193 | 198 |
|
| 199 | + @spec xml_parse([String.t()]) :: map() |
| 200 | + def xml_parse([xml]) when is_binary(xml) do |
| 201 | + {document, _rest} = :xmerl_scan.string(String.to_charlist(xml), quiet: true) |
| 202 | + %{element_name(document) => convert_element(document)} |
| 203 | + catch |
| 204 | + :exit, reason -> |
| 205 | + raise ArgumentError, "invalid XML: #{Exception.format_exit(reason)}" |
| 206 | + end |
| 207 | + |
194 | 208 | @spec password_hash(list()) :: String.t() |
195 | 209 | def password_hash([password, iterations]) |
196 | 210 | when is_binary(password) and is_integer(iterations) and iterations > 0 do |
@@ -252,6 +266,78 @@ defmodule QuickBEAM.BeamAPI do |
252 | 266 | end |
253 | 267 | end |
254 | 268 |
|
| 269 | + defp convert_element(element) do |
| 270 | + attributes = |
| 271 | + element |
| 272 | + |> xml_element(:attributes) |
| 273 | + |> Enum.reduce(%{}, fn attribute, acc -> |
| 274 | + Map.put(acc, "@#{attribute_name(attribute)}", attribute_value(attribute)) |
| 275 | + end) |
| 276 | + |
| 277 | + children = |
| 278 | + element |
| 279 | + |> xml_element(:content) |
| 280 | + |> Enum.reduce(%{text: [], elements: %{}}, &reduce_xml_content/2) |
| 281 | + |
| 282 | + text = |
| 283 | + children.text |
| 284 | + |> Enum.reverse() |
| 285 | + |> Enum.join(" ") |
| 286 | + |> String.trim() |
| 287 | + |
| 288 | + cond do |
| 289 | + map_size(attributes) == 0 and map_size(children.elements) == 0 -> |
| 290 | + text |
| 291 | + |
| 292 | + text == "" -> |
| 293 | + Map.merge(attributes, children.elements) |
| 294 | + |
| 295 | + true -> |
| 296 | + Map.merge(attributes, Map.put(children.elements, "#text", text)) |
| 297 | + end |
| 298 | + end |
| 299 | + |
| 300 | + defp reduce_xml_content(content, acc) do |
| 301 | + cond do |
| 302 | + match?({:xmlText, _, _, _, _, _}, content) -> |
| 303 | + case text_value(content) do |
| 304 | + "" -> acc |
| 305 | + value -> %{acc | text: [value | acc.text]} |
| 306 | + end |
| 307 | + |
| 308 | + match?({:xmlElement, _, _, _, _, _, _, _, _, _, _, _}, content) -> |
| 309 | + name = element_name(content) |
| 310 | + value = convert_element(content) |
| 311 | + %{acc | elements: Map.update(acc.elements, name, value, &merge_xml_children(&1, value))} |
| 312 | + |
| 313 | + true -> |
| 314 | + acc |
| 315 | + end |
| 316 | + end |
| 317 | + |
| 318 | + defp merge_xml_children(existing, value) when is_list(existing), do: existing ++ [value] |
| 319 | + defp merge_xml_children(existing, value), do: [existing, value] |
| 320 | + |
| 321 | + defp element_name(element) do |
| 322 | + element |> xml_element(:name) |> Atom.to_string() |
| 323 | + end |
| 324 | + |
| 325 | + defp attribute_name(attribute) do |
| 326 | + attribute |> xml_attribute(:name) |> Atom.to_string() |
| 327 | + end |
| 328 | + |
| 329 | + defp attribute_value(attribute) do |
| 330 | + attribute |> xml_attribute(:value) |> to_string() |
| 331 | + end |
| 332 | + |
| 333 | + defp text_value(text) do |
| 334 | + text |
| 335 | + |> xml_text(:value) |
| 336 | + |> to_string() |
| 337 | + |> String.replace(~r/\s+/, " ") |
| 338 | + |> String.trim() |
| 339 | + end |
| 340 | + |
255 | 341 | defp escape_html_binary(<<>>, acc), do: acc |
256 | 342 |
|
257 | 343 | defp escape_html_binary(<<"&", rest::binary>>, acc), |
|
0 commit comments