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">«</a>`.format(link)); 198 put(`<a href="%s#show-toc" class="show" id="show-toc">»</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