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