defmodule Mecto.MarkupParser do
@moduledoc """
Parses `"some text with [[tag.module]]"` into `[tag: [:module]]`
"""
import NimbleParsec
alias Mecto.MarkupParser.Field
whitespace = utf8_string([?\s, ?\n, ?\r, ?\t], min: 1)
opening_tag = string("[[")
closing_tag = string("]]")
tag =
ignore(opening_tag)
|> ignore(optional(whitespace))
|> lookahead_not(closing_tag)
|> tag(Field.field(), :field)
|> ignore(optional(whitespace))
|> ignore(closing_tag)
text =
lookahead_not(opening_tag)
|> utf8_string([], 1)
|> times(min: 1)
|> reduce({Enum, :join, []})
leading_whitespace =
whitespace
|> lookahead(opening_tag)
|> ignore()
defparsec(:parse, repeat(choice([tag, text, leading_whitespace])) |> eos())
@spec extract_nodes(String.t()) :: map() | {:error, String.t()}
def extract_nodes(text) do
with {:ok, nodes, _, _, _, _} <- parse(text) do
nodes
|> Enum.reject(&is_binary/1)
|> Enum.reduce(%{}, &merge_node/2)
else
{:error, _e, _, _, _, _} ->
{:error, "invalid markup"}
end
rescue
e in ArgumentError ->
if e.message =~ "1st argument: not an already existing atom" do
{:error,
"field cannot be converted to a non-existing atom. Maybe your markup has a typo?"}
else
raise e
end
end
defp merge_node({:field, path}, fields) do
atomised_path =
Enum.map(path, fn
entry when is_integer(entry) -> entry
otherwise -> String.to_existing_atom("#{otherwise}")
end)
merge_node(fields, atomised_path)
end
defp merge_node(fields, []), do: fields
defp merge_node(fields, [entry]) do
case get_node(fields, entry) do
{nil, remainder} -> Map.put(remainder, entry, 1)
_ -> Map.update!(fields, entry, &(&1 + 1))
end
end
defp merge_node(fields, [head | tail]) do
case get_node(fields, head) do
{nil, remainder} ->
Map.put(remainder, head, merge_node(%{}, tail))
{node, remainder} ->
Map.put(remainder, head, merge_node(node, tail))
end
end
defp get_node(fields, node) do
Map.pop(fields, node)
end
end