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 }