1 /**
2  * D Documentation Generator
3  * Copyright: © 2014 Economic Modeling Specialists, Intl.
4  *            © 2021 Eugene Stulin
5  * Authors: Brian Schott and other members of the D community,
6  *          Eugene Stulin
7  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
8  */
9 module main;
10 
11 import std.algorithm,
12        std.array,
13        std.conv,
14        std.datetime,
15        std.exception,
16        std.file,
17        std.getopt,
18        std.path,
19        std.stdio,
20        std.string;
21 
22 import dparse.ast,
23        dparse.lexer,
24        dparse.parser;
25 
26 import ddoc.lexer;
27 
28 import allocator;
29 import config;
30 import macros;
31 import symboldatabase;
32 import tocbuilder;
33 import unittest_preprocessor;
34 import visitor;
35 import writer;
36 
37 immutable string defaultConfigPath = "hgen.cfg";
38 immutable string linuxConfigDir;
39 immutable string linuxConfigPath;
40 immutable string language;
41 
42 
43 shared static this() {
44     version (linux) {
45         import std.process : environment;
46         auto home = environment["HOME"];
47         linuxConfigDir = std.path.buildPath(home, ".config", "hgen");
48         linuxConfigPath = std.path.buildPath(linuxConfigDir, "hgen.cfg");
49     }
50     auto LANG = environment.get("LANG", "en_US.UTF-8");
51     auto langAndCharset = LANG.split(".").array;
52     auto tempLang = "";
53     if (langAndCharset.length > 0) {
54         auto langAndCountry = langAndCharset[0].split("_").array;
55         if (langAndCountry.length > 0) {
56             tempLang = langAndCountry[0];
57         }
58     }
59     language = tempLang=="" ? "en" : tempLang;
60 }
61 
62 
63 int main(string[] args) {
64     const startTime = Clock.currStdTime;
65 
66     Config config;
67 
68     config.loadConfigFile(getDefaultConfigPath());
69     config.loadCLI(args);
70     
71     if (config.doHelp) {
72         writeln(helpTranslations.get(language, "en"));
73         return 0;
74     }
75 
76     if (config.showVersion) {
77         writeln(versionString);
78         return 0;
79     }
80 
81     scope(exit) {
82         if (config.performance) {
83             writefln("Time spent: %.3fs",
84                     (Clock.currStdTime - startTime) / 10_000_000.0);
85             writefln("Peak memory usage (KiB): %s", peakMemoryUsageK());
86         }
87     }
88 
89     if (config.doGenerateCSSPath !is null) {
90         writefln("Generating CSS file '%s'", config.doGenerateCSSPath);
91         return writeProtected(config.doGenerateCSSPath, stylecss, "CSS");
92     }
93     if (config.doGenerateConfig) {
94         writefln("Generating config file '%s'", defaultConfigPath);
95         return writeProtected(defaultConfigPath, defaultConfigString, "config");
96     }
97 
98     version(linux) {
99         if (config.doGenerateConfigForLinux) {
100             if (!std.file.exists(linuxConfigPath)) {
101                 mkdirRecurse(linuxConfigDir);
102                 writefln("Generating config file '%s'", linuxConfigPath);
103                 return writeProtected(
104                     linuxConfigPath, defaultConfigString, "config"
105                 );
106             }
107             return 0;
108         }
109     }
110 
111     try {
112         config.macros = readMacros(config.macroFileNames);
113     } catch (Exception e) {
114         stderr.writeln(e.msg);
115         return 1;
116     }
117 
118     switch(config.format) {
119         case "html-simple":
120             generateDocumentation!HTMLWriterSimple(config);
121             break;
122         case "html-aggregated":
123             generateDocumentation!HTMLWriterAggregated(config);
124             break;
125         default:
126             writeln("Unknown format: ", config.format);
127     }
128 
129     return 0;
130 }
131 
132 
133 /*******************************************************************************
134  * Returns path to config file by default: in current directory or (for Linux)
135  * in ~/.config/hgen/ if in the current directory the file doesn't exist.
136  */
137 string getDefaultConfigPath() {
138     string configPath = defaultConfigPath;
139     version (linux) {
140         if (!exists(defaultConfigPath) && exists(linuxConfigPath)) {
141             configPath = linuxConfigPath;
142         }
143     }
144     return configPath;
145 }
146 
147 
148 /// Used to write default CSS/config with overwrite checking.
149 int writeProtected(string path, string content, string type) {
150     if (path.exists) {
151         writefln("'%s' exists. Overwrite? (y/N)", path);
152         import std.ascii: toLower;
153         char overwrite;
154         readf("%s", &overwrite);
155         if(overwrite.toLower != 'y') {
156             writefln("Exited without overwriting '%s'", path);
157             return 1;
158         }
159         writefln("Overwriting '%s'", path);
160     }
161     try {
162         std.file.write(path, content);
163     } catch(Exception e) {
164         writefln("Failed to write default %s to file `%s` : %s",
165             type, path, e.msg);
166         return 1;
167     }
168     return 0;
169 }
170 
171 
172 private string[string] readMacros(const string[] macroFiles) {
173     import ddoc.macros : defaultMacros = DEFAULT_MACROS;
174 
175     string[string] result;
176     defaultMacros.byKeyValue.each!(a => result[a.key] = a.value);
177     result["D"]    = `<code class="d_inline_code">$0</code>`;
178     result["HTTP"] = "<a href=\"http://$1\">$+</a>";
179     result["WEB"]  = "$(HTTP $1,$2)";
180     uniformCodeStyle(result);
181     macroFiles.each!(mf => mf.readMacroFile(result));
182     return result;
183 }
184 
185 
186 private void uniformCodeStyle(ref string[string] macros) {
187     macros[`D_CODE`] = `<pre><code class="hljs_d">$0</code></pre>`;
188     macros[`D`] = `<b>$0</b>`;
189     macros[`D_INLINECODE`] = `<b>$0</b>`;
190     macros[`D_COMMENT`] = `$0`;
191     macros[`D_KEYWORD`] = `$0`;
192     macros[`D_PARAM`] = macros[`D_INLINECODE`];
193 }
194 
195 
196 private void generateDocumentation(Writer)(ref Config config) {
197     string[] files = getFilesToProcess(config);
198     import std.stdio : writeln;
199     stdout.writeln("Writing documentation to ", config.outputDirectory);
200 
201     mkdirRecurse(config.outputDirectory);
202 
203     File search = File(buildPath(config.outputDirectory, "search.js"), "w");
204     search.writeln(`"use strict";`);
205     search.writeln(`var items = [`);
206 
207     auto database =
208         gatherData(config, new Writer(config, search, null, null), files);
209 
210     TocItem[] tocItems = buildTree(
211         database.moduleNames, database.moduleNameToLink
212     );
213 
214     enum noFile = "missing file";
215     string[] tocAdditionals =
216         config.tocAdditionalFileNames.map!(
217             path => path.exists ? readText(path) : noFile
218         ).array ~ config.tocAdditionalStrings;
219 
220     if (!tocAdditionals.empty) {
221         foreach(ref text; tocAdditionals) {
222             auto html = new Writer(config, search, null, null);
223             auto writer = appender!string();
224             html.readAndWriteComment(writer, text);
225             text = writer.data;
226         }
227     }
228 
229     foreach (f; database.moduleFiles) {
230         writeln("Generating documentation for ", f);
231         try {
232             writeDocumentation!Writer(
233                 config, database, f, search, tocItems, tocAdditionals
234             );
235         } catch (DdocParseException e) {
236             stderr.writeln(
237                 "Could not generate documentation for ",
238                 f, ": ", e.msg, ": ", e.snippet
239             );
240         } catch (Exception e) {
241             stderr.writeln(
242                 "Could not generate documentation for ", f, ": ", e.msg
243             );
244         }
245     }
246     search.writeln(`];`);
247     search.writeln(searchjs);
248 
249     // Write index.html and style.css
250     {
251         writeln("Generating main page");
252         File css = File(buildPath(config.outputDirectory, "style.css"), "w");
253         css.write(getCSS(config.cssFileName));
254         File index = File(buildPath(config.outputDirectory, "index.html"), "w");
255 
256         auto fileWriter = index.lockingTextWriter;
257         auto html = new Writer(config, search, tocItems, tocAdditionals);
258         html.writeHeader(fileWriter, "Index", 0);
259         const projectStr = config.projectName ~ " " ~ config.projectVersion;
260         const heading = projectStr == " " ? "Main Page"
261                                           : (projectStr ~ ": Main Page");
262         html.writeBreadcrumbs(fileWriter, heading);
263         html.writeTOC(fileWriter);
264 
265         // Index content added by the user.
266         if (config.indexFileName !is null) {
267             File indexFile = File(config.indexFileName);
268             ubyte[] indexBytes = new ubyte[cast(uint) indexFile.size];
269             indexFile.rawRead(indexBytes);
270             html.readAndWriteComment(fileWriter, cast(string)indexBytes);
271         }
272 
273         // A full list of all modules.
274         if (database.moduleNames.length <= config.maxModuleListLength) {
275             html.writeModuleList(fileWriter, database);
276         }
277 
278         index.writeln(HTML_END);
279     }
280 }
281 
282 
283 /*******************************************************************************
284  * Get the CSS content to write into style.css.
285  *
286  * If customCSS is not null, try to load from that file.
287  */
288 string getCSS(string customCSS) {
289     if (customCSS is null) {
290         return stylecss;
291     }
292     try {
293         return readText(customCSS);
294     } catch(Exception e) {
295         stderr.writefln("Failed to load custom CSS `%s`: %s", customCSS, e.msg);
296         return stylecss;
297     }
298 }
299 
300 
301 /// Creates documentation for the module at the given path
302 void writeDocumentation(Writer)(
303     ref Config config, SymbolDatabase database, 
304     string path, File search, TocItem[] tocItems, string[] tocAdditionals
305 ) {
306     LexerConfig lexConfig;
307     lexConfig.fileName = path;
308     lexConfig.stringBehavior = StringBehavior.source;
309 
310     File f = File(path);
311     ubyte[] fileBytes = uninitializedArray!(ubyte[])(to!size_t(f.size));
312     import core.memory;
313     scope(exit) { GC.free(fileBytes.ptr); }
314     f.rawRead(fileBytes);
315     StringCache cache = StringCache(1024 * 4);
316     auto tokens = getTokensForParser(fileBytes, lexConfig, &cache).array;
317 
318     import std.functional : toDelegate;
319     import dparse.rollback_allocator;
320     RollbackAllocator allocator;
321 
322     Module m = parseModule(tokens, path, &allocator, toDelegate(&doNothing));
323 
324     TestRange[][size_t] unitTestMapping = getUnittestMap(m);
325     
326     auto htmlWriter  = new Writer(config, search, tocItems, tocAdditionals);
327     auto visitor = new DocVisitor!Writer(
328         config, database, unitTestMapping, fileBytes, htmlWriter
329     );
330     visitor.visit(m);
331 }
332 
333 
334 /*******************************************************************************
335  * Get .d/.di files to process.
336  *
337  * Files that don't exist, are bigger than config.maxFileSizeK or
338  * could not be opened will be ignored.
339  *
340  * Params:
341  *
342  * config = Access to config to get source file and directory paths
343  *         get max file size.
344  * 
345  * Returns: Paths of files to process.
346  */
347 private string[] getFilesToProcess(ref const Config config) {
348     auto paths = config.sourcePaths.dup;
349     auto files = appender!(string[])();
350     void addFile(string path) {
351         const size = path.getSize();
352         if (size > config.maxFileSizeK * 1024) {
353             writefln(
354                 "WARNING: '%s' (%skiB) bigger than max file size (%skiB), " ~
355                 "ignoring", path, size / 1024, config.maxFileSizeK
356             );
357             return;
358         }
359         files.put(path);
360     }
361 
362     foreach (arg; paths) {
363         if (!arg.exists) {
364             stderr.writefln("WARNING: '%s' does not exist, ignoring", arg);
365         } else if (arg.isDir) {
366             foreach (fileName; arg.dirEntries("*.{d,di}", SpanMode.depth)) {
367                 addFile(fileName.expandTilde);
368             }
369         } else if (arg.isFile) {
370             addFile(arg.expandTilde);
371         } else {
372             stderr.writefln("WARNING: Could not open '%s', ignoring", arg);
373         }
374     }
375     return files.data;
376 }
377 
378 
379 /// Special empty function.
380 void doNothing(string, size_t, size_t, string, bool) {}
381 
382 
383 /// String representing the default CSS.
384 immutable string stylecss = import("style.css");
385 /// String representing the script used to search.
386 immutable string searchjs = import("search.js");
387 
388 
389 private ulong peakMemoryUsageK() {
390     version(linux) {
391         try {
392             auto line = File("/proc/self/status")
393                 .byLine().filter!(l => l.startsWith("VmHWM"));
394             enforce(!line.empty, "No VmHWM in /proc/self/status");
395             return line.front.split()[1].to!ulong;
396         } catch(Exception e) {
397             writeln("Failed to get peak memory usage: ", e);
398             return 0;
399         }
400     } else {
401         writeln("peakMemoryUsageK not implemented on non-Linux platforms");
402         return 0;
403     }
404 }