1 module dgraphviz;
2 
3 import std.format : format;
4 
5 
6 struct Option {
7     string[string] option;
8     alias option this;
9 
10     auto toString() {
11         if (option.length == 0) return "";
12         auto s = " [ ";
13         foreach (k, v; option) {
14             s ~= "%s = \"%s\", ".format(k, v);
15         }
16         s = s[0 .. $-2] ~ " ]";
17         return s;
18     }
19 }
20 
21 private struct Edge {
22     string ark;
23     Node src, dst;
24     Option option;
25 
26     auto toString() {
27         return "\"%s\" %s \"%s\" %s;\n".format(src.label, ark, dst.label, option);
28     }
29 }
30 
31 private class Node {
32     string label;
33     Option option;
34     size_t nIn = 0, nOut = 0;
35 
36     this(string label) {
37         import std.string : replace;
38         this.label = label.replace("\"", "\\\"");
39     }
40 
41     this(string label, Option option) {
42         this(label);
43         this.option = option;
44     }
45 
46     auto info() {
47         if (option.length == 0) return "";
48         auto s = "\"%s\" %s;\n".format(label, option);
49         return s;
50     }
51 }
52 
53 
54 abstract class Graph {
55     import std.conv : to;
56 
57     // TODO use Set
58     Node[string] nodes;
59     Edge[string] edges;
60     Option graphOpt, nodeOpt, edgeOpt;
61 
62     ref auto node(ref Node d) { return d; }
63 
64     ref auto node(T)(T t) {
65         string[string] opt;
66         return node(t, opt);
67     }
68 
69     ref auto node(T)(T t, string[string] option) {
70         auto key = t.to!string;
71         if (key !in this.nodes) {
72             this.nodes[key] = new Node(t.to!string, Option(option));
73         }
74         return this.nodes[key];
75     }
76 
77     auto edge(S, D)(S src, D dst,) {
78         string[string] opt;
79         return edge(src, dst, opt);
80     }
81 
82     auto edge(S, D)(S src, D dst, string[string] option) {
83         auto s = node(src);
84         auto d = node(dst);
85         auto e = Edge(this.ark, s, d, Option(option));
86         ++s.nOut;
87         ++d.nIn;
88         this.edges[e.to!string] = e;
89         return e;
90     }
91 
92     abstract string typename();
93     abstract string ark();
94 
95     override string toString() {
96         import std.array : array;
97         import std.algorithm : uniq, map, sort;
98         auto s = this.typename ~ " g{\n";
99 
100         if (graphOpt.length > 0) s ~= "graph %s;\n".format(graphOpt);
101         if (nodeOpt.length > 0) s ~= "node %s;\n".format(nodeOpt);
102         if (edgeOpt.length > 0) s ~= "edge %s;\n".format(edgeOpt);
103 
104         foreach (k, n; this.nodes) {
105             s ~= n.info;
106         }
107         foreach (k, e; this.edges) {
108             s ~= k;
109         }
110         s ~= "}\n";
111         return s;
112     }
113 
114     void save(string path) {
115         import std.stdio : File;
116         auto f = File(path, "w");
117         f.write(this.toString());
118         f.detach();
119     }
120 }
121 
122 class Undirected : Graph {
123     override string typename() { return "graph"; }
124     override string ark() { return "--"; }
125 }
126 
127 class Directed : Graph {
128     override string typename() { return "digraph"; }
129     override string ark() { return "->"; }
130 }
131 
132 
133 ///
134 unittest {
135     import std.stdio;
136     import std.format;
137     import dgraphviz;
138 
139     struct A {
140         auto toString() {
141             return "A\n\"struct\"";
142         }
143     }
144 
145     auto g = new Directed;
146     A a;
147     with (g) {
148         node(a, ["shape": "box", "color": "#ff0000"]);
149         edge(a, true);
150         edge(a, 1, ["style": "dashed", "label": "a-to-1"]);
151         edge(true, "foo");
152     }
153     g.save("simple.dot");
154 }
155 
156 Directed libraryDependency(string root, string prefix="",
157                            bool verbose=false, size_t maxDepth=3) {
158     import std.file : dirEntries, SpanMode, readText;
159     import std.format : formattedRead;
160     import std.string : split, strip, join, endsWith, replace, startsWith;
161     import std.algorithm : map, canFind, min, any, filter;
162     import std.stdio : writefln;
163 
164     auto g = new Directed;
165 
166     with (g) {
167         enum invalidTokens = ["\"", "$", "/", "\\"];
168         auto removeSub(string s) {
169             return s.split(".")[0..min($, maxDepth)].join(".");
170         }
171 
172         void registerEdge(string src, string dst) {
173             dst = dst.strip;
174             // FIXME follow import expr spec.
175             if (invalidTokens.map!(i => dst.canFind(i)).any) {
176                 return;
177             } else if (dst.canFind(":")) {
178                 registerEdge(src, dst.split(":")[0]);
179             } else if (dst.canFind(",")) {
180                 foreach (d; split(dst, ",")) {
181                     registerEdge(src, d);
182                 }
183             } else if (dst.canFind(" ")) {
184                 return;
185             } else if (dst.canFind("std.")) {
186                 if (verbose) writefln("%s -> %s", src, dst);
187                 edge(removeSub(src), removeSub(dst));
188             }
189         }
190 
191         auto dfiles = dirEntries(root, SpanMode.depth)
192             .filter!(f => f.name.startsWith(root ~ prefix) && f.name.endsWith(".d"));
193         foreach (dpath; dfiles) {
194             auto src = dpath[root.length .. $].replace("/", ".")[0 .. $-2];
195             try {
196                 foreach (txt; dpath.readText.split("import")[1..$]) {
197                     txt = "import " ~ txt;
198                     string dst, rest;
199                     txt.formattedRead!"import %s;%s"(dst, rest);
200                     if (verbose) writefln("%s ---------> %s", src, dst);
201                     registerEdge(src, dst);
202                 }
203             } catch (Exception e) {
204                 // FIXME display warnings
205             }
206         }
207     }
208     return g;
209 }
210 
211 ///
212 unittest {
213     import std.path;
214     import std.process;
215 
216     auto dc = environment.get("DC");
217     assert(dc != "", "use DUB or set DC enviroment variable");
218     auto which = executeShell("which " ~ dc);
219     assert(which.status == 0);
220     version(DigitalMars) {
221         auto root = which.output.dirName ~ "/../../src/phobos/";
222     }
223     version(LDC) {
224         auto root = which.output.dirName ~ "/../import/";
225     }
226 
227     auto g = libraryDependency(root, "std/range", true);
228     g.save("range.dot");
229 }