1 /**
2  * D Documentation Generator
3  * Copyright: © 2014 Economic Modeling Specialists, Intl., © 2015 Ferdinand Majerech
4  * Authors: Brian Schott, Ferdinand Majerech
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
6  */
7 module writer;
8 
9 import config;
10 import ddoc.comments;
11 import formatter;
12 import std.algorithm;
13 import std.array : appender, empty, array, back, popBack;
14 import std.conv : to;
15 import dparse.ast;
16 import std.file : exists, mkdirRecurse;
17 import std.path : buildPath, setExtension;
18 import std.stdio;
19 import std.string : format, outdent, split;
20 import std.typecons;
21 import symboldatabase;
22 import tocbuilder : TocItem;
23 import item : Item;
24 
25 // Only used for shared implementation, not interface (could probably use composition too)
26 private class HTMLWriterBase(alias symbolLink)
27 {
28 	/** Construct a HTMLWriter.
29 	 *
30 	 * Params:
31 	 *
32 	 * config         = Configuration data, including macros and the output directory.
33 	 *                  A non-const reference is needed because libddoc wants
34 	 *                  a non-const reference to macros for parsing comments, even
35 	 *                  though it doesn't modify the macros.
36 	 * searchIndex    = A file where the search information will be written
37 	 * tocItems       = Items of the table of contents to write into each documentation file.
38 	 * tocAdditionals = Additional pieces of content for the table of contents sidebar.
39 	 */
40 	this(ref Config config, File searchIndex,
41 		 TocItem[] tocItems, string[] tocAdditionals)
42 	{
43 		this.config         = &config;
44 		this.macros         = config.macros;
45 		this.searchIndex    = searchIndex;
46 		this.tocItems       = tocItems;
47 		this.tocAdditionals = tocAdditionals;
48 		this.processCode    = &processCodeDefault;
49 	}
50 
51 	/** Get a link to the module for which we're currently writing documentation.
52 	 *
53 	 * See_Also: `prepareModule`
54 	 */
55 	final string moduleLink() { return moduleLink_; }
56 
57 	/** Get a link to a module.
58 	 *
59 	 * Note: this does not check if the module exists; calling moduleLink() for a
60 	 * nonexistent or undocumented module will return a link to a nonexistent file.
61 	 *
62 	 * Params: moduleNameParts = Name of the module containing the symbols, as an array
63 	 *                           of parts (e.g. ["std", "stdio"])
64 	 */
65 	final string moduleLink(const string[] moduleNameParts)
66 	{
67 		return moduleNameParts.buildPath.setExtension("html");
68 	}
69 
70 	final size_t moduleNameLength() { return moduleNameLength_; }
71 
72 	/** Prepare for writing documentation for symbols in specified module.
73 	 *
74 	 * Initializes module-related file paths and creates the directory to write
75 	 * documentation of module members into.
76 	 *
77 	 * Params: moduleNameParts = Parts of the module name, without the dots.
78 	 */
79 	final void prepareModule(string[] moduleNameParts)
80 	{
81 		moduleFileBase_   = moduleNameParts.buildPath;
82 		moduleLink_       = moduleLink(moduleNameParts);
83 		moduleNameLength_ = moduleNameParts.length;
84 
85 		// Not really absolute, just relative to working, not output, directory
86 		const moduleFileBaseAbs = config.outputDirectory.buildPath(moduleFileBase_);
87 		// Create directory to write documentation for module members.
88 		if (!moduleFileBaseAbs.exists) { moduleFileBaseAbs.mkdirRecurse(); }
89 		assert(symbolFileStack.empty,
90 			   "prepareModule called before finishing previous module?");
91 		// Need a "parent" in the stack that will contain the module File
92 		symbolFileStack.length = 1;
93 	}
94 
95 	/** Finish writing documentation for current module.
96 	 *
97 	 * Must be called to ensure any open files are closed.
98 	 */
99 	final void finishModule()
100 	{
101 		moduleFileBase_  = null;
102 		moduleLink_      = null;
103 		moduleNameLength_ = 0;
104 		popSymbol();
105 	}
106 
107 	/** Writes HTML header information to the given range.
108 	 *
109 	 * Params:
110 	 *
111 	 * dst   = Range to write to
112 	 * title = The content of the HTML "title" element
113 	 * depth = The directory depth of the file. This is used for ensuring that
114 	 *         the "base" element is correct so that links resolve properly.
115 	 */
116 	void writeHeader(R)(ref R dst, string title, size_t depth)
117 	{
118 		import std.range: repeat;
119 		const rootPath = "../".repeat(depth).joiner.array;
120 		dst.put(
121 `<!DOCTYPE html>
122 <html>
123 <head>
124 <meta charset="utf-8"/>
125 <link rel="stylesheet" type="text/css" href="%sstyle.css"/>
126 <script src="%shighlight.pack.js"></script>
127 <script src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.2.2/anchor.min.js"></script>
128 <title>%s</title>
129 <base href="%s"/>
130 <script src="search.js"></script>
131 <script src="show_hide.js"></script>
132 </head>
133 <body>
134 <div class="main">
135 `.format(rootPath, rootPath, title, rootPath));
136 	}
137 
138 	/** Write the main module list (table of module links and descriptions).
139 	 *
140 	 * Written to the main page.
141 	 *
142 	 * Params:
143 	 *
144 	 * dst      = Range to write to.
145 	 * database = Symbol database aware of all modules.
146 	 *
147 	 */
148 	void writeModuleList(R)(ref R dst, SymbolDatabase database)
149 	{
150 		writeln("writeModuleList called");
151 
152 		void put(string str) { dst.put(str); dst.put("\n"); }
153 
154 		writeSection(dst,
155 		{
156 			// Sort the names by alphabet
157 			// duplicating should be cheap here; there is only one module list
158 			import std.algorithm: sort;
159 			auto sortedModuleNames = sort(database.moduleNames.dup);
160 			dst.put(`<h2>Module list</h2>`);
161 			put(`<table class="module-list">`);
162 			foreach (name; sortedModuleNames)
163 			{
164 				dst.put(`<tr><td class="module-name">`);
165 				writeLink(dst, database.moduleNameToLink[name],
166 						  { dst.put(name); });
167 				dst.put(`</td><td>`);
168 				dst.put(processMarkdown(database.moduleData(name).summary));
169 				put("</td></tr>");
170 			}
171 			put(`</table>`);
172 		} , "imports");
173 
174 	}
175 
176 	/** Writes the table of contents to provided range.
177 	 *
178 	 * Also starts the "content" <div>; must be called after writeBreadcrumbs(),
179 	 * before writing main content.
180 	 *
181 	 * Params:
182 	 *
183 	 * dst        = Range to write to.
184 	 * moduleName = Name of the module or package documentation page of which we're
185 	 *              writing the TOC for.
186 	 */
187 	void writeTOC(R)(ref R dst, string moduleName = "")
188 	{
189 		void put(string str) { dst.put(str); dst.put("\n"); }
190 		const link = moduleName ? moduleLink(moduleName.split(".")) : "";
191 		put(`<div class="sidebar">`);
192 		// Links allowing to show/hide the TOC.
193 		put(`<a href="%s#hide-toc" class="hide" id="hide-toc">&#171;</a>`.format(link));
194 		put(`<a href="%s#show-toc" class="show" id="show-toc">&#187;</a>`.format(link));
195 		put(`<div id="toc-id" class="toc">`);
196 		import std.range: retro;
197 		tocAdditionals.retro.each!((text)
198 		{
199 			put(`<div class="toc-additional">`);
200 			put(text);
201 			put(`</div>`);
202 		});
203 		writeList(dst, null,
204 		{
205 			// Buffering to scopeBuffer to avoid small file writes *and*
206 			// allocations
207 			import std.internal.scopebuffer : ScopeBuffer;
208 			char[1024 * 64] buf;
209 			auto scopeBuf = ScopeBuffer!char(buf);
210 			scope(exit) { scopeBuf.free(); }
211 
212 			tocItems.each!(t => t.write(scopeBuf, moduleName));
213 			dst.put(scopeBuf[]);
214 		});
215 		put(`</div></div>`);
216 		put(`<div class="content">`);
217 	}
218 
219 	/** Writes navigation breadcrumbs to the given range.
220 	 *
221 	 * For symbols, use the other writeBreadcrumbs overload.
222 	 *
223 	 * Params:
224 	 *
225 	 * dst     = Range (e.g. appender) to write to.
226 	 * heading = Page heading (e.g. module name or "Main Page").
227 	 */
228 	void writeBreadcrumbs(R)(ref R dst, string heading)
229 	{
230 		void put(string str) { dst.put(str); dst.put("\n"); }
231 		put(`<div class="breadcrumbs">`);
232 		put(`<table id="results"></table>`);
233 
234 		writeLink(dst, "index.html", { dst.put("⌂"); }, "home");
235 		put(`<input type="search" id="search" placeholder="Search" onkeyup="searchSubmit(this.value, event)"/>`);
236 		put(heading);
237 		put(`</div>`);
238 	}
239 
240 	/** Writes navigation breadcrumbs for a symbol's documentation file.
241 	 *
242 	 * Params:
243 	 *
244 	 * dst              = Range to write to.
245 	 * symbolStack      = Name stack of the current symbol, including module name parts.
246 	 */
247 	void writeBreadcrumbs(R)(ref R dst, string[] symbolStack, SymbolDatabase database)
248 	{
249 		string heading;
250 		scope(exit) { writeBreadcrumbs(dst, heading); }
251 
252 		assert(moduleNameLength_ <= symbolStack.length, "stack shallower than the current module?");
253 		size_t depth;
254 
255 		string link()
256 		{
257 			assert(depth + 1 >= moduleNameLength_, "unexpected value of depth");
258 			return symbolLink(database.symbolStack(
259 							  symbolStack[0 .. moduleNameLength],
260 							  symbolStack[moduleNameLength .. depth + 1]));
261 		}
262 
263 		// Module
264 		{
265 			heading ~= "<small>";
266 			scope(exit) { heading ~= "</small>"; }
267 			for(; depth + 1 < moduleNameLength_; ++depth)
268 			{
269 				heading ~= symbolStack[depth] ~ ".";
270 			}
271 			// Module link if the module is a parent of the current page.
272 			if (depth + 1 < symbolStack.length)
273 			{
274 				heading ~= `<a href=%s>%s</a>.`.format(link(), symbolStack[depth]);
275 				++depth;
276 			}
277 			// Just the module name, not a link, if we're at the module page.
278 			else
279 			{
280 				heading ~= symbolStack[depth];
281 				return;
282 			}
283 		}
284 
285 		// Class/Function/etc. in the module
286 		heading ~= `<span class="highlight">`;
287 		// The rest of the stack except the last element (parents of current page).
288 		for(; depth + 1 < symbolStack.length; ++depth)
289 		{
290 			heading  ~= `<a href=%s>%s</a>.`.format(link(), symbolStack[depth]);
291 		}
292 		// The last element (no need to link to the current page).
293 		heading ~= symbolStack[depth];
294 		heading ~= `</span>`;
295 	}
296 
297 
298 	/** Writes a doc comment to the given range and returns the summary text.
299 	 *
300 	 * Params:
301 	 * dst          = Range to write the comment to.
302 	 * comment      = The comment to write
303 	 * prevComments = Previously encountered comments. This is used for handling
304 	 *                "ditto" comments. May be null.
305 	 * functionBody = A function body used for writing contract information. May be null.
306 	 * testdocs     = Pairs of unittest bodies and unittest doc comments. May be null.
307 	 *
308 	 * Returns: the summary from the given comment
309 	 */
310 	string readAndWriteComment(R)
311 		(ref R dst, string comment, Comment[] prevComments = null,
312 		 const FunctionBody functionBody = null,
313 		 Tuple!(string, string)[] testDocs = null)
314 	{
315 		if (comment.empty)
316 			return null;
317 
318 		import core.exception: RangeError;
319 		try
320 		{
321 			return readAndWriteComment_(dst, comment, prevComments, functionBody, testDocs);
322 		}
323 		catch(RangeError e)
324 		{
325 			writeln("failed to process comment: ", e);
326 			dst.put("<div class='error'><h3>failed to process comment</h3>\n" ~
327 					"\n<pre>%s</pre>\n<h3>error</h3>\n<pre>%s</pre></div>"
328 					.format(comment, e));
329 			return null;
330 		}
331 	}
332 
333 	/** Writes a code block to range dst, using blockCode to write code block contents.
334 	 *
335 	 * Params:
336 	 *
337 	 * dst       = Range to write to.
338 	 * blockCode = Function that will write the code block contents (presumably also
339 	 *             into dst).
340 	 */
341 	void writeCodeBlock(R)(ref R dst, void delegate() blockCode)
342 	{
343 		dst.put(`<pre><code>`); blockCode(); dst.put("\n</code></pre>\n");
344 	}
345 
346 	/** Writes a section to range dst, using sectionCode to write section contents.
347 	 *
348 	 * Params:
349 	 *
350 	 * dst         = Range to write to.
351 	 * blockCode   = Function that will write the section contents (presumably also
352 	 *               into dst).
353 	 * extraStyles = Extra style classes to use in the section, separated by spaces.
354 	 *               May be ignored by non-HTML writers.
355 	 */
356 	void writeSection(R)(ref R dst, void delegate() sectionCode, string extraStyles = "")
357 	{
358 		dst.put(`<div class="section%s">`
359 				.format(extraStyles.length == 0 ? "" : " " ~ extraStyles));
360 		sectionCode();
361 		dst.put("\n</div>\n");
362 	}
363 
364 	/** Writes an unordered list to range dst, using listCode to write list contents.
365 	 *
366 	 * Params:
367 	 *
368 	 * dst      = Range to write to.
369 	 * name     = Name of the list, if any. Will be used as heading if specified.
370 	 * listCode = Function that will write the list contents.
371 	 */
372 	void writeList(R)(ref R dst, string name, void delegate() listCode)
373 	{
374 		if (name.length)
375 			dst.put(`<h2>%s</h2>`.format(name));
376 		dst.put(`<ul>`);
377 		listCode();
378 		dst.put("\n</ul>\n");
379 	}
380 
381 	/** Writes a list item to range dst, using itemCode to write list contents.
382 	 *
383 	 * Params:
384 	 *
385 	 * dst      = Range to write to.
386 	 * itemCode = Function that will write the item contents.
387 	 */
388 	void writeListItem(R)(ref R dst, void delegate() itemCode)
389 	{
390 		dst.put(`<li>`); itemCode(); dst.put("</li>");
391 	}
392 
393 	/** Writes a link to range dst, using linkCode to write link text (but not the
394 	 * link itself).
395 	 *
396 	 * Params:
397 	 *
398 	 * dst         = Range to write to.
399 	 * link        = Link (URL) to write.
400 	 * linkCode    = Function that will write the link text.
401 	 * extraStyles = Extra style classes to use for the link, separated by spaces.
402 	 *               May be ignored by non-HTML writers.
403 	 */
404 	void writeLink(R)(ref R dst, string link, void delegate() linkCode, string extraStyles = "")
405 	{
406 		const styles = extraStyles.empty ? "" : ` class="%s"`.format(extraStyles);
407 		dst.put(`<a href="%s"%s>`.format(link, styles)); linkCode(); dst.put("</a>");
408 	}
409 
410 	final auto newFormatter(R)(ref R dst)
411 	{
412 		return new HarboredFormatter!R(dst, processCode);
413 	}
414 
415 	final void popSymbol()
416 	{
417 		foreach (f; symbolFileStack.back)
418 		{
419 			f.writeln(`<script>hljs.initHighlightingOnLoad();</script>`);
420 			f.writeln(HTML_END);
421 			f.close();
422 		}
423 		symbolFileStack.popBack();
424 	}
425 
426 	/// Default processCode function.
427 	final string processCodeDefault(string str) @safe nothrow { return str; }
428 
429 	/// Function to process inline code and code blocks with (used for cross-referencing).
430 	public string delegate(string) @safe nothrow processCode;
431 
432 protected:
433 	/** Add an entry for JavaScript search for the symbol with specified name stack.
434 	 *
435 	 * symbolStack = Name stack of the current symbol, including module name parts.
436 	 */
437 	void addSearchEntry(SymbolStack)(SymbolStack symbolStack)
438 	{
439 		const symbol = symbolStack.map!(s => s.name).joiner(".").array;
440 		searchIndex.writefln(`{"%s" : "%s"},`, symbol, symbolLink(symbolStack));
441 	}
442 
443 	/** If markdown enabled, run input through markdown and return it. Otherwise
444 	 * return input unchanged.
445 	 */
446 	final string processMarkdown(string input)
447 	{
448 		if (config.noMarkdown)
449 			return input;
450 
451 		import dmarkdown : MarkdownSettings, MarkdownFlags, filterMarkdown;
452 		// We want to enable '***' subheaders and to post-process code
453 		// for cross-referencing.
454 		auto mdSettings = new MarkdownSettings();
455 		with (MarkdownFlags) mdSettings.flags = alternateSubheaders | disableUnderscoreEmphasis;
456 		mdSettings.processCode = processCode;
457 		return filterMarkdown(input, mdSettings);
458 	}
459 
460 	/// See_Also: `readAndWriteComment`
461 	string readAndWriteComment_(R)
462 		(ref R dst, string comment, Comment[] prevComments,
463 		 const FunctionBody functionBody, Tuple!(string, string)[] testDocs)
464 	{
465 		import dparse.lexer : unDecorateComment;
466 		auto app = appender!string();
467 
468 		if (comment.length >= 3)
469 		{
470 			comment.unDecorateComment(app);
471 		}
472 
473 		Comment c = parseComment(app.data, macros);
474 
475 		immutable ditto = c.isDitto;
476 
477 		// Finds code blocks generated by libddoc and calls processCode() on them,
478 		string processCodeBlocks(string remaining)
479 		{
480 			auto codeApp = appender!string();
481 			do
482 			{
483 				auto parts = remaining.findSplit("<pre><code>");
484 				codeApp.put(parts[0]);
485 				codeApp.put(parts[1]); //<code><pre>
486 
487 				parts = parts[2].findSplit("</code></pre>");
488 				codeApp.put(processCode(parts[0]));
489 				codeApp.put(parts[1]); //</code></pre>
490 				remaining = parts[2];
491 			}
492 			while(!remaining.empty);
493 			return codeApp.data;
494 		}
495 
496 		// Run sections through markdown.
497 		foreach (ref section; c.sections)
498 		{
499 			// Ensure param descriptions run through Markdown
500 			if (section.name == "Params")
501 				foreach (ref kv; section.mapping)
502 					kv[1] = processMarkdown(kv[1]);
503 			// Do not run code examples through markdown.
504 			//
505 			// We could check for section.name == "Examples" but code blocks can be
506 			// outside examples. Alternatively, we could look for *multi-line*
507 			// <pre>/<code> blocks, or, before parsing comments, for "---" pairs.
508 			// Or, dmarkdown could be changed to ignore <pre>/<code> blocks.
509 			const isCode = section.content.canFind("<pre><code>");
510 			section.content = isCode ? processCodeBlocks(section.content)
511 									 : processMarkdown(section.content);
512 		}
513 
514 		if (prevComments.length > 0)
515 		{
516 			if (ditto)
517 				c = prevComments[$ - 1];
518 			else
519 				prevComments[$ - 1] = c;
520 		}
521 
522 
523 		writeComment(dst, c, functionBody);
524 
525 		// Find summary and return value info
526 		string rVal = "";
527 		if (c.sections.length && c.sections[0].name == "Summary")
528 			rVal = c.sections[0].content;
529 		else foreach (section; c.sections.find!(s => s.name == "Returns"))
530 		{
531 			rVal = "Returns: " ~ section.content;
532 		}
533 		if (testDocs !is null) foreach (doc; testDocs)
534 		{
535 	//		writeln("Writing a unittest doc comment");
536 			writeSection(dst,
537 			{
538 				dst.put("<h2>Example</h2>\n");
539 				auto docApp = appender!string();
540 
541 				// Unittest doc can be empty, so nothing to undecorate
542 				if (doc[1].length >= 3)
543 				{
544 					doc[1].unDecorateComment(docApp);
545 				}
546 
547 				Comment dc = parseComment(docApp.data, macros);
548 				writeComment(dst, dc);
549 				writeCodeBlock(dst, { dst.put(processCode(outdent(doc[0]))); } );
550 			});
551 		}
552 		return rVal;
553 	}
554 
555 	void writeComment(R)(ref R dst, Comment comment, const FunctionBody functionBody = null)
556 	{
557 	//		writeln("writeComment: ", comment.sections.length, " sections.");
558 		// Shortcut to write text followed by newline
559 		void put(string str) { dst.put(str); dst.put("\n"); }
560 
561 		size_t i;
562 		for (i = 0; i < comment.sections.length && (comment.sections[i].name == "Summary"
563 			|| comment.sections[i].name == "description"); i++)
564 		{
565 			writeSection(dst, { put(comment.sections[i].content); });
566 		}
567 
568 		if (functionBody) with (functionBody)
569 		{
570 			const(FunctionContract)[] contracts = missingFunctionBody
571 				? missingFunctionBody.functionContracts
572 				: specifiedFunctionBody ? specifiedFunctionBody.functionContracts
573 				: null;
574 
575 			if (contracts)
576 				writeContracts(dst,contracts);
577 		}
578 
579 		const seealsoNames = ["See_also", "See_Also", "See also", "See Also"];
580 		foreach (section; comment.sections[i .. $])
581 		{
582 			if (seealsoNames.canFind(section.name) || section.name == "Macros")
583 				continue;
584 
585 			// Note sections a use different style
586 			const isNote = section.name == "Note";
587 			string extraClasses;
588 
589 			if (isNote)
590 				extraClasses ~= "note";
591 
592 			writeSection(dst,
593 			{
594 				if (section.name.length && section.name != "Summary" && section.name != "Description")
595 				{
596 					dst.put("<h2>");
597 					dst.put(prettySectionName(section.name));
598 					put("</h2>");
599 				}
600 				if (isNote)
601 					put(`<div class="note-content">`);
602 				scope(exit) if (isNote) put(`</div>`);
603 
604 				if (section.name == "Params")
605 				{
606 					put(`<table class="params">`);
607 					foreach (kv; section.mapping)
608 					{
609 						dst.put(`<tr class="param"><td class="paramName">`);
610 						dst.put(kv[0]);
611 						dst.put(`</td><td class="paramDoc">`);
612 						dst.put(kv[1]);
613 						put("</td></tr>");
614 					}
615 					dst.put("</table>");
616 				}
617 				else put(section.content);
618 			}, extraClasses);
619 		}
620 
621 		// Merge any see also sections into one, and draw it with different style than
622 		// other sections.
623 		auto seealsos = comment.sections.filter!(s => seealsoNames.canFind(s.name));
624 		if (!seealsos.empty)
625 		{
626 			put(`<div class="section seealso">`);
627 			dst.put("<h2>");
628 			dst.put(prettySectionName(seealsos.front.name));
629 			put("</h2>");
630 			put(`<div class="seealso-content">`);
631 			seealsos.each!(section => put(section.content));
632 			put(`</div>`);
633 			put(`</div>`);
634 		}
635 	}
636 
637 	void writeContracts(R)(ref R dst, const(FunctionContract)[] contracts)
638 	{
639 		if (!contracts.length)
640 			return;
641 
642 		writeSection(dst,
643 		{
644 			dst.put(`<h2>Contracts</h2>`);
645 			writeCodeBlock(dst,
646 			{
647 				auto formatter = newFormatter(dst);
648 				scope (exit) formatter.sink = R.init;
649 				foreach (i, const c; contracts)
650 				{
651 					formatter.format(c);
652 					if (c.inOutStatement && i != contracts.length -1)
653 						dst.put("\n");
654 				}
655 			});
656 		});
657 	}
658 
659 	void writeItemEntry(R)(ref R dst, ref Item item)
660 	{
661 		dst.put(`<tr><td>`);
662 		void writeName()
663 		{
664 			dst.put(item.url == "#"
665 				? item.name : `<a href="%s">%s</a>`.format(item.url, item.name));
666 		}
667 
668 		// TODO print attributes for everything, and move it to separate function/s
669 		if (FunctionDeclaration fd = cast(FunctionDeclaration) item.node)
670 		{
671 			// extremely inefficient, rewrite if too much slowdown
672 			string formatAttrib(T)(T attr)
673 			{
674 				auto writer = appender!(char[])();
675 				auto formatter = newFormatter(writer);
676 				formatter.format(attr);
677 				auto str = writer.data.idup;
678 				writer.clear();
679 				import std.ascii: isAlpha;
680 				// Sanitize CSS class name for the attribute,
681 				auto strSane = str.filter!isAlpha.array.to!string;
682 				return `<span class="attr-` ~ strSane ~ `">` ~ str ~ `</span>`;
683 			}
684 
685 			void writeSpan(C)(string class_, C content)
686 			{
687 				dst.put(`<span class="%s">%s</span>`.format(class_, content));
688 			}
689 
690 			// Above the function name
691 			if (!fd.attributes.empty)
692 			{
693 				dst.put(`<span class="extrainfo">`);
694 				writeSpan("attribs", fd.attributes.map!(a => formatAttrib(a)).joiner(", "));
695 				dst.put(`</span>`);
696 			}
697 
698 			// The actual function name
699 			writeName();
700 
701 			// Below the function name
702 			dst.put(`<span class="extrainfo">`);
703 			if (!fd.memberFunctionAttributes.empty)
704 			{
705 				writeSpan("method-attribs",
706 					fd.memberFunctionAttributes.map!(a => formatAttrib(a)).joiner(", "));
707 			}
708 			// TODO storage classes don't seem to work. libdparse issue?
709 			if (!fd.storageClasses.empty)
710 			{
711 				writeSpan("stor-classes", fd.storageClasses.map!(a => formatAttrib(a)).joiner(", "));
712 			}
713 			dst.put(`</span>`);
714 		}
715 		// By default, just print the name of the item.
716 		else writeName();
717 		dst.put(`</td>`);
718 
719 		dst.put(`<td>`);
720 		if (item.type !is null)
721 		{
722 			void delegate() dg =
723 			{
724 				dst.put(item.type);
725 				if (Declarator decl = cast(Declarator) item.node)
726 				{
727 					if (!decl.initializer)
728 						return;
729 
730 					import dparse.formatter : fmt = format;
731 					dst.put(" = ");
732 					fmt(&dst,  decl.initializer);
733 				}
734 			};
735 			writeCodeBlock(dst, dg);
736 		}
737 		dst.put(`</td><td>%s</td></tr>`.format(item.summary));
738 	}
739 
740 	/** Write a table of items of specified category.
741 	 *
742 	 * Params:
743 	 *
744 	 * dst      = Range to write to.
745 	 * items    = Items the table will contain.
746 	 * category = Category of the items, used in heading, E.g. "Functions" or
747 	 *            "Variables" or "Structs".
748 	 */
749 	public void writeItems(R)(ref R dst, Item[] items, string category)
750 	{
751 		if (category.length)
752 			dst.put("<h2>%s</h2>".format(category));
753 		dst.put(`<table>`);
754 		foreach (ref i; items) { writeItemEntry(dst, i); }
755 		dst.put(`</table>`);
756 	}
757 
758 	/** Formats an AST node to a string.
759 	 */
760 	public string formatNode(T)(const T t)
761 	{
762 		auto writer = appender!string();
763 		auto formatter = newFormatter(writer);
764 		scope(exit) destroy(formatter.sink);
765 		formatter.format(t);
766 		return writer.data;
767 	}
768 
769 	const(Config)* config;
770 	string[string] macros;
771 	File searchIndex;
772 	TocItem[] tocItems;
773 	string[] tocAdditionals;
774 
775 	/** Stack of associative arrays.
776 	 *
777 	 * Each level contains documentation page files of members of the symbol at that
778 	 * level; e.g. symbolFileStack[0] contains the module documentation file,
779 	 * symbolFileStack[1] doc pages of the module's child classes, and so on.
780 	 *
781 	 * Note that symbolFileStack levels correspond to symbol stack levels. Depending
782 	 * on the HTMLWriter implementation, there may not be files for all levels.
783 	 *
784 	 * E.g. with HTMLWriterAggregated, if we have a class called `Class.method.NestedClass`,
785 	 * when writing `NestedClass` docs symbolFileStack[$ - 3 .. 0] will be something like:
786 	 * `[["ClassFileName": File(stuff)], [], ["NestedClassFileName": * File(stuff)]]`,
787 	 * i.e. there will be a stack level for `method` but it will have no contents.
788 	 *
789 	 * When popSymbol() is called, all doc page files of that symbol's members are closed
790 	 * (they must be kept open until then to ensure overloads are put into the same file).
791 	 */
792 	File[string][] symbolFileStack;
793 
794 	string moduleFileBase_;
795 	// Path to the HTML file relative to the output directory.
796 	string moduleLink_;
797 	// Name length of the module (e.g. 2 for std.stdio)
798 	size_t moduleNameLength_;
799 }
800 
801 /** Get a link to a symbol.
802  *
803  * Note: this does not check if the symbol exists; calling symbolLink() with a SymbolStack
804  * of a nonexistent symbol will result in a link to the deepest existing parent symbol.
805  *
806  * Params: nameStack = SymbolStack returned by SymbolDatabase.symbolStack(),
807  *                     describing a fully qualified symbol name.
808  *
809  * Returns: Link to the file with documentation for the symbol.
810  */
811 string symbolLinkAggregated(SymbolStack)(auto ref SymbolStack nameStack)
812 {
813 	if (nameStack.empty)
814 		return "UNKNOWN.html";
815 	// Start with the first part of the name so we have something we can buildPath() with.
816 	string result = nameStack.front.name;
817 	const firstType = nameStack.front.type;
818 	bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package;
819 	nameStack.popFront();
820 
821 	bool inAnchor;
822 	foreach (name; nameStack)
823 		final switch(name.type) with(SymbolType)
824 	{
825 		// A new directory is created for each module
826 		case Module, Package:
827 			result = result.buildPath(name.name);
828 			moduleParent = true;
829 			break;
830 		// These symbol types have separate files in a module directory.
831 		case Class, Struct, Interface, Enum, Template:
832 			// If last name was module/package, the file will be in its
833 			// directory. Otherwise it will be in the same dir as the parent.
834 			result = moduleParent ? result.buildPath(name.name)
835 								   : result ~ "." ~ name.name;
836 			moduleParent = false;
837 			break;
838 		// These symbol types are documented in their parent symbol's files.
839 		case Function, Variable, Alias, Value:
840 			// inAnchor allows us to handle nested functions, which are still
841 			// documented in the same file as their parent function.
842 			// E.g. a nested function called entity.EntityManager.foo.bar will
843 			// have link entity/EntityManager#foo.bar
844 			result = inAnchor ? result ~ "." ~ name.name
845 							  : result ~ ".html#" ~ name.name;
846 			inAnchor = true;
847 			break;
848 	}
849 
850 	return result ~ (inAnchor ? "" : ".html");
851 }
852 
853 /** A HTML writer generating 'aggregated' HTML documentation.
854  *
855  * Instead of generating a separate file for every variable or function, this only
856  * generates files for aggregates (module, struct, class, interface, template, enum),
857  * and any non-aggregate symbols are put documented in their aggregate parent's
858  * documentation files.
859  *
860  * E.g. all member functions and data members of a class are documented directly in the
861  * file documenting that class instead of in separate files the class documentation would
862  * link to like with HTMLWriterSimple.
863  *
864  * This output results in much less files and lower file size than HTMLWriterSimple, and
865  * is arguably easier to use due to less clicking between files.
866  */
867 class HTMLWriterAggregated: HTMLWriterBase!symbolLinkAggregated
868 {
869 	alias Super = typeof(super);
870 	private alias config = Super.config;
871 	alias writeBreadcrumbs = Super.writeBreadcrumbs;
872 	alias symbolLink = symbolLinkAggregated;
873 
874 	this(ref Config config, File searchIndex,
875 		 TocItem[] tocItems, string[] tocAdditionals)
876 	{
877 		super(config, searchIndex, tocItems, tocAdditionals);
878 	}
879 
880 	// No separator needed; symbols are already in divs.
881 	void writeSeparator(R)(ref R dst) {}
882 
883 	void writeSymbolStart(R)(ref R dst, string link)
884 	{
885 		const isAggregate = !link.canFind("#");
886 		if (!isAggregate)
887 		{
888 			// We need a separate anchor so we can style it separately to
889 			// compensate for fixed breadcrumbs.
890 			dst.put(`<a class="anchor" id="`);
891 			dst.put(link.findSplit("#")[2]);
892 			dst.put(`"></a>`);
893 		}
894 		dst.put(isAggregate ? `<div class="aggregate-symbol">` : `<div class="symbol">`);
895 	}
896 
897 	void writeSymbolEnd(R)(ref R dst) { dst.put(`</div>`); }
898 
899 	void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode)
900 	{
901 		dst.put(`<div class="description">`); descriptionCode(); dst.put(`</div>`);
902 	}
903 
904 	auto pushSymbol(string[] symbolStackRaw, SymbolDatabase database,
905 					ref bool first, ref string itemURL)
906 	{
907 		assert(symbolStackRaw.length >= moduleNameLength_,
908 			   "symbol stack shorter than module name");
909 
910 		// A symbol-type-aware stack.
911 		auto symbolStack = database.symbolStack(symbolStackRaw[0 .. moduleNameLength],
912 												symbolStackRaw[moduleNameLength .. $]);
913 
914 		// Is this symbol an aggregate?
915 		// If the last part of the symbol stack (this symbol) is an aggregate, we
916 		// create a new file for it. Otherwise we write into parent aggregate's file.
917 		bool isAggregate;
918 		// The deepest level in the symbol stack that is an aggregate symbol.
919 		// If this symbol is an aggregate, that's symbolStack.walkLength - 1, if
920 		// this symbol is not an aggregate but its parent is, that's
921 		// symbolStack.walkLength - 2, etc.
922 		size_t deepestAggregateLevel = size_t.max;
923 		size_t nameDepth;
924 		foreach (name; symbolStack)
925 		{
926 			scope(exit) { ++nameDepth; }
927 			final switch(name.type) with(SymbolType)
928 			{
929 				case Module, Package, Class, Struct, Interface, Enum, Template:
930 					isAggregate = true;
931 					deepestAggregateLevel = nameDepth;
932 					break;
933 				case Function, Variable, Alias, Value:
934 					isAggregate = false;
935 					break;
936 			}
937 		}
938 
939 		symbolFileStack.length = symbolFileStack.length + 1;
940 		addSearchEntry(symbolStack);
941 
942 		// Name stack of the symbol in the documentation file of which we will
943 		// write, except the module name part.
944 		string[] targetSymbolStack;
945 		size_t fileDepth;
946 		// If the symbol is not an aggregate, its docs will be written into its
947 		// closest aggregate parent.
948 		if (!isAggregate)
949 		{
950 			assert(deepestAggregateLevel != size_t.max,
951 				   "A non-aggregate with no aggregate parent; maybe modules " ~
952 				   "are not considered aggregates? (we can't handle that case)");
953 
954 			// Write into the file for the deepest aggregate parent (+1 is
955 			// needed to include the name of the parent itself)
956 			targetSymbolStack =
957 				symbolStackRaw[moduleNameLength_ .. deepestAggregateLevel + 1];
958 
959 			// Going relatively from the end, as the symbolFileStack does not
960 			// contain items for some or all top-most packages.
961 			fileDepth = symbolFileStack.length -
962 						(symbolStackRaw.length - deepestAggregateLevel) - 1;
963 		}
964 		// If the symbol is an aggregate, it will have a file just for itself.
965 		else
966 		{
967 			// The symbol itself is the target.
968 			targetSymbolStack = symbolStackRaw[moduleNameLength_ .. $];
969 			// Parent is the second last element of symbolFileStack
970 			fileDepth = symbolFileStack.length - 2;
971 			assert(fileDepth < symbolFileStack.length,
972 				   "integer overflow (symbolFileStack should have length >= 2 here): %s %s"
973 				   .format(fileDepth, symbolFileStack.length));
974 		}
975 
976 		// Path relative to output directory
977 		string docFileName = targetSymbolStack.empty
978 			? moduleFileBase_ ~ ".html"
979 			: moduleFileBase_.buildPath(targetSymbolStack.joiner(".").array.to!string).setExtension("html");
980 		itemURL = symbolLink(symbolStack);
981 
982 		// Look for a file if it already exists, create if it does not.
983 		File* p = docFileName in symbolFileStack[fileDepth];
984 		first = p is null;
985 		if (first)
986 		{
987 			auto f = File(config.outputDirectory.buildPath(docFileName), "w");
988 			symbolFileStack[fileDepth][docFileName] = f;
989 			return f.lockingTextWriter;
990 		}
991 		else return p.lockingTextWriter;
992 	}
993 }
994 
995 /** symbolLink implementation for HTMLWriterSimple.
996  *
997  * See_Also: symbolLinkAggregated
998  */
999 string symbolLinkSimple(SymbolStack)(auto ref SymbolStack nameStack)
1000 {
1001 	if (nameStack.empty)
1002 		return "UNKNOWN.html";
1003 	// Start with the first part of the name so we have something we can buildPath() with.
1004 	string result = nameStack.front.name;
1005 	const firstType = nameStack.front.type;
1006 	bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package;
1007 	nameStack.popFront();
1008 
1009 	foreach (name; nameStack)
1010 		final switch(name.type) with(SymbolType)
1011 	{
1012 		// A new directory is created for each module
1013 		case Module, Package:
1014 			result = result.buildPath(name.name);
1015 			moduleParent = true;
1016 			break;
1017 		// These symbol types have separate files in a module directory.
1018 		case Class, Struct, Interface, Enum, Function, Variable, Alias, Template:
1019 			// If last name was module/package, the file will be in its
1020 			// directory. Otherwise it will be in the same dir as the parent.
1021 			result = moduleParent ? result.buildPath(name.name)
1022 								   : result ~ "." ~ name.name;
1023 			moduleParent = false;
1024 			break;
1025 		// Enum members are documented in their enums.
1026 		case Value: result = result; break;
1027 	}
1028 
1029 	return result ~ ".html";
1030 }
1031 
1032 class HTMLWriterSimple: HTMLWriterBase!symbolLinkSimple
1033 {
1034 	alias Super = typeof(super);
1035 	private alias config = Super.config;
1036 	alias writeBreadcrumbs = Super.writeBreadcrumbs;
1037 	alias symbolLink = symbolLinkSimple;
1038 
1039 	this(ref Config config, File searchIndex,
1040 		 TocItem[] tocItems, string[] tocAdditionals)
1041 	{
1042 		super(config, searchIndex, tocItems, tocAdditionals);
1043 	}
1044 
1045 	/// Write a separator (e.g. between two overloads of a function)
1046 	void writeSeparator(R)(ref R dst) { dst.put("<hr/>"); }
1047 
1048 	// Do nothing. No divs needed as every symbol is in a separate file.
1049 	void writeSymbolStart(R)(ref R dst, string link) { }
1050 	void writeSymbolEnd(R)(ref R dst) { }
1051 
1052 	void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode)
1053 	{
1054 		descriptionCode();
1055 	}
1056 
1057 	auto pushSymbol(string[] symbolStack, SymbolDatabase database,
1058 					ref bool first, ref string itemURL)
1059 	{
1060 		symbolFileStack.length = symbolFileStack.length + 1;
1061 
1062 		assert(symbolStack.length >= moduleNameLength_,
1063 			   "symbol stack shorter than module name");
1064 
1065 		auto tail = symbolStack[moduleNameLength_ .. $];
1066 		// Path relative to output directory
1067 		const docFileName = tail.empty
1068 			? moduleFileBase_ ~ ".html"
1069 			: moduleFileBase_.buildPath(tail.joiner(".").array.to!string).setExtension("html");
1070 
1071 		addSearchEntry(database.symbolStack(symbolStack[0 .. moduleNameLength],
1072 											symbolStack[moduleNameLength .. $]));
1073 
1074 		// The second last element of symbolFileStack
1075 		immutable size_t i = symbolFileStack.length - 2;
1076 		assert (i < symbolFileStack.length, "%s %s".format(i, symbolFileStack.length));
1077 		auto p = docFileName in symbolFileStack[i];
1078 		first = p is null;
1079 		itemURL = docFileName;
1080 		if (first)
1081 		{
1082 			first = true;
1083 			auto f = File(config.outputDirectory.buildPath(docFileName), "w");
1084 			symbolFileStack[i][docFileName] = f;
1085 			return f.lockingTextWriter;
1086 		}
1087 		else return p.lockingTextWriter;
1088 	}
1089 }
1090 
1091 enum HTML_END = `
1092 </div>
1093 <footer>
1094 Generated with <a href="https://gitlab.com/basile.b/harbored-mod">harbored-mod</a>
1095 </footer>
1096 </div>
1097 <script>anchors.add();</script>
1098 </body>
1099 </html>`;
1100 
1101 private:
1102 
1103 string prettySectionName(string sectionName)
1104 {
1105 	switch (sectionName)
1106 	{
1107 		case "See_also", "See_Also", "See also", "See Also": return "See Also:";
1108 		case "Note":   return "Note:";
1109 		case "Params": return "Parameters";
1110 		default:       return sectionName;
1111 	}
1112 }