1 module ddox.main; 2 3 import ddox.ddoc; 4 import ddox.ddox; 5 import ddox.entities; 6 import ddox.htmlgenerator; 7 import ddox.htmlserver; 8 import ddox.jsonparser; 9 import ddox.jsonparser_old; 10 11 import vibe.core.core; 12 import vibe.core.file; 13 import vibe.data.json; 14 import vibe.http.fileserver; 15 import vibe.http.router; 16 import vibe.http.server; 17 import vibe.stream.operations; 18 import std.array; 19 import std.exception : enforce; 20 import std.file; 21 import std.getopt; 22 import std.stdio; 23 import std.string; 24 25 26 int ddoxMain(string[] args) 27 { 28 if( args.length < 2 ){ 29 showUsage(args); 30 return 1; 31 } 32 33 switch( args[1] ){ 34 default: showUsage(args); return 1; 35 case "generate-html": return cmdGenerateHtml(args); 36 case "serve-html": return cmdServeHtml(args); 37 case "filter": return cmdFilterDocs(args); 38 } 39 } 40 41 int cmdGenerateHtml(string[] args) 42 { 43 GeneratorSettings gensettings; 44 Package pack; 45 if( auto ret = setupGeneratorInput(args, gensettings, pack) ) 46 return ret; 47 48 generateHtmlDocs(Path(args[3]), pack, gensettings); 49 return 0; 50 } 51 52 int cmdServeHtml(string[] args) 53 { 54 string[] webfiledirs; 55 getopt(args, 56 config.passThrough, 57 "web-file-dir", &webfiledirs); 58 59 GeneratorSettings gensettings; 60 Package pack; 61 if( auto ret = setupGeneratorInput(args, gensettings, pack) ) 62 return ret; 63 64 // register the api routes and start the server 65 auto router = new URLRouter; 66 registerApiDocs(router, pack, gensettings); 67 68 foreach (dir; webfiledirs) 69 router.get("*", serveStaticFiles(dir)); 70 71 writefln("Listening on port 8080..."); 72 auto settings = new HTTPServerSettings; 73 settings.port = 8080; 74 listenHTTP(settings, router); 75 76 return runEventLoop(); 77 } 78 79 int setupGeneratorInput(ref string[] args, out GeneratorSettings gensettings, out Package pack) 80 { 81 string[] macrofiles; 82 string[] overridemacrofiles; 83 NavigationType navtype; 84 string[] pack_order; 85 string sitemapurl = "http://127.0.0.1/"; 86 MethodStyle file_name_style = MethodStyle.unaltered; 87 bool lowercasenames = false; 88 getopt(args, 89 //config.passThrough, 90 "std-macros", ¯ofiles, 91 "override-macros", &overridemacrofiles, 92 "navigation-type", &navtype, 93 "package-order", &pack_order, 94 "sitemap-url", &sitemapurl, 95 "file-name-style", &file_name_style, 96 "lowercase-names", &lowercasenames); 97 98 if (lowercasenames) file_name_style = MethodStyle.lowerCase; 99 100 if( args.length < 3 ){ 101 showUsage(args); 102 return 1; 103 } 104 105 setDefaultDdocMacroFiles(macrofiles); 106 setOverrideDdocMacroFiles(overridemacrofiles); 107 108 // parse the json output file 109 auto docsettings = new DdoxSettings; 110 docsettings.packageOrder = pack_order; 111 pack = parseDocFile(args[2], docsettings); 112 113 gensettings = new GeneratorSettings; 114 gensettings.siteUrl = URL(sitemapurl); 115 gensettings.navigationType = navtype; 116 gensettings.fileNameStyle = file_name_style; 117 return 0; 118 } 119 120 int cmdFilterDocs(string[] args) 121 { 122 string[] excluded, included; 123 Protection minprot = Protection.Private; 124 bool keeputests = false; 125 bool keepinternals = false; 126 bool unittestexamples = true; 127 bool nounittestexamples = false; 128 bool justdoc = false; 129 getopt(args, 130 //config.passThrough, 131 "ex", &excluded, 132 "in", &included, 133 "min-protection", &minprot, 134 "only-documented", &justdoc, 135 "keep-unittests", &keeputests, 136 "keep-internals", &keepinternals, 137 "unittest-examples", &unittestexamples, // deprecated, kept to not break existing scripts 138 "no-unittest-examples", &nounittestexamples); 139 140 if (keeputests) keepinternals = true; 141 if (nounittestexamples) unittestexamples = false; 142 143 string jsonfile; 144 if( args.length < 3 ){ 145 showUsage(args); 146 return 1; 147 } 148 149 Json filterProt(Json json, Json parent, Json last_decl, Json mod) 150 { 151 if (last_decl.type == Json.Type.undefined) last_decl = parent; 152 153 string templateName(Json j){ 154 auto n = j.name.opt!string(); 155 auto idx = n.indexOf('('); 156 if( idx >= 0 ) return n[0 .. idx]; 157 return n; 158 } 159 160 if( json.type == Json.Type.Object ){ 161 auto comment = json.comment.opt!string().strip(); 162 if( justdoc && comment.empty ){ 163 if( parent.type != Json.Type.Object || parent.kind.opt!string() != "template" || templateName(parent) != json.name.opt!string() ) 164 return Json.undefined; 165 } 166 167 Protection prot = Protection.Public; 168 if( auto p = "protection" in json ){ 169 switch(p.get!string){ 170 default: break; 171 case "private": prot = Protection.Private; break; 172 case "package": prot = Protection.Package; break; 173 case "protected": prot = Protection.Protected; break; 174 } 175 } 176 if( comment == "private" ) prot = Protection.Private; 177 if( prot < minprot ) return Json.undefined; 178 179 auto name = json.name.opt!string(); 180 bool is_internal = name.startsWith("__"); 181 bool is_unittest = name.startsWith("__unittest"); 182 if (name.startsWith("_staticCtor") || name.startsWith("_staticDtor")) is_internal = true; 183 else if (name.startsWith("_sharedStaticCtor") || name.startsWith("_sharedStaticDtor")) is_internal = true; 184 185 if (unittestexamples && is_unittest && "comment" in json) { 186 assert(last_decl.type == Json.Type.object, "Don't have a last_decl context."); 187 try { 188 string source = extractUnittestSourceCode(json, mod); 189 if (last_decl.comment.opt!string.empty) { 190 writefln("Warning: Cannot add documented unit test %s to %s, which is not documented.", name, last_decl.name.opt!string); 191 } else { 192 last_decl.comment ~= format("Example:\n%s\n---\n%s\n---\n", comment, source); 193 } 194 } catch (Exception e) { 195 writefln("Failed to add documented unit test %s:%s as example: %s", 196 mod.file.get!string(), json["line"].get!long, e.msg); 197 return Json.undefined; 198 } 199 } 200 201 if (!keepinternals && is_internal) return Json.undefined; 202 203 if (!keeputests && is_unittest) return Json.undefined; 204 205 if (auto mem = "members" in json) 206 json.members = filterProt(*mem, json, Json.undefined, mod); 207 } else if( json.type == Json.Type.Array ){ 208 auto last_child_decl = Json.undefined; 209 Json[] newmem; 210 foreach (m; json) { 211 auto mf = filterProt(m, parent, last_child_decl, mod); 212 if (mf.type == Json.Type.undefined) continue; 213 if (mf.type == Json.Type.object && !mf.name.opt!string.startsWith("__unittest") && mf.comment.opt!string.strip != "ditto") 214 last_child_decl = mf; 215 newmem ~= mf; 216 } 217 return Json(newmem); 218 } 219 return json; 220 } 221 222 writefln("Reading doc file..."); 223 auto text = readText(args[2]); 224 int line = 1; 225 writefln("Parsing JSON..."); 226 auto json = parseJson(text, &line); 227 228 writefln("Filtering modules..."); 229 Json[] dst; 230 foreach (m; json) { 231 if ("name" !in m) { 232 writefln("No name for module %s - ignoring", m.file.opt!string); 233 continue; 234 } 235 auto n = m.name.get!string; 236 bool include = true; 237 foreach (ex; excluded) 238 if (n.startsWith(ex)) { 239 include = false; 240 break; 241 } 242 foreach (inc; included) 243 if (n.startsWith(inc)) { 244 include = true; 245 break; 246 } 247 if (include) dst ~= filterProt(m, Json.undefined, Json.undefined, m); 248 } 249 250 writefln("Writing filtered docs..."); 251 auto buf = appender!string(); 252 writePrettyJsonString(buf, Json(dst)); 253 std.file.write(args[2], buf.data()); 254 255 return 0; 256 } 257 258 Package parseDocFile(string filename, DdoxSettings settings) 259 { 260 writefln("Reading doc file..."); 261 auto text = readText(filename); 262 int line = 1; 263 writefln("Parsing JSON..."); 264 auto json = parseJson(text, &line); 265 writefln("Parsing docs..."); 266 Package root; 267 if( settings.oldJsonFormat ) root = parseJsonDocsOld(json); 268 else root = parseJsonDocs(json); 269 writefln("Finished parsing docs."); 270 271 processDocs(root, settings); 272 return root; 273 } 274 275 void showUsage(string[] args) 276 { 277 string cmd; 278 if( args.length >= 2 ) cmd = args[1]; 279 280 switch(cmd){ 281 default: 282 writefln( 283 `Usage: %s <COMMAND> [args...] 284 285 <COMMAND> can be one of: 286 generate-html 287 serve-html 288 filter 289 290 Specifying only the command with no further arguments will print detailed usage 291 information. 292 `, args[0]); 293 break; 294 case "serve-html": 295 writefln( 296 `Usage: %s serve-html <ddocx-input-file> 297 --std-macros=FILE File containing DDOC macros that will be available 298 --override-macros=FILE File containing DDOC macros that will override local 299 definitions (Macros: section) 300 --navigation-type=TYPE Change the type of navigation (ModuleList, 301 ModuleTree, DeclarationTree) 302 --package-order=NAME Causes the specified module to be ordered first. Can 303 be specified multiple times. 304 --sitemap-url Specifies the base URL used for sitemap generation 305 --web-file-dir=DIR Make files from dir available on the served site 306 `, args[0]); 307 break; 308 case "generate-html": 309 writefln( 310 `Usage: %s generate-html <ddocx-input-file> <output-dir> 311 --std-macros=FILE File containing DDOC macros that will be available 312 --override-macros=FILE File containing DDOC macros that will override local 313 definitions (Macros: section) 314 --navigation-type=TYPE Change the type of navigation (ModuleList, 315 ModuleTree, DeclarationTree) 316 --package-order=NAME Causes the specified module to be ordered first. Can 317 be specified multiple times. 318 --sitemap-url Specifies the base URL used for sitemap generation 319 --file-name-style=STY Sets a translation style for symbol names to file 320 names. Use this instead of --lowercase-name. 321 Possible values for STY: 322 unaltered, camelCase, pascalCase, lowerCase, 323 upperCase, lowerUnderscored, upperUnderscored 324 --lowercase-names DEPRECATED: Outputs all file names in lower case. 325 This option is useful on case insensitive file 326 systems. 327 `, args[0]); 328 break; 329 case "filter": 330 writefln( 331 `Usage: %s filter <ddocx-input-file> [options] 332 --ex=PREFIX Exclude modules with prefix 333 --in=PREFIX Force include of modules with prefix 334 --min-protection=PROT Remove items with lower protection level than 335 specified. 336 PROT can be: Public, Protected, Package, Private 337 --only-documented Remove undocumented entities. 338 --keep-unittests Do not remove unit tests from documentation. 339 Implies --keep-internals. 340 --keep-internals Do not remove symbols starting with two unterscores. 341 --unittest-examples Add documented unit tests as examples to the 342 preceeding declaration (deprecated, enabled by 343 default) 344 --no-unittest-examples Don't convert documented unit tests to examples 345 `, args[0]); 346 } 347 if( args.length < 2 ){ 348 } else { 349 350 } 351 } 352 353 private string extractUnittestSourceCode(Json decl, Json mod) 354 { 355 auto filename = mod.file.get!string(); 356 enforce("line" in decl && "endline" in decl, "Missing line/endline fields."); 357 auto from = decl["line"].get!long; 358 auto to = decl.endline.get!long; 359 360 // read the matching lines out of the file 361 auto app = appender!string(); 362 long lc = 1; 363 foreach (str; File(filename).byLine) { 364 if (lc >= from) { 365 app.put(str); 366 app.put('\n'); 367 } 368 if (++lc > to) break; 369 } 370 auto ret = app.data; 371 372 // strip the "unittest { .. }" surroundings 373 auto idx = ret.indexOf("unittest"); 374 enforce(idx >= 0, format("Missing 'unittest' for unit test at %s:%s.", filename, from)); 375 ret = ret[idx .. $]; 376 377 idx = ret.indexOf("{"); 378 enforce(idx >= 0, format("Missing opening '{' for unit test at %s:%s.", filename, from)); 379 ret = ret[idx+1 .. $]; 380 381 idx = ret.lastIndexOf("}"); 382 enforce(idx >= 0, format("Missing closing '}' for unit test at %s:%s.", filename, from)); 383 ret = ret[0 .. idx]; 384 385 // unindent lines according to the indentation of the first line 386 app = appender!string(); 387 string indent; 388 foreach (i, ln; ret.splitLines) { 389 if (i == 1) { 390 foreach (j; 0 .. ln.length) 391 if (ln[j] != ' ' && ln[j] != '\t') { 392 indent = ln[0 .. j]; 393 break; 394 } 395 } 396 if (i > 0 || ln.strip.length > 0) { 397 size_t j = 0; 398 while (j < indent.length && !ln.empty) { 399 if (ln.front != indent[j]) break; 400 ln.popFront(); 401 j++; 402 } 403 app.put(ln); 404 app.put('\n'); 405 } 406 } 407 return app.data; 408 }