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