1 /// Author: Aziz Köksal 2 /// License: GPL3 3 /// $(Maturity high) 4 module cmd.DDoc; 5 6 import cmd.Command; 7 import dil.doc.DDocEmitter, 8 dil.doc.DDocHTML, 9 dil.doc.DDocXML, 10 dil.doc.Parser, 11 dil.doc.Macro, 12 dil.doc.Doc; 13 import dil.lexer.Token, 14 dil.lexer.Funcs; 15 import dil.semantic.Module, 16 dil.semantic.Package, 17 dil.semantic.Pass1, 18 dil.semantic.Symbol, 19 dil.semantic.Symbols; 20 import dil.ModuleManager, 21 dil.Highlighter, 22 dil.Compilation, 23 dil.Diagnostics, 24 dil.Converter, 25 dil.SourceText, 26 dil.Enums, 27 dil.Time, 28 dil.Array; 29 import util.Path; 30 import SettingsLoader; 31 import Settings; 32 import common; 33 34 import std.file, 35 std.regex; 36 import std.datetime : Clock; 37 38 /// The ddoc command. 39 class DDocCommand : Command 40 { 41 cstring destDirPath; /// Destination directory. 42 cstring[] macroPaths; /// Macro file paths. 43 cstring[] filePaths; /// Module file paths. 44 cstring modsTxtPath; /// Write list of modules to this file if specified. 45 cstring outFileExtension; /// The extension of the output files. 46 Regex!char[] regexps; /// Regular expressions. 47 bool includeUndocumented; /// Whether to include undocumented symbols. 48 bool includePrivate; /// Whether to include private symbols. 49 bool writeReport; /// Whether to write a problem report. 50 bool useKandil; /// Whether to use kandil. 51 bool writeXML; /// Whether to write XML instead of HTML docs. 52 bool writeHLFiles; /// Whether to write syntax highlighted files. 53 bool rawOutput; /// Whether to expand macros or not. 54 55 CompilationContext context; /// Environment variables of the compilation. 56 Diagnostics diag; /// Collects error messages. 57 Diagnostics reportDiag; /// Collects problem messages. 58 Highlighter hl; /// For highlighting source files or DDoc code sections. 59 /// For managing loaded modules and getting a sorted list of them. 60 ModuleManager mm; 61 62 /// Executes the doc generation command. 63 override void run() 64 { 65 context.addVersionId("D_Ddoc"); // Define D_Ddoc version identifier. 66 67 auto destDirPath = Path(destDirPath); 68 destDirPath.exists || destDirPath.createFolder(); 69 70 if (useKandil) 71 if (auto symDir = destDirPath/"symbols") 72 symDir.exists || symDir.createFolder(); 73 74 if (writeHLFiles) 75 if (auto srcDir = destDirPath/"htmlsrc") 76 srcDir.exists || srcDir.createFolder(); 77 78 if (useKandil && writeXML) 79 return cast(void) 80 Stdout("Error: kandil uses only HTML at the moment.").newline; 81 82 // Prepare macro files: 83 // Get file paths from the config file. 84 cstring[] macroPaths = GlobalSettings.ddocFilePaths.dup; 85 86 if (useKandil) 87 macroPaths ~= (Path(GlobalSettings.kandilDir) /= "kandil.ddoc")[]; 88 macroPaths ~= this.macroPaths; // Add files from the cmd line arguments. 89 90 // Parse macro files and build macro table hierarchy. 91 MacroTable mtable; 92 foreach (macroPath; macroPaths) 93 { 94 auto macros = MacroParser.parse(loadMacroFile(macroPath, diag)); 95 mtable = new MacroTable(mtable); 96 mtable.insert(macros); 97 } 98 99 if (writeReport) 100 reportDiag = new Diagnostics(); 101 102 // For Ddoc code sections. 103 auto mapFilePath = GlobalSettings.htmlMapFile; 104 if (writeXML) 105 mapFilePath = GlobalSettings.xmlMapFile; 106 auto map = TagMapLoader(context, diag).load(mapFilePath); 107 auto tags = new TagMap(map); 108 109 hl = new Highlighter(tags, context); 110 111 outFileExtension = writeXML ? ".xml" : ".html"; 112 113 mm = new ModuleManager(context); 114 115 // Process D files. 116 foreach (filePath; filePaths) 117 { 118 auto mod = new Module(filePath, context); 119 120 // Only parse if the file is not a "Ddoc"-file. 121 if (!DDocEmitter.isDDocFile(mod)) 122 { 123 if (mm.moduleByPath(mod.filePath())) 124 continue; // The same file path was already loaded. TODO: warning? 125 126 lzy(log("Parsing: {}", mod.filePath())); 127 128 mod.parse(); 129 if (mm.moduleByFQN(mod.getFQNPath())) 130 continue; // Same FQN, but different file path. TODO: error? 131 if (mod.hasErrors) 132 continue; // No documentation for erroneous source files. 133 // Add the module to the manager. 134 mm.addModule(mod); 135 // Write highlighted files before SA, since it mutates the tree. 136 if (writeHLFiles) 137 writeSyntaxHighlightedFile(mod); 138 // Start semantic analysis. 139 auto pass1 = new SemanticPass1(mod, context); 140 pass1.run(); 141 } 142 else // Normally done in mod.parse(). 143 mod.setFQN(Path(filePath).name()); 144 145 // Write the documentation file. 146 writeDocumentationFile(mod, mtable); 147 } 148 149 if (useKandil || writeReport) 150 mm.sortPackageTree(); 151 if (useKandil || modsTxtPath.length) 152 writeModuleLists(); 153 if (writeReport) 154 writeDDocReport(); 155 lzy({if (diag.hasInfo()) log("\nDdoc diagnostic messages:");}()); 156 } 157 158 /// Writes a syntax highlighted file for mod. 159 void writeSyntaxHighlightedFile(Module mod) 160 { 161 auto filePath = (((Path(destDirPath) /= "htmlsrc") /= mod.getFQN()) 162 ~= outFileExtension)[]; 163 lzy(log("hl > {}", filePath)); 164 hl.highlightSyntax(mod, !writeXML, true); 165 filePath.write(hl.takeText()); 166 } 167 168 /// Writes the documentation for a module to the disk. 169 /// Params: 170 /// mod = The module to be processed. 171 /// mtable = The main macro environment. 172 void writeDocumentationFile(Module mod, MacroTable mtable) 173 { 174 // Build destination file path. 175 auto destPath = Path(destDirPath); 176 (destPath /= mod.getFQN()) ~= outFileExtension; 177 178 // TODO: create a setting for this format string in dilconf.d? 179 lzy(log("ddoc > {}", destPath)); 180 181 // Create an own macro environment for this module. 182 mtable = new MacroTable(mtable); 183 // Define runtime macros. 184 auto modFQN = mod.getFQN(); 185 mtable.insert("DIL_MODPATH", mod.getFQNPath() ~ "." ~ mod.fileExtension()); 186 mtable.insert("DIL_MODFQN", modFQN); 187 mtable.insert("DIL_DOCFILENAME", modFQN ~ outFileExtension); 188 mtable.insert("TITLE", modFQN); 189 auto timeStr = Time.now(); 190 mtable.insert("DATETIME", timeStr); 191 mtable.insert("YEAR", Time.year(timeStr)); 192 193 // Create the appropriate DDocEmitter. 194 DDocEmitter ddocEmitter; 195 if (writeXML) 196 ddocEmitter = new DDocXMLEmitter(mod, mtable, includeUndocumented, 197 includePrivate, getReportDiag(modFQN), hl); 198 else 199 ddocEmitter = new DDocHTMLEmitter(mod, mtable, includeUndocumented, 200 includePrivate, getReportDiag(modFQN), hl); 201 // Start the emitter. 202 auto ddocText = ddocEmitter.emit(); 203 // Set the BODY macro to the text produced by the emitter. 204 mtable.insert("BODY", ddocText); 205 // Do the macro expansion pass. 206 auto dg = verbose ? this.diag : null; 207 cstring DDOC = "$(DDOC)"; 208 if (auto ddocMacro = mtable.search("DDOC")) 209 DDOC = ddocMacro.text; 210 auto fileText = rawOutput ? ddocText : 211 MacroExpander.expand(mtable, DDOC, mod.filePath, dg); 212 213 // Finally write the XML/XHTML file out to the harddisk. 214 destPath[].write(fileText); 215 216 // Write the documented symbols in this module to a json file. 217 if (ddocEmitter.symbolTree.length && useKandil) 218 { 219 auto filePath = ((Path(destDirPath) /= "symbols") /= modFQN) ~= ".json"; 220 CharArray text; 221 symbolsToJSON(ddocEmitter.symbolTree[0], text); 222 filePath[].write(text[]); 223 } 224 } 225 226 /// Writes the list of processed modules to the disk. 227 /// Also writes DEST/js/modules.js if kandil is used. 228 /// Params: 229 /// mm = Has the list of modules. 230 void writeModuleLists() 231 { 232 CharArray buffer; 233 234 auto write = (cstring s) => buffer ~= s; 235 236 if (modsTxtPath.length) 237 { 238 foreach (modul; mm.loadedModules) 239 buffer.put(modul.filePath(), ", ", modul.getFQN(), "\n"); 240 modsTxtPath.write(buffer.take()); 241 } 242 243 if (!useKandil) 244 return; 245 246 copyKandilFiles(); 247 248 auto filePath = ((Path(destDirPath) /= "js") /= "modules.js")[]; 249 250 write("var g_moduleList = [\n "); // Write a flat list of FQNs. 251 size_t max_line_len = 80; 252 size_t line_len; 253 foreach (modul; mm.loadedModules) 254 { 255 auto fragment = ` "` ~ modul.getFQN() ~ `",`; 256 line_len += fragment.length; 257 if (line_len >= max_line_len) // See if we have to start a new line. 258 { 259 line_len = fragment.length + 1; // +1 for the space in "\n ". 260 write("\n "); 261 } 262 write(fragment); 263 } 264 write("\n];\n\n"); // Closing ]. 265 266 write("var g_packageTree = new PackageTree(P('', [\n"); 267 writePackage(buffer, mm.rootPackage); 268 write("])\n);\n"); 269 270 // Write a timestamp. Checked by kandil to clear old storage. 271 auto stamp = Clock.currTime.toUnixTime(); 272 write(Format("\nvar g_creationTime = {};\n", stamp)); 273 274 filePath.write(buffer[]); 275 } 276 277 /// Creates sub-folders and copies kandil's files into them. 278 void copyKandilFiles() 279 { // Create folders if they do not exist yet. 280 auto destDir = Path(destDirPath); 281 auto dir = destDir.dup; 282 foreach (path; ["css", "js", "img"]) 283 dir.set(destDir / path).exists() || dir.createFolder(); 284 // Copy kandil files. 285 auto kandil = Path(GlobalSettings.kandilDir); 286 auto data = Path(GlobalSettings.dataDir); 287 (destDir / "css" /= "style.css").copy(kandil / "css" /= "style.css"); 288 if (writeHLFiles) 289 (destDir / "htmlsrc" /= "html.css").copy(data / "html.css"); 290 foreach (js_file; ["navigation.js", "jquery.js", "quicksearch.js", 291 "symbols.js", "treeview.js", "utilities.js"]) 292 (destDir / "js" /= js_file).copy(kandil / "js" /= js_file); 293 foreach (file; ["alias", "class", "enummem", "enum", "function", 294 "interface", "module", "package", "struct", "template", 295 "typedef", "union", "variable", 296 "tv_dot", "tv_minus", "tv_plus", "magnifier"]) 297 { 298 file = "icon_" ~ file ~ ".png"; 299 (destDir / "img" /= file).copy(kandil / "img" /= file); 300 } 301 (destDir / "img" /= "loading.gif").copy(kandil / "img" /= "loading.gif"); 302 } 303 304 /// Writes the sub-packages and sub-modules of a package to the disk. 305 static void writePackage(ref CharArray a, Package pckg, uint_t indent = 2) 306 { 307 void writeIndented(Args...)(Args args) 308 { 309 a.len = a.len + indent; // Reserve. 310 a[Neg(indent)..Neg(0)][] = ' '; // Write spaces. 311 a.put(args); 312 } 313 foreach (p; pckg.packages) 314 { 315 writeIndented("P('", p.getFQN(), "',[\n"); 316 writePackage(a, p, indent + 2); 317 writeIndented("]),\n"); 318 } 319 foreach (m; pckg.modules) 320 writeIndented("M('", m.getFQN(), "'),\n"); 321 } 322 323 /// Converts the symbol tree into JSON. 324 static char[] symbolsToJSON(DocSymbol symbol, ref CharArray text) 325 { 326 auto locbeg = symbol.begin.lineNum, locend = symbol.end.lineNum; 327 text.put(`["`, symbol.name, `",`, symbol.kind.itoa, ",", 328 symbol.formatAttrsAsIDs(), ",[", locbeg.itoa, ",", locend.itoa, "],["); 329 foreach (s; symbol.members) 330 { 331 text ~= '\n'; 332 symbolsToJSON(s, text); 333 text ~= ','; 334 } 335 if (text[Neg(1)] == ',') 336 text.cur--; // Remove last comma. 337 text ~= "]]"; 338 return text[]; 339 } 340 341 version(unused) 342 { 343 /// Converts the symbol tree into JSON. 344 static char[] symbolsToJSON(DocSymbol symbol, cstring indent = "\t") 345 { 346 char[] text; 347 auto locbeg = symbol.begin.lineNum, locend = symbol.end.lineNum; 348 text ~= Format( 349 "{{\n"`{}"name":"{}","fqn":"{}","kind":"{}","loc":[{},{}],`"\n", 350 indent, symbol.name, symbol.fqn, symbol.kind, locbeg, locend); 351 text ~= indent~`"sub":[`; 352 foreach (s; symbol.members) 353 { 354 text ~= symbolsToJSON(s, indent~"\t"); 355 text ~= ","; 356 } 357 if (text[$-1] == ',') 358 text = text[0..$-1]; // Remove last comma. 359 text ~= "]\n"~indent[0..$-1]~"}"; 360 return text; 361 } 362 } 363 364 /// Loads a macro file. Converts any Unicode encoding to UTF-8. 365 /// Params: 366 /// filePath = Path to the macro file. 367 /// diag = For error messages. 368 static char[] loadMacroFile(cstring filePath, Diagnostics diag) 369 { 370 auto src = new SourceText(filePath); 371 src.load(diag); 372 // Casting away const is okay here. 373 return sanitizeText(cast(char[])src.text()); 374 } 375 376 // Report functions: 377 378 /// Returns the reportDiag for moduleName if it is not filtered 379 /// by the -rx=REGEXP option. 380 Diagnostics getReportDiag(cstring moduleName) 381 { 382 foreach (rx; regexps) 383 if (!matchFirst(moduleName, rx).empty) 384 return null; 385 return reportDiag; 386 } 387 388 /// Used for collecting data for the report. 389 static class ModuleData 390 { 391 cstring name; /// Module name. 392 DDocProblem[] kind1, kind2, kind3, kind4; 393 394 static: 395 ModuleData[hash_t] table; 396 ModuleData[] sortedList; 397 /// Returns a ModuleData for name. 398 /// Inserts a new instance into the table if not present. 399 ModuleData get(cstring name) 400 { 401 auto hash = hashOf(name); 402 auto mod = hash in table; 403 if (mod) 404 return *mod; 405 auto md = new ModuleData(); 406 md.name = name; 407 table[hash] = md; 408 return md; 409 } 410 /// Uses mm to set the member sortedList. 411 void sort(ModuleManager mm) 412 { 413 auto allModules = mm.rootPackage.getModuleList(); 414 foreach (mod; allModules) 415 if (auto data = hashOf(mod.getFQN()) in table) 416 sortedList ~= *data; 417 } 418 } 419 420 /// Writes the DDoc report. 421 void writeDDocReport() 422 { 423 assert(writeReport); 424 CharArray buffer; 425 426 auto filePath = (Path(destDirPath) /= "report.txt")[]; 427 428 lzy(log("Writing report to ‘{}’.", filePath)); 429 430 auto titles = ["Undocumented symbols", "Empty comments", 431 "No params section", "Undocumented parameters"]; 432 433 // Sort problems. 434 uint kind1Total, kind2Total, kind3Total, kind4Total; 435 foreach (info; reportDiag.info) 436 { 437 auto p = cast(DDocProblem)info; 438 auto mod = ModuleData.get(p.filePath); 439 final switch (p.kind) with (DDocProblem.Kind) 440 { 441 case UndocumentedSymbol: kind1Total++; mod.kind1 ~= p; break; 442 case EmptyComment: kind2Total++; mod.kind2 ~= p; break; 443 case NoParamsSection: kind3Total++; mod.kind3 ~= p; break; 444 case UndocumentedParam: kind4Total++; mod.kind4 ~= p; break; 445 } 446 } 447 448 ModuleData.sort(mm); 449 450 // Write the legend. 451 buffer.put("US = ", titles[0], "\nEC = ", titles[1], 452 "\nNP = ", titles[2], "\nUP = ", titles[3], "\n"); 453 454 // Calculate the maximum module name length. 455 size_t maxNameLength; 456 foreach (mod; ModuleData.sortedList) 457 if (maxNameLength < mod.name.length) 458 maxNameLength = mod.name.length; 459 460 auto rowFormat = "{,-"~maxNameLength.itoa()~"} | {,6} {,6} {,6} {,6}\n"; 461 // Write the headers. 462 buffer.put(Format(rowFormat, "Module", "US", "EC", "NP", "UP")); 463 auto ruler = new char[maxNameLength+2+4*7]; 464 ruler[] = '-'; 465 buffer.put(ruler, "\n"); 466 // Write the table rows. 467 foreach (mod; ModuleData.sortedList) 468 buffer ~= Format(rowFormat, mod.name, 469 mod.kind1.length, mod.kind2.length, mod.kind3.length, mod.kind4.length 470 ); 471 buffer.put(ruler, "\n"); 472 // Write the totals. 473 buffer ~= Format(rowFormat, "Totals", 474 kind1Total, kind2Total, kind3Total, kind4Total); 475 476 // Write the list of locations. 477 buffer ~= "\nList of locations:\n"; 478 foreach (i, title; titles) 479 { 480 buffer.put("\n***** ", title, " ******\n"); 481 foreach (mod; ModuleData.sortedList) 482 { 483 // Print list of locations. 484 auto kind = ([mod.kind1, mod.kind2, mod.kind3, mod.kind4])[i]; 485 if (!kind.length) 486 continue; // Nothing to print for this module. 487 buffer.put(mod.name, ":\n"); // Module name: 488 char[] line; 489 foreach (p; kind) 490 { // (x,y) (x,y) etc. 491 line ~= p.location.str("({},{}) "); 492 if (line.length > 80) 493 buffer.put(" ", line[0..$-1], "\n"), (line = null); 494 } 495 if (line.length) 496 buffer.put(" ", line[0..$-1], "\n"); 497 } 498 } 499 500 filePath.write(buffer[]); 501 } 502 }