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