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