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