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 <title>%s</title>
128 <base href="%s"/>
129 <script src="anchor.js"></script>
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.length ? moduleLink(moduleName.split(".")) : "index.html";
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 		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 + 1 != contracts.length)
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 		void writeSpan(C)(string class_, C content)
669 		{
670 			dst.put(`<span class="%s">%s</span>`.format(class_, content));
671 		}
672 
673 		// extremely inefficient, rewrite if too much slowdown
674 		string formatAttrib(T)(T attr)
675 		{
676 			auto writer = appender!(char[])();
677 			auto formatter = newFormatter(writer);
678 			formatter.format(attr);
679 			auto str = writer.data.idup;
680 			writer.clear();
681 			import std.ascii: isAlpha;
682 			// Sanitize CSS class name for the attribute,
683 			auto strSane = str.filter!isAlpha.array.to!string;
684 			return `<span class="attr-` ~ strSane ~ `">` ~ str ~ `</span>`;
685 		}
686 
687 		// enum attributes
688 		if (EnumMember em = cast(EnumMember) item.node)
689 		{
690 			if (em.enumMemberAttributes)
691 			{
692 				writeSpan("extrainfo", em.enumMemberAttributes.map!(a => formatAttrib(a)).joiner(", "));
693 			}
694 		}
695 
696 		// TODO print attributes for everything, and move it to separate function/s
697 		if (FunctionDeclaration fd = cast(FunctionDeclaration) item.node)
698 		{
699 			// Above the function name
700 			if (!fd.attributes.empty)
701 			{
702 				dst.put(`<span class="extrainfo">`);
703 				writeSpan("attribs", fd.attributes.map!(a => formatAttrib(a)).joiner(", "));
704 				dst.put(`</span>`);
705 			}
706 
707 			// The actual function name
708 			writeName();
709 
710 			// Below the function name
711 			dst.put(`<span class="extrainfo">`);
712 			if (!fd.memberFunctionAttributes.empty)
713 			{
714 				writeSpan("method-attribs",
715 					fd.memberFunctionAttributes.map!(a => formatAttrib(a)).joiner(", "));
716 			}
717 			// TODO storage classes don't seem to work. libdparse issue?
718 			if (!fd.storageClasses.empty)
719 			{
720 				writeSpan("stor-classes", fd.storageClasses.map!(a => formatAttrib(a)).joiner(", "));
721 			}
722 			dst.put(`</span>`);
723 		}
724 		// By default, just print the name of the item.
725 		else writeName();
726 		dst.put(`</td>`);
727 
728 		dst.put(`<td>`);
729 		if (item.type.length)
730 		{
731 			void delegate() dg =
732 			{
733 				dst.put(item.type);
734 				if (Declarator decl = cast(Declarator) item.node)
735 				{
736 					if (!decl.initializer)
737 						return;
738 
739 					import dparse.formatter : fmt = format;
740 					dst.put(" = ");
741 					fmt(&dst,  decl.initializer);
742 				}
743 			};
744 			writeCodeBlock(dst, dg);
745 		}
746 		dst.put(`</td><td>%s</td></tr>`.format(item.summary));
747 	}
748 
749 	/** Write a table of items of specified category.
750 	 *
751 	 * Params:
752 	 *
753 	 * dst      = Range to write to.
754 	 * items    = Items the table will contain.
755 	 * category = Category of the items, used in heading, E.g. "Functions" or
756 	 *            "Variables" or "Structs".
757 	 */
758 	public void writeItems(R)(ref R dst, Item[] items, string category)
759 	{
760 		if (category.length)
761 			dst.put("<h2>%s</h2>".format(category));
762 		dst.put(`<table>`);
763 		foreach (ref i; items) { writeItemEntry(dst, i); }
764 		dst.put(`</table>`);
765 	}
766 
767 	/** Formats an AST node to a string.
768 	 */
769 	public string formatNode(T)(const T t)
770 	{
771 		auto writer = appender!string();
772 		auto formatter = newFormatter(writer);
773 		scope(exit) destroy(formatter.sink);
774 		formatter.format(t);
775 		return writer.data;
776 	}
777 
778 	const(Config)* config;
779 	string[string] macros;
780 	File searchIndex;
781 	TocItem[] tocItems;
782 	string[] tocAdditionals;
783 
784 	/** Stack of associative arrays.
785 	 *
786 	 * Each level contains documentation page files of members of the symbol at that
787 	 * level; e.g. symbolFileStack[0] contains the module documentation file,
788 	 * symbolFileStack[1] doc pages of the module's child classes, and so on.
789 	 *
790 	 * Note that symbolFileStack levels correspond to symbol stack levels. Depending
791 	 * on the HTMLWriter implementation, there may not be files for all levels.
792 	 *
793 	 * E.g. with HTMLWriterAggregated, if we have a class called `Class.method.NestedClass`,
794 	 * when writing `NestedClass` docs symbolFileStack[$ - 3 .. 0] will be something like:
795 	 * `[["ClassFileName": File(stuff)], [], ["NestedClassFileName": * File(stuff)]]`,
796 	 * i.e. there will be a stack level for `method` but it will have no contents.
797 	 *
798 	 * When popSymbol() is called, all doc page files of that symbol's members are closed
799 	 * (they must be kept open until then to ensure overloads are put into the same file).
800 	 */
801 	File[string][] symbolFileStack;
802 
803 	string moduleFileBase_;
804 	// Path to the HTML file relative to the output directory.
805 	string moduleLink_;
806 	// Name length of the module (e.g. 2 for std.stdio)
807 	size_t moduleNameLength_;
808 }
809 
810 /** Get a link to a symbol.
811  *
812  * Note: this does not check if the symbol exists; calling symbolLink() with a SymbolStack
813  * of a nonexistent symbol will result in a link to the deepest existing parent symbol.
814  *
815  * Params: nameStack = SymbolStack returned by SymbolDatabase.symbolStack(),
816  *                     describing a fully qualified symbol name.
817  *
818  * Returns: Link to the file with documentation for the symbol.
819  */
820 string symbolLinkAggregated(SymbolStack)(auto ref SymbolStack nameStack)
821 {
822 	if (nameStack.empty)
823 		return "UNKNOWN.html";
824 	// Start with the first part of the name so we have something we can buildPath() with.
825 	string result = nameStack.front.name;
826 	const firstType = nameStack.front.type;
827 	bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package;
828 	nameStack.popFront();
829 
830 	bool inAnchor;
831 	foreach (name; nameStack)
832 		final switch(name.type) with(SymbolType)
833 	{
834 		// A new directory is created for each module
835 		case Module, Package:
836 			result = result.buildPath(name.name);
837 			moduleParent = true;
838 			break;
839 		// These symbol types have separate files in a module directory.
840 		case Class, Struct, Interface, Enum, Template:
841 			// If last name was module/package, the file will be in its
842 			// directory. Otherwise it will be in the same dir as the parent.
843 			result = moduleParent ? result.buildPath(name.name)
844 								   : result ~ "." ~ name.name;
845 			moduleParent = false;
846 			break;
847 		// These symbol types are documented in their parent symbol's files.
848 		case Function, Variable, Alias, Value:
849 			// inAnchor allows us to handle nested functions, which are still
850 			// documented in the same file as their parent function.
851 			// E.g. a nested function called entity.EntityManager.foo.bar will
852 			// have link entity/EntityManager#foo.bar
853 			result = inAnchor ? result ~ "." ~ name.name
854 							  : result ~ ".html#" ~ name.name;
855 			inAnchor = true;
856 			break;
857 	}
858 
859 	return result ~ (inAnchor ? "" : ".html");
860 }
861 
862 /** A HTML writer generating 'aggregated' HTML documentation.
863  *
864  * Instead of generating a separate file for every variable or function, this only
865  * generates files for aggregates (module, struct, class, interface, template, enum),
866  * and any non-aggregate symbols are put documented in their aggregate parent's
867  * documentation files.
868  *
869  * E.g. all member functions and data members of a class are documented directly in the
870  * file documenting that class instead of in separate files the class documentation would
871  * link to like with HTMLWriterSimple.
872  *
873  * This output results in much less files and lower file size than HTMLWriterSimple, and
874  * is arguably easier to use due to less clicking between files.
875  */
876 class HTMLWriterAggregated: HTMLWriterBase!symbolLinkAggregated
877 {
878 	alias Super = typeof(super);
879 	private alias config = Super.config;
880 	alias writeBreadcrumbs = Super.writeBreadcrumbs;
881 	alias symbolLink = symbolLinkAggregated;
882 
883 	this(ref Config config, File searchIndex,
884 		 TocItem[] tocItems, string[] tocAdditionals)
885 	{
886 		super(config, searchIndex, tocItems, tocAdditionals);
887 	}
888 
889 	// No separator needed; symbols are already in divs.
890 	void writeSeparator(R)(ref R dst) {}
891 
892 	void writeSymbolStart(R)(ref R dst, string link)
893 	{
894 		const isAggregate = !link.canFind("#");
895 		if (!isAggregate)
896 		{
897 			// We need a separate anchor so we can style it separately to
898 			// compensate for fixed breadcrumbs.
899 			dst.put(`<a class="anchor" id="`);
900 			dst.put(link.findSplit("#")[2]);
901 			dst.put(`"></a>`);
902 		}
903 		dst.put(isAggregate ? `<div class="aggregate-symbol">` : `<div class="symbol">`);
904 	}
905 
906 	void writeSymbolEnd(R)(ref R dst) { dst.put(`</div>`); }
907 
908 	void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode)
909 	{
910 		dst.put(`<div class="description">`); descriptionCode(); dst.put(`</div>`);
911 	}
912 
913 	auto pushSymbol(string[] symbolStackRaw, SymbolDatabase database,
914 					ref bool first, ref string itemURL)
915 	{
916 		assert(symbolStackRaw.length >= moduleNameLength_,
917 			   "symbol stack shorter than module name");
918 
919 		// A symbol-type-aware stack.
920 		auto symbolStack = database.symbolStack(symbolStackRaw[0 .. moduleNameLength],
921 												symbolStackRaw[moduleNameLength .. $]);
922 
923 		// Is this symbol an aggregate?
924 		// If the last part of the symbol stack (this symbol) is an aggregate, we
925 		// create a new file for it. Otherwise we write into parent aggregate's file.
926 		bool isAggregate;
927 		// The deepest level in the symbol stack that is an aggregate symbol.
928 		// If this symbol is an aggregate, that's symbolStack.walkLength - 1, if
929 		// this symbol is not an aggregate but its parent is, that's
930 		// symbolStack.walkLength - 2, etc.
931 		size_t deepestAggregateLevel = size_t.max;
932 		size_t nameDepth;
933 		foreach (name; symbolStack)
934 		{
935 			scope(exit) { ++nameDepth; }
936 			final switch(name.type) with(SymbolType)
937 			{
938 				case Module, Package, Class, Struct, Interface, Enum, Template:
939 					isAggregate = true;
940 					deepestAggregateLevel = nameDepth;
941 					break;
942 				case Function, Variable, Alias, Value:
943 					isAggregate = false;
944 					break;
945 			}
946 		}
947 
948 		symbolFileStack.length = symbolFileStack.length + 1;
949 		addSearchEntry(symbolStack);
950 
951 		// Name stack of the symbol in the documentation file of which we will
952 		// write, except the module name part.
953 		string[] targetSymbolStack;
954 		size_t fileDepth;
955 		// If the symbol is not an aggregate, its docs will be written into its
956 		// closest aggregate parent.
957 		if (!isAggregate)
958 		{
959 			assert(deepestAggregateLevel != size_t.max,
960 				   "A non-aggregate with no aggregate parent; maybe modules " ~
961 				   "are not considered aggregates? (we can't handle that case)");
962 
963 			// Write into the file for the deepest aggregate parent (+1 is
964 			// needed to include the name of the parent itself)
965 			targetSymbolStack =
966 				symbolStackRaw[moduleNameLength_ .. deepestAggregateLevel + 1];
967 
968 			// Going relatively from the end, as the symbolFileStack does not
969 			// contain items for some or all top-most packages.
970 			fileDepth = symbolFileStack.length -
971 						(symbolStackRaw.length - deepestAggregateLevel) - 1;
972 		}
973 		// If the symbol is an aggregate, it will have a file just for itself.
974 		else
975 		{
976 			// The symbol itself is the target.
977 			targetSymbolStack = symbolStackRaw[moduleNameLength_ .. $];
978 			// Parent is the second last element of symbolFileStack
979 			fileDepth = symbolFileStack.length - 2;
980 			assert(fileDepth < symbolFileStack.length,
981 				   "integer overflow (symbolFileStack should have length >= 2 here): %s %s"
982 				   .format(fileDepth, symbolFileStack.length));
983 		}
984 
985 		// Path relative to output directory
986 		string docFileName = targetSymbolStack.empty
987 			? moduleFileBase_ ~ ".html"
988 			: moduleFileBase_.buildPath(targetSymbolStack.joiner(".").array.to!string).setExtension("html");
989 		itemURL = symbolLink(symbolStack);
990 
991 		// Look for a file if it already exists, create if it does not.
992 		File* p = docFileName in symbolFileStack[fileDepth];
993 		first = p is null;
994 		if (first)
995 		{
996 			auto f = File(config.outputDirectory.buildPath(docFileName), "w");
997 			symbolFileStack[fileDepth][docFileName] = f;
998 			return f.lockingTextWriter;
999 		}
1000 		else return p.lockingTextWriter;
1001 	}
1002 }
1003 
1004 /** symbolLink implementation for HTMLWriterSimple.
1005  *
1006  * See_Also: symbolLinkAggregated
1007  */
1008 string symbolLinkSimple(SymbolStack)(auto ref SymbolStack nameStack)
1009 {
1010 	if (nameStack.empty)
1011 		return "UNKNOWN.html";
1012 	// Start with the first part of the name so we have something we can buildPath() with.
1013 	string result = nameStack.front.name;
1014 	const firstType = nameStack.front.type;
1015 	bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package;
1016 	nameStack.popFront();
1017 
1018 	foreach (name; nameStack)
1019 		final switch(name.type) with(SymbolType)
1020 	{
1021 		// A new directory is created for each module
1022 		case Module, Package:
1023 			result = result.buildPath(name.name);
1024 			moduleParent = true;
1025 			break;
1026 		// These symbol types have separate files in a module directory.
1027 		case Class, Struct, Interface, Enum, Function, Variable, Alias, Template:
1028 			// If last name was module/package, the file will be in its
1029 			// directory. Otherwise it will be in the same dir as the parent.
1030 			result = moduleParent ? result.buildPath(name.name)
1031 								   : result ~ "." ~ name.name;
1032 			moduleParent = false;
1033 			break;
1034 		// Enum members are documented in their enums.
1035 		case Value: result = result; break;
1036 	}
1037 
1038 	return result ~ ".html";
1039 }
1040 
1041 class HTMLWriterSimple: HTMLWriterBase!symbolLinkSimple
1042 {
1043 	alias Super = typeof(super);
1044 	private alias config = Super.config;
1045 	alias writeBreadcrumbs = Super.writeBreadcrumbs;
1046 	alias symbolLink = symbolLinkSimple;
1047 
1048 	this(ref Config config, File searchIndex,
1049 		 TocItem[] tocItems, string[] tocAdditionals)
1050 	{
1051 		super(config, searchIndex, tocItems, tocAdditionals);
1052 	}
1053 
1054 	/// Write a separator (e.g. between two overloads of a function)
1055 	void writeSeparator(R)(ref R dst) { dst.put("<hr/>"); }
1056 
1057 	/// Do nothing. No divs needed as every symbol is in a separate file.
1058 	void writeSymbolStart(R)(ref R dst, string link) { }
1059 	/// Do nothing. No divs needed as every symbol is in a separate file.
1060 	void writeSymbolEnd(R)(ref R dst) { }
1061 
1062 	void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode)
1063 	{
1064 		descriptionCode();
1065 	}
1066 
1067 	auto pushSymbol(string[] symbolStack, SymbolDatabase database,
1068 					ref bool first, ref string itemURL)
1069 	{
1070 		symbolFileStack.length = symbolFileStack.length + 1;
1071 
1072 		assert(symbolStack.length >= moduleNameLength_,
1073 			   "symbol stack shorter than module name");
1074 
1075 		auto tail = symbolStack[moduleNameLength_ .. $];
1076 		// Path relative to output directory
1077 		const docFileName = tail.empty
1078 			? moduleFileBase_ ~ ".html"
1079 			: moduleFileBase_.buildPath(tail.joiner(".").array.to!string).setExtension("html");
1080 
1081 		addSearchEntry(database.symbolStack(symbolStack[0 .. moduleNameLength],
1082 											symbolStack[moduleNameLength .. $]));
1083 
1084 		// The second last element of symbolFileStack
1085 		immutable size_t i = symbolFileStack.length - 2;
1086 		assert (i < symbolFileStack.length, "%s %s".format(i, symbolFileStack.length));
1087 		auto p = docFileName in symbolFileStack[i];
1088 		first = p is null;
1089 		itemURL = docFileName;
1090 		if (first)
1091 		{
1092 			first = true;
1093 			auto f = File(config.outputDirectory.buildPath(docFileName), "w");
1094 			symbolFileStack[i][docFileName] = f;
1095 			return f.lockingTextWriter;
1096 		}
1097 		else return p.lockingTextWriter;
1098 	}
1099 }
1100 
1101 enum HTML_END = `
1102 </div>
1103 <footer>
1104 Generated with <a href="https://gitlab.com/basile.b/harbored-mod">harbored-mod</a>
1105 </footer>
1106 </div>
1107 <script>anchors.add();</script>
1108 </body>
1109 </html>`;
1110 
1111 private:
1112 
1113 string prettySectionName(string sectionName)
1114 {
1115 	switch (sectionName)
1116 	{
1117 		case "See_also", "See_Also", "See also", "See Also": return "See Also:";
1118 		case "Note":   return "Note:";
1119 		case "Params": return "Parameters";
1120 		default:       return sectionName;
1121 	}
1122 }