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