lib/archeometer/graphs/mermaid.ex

defmodule Archeometer.Graphs.Mermaid do
  @moduledoc """
    Functions for working with mermaid-JS graphs
  """

  @template """
  graph TD;
  <% mods_map =
    @xrefs
    |> Map.keys()
    |> Enum.with_index()
    |> Enum.map(fn {mod, idx} -> {mod, "id_" <> to_string(idx)} end)
    |> Enum.into(%{})
  %>

  <%= for module <- Map.keys(@xrefs) do %>
    <%= Map.get(mods_map, module) %>([<%= module %>]);
  <% end %>

  <%= for {mod, refs} <- @xrefs do %>
  <%= for ref <- refs do %>
    <%= Map.get(mods_map, mod) %>--><%= Map.get(mods_map, ref) %>;
  <% end %>
  <% end %>
  """

  @doc """
  Renders an graph represented by an adjacency list into .dot format.

  Returns a string containing de rendered graph.

  ## Examples

      iex(2)> graph = %{
      ...(2)>   :a => [:b, :c],
      ...(2)>   :b => [:c, :d],
      ...(2)>   :d => [:a]
      ...(2)> }
      %{a: [:b, :c], b: [:c, :d], d: [:a]}
      iex(3)> str = Mermaid.render(graph)
      iex(3)> assert is_binary(str)
  """
  def render(xrefs) do
    xrefs = add_independent_apps(xrefs)

    @template
    |> EEx.eval_string(assigns: [xrefs: xrefs])
    |> String.replace(~r/(\n\s+\n)+|\n{2,}/, "\n")
  end

  defp add_independent_apps(xrefs) do
    independent_apps = (Map.values(xrefs) |> List.flatten() |> Enum.uniq()) -- Map.keys(xrefs)

    Enum.reduce(independent_apps, xrefs, fn app, new_adjacency_list ->
      Map.put(new_adjacency_list, app, [])
    end)
  end
end