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 }