Current section
Files
Jump to
Current section
Files
lib/exdot.ex
defmodule Exdot do
@type expr ::
String.t()
| [{name :: atom | String.t(), value :: keyword() | String.t()}]
| nonempty_improper_list(name :: atom | String.t(), value :: keyword() | String.t())
| (... -> expr)
@moduledoc """
Elixir abstraction generate Graphviz dot formatted string.
## Block Constructs
Block constructs takes an optional name and set of *expressions* to return dot formatted string. Each expression inside the block is converted to dot format statement.
### Valid expressions within the block
* Two element list. If second element is a list, it is treated as attr list
```
[:graph, [rankdir: "LR", labeljust: "jleft"]] # => graph [ rankdir = "LR" labeljust = "jleft" ]
["node", [shape: "plaintext"]] # => node [ shape = "plaintext" ]
[:ranksep, "2"] # => raksep = "2"
```
It can also be single pair keyword list. It is treated same as 2 element list
```
[graph: [rankdir: "LR", labeljust: "jleft"]] # => graph [ rankdir = "LR" labeljust = "jleft" ]
["node" => [shape: "plaintext"]] # => graph [ rankdir = "LR" labeljust = "jleft" ]
[ranksep: "2"] # => ranksep = "2"
```
* String. A String expression is emmitted as it is, this can used as an escape hatch to return any dot statements for which there is not special synatx, like edge.
```
"Company:emp_name -> Employee:name [ style = \\"dashed\\" ]" # => Company:emp_name -> Employee:name [ style = "dashed" ]
```
* Function call. Function must return any of the above valid values. This can be used as an abstraction to build high-level syntax or templates. See `Exdot.ERD`
```
defmodule Graph do
def edge(a, b), do: a <> " -> " <> b
end
digraph do
["Company", [label: "Company"]]
["Employee", [label: "Employee"]]
Graph.edge("Company", "Employee")
end
```
"""
for name <- [:digraph, :subgraph, :graph] do
@doc """
Returns #{name} formatted string.
See module documentation for more details
"""
@spec unquote(name)(String.t(), do_block :: [expr]) :: String.t()
defmacro unquote(name)(label \\ "", do: body) do
name = unquote(name)
quote do
fmt_block(unquote(name), unquote(label), unquote(do_block_to_list(body)))
end
end
end
@doc false
def do_block_to_list({:__block__, _meta, block}), do: block
def do_block_to_list(ast), do: [ast]
@doc false
def indent_str(body, indentation) do
String.trim(body)
|> String.split("\n")
|> Enum.join("\n" <> indentation)
end
@doc false
def indent(lines, joiner, indentation \\ " ") when is_list(lines) do
Enum.join(lines, joiner) |> indent_str(indentation)
end
defp fmt_value("<" <> _ = html_label), do: html_label
defp fmt_value(v), do: inspect(v)
defp fmt_kv(k, v), do: "#{k} = #{fmt_value(v)}"
defp fmt_attrs(attrs) do
attrs_str = Enum.map(attrs, fn {k, v} -> fmt_kv(k, v) end) |> indent(" ")
"[ #{attrs_str} ]"
end
defp fmt_body(stmt) do
case stmt do
[{name, attrs}] -> fmt_body([name, attrs])
[name, attrs] when is_list(attrs) -> to_string(name) <> " " <> fmt_attrs(attrs)
[key, value] -> fmt_kv(key, value)
[{func, _, args}] when is_atom(func) and is_list(args) -> stmt
stmt when is_binary(stmt) -> stmt
end
end
@doc false
def fmt_block(type, name, stmts) do
body = Enum.map(stmts, &fmt_body/1) |> indent("\n")
"""
#{type} #{name} {
#{body}
}
"""
end
end