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 <title>%s</title> 128 <base href="%s"/> 129 <script src="anchor.js"></script> 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.length ? moduleLink(moduleName.split(".")) : "index.html"; 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 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 + 1 != contracts.length) 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 void writeSpan(C)(string class_, C content) 669 { 670 dst.put(`<span class="%s">%s</span>`.format(class_, content)); 671 } 672 673 // extremely inefficient, rewrite if too much slowdown 674 string formatAttrib(T)(T attr) 675 { 676 auto writer = appender!(char[])(); 677 auto formatter = newFormatter(writer); 678 formatter.format(attr); 679 auto str = writer.data.idup; 680 writer.clear(); 681 import std.ascii: isAlpha; 682 // Sanitize CSS class name for the attribute, 683 auto strSane = str.filter!isAlpha.array.to!string; 684 return `<span class="attr-` ~ strSane ~ `">` ~ str ~ `</span>`; 685 } 686 687 // enum attributes 688 if (EnumMember em = cast(EnumMember) item.node) 689 { 690 if (em.enumMemberAttributes) 691 { 692 writeSpan("extrainfo", em.enumMemberAttributes.map!(a => formatAttrib(a)).joiner(", ")); 693 } 694 } 695 696 // TODO print attributes for everything, and move it to separate function/s 697 if (FunctionDeclaration fd = cast(FunctionDeclaration) item.node) 698 { 699 // Above the function name 700 if (!fd.attributes.empty) 701 { 702 dst.put(`<span class="extrainfo">`); 703 writeSpan("attribs", fd.attributes.map!(a => formatAttrib(a)).joiner(", ")); 704 dst.put(`</span>`); 705 } 706 707 // The actual function name 708 writeName(); 709 710 // Below the function name 711 dst.put(`<span class="extrainfo">`); 712 if (!fd.memberFunctionAttributes.empty) 713 { 714 writeSpan("method-attribs", 715 fd.memberFunctionAttributes.map!(a => formatAttrib(a)).joiner(", ")); 716 } 717 // TODO storage classes don't seem to work. libdparse issue? 718 if (!fd.storageClasses.empty) 719 { 720 writeSpan("stor-classes", fd.storageClasses.map!(a => formatAttrib(a)).joiner(", ")); 721 } 722 dst.put(`</span>`); 723 } 724 // By default, just print the name of the item. 725 else writeName(); 726 dst.put(`</td>`); 727 728 dst.put(`<td>`); 729 if (item.type.length) 730 { 731 void delegate() dg = 732 { 733 dst.put(item.type); 734 if (Declarator decl = cast(Declarator) item.node) 735 { 736 if (!decl.initializer) 737 return; 738 739 import dparse.formatter : fmt = format; 740 dst.put(" = "); 741 fmt(&dst, decl.initializer); 742 } 743 }; 744 writeCodeBlock(dst, dg); 745 } 746 dst.put(`</td><td>%s</td></tr>`.format(item.summary)); 747 } 748 749 /** Write a table of items of specified category. 750 * 751 * Params: 752 * 753 * dst = Range to write to. 754 * items = Items the table will contain. 755 * category = Category of the items, used in heading, E.g. "Functions" or 756 * "Variables" or "Structs". 757 */ 758 public void writeItems(R)(ref R dst, Item[] items, string category) 759 { 760 if (category.length) 761 dst.put("<h2>%s</h2>".format(category)); 762 dst.put(`<table>`); 763 foreach (ref i; items) { writeItemEntry(dst, i); } 764 dst.put(`</table>`); 765 } 766 767 /** Formats an AST node to a string. 768 */ 769 public string formatNode(T)(const T t) 770 { 771 auto writer = appender!string(); 772 auto formatter = newFormatter(writer); 773 scope(exit) destroy(formatter.sink); 774 formatter.format(t); 775 return writer.data; 776 } 777 778 const(Config)* config; 779 string[string] macros; 780 File searchIndex; 781 TocItem[] tocItems; 782 string[] tocAdditionals; 783 784 /** Stack of associative arrays. 785 * 786 * Each level contains documentation page files of members of the symbol at that 787 * level; e.g. symbolFileStack[0] contains the module documentation file, 788 * symbolFileStack[1] doc pages of the module's child classes, and so on. 789 * 790 * Note that symbolFileStack levels correspond to symbol stack levels. Depending 791 * on the HTMLWriter implementation, there may not be files for all levels. 792 * 793 * E.g. with HTMLWriterAggregated, if we have a class called `Class.method.NestedClass`, 794 * when writing `NestedClass` docs symbolFileStack[$ - 3 .. 0] will be something like: 795 * `[["ClassFileName": File(stuff)], [], ["NestedClassFileName": * File(stuff)]]`, 796 * i.e. there will be a stack level for `method` but it will have no contents. 797 * 798 * When popSymbol() is called, all doc page files of that symbol's members are closed 799 * (they must be kept open until then to ensure overloads are put into the same file). 800 */ 801 File[string][] symbolFileStack; 802 803 string moduleFileBase_; 804 // Path to the HTML file relative to the output directory. 805 string moduleLink_; 806 // Name length of the module (e.g. 2 for std.stdio) 807 size_t moduleNameLength_; 808 } 809 810 /** Get a link to a symbol. 811 * 812 * Note: this does not check if the symbol exists; calling symbolLink() with a SymbolStack 813 * of a nonexistent symbol will result in a link to the deepest existing parent symbol. 814 * 815 * Params: nameStack = SymbolStack returned by SymbolDatabase.symbolStack(), 816 * describing a fully qualified symbol name. 817 * 818 * Returns: Link to the file with documentation for the symbol. 819 */ 820 string symbolLinkAggregated(SymbolStack)(auto ref SymbolStack nameStack) 821 { 822 if (nameStack.empty) 823 return "UNKNOWN.html"; 824 // Start with the first part of the name so we have something we can buildPath() with. 825 string result = nameStack.front.name; 826 const firstType = nameStack.front.type; 827 bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package; 828 nameStack.popFront(); 829 830 bool inAnchor; 831 foreach (name; nameStack) 832 final switch(name.type) with(SymbolType) 833 { 834 // A new directory is created for each module 835 case Module, Package: 836 result = result.buildPath(name.name); 837 moduleParent = true; 838 break; 839 // These symbol types have separate files in a module directory. 840 case Class, Struct, Interface, Enum, Template: 841 // If last name was module/package, the file will be in its 842 // directory. Otherwise it will be in the same dir as the parent. 843 result = moduleParent ? result.buildPath(name.name) 844 : result ~ "." ~ name.name; 845 moduleParent = false; 846 break; 847 // These symbol types are documented in their parent symbol's files. 848 case Function, Variable, Alias, Value: 849 // inAnchor allows us to handle nested functions, which are still 850 // documented in the same file as their parent function. 851 // E.g. a nested function called entity.EntityManager.foo.bar will 852 // have link entity/EntityManager#foo.bar 853 result = inAnchor ? result ~ "." ~ name.name 854 : result ~ ".html#" ~ name.name; 855 inAnchor = true; 856 break; 857 } 858 859 return result ~ (inAnchor ? "" : ".html"); 860 } 861 862 /** A HTML writer generating 'aggregated' HTML documentation. 863 * 864 * Instead of generating a separate file for every variable or function, this only 865 * generates files for aggregates (module, struct, class, interface, template, enum), 866 * and any non-aggregate symbols are put documented in their aggregate parent's 867 * documentation files. 868 * 869 * E.g. all member functions and data members of a class are documented directly in the 870 * file documenting that class instead of in separate files the class documentation would 871 * link to like with HTMLWriterSimple. 872 * 873 * This output results in much less files and lower file size than HTMLWriterSimple, and 874 * is arguably easier to use due to less clicking between files. 875 */ 876 class HTMLWriterAggregated: HTMLWriterBase!symbolLinkAggregated 877 { 878 alias Super = typeof(super); 879 private alias config = Super.config; 880 alias writeBreadcrumbs = Super.writeBreadcrumbs; 881 alias symbolLink = symbolLinkAggregated; 882 883 this(ref Config config, File searchIndex, 884 TocItem[] tocItems, string[] tocAdditionals) 885 { 886 super(config, searchIndex, tocItems, tocAdditionals); 887 } 888 889 // No separator needed; symbols are already in divs. 890 void writeSeparator(R)(ref R dst) {} 891 892 void writeSymbolStart(R)(ref R dst, string link) 893 { 894 const isAggregate = !link.canFind("#"); 895 if (!isAggregate) 896 { 897 // We need a separate anchor so we can style it separately to 898 // compensate for fixed breadcrumbs. 899 dst.put(`<a class="anchor" id="`); 900 dst.put(link.findSplit("#")[2]); 901 dst.put(`"></a>`); 902 } 903 dst.put(isAggregate ? `<div class="aggregate-symbol">` : `<div class="symbol">`); 904 } 905 906 void writeSymbolEnd(R)(ref R dst) { dst.put(`</div>`); } 907 908 void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode) 909 { 910 dst.put(`<div class="description">`); descriptionCode(); dst.put(`</div>`); 911 } 912 913 auto pushSymbol(string[] symbolStackRaw, SymbolDatabase database, 914 ref bool first, ref string itemURL) 915 { 916 assert(symbolStackRaw.length >= moduleNameLength_, 917 "symbol stack shorter than module name"); 918 919 // A symbol-type-aware stack. 920 auto symbolStack = database.symbolStack(symbolStackRaw[0 .. moduleNameLength], 921 symbolStackRaw[moduleNameLength .. $]); 922 923 // Is this symbol an aggregate? 924 // If the last part of the symbol stack (this symbol) is an aggregate, we 925 // create a new file for it. Otherwise we write into parent aggregate's file. 926 bool isAggregate; 927 // The deepest level in the symbol stack that is an aggregate symbol. 928 // If this symbol is an aggregate, that's symbolStack.walkLength - 1, if 929 // this symbol is not an aggregate but its parent is, that's 930 // symbolStack.walkLength - 2, etc. 931 size_t deepestAggregateLevel = size_t.max; 932 size_t nameDepth; 933 foreach (name; symbolStack) 934 { 935 scope(exit) { ++nameDepth; } 936 final switch(name.type) with(SymbolType) 937 { 938 case Module, Package, Class, Struct, Interface, Enum, Template: 939 isAggregate = true; 940 deepestAggregateLevel = nameDepth; 941 break; 942 case Function, Variable, Alias, Value: 943 isAggregate = false; 944 break; 945 } 946 } 947 948 symbolFileStack.length = symbolFileStack.length + 1; 949 addSearchEntry(symbolStack); 950 951 // Name stack of the symbol in the documentation file of which we will 952 // write, except the module name part. 953 string[] targetSymbolStack; 954 size_t fileDepth; 955 // If the symbol is not an aggregate, its docs will be written into its 956 // closest aggregate parent. 957 if (!isAggregate) 958 { 959 assert(deepestAggregateLevel != size_t.max, 960 "A non-aggregate with no aggregate parent; maybe modules " ~ 961 "are not considered aggregates? (we can't handle that case)"); 962 963 // Write into the file for the deepest aggregate parent (+1 is 964 // needed to include the name of the parent itself) 965 targetSymbolStack = 966 symbolStackRaw[moduleNameLength_ .. deepestAggregateLevel + 1]; 967 968 // Going relatively from the end, as the symbolFileStack does not 969 // contain items for some or all top-most packages. 970 fileDepth = symbolFileStack.length - 971 (symbolStackRaw.length - deepestAggregateLevel) - 1; 972 } 973 // If the symbol is an aggregate, it will have a file just for itself. 974 else 975 { 976 // The symbol itself is the target. 977 targetSymbolStack = symbolStackRaw[moduleNameLength_ .. $]; 978 // Parent is the second last element of symbolFileStack 979 fileDepth = symbolFileStack.length - 2; 980 assert(fileDepth < symbolFileStack.length, 981 "integer overflow (symbolFileStack should have length >= 2 here): %s %s" 982 .format(fileDepth, symbolFileStack.length)); 983 } 984 985 // Path relative to output directory 986 string docFileName = targetSymbolStack.empty 987 ? moduleFileBase_ ~ ".html" 988 : moduleFileBase_.buildPath(targetSymbolStack.joiner(".").array.to!string).setExtension("html"); 989 itemURL = symbolLink(symbolStack); 990 991 // Look for a file if it already exists, create if it does not. 992 File* p = docFileName in symbolFileStack[fileDepth]; 993 first = p is null; 994 if (first) 995 { 996 auto f = File(config.outputDirectory.buildPath(docFileName), "w"); 997 symbolFileStack[fileDepth][docFileName] = f; 998 return f.lockingTextWriter; 999 } 1000 else return p.lockingTextWriter; 1001 } 1002 } 1003 1004 /** symbolLink implementation for HTMLWriterSimple. 1005 * 1006 * See_Also: symbolLinkAggregated 1007 */ 1008 string symbolLinkSimple(SymbolStack)(auto ref SymbolStack nameStack) 1009 { 1010 if (nameStack.empty) 1011 return "UNKNOWN.html"; 1012 // Start with the first part of the name so we have something we can buildPath() with. 1013 string result = nameStack.front.name; 1014 const firstType = nameStack.front.type; 1015 bool moduleParent = firstType == SymbolType.Module || firstType == SymbolType.Package; 1016 nameStack.popFront(); 1017 1018 foreach (name; nameStack) 1019 final switch(name.type) with(SymbolType) 1020 { 1021 // A new directory is created for each module 1022 case Module, Package: 1023 result = result.buildPath(name.name); 1024 moduleParent = true; 1025 break; 1026 // These symbol types have separate files in a module directory. 1027 case Class, Struct, Interface, Enum, Function, Variable, Alias, Template: 1028 // If last name was module/package, the file will be in its 1029 // directory. Otherwise it will be in the same dir as the parent. 1030 result = moduleParent ? result.buildPath(name.name) 1031 : result ~ "." ~ name.name; 1032 moduleParent = false; 1033 break; 1034 // Enum members are documented in their enums. 1035 case Value: result = result; break; 1036 } 1037 1038 return result ~ ".html"; 1039 } 1040 1041 class HTMLWriterSimple: HTMLWriterBase!symbolLinkSimple 1042 { 1043 alias Super = typeof(super); 1044 private alias config = Super.config; 1045 alias writeBreadcrumbs = Super.writeBreadcrumbs; 1046 alias symbolLink = symbolLinkSimple; 1047 1048 this(ref Config config, File searchIndex, 1049 TocItem[] tocItems, string[] tocAdditionals) 1050 { 1051 super(config, searchIndex, tocItems, tocAdditionals); 1052 } 1053 1054 /// Write a separator (e.g. between two overloads of a function) 1055 void writeSeparator(R)(ref R dst) { dst.put("<hr/>"); } 1056 1057 /// Do nothing. No divs needed as every symbol is in a separate file. 1058 void writeSymbolStart(R)(ref R dst, string link) { } 1059 /// Do nothing. No divs needed as every symbol is in a separate file. 1060 void writeSymbolEnd(R)(ref R dst) { } 1061 1062 void writeSymbolDescription(R)(ref R dst, void delegate() descriptionCode) 1063 { 1064 descriptionCode(); 1065 } 1066 1067 auto pushSymbol(string[] symbolStack, SymbolDatabase database, 1068 ref bool first, ref string itemURL) 1069 { 1070 symbolFileStack.length = symbolFileStack.length + 1; 1071 1072 assert(symbolStack.length >= moduleNameLength_, 1073 "symbol stack shorter than module name"); 1074 1075 auto tail = symbolStack[moduleNameLength_ .. $]; 1076 // Path relative to output directory 1077 const docFileName = tail.empty 1078 ? moduleFileBase_ ~ ".html" 1079 : moduleFileBase_.buildPath(tail.joiner(".").array.to!string).setExtension("html"); 1080 1081 addSearchEntry(database.symbolStack(symbolStack[0 .. moduleNameLength], 1082 symbolStack[moduleNameLength .. $])); 1083 1084 // The second last element of symbolFileStack 1085 immutable size_t i = symbolFileStack.length - 2; 1086 assert (i < symbolFileStack.length, "%s %s".format(i, symbolFileStack.length)); 1087 auto p = docFileName in symbolFileStack[i]; 1088 first = p is null; 1089 itemURL = docFileName; 1090 if (first) 1091 { 1092 first = true; 1093 auto f = File(config.outputDirectory.buildPath(docFileName), "w"); 1094 symbolFileStack[i][docFileName] = f; 1095 return f.lockingTextWriter; 1096 } 1097 else return p.lockingTextWriter; 1098 } 1099 } 1100 1101 enum HTML_END = ` 1102 </div> 1103 <footer> 1104 Generated with <a href="https://gitlab.com/basile.b/harbored-mod">harbored-mod</a> 1105 </footer> 1106 </div> 1107 <script>anchors.add();</script> 1108 </body> 1109 </html>`; 1110 1111 private: 1112 1113 string prettySectionName(string sectionName) 1114 { 1115 switch (sectionName) 1116 { 1117 case "See_also", "See_Also", "See also", "See Also": return "See Also:"; 1118 case "Note": return "Note:"; 1119 case "Params": return "Parameters"; 1120 default: return sectionName; 1121 } 1122 }