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 }