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