1 module ddox.main;
2 
3 import ddox.ddoc;
4 import ddox.ddox;
5 import ddox.entities;
6 import ddox.htmlgenerator;
7 import ddox.htmlserver;
8 import ddox.jsonparser;
9 import ddox.jsonparser_old;
10 
11 import vibe.core.core;
12 import vibe.core.file;
13 import vibe.data.json;
14 import vibe.http.fileserver;
15 import vibe.http.router;
16 import vibe.http.server;
17 import vibe.stream.operations;
18 import std.array;
19 import std.exception : enforce;
20 import std.file;
21 import std.getopt;
22 import std.stdio;
23 import std.string;
24 
25 
26 int ddoxMain(string[] args)
27 {
28 	if( args.length < 2 ){
29 		showUsage(args);
30 		return 1;
31 	}
32 
33 	switch( args[1] ){
34 		default: showUsage(args); return 1;
35 		case "generate-html": return cmdGenerateHtml(args);
36 		case "serve-html": return cmdServeHtml(args);
37 		case "filter": return cmdFilterDocs(args);
38 	}
39 }
40 
41 int cmdGenerateHtml(string[] args)
42 {
43 	GeneratorSettings gensettings;
44 	Package pack;
45 	if( auto ret = setupGeneratorInput(args, gensettings, pack) )
46 		return ret;
47 
48 	generateHtmlDocs(Path(args[3]), pack, gensettings);
49 	return 0;
50 }
51 
52 int cmdServeHtml(string[] args)
53 {
54 	string[] webfiledirs;
55 	getopt(args,
56 		config.passThrough,
57 		"web-file-dir", &webfiledirs);
58 
59 	GeneratorSettings gensettings;
60 	Package pack;
61 	if( auto ret = setupGeneratorInput(args, gensettings, pack) )
62 		return ret;
63 
64 	// register the api routes and start the server
65 	auto router = new URLRouter;
66 	registerApiDocs(router, pack, gensettings);
67 
68 	foreach (dir; webfiledirs)
69 		router.get("*", serveStaticFiles(dir));
70 
71 	writefln("Listening on port 8080...");
72 	auto settings = new HTTPServerSettings;
73 	settings.port = 8080;
74 	listenHTTP(settings, router);
75 
76 	return runEventLoop();
77 }
78 
79 int setupGeneratorInput(ref string[] args, out GeneratorSettings gensettings, out Package pack)
80 {
81 	string[] macrofiles;
82 	string[] overridemacrofiles;
83 	NavigationType navtype;
84 	string[] pack_order;
85 	string sitemapurl = "http://127.0.0.1/";
86 	MethodStyle file_name_style = MethodStyle.unaltered;
87 	bool lowercasenames = false;
88 	getopt(args,
89 		//config.passThrough,
90 		"std-macros", &macrofiles,
91 		"override-macros", &overridemacrofiles,
92 		"navigation-type", &navtype,
93 		"package-order", &pack_order,
94 		"sitemap-url", &sitemapurl,
95 		"file-name-style", &file_name_style,
96 		"lowercase-names", &lowercasenames);
97 
98 	if (lowercasenames) file_name_style = MethodStyle.lowerCase;
99 
100 	if( args.length < 3 ){
101 		showUsage(args);
102 		return 1;
103 	}
104 
105 	setDefaultDdocMacroFiles(macrofiles);
106 	setOverrideDdocMacroFiles(overridemacrofiles);
107 
108 	// parse the json output file
109 	auto docsettings = new DdoxSettings;
110 	docsettings.packageOrder = pack_order;
111 	pack = parseDocFile(args[2], docsettings);
112 
113 	gensettings = new GeneratorSettings;
114 	gensettings.siteUrl = URL(sitemapurl);
115 	gensettings.navigationType = navtype;
116 	gensettings.fileNameStyle = file_name_style;
117 	return 0;
118 }
119 
120 int cmdFilterDocs(string[] args)
121 {
122 	string[] excluded, included;
123 	Protection minprot = Protection.Private;
124 	bool keeputests = false;
125 	bool keepinternals = false;
126 	bool unittestexamples = true;
127 	bool nounittestexamples = false;
128 	bool justdoc = false;
129 	getopt(args,
130 		//config.passThrough,
131 		"ex", &excluded,
132 		"in", &included,
133 		"min-protection", &minprot,
134 		"only-documented", &justdoc,
135 		"keep-unittests", &keeputests,
136 		"keep-internals", &keepinternals,
137 		"unittest-examples", &unittestexamples, // deprecated, kept to not break existing scripts
138 		"no-unittest-examples", &nounittestexamples);
139 
140 	if (keeputests) keepinternals = true;
141 	if (nounittestexamples) unittestexamples = false;
142 
143 	string jsonfile;
144 	if( args.length < 3 ){
145 		showUsage(args);
146 		return 1;
147 	}
148 
149 	Json filterProt(Json json, Json parent, Json last_decl, Json mod)
150 	{
151 		if (last_decl.type == Json.Type.undefined) last_decl = parent;
152 
153 		string templateName(Json j){
154 			auto n = j.name.opt!string();
155 			auto idx = n.indexOf('(');
156 			if( idx >= 0 ) return n[0 .. idx];
157 			return n;
158 		}
159 	
160 		if( json.type == Json.Type.Object ){
161 			auto comment = json.comment.opt!string().strip();
162 			if( justdoc && comment.empty ){
163 				if( parent.type != Json.Type.Object || parent.kind.opt!string() != "template" || templateName(parent) != json.name.opt!string() )
164 					return Json.undefined;
165 			}
166 			
167 			Protection prot = Protection.Public;
168 			if( auto p = "protection" in json ){
169 				switch(p.get!string){
170 					default: break;
171 					case "private": prot = Protection.Private; break;
172 					case "package": prot = Protection.Package; break;
173 					case "protected": prot = Protection.Protected; break;
174 				}
175 			}
176 			if( comment == "private" ) prot = Protection.Private;
177 			if( prot < minprot ) return Json.undefined;
178 
179 			auto name = json.name.opt!string();
180 			bool is_internal = name.startsWith("__");
181 			bool is_unittest = name.startsWith("__unittest");
182 			if (name.startsWith("_staticCtor") || name.startsWith("_staticDtor")) is_internal = true;
183 			else if (name.startsWith("_sharedStaticCtor") || name.startsWith("_sharedStaticDtor")) is_internal = true;
184 
185 			if (unittestexamples && is_unittest && "comment" in json) {
186 				assert(last_decl.type == Json.Type.object, "Don't have a last_decl context.");
187 				try {
188 					string source = extractUnittestSourceCode(json, mod);
189 					if (last_decl.comment.opt!string.empty) {
190 						writefln("Warning: Cannot add documented unit test %s to %s, which is not documented.", name, last_decl.name.opt!string);
191 					} else {
192 						last_decl.comment ~= format("Example:\n%s\n---\n%s\n---\n", comment, source);
193 					}
194 				} catch (Exception e) {
195 					writefln("Failed to add documented unit test %s:%s as example: %s",
196 						mod.file.get!string(), json["line"].get!long, e.msg);
197 					return Json.undefined;
198 				}
199 			}
200 			
201 			if (!keepinternals && is_internal) return Json.undefined;
202 
203 			if (!keeputests && is_unittest) return Json.undefined;
204 
205 			if (auto mem = "members" in json)
206 				json.members = filterProt(*mem, json, Json.undefined, mod);
207 		} else if( json.type == Json.Type.Array ){
208 			auto last_child_decl = Json.undefined;
209 			Json[] newmem;
210 			foreach (m; json) {
211 				auto mf = filterProt(m, parent, last_child_decl, mod);
212 				if (mf.type == Json.Type.undefined) continue;
213 				if (mf.type == Json.Type.object && !mf.name.opt!string.startsWith("__unittest") && mf.comment.opt!string.strip != "ditto")
214 					last_child_decl = mf;
215 				newmem ~= mf;
216 			}
217 			return Json(newmem);
218 		}
219 		return json;
220 	}
221 
222 	writefln("Reading doc file...");
223 	auto text = readText(args[2]);
224 	int line = 1;
225 	writefln("Parsing JSON...");
226 	auto json = parseJson(text, &line);
227 
228 	writefln("Filtering modules...");
229 	Json[] dst;
230 	foreach (m; json) {
231 		if ("name" !in m) {
232 			writefln("No name for module %s - ignoring", m.file.opt!string);
233 			continue;
234 		}
235 		auto n = m.name.get!string;
236 		bool include = true;
237 		foreach (ex; excluded)
238 			if (n.startsWith(ex)) {
239 				include = false;
240 				break;
241 			}
242 		foreach (inc; included)
243 			if (n.startsWith(inc)) {
244 				include = true;
245 				break;
246 			}
247 		if (include) dst ~= filterProt(m, Json.undefined, Json.undefined, m);
248 	}
249 
250 	writefln("Writing filtered docs...");
251 	auto buf = appender!string();
252 	writePrettyJsonString(buf, Json(dst));
253 	std.file.write(args[2], buf.data());
254 
255 	return 0;
256 }
257 
258 Package parseDocFile(string filename, DdoxSettings settings)
259 {
260 	writefln("Reading doc file...");
261 	auto text = readText(filename);
262 	int line = 1;
263 	writefln("Parsing JSON...");
264 	auto json = parseJson(text, &line);
265 	writefln("Parsing docs...");
266 	Package root;
267 	if( settings.oldJsonFormat ) root = parseJsonDocsOld(json);
268 	else root = parseJsonDocs(json);
269 	writefln("Finished parsing docs.");
270 
271 	processDocs(root, settings);
272 	return root;
273 }
274 
275 void showUsage(string[] args)
276 {
277 	string cmd;
278 	if( args.length >= 2 ) cmd = args[1];
279 
280 	switch(cmd){
281 		default:
282 			writefln(
283 `Usage: %s <COMMAND> [args...]
284 
285     <COMMAND> can be one of:
286         generate-html
287         serve-html
288         filter
289 
290 Specifying only the command with no further arguments will print detailed usage
291 information.
292 `, args[0]);
293 			break;
294 		case "serve-html":
295 			writefln(
296 `Usage: %s serve-html <ddocx-input-file>
297     --std-macros=FILE      File containing DDOC macros that will be available
298     --override-macros=FILE File containing DDOC macros that will override local
299                            definitions (Macros: section)
300     --navigation-type=TYPE Change the type of navigation (ModuleList,
301                            ModuleTree, DeclarationTree)
302     --package-order=NAME   Causes the specified module to be ordered first. Can
303                            be specified multiple times.
304     --sitemap-url          Specifies the base URL used for sitemap generation
305     --web-file-dir=DIR     Make files from dir available on the served site
306 `, args[0]);
307 			break;
308 		case "generate-html":
309 			writefln(
310 `Usage: %s generate-html <ddocx-input-file> <output-dir>
311     --std-macros=FILE      File containing DDOC macros that will be available
312     --override-macros=FILE File containing DDOC macros that will override local
313                            definitions (Macros: section)
314     --navigation-type=TYPE Change the type of navigation (ModuleList,
315                            ModuleTree, DeclarationTree)
316     --package-order=NAME   Causes the specified module to be ordered first. Can
317                            be specified multiple times.
318     --sitemap-url          Specifies the base URL used for sitemap generation
319     --file-name-style=STY  Sets a translation style for symbol names to file
320                            names. Use this instead of --lowercase-name.
321                            Possible values for STY:
322                              unaltered, camelCase, pascalCase, lowerCase,
323                              upperCase, lowerUnderscored, upperUnderscored
324     --lowercase-names      DEPRECATED: Outputs all file names in lower case.
325                            This option is useful on case insensitive file
326                            systems.
327 `, args[0]);
328 			break;
329 		case "filter":
330 			writefln(
331 `Usage: %s filter <ddocx-input-file> [options]
332     --ex=PREFIX            Exclude modules with prefix
333     --in=PREFIX            Force include of modules with prefix
334     --min-protection=PROT  Remove items with lower protection level than
335                            specified.
336                            PROT can be: Public, Protected, Package, Private
337     --only-documented      Remove undocumented entities.
338     --keep-unittests       Do not remove unit tests from documentation.
339                            Implies --keep-internals.
340     --keep-internals       Do not remove symbols starting with two unterscores.
341     --unittest-examples    Add documented unit tests as examples to the
342                            preceeding declaration (deprecated, enabled by
343                            default)
344     --no-unittest-examples Don't convert documented unit tests to examples
345 `, args[0]);
346 	}
347 	if( args.length < 2 ){
348 	} else {
349 
350 	}
351 }
352 
353 private string extractUnittestSourceCode(Json decl, Json mod)
354 {
355 	auto filename = mod.file.get!string();
356 	enforce("line" in decl && "endline" in decl, "Missing line/endline fields.");
357 	auto from = decl["line"].get!long;
358 	auto to = decl.endline.get!long;
359 
360 	// read the matching lines out of the file
361 	auto app = appender!string();
362 	long lc = 1;
363 	foreach (str; File(filename).byLine) {
364 		if (lc >= from) {
365 			app.put(str);
366 			app.put('\n');
367 		}
368 		if (++lc > to) break;
369 	}
370 	auto ret = app.data;
371 
372 	// strip the "unittest { .. }" surroundings
373 	auto idx = ret.indexOf("unittest");
374 	enforce(idx >= 0, format("Missing 'unittest' for unit test at %s:%s.", filename, from));
375 	ret = ret[idx .. $];
376 
377 	idx = ret.indexOf("{");
378 	enforce(idx >= 0, format("Missing opening '{' for unit test at %s:%s.", filename, from));
379 	ret = ret[idx+1 .. $];
380 
381 	idx = ret.lastIndexOf("}");
382 	enforce(idx >= 0, format("Missing closing '}' for unit test at %s:%s.", filename, from));
383 	ret = ret[0 .. idx];
384 
385 	// unindent lines according to the indentation of the first line
386 	app = appender!string();
387 	string indent;
388 	foreach (i, ln; ret.splitLines) {
389 		if (i == 1) {
390 			foreach (j; 0 .. ln.length)
391 				if (ln[j] != ' ' && ln[j] != '\t') {
392 					indent = ln[0 .. j];
393 					break;
394 				}
395 		}
396 		if (i > 0 || ln.strip.length > 0) {
397 			size_t j = 0;
398 			while (j < indent.length && !ln.empty) {
399 				if (ln.front != indent[j]) break;
400 				ln.popFront();
401 				j++;
402 			}
403 			app.put(ln);
404 			app.put('\n');
405 		}
406 	}
407 	return app.data;
408 }