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