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