1 /// Author: Aziz Köksal
2 /// License: GPL3
3 /// $(Maturity high)
4 module dil.doc.DDocEmitter;
5 
6 import dil.doc.Parser,
7        dil.doc.Macro,
8        dil.doc.Doc;
9 import dil.ast.DefaultVisitor,
10        dil.ast.ASTPrinter,
11        dil.ast.Node,
12        dil.ast.Declarations,
13        dil.ast.Statements,
14        dil.ast.Expressions,
15        dil.ast.Parameters,
16        dil.ast.Types;
17 import dil.lexer.Token,
18        dil.lexer.Funcs;
19 import dil.semantic.Module;
20 import dil.i18n.Messages;
21 import dil.Highlighter,
22        dil.Diagnostics,
23        dil.SourceText,
24        dil.Enums,
25        dil.String,
26        dil.Array;
27 import common;
28 
29 /// Traverses the syntax tree and writes DDoc macros to a string buffer.
30 abstract class DDocEmitter : DefaultVisitor2
31 {
32   CharArray text; /// The buffer that is written to.
33   bool includeUndocumented; /// Include undocumented symbols?
34   bool includePrivate; /// Include symbols with private protection?
35   MacroTable mtable; /// The macro table.
36   Module modul; /// The module.
37   Highlighter tokenHL; /// The token highlighter.
38   Diagnostics reportDiag; /// Collects problem messages.
39   ASTPrinter astPrinter; /// Used to print Types/Expressions/etc.
40   /// Counts code examples in comments.
41   /// This is used to make the code lines targetable in HTML.
42   uint codeExamplesCounter;
43 
44   /// Constructs a DDocEmitter object.
45   /// Params:
46   ///   modul = The module to generate text for.
47   ///   mtable = The macro table.
48   ///   includeUndocumented = Whether to include undocumented symbols.
49   ///   includePrivate = Whether to include private symbols.
50   ///   tokenHL = Used to highlight code sections.
51   this(Module modul, MacroTable mtable,
52        bool includeUndocumented, bool includePrivate,
53        Diagnostics reportDiag, Highlighter tokenHL)
54   {
55     this.mtable = mtable;
56     this.includeUndocumented = includeUndocumented;
57     this.includePrivate = includePrivate;
58     this.modul = modul;
59     this.tokenHL = tokenHL;
60     this.reportDiag = reportDiag;
61     this.astPrinter = new ASTPrinter(true, modul.cc);
62   }
63 
64   /// Entry method.
65   char[] emit()
66   {
67     if (isDDocFile(modul))
68     { // The module is actually a DDoc text file.
69       auto c = DDocUtils.getDDocComment(getDDocText(modul));
70       foreach (s; c.sections)
71         if (s.Is("macros"))
72         { // Declare the macros in this section.
73           auto ms = new MacrosSection(s.name, s.text);
74           mtable.insert(ms.macroNames, ms.macroTexts);
75         }
76         else
77           write(s.wholeText);
78       return text[];
79     }
80 
81     // Handle as a normal D module with declarations:
82 
83     // Initialize the root symbol in the symbol tree.
84     auto lexer = modul.parser.lexer;
85     symbolTree[0] = new DocSymbol(modul.moduleName, modul.getFQN(), K.Module,
86       null,
87       locationOf(lexer.firstToken),
88       locationOf(lexer.lastToken)
89     ); // Root symbol.
90 
91     if (auto d = modul.moduleDecl)
92       if (ddoc(d))
93       {
94         if (auto copyright = cmnt.takeCopyright())
95           mtable.insert("COPYRIGHT", copyright.text);
96         writeComment();
97       }
98     // Start traversing the tree and emitting macro text.
99     MEMBERS("MODULE", "", modul.root);
100     return text[];
101   }
102 
103   /// Returns the location of t.
104   Location locationOf(Token* t, cstring filePath = null)
105   {
106     return t.getRealLocation(filePath ? filePath : modul.filePath);
107   }
108 
109   /// Reports an undocumented symbol.
110   void reportUndocumented()
111   {
112     if (reportDiag is null)
113       return;
114     auto loc = locationOf(currentDecl.begin, modul.getFQN());
115     reportDDocProblem(loc, DDocProblem.Kind.UndocumentedSymbol,
116       MID.UndocumentedSymbol);
117   }
118 
119   /// Reports an empty comment.
120   void reportEmptyComment()
121   {
122     if (reportDiag is null || !this.cmnt.isEmpty())
123       return;
124     auto loc = locationOf(currentDecl.begin, modul.getFQN());
125     reportDDocProblem(loc, DDocProblem.Kind.EmptyComment, MID.EmptyDDocComment);
126   }
127 
128   /// Reports a problem.
129   void reportDDocProblem(Location loc, DDocProblem.Kind kind, MID mid, ...)
130   {
131     reportDiag ~= new DDocProblem(loc, kind,
132       reportDiag.formatMsg(mid, _arguments, _argptr));
133   }
134 
135   /// Reports a missing params section or undocumented parameters.
136   void reportParameters(Parameters params)
137   {
138     if (reportDiag is null || cmntIsDitto || params.items.length == 0)
139       return;
140     Token*[] paramNames;
141     foreach (param; params.items)
142       if (param.hasName)
143         paramNames ~= param.name;
144     reportParameters(params.begin, paramNames);
145   }
146 
147   /// ditto
148   void reportParameters(TemplateParameters params)
149   {
150     if (reportDiag is null || cmntIsDitto || params.items.length == 0)
151       return;
152     Token*[] paramNames;
153     foreach (param; params.items)
154       paramNames ~= param.name;
155     reportParameters(params.begin, paramNames);
156   }
157 
158   /// Params:
159   ///   paramsBegin = The left parenthesis of the parameter list.
160   ///   params = The identifier tokens of the parameter names.
161   void reportParameters(Token* paramsBegin, Token*[] params)
162   {
163     assert(currentDecl !is null);
164     // TODO: exclude some functions? like "new"?
165     // Search for the params section.
166     Section paramsSection;
167     foreach (s; this.cmnt.sections)
168       if (s.Is("params"))
169         paramsSection = s;
170     if (paramsSection is null)
171     {
172       auto loc = locationOf(paramsBegin, modul.getFQN());
173       reportDDocProblem(loc, DDocProblem.Kind.NoParamsSection,
174         MID.MissingParamsSection);
175       return;
176     }
177     // Search for undocumented parameters.
178     bool[hash_t] documentedParams;
179     auto ps = new ParamsSection(paramsSection.name, paramsSection.text);
180     foreach (name; ps.paramNames) // Create set of documented parameters.
181       documentedParams[hashOf(name)] = true;
182     foreach (param; params) // Find undocumented parameters.
183       if (!(hashOf(param.ident.str) in documentedParams))
184       {
185         auto loc = locationOf(param, modul.getFQN());
186         reportDDocProblem(loc, DDocProblem.Kind.UndocumentedParam,
187           MID.UndocumentedParam, param);
188       }
189   }
190 
191   /// Returns true if the source text starts with "Ddoc\n" (ignores letter case.)
192   static bool isDDocFile(Module mod)
193   {
194     auto text = mod.sourceText.text();
195     if (text.length >= "ddoc\n".length && // Check for minimum length.
196         String(text[0..4]).ieql("ddoc") && // Check first four characters.
197         isNewline(text.ptr + 4)) // Check for a newline.
198       return true;
199     return false;
200   }
201 
202   /// Returns the DDoc text of this module.
203   static cstring getDDocText(Module mod)
204   {
205     auto text = mod.sourceText.text();
206     auto p = text.ptr + "ddoc".length;
207     if (scanNewline(p)) // Skip the newline.
208       // Exclude preceding "Ddoc\n".
209       return text[p-text.ptr .. $];
210     return null;
211   }
212 /+
213   char[] textSpan(Token* left, Token* right)
214   {
215     //assert(left && right && (left.end <= right.start || left is right));
216     //char[] result;
217     //TODO: filter out whitespace tokens.
218     return Token.textSpan(left, right);
219   }
220 +/
221   /// The current declaration.
222   Node currentDecl;
223 
224   /// The template parameters of the current declaration.
225   TemplateParameters currentTParams;
226 
227   /// Reflects the fully qualified name of the current symbol's parent.
228   cstring parentFQN;
229   /// Counts symbols with the same FQN.
230   /// This is useful for anchor names that require unique strings.
231   uint[hash_t] fqnCount;
232 
233   /// Appends to parentFQN.
234   void pushFQN(cstring name)
235   {
236     if (parentFQN.length)
237       parentFQN ~= ".";
238     parentFQN ~= name;
239 
240     auto pfqn = hashOf(parentFQN) in fqnCount;
241     uint count = pfqn ? *pfqn : 0;
242     if (count > 1) // Start adding suffixes with 2.
243       parentFQN ~= ":" ~ itoa(count);
244   }
245 
246   /// Returns a unique, identifying string for the current symbol.
247   cstring getSymbolFQN(cstring name)
248   {
249     char[] fqn = parentFQN.dup;
250     if (fqn.length)
251       fqn ~= ".";
252     fqn ~= name;
253 
254     uint count;
255     auto hash = hashOf(fqn);
256     auto pfqn = hash in fqnCount;
257     if (pfqn)
258       count = (*pfqn += 1); // Update counter.
259     else
260       fqnCount[hash] = 1; // Start counting with 1.
261 
262     if (count > 1) // Start adding suffixes with 2.
263       fqn ~= ":" ~ itoa(count);
264     return fqn;
265   }
266 
267   DDocComment cmnt; /// Current comment.
268   DDocComment prevCmnt; /// Previous comment in scope.
269   /// An empty comment. Used for undocumented symbols.
270   static DDocComment emptyCmnt;
271 
272   /// Initializes the empty comment.
273   static this()
274   {
275     emptyCmnt = new DDocComment(null, null, null);
276   }
277 
278   /// Keeps track of previous comments in each scope.
279   scope class DDocScope
280   {
281     DDocComment saved_prevCmnt;
282     bool saved_cmntIsDitto;
283     size_t saved_prevDeclOffset;
284     cstring saved_parentFQN;
285     /// When constructed, variables are saved.
286     /// Params:
287     ///   name = The name of the current symbol.
288     this(cstring name)
289     { // Save the previous comment of the parent scope.
290       saved_prevCmnt = this.outer.prevCmnt;
291       saved_cmntIsDitto = this.outer.cmntIsDitto;
292       saved_prevDeclOffset = this.outer.prevDeclOffset;
293       saved_parentFQN = this.outer.parentFQN;
294       pushFQN(name);
295       // Entering a new scope. Clear variables.
296       this.outer.prevCmnt = null;
297       this.outer.cmntIsDitto = false;
298       this.outer.prevDeclOffset = 0;
299     }
300     /// When destructed, variables are restored.
301     ~this()
302     { // Restore the previous comment of the parent scope.
303       this.outer.prevCmnt = saved_prevCmnt;
304       this.outer.cmntIsDitto = saved_cmntIsDitto;
305       this.outer.prevDeclOffset = saved_prevDeclOffset;
306       this.outer.parentFQN = saved_parentFQN;
307     }
308   }
309 
310   bool cmntIsDitto; /// True if current comment is "ditto".
311 
312   /// Sets some members and returns true if a comment was found.
313   bool ddoc(Declaration node)
314   {
315     if (!includePrivate && node.prot == Protection.Private)
316       return false;
317     this.currentDecl = node;
318     this.cmnt = DDocUtils.getDDocComment(node);
319     if (this.cmnt)
320     {
321       if (this.cmnt.isDitto) // A ditto comment.
322         (this.cmnt = this.prevCmnt), (this.cmntIsDitto = true);
323       else // A normal comment.
324       {
325         reportEmptyComment();
326         (this.prevCmnt = this.cmnt), (this.cmntIsDitto = false);
327       }
328     }
329     else
330     {
331       reportUndocumented();
332       if (includeUndocumented)
333         this.cmnt = this.emptyCmnt; // Assign special empty comment.
334     }
335     return this.cmnt !is null;
336   }
337 
338   /// List of predefined, special sections.
339   static string[hash_t] specialSections;
340   static this()
341   {
342     foreach (name; ["AUTHORS", "BUGS", "COPYRIGHT", "DATE", "DEPRECATED",
343                     "EXAMPLES", "HISTORY", "LICENSE", "RETURNS", "SEE_ALSO",
344                     "STANDARDS", "THROWS", "VERSION"] ~
345                    ["AUTHOR"]) // Additions by DIL.
346       specialSections[hashOf(name)] = name;
347   }
348 
349   /// Writes the DDoc comment to the text buffer.
350   void writeComment()
351   {
352     auto c = this.cmnt;
353     assert(c !is null);
354     write("\1DDOC_SECTIONS ");
355       foreach (s; c.sections)
356       {
357         if (s is c.summary)
358           write("\n\1DDOC_SUMMARY ");
359         else if (s is c.description)
360           write("\n\1DDOC_DESCRIPTION ");
361         else if (auto name =
362             String(s.name).toupper().hashOf() in specialSections)
363           write("\n\1DDOC_", *name, " ");
364         else if (s.Is("params"))
365         { // Process parameters section.
366           auto ps = new ParamsSection(s.name, s.text);
367           write("\n\1DDOC_PARAMS ");
368           foreach (i, paramName; ps.paramNames)
369             write("\n\1DDOC_PARAM_ROW \1DDOC_PARAM_ID \1DDOC_PARAM ",
370               paramName, "\2\2\1DDOC_PARAM_DESC "),
371             writeCommentText(ps.paramDescs[i]),
372             write("\2\2");
373           write("\2");
374           continue;
375         }
376         else if (s.Is("macros"))
377         { // Declare the macros in this section.
378           auto ms = new MacrosSection(s.name, s.text);
379           mtable.insert(ms.macroNames, ms.macroTexts);
380           continue;
381         }
382         else
383           write("\n\1DDOC_SECTION "
384             "\1DDOC_SECTION_H ", s.name.replace('_', ' '), ":\2");
385         write("\1DIL_CMT ");
386         writeCommentText(s.text);
387         write("\2\2");
388       }
389     write("\2");
390   }
391 
392   /// Scans the comment text and:
393   /// $(UL
394   /// $(LI skips HTML tags)
395   /// $(LI escapes '&lt;', '&gt;' and '&amp;' with named HTML entities)
396   /// $(LI inserts $&#40;DDOC_BLANKLINE&#41; in place of '\n\n')
397   /// $(LI highlights the tokens in code sections)
398   /// )
399   void writeCommentText(cstring comment)
400   { // Init pointers for parsing.
401     auto p = comment.ptr;
402     auto lastLineEnd = p; // The position of the last \n seen.
403     auto end = p + comment.length;
404     // Nesting level of macro invocations and the parentheses inside of them.
405     uint level = 0;
406     char[] parens; // Stack of parentheses and markers.
407 
408     while (p < end)
409     {
410       switch (*p)
411       {
412       case '$':
413         auto p2 = p+2;
414         if (p2 < end && p[1] == '(' && isIdentifierStart(p2, end)) // IdStart
415         {
416           parens ~= Macro.Marker.Closing;
417           write(Macro.Marker.Opening); // Relace "$(".
418           p = p2; // Skip "$(".
419         }
420         goto default;
421       case '(':
422         if (parens.length) parens ~= ')';
423         goto default;
424       case ')':
425         if (!parens.length) goto default;
426         write(parens[$-1]); // Append Macro.Marker.Closing or ")".
427         parens.length--; // Pop one char.
428         break;
429       case '<':
430         auto begin = p;
431         p++;
432         if (p+2 < end && *p == '!' && p[1] == '-' && p[2] == '-') // <!--
433         {
434           p += 2; // Point to 2nd '-'.
435           // Scan to closing "-->".
436           while (++p < end)
437             if (*p == '-' && p+2 < end && p[1] == '-' && p[2] == '>')
438             {
439               p += 3; // Point one past '>'.
440               break;
441             }
442           write(slice(begin, p));
443         } // <tag ...> or </tag>
444         else if (p < end && (isalpha(*p) || *p == '/'))
445         {
446           while (++p < end && *p != '>') // Skip to closing '>'.
447           {}
448           if (p == end)
449           { // No closing '>' found.
450             p = begin + 1;
451             write("&lt;");
452             continue;
453           }
454           p++; // Skip '>'.
455           write(slice(begin, p));
456         }
457         else
458           write("&lt;");
459         continue;
460       // case '\'': write("&apos;"); break; // &#39;
461       // case '"': write("&quot;"); break;
462       case '>': write("&gt;"); break;
463       case '&':
464         auto entityBegin = p;
465         if (++p < end && (isalpha(*p) || *p == '#'))
466         {
467           if (*p == '#')
468             while (++p < end && isdigit(*p)){} // Numerical entity.
469           else
470             while (++p < end && isalpha(*p)){} // Named entity.
471           if (p < end && *p == ';') {
472             write(slice(entityBegin, ++p)); // Copy valid entity.
473             continue;
474           }
475           p = entityBegin + 1; // Reset. It's not a valid entity.
476         }
477         write("&amp;");
478         continue;
479       case '\n':
480         if (!(p+1 < end && p[1] == '\n'))
481         {
482           lastLineEnd = p;
483           goto default;
484         }
485         ++p;
486         write("\n\1DDOC_BLANKLINE\2\n");
487         lastLineEnd = p;
488         break;
489       case '-':
490         if (p+2 < end && p[1] == '-' && p[2] == '-' &&
491             isAllSpace(lastLineEnd + 1, p))
492         { // Found "---" at start of line.
493           while (p < end && *p == '-') // Skip trailing dashes.
494             p++;
495           auto codeBegin = p;
496           while (p < end && isspace(*p)) // Skip whitespace.
497             p++;
498           if (p < end && *p == '\n') // Skip first newline.
499             codeBegin = ++p;
500           // Find closing dashes.
501           while (p < end && !(*p == '-' && p+2 < end &&
502                             p[1] == '-' && p[2] == '-' &&
503                             isAllSpace(lastLineEnd + 1, p)))
504           {
505             if (*p == '\n')
506               lastLineEnd = p;
507             p++;
508           }
509 
510           // Remove last newline if present.
511           auto codeEnd = p;
512           while (isspace(*--codeEnd))
513           {}
514           if (*codeEnd != '\n') // Leaving the pointer on '\n' will exclude it.
515             codeEnd++; // Include the non-newline character.
516           if (codeBegin < codeEnd)
517           { // Highlight the extracted source code.
518             cstring codeText = slice(codeBegin, codeEnd);
519             uint lines; // Number of lines in the code text.
520 
521             codeExamplesCounter++; // Found a code section. Increment counter.
522 
523             // TODO: use a scratch buffer to unindent and to highlight.
524             codeText = DDocUtils.unindentText(codeText);
525             codeText = tokenHL.highlightTokens(codeText, modul.getFQN(), lines);
526             write("\1D_CODE\n\1DIL_CODELINES ");
527             for (uint num = 1; num <= lines; num++)
528             {
529               auto numtxt = itoa(num);
530               auto id = "L"~numtxt~"_ex"~itoa(codeExamplesCounter);
531               write(`<a href="#`, id, `" name="`, id, `">`, numtxt, "</a>\n");
532             }
533             write("\2,\1DIL_CODETEXT \4", codeText, "\2\n\2");
534           }
535           while (p < end && *p == '-') // Skip remaining dashes.
536             p++;
537           continue;
538         }
539         goto default;
540       default:
541         write(*p);
542       }
543       p++;
544     }
545     assert(p is end);
546     foreach (c; parens)
547       if (c == Macro.Marker.Closing) // Unclosed macros?
548         write(Macro.Marker.Unclosed); // Add marker for errors.
549   }
550 
551   /// Writes a character to the buffer.
552   void write(char c)
553   {
554     text ~= c;
555   }
556 
557   /// Writes an array of strings to the text buffer.
558   void write(cstring[] strings...)
559   {
560     foreach (s; strings)
561       text ~= s;
562   }
563 
564   /// Writes a Node tree to the buffer.
565   void write(Node n)
566   {
567     astPrinter.print(n);
568     writeHL(astPrinter.tokens);
569   }
570 
571   /// Write highlighted tokens to the buffer.
572   void writeHL(Token[] tokens)
573   {
574     tokenHL.highlightTokens(text, tokens, true);
575   }
576 
577   /// Writes params to the text buffer.
578   void writeParams(Parameters params)
579   {
580     reportParameters(params);
581     write("\1DIL_PARAMS ");
582     auto item_count = params.items.length;
583     foreach (param; params.items)
584     {
585       if (param.isCVariadic)
586         write("...");
587       else
588       {
589         // Write storage class(es).
590         auto lastSTC = param.tokenOfLastSTC();
591         if (lastSTC) // Write storage classes.
592           writeHL(param.begin[0..lastSTC-param.begin]), write(" ");
593         if (param.type)
594           write(param.type); // Write the type.
595         if (param.hasName)
596           write((param.type?" ":""), "\1DDOC_PARAM ", param.nameStr, "\2");
597         if (param.defValue)
598           write(" = \1DIL_DEFVAL "), write(param.defValue), write("\2");
599         if (param.isDVariadic)
600           write("...");
601       }
602       --item_count && (text ~= ", "); // Skip for last item.
603     }
604     write("\2");
605   }
606 
607   /// Writes the current template parameters to the text buffer.
608   void writeTemplateParams()
609   {
610     if (!currentTParams)
611       return;
612 
613     void writeSpecDef(Node spec, Node def)
614     {
615       if (spec) write(" : \1DIL_SPEC "), write(spec), write("\2");
616       if (def)  write(" = \1DIL_DEFVAL "), write(def), write("\2");
617     }
618 
619     reportParameters(currentTParams);
620     write("\1DIL_TEMPLATE_PARAMS ");
621     auto item_count = currentTParams.items.length;
622     foreach (tparam; currentTParams.items)
623     {
624       if (auto p = tparam.Is!(TemplateAliasParam))
625         write("\1DIL_TPALIAS \1DIL_TPID ", p.nameStr, "\2"),
626         writeSpecDef(p.spec, p.def),
627         write("\2");
628       else if (auto p = tparam.Is!(TemplateTypeParam))
629         write("\1DIL_TPTYPE \1DIL_TPID ", p.nameStr, "\2"),
630         writeSpecDef(p.specType, p.defType),
631         write("\2");
632       else if (auto p = tparam.Is!(TemplateTupleParam))
633         write("\1DIL_TPTUPLE \1DIL_TPID ", p.nameStr, "\2\2");
634       else if (auto p = tparam.Is!(TemplateValueParam))
635         write("\1DIL_TPVALUE "),
636         write(p.valueType),
637         write(" \1DIL_TPID ", p.nameStr, "\2"),
638         writeSpecDef(p.specValue, p.defValue),
639         write("\2");
640       else if (auto p = tparam.Is!(TemplateThisParam))
641         write("\1DIL_TPTHIS \1DIL_TPID ", p.nameStr, "\2"),
642         writeSpecDef(p.specType, p.defType),
643         write("\2");
644       --item_count && write(", ");
645     }
646     write("\2");
647     currentTParams = null;
648   }
649 
650   /// Writes bases to the text buffer.
651   void writeInheritanceList(BaseClassType[] bases)
652   {
653     auto item_count = bases.length;
654     if (item_count == 0)
655       return;
656     write(" \1DIL_BASECLASSES ");
657     foreach (base; bases)
658       write("\1DIL_BASECLASS "), write(base),
659       write(--item_count ? "\2, " : "\2");
660     write("\2");
661   }
662 
663   /// Offset at which to insert a declaration with a "ditto" comment.
664   size_t prevDeclOffset;
665 
666   /// Writes a declaration to the text buffer.
667   void DECL(void delegate() dg, Declaration d, bool writeSemicolon = true)
668   {
669     void writeDECL()
670     {
671       write("\n\1DDOC_DECL ");
672       dg();
673       writeSemicolon && write("\1DIL_SC\2");
674       writeAttributes(d);
675       write(" \1DIL_SYMEND ", currentSymbolParams, "\2\2");
676     }
677 
678     if (/+includeUndocumented &&+/ this.cmnt is this.emptyCmnt)
679       // Handle undocumented symbols separately.
680       // This way they don't interrupt consolidated declarations (via 'ditto'.)
681       writeDECL();
682     else if (cmntIsDitto)
683     { // The declaration has a ditto comment.
684       alias offs = prevDeclOffset;
685       assert(offs != 0);
686       auto savedEnd = text[offs..Neg(0)].dup; // Copy text past offset.
687       text.len = offs;
688       writeDECL();
689       text ~= savedEnd; // Append the snippet.
690     }
691     else
692     {
693       writeDECL();
694       // Set the offset. At this offset other declarations with a ditto
695       // comment will be inserted, if present.
696       prevDeclOffset = text.len;
697     }
698   }
699 
700   /// Wraps the DDOC_DECL_DD macro around the text written by dg().
701   /// Writes the comment before dg() is called.
702   void DESC(void delegate() dg = null)
703   {
704     auto isEmpty = this.cmnt is this.emptyCmnt;
705     if (cmntIsDitto && !isEmpty)
706       return; // Don't write a description when we have a ditto-comment.
707     write("\n\1DDOC_DECL_DD ");
708     if (isEmpty)
709       write("\1DIL_NOCMNT\2");
710     else
711       writeComment();
712     dg && dg();
713     write("\2");
714   }
715 
716   /// Saves the attributes of the current symbol.
717   cstring currentSymbolParams;
718 
719   /// Writes a symbol to the text buffer.
720   /// E.g: &#36;(DIL_SYMBOL scan, Lexer.scan, func, 229, 646);
721   void SYMBOL(cstring name, K kind, Declaration d)
722   {
723     auto kindStr = DocSymbol.kindIDToStr[kind];
724     auto fqn = getSymbolFQN(name);
725     auto loc = locationOf(d.begin);
726     auto loc_end = locationOf(d.end);
727     storeAttributes(d);
728     addSymbol(name, fqn, kind, loc, loc_end);
729     currentSymbolParams = Format("{}, {}, {}, {}, {}",
730       name, fqn, kindStr, loc.lineNum, loc_end.lineNum);
731     write("\1DIL_SYMBOL ", currentSymbolParams, "\2");
732     // write("\1DDOC_PSYMBOL ", name, "\2"); // DMD's macro with no info.
733   }
734 
735   /// The symbols that will appear in the result document.
736   DocSymbol[hash_t] symbolTree;
737 
738   /// Adds a symbol to the symbol tree.
739   void addSymbol(cstring name, cstring fqn, K kind,
740     Location begin, Location end)
741   {
742     auto attrs = currentAttributes.asArray();
743     auto symbol = new DocSymbol(name, fqn, kind, attrs, begin, end);
744     symbolTree[hashOf(fqn)] = symbol; // Add the symbol itself.
745     symbolTree[hashOf(parentFQN)].members ~= symbol; // Append as a child to parent.
746   }
747 
748   /// Wraps the DDOC_kind_MEMBERS macro around the text
749   /// written by visit(members).
750   void MEMBERS(D)(cstring kind, cstring name, D members)
751   {
752     scope s = new DDocScope(name);
753     write("\n\1DDOC_"~kind~"_MEMBERS ");
754     if (members !is null)
755       super.visit(members);
756     write("\2");
757   }
758 
759   /// Writes a class or interface declaration.
760   void writeClassOrInterface(T)(T d)
761   {
762     if (!ddoc(d))
763       return;
764     const kind = is(T == ClassDecl) ? "class" : "interface";
765     const kindID = is(T == ClassDecl) ? K.Class : K.Interface;
766     const KIND = is(T == ClassDecl) ? "CLASS" : "INTERFACE";
767     DECL({
768       write("\1DIL_KW ", kind, "\2 ");
769       SYMBOL(d.name.text, kindID, d);
770       writeTemplateParams();
771       writeInheritanceList(d.bases);
772     }, d);
773     DESC({ MEMBERS(KIND, d.name.text, d.decls); });
774   }
775 
776   /// Writes a struct or union declaration.
777   void writeStructOrUnion(T)(T d)
778   {
779     if (!ddoc(d))
780       return;
781     const kind = is(T == StructDecl) ? "struct" : "union";
782     const kindID = is(T == StructDecl) ? K.Struct : K.Union;
783     const KIND = is(T == StructDecl) ? "STRUCT" : "UNION";
784     cstring name = d.name ? d.name.text : kind;
785     DECL({
786       d.name && write("\1DIL_KW ", kind, "\2 ");
787       SYMBOL(name, kindID, d);
788       writeTemplateParams();
789     }, d);
790     DESC({ MEMBERS(KIND, name, d.decls); });
791   }
792 
793   /// Writes an alias or typedef declaration.
794   void writeAliasOrTypedef(T)(T d)
795   {
796     const kind = is(T == AliasDecl) ? "alias" : "typedef";
797     const kindID = is(T == AliasDecl) ? K.Alias : K.Typedef;
798     if (auto vd = d.vardecl.Is!(VariablesDecl))
799       foreach (name; vd.names)
800         DECL({
801           write("\1DIL_KW ", kind, "\2 "); write(vd.type); write(" ");
802           auto saved_begin = vd.begin;
803           // 'vd' instead of 'd' is passed to SYMBOL, because it
804           // has a linkageType member, which has to appear in the docs.
805           vd.begin = d.begin; // Use the begin token of the outer declaration.
806           SYMBOL(name.text, kindID, vd);
807           vd.begin = saved_begin; // Restore to the old value.
808         }, d);
809     else
810       assert(0, "unhandled case");
811     DESC();
812   }
813 
814   /// All attributes a symbol can have.
815   struct SymbolAttributes
816   {
817     string prot;   /// Protection attribute.
818     string[] stcs; /// Storage classes.
819     string link;   /// Linkage type.
820     /// Returns all attributes in an array.
821     string[] asArray()
822     {
823       string[] attrs;
824       if (prot.length)
825         attrs = [prot];
826       attrs ~= stcs;
827       if (link.length)
828         attrs ~= link;
829       return attrs;
830     }
831   }
832 
833   /// The attributes of the current symbol.
834   SymbolAttributes currentAttributes;
835 
836   /// Stores the attributes of the current symbol.
837   void storeAttributes(Declaration d)
838   {
839     alias attrs = currentAttributes;
840     attrs.prot = (d.prot == Protection.None) ? null : EnumString(d.prot);
841 
842     auto stcs = d.stcs;
843     stcs &= ~StorageClass.Auto; // Ignore "auto".
844     attrs.stcs = EnumString.all(stcs);
845 
846     LinkageType ltype;
847     if (auto vd = d.Is!(VariablesDecl))
848       ltype = vd.lnkg;
849     else if (auto fd = d.Is!(FunctionDecl))
850       ltype = fd.lnkg;
851 
852     attrs.link = (ltype == LinkageType.None) ? null : EnumString(ltype);
853   }
854 
855   /// Writes the attributes of a declaration in brackets.
856   void writeAttributes(Declaration d)
857   {
858     string[] attributes;
859 
860     if (currentAttributes.prot.length)
861       attributes ~= "\1DIL_PROT " ~ currentAttributes.prot ~ "\2";
862 
863     foreach (stcString; currentAttributes.stcs)
864       attributes ~= "\1DIL_STC " ~ stcString ~ "\2";
865 
866     if (currentAttributes.link.length)
867       attributes ~= "\1DIL_LINKAGE extern(" ~ currentAttributes.link ~ ")\2";
868 
869     if (!attributes.length)
870       return;
871 
872     write(" \1DIL_ATTRIBUTES ", attributes[0]);
873     foreach (attribute; attributes[1..$])
874       write(", ", attribute);
875     write("\2");
876   }
877 
878   alias K = DocSymbol.Kind;
879 
880 override:
881   //alias visit = super.visit;
882 
883   void visit(AliasDecl d)
884   {
885     if (ddoc(d))
886       writeAliasOrTypedef(d);
887   }
888 
889   void visit(TypedefDecl d)
890   {
891     if (ddoc(d))
892       writeAliasOrTypedef(d);
893   }
894 
895   void visit(EnumDecl d)
896   {
897     if (!ddoc(d))
898       return;
899     cstring name = d.name ? d.name.text : "enum";
900     DECL({
901       d.name && write("\1DIL_KW enum\2 ");
902       SYMBOL(name, K.Enum, d);
903       d.baseType && (write(" : "), write(d.baseType));
904     }, d);
905     DESC({ MEMBERS("ENUM", name, d); });
906   }
907 
908   void visit(EnumMemberDecl d)
909   {
910     if (!ddoc(d))
911       return;
912     // TODO: emit d.type (D2).
913     DECL({ SYMBOL(d.name.text, K.Enummem, d); }, d, false);
914     DESC();
915   }
916 
917   void visit(TemplateDecl d)
918   {
919     this.currentTParams = d.tparams;
920     if (d.isWrapper)
921     { // This is a templatized class/interface/struct/union/function.
922       super.visit(d.decls);
923       this.currentTParams = null;
924       return;
925     }
926     if (!ddoc(d))
927       return;
928     DECL({
929       version(D2)
930       if (d.isMixin) write("\1DIL_KW mixin\2 ");
931       write("\1DIL_KW template\2 ");
932       SYMBOL(d.name.text, K.Template, d);
933       writeTemplateParams();
934     }, d);
935     DESC({ MEMBERS("TEMPLATE", d.name.text, d.decls); });
936   }
937 
938   void visit(ClassDecl d)
939   {
940     writeClassOrInterface(d);
941   }
942 
943   void visit(InterfaceDecl d)
944   {
945     writeClassOrInterface(d);
946   }
947 
948   void visit(StructDecl d)
949   {
950     writeStructOrUnion(d);
951   }
952 
953   void visit(UnionDecl d)
954   {
955     writeStructOrUnion(d);
956   }
957 
958   void visit(ConstructorDecl d)
959   {
960     if (!ddoc(d))
961       return;
962     DECL({ SYMBOL("this", K.Ctor, d);
963       version(D2)
964       writeTemplateParams();
965       writeParams(d.params);
966     }, d);
967     DESC();
968   }
969 
970   void visit(StaticCtorDecl d)
971   {
972     if (!ddoc(d))
973       return;
974     DECL({ write("\1DIL_KW static\2 ");
975       SYMBOL("this", K.Sctor, d); write("()"); }, d);
976     DESC();
977   }
978 
979   void visit(DestructorDecl d)
980   {
981     if (!ddoc(d))
982       return;
983     DECL({ SYMBOL("~this", K.Dtor, d); write("()"); }, d);
984     DESC();
985   }
986 
987   void visit(StaticDtorDecl d)
988   {
989     if (!ddoc(d))
990       return;
991     DECL({ write("\1DIL_KW static\2 ");
992       SYMBOL("~this", K.Sdtor, d); write("()"); }, d);
993     DESC();
994   }
995 
996   void visit(FunctionDecl d)
997   {
998     if (!ddoc(d))
999       return;
1000     DECL({
1001       if (d.returnType)
1002         write("\1DIL_RETTYPE "), write(d.returnType), write("\2 ");
1003       else write("\1DIL_KW auto\2 ");
1004       SYMBOL(d.name.text, K.Function, d);
1005       writeTemplateParams();
1006       writeParams(d.params);
1007     }, d);
1008     DESC();
1009   }
1010 
1011   void visit(NewDecl d)
1012   {
1013     if (!ddoc(d))
1014       return;
1015     DECL({ SYMBOL("new", K.New, d); writeParams(d.params); }, d);
1016     DESC();
1017   }
1018 
1019   void visit(DeleteDecl d)
1020   {
1021     if (!ddoc(d))
1022       return;
1023     DECL({ SYMBOL("delete", K.Delete, d); writeParams(d.params); }, d);
1024     DESC();
1025   }
1026 
1027   void visit(VariablesDecl d)
1028   {
1029     if (!ddoc(d))
1030       return;
1031     foreach (name; d.names)
1032       DECL({
1033         if (d.type) write(d.type);
1034         else write("\1DIL_KW auto\2");
1035         write(" ");
1036         SYMBOL(name.text, K.Variable, d);
1037       }, d);
1038     DESC();
1039   }
1040 
1041   void visit(InvariantDecl d)
1042   {
1043     if (!ddoc(d))
1044       return;
1045     DECL({ SYMBOL("invariant", K.Invariant, d); }, d);
1046     DESC();
1047   }
1048 
1049   void visit(UnittestDecl d)
1050   {
1051     if (!ddoc(d))
1052       return;
1053     DECL({ SYMBOL("unittest", K.Unittest, d); }, d);
1054     DESC();
1055   }
1056 
1057   void visit(DebugDecl d)
1058   {
1059     d.compiledDecls && visitD(d.compiledDecls);
1060   }
1061 
1062   void visit(VersionDecl d)
1063   {
1064     d.compiledDecls && visitD(d.compiledDecls);
1065   }
1066 
1067   void visit(StaticIfDecl d)
1068   {
1069     d.ifDecls && visitD(d.ifDecls);
1070   }
1071 }
1072 
1073 /// A class that holds some info about a symbol.
1074 class DocSymbol
1075 {
1076   /// Enum of symbol kinds.
1077   static enum Kind
1078   {
1079     Package, Module, Template, Class, Interface, Struct,
1080     Union, Alias, Typedef, Enum, Enummem, Variable, Function,
1081     Invariant, New, Delete, Unittest, Ctor, Dtor, Sctor, Sdtor
1082   }
1083 
1084   cstring name, fqn; /// Name and fully qualified name.
1085   Kind kind; /// The kind of this symbol.
1086   Location begin, end; /// Beginning and end locations.
1087   DocSymbol[] members; /// All symbol members.
1088   string[] attrs; /// All attributes of this symbol.
1089 
1090   /// Constructs a DocSymbol object.
1091   this(cstring name, cstring fqn, Kind kind, string[] attrs,
1092     Location begin, Location end)
1093   {
1094     this.name = name;
1095     this.fqn = fqn;
1096     this.kind = kind;
1097     this.attrs = attrs;
1098     this.begin = begin;
1099     this.end = end;
1100   }
1101 
1102   /// Symbol kinds as strings.
1103   static string[] kindIDToStr = ["package", "module", "template",
1104     "class", "interface", "struct", "union", "alias", "typedef",
1105     "enum", "enummem", "variable", "function", "invariant", "new",
1106     "delete", "unittest", "ctor", "dtor", "sctor", "sdtor"];
1107 
1108   /// Maps the kind of a symbol to its ID.
1109   /// Must match the list in "kandil/js/symbols.js".
1110   static uint[hash_t] kindStrToID;
1111   /// Maps the attribute of a symbol to its ID.
1112   /// Must match the list in "kandil/js/symbols.js".
1113   static uint[hash_t] attrToID;
1114   /// Initialize the associative arrays.
1115   static this()
1116   {
1117     foreach (i, kind; kindIDToStr)
1118       kindStrToID[hashOf(kind)] = cast(uint)i;
1119 
1120     // Combine attributes and add them to attrToID.
1121     auto attrs = EnumString.prots[1..$] ~
1122       EnumString.stcs[1..$] ~ EnumString.ltypes[1..$];
1123     foreach (i, attr; attrs)
1124       attrToID[hashOf(attr)] = cast(uint)i;
1125   }
1126 
1127   /// Return the attributes as IDs. E.g.: "[1,9,22]"
1128   cstring formatAttrsAsIDs()
1129   {
1130     if (!attrs.length)
1131       return "[]";
1132     char[] result = "[".dup;
1133     foreach (attr; attrs)
1134       result ~= itoa(attrToID[hashOf(attr)]) ~ ",";
1135     result[$-1] = ']';
1136     return result;
1137   }
1138 }