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