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