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 !is null) 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 File css = File(buildPath(config.outputDirectory, "style.css"), "w"); 182 css.write(getCSS(config.cssFileName)); 183 File js = File(buildPath(config.outputDirectory, "highlight.pack.js"), "w"); 184 js.write(hljs); 185 File showHideJs = File(buildPath(config.outputDirectory, "show_hide.js"), "w"); 186 showHideJs.write(showhidejs); 187 File index = File(buildPath(config.outputDirectory, "index.html"), "w"); 188 189 auto fileWriter = index.lockingTextWriter; 190 auto html = new Writer(config, search, tocItems, tocAdditionals); 191 html.writeHeader(fileWriter, "Index", 0); 192 const projectStr = config.projectName ~ " " ~ config.projectVersion; 193 const heading = projectStr == " " ? "Main Page" : (projectStr ~ ": Main Page"); 194 html.writeBreadcrumbs(fileWriter, heading); 195 html.writeTOC(fileWriter); 196 197 // Index content added by the user. 198 if (config.indexFileName !is null) 199 { 200 File indexFile = File(config.indexFileName); 201 ubyte[] indexBytes = new ubyte[cast(uint) indexFile.size]; 202 indexFile.rawRead(indexBytes); 203 html.readAndWriteComment(fileWriter, cast(string)indexBytes); 204 } 205 206 // A full list of all modules. 207 if (database.moduleNames.length <= config.maxModuleListLength) 208 { 209 html.writeModuleList(fileWriter, database); 210 } 211 212 index.writeln(HTML_END); 213 } 214 215 /** Get the CSS content to write into style.css. 216 * 217 * If customCSS is not null, try to load from that file. 218 */ 219 string getCSS(string customCSS) 220 { 221 if (customCSS.length == 0) 222 return stylecss; 223 try 224 return readText(customCSS); 225 catch(FileException e) 226 stderr.writefln("Failed to load custom CSS `%s`: %s", customCSS, e.msg); 227 return stylecss; 228 } 229 230 /// Creates documentation for the module at the given path 231 void writeDocumentation(Writer)(ref Config config, SymbolDatabase database, 232 string path, File search, TocItem[] tocItems, string[] tocAdditionals) 233 { 234 LexerConfig lexConfig; 235 lexConfig.fileName = path; 236 lexConfig.stringBehavior = StringBehavior.source; 237 238 // Load the module file. 239 ubyte[] fileBytes; 240 try 241 fileBytes = cast(ubyte[]) path.readText!(char[]); 242 catch (FileException e) 243 { 244 writefln("Failed to load file %s: will be ignored", path); 245 return; 246 } 247 StringCache cache = StringCache(optimalBucketCount(fileBytes.length)); 248 RollbackAllocator allocator; 249 Module m = fileBytes 250 .getTokensForParser(lexConfig, &cache) 251 .parseModule(path, &allocator, &ignoreParserError); 252 253 auto htmlWriter = new Writer(config, search, tocItems, tocAdditionals); 254 auto visitor = new DocVisitor!Writer(config, database, getUnittestMap(m), 255 fileBytes, htmlWriter); 256 visitor.visit(m); 257 } 258 259 /** Get .d/.di files to process. 260 * 261 * Files that don't exist, are bigger than config.maxFileSizeK or could not be 262 * opened will be ignored. 263 * 264 * Params: 265 * 266 * config = Access to config to get source file and directory paths get max file size. 267 * 268 * Returns: Paths of files to process. 269 */ 270 private string[] getFilesToProcess(ref const Config config) 271 { 272 auto paths = config.sourcePaths.dup; 273 auto files = appender!(string[])(); 274 void addFile(string path) 275 { 276 const size = path.getSize(); 277 if (size > config.maxFileSizeK * 1024) 278 { 279 writefln("WARNING: '%s' (%skiB) bigger than max file size (%skiB), " ~ 280 "ignoring", path, size / 1024, config.maxFileSizeK); 281 return; 282 } 283 files.put(path); 284 } 285 286 foreach (arg; paths) 287 { 288 if (!arg.exists) 289 stderr.writefln("WARNING: '%s' does not exist, ignoring", arg); 290 else if (arg.isDir) foreach (string fileName; arg.dirEntries("*.{d,di}", SpanMode.depth)) 291 addFile(fileName.expandTilde); 292 else if (arg.isFile) 293 addFile(arg.expandTilde); 294 else 295 stderr.writefln("WARNING: Could not open '%s', ignoring", arg); 296 } 297 return files.data; 298 } 299 300 /// Sink used to ignore error happening when parsing a module. 301 void ignoreParserError(string, size_t, size_t, string, bool) {} 302 303 /// String representing the script used to highlight. 304 immutable string hljs = import("highlight.pack.js"); 305 306 /// String representing the default CSS. 307 immutable string stylecss = import("style.css"); 308 309 /// String representing the script used to search. 310 immutable string searchjs = import("search.js"); 311 312 /// String representing the script used to show or hide the navbar. 313 immutable string showhidejs = import("show_hide.js"); 314 315 private ulong peakMemoryUsageK() 316 { 317 version(linux) 318 { 319 try 320 { 321 import std.exception : enforce; 322 import std.algorithm.searching : startsWith; 323 auto line = File("/proc/self/status").byLine().filter!(l => l.startsWith("VmHWM")); 324 enforce(!line.empty, new Exception("No VmHWM in /proc/self/status")); 325 return line.front.split()[1].to!ulong; 326 } 327 catch(Exception e) 328 { 329 writeln("Failed to get peak memory usage: ", e); 330 return 0; 331 } 332 } 333 else 334 { 335 writeln("peakMemoryUsageK not implemented on non-Linux platforms"); 336 return 0; 337 } 338 }