defmodule DG.Sigil do
@moduledoc """
Sigils that parse `mermaid.js` format flowchart into `DG`
iex> import DG.Sigil
...> dg = ~G"\""
...> graph LR
...> a[some label]
...> b[other label]
...> 1-->2
...> 3[three] -- three to four --> 4[four]
...> a --> b
...> "\""
iex> DG.vertex(dg, "a")
{"a", "some label"}
iex> Enum.sort(DG.vertices(dg))
["1", "2", "3", "4", "a", "b"]
iex> length(DG.edges(dg))
3
"""
use AbnfParsec,
abnf: """
wsp = %x20 / %x09
lwsp = *(wsp / LF / CRLF)
direction = "TB" / "TD" / "LR"
type = "flowchart" / "graph"
label = 1*(ALPHA / DIGIT / %x20)
square-brace-open = "["
square-brace-close = "]"
vertex-id = 1*(ALPHA / DIGIT)
vertex = vertex-id [square-brace-open label square-brace-close]
edge = vertex *wsp ("-->" / "--" label "-->") *wsp vertex
vertex-or-edge = [lwsp] (edge / vertex)
graph = lwsp type 1*wsp direction lwsp *vertex-or-edge
""",
parse: :graph,
unbox: ["vertex-or-edge", "vertex-id"],
ignore: ["wsp", "lwsp", "square-brace-open", "square-brace-close"],
unwrap: ["type", "direction", "label"],
transform: %{
"vertex-id" => {:reduce, {List, :to_string, []}},
"label" => [{:reduce, {List, :to_string, []}}, {:map, {String, :trim, []}}]
}
defp unwrap_vertex({:vertex, [v]}) do
{:vertex, v}
end
defp unwrap_vertex({:vertex, [v, label: label]}) do
{:vertex, v, label}
end
defp extract_vertex({:vertex, [v | _]}), do: v
defp extract_vertex({:vertex, v, _label}), do: v
defp extract_vertex({:vertex, v}), do: v
defp extract_edge({:edge, v1, v2, _label}), do: {v1, v2}
defp extract_edge({:edge, v1, v2}), do: {v1, v2}
@doc false
def prepare_gen(string) do
{:ok, [graph: [{:type, _type}, {:direction, direction} | content]], _, _, _, _} =
parse(string)
vertices =
content
|> Enum.flat_map(fn
{:vertex, _} = v ->
[unwrap_vertex(v)]
{:edge, [v1, "-->", v2]} ->
[unwrap_vertex(v1), unwrap_vertex(v2)]
{:edge, [v1, "--", _label, "-->", v2]} ->
[unwrap_vertex(v1), unwrap_vertex(v2)]
end)
|> Enum.uniq_by(&extract_vertex/1)
edges =
content
|> Enum.filter(fn
{:edge, _} -> true
_ -> false
end)
|> Enum.map(fn
{:edge, [v1, "-->", v2]} ->
{:edge, extract_vertex(v1), extract_vertex(v2)}
{:edge, [v1, "--", {:label, label}, "-->", v2]} ->
{:edge, extract_vertex(v1), extract_vertex(v2), label}
end)
|> Enum.uniq_by(&extract_edge/1)
{direction, vertices, edges}
end
defp gen(string) do
{direction, vertices, edges} = prepare_gen(string)
quote do
DG.new(
unquote(Macro.escape(vertices)),
unquote(Macro.escape(edges)),
direction: unquote(direction)
)
end
end
defmacro sigil_g({:<<>>, _, [string]}, _opts), do: gen(string)
defmacro sigil_g({:<<>>, _, _pieces} = string, _opts) do
quote do
import unquote(__MODULE__), only: [prepare_gen: 1]
{direction, vertices, edges} = prepare_gen(unquote(string))
DG.new(vertices, edges, direction: direction)
end
end
defmacro sigil_G({:<<>>, _, [string]}, _opts), do: gen(string)
end