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 visitor;
8 
9 import ddoc.comments;
10 import std.algorithm;
11 import dparse.ast;
12 import dparse.formatter;
13 import dparse.lexer;
14 import std.file;
15 import std.path;
16 import std.stdio;
17 import std.typecons;
18 import unittest_preprocessor;
19 
20 
21 /**
22  * Generates documentation for a module.
23  */
24 class DocVisitor : ASTVisitor
25 {
26 	/**
27 	 * Params:
28 	 *     outputDirectory = The directory where files will be written
29 	 *     macros = Macro definitions used in processing documentation comments
30 	 *     searchIndex = A file where the search information will be written
31 	 *     unitTestMapping = The mapping of declaration addresses to their
32 	 *         documentation unittests
33 	 *     fileBytes = The source code of the module as a byte array.
34 	 */
35 	this(string outputDirectory, string[string] macros, File searchIndex,
36 		TestRange[][size_t] unitTestMapping, const(ubyte[]) fileBytes)
37 	{
38 		this.outputDirectory = outputDirectory;
39 		this.macros = macros;
40 		this.searchIndex = searchIndex;
41 		this.unitTestMapping = unitTestMapping;
42 		this.fileBytes = fileBytes;
43 	}
44 
45 	override void visit(const Module mod)
46 	{
47 		import std.array : array;
48 		import std.algorithm : map;
49 		import std.path : dirName;
50 		import std.range : chain, only, join;
51 		import std.file : mkdirRecurse;
52 		import std.conv : to;
53 
54 		if (mod.moduleDeclaration is null)
55 			return;
56 		pushAttributes();
57 		stack = cast(string[]) mod.moduleDeclaration.moduleName.identifiers.map!(a => a.text).array;
58 		baseLength = stack.length;
59 		moduleFileBase = chain(only(outputDirectory), stack).buildPath;
60 		if (!exists(moduleFileBase.dirName()))
61 			moduleFileBase.dirName().mkdirRecurse();
62 		File output = File(moduleFileBase ~ ".html", "w");
63 
64 		location = output.name;
65 		moduleName = to!string(stack.join("."));
66 		writeHeader(output, moduleName, baseLength - 1);
67 
68 		writeBreadcrumbs(output);
69 
70 		if (mod.moduleDeclaration.comment !is null)
71 			readAndWriteComment(output, mod.moduleDeclaration.comment, macros,
72 				prevComments, null, getUnittestDocTuple(mod.moduleDeclaration));
73 
74 		memberStack.length = 1;
75 
76 		mod.accept(this);
77 
78 		memberStack[$ - 1].write(output, outputDirectory);
79 
80 		output.writeln(HTML_END);
81 		output.close();
82 	}
83 
84 	override void visit(const EnumDeclaration ed)
85 	{
86 		enum formattingCode = q{
87 		f.write("enum ", ad.name.text);
88 		if (ad.type !is null)
89 		{
90 			f.write(" : ");
91 			formatter.format(ad.type);
92 		}
93 		};
94 		visitAggregateDeclaration!(formattingCode, "enums")(ed);
95 	}
96 
97 	override void visit(const EnumMember member)
98 	{
99 		if (member.comment is null)
100 			return;
101 		string summary = readAndWriteComment(File.init, member.comment, macros,
102 			prevComments, null, getUnittestDocTuple(member));
103 		memberStack[$ - 1].values ~= Item("#", member.name.text, summary);
104 	}
105 
106 	override void visit(const ClassDeclaration cd)
107 	{
108 		enum formattingCode = q{
109 		f.write("class ", ad.name.text);
110 		if (ad.baseClassList !is null)
111 			formatter.format(ad.baseClassList);
112 		if (ad.templateParameters !is null)
113 			formatter.format(ad.templateParameters);
114 		if (ad.constraint !is null)
115 			formatter.format(ad.constraint);
116 		};
117 		visitAggregateDeclaration!(formattingCode, "classes")(cd);
118 	}
119 
120 	override void visit(const TemplateDeclaration td)
121 	{
122 		enum formattingCode = q{
123 		f.write("template ", ad.name.text);
124 		if (ad.templateParameters !is null)
125 			formatter.format(ad.templateParameters);
126 		if (ad.constraint)
127 			formatter.format(ad.constraint);
128 		};
129 		visitAggregateDeclaration!(formattingCode, "templates")(td);
130 	}
131 
132 	override void visit(const StructDeclaration sd)
133 	{
134 		enum formattingCode = q{
135 		f.write("struct ", ad.name.text);
136 		if (ad.templateParameters)
137 			formatter.format(ad.templateParameters);
138 		if (ad.constraint)
139 			formatter.format(ad.constraint);
140 		};
141 		visitAggregateDeclaration!(formattingCode, "structs")(sd);
142 	}
143 
144 	override void visit(const InterfaceDeclaration id)
145 	{
146 		enum formattingCode = q{
147 		if (ad.baseClassList !is null)
148 			formatter.format(ad.baseClassList);
149 		if (ad.templateParameters !is null)
150 			formatter.format(ad.templateParameters);
151 		if (ad.constraint !is null)
152 			formatter.format(ad.constraint);
153 		};
154 		visitAggregateDeclaration!(formattingCode, "interfaces")(id);
155 	}
156 
157 	override void visit(const AliasDeclaration ad)
158 	{
159 		import std.path : dirSeparator;
160 		if (ad.comment is null)
161 			return;
162 		bool first;
163 		if (ad.identifierList !is null)
164 		{
165 			foreach (name; ad.identifierList.identifiers)
166 			{
167 				File f = pushSymbol(name.text, first);
168 				scope(exit) popSymbol(f);
169 				writeBreadcrumbs(f);
170 				string type = writeAliasType(f, name.text, ad.type);
171 				string summary = readAndWriteComment(f, ad.comment, macros, prevComments);
172 				memberStack[$ - 2].aliases ~= Item(f.name, name.text, summary, type);
173 			}
174 		}
175 		else foreach (initializer; ad.initializers)
176 		{
177 			File f = pushSymbol(initializer.name.text, first);
178 			scope(exit) popSymbol(f);
179 			writeBreadcrumbs(f);
180 			string type = writeAliasType(f, initializer.name.text, initializer.type);
181 			string summary = readAndWriteComment(f, ad.comment, macros, prevComments);
182 			memberStack[$ - 2].aliases ~= Item(f.name, initializer.name.text, summary, type);
183 		}
184 	}
185 
186 	override void visit(const VariableDeclaration vd)
187 	{
188 		bool first;
189 		foreach (const Declarator dec; vd.declarators)
190 		{
191 			if (dec.comment is null)
192 				continue;
193 			File f = pushSymbol(dec.name.text, first);
194 			scope(exit) popSymbol(f);
195 			writeBreadcrumbs(f);
196 			string summary = readAndWriteComment(f, dec.comment, macros, prevComments);
197 			memberStack[$ - 2].variables ~= Item(f.name, dec.name.text, summary, formatNode(vd.type));
198 		}
199 		if (vd.comment !is null && vd.autoDeclaration !is null) foreach (ident; vd.autoDeclaration.identifiers)
200 		{
201 			File f = pushSymbol(ident.text, first);
202 			scope(exit) popSymbol(f);
203 			writeBreadcrumbs(f);
204 			string summary = readAndWriteComment(f, vd.comment, macros, prevComments);
205 			string storageClass;
206 			foreach (storage; vd.storageClasses)
207 			{
208 				if (storage !is null)
209 					storageClass = str(storage.token.type);
210 			}
211 			auto i = Item(f.name, ident.text,
212 				summary, storageClass == "enum" ? null : "auto");
213 			if (storageClass == "enum")
214 				memberStack[$ - 2].enums ~= i;
215 			else
216 				memberStack[$ - 2].variables ~= i;
217 		}
218 	}
219 
220 	override void visit(const StructBody sb)
221 	{
222 		pushAttributes();
223 		sb.accept(this);
224 		popAttributes();
225 	}
226 
227 	override void visit(const BlockStatement bs)
228 	{
229 		pushAttributes();
230 		bs.accept(this);
231 		popAttributes();
232 	}
233 
234 	override void visit(const Declaration dec)
235 	{
236 		attributes[$ - 1] ~= dec.attributes;
237 		dec.accept(this);
238 		if (dec.attributeDeclaration is null)
239 			attributes[$ - 1] = attributes[$ - 1][0 .. $ - dec.attributes.length];
240 	}
241 
242 	override void visit(const AttributeDeclaration dec)
243 	{
244 		attributes[$ - 1] ~= dec.attribute;
245 	}
246 
247 	override void visit(const Constructor cons)
248 	{
249 		if (cons.comment is null)
250 			return;
251 		bool first;
252 		File f = pushSymbol("this", first);
253 		writeFnDocumentation(f, cons, attributes[$ - 1], first);
254 	}
255 
256 	override void visit(const FunctionDeclaration fd)
257 	{
258 		if (fd.comment is null)
259 			return;
260 		bool first;
261 		File f = pushSymbol(fd.name.text, first);
262 		writeFnDocumentation(f, fd, attributes[$ - 1], first);
263 	}
264 
265 	// Deliberately skip unittests
266 	override void visit(const Unittest) {}
267 
268 	alias visit = ASTVisitor.visit;
269 
270 	/// The module name in "package.package.module" format.
271 	string moduleName;
272 
273 	/// The path to the HTML file that was generated for the module being
274 	/// processed.
275 	string location;
276 
277 	invariant
278 	{
279 		foreach (c; prevComments[])
280 		{
281 			assert(c.sections.length >= 2);
282 		}
283 	}
284 
285 private:
286 
287 	void visitAggregateDeclaration(string formattingCode, string name, A)(const A ad)
288 	{
289 		bool first;
290 		if (ad.comment is null)
291 			return;
292 		File f = pushSymbol(ad.name.text, first);
293 		if (first)
294 			writeBreadcrumbs(f);
295 		else
296 			f.writeln("<hr/>");
297 		{
298 			auto writer = f.lockingTextWriter();
299 			writer.put(`<pre><code>`);
300 			auto formatter = new Formatter!(File.LockingTextWriter)(writer);
301 			scope(exit) formatter.sink = File.LockingTextWriter.init;
302 			writeAttributes(formatter, writer, attributes[$ - 1]);
303 			mixin(formattingCode);
304 			writer.put("\n</code></pre>");
305 		}
306 		string summary = readAndWriteComment(f, ad.comment, macros, prevComments,
307 			null, getUnittestDocTuple(ad));
308 		mixin(`memberStack[$ - 2].` ~ name ~ ` ~= Item(f.name, ad.name.text, summary);`);
309 		ad.accept(this);
310 		memberStack[$ - 1].write(f, outputDirectory);
311 
312 		stack = stack[0 .. $ - 1];
313 		memberStack = memberStack[0 .. $ - 1];
314 	}
315 
316 	/**
317 	 * Params:
318 	 *     t = The declaration.
319 	 * Returns: An array of tuples where the first item is the contents of the
320 	 *     unittest block and the second item is the doc comment for the
321 	 *     unittest block. This array may be empty.
322 	 */
323 	Tuple!(string, string)[] getUnittestDocTuple(T)(const T t)
324 	{
325 		immutable size_t index = cast(size_t) (cast(void*) t);
326 //		writeln("Searching for unittest associated with ", index);
327 		auto tupArray = index in unitTestMapping;
328 		if (tupArray is null)
329 			return [];
330 //		writeln("Found a doc unit test for ", cast(size_t) &t);
331 		Tuple!(string, string)[] rVal;
332 		foreach (tup; *tupArray)
333 			rVal ~= tuple(cast(string) fileBytes[tup[0] + 2 .. tup[1]], tup[2]);
334 		return rVal;
335 	}
336 
337 	/**
338 	 *
339 	 */
340 	void writeFnDocumentation(Fn)(File f, Fn fn, const(Attribute)[] attrs, bool first)
341 	{
342 		auto writer = f.lockingTextWriter();
343 		if (first)
344 			writeBreadcrumbs(f);
345 		else
346 			writer.put("<hr/>");
347 		auto formatter = new Formatter!(File.LockingTextWriter)(writer);
348 		scope(exit) formatter.sink = File.LockingTextWriter.init;
349 		writer.put(`<pre><code>`);
350 		writeAttributes(formatter, writer, attrs);
351 		static if (__traits(hasMember, typeof(fn), "returnType"))
352 		{
353 			if (fn.returnType)
354 			{
355 				formatter.format(fn.returnType);
356 				writer.put(" ");
357 			}
358 			formatter.format(fn.name);
359 		}
360 		else
361 			writer.put("this");
362 		if (fn.templateParameters !is null)
363 			formatter.format(fn.templateParameters);
364 		if (fn.parameters !is null)
365 			formatter.format(fn.parameters);
366 		foreach (a; fn.memberFunctionAttributes)
367 		{
368 			writer.put(" ");
369 			formatter.format(a);
370 		}
371 		if (fn.constraint)
372 		{
373 			writer.put(" ");
374 			formatter.format(fn.constraint);
375 		}
376 		writer.put("\n</code></pre>");
377 		string summary = readAndWriteComment(f, fn.comment, macros,
378 			prevComments, fn.functionBody, getUnittestDocTuple(fn));
379 		string fdName;
380 		static if (__traits(hasMember, typeof(fn), "name"))
381 			fdName = fn.name.text;
382 		else
383 			fdName = "this";
384 		memberStack[$ - 2].functions ~= Item(f.name, fdName, summary);
385 		fn.accept(this);
386 		stack = stack[0 .. $ - 1];
387 		memberStack = memberStack[0 .. $ - 1];
388 	}
389 
390 	/**
391 	 * Writes attributes to the given writer using the given formatter.
392 	 * Params:
393 	 *     F = The formatter type
394 	 *     W = The writer type
395 	 *     formatter = The formatter instance to use
396 	 *     writer = The writer that will be output to.
397 	 *     attrs = The attributes to write.
398 	 */
399 	void writeAttributes(F, W)(F formatter, W writer, const(Attribute)[] attrs)
400 	{
401 		IdType protection;
402 		if (attrs is null)
403 			attrs = attributes[$ - 1];
404 		if (attributes.length > 0) foreach (a; attrs)
405 		{
406 			if (isProtection(a.attribute.type))
407 				protection = a.attribute.type;
408 		}
409 		switch (protection)
410 		{
411 		case tok!"private": writer.put("private "); break;
412 		case tok!"package": writer.put("package "); break;
413 		default: writer.put("public "); break;
414 		}
415 		if (attributes.length > 0) foreach (a; attrs)
416 		{
417 			if (!isProtection(a.attribute.type))
418 			{
419 				formatter.format(a);
420 				writer.put(" ");
421 			}
422 		}
423 	}
424 
425 	/**
426 	 * Formats an AST node to a string
427 	 */
428 	static string formatNode(T)(const T t)
429 	{
430 		import std.array;
431 		auto writer = appender!string();
432 		auto formatter = new Formatter!(typeof(writer))(writer);
433 		formatter.format(t);
434 		return writer.data;
435 	}
436 
437 	/**
438 	 * Writes an alias' type to the given file and returns it.
439 	 * Params:
440 	 *     f = The file to write to
441 	 *     name = the name of the alias
442 	 *     t = the aliased type
443 	 * Returns: A string reperesentation of the given type.
444 	 */
445 	static string writeAliasType(File f, string name, const Type t)
446 	{
447 		if (t is null)
448 			return null;
449 		f.write(`<pre><code>`);
450 		f.write("alias ", name, " = ");
451 		string formatted = formatNode(t);
452 		f.write(formatted);
453 		f.writeln(`</code></pre>`);
454 		return formatted;
455 	}
456 
457 	/**
458 	 * Writes navigation breadcrumbs in HTML format to the given file.
459 	 */
460 	void writeBreadcrumbs(File f)
461 	{
462 		import std.array : join;
463 		import std.conv : to;
464 		import std.range : chain, only;
465 		f.writeln(`<div class="breadcrumbs">`);
466 		f.writeln(`<table id="results"></table>`);
467 		f.writeln(`<input type="search" id="search" placeholder="Search" onkeyup="searchSubmit(this.value, event)"/>`);
468 		foreach (i; 0 .. stack.length)
469 		{
470 			if (i + 1 < stack.length)
471 			{
472 				if (i >= baseLength - 1)
473 				{
474 					string link = buildPath(chain(stack[0 .. baseLength - 1],
475 						only(stack[baseLength - 1.. $ - 1].join("."))));
476 //					writeln(link, ".html");
477 					f.write(`<a href="`, stripLeadingDirectory(link, outputDirectory), `.html">`);
478 					f.write(stack[i]);
479 					f.write(`</a>.`);
480 				}
481 				else
482 				{
483 					f.write(stack[i]);
484 					f.write(".");
485 				}
486 			}
487 			else
488 				f.write(stack[i]);
489 		}
490 		f.writeln(`</div>`);
491 	}
492 
493 	/**
494 	 * Params:
495 	 *     name = The symbol's name
496 	 *     first = True if this is the first time that pushSymbol has been
497 	 *         called for this name.
498 	 *     isFunction = True if the symbol being pushed is a function, false
499 	 *         otherwise.
500 	 * Returns: A file that the symbol's documentation should be written to.
501 	 */
502 	File pushSymbol(string name, ref bool first)
503 	{
504 		import std.array : array, join;
505 		import std.string : format;
506 		stack ~= name;
507 		memberStack.length = memberStack.length + 1;
508 		string classDocFileName = format("%s.%s.html", moduleFileBase,
509 			join(stack[baseLength .. $], ".").array);
510 		immutable string path = stripLeadingDirectory(classDocFileName, outputDirectory);
511 		searchIndex.writefln(`{"%s" : "%s"},`, join(stack, ".").array, path);
512 		immutable size_t i = memberStack.length - 2;
513 		assert (i < memberStack.length, "%s %s".format(i, memberStack.length));
514 		auto p = classDocFileName in memberStack[i].overloadFiles;
515 		first = p is null;
516 		if (first)
517 		{
518 			first = true;
519 			auto f = File(classDocFileName, "w");
520 			memberStack[i].overloadFiles[classDocFileName] = f;
521 			writeHeader(f, name, baseLength - 1);
522 			return f;
523 		}
524 		else
525 			return *p;
526 	}
527 
528 	void popSymbol(File f)
529 	{
530 		f.writeln(HTML_END);
531 		stack = stack[0 .. $ - 1];
532 		memberStack = memberStack[0 .. $ - 1];
533 	}
534 
535 	void pushAttributes()
536 	{
537 		attributes.length = attributes.length + 1;
538 	}
539 
540 	void popAttributes()
541 	{
542 		attributes = attributes[0 .. $ - 1];
543 	}
544 
545 	const(Attribute)[][] attributes;
546 	Comment[] prevComments;
547 	size_t baseLength;
548 	string outputDirectory;
549 	string moduleFileBase;
550 	string[] stack;
551 	string[string] macros;
552 	Members[] memberStack;
553 	File searchIndex;
554 	TestRange[][size_t] unitTestMapping;
555 	const(ubyte[]) fileBytes;
556 }
557 
558 /**
559  * Writes HTML header information to the given file.
560  * Params:
561  *     f = The file to write to
562  *     title = The content of the HTML "title" element
563  *     depth = The directory depth of the file. This is used for ensuring that
564  *         the "base" element is correct so that links resolve properly.
565  */
566 void writeHeader(File f, string title, size_t depth)
567 {
568 	f.write(`<!DOCTYPE html>
569 <html>
570 <head>
571 <meta charset="utf-8"/>
572 <link rel="stylesheet" type="text/css" href="`);
573 	foreach (i; 0 .. depth)
574 		f.write("../");
575 	f.write(`style.css"/><script src="`);
576 	foreach (i; 0 .. depth)
577 		f.write("../");
578 	f.write(`highlight.pack.js"></script>
579 <title>`);
580 	f.write(title);
581 	f.writeln(`</title>`);
582 	f.write(`<base href="`);
583 	foreach (i; 0 .. depth)
584 		f.write("../");
585 	f.write(`"/>
586 <script src="search.js"></script>
587 </head>
588 <body>`);
589 }
590 
591 /**
592  * Writes a doc comment to the given file and returns the summary text.
593  * Params:
594  *     f = The file to write the comment to
595  *     comment = The comment to write
596  *     macros = Macro definitions used in processing the comment
597  *     prevComments = Previously encountered comments. This is used for handling
598  *         "ditto" comments. May be null.
599  *     functionBody = A function body used for writing contract information. May
600  *         be null.
601  *     testdocs = Pairs of unittest bodies and unittest doc comments. May be null.
602  * Returns: the summary from the given comment
603  */
604 string readAndWriteComment(File f, string comment, ref string[string] macros,
605 	ref Comment[] prevComments, const FunctionBody functionBody = null,
606 	Tuple!(string, string)[] testDocs = null)
607 {
608 	assert(comment !is null);
609 
610 	Comment c = parseComment(comment, macros);
611 	assert(c.sections.length >= 2);
612 	if (c.isDitto)
613 	{
614 		if (prevComments.length)
615 			c = prevComments[$ - 1];
616 		else
617 			stderr.writeln("Found 'ditto' comment without preceding comment to copy.");
618 	}
619 	else
620 		prevComments ~= c;
621 
622 	if (f != File.init)
623 		writeComment(f, c, functionBody);
624 	string rVal = "";
625 	if (c.sections.length && c.sections[0].content !is null)
626 		rVal = c.sections[0].content;
627 	else
628 	{
629 		foreach (section; c.sections)
630 		{
631 			if (section.name == "Returns")
632 				rVal = "Returns: " ~ section.content;
633 		}
634 	}
635 	if (f != File.init && testDocs !is null) foreach (doc; testDocs)
636 	{
637 //		writeln("Writing a unittest doc comment");
638 		import std.string : outdent;
639 		f.writeln(`<div class="section"><h3>Example</h3>`);
640 		Comment dc = parseComment(doc[1], macros);
641 		writeComment(f, dc);
642 		f.writeln(`<pre><code>`, outdent(doc[0]), `</code></pre>`);
643 		f.writeln(`</div>`);
644 	}
645 	return rVal;
646 }
647 /**
648  * Returns: the input string with its first directory removed.
649  */
650 string stripLeadingDirectory(string s, string outputDirectory)
651 {
652 	import std.path : pathSplitter, buildPath;
653 
654 	auto r1 = pathSplitter(s);
655 	auto r2 = pathSplitter(outputDirectory);
656 	while (!r1.empty && !r2.empty && r1.front == r2.front)
657 	{
658 		r1.popFront();
659 		r2.popFront();
660 	}
661 	return buildPath(r1);
662 }
663 
664 ///
665 unittest
666 {
667 	import std.stdio : stderr;
668 
669 	immutable a = stripLeadingDirectory("foo/bar/baz/", "foo");
670 	assert(a == "bar/baz", a);
671 	immutable b = stripLeadingDirectory("/home/user/project/docs", "/home/user/project/docs");
672 	assert(b == "", b);
673 	immutable c = stripLeadingDirectory("/home/user/project/docs/file.name.html",
674 		"/home/user/project/docs");
675 	assert(c == "file.name.html", c);
676 }
677 
678 private:
679 
680 void writeComment(File f, Comment comment, const FunctionBody functionBody = null)
681 in
682 {
683 	assert(comment.sections.length >= 2);
684 }
685 body
686 {
687 	foreach (i; 0 .. 2)
688 	{
689 		if (comment.sections[i].content is null)
690 			continue;
691 		f.writeln(`<div class="section">`);
692 		f.writeln(comment.sections[i].content);
693 		f.writeln(`</div>`);
694 	}
695 	if (functionBody !is null)
696 		writeContracts(f, functionBody.inStatement, functionBody.outStatement);
697 	foreach (section; comment.sections[2 .. $])
698 	{
699 		if (section.name == "Macros")
700 			continue;
701 		f.writeln(`<div class="section">`);
702 		if (section.name != "Summary" && section.name != "Description")
703 		{
704 			f.write("<h3>");
705 			f.write(prettySectionName(section.name));
706 			f.writeln("</h3>");
707 		}
708 		if (section.name == "Params")
709 		{
710 			f.writeln(`<table class="params">`);
711 			foreach (kv; section.mapping)
712 			{
713 				f.write(`<tr class="param"><td class="paramName">`);
714 				f.write(kv[0]);
715 				f.write(`</td><td class="paramDoc">`);
716 				f.write(kv[1]);
717 				f.writeln("</td></tr>");
718 			}
719 			f.write("</table>");
720 		}
721 		else
722 		{
723 			f.writeln(section.content);
724 		}
725 		f.writeln(`</div>`);
726 	}
727 }
728 
729 void writeContracts(File f, const InStatement inStatement,
730 	const OutStatement outStatement)
731 {
732 	if (inStatement is null && outStatement is null)
733 		return;
734 	f.write(`<div class="section"><h3>Contracts</h3><pre><code>`);
735 	auto formatter = new Formatter!(File.LockingTextWriter)(f.lockingTextWriter());
736 	scope(exit) formatter.sink = File.LockingTextWriter.init;
737 	if (inStatement !is null)
738 	{
739 		formatter.format(inStatement);
740 		if (outStatement !is null)
741 			f.writeln();
742 	}
743 	if (outStatement !is null)
744 		formatter.format(outStatement);
745 	f.writeln("</code></pre></div>");
746 }
747 
748 string prettySectionName(string sectionName)
749 {
750 	switch (sectionName)
751 	{
752 	case "See_also": return "See Also";
753 	case "Params": return "Parameters";
754 	default: return sectionName;
755 	}
756 }
757 
758 enum HTML_END = `
759 <script>hljs.initHighlightingOnLoad();</script>
760 </body>
761 </html>`;
762 
763 struct Item
764 {
765 	string url;
766 	string name;
767 	string summary;
768 	string type;
769 
770 	void write(File f, string outputDirectory)
771 	{
772 		f.write(`<tr><td>`);
773 		if (url == "#")
774 			f.write(name, `</td>`);
775 		else
776 			f.write(`<a href="`, stripLeadingDirectory(url, outputDirectory), `">`, name, `</a></td>`);
777 		if (type is null)
778 			f.write(`<td></td><td>`, summary ,`</td></tr>`);
779 		else
780 			f.write(`<td><pre><code>`, type, `</code></pre></td><td>`, summary ,`</td></tr>`);
781 	}
782 }
783 
784 struct Members
785 {
786 	File[string] overloadFiles;
787 	Item[] aliases;
788 	Item[] classes;
789 	Item[] enums;
790 	Item[] functions;
791 	Item[] interfaces;
792 	Item[] structs;
793 	Item[] templates;
794 	Item[] values;
795 	Item[] variables;
796 
797 	void write(File f, string outputDirectory)
798 	{
799 		if (aliases.length == 0 && classes.length == 0 && enums.length == 0
800 			&& functions.length == 0 && interfaces.length == 0
801 			&& structs.length == 0 && templates.length == 0 && values.length == 0
802 			&& variables.length == 0)
803 		{
804 			return;
805 		}
806 		f.writeln(`<div class="section">`);
807 		if (enums.length > 0)
808 			write(f, enums, "Enums", outputDirectory);
809 		if (aliases.length > 0)
810 			write(f, aliases, "Aliases", outputDirectory);
811 		if (variables.length > 0)
812 			write(f, variables, "Variables", outputDirectory);
813 		if (functions.length > 0)
814 			write(f, functions, "Functions", outputDirectory);
815 		if (structs.length > 0)
816 			write(f, structs, "Structs", outputDirectory);
817 		if (interfaces.length > 0)
818 			write(f, interfaces, "Interfaces", outputDirectory);
819 		if (classes.length > 0)
820 			write(f, classes, "Classes", outputDirectory);
821 		if (templates.length > 0)
822 			write(f, templates, "Templates", outputDirectory);
823 		if (values.length > 0)
824 			write(f, values, "Values", outputDirectory);
825 		f.writeln(`</div>`);
826 		foreach (f; overloadFiles)
827 		{
828 			f.writeln(HTML_END);
829 			f.close();
830 		}
831 	}
832 
833 private:
834 
835 	void write(File f, Item[] items, string name, string outputDirectory)
836 	{
837 		f.writeln(`<h3>`, name, `</h3>`);
838 		f.writeln(`<table>`);
839 		foreach (i; items)
840 			i.write(f, outputDirectory);
841 		f.writeln(`</table>`);
842 	}
843 }