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