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