1 /** 2 * D Documentation Generator 3 * Copyright: © 2014 Economic Modeling Specialists, Intl. 4 * Authors: Brian Schott 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0) 6 */ 7 module visitor; 8 9 import ddoc.comments; 10 import std.algorithm; 11 import dparse.ast; 12 import dparse.formatter; 13 import dparse.lexer; 14 import std.file; 15 import std.path; 16 import std.stdio; 17 import std.typecons; 18 import unittest_preprocessor; 19 20 21 /** 22 * Generates documentation for a module. 23 */ 24 class DocVisitor : ASTVisitor 25 { 26 /** 27 * Params: 28 * outputDirectory = The directory where files will be written 29 * macros = Macro definitions used in processing documentation comments 30 * searchIndex = A file where the search information will be written 31 * unitTestMapping = The mapping of declaration addresses to their 32 * documentation unittests 33 * fileBytes = The source code of the module as a byte array. 34 */ 35 this(string outputDirectory, string[string] macros, File searchIndex, 36 const TestRange[][size_t] unitTestMapping, const(ubyte[]) fileBytes) 37 { 38 this.outputDirectory = outputDirectory; 39 this.macros = macros; 40 this.searchIndex = searchIndex; 41 this.unitTestMapping = unitTestMapping; 42 this.fileBytes = fileBytes; 43 } 44 45 override void visit(const Module mod) 46 { 47 import std.array : array; 48 import std.algorithm : map; 49 import std.path : dirName; 50 import std.range : chain, only, join; 51 import std.file : mkdirRecurse; 52 import std.conv : to; 53 54 if (mod.moduleDeclaration is null) 55 return; 56 pushAttributes(); 57 stack = cast(string[]) mod.moduleDeclaration.moduleName.identifiers.map!(a => a.text).array; 58 baseLength = stack.length; 59 moduleFileBase = chain(only(outputDirectory), stack).buildPath; 60 if (!exists(moduleFileBase.dirName())) 61 moduleFileBase.dirName().mkdirRecurse(); 62 File output = File(moduleFileBase ~ ".html", "w"); 63 64 location = output.name; 65 moduleName = to!string(stack.join(".")); 66 writeHeader(output, moduleName, baseLength - 1); 67 68 writeBreadcrumbs(output); 69 70 if (mod.moduleDeclaration.comment !is null) 71 readAndWriteComment(output, mod.moduleDeclaration.comment, macros, 72 prevComments, null, getUnittestDocTuple(mod.moduleDeclaration)); 73 74 memberStack.length = 1; 75 76 mod.accept(this); 77 78 memberStack[$ - 1].write(output, outputDirectory); 79 80 output.writeln(HTML_END); 81 output.close(); 82 } 83 84 override void visit(const EnumDeclaration ed) 85 { 86 enum formattingCode = q{ 87 f.write("enum ", ad.name.text); 88 if (ad.type !is null) 89 { 90 f.write(" : "); 91 formatter.format(ad.type); 92 } 93 }; 94 visitAggregateDeclaration!(formattingCode, "enums")(ed); 95 } 96 97 override void visit(const EnumMember member) 98 { 99 if (member.comment is null) 100 return; 101 string summary = readAndWriteComment(File.init, member.comment, macros, 102 prevComments, null, getUnittestDocTuple(member)); 103 memberStack[$ - 1].values ~= Item("#", member.name.text, summary); 104 } 105 106 override void visit(const ClassDeclaration cd) 107 { 108 enum formattingCode = q{ 109 f.write("class ", ad.name.text); 110 if (ad.baseClassList !is null) 111 formatter.format(ad.baseClassList); 112 if (ad.templateParameters !is null) 113 formatter.format(ad.templateParameters); 114 if (ad.constraint !is null) 115 formatter.format(ad.constraint); 116 }; 117 visitAggregateDeclaration!(formattingCode, "classes")(cd); 118 } 119 120 override void visit(const TemplateDeclaration td) 121 { 122 enum formattingCode = q{ 123 f.write("template ", ad.name.text); 124 if (ad.templateParameters !is null) 125 formatter.format(ad.templateParameters); 126 if (ad.constraint) 127 formatter.format(ad.constraint); 128 }; 129 visitAggregateDeclaration!(formattingCode, "templates")(td); 130 } 131 132 override void visit(const StructDeclaration sd) 133 { 134 enum formattingCode = q{ 135 f.write("struct ", ad.name.text); 136 if (ad.templateParameters) 137 formatter.format(ad.templateParameters); 138 if (ad.constraint) 139 formatter.format(ad.constraint); 140 }; 141 visitAggregateDeclaration!(formattingCode, "structs")(sd); 142 } 143 144 override void visit(const InterfaceDeclaration id) 145 { 146 enum formattingCode = q{ 147 if (ad.baseClassList !is null) 148 formatter.format(ad.baseClassList); 149 if (ad.templateParameters !is null) 150 formatter.format(ad.templateParameters); 151 if (ad.constraint !is null) 152 formatter.format(ad.constraint); 153 }; 154 visitAggregateDeclaration!(formattingCode, "interfaces")(id); 155 } 156 157 override void visit(const AliasDeclaration ad) 158 { 159 import std.path : dirSeparator; 160 if (ad.comment is null) 161 return; 162 bool first; 163 if (ad.declaratorIdentifierList !is null) 164 { 165 foreach (name; ad.declaratorIdentifierList.identifiers) 166 { 167 File f = pushSymbol(name.text, first); 168 scope(exit) popSymbol(f); 169 writeBreadcrumbs(f); 170 string type = writeAliasType(f, name.text, ad.type); 171 string summary = readAndWriteComment(f, ad.comment, macros, prevComments); 172 memberStack[$ - 2].aliases ~= Item(f.name, name.text, summary, type); 173 } 174 } 175 else foreach (initializer; ad.initializers) 176 { 177 File f = pushSymbol(initializer.name.text, first); 178 scope(exit) popSymbol(f); 179 writeBreadcrumbs(f); 180 string type = writeAliasType(f, initializer.name.text, initializer.type); 181 string summary = readAndWriteComment(f, ad.comment, macros, prevComments); 182 memberStack[$ - 2].aliases ~= Item(f.name, initializer.name.text, summary, type); 183 } 184 } 185 186 override void visit(const VariableDeclaration vd) 187 { 188 bool first; 189 foreach (const Declarator dec; vd.declarators) 190 { 191 if (dec.comment is null) 192 continue; 193 File f = pushSymbol(dec.name.text, first); 194 scope(exit) popSymbol(f); 195 writeBreadcrumbs(f); 196 string summary = readAndWriteComment(f, dec.comment, macros, prevComments); 197 memberStack[$ - 2].variables ~= Item(f.name, dec.name.text, summary, formatNode(vd.type)); 198 } 199 if (vd.comment is null || vd.autoDeclaration is null) 200 return; 201 foreach (part; vd.autoDeclaration.parts) 202 { 203 File f = pushSymbol(part.identifier.text, first); 204 scope(exit) popSymbol(f); 205 writeBreadcrumbs(f); 206 string summary = readAndWriteComment(f, vd.comment, macros, prevComments); 207 string storageClass; 208 foreach (storage; vd.storageClasses) 209 { 210 if (storage !is null) 211 storageClass = str(storage.token.type); 212 } 213 auto i = Item(f.name, part.identifier.text, 214 summary, storageClass == "enum" ? null : "auto"); 215 if (storageClass == "enum") 216 memberStack[$ - 2].enums ~= i; 217 else 218 memberStack[$ - 2].variables ~= i; 219 } 220 } 221 222 override void visit(const StructBody sb) 223 { 224 pushAttributes(); 225 sb.accept(this); 226 popAttributes(); 227 } 228 229 override void visit(const BlockStatement bs) 230 { 231 pushAttributes(); 232 bs.accept(this); 233 popAttributes(); 234 } 235 236 override void visit(const Declaration dec) 237 { 238 attributes[$ - 1] ~= dec.attributes; 239 dec.accept(this); 240 if (dec.attributeDeclaration is null) 241 attributes[$ - 1] = attributes[$ - 1][0 .. $ - dec.attributes.length]; 242 } 243 244 override void visit(const AttributeDeclaration dec) 245 { 246 attributes[$ - 1] ~= dec.attribute; 247 } 248 249 override void visit(const Constructor cons) 250 { 251 if (cons.comment is null) 252 return; 253 bool first; 254 File f = pushSymbol("this", first); 255 writeFnDocumentation(f, cons, attributes[$ - 1], first); 256 } 257 258 override void visit(const FunctionDeclaration fd) 259 { 260 if (fd.comment is null) 261 return; 262 bool first; 263 File f = pushSymbol(fd.name.text, first); 264 writeFnDocumentation(f, fd, attributes[$ - 1], first); 265 } 266 267 // Deliberately skip unittests 268 override void visit(const Unittest) {} 269 270 alias visit = ASTVisitor.visit; 271 272 /// The module name in "package.package.module" format. 273 string moduleName; 274 275 /// The path to the HTML file that was generated for the module being 276 /// processed. 277 string location; 278 279 invariant 280 { 281 foreach (c; prevComments[]) 282 { 283 assert(c.sections.length >= 2); 284 } 285 } 286 287 private: 288 289 void visitAggregateDeclaration(string formattingCode, string name, A)(const A ad) 290 { 291 bool first; 292 if (ad.comment is null) 293 return; 294 File f = pushSymbol(ad.name.text, first); 295 if (first) 296 writeBreadcrumbs(f); 297 else 298 f.writeln("<hr/>"); 299 { 300 auto writer = f.lockingTextWriter(); 301 writer.put(`<pre><code>`); 302 auto formatter = new Formatter!(File.LockingTextWriter)(writer); 303 scope(exit) formatter.sink = File.LockingTextWriter.init; 304 writeAttributes(formatter, writer, attributes[$ - 1]); 305 mixin(formattingCode); 306 writer.put("\n</code></pre>"); 307 } 308 string summary = readAndWriteComment(f, ad.comment, macros, prevComments, 309 null, getUnittestDocTuple(ad)); 310 mixin(`memberStack[$ - 2].` ~ name ~ ` ~= Item(f.name, ad.name.text, summary);`); 311 ad.accept(this); 312 memberStack[$ - 1].write(f, outputDirectory); 313 314 stack = stack[0 .. $ - 1]; 315 memberStack = memberStack[0 .. $ - 1]; 316 } 317 318 /** 319 * Params: 320 * t = The declaration. 321 * Returns: An array of tuples where the first item is the contents of the 322 * unittest block and the second item is the doc comment for the 323 * unittest block. This array may be empty. 324 */ 325 Tuple!(string, string)[] getUnittestDocTuple(T)(const T t) 326 { 327 immutable size_t index = cast(size_t) (cast(void*) t); 328 // writeln("Searching for unittest associated with ", index); 329 auto tupArray = index in unitTestMapping; 330 if (tupArray is null) 331 return []; 332 // writeln("Found a doc unit test for ", cast(size_t) &t); 333 Tuple!(string, string)[] rVal; 334 foreach (tup; *tupArray) 335 rVal ~= tuple(cast(string) fileBytes[tup[0] + 2 .. tup[1]], tup[2]); 336 return rVal; 337 } 338 339 /** 340 * 341 */ 342 void writeFnDocumentation(Fn)(File f, Fn fn, const(Attribute)[] attrs, bool first) 343 { 344 auto writer = f.lockingTextWriter(); 345 if (first) 346 writeBreadcrumbs(f); 347 else 348 writer.put("<hr/>"); 349 auto formatter = new Formatter!(File.LockingTextWriter)(writer); 350 scope(exit) formatter.sink = File.LockingTextWriter.init; 351 writer.put(`<pre><code>`); 352 writeAttributes(formatter, writer, attrs); 353 static if (__traits(hasMember, typeof(fn), "returnType")) 354 { 355 if (fn.returnType) 356 { 357 formatter.format(fn.returnType); 358 writer.put(" "); 359 } 360 formatter.format(fn.name); 361 } 362 else 363 writer.put("this"); 364 if (fn.templateParameters !is null) 365 formatter.format(fn.templateParameters); 366 if (fn.parameters !is null) 367 formatter.format(fn.parameters); 368 foreach (a; fn.memberFunctionAttributes) 369 { 370 writer.put(" "); 371 formatter.format(a); 372 } 373 if (fn.constraint) 374 { 375 writer.put(" "); 376 formatter.format(fn.constraint); 377 } 378 writer.put("\n</code></pre>"); 379 string summary = readAndWriteComment(f, fn.comment, macros, 380 prevComments, fn.functionBody, getUnittestDocTuple(fn)); 381 string fdName; 382 static if (__traits(hasMember, typeof(fn), "name")) 383 fdName = fn.name.text; 384 else 385 fdName = "this"; 386 memberStack[$ - 2].functions ~= Item(f.name, fdName, summary); 387 fn.accept(this); 388 stack = stack[0 .. $ - 1]; 389 memberStack = memberStack[0 .. $ - 1]; 390 } 391 392 /** 393 * Writes attributes to the given writer using the given formatter. 394 * Params: 395 * F = The formatter type 396 * W = The writer type 397 * formatter = The formatter instance to use 398 * writer = The writer that will be output to. 399 * attrs = The attributes to write. 400 */ 401 void writeAttributes(F, W)(F formatter, W writer, const(Attribute)[] attrs) 402 { 403 IdType protection; 404 if (attrs is null) 405 attrs = attributes[$ - 1]; 406 if (attributes.length > 0) foreach (a; attrs) 407 { 408 if (isProtection(a.attribute.type)) 409 protection = a.attribute.type; 410 } 411 switch (protection) 412 { 413 case tok!"private": writer.put("private "); break; 414 case tok!"package": writer.put("package "); break; 415 default: writer.put("public "); break; 416 } 417 if (attributes.length > 0) foreach (a; attrs) 418 { 419 if (!isProtection(a.attribute.type)) 420 { 421 formatter.format(a); 422 writer.put(" "); 423 } 424 } 425 } 426 427 /** 428 * Formats an AST node to a string 429 */ 430 static string formatNode(T)(const T t) 431 { 432 import std.array; 433 auto writer = appender!string(); 434 auto formatter = new Formatter!(typeof(writer))(writer); 435 formatter.format(t); 436 return writer.data; 437 } 438 439 /** 440 * Writes an alias' type to the given file and returns it. 441 * Params: 442 * f = The file to write to 443 * name = the name of the alias 444 * t = the aliased type 445 * Returns: A string reperesentation of the given type. 446 */ 447 static string writeAliasType(File f, string name, const Type t) 448 { 449 if (t is null) 450 return null; 451 f.write(`<pre><code>`); 452 f.write("alias ", name, " = "); 453 string formatted = formatNode(t); 454 f.write(formatted); 455 f.writeln(`</code></pre>`); 456 return formatted; 457 } 458 459 /** 460 * Writes navigation breadcrumbs in HTML format to the given file. 461 */ 462 void writeBreadcrumbs(File f) 463 { 464 import std.array : join; 465 import std.conv : to; 466 import std.range : chain, only; 467 f.writeln(`<div class="breadcrumbs">`); 468 f.writeln(`<table id="results"></table>`); 469 f.writeln(`<input type="search" id="search" placeholder="Search" onkeyup="searchSubmit(this.value, event)"/>`); 470 foreach (i; 0 .. stack.length) 471 { 472 if (i + 1 < stack.length) 473 { 474 if (i >= baseLength - 1) 475 { 476 string link = buildPath(chain(stack[0 .. baseLength - 1], 477 only(stack[baseLength - 1.. $ - 1].join(".")))); 478 // writeln(link, ".html"); 479 f.write(`<a href="`, stripLeadingDirectory(link, outputDirectory), `.html">`); 480 f.write(stack[i]); 481 f.write(`</a>.`); 482 } 483 else 484 { 485 f.write(stack[i]); 486 f.write("."); 487 } 488 } 489 else 490 f.write(stack[i]); 491 } 492 f.writeln(`</div>`); 493 } 494 495 /** 496 * Params: 497 * name = The symbol's name 498 * first = True if this is the first time that pushSymbol has been 499 * called for this name. 500 * isFunction = True if the symbol being pushed is a function, false 501 * otherwise. 502 * Returns: A file that the symbol's documentation should be written to. 503 */ 504 File pushSymbol(string name, ref bool first) 505 { 506 import std.array : array, join; 507 import std.string : format; 508 stack ~= name; 509 memberStack.length = memberStack.length + 1; 510 string classDocFileName = format("%s.%s.html", moduleFileBase, 511 join(stack[baseLength .. $], ".").array); 512 immutable string path = stripLeadingDirectory(classDocFileName, outputDirectory); 513 searchIndex.writefln(`{"%s" : "%s"},`, join(stack, ".").array, path); 514 immutable size_t i = memberStack.length - 2; 515 assert (i < memberStack.length, "%s %s".format(i, memberStack.length)); 516 auto p = classDocFileName in memberStack[i].overloadFiles; 517 first = p is null; 518 if (first) 519 { 520 first = true; 521 auto f = File(classDocFileName, "w"); 522 memberStack[i].overloadFiles[classDocFileName] = f; 523 writeHeader(f, name, baseLength - 1); 524 return f; 525 } 526 else 527 return *p; 528 } 529 530 void popSymbol(File f) 531 { 532 f.writeln(HTML_END); 533 stack = stack[0 .. $ - 1]; 534 memberStack = memberStack[0 .. $ - 1]; 535 } 536 537 void pushAttributes() 538 { 539 attributes.length = attributes.length + 1; 540 } 541 542 void popAttributes() 543 { 544 attributes = attributes[0 .. $ - 1]; 545 } 546 547 const(Attribute)[][] attributes; 548 Comment[] prevComments; 549 size_t baseLength; 550 string outputDirectory; 551 string moduleFileBase; 552 string[] stack; 553 string[string] macros; 554 Members[] memberStack; 555 File searchIndex; 556 const TestRange[][size_t] unitTestMapping; 557 const(ubyte[]) fileBytes; 558 } 559 560 /** 561 * Writes HTML header information to the given file. 562 * Params: 563 * f = The file to write to 564 * title = The content of the HTML "title" element 565 * depth = The directory depth of the file. This is used for ensuring that 566 * the "base" element is correct so that links resolve properly. 567 */ 568 void writeHeader(File f, string title, size_t depth) 569 { 570 f.write(`<!DOCTYPE html> 571 <html> 572 <head> 573 <meta charset="utf-8"/> 574 <link rel="stylesheet" type="text/css" href="`); 575 foreach (i; 0 .. depth) 576 f.write("../"); 577 f.write(`style.css"/><script src="`); 578 foreach (i; 0 .. depth) 579 f.write("../"); 580 f.write(`highlight.pack.js"></script> 581 <title>`); 582 f.write(title); 583 f.writeln(`</title>`); 584 f.write(`<base href="`); 585 foreach (i; 0 .. depth) 586 f.write("../"); 587 f.write(`"/> 588 <script src="search.js"></script> 589 </head> 590 <body>`); 591 } 592 593 /** 594 * Writes a doc comment to the given file and returns the summary text. 595 * Params: 596 * f = The file to write the comment to 597 * comment = The comment to write 598 * macros = Macro definitions used in processing the comment 599 * prevComments = Previously encountered comments. This is used for handling 600 * "ditto" comments. May be null. 601 * functionBody = A function body used for writing contract information. May 602 * be null. 603 * testdocs = Pairs of unittest bodies and unittest doc comments. May be null. 604 * Returns: the summary from the given comment 605 */ 606 string readAndWriteComment(File f, string comment, ref string[string] macros, 607 ref Comment[] prevComments, const FunctionBody functionBody = null, 608 Tuple!(string, string)[] testDocs = null) 609 { 610 assert(comment !is null); 611 612 Comment c = parseComment(comment, macros); 613 assert(c.sections.length >= 2); 614 if (c.isDitto) 615 { 616 if (prevComments.length) 617 c = prevComments[$ - 1]; 618 else 619 stderr.writeln("Found 'ditto' comment without preceding comment to copy."); 620 } 621 else 622 prevComments ~= c; 623 624 if (f != File.init) 625 writeComment(f, c, functionBody); 626 string rVal = ""; 627 if (c.sections.length && c.sections[0].content !is null) 628 rVal = c.sections[0].content; 629 else 630 { 631 foreach (section; c.sections) 632 { 633 if (section.name == "Returns") 634 rVal = "Returns: " ~ section.content; 635 } 636 } 637 if (f != File.init && testDocs !is null) foreach (doc; testDocs) 638 { 639 // writeln("Writing a unittest doc comment"); 640 import std.string : outdent; 641 f.writeln(`<div class="section"><h3>Example</h3>`); 642 Comment dc = parseComment(doc[1], macros); 643 writeComment(f, dc); 644 f.writeln(`<pre><code>`, outdent(doc[0]), `</code></pre>`); 645 f.writeln(`</div>`); 646 } 647 return rVal; 648 } 649 /** 650 * Returns: the input string with its first directory removed. 651 */ 652 string stripLeadingDirectory(string s, string outputDirectory) 653 { 654 import std.path : pathSplitter, buildPath; 655 656 auto r1 = pathSplitter(s); 657 auto r2 = pathSplitter(outputDirectory); 658 while (!r1.empty && !r2.empty && r1.front == r2.front) 659 { 660 r1.popFront(); 661 r2.popFront(); 662 } 663 return buildPath(r1); 664 } 665 666 /// 667 unittest 668 { 669 import std.stdio : stderr; 670 671 immutable a = stripLeadingDirectory("foo/bar/baz/", "foo"); 672 assert(a == "bar/baz", a); 673 immutable b = stripLeadingDirectory("/home/user/project/docs", "/home/user/project/docs"); 674 assert(b == "", b); 675 immutable c = stripLeadingDirectory("/home/user/project/docs/file.name.html", 676 "/home/user/project/docs"); 677 assert(c == "file.name.html", c); 678 } 679 680 private: 681 682 void writeComment(File f, Comment comment, const FunctionBody functionBody = null) 683 in 684 { 685 assert(comment.sections.length >= 2); 686 } 687 body 688 { 689 foreach (i; 0 .. 2) 690 { 691 if (comment.sections[i].content is null) 692 continue; 693 f.writeln(`<div class="section">`); 694 f.writeln(comment.sections[i].content); 695 f.writeln(`</div>`); 696 } 697 if (functionBody !is null && functionBody.specifiedFunctionBody !is null && 698 functionBody.specifiedFunctionBody.functionContracts.length > 0) 699 writeContracts(f, functionBody.specifiedFunctionBody.functionContracts); 700 foreach (section; comment.sections[2 .. $]) 701 { 702 if (section.name == "Macros") 703 continue; 704 f.writeln(`<div class="section">`); 705 if (section.name != "Summary" && section.name != "Description") 706 { 707 f.write("<h3>"); 708 f.write(prettySectionName(section.name)); 709 f.writeln("</h3>"); 710 } 711 if (section.name == "Params") 712 { 713 f.writeln(`<table class="params">`); 714 foreach (kv; section.mapping) 715 { 716 f.write(`<tr class="param"><td class="paramName">`); 717 f.write(kv[0]); 718 f.write(`</td><td class="paramDoc">`); 719 f.write(kv[1]); 720 f.writeln("</td></tr>"); 721 } 722 f.write("</table>"); 723 } 724 else 725 { 726 f.writeln(section.content); 727 } 728 f.writeln(`</div>`); 729 } 730 } 731 732 void writeContracts(File f, const FunctionContract[] contracts) 733 { 734 auto writer = f.lockingTextWriter(); 735 auto formatter = new Formatter!(File.LockingTextWriter)(writer); 736 scope(exit) formatter.sink = File.LockingTextWriter.init; 737 writer.put("<div class=\"section\"><h3>Contracts</h3>\n"); 738 foreach (contract; contracts) 739 { 740 writer.put("<pre><code>\n"); 741 formatter.format(contract); 742 writer.put("</code></pre>\n"); 743 } 744 writer.put("</div>\n"); 745 } 746 747 string prettySectionName(string sectionName) 748 { 749 switch (sectionName) 750 { 751 case "See_also": return "See Also"; 752 case "Params": return "Parameters"; 753 default: return sectionName; 754 } 755 } 756 757 enum HTML_END = ` 758 <script>hljs.initHighlightingOnLoad();</script> 759 </body> 760 </html>`; 761 762 struct Item 763 { 764 string url; 765 string name; 766 string summary; 767 string type; 768 769 void write(File f, string outputDirectory) 770 { 771 f.write(`<tr><td>`); 772 if (url == "#") 773 f.write(name, `</td>`); 774 else 775 f.write(`<a href="`, stripLeadingDirectory(url, outputDirectory), `">`, name, `</a></td>`); 776 if (type is null) 777 f.write(`<td></td><td>`, summary ,`</td></tr>`); 778 else 779 f.write(`<td><pre><code>`, type, `</code></pre></td><td>`, summary ,`</td></tr>`); 780 } 781 } 782 783 struct Members 784 { 785 File[string] overloadFiles; 786 Item[] aliases; 787 Item[] classes; 788 Item[] enums; 789 Item[] functions; 790 Item[] interfaces; 791 Item[] structs; 792 Item[] templates; 793 Item[] values; 794 Item[] variables; 795 796 void write(File f, string outputDirectory) 797 { 798 if (aliases.length == 0 && classes.length == 0 && enums.length == 0 799 && functions.length == 0 && interfaces.length == 0 800 && structs.length == 0 && templates.length == 0 && values.length == 0 801 && variables.length == 0) 802 { 803 return; 804 } 805 f.writeln(`<div class="section">`); 806 if (enums.length > 0) 807 write(f, enums, "Enums", outputDirectory); 808 if (aliases.length > 0) 809 write(f, aliases, "Aliases", outputDirectory); 810 if (variables.length > 0) 811 write(f, variables, "Variables", outputDirectory); 812 if (functions.length > 0) 813 write(f, functions, "Functions", outputDirectory); 814 if (structs.length > 0) 815 write(f, structs, "Structs", outputDirectory); 816 if (interfaces.length > 0) 817 write(f, interfaces, "Interfaces", outputDirectory); 818 if (classes.length > 0) 819 write(f, classes, "Classes", outputDirectory); 820 if (templates.length > 0) 821 write(f, templates, "Templates", outputDirectory); 822 if (values.length > 0) 823 write(f, values, "Values", outputDirectory); 824 f.writeln(`</div>`); 825 foreach (f; overloadFiles) 826 { 827 f.writeln(HTML_END); 828 f.close(); 829 } 830 } 831 832 private: 833 834 void write(File f, Item[] items, string name, string outputDirectory) 835 { 836 f.writeln(`<h3>`, name, `</h3>`); 837 f.writeln(`<table>`); 838 foreach (i; items) 839 i.write(f, outputDirectory); 840 f.writeln(`</table>`); 841 } 842 }