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