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;
10 import std.array;
11 import std.conv;
12 import dparse.ast;
13 import dparse.lexer;
14 import dparse.parser;
15 import std.file;
16 import std.getopt;
17 import std.path;
18 import std.stdio;
19 import visitor;
20 import unittest_preprocessor;
21 import macros;
22 import tocbuilder;
23 import ddoc.comments;
24 
25 int main(string[] args)
26 {
27 	version(unittest)
28 		return 0;
29 
30 	string[] macroFiles;
31 	string[] excludes;
32 	string outputDirectory;
33 	bool help;
34 	string indexContent;
35 
36 	getopt(args, "m|macros", &macroFiles, "o|output-directory", &outputDirectory,
37 		"h|help", &help, "i|index", &indexContent, "e|exclude", &excludes);
38 
39 	if (help)
40 	{
41 		writeln(helpString);
42 		return 0;
43 	}
44 
45 	string[string] macros;
46 	try
47 		macros = readMacros(macroFiles);
48 	catch (Exception e)
49 	{
50 		stderr.writeln(e.msg);
51 		return 1;
52 	}
53 
54 	if (outputDirectory is null)
55 		outputDirectory = "doc";
56 
57 	generateDocumentation(outputDirectory, indexContent, macros, args[1 .. $]);
58 
59 	return 0;
60 }
61 
62 string[string] readMacros(const string[] macroFiles)
63 {
64 	import ddoc.macros : DEFAULT_MACROS;
65 	string[string] rVal;
66 	foreach (k, v; DEFAULT_MACROS)
67 		rVal[k] = v;
68 	foreach (mf; macroFiles)
69 		readMacroFile(mf, rVal);
70 	return rVal;
71 }
72 
73 void generateDocumentation(string outputDirectory, string indexContent,
74 	string[string] macros, string[] args)
75 {
76 	string[] files = getFilesToProcess(args);
77 	import std.stdio : stderr, File;
78 	stderr.writeln("Writing documentation to ", outputDirectory);
79 
80 	mkdirRecurse(outputDirectory);
81 
82 	{
83 		File css = File(buildPath(outputDirectory, "style.css"), "w");
84 		css.write(stylecss);
85 		File js = File(buildPath(outputDirectory, "highlight.pack.js"), "w");
86 		js.write(hljs);
87 		File index = File(buildPath(outputDirectory, "index.html"), "w");
88 		index.write(indexhtml);
89 		if (indexContent !is null)
90 		{
91 			File frontPage = File(buildPath(outputDirectory, "index_content.html"), "w");
92 			writeHeader(frontPage, "Index", 0);
93 			frontPage.writeln(`<div class="breadcrumbs">
94 <table id="results"></table>
95 <input type="search" id="search" placeholder="Search" onkeyup="searchSubmit(this.value, event)"/>
96 Main Page</div>`);
97 			File indexContentFile = File(indexContent);
98 			ubyte[] indexContentBytes = new ubyte[cast(uint) indexContentFile.size];
99 			indexContentFile.rawRead(indexContentBytes);
100 			Comment[] prev;
101 			readAndWriteComment(frontPage, cast(string) indexContentBytes, macros, prev);
102 			frontPage.writeln(`
103 	</body>
104 </html>`);
105 		}
106 
107 	}
108 
109 	File search = File(buildPath(outputDirectory, "search.js"), "w");
110 	search.writeln(`"use strict";`);
111 	search.writeln(`var items = [`);
112 
113 	string[] modules;
114 	string[string] moduleMap;
115 
116 	foreach (f; files)
117 	{
118 		writeln("Generating documentation for ", f);
119 		string moduleName;
120 		string location;
121 		try
122 		{
123 			writeDocumentation(outputDirectory, f, macros, moduleName, location, search);
124 			if (moduleName != "")
125 			{
126 				immutable string path = stripLeadingDirectory(location, outputDirectory);
127 				modules ~= moduleName;
128 				moduleMap[moduleName] = path;
129 			}
130 		}
131 		catch (Exception e)
132 		{
133 			stderr.writeln("Could not generate documentation for ", f, ": ", e.msg);
134 		}
135 	}
136 	search.writeln(`];`);
137 	search.writeln(searchjs);
138 
139 	File toc = File(buildPath(outputDirectory, "toc.html"), "w");
140 	toc.writeln(`<!DOCTYPE html>
141 <html>
142 <head>
143 <meta charset="utf-8"/>
144 <style type="text/css">
145 html {
146 	background-color: #eee;
147     padding: 0;
148     margin: 0;
149 }
150 
151 body {
152     padding: 0;
153     margin: 0;
154 }
155 
156 ul {
157     font-family: sans;
158     list-style: none;
159     padding: 0 0 0 1.5em;
160 }
161 ul ul {
162 	display: none;
163 }
164 span {
165 	cursor: pointer;
166 	font-weight: bold;
167 }
168 
169 .expanded::before {
170 	content: "▼ ";
171 }
172 
173 span::before {
174 	content: "▶ ";
175 }
176 </style>
177 <script type="text/javascript">
178 "use strict";
179 function toggleChildren(t) {
180 	var c = t.nextElementSibling;
181 	if (t.className != "expanded" && (c.style.display === "" || c.style.display === "none")) {
182 		c.style.display = "list-item";
183 		t.className = "expanded";
184 	} else {
185 		c.style.display = "none";
186 		t.className = "";
187 	}
188 }
189 </script>
190 </head>
191 <body>`);
192 	toc.writeln(`<ul>`);
193 
194 	sort(modules);
195 	buildTree(modules, moduleMap).each!(a => a.write(toc));
196 	toc.writeln(`</ul></body></html>`);
197 }
198 
199 /// Creates documentation for the module at the given path
200 void writeDocumentation(string outputDirectory, string path,
201 	string[string] macros, ref string moduleName, ref string location,
202 	File search)
203 {
204 	import dparse.rollback_allocator : RollbackAllocator;
205 
206 	LexerConfig config;
207 	config.fileName = path;
208 	config.stringBehavior = StringBehavior.source;
209 
210 	File f = File(path);
211 	ubyte[] fileBytes = uninitializedArray!(ubyte[])(to!size_t(f.size));
212 	f.rawRead(fileBytes);
213 	StringCache cache = StringCache(1024 * 4);
214 	auto tokens = getTokensForParser(fileBytes, config, &cache).array;
215 	RollbackAllocator rba;
216 	void doNothing(string, size_t, size_t, string, bool) {}
217 	Module m = parseModule(tokens, path, &rba, &doNothing);
218 	const TestRange[][size_t] unitTestMapping = getUnittestMap(m);
219 	DocVisitor visitor = new DocVisitor(outputDirectory, macros, search,
220 		unitTestMapping, fileBytes);
221 	visitor.visit(m);
222 	moduleName = visitor.moduleName;
223 	location = visitor.location;
224 }
225 
226 string[] getFilesToProcess(string[] args)
227 {
228 	auto files = appender!(string[])();
229 	foreach (arg; args)
230 	{
231 		if (isDir(arg)) foreach (string fileName; dirEntries(arg, "*.{d,di}", SpanMode.depth))
232 			files.put(expandTilde(fileName));
233 		else if (isFile(arg))
234 			files.put(expandTilde(arg));
235 		else
236 			stderr.writeln("Could not open `", arg, "`");
237 	}
238 	return files.data;
239 }
240 
241 enum helpString = `
242 Generates documentation for D source code.
243 
244 Usage:
245     harbored [Options] file.d
246     harbored [Options] directory1/ directory2/ ...
247 
248 Options:
249     --macros | -m MACRO_FILE
250         Specifies a macro definition file
251 
252     --output-directory | -o DIR
253         Writes the generated documentation to the given directory. If this
254         option is not specified, documentation will be written to a folder
255         called "doc" in the current directory.
256 
257     --exclude | -e MODULE_NAME
258         Exclude the given module or package from the generated documentation.
259         By default no modules or packages will be excluded unless they do not
260         contain a module declaration.
261 
262     --index | -i DDOC_FILE
263         Use DDOC_FILE as the content of the index.html page. By default this
264         page will be blank.
265 
266     --help | -h
267         Prints this message.
268 `;
269 
270 immutable string hljs = import("highlight.pack.js");
271 immutable string stylecss = import("style.css");
272 immutable string indexhtml = import("index.html");
273 immutable string searchjs = import("search.js");