├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── dotx.ex ├── dotx_decode.ex ├── dotx_encode.ex └── helpers.ex ├── mix.exs ├── mix.lock ├── src ├── dot_lexer.xrl └── dot_parser.yrl └── test ├── dotx_test.exs ├── examples ├── ex1.dot ├── ex10.dot ├── ex11.dot ├── ex12.dot ├── ex13.dot ├── ex14.dot ├── ex15.dot ├── ex2.dot ├── ex3.dot ├── ex4.dot ├── ex5.dot ├── ex6.dot ├── ex7.dot ├── ex8.dot └── ex9.dot └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | dotx-*.tar 24 | 25 | # Ignore compiled erlang from lexer and parser 26 | /src/*.erl 27 | 28 | # Ignore outputs encoded file from tests 29 | /test/examples_out 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Kbrw 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dotx 2 | 3 | Dotx is a full feature library for DOT file parsing and generation. 4 | The whole spec [https://www.graphviz.org/doc/info/lang.html](https://www.graphviz.org/doc/info/lang.html) is implemented. 5 | 6 | ## Documentation 7 | 8 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 9 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 10 | be found at [https://hexdocs.pm/dotx](https://hexdocs.pm/dotx). 11 | 12 | ## Installation 13 | 14 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 15 | by adding `khost_topo` to your list of dependencies in `mix.exs`: 16 | 17 | ```elixir 18 | def deps do 19 | [ 20 | {:dotx, "~> 0.3.0"} 21 | ] 22 | end 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```elixir 28 | graph = Dotx.decode(dot_string) 29 | # now you can use graph : see `Dotx.graph()` typespec in doc for usage 30 | 31 | dot_string = Dotx.encode(graph) 32 | dot_string = "#{graph}" 33 | # pretty print of graph in the dot format 34 | # to_string() encode the graph thanks to String.Chars protocol 35 | 36 | # You can flatten all edge shorthands of DOT : {a b}-> c -> d became 37 | # {a b} a->c b->c b->d 38 | flatgraph = Dotx.flatten(graph) 39 | 40 | # You can add a unique ID for every graph and subgraph without one to allow 41 | # easy graph property association of nodes and edges 42 | idgraph = Dotx.identify(graph) 43 | 44 | # You can Spread default attributes (`node [...]`, `graph [...]`, `edge [...]` 45 | # to all edges/graphs/nodes descendants of attribute definitions 46 | graph = Dotx.spread_attributes(graph) 47 | 48 | # You can create a node database where all shorthands of dot (attributes 49 | # inheritance, inline edges or edges between subgraph) are resolved to get a 50 | # simple usable view of your graph 51 | {nodes,graphs} = Dotx.to_nodes(graph) 52 | %{attrs: %{"edges_from"=> from_a_edges, "graph"=>graphid, "otherattr"=>attr}} = nodes[["A"]] 53 | nodea_graph = %{attrs: %{"someattr"=>attr}} = graphs[graphid] 54 | [%{attrs: %{"someattr"=>attr, "graph"=>graphid}, to: %{id: tonodeid}}|_] = from_a_edges 55 | firstnode_linked_to_a = nodes[tonodeid] 56 | 57 | # You can use an erlang `:digraph` to handle your graph and make complex graph analysis : 58 | digraph = Dotx.to_digraph(graph) 59 | vertices = :digraph.get_short_path(digraph,["A"],["B"]) 60 | ``` 61 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /lib/dotx.ex: -------------------------------------------------------------------------------- 1 | # public interface and doc main library file 2 | 3 | defmodule Dotx do 4 | @typedoc """ 5 | An `id` type is a value in DOT : either a simple string or an HTML string. 6 | The `%Dotx.HTML{}` allows you to match the latter. 7 | """ 8 | @type id :: binary | %Dotx.HTML{html: binary} 9 | @typedoc """ 10 | A `nodeid` type is a either a simple node id `["myid"]` or a node id with a port : `["myid","myport"]` 11 | """ 12 | @type nodeid :: [binary] 13 | @type graph :: graph(edge) | graph(flatedge) 14 | @typedoc """ 15 | The main structure containing all parsed info from a DOT graph : 16 | - `strict` is `true` if the strict prefix is present 17 | - `type` is `:digraph` if graph is a directed graph, `:graph` otherwise 18 | - `attrs` are the attributes of the graph itself : any key-values are allowed 19 | - `(nodes|edges|graphs)_attrs` are attributes which all subelements 20 | (respectively node, edge or subgraph) will inherited (`node [key=value]` in DOT) 21 | - `children` is the list of childs : dot, edge or subgraph 22 | """ 23 | @type graph(edgetype) :: %Dotx.Graph{ 24 | strict: boolean, 25 | type: :graph | :digraph, 26 | id: nil | id, 27 | attrs: %{optional(id) => id}, 28 | nodes_attrs: %{optional(id) => id}, 29 | edges_attrs: %{optional(id) => id}, 30 | graphs_attrs: %{optional(id) => id}, 31 | children: [dotnode | edgetype | subgraph(edgetype)] 32 | } 33 | @typedoc """ 34 | A `dotnode` is the leaf structure of the graph: only an id and its attributes as a free map. 35 | """ 36 | @type dotnode :: %Dotx.Node{id: nodeid, attrs: %{optional(id) => id}} 37 | 38 | @typedoc """ 39 | An `edge` is a link between nodes (from:,to:), it has attributes which are set by itself or inherited (see `graph()`) 40 | 41 | `to` can be another edge (`a->b->c->d`) to inline multiple edges or subgraph `{a b}->{c d}` as a shortcut to 42 | `a->c a->d b->c b->d`. You can use `Dotx.flatten/1` to expand edges and get only `flatedge()` with link between raw nodes. 43 | """ 44 | @type edge :: %Dotx.Edge{ 45 | attrs: %{optional(id) => id}, bidir: boolean, 46 | from: dotnode | subgraph(edge), to: dotnode | subgraph(edge) | edge 47 | } 48 | @typedoc "see `edge()` : an edge with raw `dotnode()`, after `Dotx.flatten` all edges are `flatedge()`" 49 | @type flatedge :: %Dotx.Edge{ 50 | attrs: %{optional(id) => id}, bidir: boolean, 51 | from: dotnode, to: dotnode 52 | } 53 | @typedoc "see `graph()` : same as graph without graph type specification" 54 | @type subgraph(edgetype) :: %Dotx.SubGraph{ 55 | id: nil | id, 56 | attrs: %{optional(id) => id}, 57 | nodes_attrs: %{optional(id) => id}, 58 | edges_attrs: %{optional(id) => id}, 59 | graphs_attrs: %{optional(id) => id}, 60 | children: [dotnode | edgetype | subgraph(edgetype)] 61 | } 62 | @moduledoc """ 63 | This library is a DOT parser and generator. 64 | Main functions are `encode/1` and `decode/1` (usable also via `to_string` 65 | and the `String.Chars` protocol). 66 | 67 | The structure of type `graph()` allows easy handling of decoding dot graph, 68 | the principle is that the structure is exactly homogeneous with a dot graph : 69 | - it contains all inherited attributes for nodes, edges and subgraphs (`*_attrs`) 70 | - it contains `attrs` of itself in addition of the `id` 71 | - it is a recursive structure containing `children`: either `subgraph` or `node` or `edge` 72 | - and edge can be `from` and `to` nodes but also subgraph (`a->{c d}`) or 73 | other edge (`a->c->d`). They are *edge shorthands* which are actually 74 | sugars to define lists of `node->node` edges. 75 | 76 | The structure is usable by itself, but subgraph tree, edge shorthands and attribute 77 | inheritance make it non trivial to handle. So to help you manage this complexity Dotx provides 78 | helper functions : 79 | - `flatten/1` create unitary edge for every DOT shortand (inline edge 80 | `a->b->c` or graph edge `{a b}->c`) so all edges are expanded to get only 81 | `node->node` edges (`a->b a->c b->c`) 82 | - `spread_attributes/1` spread default attributes from graph/subgraphs tree to 83 | all children handling inheritance of attributes, but keeping original graph structure. 84 | - `identify/1` add an identifier to all graph and subgraph without id in 85 | original graph : `xN` where `N` is the index of the subgraph in the order 86 | of appearance in the file. 87 | - `to_nodes/1` returns a flat databases of nodes and graphs containing 88 | additional special attributes to preserve the graph informations 89 | (`"graph"`,`"edges_from"`,`"parent"`,`"children"`), and where all 90 | inherited attributes are filled. 91 | - `to_edges/1` returns a flat databases of edges and graphs containing 92 | additional special attributes to preserve the graph informations 93 | (`"graph"`,`"parent"`,`"children"`) and where the `from` and `to` fields 94 | are filled with complete node structures with inherited attributes. 95 | - `to_digraph/1` returns an erlang `digraph` structure where vertices are 96 | nodes id. This allows you to use `:digraph_utils` module to do complex graph 97 | computations. 98 | """ 99 | 100 | @doc "Main lib function: same as `to_string(graph)`, encode (pretty) graph as a DOT binary string" 101 | @spec encode(graph) :: binary 102 | def encode(graph) do to_string(graph) end 103 | 104 | @doc "Main lib function: parse a DOT graph to get a `Dotx.Graph` structure" 105 | @spec decode(binary) :: {:ok,graph(edge)} | {:error,msg :: binary} 106 | defdelegate decode(bin), to: Dotx.Graph, as: :parse 107 | 108 | @doc "Same as `decode/1` but with an `BadArgument` error if DOT file is not valid" 109 | @spec decode!(binary) :: graph(edge) 110 | defdelegate decode!(bin), to: Dotx.Graph, as: :parse! 111 | 112 | @doc """ 113 | flatten all dot edge shortcuts (`a->{b c}->d` became `a->b a->c b->d c->d`), so that all `Dotx.Edge` 114 | have only `Dotx.Node` in both sides (from and to). 115 | """ 116 | @spec flatten(graph(edge)) :: graph(flatedge) 117 | defdelegate flatten(graph), to: Dotx.Helpers 118 | 119 | @doc """ 120 | Spread all inherited attributes `(nodes|edges|graphs)_attrs` or graphs to 121 | descendants `attrs` 122 | """ 123 | @spec spread_attributes(graph) :: graph 124 | defdelegate spread_attributes(graph), to: Dotx.Helpers 125 | 126 | @doc """ 127 | Give an `id` to all graph and subgraph if none are given : 128 | `{ a { b c } }` became `subgraph x0 { a subgraph x1 { b c } }` 129 | """ 130 | @spec identify(graph(edge)) :: graph(edge) 131 | defdelegate identify(graph), to: Dotx.Helpers 132 | 133 | @doc """ 134 | Returns a flat databases of nodes and graphs containing 135 | additional special attributes to preserve the graph informations 136 | (`"graph"`,`"edges_from"`,`"parent"`,`"children"`), and where all 137 | inherited attributes are filled : 138 | 139 | - `identify/1` is called to ensure every subgraph has an id 140 | - `flatten/1` is called to ensure that every unitary edges are expanded from DOT shorthands. 141 | 142 | For nodes returned : 143 | - the attrs are filled with inherited attributes from parent subgraphs `nodes_attrs` (`node [k=v]`) 144 | - `"graph"` attribute is added to each node and contains the identifier of 145 | the subgraph owning the node (the deepest subgraph containing the node in the DOT graph tree) 146 | - `"edges_from"` attribute is added to every node and contains the list of 147 | `%Dotx.Edge{}` from this node in the graph. For these edges structures : 148 | - the `"graph"` is also filled (the graph owning the edge is not 149 | necessary the one owning the nodes on both sides) 150 | - the attrs are filled with inherited attributes from parent subgraphs `edges_attrs` (`edge [k=v]`) 151 | - the `from` and `to` `%Dotx.Node` contains only `id`, attributes 152 | `attrs` are not set to avoid redundancy of data with parent nodes 153 | data. 154 | 155 | For graphs returned : 156 | - the attrs are filled with inherited attributes from parent subgraphs `graphs_attrs` (`graph [k=v]`) 157 | - the `"parent"` attribute is added containing parent graph id in the subgraph tree 158 | - the `"children"` attribute is added containing childs graph id list in the subgraph tree 159 | - the `:children` is set to empty list `[]` to only use the graph 160 | structure to get attributes and not nodes and edges already present in the 161 | nodes map returned. 162 | """ 163 | @type nodes_db :: { 164 | nodes :: %{ nodeid() => node() }, 165 | graphs :: %{ id() => graph() } 166 | } 167 | @spec to_nodes(graph) :: nodes_db 168 | defdelegate to_nodes(graph), to: Dotx.Helpers 169 | 170 | @doc """ 171 | Other variant of `to_nodes/1` : fill edges and nodes with all inherited 172 | attributes and also with a `"graph"` attribute. But instead of returning 173 | nodes with edges filled in attribute `edges_from`, it returns the list of all edges 174 | where all nodes in `from` and `to` are fully filled `%Dotx.Node{}` structures. 175 | 176 | - The function actually call `to_nodes/1` so you can put `to_nodes/1` result as parameter 177 | to avoid doing heavy computation 2 times. 178 | - all rules for graphs, nodes and edges fullfilment are the same as `to_nodes/1` 179 | """ 180 | @spec to_edges(graph | nodes_db) :: {edges :: [flatedge()], graphs :: %{ id() => graph() }} 181 | defdelegate to_edges(graph_or_nodesdb), to: Dotx.Helpers 182 | 183 | @doc """ 184 | Create an erlang `:digraph` structure from graph (see [erlang doc](http://erlang.org/doc/man/digraph.html)) 185 | where vertices are `nodeid()`. 186 | This allows to easily use `:digraph` and `:digraph_utils` handlers to go 187 | through the graph and make complex analysis of the graph. 188 | """ 189 | @spec to_digraph(graph) :: :digraph.graph() 190 | defdelegate to_digraph(graph), to: Dotx.Helpers 191 | end 192 | -------------------------------------------------------------------------------- /lib/dotx_decode.ex: -------------------------------------------------------------------------------- 1 | # DOT file parsing is done with leex and yecc `src/dot_(lexer.xrl|parser.yrl)` 2 | # This file contains all additional code for parsing 3 | # - entry point Dotx.Graph.parse which precompute the file before leex and yecc 4 | # - different structs models used by the yecc parser 5 | # - additional functions used by `dot_parser` in order to flatten the dot structure to make it more usable 6 | 7 | defmodule Dotx.Graph do 8 | defstruct strict: false, type: :digraph, id: nil, children: [], attrs: %{}, nodes_attrs: %{}, graphs_attrs: %{}, edges_attrs: %{} 9 | 10 | def parse(bin) do 11 | # tricks : 12 | # - since recursive regex is not implemented in leex lexer, replace 13 | # <> by \x1e (record separator control char) to make it easy for leex to tokenize html "id" 14 | # - since ungreedy regex are not implemented in leex lexer, remove multiline c comment 15 | bin = 16 | bin 17 | |> String.replace(~r"<(([^<>]|(?R))*)>", "\x1e\\1\x1e") 18 | |> String.replace(~r"/\*.*\*/"sU, "") 19 | case :dot_lexer.string(to_charlist(bin)) do 20 | {:ok, tokens, _} -> 21 | case :dot_parser.parse(tokens) do 22 | {:ok, tree} -> {:ok,tree} 23 | {:error,{line,_,msg}} -> 24 | {:error,"line #{line}: #{:dot_parser.format_error(msg)}"} 25 | end 26 | {:error,{line,_,msg},_} -> {:error,"line #{line}: #{:dot_lexer.format_error(msg)}"} 27 | end 28 | end 29 | 30 | def parse!(bin) do 31 | case parse(bin) do 32 | {:ok,graph}->graph 33 | {:error,msg}-> 34 | raise ArgumentError, "cannot parse DOT : #{msg}" 35 | end 36 | end 37 | 38 | def childattrs2fields(%{children: children} = graph) do 39 | graph = 40 | Enum.reduce(children, %{graph | children: []}, fn 41 | {:graph, attrs}, graph -> %{graph | graphs_attrs: Enum.into(attrs, graph.graphs_attrs)} 42 | {:edge, attrs}, graph -> %{graph | edges_attrs: Enum.into(attrs, graph.edges_attrs)} 43 | {:node, attrs}, graph -> %{graph | nodes_attrs: Enum.into(attrs, graph.nodes_attrs)} 44 | {key, val}, graph -> %{graph | attrs: Map.put(graph.attrs, key, val)} 45 | node_edge_subgraph, graph -> %{graph | children: [node_edge_subgraph | graph.children]} 46 | end) 47 | %{graph | children: Enum.reverse(graph.children)} 48 | end 49 | end 50 | 51 | defmodule Dotx.SubGraph do 52 | defstruct id: nil, children: [], attrs: %{}, nodes_attrs: %{}, graphs_attrs: %{}, edges_attrs: %{} 53 | end 54 | 55 | defmodule Dotx.Node do 56 | defstruct id: [], attrs: %{} 57 | end 58 | 59 | defmodule Dotx.Edge do 60 | defstruct from: [], to: [], attrs: %{}, bidir: true 61 | 62 | # flatten %Edge{to: %Edge{} | %SubGraph{}}-> [%Edge{from: %Node{},to: %Node{}}] 63 | def flatten(edge) do do_flatten(edge,false) end 64 | 65 | # do_flatten: a->b->c becomes a->b b->c 66 | def do_flatten(%__MODULE__{from: from,to: %__MODULE__{from: to} = toedge, attrs: attrs} = edge,nested?) do 67 | do_flatten2(edge,from,to,nested?) ++ do_flatten(%{toedge | attrs: attrs}, true) 68 | end 69 | def do_flatten(%__MODULE__{from: from, to: to} = edge, nested?) do 70 | do_flatten2(edge,from,to,nested?) 71 | end 72 | 73 | # do_flatten2: {a b}->{c d} becomes {a b} {c d} a->c a->d b->c b->d 74 | def do_flatten2(edge,from,to,nested?) do 75 | case {from,to} do 76 | {%Dotx.Node{},%Dotx.Node{}}-> [%{edge| from: from, to: to}] 77 | {%Dotx.SubGraph{}=g,%Dotx.Node{}}-> 78 | if nested? do [] else [g] end ++ 79 | for %Dotx.Node{}=from<-g.children do %{edge|from: from} end 80 | {%Dotx.Node{},%Dotx.SubGraph{}=g}-> 81 | [g| for %Dotx.Node{}=to<-g.children do %{edge|to: to} end] 82 | {%Dotx.SubGraph{}=gfrom,%Dotx.SubGraph{}=gto}-> 83 | if nested? do [] else [gfrom] end ++ [gto] ++ 84 | for %Dotx.Node{}=from<-gfrom.children, 85 | %Dotx.Node{}=to<-gto.children do %{edge|from: from, to: to} end 86 | end 87 | end 88 | end 89 | 90 | defmodule Dotx.HTML do 91 | defstruct html: "" 92 | 93 | def trim(%{html: html} = doc) do 94 | # if html, can be multiline... so remove any existing indentation 95 | html = String.trim_trailing(html) 96 | html = case String.split(html, "\n", trim: true) do 97 | [_] -> html # single line 98 | lines -> 99 | case :binary.longest_common_prefix(lines) do 100 | 0 -> html # multi line, but no common prefix 101 | bytes -> 102 | <> <> _ = html 103 | case Regex.run(~r"^\s+$", prefix) do 104 | nil -> html # common prefix is not blank 105 | # there is a common prefix for all lines containing only blank chars : remove them 106 | _ -> 107 | Enum.map_join(lines, "\n", fn <<_::binary-size(bytes)>> <> rest -> rest; o -> o end) 108 | end 109 | end 110 | end 111 | %{doc | html: html} 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/dotx_encode.ex: -------------------------------------------------------------------------------- 1 | # This file contains all code for Dotx.Graph encoding to DOT file format 2 | 3 | defimpl String.Chars, for: Dotx.Graph do 4 | def indent(str, n) do 5 | str |> String.split("\n") |> Enum.map_join("\n", &"#{List.duplicate(" ", n)}#{&1}") 6 | end 7 | 8 | def format_id(%Dotx.HTML{html: html}) do 9 | # an "id" is either HTML, or alphanumeric str or quoted str 10 | if String.contains?(html, "\n") do 11 | "<\n#{indent(html, 2)}\n>" 12 | else 13 | "<" <> html <> ">" 14 | end 15 | end 16 | 17 | def format_id(str) when is_binary(str) do 18 | case Regex.run(~r"^\w+$"u, str) do 19 | nil -> "\"#{String.replace(str, "\"", "\\\"")}\"" 20 | _ -> str 21 | end 22 | end 23 | 24 | def format_id(_) do "nottext" end 25 | 26 | def to_string(graph) do 27 | prefix = "#{if graph.strict, do: "strict "}#{graph.type}#{if graph.id, do: " " <> format_id(graph.id)}" 28 | parts = graph_parts(graph) 29 | parts_flat = Enum.map(parts, fn l when is_list(l) -> Enum.join(l, "\n"); s -> s end) 30 | "#{prefix} {\n\n#{parts_flat |> Enum.join("\n\n") |> indent(2)}\n\n}" 31 | end 32 | 33 | def id_width(str) do 34 | str |> String.split("\n", parts: 2) |> Enum.at(0) |> byte_size() 35 | end 36 | 37 | def format_attrs(attrs) when map_size(attrs) == 0 do "" end 38 | def format_attrs(attrs) do 39 | lines = attrs |> Enum.reduce([], fn {key, val}, acc -> 40 | key = format_id(key); val = format_id(val) 41 | case acc do 42 | [lastline | rest] = acc -> 43 | if byte_size(lastline) + id_width(key) + id_width(val) > 130 do 44 | ["#{key}=#{val}" | acc] 45 | else 46 | ["#{lastline} #{key}=#{val}" | rest] 47 | end 48 | [] -> 49 | ["#{key}=#{val}"] 50 | end 51 | end) |> Enum.reverse() 52 | 53 | if length(lines) == 1 do 54 | "[#{hd(lines)}]" 55 | else 56 | "[\n#{lines |> Enum.join("\n") |> indent(2)}\n]" 57 | end 58 | end 59 | 60 | def format_child(%Dotx.Edge{attrs: attrs, from: from, to: to, bidir: bidir}) do 61 | "#{format_child(from)} #{if bidir do "--" else "->" end} #{format_child(to)}" <> 62 | "#{if map_size(attrs) > 0 do " " <> format_attrs(attrs) end}" 63 | end 64 | 65 | def format_child(%Dotx.Node{id: id, attrs: attrs}) do 66 | id = Enum.map_join(id, ":", &format_id/1) 67 | "#{id}#{if map_size(attrs) > 0 do " " <> format_attrs(attrs) end}" 68 | end 69 | 70 | def format_child(%Dotx.SubGraph{} = graph) do 71 | parts = graph_parts(graph) 72 | prefix = if graph.id do "subgraph #{format_id(graph.id)} " end 73 | flat_parts = List.flatten(parts) 74 | if not Enum.all?(graph.children, &match?(%Dotx.Node{}, &1)) or 75 | Enum.any?(flat_parts, &String.contains?(&1, "\n")) or 76 | Enum.sum(Enum.map(flat_parts, &(byte_size(&1) + 1))) > 30 do 77 | # multiline subgraph (either not containing node only, or subpart is already multiline or too long 78 | parts_flat = Enum.map(parts, fn l when is_list(l) -> Enum.join(l, "\n"); s -> s end) 79 | "#{prefix}{\n#{parts_flat |> Enum.join("\n\n") |> indent(2)}\n}" 80 | else 81 | "#{prefix}{ #{List.flatten(flat_parts) |> Enum.join(" ")} }" 82 | end 83 | end 84 | 85 | def graph_parts(graph) do 86 | parts = [ 87 | for {k, v} <- graph.attrs do 88 | "#{format_id(k)}=#{format_id(v)}" 89 | end, 90 | [ 91 | if map_size(graph.nodes_attrs) > 0 do 92 | "node #{format_attrs(graph.nodes_attrs)}" 93 | end, 94 | if map_size(graph.edges_attrs) > 0 do 95 | "edge #{format_attrs(graph.edges_attrs)}" 96 | end, 97 | if map_size(graph.graphs_attrs) > 0 do 98 | "graph #{format_attrs(graph.graphs_attrs)}" 99 | end 100 | ] |> Enum.reject(&is_nil/1) 101 | ] ++ for children <- Enum.chunk_by(graph.children, & &1.__struct__) do 102 | for child <- children do 103 | format_child(child) 104 | end 105 | end 106 | Enum.reject(parts, &(length(&1) == 0)) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Dotx.Helpers do 2 | # recursive Edge.flatten 3 | def flatten(%Dotx.Graph{children: children}=graph) do 4 | %{graph|children: Enum.flat_map(children,&flatten(&1))} 5 | end 6 | def flatten(%Dotx.SubGraph{children: children}=graph) do 7 | [%{graph|children: Enum.flat_map(children,&flatten(&1))}] 8 | end 9 | def flatten(%Dotx.Edge{}=e) do Dotx.Edge.flatten(e) end 10 | def flatten(other) do [other] end 11 | 12 | def spread_attributes(graph) do 13 | %{graph|children: Enum.map(graph.children,&spread_attributes(&1,graph))} 14 | end 15 | def spread_attributes(%Dotx.SubGraph{children: children}=e,graph) do 16 | graph = %{graph|nodes_attrs: Map.merge(graph.nodes_attrs,e.nodes_attrs), 17 | edges_attrs: Map.merge(graph.edges_attrs,e.edges_attrs), 18 | graphs_attrs: Map.merge(graph.graphs_attrs,e.graphs_attrs)} 19 | %{e|attrs: Map.merge(graph.graphs_attrs,e.attrs), 20 | children: Enum.map(children,&spread_attributes(&1,graph))} 21 | end 22 | def spread_attributes(%Dotx.Edge{attrs: attrs,from: from, to: to}=e,graph) do 23 | %{e|attrs: Map.merge(graph.edges_attrs,attrs), 24 | from: spread_attributes(from,graph), to: spread_attributes(to,graph)} 25 | end 26 | def spread_attributes(%Dotx.Node{attrs: attrs}=e,graph) do 27 | %{e|attrs: Map.merge(graph.nodes_attrs,attrs)} 28 | end 29 | 30 | def identify(graph) do {g,_} = identify(graph,0); g end 31 | def identify(%{id: id, children: children}=graph,i) do 32 | {graph,i} = case id do nil-> {%{graph| id: id || "x#{i}"},i+1}; _-> {graph,i} end 33 | {backchildren,i} = Enum.reduce(children,{[],i},fn e, {acc,i}-> 34 | {e,i} = identify(e,i) 35 | {[e|acc],i} 36 | end) 37 | {%{graph|children: Enum.reverse(backchildren)},i} 38 | end 39 | def identify(%Dotx.Edge{}=e,i) do 40 | {from,i} = identify(e.from,i) 41 | {to,i} = identify(e.to ,i) 42 | {%{e|from: from, to: to},i} 43 | end 44 | def identify(o,i) do {o,i} end 45 | 46 | def to_nodes(graph) do 47 | graph = graph |> identify() |> flatten() 48 | res = to_nodes(graph,%{nodes: %{},graphs: %{},parent_graph: {nil,0}, 49 | nodes_attrs: %{}, edges_attrs: %{}, graphs_attrs: %{}}) 50 | nodes = Enum.into(res.nodes,%{},fn {k,n}-> 51 | attrs = %{n.attrs|"graph"=> elem(n.attrs["graph"],0)} # remove graph depth 52 | attrs = Map.put_new(attrs,"edges_from",[]) # if no edges, add attribute edges_from to empty 53 | {k,%{n|attrs: attrs}} 54 | end) 55 | {nodes,res.graphs} 56 | end 57 | def to_nodes(%{children: children}=g,%{parent_graph: {parent,depth}}=acc) do 58 | ## put graph to db: add parent and children attributes, inherit attributes 59 | g = %{g|children: [],attrs: Enum.into([ 60 | {"parent",parent}, 61 | {"children",for %Dotx.SubGraph{id: childid}<-children do childid end} 62 | ],Map.merge(acc.graphs_attrs,g.attrs))} 63 | acc = %{acc|graphs: Map.put(acc.graphs,g.id,g)} 64 | 65 | nodes_attrs = Map.merge(acc.nodes_attrs,g.nodes_attrs) 66 | edges_attrs = Map.merge(acc.edges_attrs,g.edges_attrs) 67 | graphs_attrs = Map.merge(acc.graphs_attrs,g.graphs_attrs) 68 | Enum.reduce(children,acc,fn e,acc-> 69 | to_nodes(e,%{acc|nodes_attrs: nodes_attrs, edges_attrs: edges_attrs, graphs_attrs: graphs_attrs, 70 | parent_graph: {g.id,depth+1}}) 71 | end) 72 | end 73 | def to_nodes(%Dotx.Edge{from: %Dotx.Node{}=from, to: %Dotx.Node{}=to}=e,%{parent_graph: {parent,_}}=acc) do 74 | e = %{e|attrs: acc.edges_attrs |> Map.merge(e.attrs) |> Map.put("graph",parent)} 75 | acc = to_nodes(to,to_nodes(from,acc)) 76 | %{acc|nodes: Map.update!(acc.nodes,from.id,fn oldn-> 77 | %{oldn|attrs: Map.update(oldn.attrs,"edges_from",[e],&[e|&1])} 78 | end)} 79 | end 80 | def to_nodes(%Dotx.Node{}=n,%{parent_graph: {_,parentdepth}=pgraph}=acc) do 81 | n = %{n|attrs: Map.merge(acc.nodes_attrs,n.attrs)} 82 | default_n = %{n|attrs: Map.put(n.attrs,"graph",pgraph)} 83 | %{acc|nodes: Map.update(acc.nodes,n.id,default_n,fn oldn-> 84 | n = %{oldn|attrs: Map.merge(oldn.attrs,n.attrs)} 85 | %{n|attrs: Map.update(n.attrs,"graph",acc.parent_graph, fn {_,depth}=graph-> 86 | if parentdepth > depth do pgraph else graph end 87 | end)} 88 | end)} 89 | end 90 | 91 | def to_edges(%Dotx.Graph{}=g) do g |> to_nodes() |> to_edges() end 92 | def to_edges({nodes,graphs}) do 93 | edges = Enum.flat_map(nodes,fn {_,%Dotx.Node{attrs: attrs}}-> 94 | Enum.map(attrs["edges_from"],fn %{from: from, to: to}=e-> 95 | %{e|from: del_edges_from(nodes[from.id]), to: del_edges_from(nodes[to.id])} 96 | end) 97 | end) 98 | {edges,graphs} 99 | end 100 | def del_edges_from(n) do %{n|attrs: Map.delete(n.attrs,"edges_from")} end 101 | 102 | def to_digraph(%Dotx.Graph{}=dot) do g=:digraph.new ; fill_digraph(g,flatten(dot)); g end 103 | def fill_digraph(g,%{children: children}) do for e<-children do fill_digraph(g,e) end end 104 | def fill_digraph(g,%Dotx.Node{id: id}) do :digraph.add_vertex(g,id) end 105 | def fill_digraph(g,%Dotx.Edge{from: %Dotx.Node{id: fromid}, to: %Dotx.Node{id: toid}}) do 106 | :digraph.add_vertex(g,fromid) 107 | :digraph.add_vertex(g,toid) 108 | :digraph.add_edge(g,fromid,toid) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dotx.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dotx, 7 | version: "0.3.1", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | name: "Dotx Elixir dot parser", 11 | package: [ 12 | description: """ 13 | Dotx is a full feature library for DOT file parsing and generation. 14 | The whole spec [https://www.graphviz.org/doc/info/lang.html](https://www.graphviz.org/doc/info/lang.html) is implemented. 15 | """, 16 | links: %{repo: "https://github.com/kbrw/dotx", doc: "https://hexdocs.pm/dotx"}, 17 | files: ["lib", "mix.exs", "README*", "LICENSE*", "src/dot_lexer.xrl","src/dot_parser.yrl"], 18 | licenses: ["MIT"], 19 | ], 20 | source_url: "https://github.com/kbrw/dotx", 21 | docs: [main: "Dotx"], 22 | deps: [ 23 | {:ex_doc, "~> 0.19", only: :dev, runtime: false}, 24 | ] 25 | ] 26 | end 27 | 28 | def application do 29 | [ 30 | extra_applications: [:logger] 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 7 | } 8 | -------------------------------------------------------------------------------- /src/dot_lexer.xrl: -------------------------------------------------------------------------------- 1 | 2 | Definitions. 3 | 4 | A = [Aa] 5 | B = [Bb] 6 | C = [Cc] 7 | D = [Dd] 8 | E = [Ee] 9 | G = [Gg] 10 | H = [Hh] 11 | I = [Ii] 12 | N = [Nn] 13 | O = [Oo] 14 | P = [Pp] 15 | R = [Rr] 16 | S = [Ss] 17 | T = [Tt] 18 | U = [Uu] 19 | 20 | DoubleSlashComment = (//[^\r\n]*\r?\n)+ 21 | SharpComment = (#[^\r\n]*\r?\n)+ 22 | Blank = [\000-\040] 23 | 24 | HTML = (\x1e[^\x1e]*\x1e) 25 | AlNum = ([a-zA-Z\200-\377_][a-zA-Z\200-\377_0-9]*) 26 | Numeral = (-?(\.[0-9]+|[0-9]+(\.[0-9]*)?)) 27 | Quoted = ("(\\([^\\]|\\)|[^\\""])+") 28 | 29 | % edgeops 30 | DiOp = (->) 31 | UnDiOp = (--) 32 | 33 | Rules. 34 | %% Note: rule order matters. 35 | 36 | {S}{T}{R}{I}{C}{T} : {token,{'strict',TokenLine}}. 37 | {G}{R}{A}{P}{H} : {token,{'graph',TokenLine}}. 38 | {D}{I}{G}{R}{A}{P}{H} : {token,{'digraph',TokenLine}}. 39 | 40 | {N}{O}{D}{E} : {token,{'node',TokenLine}}. 41 | {E}{D}{G}{E} : {token,{'edge',TokenLine}}. 42 | 43 | {S}{U}{B}{G}{R}{A}{P}{H} : {token,{'subgraph',TokenLine}}. 44 | 45 | {DiOp} : {token,{'->',TokenLine}}. 46 | {UnDiOp} : {token,{'--',TokenLine}}. 47 | 48 | \; : {token,{';',TokenLine}}. 49 | \, : {token,{',',TokenLine}}. 50 | \: : {token,{':',TokenLine}}. 51 | \= : {token,{'=',TokenLine}}. 52 | 53 | \{ : {token,{'{',TokenLine}}. 54 | \[ : {token,{'[',TokenLine}}. 55 | \] : {token,{']',TokenLine}}. 56 | \} : {token,{'}',TokenLine}}. 57 | 58 | {AlNum} : {token,{id,TokenLine,list_to_binary(TokenChars)}}. 59 | {Numeral} : {token,{id,TokenLine,list_to_binary(TokenChars)}}. 60 | {Quoted} : {token,{id,TokenLine,unquote(list_to_binary(TokenChars))}}. 61 | {HTML} : {token,{id,TokenLine,unhtml(list_to_binary(TokenChars))}}. 62 | 63 | % Declared after ids to enable embedded comments there. 64 | {DoubleSlashComment} : skip_token. 65 | {SharpComment} : skip_token. 66 | {Blank} : skip_token. 67 | 68 | Erlang code. 69 | 70 | unquote (<<"\"\"">>) -> 71 | <<"">>; 72 | unquote (Quoted) -> 73 | Size = size(Quoted) - 2, 74 | <<$", Unquoted:Size/binary, $">> = Quoted, 75 | binary:replace(Unquoted, <<"\\\"">>, <<"\"">>, [global]). 76 | 77 | unhtml (Html) -> 78 | Size = size(Html) - 2, 79 | <<30,UnHtml:Size/binary,30>> = Html, 80 | 'Elixir.Dotx.HTML':trim('Elixir.Dotx.HTML':'__struct__'([{html,UnHtml}])). 81 | 82 | %% End of Lexer. 83 | -------------------------------------------------------------------------------- /src/dot_parser.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals 2 | Graph GraphTy Strict 3 | StmtList Stmt NodeStmt EdgeStmt AttrStmt Equality Subgraph 4 | AttrList AList NodeId EdgeRHS EdgeOp 5 | . 6 | 7 | Terminals 8 | 'strict' 'graph' 'digraph' 'node' 'edge' 'subgraph' 9 | ';' ',' ':' '=' '{' '[' ']' '}' 10 | id 11 | '--' '->' 12 | . 13 | 14 | Rootsymbol Graph. 15 | 16 | Graph -> Strict GraphTy '{' StmtList '}' : 'Elixir.Dotx.Graph':childattrs2fields('Elixir.Dotx.Graph':'__struct__'([{strict,'$1'},{type,'$2'},{children,'$4'}])). 17 | Graph -> Strict GraphTy id '{' StmtList '}' : 'Elixir.Dotx.Graph':childattrs2fields('Elixir.Dotx.Graph':'__struct__'([{strict,'$1'},{type,'$2'},{id,element(3,'$3')},{children,'$5'}])). 18 | GraphTy -> 'graph' : element(1,'$1'). 19 | GraphTy -> 'digraph' : element(1,'$1'). 20 | Strict -> '$empty' : false. 21 | Strict -> 'strict' : true. 22 | 23 | StmtList -> Stmt : ['$1']. 24 | StmtList -> Stmt StmtList : ['$1'|'$2']. 25 | StmtList -> Stmt ';' : ['$1']. 26 | StmtList -> Stmt ';' StmtList : ['$1'|'$3']. 27 | % not in spec but handled by graphvis : comma separated statements 28 | StmtList -> Stmt ',' : ['$1']. 29 | StmtList -> Stmt ',' StmtList : ['$1'|'$3']. 30 | 31 | Stmt -> NodeStmt : '$1'. 32 | Stmt -> EdgeStmt : '$1'. 33 | Stmt -> AttrStmt : '$1'. 34 | Stmt -> Equality : '$1'. 35 | Stmt -> Subgraph : '$1'. 36 | 37 | Equality -> id '=' id : {element(3,'$1'),element(3,'$3')}. 38 | 39 | AttrStmt -> 'graph' AttrList : {element(1,'$1'),'$2'}. 40 | AttrStmt -> 'node' AttrList : {element(1,'$1'),'$2'}. 41 | AttrStmt -> 'edge' AttrList : {element(1,'$1'),'$2'}. 42 | 43 | AttrList -> '[' ']' : #{}. 44 | AttrList -> '[' AList ']' : 'Elixir.Map':new('$2'). 45 | AttrList -> '[' ']' AttrList : 'Elixir.Map':new('$3'). 46 | AttrList -> '[' AList ']' AttrList : 'Elixir.Enum':into(['$2'],'Elixir.Map':new('$4')). 47 | 48 | AList -> Equality : ['$1']. 49 | AList -> Equality AList : ['$1'|'$2']. 50 | AList -> Equality ',' : ['$1']. 51 | AList -> Equality ',' AList : ['$1'|'$3']. 52 | 53 | EdgeStmt -> NodeId EdgeOp EdgeRHS : 'Elixir.Dotx.Edge':'__struct__'([{from,'Elixir.Dotx.Node':'__struct__'([{id,'$1'}])},{bidir,'$2'},{to,'$3'}]). 54 | EdgeStmt -> NodeId EdgeOp EdgeRHS AttrList : 'Elixir.Dotx.Edge':'__struct__'([{from,'Elixir.Dotx.Node':'__struct__'([{id,'$1'}])},{bidir,'$2'},{to,'$3'},{attrs,'$4'}]). 55 | EdgeStmt -> Subgraph EdgeOp EdgeRHS : 'Elixir.Dotx.Edge':'__struct__'([{from,'$1'},{bidir,'$2'},{to,'$3'}]). 56 | EdgeStmt -> Subgraph EdgeOp EdgeRHS AttrList : 'Elixir.Dotx.Edge':'__struct__'([{from,'$1'},{bidir,'$2'},{to,'$3'},{attrs,'$4'}]). 57 | 58 | %% Missing here that EdgeRHS must be another edge RHS handling edgeop direction 59 | EdgeRHS -> NodeId : 'Elixir.Dotx.Node':'__struct__'([{id,'$1'}]). 60 | EdgeRHS -> NodeId EdgeOp EdgeRHS : 'Elixir.Dotx.Edge':'__struct__'([{from,'Elixir.Dotx.Node':'__struct__'([{id,'$1'}])},{bidir,'$2'},{to,'$3'}]). 61 | EdgeRHS -> Subgraph : '$1'. 62 | EdgeRHS -> Subgraph EdgeOp EdgeRHS : 'Elixir.Dotx.Edge':'__struct__'([{from,'$1'},{bidir,'$2'},{to,'$3'}]). 63 | EdgeOp -> '--' : true. 64 | EdgeOp -> '->' : false. 65 | 66 | NodeStmt -> NodeId : 'Elixir.Dotx.Node':'__struct__'([{id,'$1'}]). 67 | NodeStmt -> NodeId AttrList : 'Elixir.Dotx.Node':'__struct__'([{id,'$1'},{attrs,'$2'}]). 68 | 69 | 70 | 71 | NodeId -> id : [element(3,'$1')]. 72 | NodeId -> id ':' id : [element(3,'$1'),element(3,'$3')]. 73 | NodeId -> id ':' id ':' id : [element(3,'$1'),element(3,'$3'),element(3,'$5')]. 74 | 75 | Subgraph -> '{' StmtList '}' : 'Elixir.Dotx.Graph':childattrs2fields('Elixir.Dotx.SubGraph':'__struct__'([{children,'$2'}])). 76 | Subgraph -> 'subgraph' id '{' StmtList '}' : 'Elixir.Dotx.Graph':childattrs2fields('Elixir.Dotx.SubGraph':'__struct__'([{id,element(3,'$2')},{children,'$4'}])). 77 | 78 | %% Number of shift/reduce conflicts 79 | Expect 2. 80 | 81 | Erlang code. 82 | 83 | 84 | %% End of Parser. 85 | -------------------------------------------------------------------------------- /test/dotx_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DotxTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | File.mkdir("test/examples_out") 6 | :ok 7 | end 8 | 9 | for dot_path <- Path.wildcard("test/examples/*.dot") do 10 | test "check parsing/encoding bijection for #{Path.basename(dot_path)}" do 11 | parsed = Dotx.decode!(File.read!(unquote(dot_path))) 12 | # same as to_string(parsed) or Dotx.encode(parsed) 13 | dot = "#{parsed}" 14 | File.write!("test/examples_out/#{Path.basename(unquote(dot_path))}", dot) 15 | assert parsed == Dotx.decode!(dot) 16 | end 17 | end 18 | 19 | test "test parse errors" do 20 | # lexer 21 | assert_raise ArgumentError, ~r/illegal characters/, fn -> 22 | Dotx.decode!("digraph €") 23 | end 24 | # parser 25 | assert_raise ArgumentError, ~r/syntax error/, fn -> 26 | Dotx.decode!("digraph { -> }") 27 | end 28 | end 29 | 30 | test "test flatten" do 31 | graph = Dotx.flatten(Dotx.decode!( 32 | "digraph D { A -> { B C D } -> { F D } }")) 33 | edges = for %Dotx.Edge{from: %{id: [from]},to: %{id: [to]}}<-graph.children do 34 | [from,to] 35 | end 36 | assert edges == [["A","B"],["A","C"],["A","D"], 37 | ["B","F"],["B","D"],["C","F"], 38 | ["C","D"],["D","F"],["D","D"]] 39 | end 40 | 41 | test "test identify" do 42 | graph = Dotx.identify(Dotx.decode!( 43 | "digraph { A -> { B C D } -> { F D } }")) 44 | dot = "#{graph}" 45 | assert String.contains?(dot, "digraph x0") 46 | assert String.contains?(dot, "subgraph x1") 47 | assert String.contains?(dot, "subgraph x2") 48 | end 49 | 50 | test "test to_nodes data conversion" do 51 | g = """ 52 | digraph { 53 | ga = 1 54 | graph [gb = 2] 55 | node [z=z] 56 | edge [ea = 1] 57 | A [a=1] 58 | { 59 | { 60 | node [b=2] 61 | { gc = 3 B [d=3] } 62 | B -> A -> D [eb = 2] 63 | } 64 | } 65 | C 66 | } 67 | """ 68 | {nodes,graphs} = Dotx.to_nodes(Dotx.decode!(g)) 69 | assert %{attrs: %{"graph"=>"x2","a"=>"1","b"=>"2","z"=>"z", "edges_from"=> [ 70 | %{to: %{id: ["D"]},attrs: %{"graph"=> "x2", "ea"=>"1", "eb"=>"2"}} 71 | ]}} = nodes[["A"]] 72 | assert %{attrs: %{"graph"=>"x3","b"=>"2","d"=>"3","z"=>"z", "edges_from"=> [ 73 | %{to: %{id: ["A"]},attrs: %{"graph"=> "x2", "ea"=>"1", "eb"=>"2"}} 74 | ]}} = nodes[["B"]] 75 | assert %{attrs: %{"graph"=>"x0","z"=>"z"}} = 76 | nodes[["C"]] 77 | assert %{attrs: %{"gc"=>"3", "gb"=>"2","parent"=>"x2"}} = graphs["x3"] 78 | end 79 | 80 | test "test to_edges data conversion" do 81 | g = """ 82 | digraph { 83 | ga = 1 84 | graph [gb = 2] 85 | node [z=z] 86 | edge [ea = 1] 87 | A [a=1] 88 | { 89 | { 90 | node [b=2] 91 | { gc = 3 B [d=3] } 92 | B -> A -> D [eb = 2] 93 | } 94 | } 95 | C 96 | } 97 | """ 98 | {edges,_graphs} = Dotx.to_edges(Dotx.decode!(g)) 99 | edges = Enum.sort_by(edges,& {&1.from.id,&1.to.id}) 100 | assert [ 101 | %{from: %{ attrs: %{"b"=> "2","graph"=>"x2"}, id: ["A"]}, to: %{ id: ["D"]}}, 102 | %{from: %{ attrs: %{"d"=> "3","graph"=>"x3"}, id: ["B"]}, to: %{ id: ["A"]}, attrs: %{"graph"=>"x2"}} 103 | ] = edges 104 | end 105 | 106 | test "test digraph" do 107 | g = """ 108 | digraph { 109 | a e f 110 | a->b 111 | a->b->{c d}->{e f}->g 112 | { c->a f->b } 113 | } 114 | """ 115 | g = Dotx.to_digraph(Dotx.decode!(g)) 116 | assert ["a", "b", "c", "d", "e", "f", "g"] == 117 | Enum.sort(for [x]<-:digraph.vertices(g) do x end) 118 | assert Enum.sort(for e<-:digraph.edges(g) do {_,[f],[t],_} = :digraph.edge(g,e); {f,t} end) == 119 | [{"a","b"},{"a","b"}, 120 | {"b","c"},{"b","d"}, 121 | {"c","a"},{"c","e"},{"c","f"}, 122 | {"d","e"},{"d","f"}, 123 | {"e","g"}, 124 | {"f","b"},{"f","g"}] 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/examples/ex1.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | A [shape=diamond] 4 | B [shape=box] 5 | C [shape=circle] 6 | 7 | A -> B [style=dashed, color=grey] 8 | A -> C [color="black:invis:black"] 9 | A -> D [penwidth=5, arrowhead=none] 10 | 11 | } 12 | -------------------------------------------------------------------------------- /test/examples/ex10.dot: -------------------------------------------------------------------------------- 1 | digraph H { 2 | 3 | aHtmlTable [ 4 | shape=plaintext 5 | color=blue // The color of the border of the table 6 | label=< 7 | 8 | 9 | 10 |
col 1foo
COL 2bar
11 | 12 | >]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /test/examples/ex11.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | 3 | tbl [ 4 | 5 | shape=plaintext 6 | label=< 7 | 8 | 9 | 10 | 17 | 23 | 24 | 25 | 26 | 27 |
foobarbaz
11 | 12 | 13 | 14 | 15 |
one two three
four five six
seveneightnine
16 |
18 | 19 | 20 | 21 |
einszweidrei
sechs
vierfun
22 |
abc
28 | 29 | >]; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/examples/ex12.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | node [shape=plaintext] 4 | 5 | some_node [ 6 | label=< 7 | 8 | 9 | 10 | 11 |
Foo
Bar
Baz
> 12 | ]; 13 | 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/examples/ex13.dot: -------------------------------------------------------------------------------- 1 | digraph H { 2 | 3 | aHtmlTable [ 4 | shape=plaintext 5 | label=< 6 | 7 | 8 | 9 | 10 |
col 1foo
COL 2bar
11 | 12 | >]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /test/examples/ex14.dot: -------------------------------------------------------------------------------- 1 | digraph H { 2 | 3 | parent [ 4 | shape=plaintext 5 | label=< 6 | 7 | 8 | 9 |
The foo, the bar and the baz
First portSecond portThird port
10 | >]; 11 | 12 | child_one [ 13 | shape=plaintext 14 | label=< 15 | 16 | 17 |
1
18 | >]; 19 | 20 | child_two [ 21 | shape=plaintext 22 | label=< 23 | 24 | 25 |
2
26 | >]; 27 | 28 | child_three [ 29 | shape=plaintext 30 | label=< 31 | 32 | 33 |
3
34 | >]; 35 | 36 | parent:port_one -> child_one; 37 | parent:port_two -> child_two; 38 | parent:port_three -> child_three; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /test/examples/ex15.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | node [shape=plaintext fontname="Sans serif" fontsize="8"]; 4 | 5 | task_menu [ label=< 6 | 7 | 8 | 9 | 10 |
Task 1
Choose Menu
done
>]; 11 | 12 | task_ingredients [ label=< 13 | 14 | 15 | 16 | 17 |
Task 2
Buy ingredients
done
>]; 18 | 19 | task_invitation [ label=< 20 | 21 | 22 | 23 | 24 |
Task 4
Send invitation
done
>]; 25 | 26 | task_cook [ label=< 27 | 28 | 29 | 30 | 31 |
Task 5
Cook
todo
>]; 32 | 33 | task_table[ label=< 34 | 35 | 36 | 37 | 38 |
Task 3
Lay table
todo
>]; 39 | 40 | task_eat[ label=< 41 | 42 | 43 | 44 | 45 |
Task 6
Eat
todo
>]; 46 | 47 | 48 | task_menu -> task_ingredients; 49 | task_ingredients -> task_cook; 50 | task_invitation -> task_cook; 51 | task_table -> task_eat; 52 | task_cook -> task_eat; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /test/examples/ex2.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | node [fontname="Arial"]; 4 | 5 | node_A [shape=record label="shape=record|{above|middle|below}|right"]; 6 | node_B [shape=plaintext label="shape=plaintext|{curly|braces and|bars without}|effect"]; 7 | 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test/examples/ex3.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | A -> {B C D} -> {F, D} 4 | 5 | } 6 | -------------------------------------------------------------------------------- /test/examples/ex4.dot: -------------------------------------------------------------------------------- 1 | digraph L { 2 | 3 | node [shape=record fontname=Arial]; 4 | 5 | a [label="one\ltwo three\lfour five six seven\l"] 6 | b [label="one\ntwo three\nfour five six seven"] 7 | c [label="one\rtwo three\rfour five six seven\r"] 8 | 9 | a -> b -> c 10 | 11 | } 12 | -------------------------------------------------------------------------------- /test/examples/ex5.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | label = "The foo, the bar and the baz"; 4 | labelloc = "t"; // place the label at the top (b seems to be default) 5 | 6 | node [shape=plaintext] 7 | 8 | FOO -> {BAR BAZ}; 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /test/examples/ex6.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | label = foo,
the bar and
the baz>; 4 | labelloc = "t"; // place the label at the top (b seems to be default) 5 | 6 | node [shape=plaintext] 7 | 8 | FOO -> {BAR, BAZ}; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /test/examples/ex7.dot: -------------------------------------------------------------------------------- 1 | digraph R { 2 | 3 | node [shape=record]; 4 | 5 | { rank=same rA sA tA } 6 | { rank=same uB vB wB } 7 | 8 | 9 | rA -> sA; 10 | sA -> vB; 11 | t -> rA; 12 | uB -> vB; 13 | wB -> u; 14 | wB -> tA; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/examples/ex8.dot: -------------------------------------------------------------------------------- 1 | digraph Q { 2 | 3 | node [shape=record]; 4 | 5 | 6 | nd_1 [label = "Node 1"]; 7 | nd_2 [label = "Node 2"]; 8 | nd_3_a [label = "Above Right Node 3"]; 9 | nd_3_l [label = "Left of Node 3"]; 10 | nd_3 [label = "Node 3"]; 11 | nd_3_r [label = "Right of Node 3"]; 12 | nd_4 [label = "Node 4"]; 13 | 14 | 15 | nd_3_a -> nd_3_r; 16 | nd_1 -> nd_2 -> nd_3 -> nd_4; 17 | 18 | subgraph cluster_R { 19 | node [level2=v2] 20 | {node [level3=v3] rank=same nd_3_l nd_3 nd_3_r} 21 | 22 | nd_3_l -> nd_3 -> nd_3_r [color=grey arrowhead=none]; 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/examples/ex9.dot: -------------------------------------------------------------------------------- 1 | digraph D { 2 | 3 | subgraph cluster_p { 4 | label = "Parent"; 5 | 6 | subgraph cluster_c1 { 7 | label = "Child one"; 8 | a; 9 | 10 | subgraph cluster_gc_1 { 11 | label = "Grand-Child one"; 12 | b; 13 | } 14 | subgraph cluster_gc_2 { 15 | label = "Grand-Child two"; 16 | c; 17 | d; 18 | } 19 | 20 | } 21 | 22 | subgraph cluster_c2 { 23 | label = "Child two"; 24 | e; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------