1 /**
2  * Ddoc documentation generation.
3  *
4  * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator)
5  *
6  * Copyright:   Copyright (C) 1999-2020 by The D Language Foundation, All Rights Reserved
7  * Authors:     $(LINK2 http://www.digitalmars.com, Walter Bright)
8  * License:     $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
9  * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/doc.d, _doc.d)
10  * Documentation:  https://dlang.org/phobos/dmd_doc.html
11  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/doc.d
12  */
13 
14 module dmd.doc;
15 
16 import core.stdc.ctype;
17 import core.stdc.stdlib;
18 import core.stdc.stdio;
19 import core.stdc.string;
20 import core.stdc.time;
21 import dmd.aggregate;
22 import dmd.arraytypes;
23 import dmd.attrib;
24 import dmd.cond;
25 import dmd.dclass;
26 import dmd.declaration;
27 import dmd.denum;
28 import dmd.dimport;
29 import dmd.dmacro;
30 import dmd.dmodule;
31 import dmd.dscope;
32 import dmd.dstruct;
33 import dmd.dsymbol;
34 import dmd.dsymbolsem;
35 import dmd.dtemplate;
36 import dmd.errors;
37 import dmd.func;
38 import dmd.globals;
39 import dmd.hdrgen;
40 import dmd.id;
41 import dmd.identifier;
42 import dmd.lexer;
43 import dmd.mtype;
44 import dmd.root.array;
45 import dmd.root.file;
46 import dmd.root.filename;
47 import dmd.root.outbuffer;
48 import dmd.root.port;
49 import dmd.root.rmem;
50 import dmd.root.string;
51 import dmd.tokens;
52 import dmd.utf;
53 import dmd.utils;
54 import dmd.visitor;
55 
56 struct Escape
57 {
58     const(char)[][char.max] strings;
59 
60     /***************************************
61      * Find character string to replace c with.
62      */
63     const(char)[] escapeChar(char c)
64     {
65         version (all)
66         {
67             //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr);
68             return strings[c];
69         }
70         else
71         {
72             const(char)[] s;
73             switch (c)
74             {
75             case '<':
76                 s = "&lt;";
77                 break;
78             case '>':
79                 s = "&gt;";
80                 break;
81             case '&':
82                 s = "&amp;";
83                 break;
84             default:
85                 s = null;
86                 break;
87             }
88             return s;
89         }
90     }
91 }
92 
93 /***********************************************************
94  */
95 private class Section
96 {
97     const(char)* name;
98     size_t namelen;
99     const(char)* _body;
100     size_t bodylen;
101     int nooutput;
102 
103     override string toString() const
104     {
105         assert(0);
106     }
107 
108     void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf)
109     {
110         assert(a.dim);
111         if (namelen)
112         {
113             static immutable table =
114             [
115                 "AUTHORS",
116                 "BUGS",
117                 "COPYRIGHT",
118                 "DATE",
119                 "DEPRECATED",
120                 "EXAMPLES",
121                 "HISTORY",
122                 "LICENSE",
123                 "RETURNS",
124                 "SEE_ALSO",
125                 "STANDARDS",
126                 "THROWS",
127                 "VERSION",
128             ];
129             foreach (entry; table)
130             {
131                 if (iequals(entry, name[0 .. namelen]))
132                 {
133                     buf.printf("$(DDOC_%s ", entry.ptr);
134                     goto L1;
135                 }
136             }
137             buf.writestring("$(DDOC_SECTION ");
138             // Replace _ characters with spaces
139             buf.writestring("$(DDOC_SECTION_H ");
140             size_t o = buf.length;
141             for (size_t u = 0; u < namelen; u++)
142             {
143                 char c = name[u];
144                 buf.writeByte((c == '_') ? ' ' : c);
145             }
146             escapeStrayParenthesis(loc, buf, o, false);
147             buf.writestring(")");
148         }
149         else
150         {
151             buf.writestring("$(DDOC_DESCRIPTION ");
152         }
153     L1:
154         size_t o = buf.length;
155         buf.write(_body[0 .. bodylen]);
156         escapeStrayParenthesis(loc, buf, o, true);
157         highlightText(sc, a, loc, *buf, o);
158         buf.writestring(")");
159     }
160 }
161 
162 /***********************************************************
163  */
164 private final class ParamSection : Section
165 {
166     override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf)
167     {
168         assert(a.dim);
169         Dsymbol s = (*a)[0]; // test
170         const(char)* p = _body;
171         size_t len = bodylen;
172         const(char)* pend = p + len;
173         const(char)* tempstart = null;
174         size_t templen = 0;
175         const(char)* namestart = null;
176         size_t namelen = 0; // !=0 if line continuation
177         const(char)* textstart = null;
178         size_t textlen = 0;
179         size_t paramcount = 0;
180         buf.writestring("$(DDOC_PARAMS ");
181         while (p < pend)
182         {
183             // Skip to start of macro
184             while (1)
185             {
186                 switch (*p)
187                 {
188                 case ' ':
189                 case '\t':
190                     p++;
191                     continue;
192                 case '\n':
193                     p++;
194                     goto Lcont;
195                 default:
196                     if (isIdStart(p) || isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
197                         break;
198                     if (namelen)
199                         goto Ltext;
200                     // continuation of prev macro
201                     goto Lskipline;
202                 }
203                 break;
204             }
205             tempstart = p;
206             while (isIdTail(p))
207                 p += utfStride(p);
208             if (isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
209                 p += 3;
210             templen = p - tempstart;
211             while (*p == ' ' || *p == '\t')
212                 p++;
213             if (*p != '=')
214             {
215                 if (namelen)
216                     goto Ltext;
217                 // continuation of prev macro
218                 goto Lskipline;
219             }
220             p++;
221             if (namelen)
222             {
223                 // Output existing param
224             L1:
225                 //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
226                 ++paramcount;
227                 HdrGenState hgs;
228                 buf.writestring("$(DDOC_PARAM_ROW ");
229                 {
230                     buf.writestring("$(DDOC_PARAM_ID ");
231                     {
232                         size_t o = buf.length;
233                         Parameter fparam = isFunctionParameter(a, namestart, namelen);
234                         if (!fparam)
235                         {
236                             // Comments on a template might refer to function parameters within.
237                             // Search the parameters of nested eponymous functions (with the same name.)
238                             fparam = isEponymousFunctionParameter(a, namestart, namelen);
239                         }
240                         bool isCVariadic = isCVariadicParameter(a, namestart[0 .. namelen]);
241                         if (isCVariadic)
242                         {
243                             buf.writestring("...");
244                         }
245                         else if (fparam && fparam.type && fparam.ident)
246                         {
247                             .toCBuffer(fparam.type, buf, fparam.ident, &hgs);
248                         }
249                         else
250                         {
251                             if (isTemplateParameter(a, namestart, namelen))
252                             {
253                                 // 10236: Don't count template parameters for params check
254                                 --paramcount;
255                             }
256                             else if (!fparam)
257                             {
258                                 warning(s.loc, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen, namestart);
259                             }
260                             buf.write(namestart[0 .. namelen]);
261                         }
262                         escapeStrayParenthesis(loc, buf, o, true);
263                         highlightCode(sc, a, *buf, o);
264                     }
265                     buf.writestring(")");
266                     buf.writestring("$(DDOC_PARAM_DESC ");
267                     {
268                         size_t o = buf.length;
269                         buf.write(textstart[0 .. textlen]);
270                         escapeStrayParenthesis(loc, buf, o, true);
271                         highlightText(sc, a, loc, *buf, o);
272                     }
273                     buf.writestring(")");
274                 }
275                 buf.writestring(")");
276                 namelen = 0;
277                 if (p >= pend)
278                     break;
279             }
280             namestart = tempstart;
281             namelen = templen;
282             while (*p == ' ' || *p == '\t')
283                 p++;
284             textstart = p;
285         Ltext:
286             while (*p != '\n')
287                 p++;
288             textlen = p - textstart;
289             p++;
290         Lcont:
291             continue;
292         Lskipline:
293             // Ignore this line
294             while (*p++ != '\n')
295             {
296             }
297         }
298         if (namelen)
299             goto L1;
300         // write out last one
301         buf.writestring(")");
302         TypeFunction tf = a.dim == 1 ? isTypeFunction(s) : null;
303         if (tf)
304         {
305             size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.dim : 0) +
306                             cast(int)(tf.parameterList.varargs == VarArg.variadic);
307             if (pcount != paramcount)
308             {
309                 warning(s.loc, "Ddoc: parameter count mismatch, expected %d, got %d", pcount, paramcount);
310                 if (paramcount == 0)
311                 {
312                     // Chances are someone messed up the format
313                     warningSupplemental(s.loc, "Note that the format is `param = description`");
314                 }
315             }
316         }
317     }
318 }
319 
320 /***********************************************************
321  */
322 private final class MacroSection : Section
323 {
324     override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf)
325     {
326         //printf("MacroSection::write()\n");
327         DocComment.parseMacros(dc.escapetable, *dc.pmacrotable, _body, bodylen);
328     }
329 }
330 
331 private alias Sections = Array!(Section);
332 
333 // Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one).
334 private bool isCVariadicParameter(Dsymbols* a, const(char)[] p)
335 {
336     foreach (member; *a)
337     {
338         TypeFunction tf = isTypeFunction(member);
339         if (tf && tf.parameterList.varargs == VarArg.variadic && p == "...")
340             return true;
341     }
342     return false;
343 }
344 
345 private Dsymbol getEponymousMember(TemplateDeclaration td)
346 {
347     if (!td.onemember)
348         return null;
349     if (AggregateDeclaration ad = td.onemember.isAggregateDeclaration())
350         return ad;
351     if (FuncDeclaration fd = td.onemember.isFuncDeclaration())
352         return fd;
353     if (auto em = td.onemember.isEnumMember())
354         return null;    // Keep backward compatibility. See compilable/ddoc9.d
355     if (VarDeclaration vd = td.onemember.isVarDeclaration())
356         return td.constraint ? null : vd;
357     return null;
358 }
359 
360 private TemplateDeclaration getEponymousParent(Dsymbol s)
361 {
362     if (!s.parent)
363         return null;
364     TemplateDeclaration td = s.parent.isTemplateDeclaration();
365     return (td && getEponymousMember(td)) ? td : null;
366 }
367 
368 private immutable ddoc_default = import("default_ddoc_theme.ddoc");
369 private immutable ddoc_decl_s = "$(DDOC_DECL ";
370 private immutable ddoc_decl_e = ")\n";
371 private immutable ddoc_decl_dd_s = "$(DDOC_DECL_DD ";
372 private immutable ddoc_decl_dd_e = ")\n";
373 
374 /****************************************************
375  */
376 extern(C++) void gendocfile(Module m)
377 {
378     __gshared OutBuffer mbuf;
379     __gshared int mbuf_done;
380     OutBuffer buf;
381     //printf("Module::gendocfile()\n");
382     if (!mbuf_done) // if not already read the ddoc files
383     {
384         mbuf_done = 1;
385         // Use our internal default
386         mbuf.writestring(ddoc_default);
387         // Override with DDOCFILE specified in the sc.ini file
388         char* p = getenv("DDOCFILE");
389         if (p)
390             global.params.ddocfiles.shift(p);
391         // Override with the ddoc macro files from the command line
392         for (size_t i = 0; i < global.params.ddocfiles.dim; i++)
393         {
394             auto buffer = readFile(m.loc, global.params.ddocfiles[i]);
395             // BUG: convert file contents to UTF-8 before use
396             const data = buffer.data;
397             //printf("file: '%.*s'\n", cast(int)data.length, data.ptr);
398             mbuf.write(data);
399         }
400     }
401     DocComment.parseMacros(m.escapetable, m.macrotable, mbuf[].ptr, mbuf[].length);
402     Scope* sc = Scope.createGlobal(m); // create root scope
403     DocComment* dc = DocComment.parse(m, m.comment);
404     dc.pmacrotable = &m.macrotable;
405     dc.escapetable = m.escapetable;
406     sc.lastdc = dc;
407     // Generate predefined macros
408     // Set the title to be the name of the module
409     {
410         const p = m.toPrettyChars().toDString;
411         m.macrotable.define("TITLE", p);
412     }
413     // Set time macros
414     {
415         time_t t;
416         time(&t);
417         char* p = ctime(&t);
418         p = mem.xstrdup(p);
419         m.macrotable.define("DATETIME", p.toDString());
420         m.macrotable.define("YEAR", p[20 .. 20 + 4]);
421     }
422     const srcfilename = m.srcfile.toString();
423     m.macrotable.define("SRCFILENAME", srcfilename);
424     const docfilename = m.docfile.toString();
425     m.macrotable.define("DOCFILENAME", docfilename);
426     if (dc.copyright)
427     {
428         dc.copyright.nooutput = 1;
429         m.macrotable.define("COPYRIGHT", dc.copyright._body[0 .. dc.copyright.bodylen]);
430     }
431     if (m.isDocFile)
432     {
433         const ploc = m.md ? &m.md.loc : &m.loc;
434         const loc = Loc(ploc.filename ? ploc.filename : srcfilename.ptr,
435                         ploc.linnum,
436                         ploc.charnum);
437 
438         size_t commentlen = strlen(cast(char*)m.comment);
439         Dsymbols a;
440         // https://issues.dlang.org/show_bug.cgi?id=9764
441         // Don't push m in a, to prevent emphasize ddoc file name.
442         if (dc.macros)
443         {
444             commentlen = dc.macros.name - m.comment;
445             dc.macros.write(loc, dc, sc, &a, &buf);
446         }
447         buf.write(m.comment[0 .. commentlen]);
448         highlightText(sc, &a, loc, buf, 0);
449     }
450     else
451     {
452         Dsymbols a;
453         a.push(m);
454         dc.writeSections(sc, &a, &buf);
455         emitMemberComments(m, buf, sc);
456     }
457     //printf("BODY= '%.*s'\n", cast(int)buf.length, buf.data);
458     m.macrotable.define("BODY", buf[]);
459     OutBuffer buf2;
460     buf2.writestring("$(DDOC)");
461     size_t end = buf2.length;
462     m.macrotable.expand(buf2, 0, end, null);
463     version (all)
464     {
465         /* Remove all the escape sequences from buf2,
466          * and make CR-LF the newline.
467          */
468         {
469             const slice = buf2[];
470             buf.setsize(0);
471             buf.reserve(slice.length);
472             auto p = slice.ptr;
473             for (size_t j = 0; j < slice.length; j++)
474             {
475                 char c = p[j];
476                 if (c == 0xFF && j + 1 < slice.length)
477                 {
478                     j++;
479                     continue;
480                 }
481                 if (c == '\n')
482                     buf.writeByte('\r');
483                 else if (c == '\r')
484                 {
485                     buf.writestring("\r\n");
486                     if (j + 1 < slice.length && p[j + 1] == '\n')
487                     {
488                         j++;
489                     }
490                     continue;
491                 }
492                 buf.writeByte(c);
493             }
494         }
495         writeFile(m.loc, m.docfile.toString(), buf[]);
496     }
497     else
498     {
499         /* Remove all the escape sequences from buf2
500          */
501         {
502             size_t i = 0;
503             char* p = buf2.data;
504             for (size_t j = 0; j < buf2.length; j++)
505             {
506                 if (p[j] == 0xFF && j + 1 < buf2.length)
507                 {
508                     j++;
509                     continue;
510                 }
511                 p[i] = p[j];
512                 i++;
513             }
514             buf2.setsize(i);
515         }
516         writeFile(m.loc, m.docfile.toString(), buf2[]);
517     }
518 }
519 
520 /****************************************************
521  * Having unmatched parentheses can hose the output of Ddoc,
522  * as the macros depend on properly nested parentheses.
523  * This function replaces all ( with $(LPAREN) and ) with $(RPAREN)
524  * to preserve text literally. This also means macros in the
525  * text won't be expanded.
526  */
527 void escapeDdocString(OutBuffer* buf, size_t start)
528 {
529     for (size_t u = start; u < buf.length; u++)
530     {
531         char c = (*buf)[u];
532         switch (c)
533         {
534         case '$':
535             buf.remove(u, 1);
536             buf.insert(u, "$(DOLLAR)");
537             u += 8;
538             break;
539         case '(':
540             buf.remove(u, 1); //remove the (
541             buf.insert(u, "$(LPAREN)"); //insert this instead
542             u += 8; //skip over newly inserted macro
543             break;
544         case ')':
545             buf.remove(u, 1); //remove the )
546             buf.insert(u, "$(RPAREN)"); //insert this instead
547             u += 8; //skip over newly inserted macro
548             break;
549         default:
550             break;
551         }
552     }
553 }
554 
555 /****************************************************
556  * Having unmatched parentheses can hose the output of Ddoc,
557  * as the macros depend on properly nested parentheses.
558  *
559  * Fix by replacing unmatched ( with $(LPAREN) and unmatched ) with $(RPAREN).
560  *
561  * Params:
562  *  loc   = source location of start of text. It is a mutable copy to allow incrementing its linenum, for printing the correct line number when an error is encountered in a multiline block of ddoc.
563  *  buf   = an OutBuffer containing the DDoc
564  *  start = the index within buf to start replacing unmatched parentheses
565  *  respectBackslashEscapes = if true, always replace parentheses that are
566  *    directly preceeded by a backslash with $(LPAREN) or $(RPAREN) instead of
567  *    counting them as stray parentheses
568  */
569 private void escapeStrayParenthesis(Loc loc, OutBuffer* buf, size_t start, bool respectBackslashEscapes)
570 {
571     uint par_open = 0;
572     char inCode = 0;
573     bool atLineStart = true;
574     for (size_t u = start; u < buf.length; u++)
575     {
576         char c = (*buf)[u];
577         switch (c)
578         {
579         case '(':
580             if (!inCode)
581                 par_open++;
582             atLineStart = false;
583             break;
584         case ')':
585             if (!inCode)
586             {
587                 if (par_open == 0)
588                 {
589                     //stray ')'
590                     warning(loc, "Ddoc: Stray ')'. This may cause incorrect Ddoc output. Use $(RPAREN) instead for unpaired right parentheses.");
591                     buf.remove(u, 1); //remove the )
592                     buf.insert(u, "$(RPAREN)"); //insert this instead
593                     u += 8; //skip over newly inserted macro
594                 }
595                 else
596                     par_open--;
597             }
598             atLineStart = false;
599             break;
600         case '\n':
601             atLineStart = true;
602             version (none)
603             {
604                 // For this to work, loc must be set to the beginning of the passed
605                 // text which is currently not possible
606                 // (loc is set to the Loc of the Dsymbol)
607                 loc.linnum++;
608             }
609             break;
610         case ' ':
611         case '\r':
612         case '\t':
613             break;
614         case '-':
615         case '`':
616         case '~':
617             // Issue 15465: don't try to escape unbalanced parens inside code
618             // blocks.
619             int numdash = 1;
620             for (++u; u < buf.length && (*buf)[u] == c; ++u)
621                 ++numdash;
622             --u;
623             if (c == '`' || (atLineStart && numdash >= 3))
624             {
625                 if (inCode == c)
626                     inCode = 0;
627                 else if (!inCode)
628                     inCode = c;
629             }
630             atLineStart = false;
631             break;
632         case '\\':
633             // replace backslash-escaped parens with their macros
634             if (!inCode && respectBackslashEscapes && u+1 < buf.length && global.params.markdown)
635             {
636                 if ((*buf)[u+1] == '(' || (*buf)[u+1] == ')')
637                 {
638                     const paren = (*buf)[u+1] == '(' ? "$(LPAREN)" : "$(RPAREN)";
639                     buf.remove(u, 2); //remove the \)
640                     buf.insert(u, paren); //insert this instead
641                     u += 8; //skip over newly inserted macro
642                 }
643                 else if ((*buf)[u+1] == '\\')
644                     ++u;
645             }
646             break;
647         default:
648             atLineStart = false;
649             break;
650         }
651     }
652     if (par_open) // if any unmatched lparens
653     {
654         par_open = 0;
655         for (size_t u = buf.length; u > start;)
656         {
657             u--;
658             char c = (*buf)[u];
659             switch (c)
660             {
661             case ')':
662                 par_open++;
663                 break;
664             case '(':
665                 if (par_open == 0)
666                 {
667                     //stray '('
668                     warning(loc, "Ddoc: Stray '('. This may cause incorrect Ddoc output. Use $(LPAREN) instead for unpaired left parentheses.");
669                     buf.remove(u, 1); //remove the (
670                     buf.insert(u, "$(LPAREN)"); //insert this instead
671                 }
672                 else
673                     par_open--;
674                 break;
675             default:
676                 break;
677             }
678         }
679     }
680 }
681 
682 // Basically, this is to skip over things like private{} blocks in a struct or
683 // class definition that don't add any components to the qualified name.
684 private Scope* skipNonQualScopes(Scope* sc)
685 {
686     while (sc && !sc.scopesym)
687         sc = sc.enclosing;
688     return sc;
689 }
690 
691 private bool emitAnchorName(ref OutBuffer buf, Dsymbol s, Scope* sc, bool includeParent)
692 {
693     if (!s || s.isPackage() || s.isModule())
694         return false;
695     // Add parent names first
696     bool dot = false;
697     auto eponymousParent = getEponymousParent(s);
698     if (includeParent && s.parent || eponymousParent)
699         dot = emitAnchorName(buf, s.parent, sc, includeParent);
700     else if (includeParent && sc)
701         dot = emitAnchorName(buf, sc.scopesym, skipNonQualScopes(sc.enclosing), includeParent);
702     // Eponymous template members can share the parent anchor name
703     if (eponymousParent)
704         return dot;
705     if (dot)
706         buf.writeByte('.');
707     // Use "this" not "__ctor"
708     TemplateDeclaration td;
709     if (s.isCtorDeclaration() || ((td = s.isTemplateDeclaration()) !is null && td.onemember && td.onemember.isCtorDeclaration()))
710     {
711         buf.writestring("this");
712     }
713     else
714     {
715         /* We just want the identifier, not overloads like TemplateDeclaration::toChars.
716          * We don't want the template parameter list and constraints. */
717         buf.writestring(s.Dsymbol.toChars());
718     }
719     return true;
720 }
721 
722 private void emitAnchor(ref OutBuffer buf, Dsymbol s, Scope* sc, bool forHeader = false)
723 {
724     Identifier ident;
725     {
726         OutBuffer anc;
727         emitAnchorName(anc, s, skipNonQualScopes(sc), true);
728         ident = Identifier.idPool(anc[]);
729     }
730 
731     auto pcount = cast(void*)ident in sc.anchorCounts;
732     typeof(*pcount) count;
733     if (!forHeader)
734     {
735         if (pcount)
736         {
737             // Existing anchor,
738             // don't write an anchor for matching consecutive ditto symbols
739             TemplateDeclaration td = getEponymousParent(s);
740             if (sc.prevAnchor == ident && sc.lastdc && (isDitto(s.comment) || (td && isDitto(td.comment))))
741                 return;
742 
743             count = ++*pcount;
744         }
745         else
746         {
747             sc.anchorCounts[cast(void*)ident] = 1;
748             count = 1;
749         }
750     }
751 
752     // cache anchor name
753     sc.prevAnchor = ident;
754     auto macroName = forHeader ? "DDOC_HEADER_ANCHOR" : "DDOC_ANCHOR";
755 
756     if (auto imp = s.isImport())
757     {
758         // For example: `public import core.stdc.string : memcpy, memcmp;`
759         if (imp.aliases.dim > 0)
760         {
761             for(int i = 0; i < imp.aliases.dim; i++)
762             {
763                 // Need to distinguish between
764                 // `public import core.stdc.string : memcpy, memcmp;` and
765                 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
766                 auto a = imp.aliases[i];
767                 auto id = a ? a : imp.names[i];
768                 auto loc = Loc.init;
769                 if (auto symFromId = sc.search(loc, id, null))
770                 {
771                     emitAnchor(buf, symFromId, sc, forHeader);
772                 }
773             }
774         }
775         else
776         {
777             // For example: `public import str = core.stdc.string;`
778             if (imp.aliasId)
779             {
780                 auto symbolName = imp.aliasId.toString();
781 
782                 buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
783                     cast(int) symbolName.length, symbolName.ptr);
784 
785                 if (forHeader)
786                 {
787                     buf.printf(", %.*s", cast(int) symbolName.length, symbolName.ptr);
788                 }
789             }
790             else
791             {
792                 // The general case:  `public import core.stdc.string;`
793 
794                 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
795                 void printFullyQualifiedImport()
796                 {
797                     if (imp.packages && imp.packages.dim)
798                     {
799                         foreach (const pid; *imp.packages)
800                         {
801                             buf.printf("%s.", pid.toChars());
802                         }
803                     }
804                     buf.writestring(imp.id.toString());
805                 }
806 
807                 buf.printf("$(%.*s ", cast(int) macroName.length, macroName.ptr);
808                 printFullyQualifiedImport();
809 
810                 if (forHeader)
811                 {
812                     buf.printf(", ");
813                     printFullyQualifiedImport();
814                 }
815             }
816 
817             buf.writeByte(')');
818         }
819     }
820     else
821     {
822         auto symbolName = ident.toString();
823         buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
824             cast(int) symbolName.length, symbolName.ptr);
825 
826         // only append count once there's a duplicate
827         if (count > 1)
828             buf.printf(".%u", count);
829 
830         if (forHeader)
831         {
832             Identifier shortIdent;
833             {
834                 OutBuffer anc;
835                 emitAnchorName(anc, s, skipNonQualScopes(sc), false);
836                 shortIdent = Identifier.idPool(anc[]);
837             }
838 
839             auto shortName = shortIdent.toString();
840             buf.printf(", %.*s", cast(int) shortName.length, shortName.ptr);
841         }
842 
843         buf.writeByte(')');
844     }
845 }
846 
847 /******************************* emitComment **********************************/
848 
849 /** Get leading indentation from 'src' which represents lines of code. */
850 private size_t getCodeIndent(const(char)* src)
851 {
852     while (src && (*src == '\r' || *src == '\n'))
853         ++src; // skip until we find the first non-empty line
854     size_t codeIndent = 0;
855     while (src && (*src == ' ' || *src == '\t'))
856     {
857         codeIndent++;
858         src++;
859     }
860     return codeIndent;
861 }
862 
863 /** Recursively expand template mixin member docs into the scope. */
864 private void expandTemplateMixinComments(TemplateMixin tm, ref OutBuffer buf, Scope* sc)
865 {
866     if (!tm.semanticRun)
867         tm.dsymbolSemantic(sc);
868     TemplateDeclaration td = (tm && tm.tempdecl) ? tm.tempdecl.isTemplateDeclaration() : null;
869     if (td && td.members)
870     {
871         for (size_t i = 0; i < td.members.dim; i++)
872         {
873             Dsymbol sm = (*td.members)[i];
874             TemplateMixin tmc = sm.isTemplateMixin();
875             if (tmc && tmc.comment)
876                 expandTemplateMixinComments(tmc, buf, sc);
877             else
878                 emitComment(sm, buf, sc);
879         }
880     }
881 }
882 
883 private void emitMemberComments(ScopeDsymbol sds, ref OutBuffer buf, Scope* sc)
884 {
885     if (!sds.members)
886         return;
887     //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars());
888     const(char)[] m = "$(DDOC_MEMBERS ";
889     if (sds.isTemplateDeclaration())
890         m = "$(DDOC_TEMPLATE_MEMBERS ";
891     else if (sds.isClassDeclaration())
892         m = "$(DDOC_CLASS_MEMBERS ";
893     else if (sds.isStructDeclaration())
894         m = "$(DDOC_STRUCT_MEMBERS ";
895     else if (sds.isEnumDeclaration())
896         m = "$(DDOC_ENUM_MEMBERS ";
897     else if (sds.isModule())
898         m = "$(DDOC_MODULE_MEMBERS ";
899     size_t offset1 = buf.length; // save starting offset
900     buf.writestring(m);
901     size_t offset2 = buf.length; // to see if we write anything
902     sc = sc.push(sds);
903     for (size_t i = 0; i < sds.members.dim; i++)
904     {
905         Dsymbol s = (*sds.members)[i];
906         //printf("\ts = '%s'\n", s.toChars());
907         // only expand if parent is a non-template (semantic won't work)
908         if (s.comment && s.isTemplateMixin() && s.parent && !s.parent.isTemplateDeclaration())
909             expandTemplateMixinComments(cast(TemplateMixin)s, buf, sc);
910         emitComment(s, buf, sc);
911     }
912     emitComment(null, buf, sc);
913     sc.pop();
914     if (buf.length == offset2)
915     {
916         /* Didn't write out any members, so back out last write
917          */
918         buf.setsize(offset1);
919     }
920     else
921         buf.writestring(")");
922 }
923 
924 private void emitProtection(ref OutBuffer buf, Import i)
925 {
926     // imports are private by default, which is different from other declarations
927     // so they should explicitly show their protection
928     emitProtection(buf, i.protection);
929 }
930 
931 private void emitProtection(ref OutBuffer buf, Declaration d)
932 {
933     auto prot = d.protection;
934     if (prot.kind != Prot.Kind.undefined && prot.kind != Prot.Kind.public_)
935     {
936         emitProtection(buf, prot);
937     }
938 }
939 
940 private void emitProtection(ref OutBuffer buf, Prot prot)
941 {
942     protectionToBuffer(&buf, prot);
943     buf.writeByte(' ');
944 }
945 
946 private void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc)
947 {
948     extern (C++) final class EmitComment : Visitor
949     {
950         alias visit = Visitor.visit;
951     public:
952         OutBuffer* buf;
953         Scope* sc;
954 
955         extern (D) this(ref OutBuffer buf, Scope* sc)
956         {
957             this.buf = &buf;
958             this.sc = sc;
959         }
960 
961         override void visit(Dsymbol)
962         {
963         }
964 
965         override void visit(InvariantDeclaration)
966         {
967         }
968 
969         override void visit(UnitTestDeclaration)
970         {
971         }
972 
973         override void visit(PostBlitDeclaration)
974         {
975         }
976 
977         override void visit(DtorDeclaration)
978         {
979         }
980 
981         override void visit(StaticCtorDeclaration)
982         {
983         }
984 
985         override void visit(StaticDtorDeclaration)
986         {
987         }
988 
989         override void visit(TypeInfoDeclaration)
990         {
991         }
992 
993         void emit(Scope* sc, Dsymbol s, const(char)* com)
994         {
995             if (s && sc.lastdc && isDitto(com))
996             {
997                 sc.lastdc.a.push(s);
998                 return;
999             }
1000             // Put previous doc comment if exists
1001             if (DocComment* dc = sc.lastdc)
1002             {
1003                 assert(dc.a.dim > 0, "Expects at least one declaration for a" ~
1004                     "documentation comment");
1005 
1006                 auto symbol = dc.a[0];
1007 
1008                 buf.writestring("$(DDOC_MEMBER");
1009                 buf.writestring("$(DDOC_MEMBER_HEADER");
1010                 emitAnchor(*buf, symbol, sc, true);
1011                 buf.writeByte(')');
1012 
1013                 // Put the declaration signatures as the document 'title'
1014                 buf.writestring(ddoc_decl_s);
1015                 for (size_t i = 0; i < dc.a.dim; i++)
1016                 {
1017                     Dsymbol sx = dc.a[i];
1018                     // the added linebreaks in here make looking at multiple
1019                     // signatures more appealing
1020                     if (i == 0)
1021                     {
1022                         size_t o = buf.length;
1023                         toDocBuffer(sx, *buf, sc);
1024                         highlightCode(sc, sx, *buf, o);
1025                         buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1026                         continue;
1027                     }
1028                     buf.writestring("$(DDOC_DITTO ");
1029                     {
1030                         size_t o = buf.length;
1031                         toDocBuffer(sx, *buf, sc);
1032                         highlightCode(sc, sx, *buf, o);
1033                     }
1034                     buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1035                     buf.writeByte(')');
1036                 }
1037                 buf.writestring(ddoc_decl_e);
1038                 // Put the ddoc comment as the document 'description'
1039                 buf.writestring(ddoc_decl_dd_s);
1040                 {
1041                     dc.writeSections(sc, &dc.a, buf);
1042                     if (ScopeDsymbol sds = dc.a[0].isScopeDsymbol())
1043                         emitMemberComments(sds, *buf, sc);
1044                 }
1045                 buf.writestring(ddoc_decl_dd_e);
1046                 buf.writeByte(')');
1047                 //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0);
1048             }
1049             if (s)
1050             {
1051                 DocComment* dc = DocComment.parse(s, com);
1052                 dc.pmacrotable = &sc._module.macrotable;
1053                 sc.lastdc = dc;
1054             }
1055         }
1056 
1057         override void visit(Import imp)
1058         {
1059             if (imp.prot().kind != Prot.Kind.public_ && sc.protection.kind != Prot.Kind.export_)
1060                 return;
1061 
1062             if (imp.comment)
1063                 emit(sc, imp, imp.comment);
1064         }
1065 
1066         override void visit(Declaration d)
1067         {
1068             //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment);
1069             //printf("type = %p\n", d.type);
1070             const(char)* com = d.comment;
1071             if (TemplateDeclaration td = getEponymousParent(d))
1072             {
1073                 if (isDitto(td.comment))
1074                     com = td.comment;
1075                 else
1076                     com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
1077             }
1078             else
1079             {
1080                 if (!d.ident)
1081                     return;
1082                 if (!d.type)
1083                 {
1084                     if (!d.isCtorDeclaration() &&
1085                         !d.isAliasDeclaration() &&
1086                         !d.isVarDeclaration())
1087                     {
1088                         return;
1089                     }
1090                 }
1091                 if (d.protection.kind == Prot.Kind.private_ || sc.protection.kind == Prot.Kind.private_)
1092                     return;
1093             }
1094             if (!com)
1095                 return;
1096             emit(sc, d, com);
1097         }
1098 
1099         override void visit(AggregateDeclaration ad)
1100         {
1101             //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars());
1102             const(char)* com = ad.comment;
1103             if (TemplateDeclaration td = getEponymousParent(ad))
1104             {
1105                 if (isDitto(td.comment))
1106                     com = td.comment;
1107                 else
1108                     com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
1109             }
1110             else
1111             {
1112                 if (ad.prot().kind == Prot.Kind.private_ || sc.protection.kind == Prot.Kind.private_)
1113                     return;
1114                 if (!ad.comment)
1115                     return;
1116             }
1117             if (!com)
1118                 return;
1119             emit(sc, ad, com);
1120         }
1121 
1122         override void visit(TemplateDeclaration td)
1123         {
1124             //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind());
1125             if (td.prot().kind == Prot.Kind.private_ || sc.protection.kind == Prot.Kind.private_)
1126                 return;
1127             if (!td.comment)
1128                 return;
1129             if (Dsymbol ss = getEponymousMember(td))
1130             {
1131                 ss.accept(this);
1132                 return;
1133             }
1134             emit(sc, td, td.comment);
1135         }
1136 
1137         override void visit(EnumDeclaration ed)
1138         {
1139             if (ed.prot().kind == Prot.Kind.private_ || sc.protection.kind == Prot.Kind.private_)
1140                 return;
1141             if (ed.isAnonymous() && ed.members)
1142             {
1143                 for (size_t i = 0; i < ed.members.dim; i++)
1144                 {
1145                     Dsymbol s = (*ed.members)[i];
1146                     emitComment(s, *buf, sc);
1147                 }
1148                 return;
1149             }
1150             if (!ed.comment)
1151                 return;
1152             if (ed.isAnonymous())
1153                 return;
1154             emit(sc, ed, ed.comment);
1155         }
1156 
1157         override void visit(EnumMember em)
1158         {
1159             //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment);
1160             if (em.prot().kind == Prot.Kind.private_ || sc.protection.kind == Prot.Kind.private_)
1161                 return;
1162             if (!em.comment)
1163                 return;
1164             emit(sc, em, em.comment);
1165         }
1166 
1167         override void visit(AttribDeclaration ad)
1168         {
1169             //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);
1170             /* A general problem with this,
1171              * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516
1172              * is that attributes are not transmitted through to the underlying
1173              * member declarations for template bodies, because semantic analysis
1174              * is not done for template declaration bodies
1175              * (only template instantiations).
1176              * Hence, Ddoc omits attributes from template members.
1177              */
1178             Dsymbols* d = ad.include(null);
1179             if (d)
1180             {
1181                 for (size_t i = 0; i < d.dim; i++)
1182                 {
1183                     Dsymbol s = (*d)[i];
1184                     //printf("AttribDeclaration::emitComment %s\n", s.toChars());
1185                     emitComment(s, *buf, sc);
1186                 }
1187             }
1188         }
1189 
1190         override void visit(ProtDeclaration pd)
1191         {
1192             if (pd.decl)
1193             {
1194                 Scope* scx = sc;
1195                 sc = sc.copy();
1196                 sc.protection = pd.protection;
1197                 visit(cast(AttribDeclaration)pd);
1198                 scx.lastdc = sc.lastdc;
1199                 sc = sc.pop();
1200             }
1201         }
1202 
1203         override void visit(ConditionalDeclaration cd)
1204         {
1205             //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
1206             if (cd.condition.inc != Include.notComputed)
1207             {
1208                 visit(cast(AttribDeclaration)cd);
1209                 return;
1210             }
1211             /* If generating doc comment, be careful because if we're inside
1212              * a template, then include(null) will fail.
1213              */
1214             Dsymbols* d = cd.decl ? cd.decl : cd.elsedecl;
1215             for (size_t i = 0; i < d.dim; i++)
1216             {
1217                 Dsymbol s = (*d)[i];
1218                 emitComment(s, *buf, sc);
1219             }
1220         }
1221     }
1222 
1223     scope EmitComment v = new EmitComment(buf, sc);
1224     if (!s)
1225         v.emit(sc, null, null);
1226     else
1227         s.accept(v);
1228 }
1229 
1230 private void toDocBuffer(Dsymbol s, ref OutBuffer buf, Scope* sc)
1231 {
1232     extern (C++) final class ToDocBuffer : Visitor
1233     {
1234         alias visit = Visitor.visit;
1235     public:
1236         OutBuffer* buf;
1237         Scope* sc;
1238 
1239         extern (D) this(ref OutBuffer buf, Scope* sc)
1240         {
1241             this.buf = &buf;
1242             this.sc = sc;
1243         }
1244 
1245         override void visit(Dsymbol s)
1246         {
1247             //printf("Dsymbol::toDocbuffer() %s\n", s.toChars());
1248             HdrGenState hgs;
1249             hgs.ddoc = true;
1250             .toCBuffer(s, buf, &hgs);
1251         }
1252 
1253         void prefix(Dsymbol s)
1254         {
1255             if (s.isDeprecated())
1256                 buf.writestring("deprecated ");
1257             if (Declaration d = s.isDeclaration())
1258             {
1259                 emitProtection(*buf, d);
1260                 if (d.isStatic())
1261                     buf.writestring("static ");
1262                 else if (d.isFinal())
1263                     buf.writestring("final ");
1264                 else if (d.isAbstract())
1265                     buf.writestring("abstract ");
1266 
1267                 if (d.isFuncDeclaration())      // functionToBufferFull handles this
1268                     return;
1269 
1270                 if (d.isImmutable())
1271                     buf.writestring("immutable ");
1272                 if (d.storage_class & STC.shared_)
1273                     buf.writestring("shared ");
1274                 if (d.isWild())
1275                     buf.writestring("inout ");
1276                 if (d.isConst())
1277                     buf.writestring("const ");
1278 
1279                 if (d.isSynchronized())
1280                     buf.writestring("synchronized ");
1281 
1282                 if (d.storage_class & STC.manifest)
1283                     buf.writestring("enum ");
1284 
1285                 // Add "auto" for the untyped variable in template members
1286                 if (!d.type && d.isVarDeclaration() &&
1287                     !d.isImmutable() && !(d.storage_class & STC.shared_) && !d.isWild() && !d.isConst() &&
1288                     !d.isSynchronized())
1289                 {
1290                     buf.writestring("auto ");
1291                 }
1292             }
1293         }
1294 
1295         override void visit(Import i)
1296         {
1297             HdrGenState hgs;
1298             hgs.ddoc = true;
1299             emitProtection(*buf, i);
1300             .toCBuffer(i, buf, &hgs);
1301         }
1302 
1303         override void visit(Declaration d)
1304         {
1305             if (!d.ident)
1306                 return;
1307             TemplateDeclaration td = getEponymousParent(d);
1308             //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--");
1309             HdrGenState hgs;
1310             hgs.ddoc = true;
1311             if (d.isDeprecated())
1312                 buf.writestring("$(DEPRECATED ");
1313             prefix(d);
1314             if (d.type)
1315             {
1316                 Type origType = d.originalType ? d.originalType : d.type;
1317                 if (origType.ty == Tfunction)
1318                 {
1319                     functionToBufferFull(cast(TypeFunction)origType, buf, d.ident, &hgs, td);
1320                 }
1321                 else
1322                     .toCBuffer(origType, buf, d.ident, &hgs);
1323             }
1324             else
1325                 buf.writestring(d.ident.toString());
1326             if (d.isVarDeclaration() && td)
1327             {
1328                 buf.writeByte('(');
1329                 if (td.origParameters && td.origParameters.dim)
1330                 {
1331                     for (size_t i = 0; i < td.origParameters.dim; i++)
1332                     {
1333                         if (i)
1334                             buf.writestring(", ");
1335                         toCBuffer((*td.origParameters)[i], buf, &hgs);
1336                     }
1337                 }
1338                 buf.writeByte(')');
1339             }
1340             // emit constraints if declaration is a templated declaration
1341             if (td && td.constraint)
1342             {
1343                 bool noFuncDecl = td.isFuncDeclaration() is null;
1344                 if (noFuncDecl)
1345                 {
1346                     buf.writestring("$(DDOC_CONSTRAINT ");
1347                 }
1348 
1349                 .toCBuffer(td.constraint, buf, &hgs);
1350 
1351                 if (noFuncDecl)
1352                 {
1353                     buf.writestring(")");
1354                 }
1355             }
1356             if (d.isDeprecated())
1357                 buf.writestring(")");
1358             buf.writestring(";\n");
1359         }
1360 
1361         override void visit(AliasDeclaration ad)
1362         {
1363             //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars());
1364             if (!ad.ident)
1365                 return;
1366             if (ad.isDeprecated())
1367                 buf.writestring("deprecated ");
1368             emitProtection(*buf, ad);
1369             buf.printf("alias %s = ", ad.toChars());
1370             if (Dsymbol s = ad.aliassym) // ident alias
1371             {
1372                 prettyPrintDsymbol(s, ad.parent);
1373             }
1374             else if (Type type = ad.getType()) // type alias
1375             {
1376                 if (type.ty == Tclass || type.ty == Tstruct || type.ty == Tenum)
1377                 {
1378                     if (Dsymbol s = type.toDsymbol(null)) // elaborate type
1379                         prettyPrintDsymbol(s, ad.parent);
1380                     else
1381                         buf.writestring(type.toChars());
1382                 }
1383                 else
1384                 {
1385                     // simple type
1386                     buf.writestring(type.toChars());
1387                 }
1388             }
1389             buf.writestring(";\n");
1390         }
1391 
1392         void parentToBuffer(Dsymbol s)
1393         {
1394             if (s && !s.isPackage() && !s.isModule())
1395             {
1396                 parentToBuffer(s.parent);
1397                 buf.writestring(s.toChars());
1398                 buf.writestring(".");
1399             }
1400         }
1401 
1402         static bool inSameModule(Dsymbol s, Dsymbol p)
1403         {
1404             for (; s; s = s.parent)
1405             {
1406                 if (s.isModule())
1407                     break;
1408             }
1409             for (; p; p = p.parent)
1410             {
1411                 if (p.isModule())
1412                     break;
1413             }
1414             return s == p;
1415         }
1416 
1417         void prettyPrintDsymbol(Dsymbol s, Dsymbol parent)
1418         {
1419             if (s.parent && (s.parent == parent)) // in current scope -> naked name
1420             {
1421                 buf.writestring(s.toChars());
1422             }
1423             else if (!inSameModule(s, parent)) // in another module -> full name
1424             {
1425                 buf.writestring(s.toPrettyChars());
1426             }
1427             else // nested in a type in this module -> full name w/o module name
1428             {
1429                 // if alias is nested in a user-type use module-scope lookup
1430                 if (!parent.isModule() && !parent.isPackage())
1431                     buf.writestring(".");
1432                 parentToBuffer(s.parent);
1433                 buf.writestring(s.toChars());
1434             }
1435         }
1436 
1437         override void visit(AggregateDeclaration ad)
1438         {
1439             if (!ad.ident)
1440                 return;
1441             version (none)
1442             {
1443                 emitProtection(buf, ad);
1444             }
1445             buf.printf("%s %s", ad.kind(), ad.toChars());
1446             buf.writestring(";\n");
1447         }
1448 
1449         override void visit(StructDeclaration sd)
1450         {
1451             //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars());
1452             if (!sd.ident)
1453                 return;
1454             version (none)
1455             {
1456                 emitProtection(buf, sd);
1457             }
1458             if (TemplateDeclaration td = getEponymousParent(sd))
1459             {
1460                 toDocBuffer(td, *buf, sc);
1461             }
1462             else
1463             {
1464                 buf.printf("%s %s", sd.kind(), sd.toChars());
1465             }
1466             buf.writestring(";\n");
1467         }
1468 
1469         override void visit(ClassDeclaration cd)
1470         {
1471             //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars());
1472             if (!cd.ident)
1473                 return;
1474             version (none)
1475             {
1476                 emitProtection(*buf, cd);
1477             }
1478             if (TemplateDeclaration td = getEponymousParent(cd))
1479             {
1480                 toDocBuffer(td, *buf, sc);
1481             }
1482             else
1483             {
1484                 if (!cd.isInterfaceDeclaration() && cd.isAbstract())
1485                     buf.writestring("abstract ");
1486                 buf.printf("%s %s", cd.kind(), cd.toChars());
1487             }
1488             int any = 0;
1489             for (size_t i = 0; i < cd.baseclasses.dim; i++)
1490             {
1491                 BaseClass* bc = (*cd.baseclasses)[i];
1492                 if (bc.sym && bc.sym.ident == Id.Object)
1493                     continue;
1494                 if (any)
1495                     buf.writestring(", ");
1496                 else
1497                 {
1498                     buf.writestring(": ");
1499                     any = 1;
1500                 }
1501 
1502                 if (bc.sym)
1503                 {
1504                     buf.printf("$(DDOC_PSUPER_SYMBOL %s)", bc.sym.toPrettyChars());
1505                 }
1506                 else
1507                 {
1508                     HdrGenState hgs;
1509                     .toCBuffer(bc.type, buf, null, &hgs);
1510                 }
1511             }
1512             buf.writestring(";\n");
1513         }
1514 
1515         override void visit(EnumDeclaration ed)
1516         {
1517             if (!ed.ident)
1518                 return;
1519             buf.printf("%s %s", ed.kind(), ed.toChars());
1520             if (ed.memtype)
1521             {
1522                 buf.writestring(": $(DDOC_ENUM_BASETYPE ");
1523                 HdrGenState hgs;
1524                 .toCBuffer(ed.memtype, buf, null, &hgs);
1525                 buf.writestring(")");
1526             }
1527             buf.writestring(";\n");
1528         }
1529 
1530         override void visit(EnumMember em)
1531         {
1532             if (!em.ident)
1533                 return;
1534             buf.writestring(em.toChars());
1535         }
1536     }
1537 
1538     scope ToDocBuffer v = new ToDocBuffer(buf, sc);
1539     s.accept(v);
1540 }
1541 
1542 /***********************************************************
1543  */
1544 struct DocComment
1545 {
1546     Sections sections;      // Section*[]
1547     Section summary;
1548     Section copyright;
1549     Section macros;
1550     MacroTable* pmacrotable;
1551     Escape* escapetable;
1552     Dsymbols a;
1553 
1554     static DocComment* parse(Dsymbol s, const(char)* comment)
1555     {
1556         //printf("parse(%s): '%s'\n", s.toChars(), comment);
1557         auto dc = new DocComment();
1558         dc.a.push(s);
1559         if (!comment)
1560             return dc;
1561         dc.parseSections(comment);
1562         for (size_t i = 0; i < dc.sections.dim; i++)
1563         {
1564             Section sec = dc.sections[i];
1565             if (iequals("copyright", sec.name[0 .. sec.namelen]))
1566             {
1567                 dc.copyright = sec;
1568             }
1569             if (iequals("macros", sec.name[0 .. sec.namelen]))
1570             {
1571                 dc.macros = sec;
1572             }
1573         }
1574         return dc;
1575     }
1576 
1577     /************************************************
1578      * Parse macros out of Macros: section.
1579      * Macros are of the form:
1580      *      name1 = value1
1581      *
1582      *      name2 = value2
1583      */
1584     static void parseMacros(Escape* escapetable, ref MacroTable pmacrotable, const(char)* m, size_t mlen)
1585     {
1586         const(char)* p = m;
1587         size_t len = mlen;
1588         const(char)* pend = p + len;
1589         const(char)* tempstart = null;
1590         size_t templen = 0;
1591         const(char)* namestart = null;
1592         size_t namelen = 0; // !=0 if line continuation
1593         const(char)* textstart = null;
1594         size_t textlen = 0;
1595         while (p < pend)
1596         {
1597             // Skip to start of macro
1598             while (1)
1599             {
1600                 if (p >= pend)
1601                     goto Ldone;
1602                 switch (*p)
1603                 {
1604                 case ' ':
1605                 case '\t':
1606                     p++;
1607                     continue;
1608                 case '\r':
1609                 case '\n':
1610                     p++;
1611                     goto Lcont;
1612                 default:
1613                     if (isIdStart(p))
1614                         break;
1615                     if (namelen)
1616                         goto Ltext; // continuation of prev macro
1617                     goto Lskipline;
1618                 }
1619                 break;
1620             }
1621             tempstart = p;
1622             while (1)
1623             {
1624                 if (p >= pend)
1625                     goto Ldone;
1626                 if (!isIdTail(p))
1627                     break;
1628                 p += utfStride(p);
1629             }
1630             templen = p - tempstart;
1631             while (1)
1632             {
1633                 if (p >= pend)
1634                     goto Ldone;
1635                 if (!(*p == ' ' || *p == '\t'))
1636                     break;
1637                 p++;
1638             }
1639             if (*p != '=')
1640             {
1641                 if (namelen)
1642                     goto Ltext; // continuation of prev macro
1643                 goto Lskipline;
1644             }
1645             p++;
1646             if (p >= pend)
1647                 goto Ldone;
1648             if (namelen)
1649             {
1650                 // Output existing macro
1651             L1:
1652                 //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
1653                 if (iequals("ESCAPES", namestart[0 .. namelen]))
1654                     parseEscapes(escapetable, textstart, textlen);
1655                 else
1656                     pmacrotable.define(namestart[0 .. namelen], textstart[0 .. textlen]);
1657                 namelen = 0;
1658                 if (p >= pend)
1659                     break;
1660             }
1661             namestart = tempstart;
1662             namelen = templen;
1663             while (p < pend && (*p == ' ' || *p == '\t'))
1664                 p++;
1665             textstart = p;
1666         Ltext:
1667             while (p < pend && *p != '\r' && *p != '\n')
1668                 p++;
1669             textlen = p - textstart;
1670             p++;
1671             //printf("p = %p, pend = %p\n", p, pend);
1672         Lcont:
1673             continue;
1674         Lskipline:
1675             // Ignore this line
1676             while (p < pend && *p != '\r' && *p != '\n')
1677                 p++;
1678         }
1679     Ldone:
1680         if (namelen)
1681             goto L1; // write out last one
1682     }
1683 
1684     /**************************************
1685      * Parse escapes of the form:
1686      *      /c/string/
1687      * where c is a single character.
1688      * Multiple escapes can be separated
1689      * by whitespace and/or commas.
1690      */
1691     static void parseEscapes(Escape* escapetable, const(char)* textstart, size_t textlen)
1692     {
1693         if (!escapetable)
1694         {
1695             escapetable = new Escape();
1696             memset(escapetable, 0, Escape.sizeof);
1697         }
1698         //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)textlen, textstart, pescapetable);
1699         const(char)* p = textstart;
1700         const(char)* pend = p + textlen;
1701         while (1)
1702         {
1703             while (1)
1704             {
1705                 if (p + 4 >= pend)
1706                     return;
1707                 if (!(*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ','))
1708                     break;
1709                 p++;
1710             }
1711             if (p[0] != '/' || p[2] != '/')
1712                 return;
1713             char c = p[1];
1714             p += 3;
1715             const(char)* start = p;
1716             while (1)
1717             {
1718                 if (p >= pend)
1719                     return;
1720                 if (*p == '/')
1721                     break;
1722                 p++;
1723             }
1724             size_t len = p - start;
1725             char* s = cast(char*)memcpy(mem.xmalloc(len + 1), start, len);
1726             s[len] = 0;
1727             escapetable.strings[c] = s[0 .. len];
1728             //printf("\t%c = '%s'\n", c, s);
1729             p++;
1730         }
1731     }
1732 
1733     /*****************************************
1734      * Parse next paragraph out of *pcomment.
1735      * Update *pcomment to point past paragraph.
1736      * Returns NULL if no more paragraphs.
1737      * If paragraph ends in 'identifier:',
1738      * then (*pcomment)[0 .. idlen] is the identifier.
1739      */
1740     void parseSections(const(char)* comment)
1741     {
1742         const(char)* p;
1743         const(char)* pstart;
1744         const(char)* pend;
1745         const(char)* idstart = null; // dead-store to prevent spurious warning
1746         size_t idlen;
1747         const(char)* name = null;
1748         size_t namelen = 0;
1749         //printf("parseSections('%s')\n", comment);
1750         p = comment;
1751         while (*p)
1752         {
1753             const(char)* pstart0 = p;
1754             p = skipwhitespace(p);
1755             pstart = p;
1756             pend = p;
1757 
1758             // Undo indent if starting with a list item
1759             if ((*p == '-' || *p == '+' || *p == '*') && (*(p+1) == ' ' || *(p+1) == '\t'))
1760                 pstart = pstart0;
1761             else
1762             {
1763                 const(char)* pitem = p;
1764                 while (*pitem >= '0' && *pitem <= '9')
1765                     ++pitem;
1766                 if (pitem > p && *pitem == '.' && (*(pitem+1) == ' ' || *(pitem+1) == '\t'))
1767                     pstart = pstart0;
1768             }
1769 
1770             /* Find end of section, which is ended by one of:
1771              *      'identifier:' (but not inside a code section)
1772              *      '\0'
1773              */
1774             idlen = 0;
1775             int inCode = 0;
1776             while (1)
1777             {
1778                 // Check for start/end of a code section
1779                 if (*p == '-' || *p == '`' || *p == '~')
1780                 {
1781                     char c = *p;
1782                     int numdash = 0;
1783                     while (*p == c)
1784                     {
1785                         ++numdash;
1786                         p++;
1787                     }
1788                     // BUG: handle UTF PS and LS too
1789                     if ((!*p || *p == '\r' || *p == '\n' || (!inCode && c != '-')) && numdash >= 3)
1790                     {
1791                         inCode = inCode == c ? false : c;
1792                         if (inCode)
1793                         {
1794                             // restore leading indentation
1795                             while (pstart0 < pstart && isIndentWS(pstart - 1))
1796                                 --pstart;
1797                         }
1798                     }
1799                     pend = p;
1800                 }
1801                 if (!inCode && isIdStart(p))
1802                 {
1803                     const(char)* q = p + utfStride(p);
1804                     while (isIdTail(q))
1805                         q += utfStride(q);
1806 
1807                     // Detected tag ends it
1808                     if (*q == ':' && isupper(*p)
1809                             && (isspace(q[1]) || q[1] == 0))
1810                     {
1811                         idlen = q - p;
1812                         idstart = p;
1813                         for (pend = p; pend > pstart; pend--)
1814                         {
1815                             if (pend[-1] == '\n')
1816                                 break;
1817                         }
1818                         p = q + 1;
1819                         break;
1820                     }
1821                 }
1822                 while (1)
1823                 {
1824                     if (!*p)
1825                         goto L1;
1826                     if (*p == '\n')
1827                     {
1828                         p++;
1829                         if (*p == '\n' && !summary && !namelen && !inCode)
1830                         {
1831                             pend = p;
1832                             p++;
1833                             goto L1;
1834                         }
1835                         break;
1836                     }
1837                     p++;
1838                     pend = p;
1839                 }
1840                 p = skipwhitespace(p);
1841             }
1842         L1:
1843             if (namelen || pstart < pend)
1844             {
1845                 Section s;
1846                 if (iequals("Params", name[0 .. namelen]))
1847                     s = new ParamSection();
1848                 else if (iequals("Macros", name[0 .. namelen]))
1849                     s = new MacroSection();
1850                 else
1851                     s = new Section();
1852                 s.name = name;
1853                 s.namelen = namelen;
1854                 s._body = pstart;
1855                 s.bodylen = pend - pstart;
1856                 s.nooutput = 0;
1857                 //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body);
1858                 sections.push(s);
1859                 if (!summary && !namelen)
1860                     summary = s;
1861             }
1862             if (idlen)
1863             {
1864                 name = idstart;
1865                 namelen = idlen;
1866             }
1867             else
1868             {
1869                 name = null;
1870                 namelen = 0;
1871                 if (!*p)
1872                     break;
1873             }
1874         }
1875     }
1876 
1877     void writeSections(Scope* sc, Dsymbols* a, OutBuffer* buf)
1878     {
1879         assert(a.dim);
1880         //printf("DocComment::writeSections()\n");
1881         Loc loc = (*a)[0].loc;
1882         if (Module m = (*a)[0].isModule())
1883         {
1884             if (m.md)
1885                 loc = m.md.loc;
1886         }
1887         size_t offset1 = buf.length;
1888         buf.writestring("$(DDOC_SECTIONS ");
1889         size_t offset2 = buf.length;
1890         for (size_t i = 0; i < sections.dim; i++)
1891         {
1892             Section sec = sections[i];
1893             if (sec.nooutput)
1894                 continue;
1895             //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body);
1896             if (!sec.namelen && i == 0)
1897             {
1898                 buf.writestring("$(DDOC_SUMMARY ");
1899                 size_t o = buf.length;
1900                 buf.write(sec._body[0 .. sec.bodylen]);
1901                 escapeStrayParenthesis(loc, buf, o, true);
1902                 highlightText(sc, a, loc, *buf, o);
1903                 buf.writestring(")");
1904             }
1905             else
1906                 sec.write(loc, &this, sc, a, buf);
1907         }
1908         for (size_t i = 0; i < a.dim; i++)
1909         {
1910             Dsymbol s = (*a)[i];
1911             if (Dsymbol td = getEponymousParent(s))
1912                 s = td;
1913             for (UnitTestDeclaration utd = s.ddocUnittest; utd; utd = utd.ddocUnittest)
1914             {
1915                 if (utd.protection.kind == Prot.Kind.private_ || !utd.comment || !utd.fbody)
1916                     continue;
1917                 // Strip whitespaces to avoid showing empty summary
1918                 const(char)* c = utd.comment;
1919                 while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r')
1920                     ++c;
1921                 buf.writestring("$(DDOC_EXAMPLES ");
1922                 size_t o = buf.length;
1923                 buf.writestring(cast(char*)c);
1924                 if (utd.codedoc)
1925                 {
1926                     auto codedoc = utd.codedoc.stripLeadingNewlines;
1927                     size_t n = getCodeIndent(codedoc);
1928                     while (n--)
1929                         buf.writeByte(' ');
1930                     buf.writestring("----\n");
1931                     buf.writestring(codedoc);
1932                     buf.writestring("----\n");
1933                     highlightText(sc, a, loc, *buf, o);
1934                 }
1935                 buf.writestring(")");
1936             }
1937         }
1938         if (buf.length == offset2)
1939         {
1940             /* Didn't write out any sections, so back out last write
1941              */
1942             buf.setsize(offset1);
1943             buf.writestring("\n");
1944         }
1945         else
1946             buf.writestring(")");
1947     }
1948 }
1949 
1950 /*****************************************
1951  * Return true if comment consists entirely of "ditto".
1952  */
1953 private bool isDitto(const(char)* comment)
1954 {
1955     if (comment)
1956     {
1957         const(char)* p = skipwhitespace(comment);
1958         if (Port.memicmp(p, "ditto", 5) == 0 && *skipwhitespace(p + 5) == 0)
1959             return true;
1960     }
1961     return false;
1962 }
1963 
1964 /**********************************************
1965  * Skip white space.
1966  */
1967 private const(char)* skipwhitespace(const(char)* p)
1968 {
1969     return skipwhitespace(p.toDString).ptr;
1970 }
1971 
1972 /// Ditto
1973 private const(char)[] skipwhitespace(const(char)[] p)
1974 {
1975     foreach (idx, char c; p)
1976     {
1977         switch (c)
1978         {
1979         case ' ':
1980         case '\t':
1981         case '\n':
1982             continue;
1983         default:
1984             return p[idx .. $];
1985         }
1986     }
1987     return p[$ .. $];
1988 }
1989 
1990 /************************************************
1991  * Scan past all instances of the given characters.
1992  * Params:
1993  *  buf           = an OutBuffer containing the DDoc
1994  *  i             = the index within `buf` to start scanning from
1995  *  chars         = the characters to skip; order is unimportant
1996  * Returns: the index after skipping characters.
1997  */
1998 private size_t skipChars(ref OutBuffer buf, size_t i, string chars)
1999 {
2000     Outer:
2001     foreach (j, c; buf[][i..$])
2002     {
2003         foreach (d; chars)
2004         {
2005             if (d == c)
2006                 continue Outer;
2007         }
2008         return i + j;
2009     }
2010     return buf.length;
2011 }
2012 
2013 unittest {
2014     OutBuffer buf;
2015     string data = "test ---\r\n\r\nend";
2016     buf.write(data);
2017 
2018     assert(skipChars(buf, 0, "-") == 0);
2019     assert(skipChars(buf, 4, "-") == 4);
2020     assert(skipChars(buf, 4, " -") == 8);
2021     assert(skipChars(buf, 8, "\r\n") == 12);
2022     assert(skipChars(buf, 12, "dne") == 15);
2023 }
2024 
2025 /****************************************************
2026  * Replace all instances of `c` with `r` in the given string
2027  * Params:
2028  *  s = the string to do replacements in
2029  *  c = the character to look for
2030  *  r = the string to replace `c` with
2031  * Returns: `s` with `c` replaced with `r`
2032  */
2033 private inout(char)[] replaceChar(inout(char)[] s, char c, string r) pure
2034 {
2035     int count = 0;
2036     foreach (char sc; s)
2037         if (sc == c)
2038             ++count;
2039     if (count == 0)
2040         return s;
2041 
2042     char[] result;
2043     result.reserve(s.length - count + (r.length * count));
2044     size_t start = 0;
2045     foreach (i, char sc; s)
2046     {
2047         if (sc == c)
2048         {
2049             result ~= s[start..i];
2050             result ~= r;
2051             start = i+1;
2052         }
2053     }
2054     result ~= s[start..$];
2055     return result;
2056 }
2057 
2058 ///
2059 unittest
2060 {
2061     assert("".replaceChar(',', "$(COMMA)") == "");
2062     assert("ab".replaceChar(',', "$(COMMA)") == "ab");
2063     assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b");
2064     assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b");
2065     assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab");
2066     assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)");
2067 }
2068 
2069 /**
2070  * Return a lowercased copy of a string.
2071  * Params:
2072  *  s = the string to lowercase
2073  * Returns: the lowercase version of the string or the original if already lowercase
2074  */
2075 private string toLowercase(string s) pure
2076 {
2077     string lower;
2078     foreach (size_t i; 0..s.length)
2079     {
2080         char c = s[i];
2081 // TODO: maybe unicode lowercase, somehow
2082         if (c >= 'A' && c <= 'Z')
2083         {
2084             if (!lower.length) {
2085                 lower.reserve(s.length);
2086             }
2087             lower ~= s[lower.length..i];
2088             c += 'a' - 'A';
2089             lower ~= c;
2090         }
2091     }
2092     if (lower.length)
2093         lower ~= s[lower.length..$];
2094     else
2095         lower = s;
2096     return lower;
2097 }
2098 
2099 ///
2100 unittest
2101 {
2102     assert("".toLowercase == "");
2103     assert("abc".toLowercase == "abc");
2104     assert("ABC".toLowercase == "abc");
2105     assert("aBc".toLowercase == "abc");
2106 }
2107 
2108 /************************************************
2109  * Get the indent from one index to another, counting tab stops as four spaces wide
2110  * per the Markdown spec.
2111  * Params:
2112  *  buf   = an OutBuffer containing the DDoc
2113  *  from  = the index within `buf` to start counting from, inclusive
2114  *  to    = the index within `buf` to stop counting at, exclusive
2115  * Returns: the indent
2116  */
2117 private int getMarkdownIndent(ref OutBuffer buf, size_t from, size_t to)
2118 {
2119     const slice = buf[];
2120     if (to > slice.length)
2121         to = slice.length;
2122     int indent = 0;
2123     foreach (const c; slice[from..to])
2124         indent += (c == '\t') ? 4 - (indent % 4) : 1;
2125     return indent;
2126 }
2127 
2128 /************************************************
2129  * Scan forward to one of:
2130  *      start of identifier
2131  *      beginning of next line
2132  *      end of buf
2133  */
2134 size_t skiptoident(ref OutBuffer buf, size_t i)
2135 {
2136     const slice = buf[];
2137     while (i < slice.length)
2138     {
2139         dchar c;
2140         size_t oi = i;
2141         if (utf_decodeChar(slice, i, c))
2142         {
2143             /* Ignore UTF errors, but still consume input
2144              */
2145             break;
2146         }
2147         if (c >= 0x80)
2148         {
2149             if (!isUniAlpha(c))
2150                 continue;
2151         }
2152         else if (!(isalpha(c) || c == '_' || c == '\n'))
2153             continue;
2154         i = oi;
2155         break;
2156     }
2157     return i;
2158 }
2159 
2160 /************************************************
2161  * Scan forward past end of identifier.
2162  */
2163 private size_t skippastident(ref OutBuffer buf, size_t i)
2164 {
2165     const slice = buf[];
2166     while (i < slice.length)
2167     {
2168         dchar c;
2169         size_t oi = i;
2170         if (utf_decodeChar(slice, i, c))
2171         {
2172             /* Ignore UTF errors, but still consume input
2173              */
2174             break;
2175         }
2176         if (c >= 0x80)
2177         {
2178             if (isUniAlpha(c))
2179                 continue;
2180         }
2181         else if (isalnum(c) || c == '_')
2182             continue;
2183         i = oi;
2184         break;
2185     }
2186     return i;
2187 }
2188 
2189 /************************************************
2190  * Scan forward past end of an identifier that might
2191  * contain dots (e.g. `abc.def`)
2192  */
2193 private size_t skipPastIdentWithDots(ref OutBuffer buf, size_t i)
2194 {
2195     const slice = buf[];
2196     bool lastCharWasDot;
2197     while (i < slice.length)
2198     {
2199         dchar c;
2200         size_t oi = i;
2201         if (utf_decodeChar(slice, i, c))
2202         {
2203             /* Ignore UTF errors, but still consume input
2204              */
2205             break;
2206         }
2207         if (c == '.')
2208         {
2209             // We need to distinguish between `abc.def`, abc..def`, and `abc.`
2210             // Only `abc.def` is a valid identifier
2211 
2212             if (lastCharWasDot)
2213             {
2214                 i = oi;
2215                 break;
2216             }
2217 
2218             lastCharWasDot = true;
2219             continue;
2220         }
2221         else
2222         {
2223             if (c >= 0x80)
2224             {
2225                 if (isUniAlpha(c))
2226                 {
2227                     lastCharWasDot = false;
2228                     continue;
2229                 }
2230             }
2231             else if (isalnum(c) || c == '_')
2232             {
2233                 lastCharWasDot = false;
2234                 continue;
2235             }
2236             i = oi;
2237             break;
2238         }
2239     }
2240 
2241     // if `abc.`
2242     if (lastCharWasDot)
2243         return i - 1;
2244 
2245     return i;
2246 }
2247 
2248 /************************************************
2249  * Scan forward past URL starting at i.
2250  * We don't want to highlight parts of a URL.
2251  * Returns:
2252  *      i if not a URL
2253  *      index just past it if it is a URL
2254  */
2255 private size_t skippastURL(ref OutBuffer buf, size_t i)
2256 {
2257     const slice = buf[][i .. $];
2258     size_t j;
2259     bool sawdot = false;
2260     if (slice.length > 7 && Port.memicmp(slice.ptr, "http://", 7) == 0)
2261     {
2262         j = 7;
2263     }
2264     else if (slice.length > 8 && Port.memicmp(slice.ptr, "https://", 8) == 0)
2265     {
2266         j = 8;
2267     }
2268     else
2269         goto Lno;
2270     for (; j < slice.length; j++)
2271     {
2272         const c = slice[j];
2273         if (isalnum(c))
2274             continue;
2275         if (c == '-' || c == '_' || c == '?' || c == '=' || c == '%' ||
2276             c == '&' || c == '/' || c == '+' || c == '#' || c == '~')
2277             continue;
2278         if (c == '.')
2279         {
2280             sawdot = true;
2281             continue;
2282         }
2283         break;
2284     }
2285     if (sawdot)
2286         return i + j;
2287 Lno:
2288     return i;
2289 }
2290 
2291 /****************************************************
2292  * Remove a previously-inserted blank line macro.
2293  * Params:
2294  *  buf           = an OutBuffer containing the DDoc
2295  *  iAt           = the index within `buf` of the start of the `$(DDOC_BLANKLINE)`
2296  *                  macro. Upon function return its value is set to `0`.
2297  *  i             = an index within `buf`. If `i` is after `iAt` then it gets
2298  *                  reduced by the length of the removed macro.
2299  */
2300 private void removeBlankLineMacro(ref OutBuffer buf, ref size_t iAt, ref size_t i)
2301 {
2302     if (!iAt)
2303         return;
2304 
2305     enum macroLength = "$(DDOC_BLANKLINE)".length;
2306     buf.remove(iAt, macroLength);
2307     if (i > iAt)
2308         i -= macroLength;
2309     iAt = 0;
2310 }
2311 
2312 /****************************************************
2313  * Attempt to detect and replace a Markdown thematic break (HR). These are three
2314  * or more of the same delimiter, optionally with spaces or tabs between any of
2315  * them, e.g. `\n- - -\n` becomes `\n$(HR)\n`
2316  * Params:
2317  *  buf         = an OutBuffer containing the DDoc
2318  *  i           = the index within `buf` of the first character of a potential
2319  *                thematic break. If the replacement is made `i` changes to
2320  *                point to the closing parenthesis of the `$(HR)` macro.
2321  *  iLineStart  = the index within `buf` that the thematic break's line starts at
2322  *  loc         = the current location within the file
2323  * Returns: whether a thematic break was replaced
2324  */
2325 private bool replaceMarkdownThematicBreak(ref OutBuffer buf, ref size_t i, size_t iLineStart, const ref Loc loc)
2326 {
2327     if (!global.params.markdown)
2328         return false;
2329 
2330     const slice = buf[];
2331     const c = buf[i];
2332     size_t j = i + 1;
2333     int repeat = 1;
2334     for (; j < slice.length; j++)
2335     {
2336         if (buf[j] == c)
2337             ++repeat;
2338         else if (buf[j] != ' ' && buf[j] != '\t')
2339             break;
2340     }
2341     if (repeat >= 3)
2342     {
2343         if (j >= buf.length || buf[j] == '\n' || buf[j] == '\r')
2344         {
2345             if (global.params.vmarkdown)
2346             {
2347                 const s = buf[][i..j];
2348                 message(loc, "Ddoc: converted '%.*s' to a thematic break", cast(int)s.length, s.ptr);
2349             }
2350 
2351             buf.remove(iLineStart, j - iLineStart);
2352             i = buf.insert(iLineStart, "$(HR)") - 1;
2353             return true;
2354         }
2355     }
2356     return false;
2357 }
2358 
2359 /****************************************************
2360  * Detect the level of an ATX-style heading, e.g. `## This is a heading` would
2361  * have a level of `2`.
2362  * Params:
2363  *  buf   = an OutBuffer containing the DDoc
2364  *  i     = the index within `buf` of the first `#` character
2365  * Returns:
2366  *          the detected heading level from 1 to 6, or
2367  *          0 if not at an ATX heading
2368  */
2369 private int detectAtxHeadingLevel(ref OutBuffer buf, const size_t i)
2370 {
2371     if (!global.params.markdown)
2372         return 0;
2373 
2374     const iHeadingStart = i;
2375     const iAfterHashes = skipChars(buf, i, "#");
2376     const headingLevel = cast(int) (iAfterHashes - iHeadingStart);
2377     if (headingLevel > 6)
2378         return 0;
2379 
2380     const iTextStart = skipChars(buf, iAfterHashes, " \t");
2381     const emptyHeading = buf[iTextStart] == '\r' || buf[iTextStart] == '\n';
2382 
2383     // require whitespace
2384     if (!emptyHeading && iTextStart == iAfterHashes)
2385         return 0;
2386 
2387     return headingLevel;
2388 }
2389 
2390 /****************************************************
2391  * Remove any trailing `##` suffix from an ATX-style heading.
2392  * Params:
2393  *  buf   = an OutBuffer containing the DDoc
2394  *  i     = the index within `buf` to start looking for a suffix at
2395  */
2396 private void removeAnyAtxHeadingSuffix(ref OutBuffer buf, size_t i)
2397 {
2398     size_t j = i;
2399     size_t iSuffixStart = 0;
2400     size_t iWhitespaceStart = j;
2401     const slice = buf[];
2402     for (; j < slice.length; j++)
2403     {
2404         switch (slice[j])
2405         {
2406         case '#':
2407             if (iWhitespaceStart && !iSuffixStart)
2408                 iSuffixStart = j;
2409             continue;
2410         case ' ':
2411         case '\t':
2412             if (!iWhitespaceStart)
2413                 iWhitespaceStart = j;
2414             continue;
2415         case '\r':
2416         case '\n':
2417             break;
2418         default:
2419             iSuffixStart = 0;
2420             iWhitespaceStart = 0;
2421             continue;
2422         }
2423         break;
2424     }
2425     if (iSuffixStart)
2426         buf.remove(iWhitespaceStart, j - iWhitespaceStart);
2427 }
2428 
2429 /****************************************************
2430  * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`).
2431  * Params:
2432  *  buf           = an OutBuffer containing the DDoc
2433  *  iStart        = the index within `buf` that the Markdown heading starts at
2434  *  iEnd          = the index within `buf` of the character after the last
2435  *                  heading character. Is incremented by the length of the
2436  *                  inserted heading macro when this function ends.
2437  *  loc           = the location of the Ddoc within the file
2438  *  headingLevel  = the level (1-6) of heading to end. Is set to `0` when this
2439  *                  function ends.
2440  */
2441 private void endMarkdownHeading(ref OutBuffer buf, size_t iStart, ref size_t iEnd, const ref Loc loc, ref int headingLevel)
2442 {
2443     if (!global.params.markdown)
2444         return;
2445     if (global.params.vmarkdown)
2446     {
2447         const s = buf[][iStart..iEnd];
2448         message(loc, "Ddoc: added heading '%.*s'", cast(int)s.length, s.ptr);
2449     }
2450 
2451     char[5] heading = "$(H0 ";
2452     heading[3] = cast(char) ('0' + headingLevel);
2453     buf.insert(iStart, heading);
2454     iEnd += 5;
2455     size_t iBeforeNewline = iEnd;
2456     while (buf[iBeforeNewline-1] == '\r' || buf[iBeforeNewline-1] == '\n')
2457         --iBeforeNewline;
2458     buf.insert(iBeforeNewline, ")");
2459     headingLevel = 0;
2460 }
2461 
2462 /****************************************************
2463  * End all nested Markdown quotes, if inside any.
2464  * Params:
2465  *  buf         = an OutBuffer containing the DDoc
2466  *  i           = the index within `buf` of the character after the quote text.
2467  *  quoteLevel  = the current quote level. Is set to `0` when this function ends.
2468  * Returns: the amount that `i` was moved
2469  */
2470 private size_t endAllMarkdownQuotes(ref OutBuffer buf, size_t i, ref int quoteLevel)
2471 {
2472     const length = quoteLevel;
2473     for (; quoteLevel > 0; --quoteLevel)
2474         i = buf.insert(i, ")");
2475     return length;
2476 }
2477 
2478 /****************************************************
2479  * Convenience function to end all Markdown lists and quotes, if inside any, and
2480  * set `quoteMacroLevel` to `0`.
2481  * Params:
2482  *  buf         = an OutBuffer containing the DDoc
2483  *  i           = the index within `buf` of the character after the list and/or
2484  *                quote text. Is adjusted when this function ends if any lists
2485  *                and/or quotes were ended.
2486  *  nestedLists = a set of nested lists. Upon return it will be empty.
2487  *  quoteLevel  = the current quote level. Is set to `0` when this function ends.
2488  *  quoteMacroLevel   = the macro level that the quote was started at. Is set to
2489  *                      `0` when this function ends.
2490  * Returns: the amount that `i` was moved
2491  */
2492 private size_t endAllListsAndQuotes(ref OutBuffer buf, ref size_t i, ref MarkdownList[] nestedLists, ref int quoteLevel, out int quoteMacroLevel)
2493 {
2494     quoteMacroLevel = 0;
2495     const i0 = i;
2496     i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
2497     i += endAllMarkdownQuotes(buf, i, quoteLevel);
2498     return i - i0;
2499 }
2500 
2501 /****************************************************
2502  * Replace Markdown emphasis with the appropriate macro,
2503  * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`.
2504  * Params:
2505  *  buf               = an OutBuffer containing the DDoc
2506  *  loc               = the current location within the file
2507  *  inlineDelimiters  = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`.
2508  *  downToLevel       = the length within `inlineDelimiters`` to reduce emphasis to
2509  * Returns: the number of characters added to the buffer by the replacements
2510  */
2511 private size_t replaceMarkdownEmphasis(ref OutBuffer buf, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int downToLevel = 0)
2512 {
2513     if (!global.params.markdown)
2514         return 0;
2515 
2516     size_t replaceEmphasisPair(ref MarkdownDelimiter start, ref MarkdownDelimiter end)
2517     {
2518         immutable count = start.count == 1 || end.count == 1 ? 1 : 2;
2519 
2520         size_t iStart = start.iStart;
2521         size_t iEnd = end.iStart;
2522         end.count -= count;
2523         start.count -= count;
2524         iStart += start.count;
2525 
2526         if (!start.count)
2527             start.type = 0;
2528         if (!end.count)
2529             end.type = 0;
2530 
2531         if (global.params.vmarkdown)
2532         {
2533             const s = buf[][iStart + count..iEnd];
2534             message(loc, "Ddoc: emphasized text '%.*s'", cast(int)s.length, s.ptr);
2535         }
2536 
2537         buf.remove(iStart, count);
2538         iEnd -= count;
2539         buf.remove(iEnd, count);
2540 
2541         string macroName = count >= 2 ? "$(STRONG " : "$(EM ";
2542         buf.insert(iEnd, ")");
2543         buf.insert(iStart, macroName);
2544 
2545         const delta = 1 + macroName.length - (count + count);
2546         end.iStart += count;
2547         return delta;
2548     }
2549 
2550     size_t delta = 0;
2551     int start = (cast(int) inlineDelimiters.length) - 1;
2552     while (start >= downToLevel)
2553     {
2554         // find start emphasis
2555         while (start >= downToLevel &&
2556             (inlineDelimiters[start].type != '*' || !inlineDelimiters[start].leftFlanking))
2557             --start;
2558         if (start < downToLevel)
2559             break;
2560 
2561         // find the nearest end emphasis
2562         int end = start + 1;
2563         while (end < inlineDelimiters.length &&
2564             (inlineDelimiters[end].type != inlineDelimiters[start].type ||
2565                 inlineDelimiters[end].macroLevel != inlineDelimiters[start].macroLevel ||
2566                 !inlineDelimiters[end].rightFlanking))
2567             ++end;
2568         if (end == inlineDelimiters.length)
2569         {
2570             // the start emphasis has no matching end; if it isn't an end itself then kill it
2571             if (!inlineDelimiters[start].rightFlanking)
2572                 inlineDelimiters[start].type = 0;
2573             --start;
2574             continue;
2575         }
2576 
2577         // multiple-of-3 rule
2578         if (((inlineDelimiters[start].leftFlanking && inlineDelimiters[start].rightFlanking) ||
2579                 (inlineDelimiters[end].leftFlanking && inlineDelimiters[end].rightFlanking)) &&
2580             (inlineDelimiters[start].count + inlineDelimiters[end].count) % 3 == 0)
2581         {
2582             --start;
2583             continue;
2584         }
2585 
2586         immutable delta0 = replaceEmphasisPair(inlineDelimiters[start], inlineDelimiters[end]);
2587 
2588         for (; end < inlineDelimiters.length; ++end)
2589             inlineDelimiters[end].iStart += delta0;
2590         delta += delta0;
2591     }
2592 
2593     inlineDelimiters.length = downToLevel;
2594     return delta;
2595 }
2596 
2597 /****************************************************
2598  */
2599 private bool isIdentifier(Dsymbols* a, const(char)* p, size_t len)
2600 {
2601     foreach (member; *a)
2602     {
2603         if (auto imp = member.isImport())
2604         {
2605             // For example: `public import str = core.stdc.string;`
2606             // This checks if `p` is equal to `str`
2607             if (imp.aliasId)
2608             {
2609                 if (p[0 .. len] == imp.aliasId.toString())
2610                     return true;
2611             }
2612             else
2613             {
2614                 // The general case:  `public import core.stdc.string;`
2615 
2616                 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
2617                 string fullyQualifiedImport;
2618                 if (imp.packages && imp.packages.dim)
2619                 {
2620                     foreach (const pid; *imp.packages)
2621                     {
2622                         fullyQualifiedImport ~= pid.toString() ~ ".";
2623                     }
2624                 }
2625                 fullyQualifiedImport ~= imp.id.toString();
2626 
2627                 // Check if `p` == `core.stdc.string`
2628                 if (p[0 .. len] == fullyQualifiedImport)
2629                     return true;
2630             }
2631         }
2632         else if (member.ident)
2633         {
2634             if (p[0 .. len] == member.ident.toString())
2635                 return true;
2636         }
2637 
2638     }
2639     return false;
2640 }
2641 
2642 /****************************************************
2643  */
2644 private bool isKeyword(const(char)* p, size_t len)
2645 {
2646     immutable string[3] table = ["true", "false", "null"];
2647     foreach (s; table)
2648     {
2649         if (p[0 .. len] == s)
2650             return true;
2651     }
2652     return false;
2653 }
2654 
2655 /****************************************************
2656  */
2657 private TypeFunction isTypeFunction(Dsymbol s)
2658 {
2659     FuncDeclaration f = s.isFuncDeclaration();
2660     /* f.type may be NULL for template members.
2661      */
2662     if (f && f.type)
2663     {
2664         Type t = f.originalType ? f.originalType : f.type;
2665         if (t.ty == Tfunction)
2666             return cast(TypeFunction)t;
2667     }
2668     return null;
2669 }
2670 
2671 /****************************************************
2672  */
2673 private Parameter isFunctionParameter(Dsymbol s, const(char)* p, size_t len)
2674 {
2675     TypeFunction tf = isTypeFunction(s);
2676     if (tf && tf.parameterList.parameters)
2677     {
2678         foreach (fparam; *tf.parameterList.parameters)
2679         {
2680             if (fparam.ident && p[0 .. len] == fparam.ident.toString())
2681             {
2682                 return fparam;
2683             }
2684         }
2685     }
2686     return null;
2687 }
2688 
2689 /****************************************************
2690  */
2691 private Parameter isFunctionParameter(Dsymbols* a, const(char)* p, size_t len)
2692 {
2693     for (size_t i = 0; i < a.dim; i++)
2694     {
2695         Parameter fparam = isFunctionParameter((*a)[i], p, len);
2696         if (fparam)
2697         {
2698             return fparam;
2699         }
2700     }
2701     return null;
2702 }
2703 
2704 /****************************************************
2705  */
2706 private Parameter isEponymousFunctionParameter(Dsymbols *a, const(char) *p, size_t len)
2707 {
2708     for (size_t i = 0; i < a.dim; i++)
2709     {
2710         TemplateDeclaration td = (*a)[i].isTemplateDeclaration();
2711         if (td && td.onemember)
2712         {
2713             /* Case 1: we refer to a template declaration inside the template
2714 
2715                /// ...ddoc...
2716                template case1(T) {
2717                  void case1(R)() {}
2718                }
2719              */
2720             td = td.onemember.isTemplateDeclaration();
2721         }
2722         if (!td)
2723         {
2724             /* Case 2: we're an alias to a template declaration
2725 
2726                /// ...ddoc...
2727                alias case2 = case1!int;
2728              */
2729             AliasDeclaration ad = (*a)[i].isAliasDeclaration();
2730             if (ad && ad.aliassym)
2731             {
2732                 td = ad.aliassym.isTemplateDeclaration();
2733             }
2734         }
2735         while (td)
2736         {
2737             Dsymbol sym = getEponymousMember(td);
2738             if (sym)
2739             {
2740                 Parameter fparam = isFunctionParameter(sym, p, len);
2741                 if (fparam)
2742                 {
2743                     return fparam;
2744                 }
2745             }
2746             td = td.overnext;
2747         }
2748     }
2749     return null;
2750 }
2751 
2752 /****************************************************
2753  */
2754 private TemplateParameter isTemplateParameter(Dsymbols* a, const(char)* p, size_t len)
2755 {
2756     for (size_t i = 0; i < a.dim; i++)
2757     {
2758         TemplateDeclaration td = (*a)[i].isTemplateDeclaration();
2759         // Check for the parent, if the current symbol is not a template declaration.
2760         if (!td)
2761             td = getEponymousParent((*a)[i]);
2762         if (td && td.origParameters)
2763         {
2764             foreach (tp; *td.origParameters)
2765             {
2766                 if (tp.ident && p[0 .. len] == tp.ident.toString())
2767                 {
2768                     return tp;
2769                 }
2770             }
2771         }
2772     }
2773     return null;
2774 }
2775 
2776 /****************************************************
2777  * Return true if str is a reserved symbol name
2778  * that starts with a double underscore.
2779  */
2780 private bool isReservedName(const(char)[] str)
2781 {
2782     immutable string[] table =
2783     [
2784         "__ctor",
2785         "__dtor",
2786         "__postblit",
2787         "__invariant",
2788         "__unitTest",
2789         "__require",
2790         "__ensure",
2791         "__dollar",
2792         "__ctfe",
2793         "__withSym",
2794         "__result",
2795         "__returnLabel",
2796         "__vptr",
2797         "__monitor",
2798         "__gate",
2799         "__xopEquals",
2800         "__xopCmp",
2801         "__LINE__",
2802         "__FILE__",
2803         "__MODULE__",
2804         "__FUNCTION__",
2805         "__PRETTY_FUNCTION__",
2806         "__DATE__",
2807         "__TIME__",
2808         "__TIMESTAMP__",
2809         "__VENDOR__",
2810         "__VERSION__",
2811         "__EOF__",
2812         "__CXXLIB__",
2813         "__LOCAL_SIZE",
2814         "__entrypoint",
2815     ];
2816     foreach (s; table)
2817     {
2818         if (str == s)
2819             return true;
2820     }
2821     return false;
2822 }
2823 
2824 /****************************************************
2825  * A delimiter for Markdown inline content like emphasis and links.
2826  */
2827 private struct MarkdownDelimiter
2828 {
2829     size_t iStart;  /// the index where this delimiter starts
2830     int count;      /// the length of this delimeter's start sequence
2831     int macroLevel; /// the count of nested DDoc macros when the delimiter is started
2832     bool leftFlanking;  /// whether the delimiter is left-flanking, as defined by the CommonMark spec
2833     bool rightFlanking; /// whether the delimiter is right-flanking, as defined by the CommonMark spec
2834     bool atParagraphStart;  /// whether the delimiter is at the start of a paragraph
2835     char type;      /// the type of delimiter, defined by its starting character
2836 
2837     /// whether this describes a valid delimiter
2838     @property bool isValid() const { return count != 0; }
2839 
2840     /// flag this delimiter as invalid
2841     void invalidate() { count = 0; }
2842 }
2843 
2844 /****************************************************
2845  * Info about a Markdown list.
2846  */
2847 private struct MarkdownList
2848 {
2849     string orderedStart;    /// an optional start number--if present then the list starts at this number
2850     size_t iStart;          /// the index where the list item starts
2851     size_t iContentStart;   /// the index where the content starts after the list delimiter
2852     int delimiterIndent;    /// the level of indent the list delimiter starts at
2853     int contentIndent;      /// the level of indent the content starts at
2854     int macroLevel;         /// the count of nested DDoc macros when the list is started
2855     char type;              /// the type of list, defined by its starting character
2856 
2857     /// whether this describes a valid list
2858     @property bool isValid() const { return type != type.init; }
2859 
2860     /****************************************************
2861      * Try to parse a list item, returning whether successful.
2862      * Params:
2863      *  buf           = an OutBuffer containing the DDoc
2864      *  iLineStart    = the index within `buf` of the first character of the line
2865      *  i             = the index within `buf` of the potential list item
2866      * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded.
2867      */
2868     static MarkdownList parseItem(ref OutBuffer buf, size_t iLineStart, size_t i)
2869     {
2870         if (!global.params.markdown)
2871             return MarkdownList();
2872 
2873         if (buf[i] == '+' || buf[i] == '-' || buf[i] == '*')
2874             return parseUnorderedListItem(buf, iLineStart, i);
2875         else
2876             return parseOrderedListItem(buf, iLineStart, i);
2877     }
2878 
2879     /****************************************************
2880      * Return whether the context is at a list item of the same type as this list.
2881      * Params:
2882      *  buf           = an OutBuffer containing the DDoc
2883      *  iLineStart    = the index within `buf` of the first character of the line
2884      *  i             = the index within `buf` of the list item
2885      * Returns: whether `i` is at a list item of the same type as this list
2886      */
2887     private bool isAtItemInThisList(ref OutBuffer buf, size_t iLineStart, size_t i)
2888     {
2889         MarkdownList item = (type == '.' || type == ')') ?
2890             parseOrderedListItem(buf, iLineStart, i) :
2891             parseUnorderedListItem(buf, iLineStart, i);
2892         if (item.type == type)
2893             return item.delimiterIndent < contentIndent && item.contentIndent > delimiterIndent;
2894         return false;
2895     }
2896 
2897     /****************************************************
2898      * Start a Markdown list item by creating/deleting nested lists and starting the item.
2899      * Params:
2900      *  buf           = an OutBuffer containing the DDoc
2901      *  iLineStart    = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`.
2902      *  i             = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro.
2903      *  iPrecedingBlankLine = the index within `buf` of the preceeding blank line. If non-zero and a new list was started, the preceeding blank line is removed and this value is set to `0`.
2904      *  nestedLists   = a set of nested lists. If this function succeeds it may contain a new nested list.
2905      *  loc           = the location of the Ddoc within the file
2906      * Returns: `true` if a list was created
2907      */
2908     bool startItem(ref OutBuffer buf, ref size_t iLineStart, ref size_t i, ref size_t iPrecedingBlankLine, ref MarkdownList[] nestedLists, const ref Loc loc)
2909     {
2910         buf.remove(iStart, iContentStart - iStart);
2911 
2912         if (!nestedLists.length ||
2913             delimiterIndent >= nestedLists[$-1].contentIndent ||
2914             buf[iLineStart - 4..iLineStart] == "$(LI")
2915         {
2916             // start a list macro
2917             nestedLists ~= this;
2918             if (type == '.')
2919             {
2920                 if (orderedStart.length)
2921                 {
2922                     iStart = buf.insert(iStart, "$(OL_START ");
2923                     iStart = buf.insert(iStart, orderedStart);
2924                     iStart = buf.insert(iStart, ",\n");
2925                 }
2926                 else
2927                     iStart = buf.insert(iStart, "$(OL\n");
2928             }
2929             else
2930                 iStart = buf.insert(iStart, "$(UL\n");
2931 
2932             removeBlankLineMacro(buf, iPrecedingBlankLine, iStart);
2933         }
2934         else if (nestedLists.length)
2935         {
2936             nestedLists[$-1].delimiterIndent = delimiterIndent;
2937             nestedLists[$-1].contentIndent = contentIndent;
2938         }
2939 
2940         iStart = buf.insert(iStart, "$(LI\n");
2941         i = iStart - 1;
2942         iLineStart = i;
2943 
2944         if (global.params.vmarkdown)
2945         {
2946             size_t iEnd = iStart;
2947             while (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n')
2948                 ++iEnd;
2949             const s = buf[][iStart..iEnd];
2950             message(loc, "Ddoc: starting list item '%.*s'", cast(int)s.length, s.ptr);
2951         }
2952 
2953         return true;
2954     }
2955 
2956     /****************************************************
2957      * End all nested Markdown lists.
2958      * Params:
2959      *  buf           = an OutBuffer containing the DDoc
2960      *  i             = the index within `buf` to end lists at.
2961      *  nestedLists   = a set of nested lists. Upon return it will be empty.
2962      * Returns: the amount that `i` changed
2963      */
2964     static size_t endAllNestedLists(ref OutBuffer buf, size_t i, ref MarkdownList[] nestedLists)
2965     {
2966         const iStart = i;
2967         for (; nestedLists.length; --nestedLists.length)
2968             i = buf.insert(i, ")\n)");
2969         return i - iStart;
2970     }
2971 
2972     /****************************************************
2973      * Look for a sibling list item or the end of nested list(s).
2974      * Params:
2975      *  buf               = an OutBuffer containing the DDoc
2976      *  i                 = the index within `buf` to end lists at. If there was a sibling or ending lists `i` will be adjusted to fit the macro endings.
2977      *  iParagraphStart   = the index within `buf` to start the next paragraph at at. May be adjusted upon return.
2978      *  nestedLists       = a set of nested lists. Some nested lists may have been removed from it upon return.
2979      */
2980     static void handleSiblingOrEndingList(ref OutBuffer buf, ref size_t i, ref size_t iParagraphStart, ref MarkdownList[] nestedLists)
2981     {
2982         size_t iAfterSpaces = skipChars(buf, i + 1, " \t");
2983 
2984         if (nestedLists[$-1].isAtItemInThisList(buf, i + 1, iAfterSpaces))
2985         {
2986             // end a sibling list item
2987             i = buf.insert(i, ")");
2988             iParagraphStart = skipChars(buf, i, " \t\r\n");
2989         }
2990         else if (iAfterSpaces >= buf.length || (buf[iAfterSpaces] != '\r' && buf[iAfterSpaces] != '\n'))
2991         {
2992             // end nested lists that are indented more than this content
2993             const indent = getMarkdownIndent(buf, i + 1, iAfterSpaces);
2994             while (nestedLists.length && nestedLists[$-1].contentIndent > indent)
2995             {
2996                 i = buf.insert(i, ")\n)");
2997                 --nestedLists.length;
2998                 iParagraphStart = skipChars(buf, i, " \t\r\n");
2999 
3000                 if (nestedLists.length && nestedLists[$-1].isAtItemInThisList(buf, i + 1, iParagraphStart))
3001                 {
3002                     i = buf.insert(i, ")");
3003                     ++iParagraphStart;
3004                     break;
3005                 }
3006             }
3007         }
3008     }
3009 
3010     /****************************************************
3011      * Parse an unordered list item at the current position
3012      * Params:
3013      *  buf           = an OutBuffer containing the DDoc
3014      *  iLineStart    = the index within `buf` of the first character of the line
3015      *  i             = the index within `buf` of the list item
3016      * Returns: the parsed list item, or a list item with type `.init` if no list item is available
3017      */
3018     private static MarkdownList parseUnorderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i)
3019     {
3020         if (i+1 < buf.length &&
3021                 (buf[i] == '-' ||
3022                 buf[i] == '*' ||
3023                 buf[i] == '+') &&
3024             (buf[i+1] == ' ' ||
3025                 buf[i+1] == '\t' ||
3026                 buf[i+1] == '\r' ||
3027                 buf[i+1] == '\n'))
3028         {
3029             const iContentStart = skipChars(buf, i + 1, " \t");
3030             const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
3031             const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
3032             auto list = MarkdownList(null, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[i]);
3033             return list;
3034         }
3035         return MarkdownList();
3036     }
3037 
3038     /****************************************************
3039      * Parse an ordered list item at the current position
3040      * Params:
3041      *  buf           = an OutBuffer containing the DDoc
3042      *  iLineStart    = the index within `buf` of the first character of the line
3043      *  i             = the index within `buf` of the list item
3044      * Returns: the parsed list item, or a list item with type `.init` if no list item is available
3045      */
3046     private static MarkdownList parseOrderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i)
3047     {
3048         size_t iAfterNumbers = skipChars(buf, i, "0123456789");
3049         if (iAfterNumbers - i > 0 &&
3050             iAfterNumbers - i <= 9 &&
3051             iAfterNumbers + 1 < buf.length &&
3052             buf[iAfterNumbers] == '.' &&
3053             (buf[iAfterNumbers+1] == ' ' ||
3054                 buf[iAfterNumbers+1] == '\t' ||
3055                 buf[iAfterNumbers+1] == '\r' ||
3056                 buf[iAfterNumbers+1] == '\n'))
3057         {
3058             const iContentStart = skipChars(buf, iAfterNumbers + 1, " \t");
3059             const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
3060             const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
3061             size_t iNumberStart = skipChars(buf, i, "0");
3062             if (iNumberStart == iAfterNumbers)
3063                 --iNumberStart;
3064             auto orderedStart = buf[][iNumberStart .. iAfterNumbers];
3065             if (orderedStart == "1")
3066                 orderedStart = null;
3067             return MarkdownList(orderedStart.idup, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[iAfterNumbers]);
3068         }
3069         return MarkdownList();
3070     }
3071 }
3072 
3073 /****************************************************
3074  * A Markdown link.
3075  */
3076 private struct MarkdownLink
3077 {
3078     string href;    /// the link destination
3079     string title;   /// an optional title for the link
3080     string label;   /// an optional label for the link
3081     Dsymbol symbol; /// an optional symbol to link to
3082 
3083     /****************************************************
3084      * Replace a Markdown link or link definition in the form of:
3085      * - Inline link: `[foo](url/ 'optional title')`
3086      * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]`
3087      * - Link reference definition: `[bar]: url/ 'optional title'`
3088      * Params:
3089      *  buf               = an OutBuffer containing the DDoc
3090      *  i                 = the index within `buf` that points to the `]` character of the potential link.
3091      *                      If this function succeeds it will be adjusted to fit the inserted link macro.
3092      *  loc               = the current location within the file
3093      *  inlineDelimiters  = previously parsed Markdown delimiters, including emphasis and link/image starts
3094      *  delimiterIndex    = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3095      *  linkReferences    = previously parsed link references. When this function returns it may contain
3096      *                      additional previously unparsed references.
3097      * Returns: whether a reference link was found and replaced at `i`
3098      */
3099     static bool replaceLink(ref OutBuffer buf, ref size_t i, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences)
3100     {
3101         const delimiter = inlineDelimiters[delimiterIndex];
3102         MarkdownLink link;
3103 
3104         size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
3105         if (iEnd > i)
3106         {
3107             i = delimiter.iStart;
3108             link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
3109             inlineDelimiters.length = delimiterIndex;
3110             return true;
3111         }
3112 
3113         iEnd = link.parseInlineLink(buf, i);
3114         if (iEnd == i)
3115         {
3116             iEnd = link.parseReferenceLink(buf, i, delimiter);
3117             if (iEnd > i)
3118             {
3119                 const label = link.label;
3120                 link = linkReferences.lookupReference(label, buf, i, loc);
3121                 // check rightFlanking to avoid replacing things like int[string]
3122                 if (!link.href.length && !delimiter.rightFlanking)
3123                     link = linkReferences.lookupSymbol(label);
3124                 if (!link.href.length)
3125                     return false;
3126             }
3127         }
3128 
3129         if (iEnd == i)
3130             return false;
3131 
3132         immutable delta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, delimiterIndex);
3133         iEnd += delta;
3134         i += delta;
3135 
3136         if (global.params.vmarkdown)
3137         {
3138             const s = buf[][delimiter.iStart..iEnd];
3139             message(loc, "Ddoc: linking '%.*s' to '%.*s'", cast(int)s.length, s.ptr, cast(int)link.href.length, link.href.ptr);
3140         }
3141 
3142         link.replaceLink(buf, i, iEnd, delimiter);
3143         return true;
3144     }
3145 
3146     /****************************************************
3147      * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'`
3148      * Params:
3149      *  buf               = an OutBuffer containing the DDoc
3150      *  i                 = the index within `buf` that points to the `]` character of the potential link.
3151      *                      If this function succeeds it will be adjusted to fit the inserted link macro.
3152      *  inlineDelimiters  = previously parsed Markdown delimiters, including emphasis and link/image starts
3153      *  delimiterIndex    = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3154      *  linkReferences    = previously parsed link references. When this function returns it may contain
3155      *                      additional previously unparsed references.
3156      *  loc               = the current location in the file
3157      * Returns: whether a reference link was found and replaced at `i`
3158      */
3159     static bool replaceReferenceDefinition(ref OutBuffer buf, ref size_t i, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
3160     {
3161         const delimiter = inlineDelimiters[delimiterIndex];
3162         MarkdownLink link;
3163         size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
3164         if (iEnd == i)
3165             return false;
3166 
3167         i = delimiter.iStart;
3168         link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
3169         inlineDelimiters.length = delimiterIndex;
3170         return true;
3171     }
3172 
3173     /****************************************************
3174      * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')`
3175      * Params:
3176      *  buf   = an OutBuffer containing the DDoc
3177      *  i     = the index within `buf` that points to the `]` character of the inline link.
3178      * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3179      */
3180     private size_t parseInlineLink(ref OutBuffer buf, size_t i)
3181     {
3182         size_t iEnd = i + 1;
3183         if (iEnd >= buf.length || buf[iEnd] != '(')
3184             return i;
3185         ++iEnd;
3186 
3187         if (!parseHref(buf, iEnd))
3188             return i;
3189 
3190         iEnd = skipChars(buf, iEnd, " \t\r\n");
3191         if (buf[iEnd] != ')')
3192         {
3193             if (parseTitle(buf, iEnd))
3194                 iEnd = skipChars(buf, iEnd, " \t\r\n");
3195         }
3196 
3197         if (buf[iEnd] != ')')
3198             return i;
3199 
3200         return iEnd + 1;
3201     }
3202 
3203     /****************************************************
3204      * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]`
3205      * Params:
3206      *  buf       = an OutBuffer containing the DDoc
3207      *  i         = the index within `buf` that points to the `]` character of the inline link.
3208      *  delimiter = the delimiter that starts this link
3209      * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3210      */
3211     private size_t parseReferenceLink(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter)
3212     {
3213         size_t iStart = i + 1;
3214         size_t iEnd = iStart;
3215         if (iEnd >= buf.length || buf[iEnd] != '[' || (iEnd+1 < buf.length && buf[iEnd+1] == ']'))
3216         {
3217             // collapsed reference [foo][] or shortcut reference [foo]
3218             iStart = delimiter.iStart + delimiter.count - 1;
3219             if (buf[iEnd] == '[')
3220                 iEnd += 2;
3221         }
3222 
3223         parseLabel(buf, iStart);
3224         if (!label.length)
3225             return i;
3226 
3227         if (iEnd < iStart)
3228             iEnd = iStart;
3229         return iEnd;
3230     }
3231 
3232     /****************************************************
3233      * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'`
3234      * Params:
3235      *  buf               = an OutBuffer containing the DDoc
3236      *  i                 = the index within `buf` that points to the `]` character of the inline link.
3237      *  delimiter = the delimiter that starts this link
3238      * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3239      */
3240     private size_t parseReferenceDefinition(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter)
3241     {
3242         if (!delimiter.atParagraphStart || delimiter.type != '[' ||
3243             i+1 >= buf.length || buf[i+1] != ':')
3244             return i;
3245 
3246         size_t iEnd = delimiter.iStart;
3247         parseLabel(buf, iEnd);
3248         if (label.length == 0 || iEnd != i + 1)
3249             return i;
3250 
3251         ++iEnd;
3252         iEnd = skipChars(buf, iEnd, " \t");
3253         skipOneNewline(buf, iEnd);
3254 
3255         if (!parseHref(buf, iEnd) || href.length == 0)
3256             return i;
3257 
3258         iEnd = skipChars(buf, iEnd, " \t");
3259         const requireNewline = !skipOneNewline(buf, iEnd);
3260         const iBeforeTitle = iEnd;
3261 
3262         if (parseTitle(buf, iEnd))
3263         {
3264             iEnd = skipChars(buf, iEnd, " \t");
3265             if (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n')
3266             {
3267                 // the title must end with a newline
3268                 title.length = 0;
3269                 iEnd = iBeforeTitle;
3270             }
3271         }
3272 
3273         iEnd = skipChars(buf, iEnd, " \t");
3274         if (requireNewline && iEnd < buf.length-1 && buf[iEnd] != '\r' && buf[iEnd] != '\n')
3275             return i;
3276 
3277         return iEnd;
3278     }
3279 
3280     /****************************************************
3281      * Parse and normalize a Markdown reference label
3282      * Params:
3283      *  buf   = an OutBuffer containing the DDoc
3284      *  i     = the index within `buf` that points to the `[` character at the start of the label.
3285      *          If this function returns a non-empty label then `i` will point just after the ']' at the end of the label.
3286      * Returns: the parsed and normalized label, possibly empty
3287      */
3288     private bool parseLabel(ref OutBuffer buf, ref size_t i)
3289     {
3290         if (buf[i] != '[')
3291             return false;
3292 
3293         const slice = buf[];
3294         size_t j = i + 1;
3295 
3296         // Some labels have already been en-symboled; handle that
3297         const inSymbol = j+15 < slice.length && slice[j..j+15] == "$(DDOC_PSYMBOL ";
3298         if (inSymbol)
3299             j += 15;
3300 
3301         for (; j < slice.length; ++j)
3302         {
3303             const c = slice[j];
3304             switch (c)
3305             {
3306             case ' ':
3307             case '\t':
3308             case '\r':
3309             case '\n':
3310                 if (label.length && label[$-1] != ' ')
3311                     label ~= ' ';
3312                 break;
3313             case ')':
3314                 if (inSymbol && j+1 < slice.length && slice[j+1] == ']')
3315                 {
3316                     ++j;
3317                     goto case ']';
3318                 }
3319                 goto default;
3320             case '[':
3321                 if (slice[j-1] != '\\')
3322                 {
3323                     label.length = 0;
3324                     return false;
3325                 }
3326                 break;
3327             case ']':
3328                 if (label.length && label[$-1] == ' ')
3329                     --label.length;
3330                 if (label.length)
3331                 {
3332                     i = j + 1;
3333                     return true;
3334                 }
3335                 return false;
3336             default:
3337                 label ~= c;
3338                 break;
3339             }
3340         }
3341         label.length = 0;
3342         return false;
3343     }
3344 
3345     /****************************************************
3346      * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets
3347      * Params:
3348      *  buf   = an OutBuffer containing the DDoc
3349      *  i     = the index within `buf` that points to the first character of the URL.
3350      *          If this function succeeds `i` will point just after the the end of the URL.
3351      * Returns: whether a URL was found and parsed
3352      */
3353     private bool parseHref(ref OutBuffer buf, ref size_t i)
3354     {
3355         size_t j = skipChars(buf, i, " \t");
3356 
3357         size_t iHrefStart = j;
3358         size_t parenDepth = 1;
3359         bool inPointy = false;
3360         const slice = buf[];
3361         for (; j < slice.length; j++)
3362         {
3363             switch (slice[j])
3364             {
3365             case '<':
3366                 if (!inPointy && j == iHrefStart)
3367                 {
3368                     inPointy = true;
3369                     ++iHrefStart;
3370                 }
3371                 break;
3372             case '>':
3373                 if (inPointy && slice[j-1] != '\\')
3374                     goto LReturnHref;
3375                 break;
3376             case '(':
3377                 if (!inPointy && slice[j-1] != '\\')
3378                     ++parenDepth;
3379                 break;
3380             case ')':
3381                 if (!inPointy && slice[j-1] != '\\')
3382                 {
3383                     --parenDepth;
3384                     if (!parenDepth)
3385                         goto LReturnHref;
3386                 }
3387                 break;
3388             case ' ':
3389             case '\t':
3390             case '\r':
3391             case '\n':
3392                 if (inPointy)
3393                 {
3394                     // invalid link
3395                     return false;
3396                 }
3397                 goto LReturnHref;
3398             default:
3399                 break;
3400             }
3401         }
3402         if (inPointy)
3403             return false;
3404     LReturnHref:
3405         auto href = slice[iHrefStart .. j].dup;
3406         this.href = cast(string) percentEncode(removeEscapeBackslashes(href)).replaceChar(',', "$(COMMA)");
3407         i = j;
3408         if (inPointy)
3409             ++i;
3410         return true;
3411     }
3412 
3413     /****************************************************
3414      * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes
3415      * Params:
3416      *  buf   = an OutBuffer containing the DDoc
3417      *  i     = the index within `buf` that points to the first character of the title.
3418      *          If this function succeeds `i` will point just after the the end of the title.
3419      * Returns: whether a title was found and parsed
3420      */
3421     private bool parseTitle(ref OutBuffer buf, ref size_t i)
3422     {
3423         size_t j = skipChars(buf, i, " \t");
3424         if (j >= buf.length)
3425             return false;
3426 
3427         char type = buf[j];
3428         if (type != '"' && type != '\'' && type != '(')
3429             return false;
3430         if (type == '(')
3431             type = ')';
3432 
3433         const iTitleStart = j + 1;
3434         size_t iNewline = 0;
3435         const slice = buf[];
3436         for (j = iTitleStart; j < slice.length; j++)
3437         {
3438             const c = slice[j];
3439             switch (c)
3440             {
3441             case ')':
3442             case '"':
3443             case '\'':
3444                 if (type == c && slice[j-1] != '\\')
3445                     goto LEndTitle;
3446                 iNewline = 0;
3447                 break;
3448             case ' ':
3449             case '\t':
3450             case '\r':
3451                 break;
3452             case '\n':
3453                 if (iNewline)
3454                 {
3455                     // no blank lines in titles
3456                     return false;
3457                 }
3458                 iNewline = j;
3459                 break;
3460             default:
3461                 iNewline = 0;
3462                 break;
3463             }
3464         }
3465         return false;
3466     LEndTitle:
3467         auto title = slice[iTitleStart .. j].dup;
3468         this.title = cast(string) removeEscapeBackslashes(title).
3469             replaceChar(',', "$(COMMA)").
3470             replaceChar('"', "$(QUOTE)");
3471         i = j + 1;
3472         return true;
3473     }
3474 
3475     /****************************************************
3476      * Replace a Markdown link or image with the appropriate macro
3477      * Params:
3478      *  buf       = an OutBuffer containing the DDoc
3479      *  i         = the index within `buf` that points to the `]` character of the inline link.
3480      *              When this function returns it will be adjusted to the end of the inserted macro.
3481      *  iLinkEnd  = the index within `buf` that points just after the last character of the link
3482      *  delimiter = the Markdown delimiter that started the link or image
3483      */
3484     private void replaceLink(ref OutBuffer buf, ref size_t i, size_t iLinkEnd, MarkdownDelimiter delimiter)
3485     {
3486         size_t iAfterLink = i - delimiter.count;
3487         string macroName;
3488         if (symbol)
3489         {
3490             macroName = "$(SYMBOL_LINK ";
3491         }
3492         else if (title.length)
3493         {
3494             if (delimiter.type == '[')
3495                 macroName = "$(LINK_TITLE ";
3496             else
3497                 macroName = "$(IMAGE_TITLE ";
3498         }
3499         else
3500         {
3501             if (delimiter.type == '[')
3502                 macroName = "$(LINK2 ";
3503             else
3504                 macroName = "$(IMAGE ";
3505         }
3506         buf.remove(delimiter.iStart, delimiter.count);
3507         buf.remove(i - delimiter.count, iLinkEnd - i);
3508         iLinkEnd = buf.insert(delimiter.iStart, macroName);
3509         iLinkEnd = buf.insert(iLinkEnd, href);
3510         iLinkEnd = buf.insert(iLinkEnd, ", ");
3511         iAfterLink += macroName.length + href.length + 2;
3512         if (title.length)
3513         {
3514             iLinkEnd = buf.insert(iLinkEnd, title);
3515             iLinkEnd = buf.insert(iLinkEnd, ", ");
3516             iAfterLink += title.length + 2;
3517 
3518             // Link macros with titles require escaping commas
3519             for (size_t j = iLinkEnd; j < iAfterLink; ++j)
3520                 if (buf[j] == ',')
3521                 {
3522                     buf.remove(j, 1);
3523                     j = buf.insert(j, "$(COMMA)") - 1;
3524                     iAfterLink += 7;
3525                 }
3526         }
3527 // TODO: if image, remove internal macros, leaving only text
3528         buf.insert(iAfterLink, ")");
3529         i = iAfterLink;
3530     }
3531 
3532     /****************************************************
3533      * Store the Markdown link definition and remove it from `buf`
3534      * Params:
3535      *  buf               = an OutBuffer containing the DDoc
3536      *  i                 = the index within `buf` that points to the `[` character at the start of the link definition.
3537      *                      When this function returns it will be adjusted to exclude the link definition.
3538      *  iEnd              = the index within `buf` that points just after the end of the definition
3539      *  linkReferences    = previously parsed link references. When this function returns it may contain
3540      *                      an additional reference.
3541      *  loc               = the current location in the file
3542      */
3543     private void storeAndReplaceDefinition(ref OutBuffer buf, ref size_t i, size_t iEnd, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
3544     {
3545         if (global.params.vmarkdown)
3546             message(loc, "Ddoc: found link reference '%.*s' to '%.*s'", cast(int)label.length, label.ptr, cast(int)href.length, href.ptr);
3547 
3548         // Remove the definition and trailing whitespace
3549         iEnd = skipChars(buf, iEnd, " \t\r\n");
3550         buf.remove(i, iEnd - i);
3551         i -= 2;
3552 
3553         string lowercaseLabel = label.toLowercase();
3554         if (lowercaseLabel !in linkReferences.references)
3555             linkReferences.references[lowercaseLabel] = this;
3556     }
3557 
3558     /****************************************************
3559      * Remove Markdown escaping backslashes from the given string
3560      * Params:
3561      *  s = the string to remove escaping backslashes from
3562      * Returns: `s` without escaping backslashes in it
3563      */
3564     private static char[] removeEscapeBackslashes(char[] s)
3565     {
3566         if (!s.length)
3567             return s;
3568 
3569         // avoid doing anything if there isn't anything to escape
3570         size_t i;
3571         for (i = 0; i < s.length-1; ++i)
3572             if (s[i] == '\\' && ispunct(s[i+1]))
3573                 break;
3574         if (i == s.length-1)
3575             return s;
3576 
3577         // copy characters backwards, then truncate
3578         size_t j = i + 1;
3579         s[i] = s[j];
3580         for (++i, ++j; j < s.length; ++i, ++j)
3581         {
3582             if (j < s.length-1 && s[j] == '\\' && ispunct(s[j+1]))
3583                 ++j;
3584             s[i] = s[j];
3585         }
3586         s.length -= (j - i);
3587         return s;
3588     }
3589 
3590     ///
3591     unittest
3592     {
3593         assert(removeEscapeBackslashes("".dup) == "");
3594         assert(removeEscapeBackslashes(`\a`.dup) == `\a`);
3595         assert(removeEscapeBackslashes(`.\`.dup) == `.\`);
3596         assert(removeEscapeBackslashes(`\.\`.dup) == `.\`);
3597         assert(removeEscapeBackslashes(`\.`.dup) == `.`);
3598         assert(removeEscapeBackslashes(`\.\.`.dup) == `..`);
3599         assert(removeEscapeBackslashes(`a\.b\.c`.dup) == `a.b.c`);
3600     }
3601 
3602     /****************************************************
3603      * Percent-encode (AKA URL-encode) the given string
3604      * Params:
3605      *  s = the string to percent-encode
3606      * Returns: `s` with special characters percent-encoded
3607      */
3608     private static inout(char)[] percentEncode(inout(char)[] s) pure
3609     {
3610         static bool shouldEncode(char c)
3611         {
3612             return ((c < '0' && c != '!' && c != '#' && c != '$' && c != '%' && c != '&' && c != '\'' && c != '(' &&
3613                     c != ')' && c != '*' && c != '+' && c != ',' && c != '-' && c != '.' && c != '/')
3614                 || (c > '9' && c < 'A' && c != ':' && c != ';' && c != '=' && c != '?' && c != '@')
3615                 || (c > 'Z' && c < 'a' && c != '[' && c != ']' && c != '_')
3616                 || (c > 'z' && c != '~'));
3617         }
3618 
3619         for (size_t i = 0; i < s.length; ++i)
3620         {
3621             if (shouldEncode(s[i]))
3622             {
3623                 immutable static hexDigits = "0123456789ABCDEF";
3624                 immutable encoded1 = hexDigits[s[i] >> 4];
3625                 immutable encoded2 = hexDigits[s[i] & 0x0F];
3626                 s = s[0..i] ~ '%' ~ encoded1 ~ encoded2 ~ s[i+1..$];
3627                 i += 2;
3628             }
3629         }
3630         return s;
3631     }
3632 
3633     ///
3634     unittest
3635     {
3636         assert(percentEncode("") == "");
3637         assert(percentEncode("aB12-._~/?") == "aB12-._~/?");
3638         assert(percentEncode("<\n>") == "%3C%0A%3E");
3639     }
3640 
3641     /**************************************************
3642      * Skip a single newline at `i`
3643      * Params:
3644      *  buf   = an OutBuffer containing the DDoc
3645      *  i     = the index within `buf` to start looking at.
3646      *          If this function succeeds `i` will point after the newline.
3647      * Returns: whether a newline was skipped
3648      */
3649     private static bool skipOneNewline(ref OutBuffer buf, ref size_t i) pure
3650     {
3651         if (i < buf.length && buf[i] == '\r')
3652             ++i;
3653         if (i < buf.length && buf[i] == '\n')
3654         {
3655             ++i;
3656             return true;
3657         }
3658         return false;
3659     }
3660 }
3661 
3662 /**************************************************
3663  * A set of Markdown link references.
3664  */
3665 private struct MarkdownLinkReferences
3666 {
3667     MarkdownLink[string] references;    // link references keyed by normalized label
3668     MarkdownLink[string] symbols;       // link symbols keyed by name
3669     Scope* _scope;      // the current scope
3670     bool extractedAll;  // the index into the buffer of the last-parsed reference
3671 
3672     /**************************************************
3673      * Look up a reference by label, searching through the rest of the buffer if needed.
3674      * Symbols in the current scope are searched for if the DDoc doesn't define the reference.
3675      * Params:
3676      *  label = the label to find the reference for
3677      *  buf   = an OutBuffer containing the DDoc
3678      *  i     = the index within `buf` to start searching for references at
3679      *  loc   = the current location in the file
3680      * Returns: a link. If the `href` member has a value then the reference is valid.
3681      */
3682     MarkdownLink lookupReference(string label, ref OutBuffer buf, size_t i, const ref Loc loc)
3683     {
3684         const lowercaseLabel = label.toLowercase();
3685         if (lowercaseLabel !in references)
3686             extractReferences(buf, i, loc);
3687 
3688         if (lowercaseLabel in references)
3689             return references[lowercaseLabel];
3690 
3691         return MarkdownLink();
3692     }
3693 
3694     /**
3695      * Look up the link for the D symbol with the given name.
3696      * If found, the link is cached in the `symbols` member.
3697      * Params:
3698      *  name  = the name of the symbol
3699      * Returns: the link for the symbol or a link with a `null` href
3700      */
3701     MarkdownLink lookupSymbol(string name)
3702     {
3703         if (name in symbols)
3704             return symbols[name];
3705 
3706         const ids = split(name, '.');
3707 
3708         MarkdownLink link;
3709         auto id = Identifier.lookup(ids[0].ptr, ids[0].length);
3710         if (id)
3711         {
3712             auto loc = Loc();
3713             auto symbol = _scope.search(loc, id, null, IgnoreErrors);
3714             for (size_t i = 1; symbol && i < ids.length; ++i)
3715             {
3716                 id = Identifier.lookup(ids[i].ptr, ids[i].length);
3717                 symbol = id !is null ? symbol.search(loc, id, IgnoreErrors) : null;
3718             }
3719             if (symbol)
3720                 link = MarkdownLink(createHref(symbol), null, name, symbol);
3721         }
3722 
3723         symbols[name] = link;
3724         return link;
3725     }
3726 
3727     /**************************************************
3728      * Remove and store all link references from the document, in the form of
3729      * `[label]: href "optional title"`
3730      * Params:
3731      *  buf   = an OutBuffer containing the DDoc
3732      *  i     = the index within `buf` to start looking at
3733      *  loc   = the current location in the file
3734      * Returns: whether a reference was extracted
3735      */
3736     private void extractReferences(ref OutBuffer buf, size_t i, const ref Loc loc)
3737     {
3738         static bool isFollowedBySpace(ref OutBuffer buf, size_t i)
3739         {
3740             return i+1 < buf.length && (buf[i+1] == ' ' || buf[i+1] == '\t');
3741         }
3742 
3743         if (extractedAll)
3744             return;
3745 
3746         bool leadingBlank = false;
3747         int inCode = false;
3748         bool newParagraph = true;
3749         MarkdownDelimiter[] delimiters;
3750         for (; i < buf.length; ++i)
3751         {
3752             const c = buf[i];
3753             switch (c)
3754             {
3755             case ' ':
3756             case '\t':
3757                 break;
3758             case '\n':
3759                 if (leadingBlank && !inCode)
3760                     newParagraph = true;
3761                 leadingBlank = true;
3762                 break;
3763             case '\\':
3764                 ++i;
3765                 break;
3766             case '#':
3767                 if (leadingBlank && !inCode)
3768                     newParagraph = true;
3769                 leadingBlank = false;
3770                 break;
3771             case '>':
3772                 if (leadingBlank && !inCode)
3773                     newParagraph = true;
3774                 break;
3775             case '+':
3776                 if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
3777                     newParagraph = true;
3778                 else
3779                     leadingBlank = false;
3780                 break;
3781             case '0':
3782             ..
3783             case '9':
3784                 if (leadingBlank && !inCode)
3785                 {
3786                     i = skipChars(buf, i, "0123456789");
3787                     if (i < buf.length &&
3788                         (buf[i] == '.' || buf[i] == ')') &&
3789                         isFollowedBySpace(buf, i))
3790                         newParagraph = true;
3791                     else
3792                         leadingBlank = false;
3793                 }
3794                 break;
3795             case '*':
3796                 if (leadingBlank && !inCode)
3797                 {
3798                     newParagraph = true;
3799                     if (!isFollowedBySpace(buf, i))
3800                         leadingBlank = false;
3801                 }
3802                 break;
3803             case '`':
3804             case '~':
3805                 if (leadingBlank && i+2 < buf.length && buf[i+1] == c && buf[i+2] == c)
3806                 {
3807                     inCode = inCode == c ? false : c;
3808                     i = skipChars(buf, i, [c]) - 1;
3809                     newParagraph = true;
3810                 }
3811                 leadingBlank = false;
3812                 break;
3813             case '-':
3814                 if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
3815                     goto case '+';
3816                 else
3817                     goto case '`';
3818             case '[':
3819                 if (leadingBlank && !inCode && newParagraph)
3820                     delimiters ~= MarkdownDelimiter(i, 1, 0, false, false, true, c);
3821                 break;
3822             case ']':
3823                 if (delimiters.length && !inCode &&
3824                     MarkdownLink.replaceReferenceDefinition(buf, i, delimiters, cast(int) delimiters.length - 1, this, loc))
3825                     --i;
3826                 break;
3827             default:
3828                 if (leadingBlank)
3829                     newParagraph = false;
3830                 leadingBlank = false;
3831                 break;
3832             }
3833         }
3834         extractedAll = true;
3835     }
3836 
3837     /**
3838      * Split a string by a delimiter, excluding the delimiter.
3839      * Params:
3840      *  s         = the string to split
3841      *  delimiter = the character to split by
3842      * Returns: the resulting array of strings
3843      */
3844     private static string[] split(string s, char delimiter) pure
3845     {
3846         string[] result;
3847         size_t iStart = 0;
3848         foreach (size_t i; 0..s.length)
3849             if (s[i] == delimiter)
3850             {
3851                 result ~= s[iStart..i];
3852                 iStart = i + 1;
3853             }
3854         result ~= s[iStart..$];
3855         return result;
3856     }
3857 
3858     ///
3859     unittest
3860     {
3861         assert(split("", ',') == [""]);
3862         assert(split("ab", ',') == ["ab"]);
3863         assert(split("a,b", ',') == ["a", "b"]);
3864         assert(split("a,,b", ',') == ["a", "", "b"]);
3865         assert(split(",ab", ',') == ["", "ab"]);
3866         assert(split("ab,", ',') == ["ab", ""]);
3867     }
3868 
3869     /**
3870      * Create a HREF for the given D symbol.
3871      * The HREF is relative to the current location if possible.
3872      * Params:
3873      *  symbol    = the symbol to create a HREF for.
3874      * Returns: the resulting href
3875      */
3876     private string createHref(Dsymbol symbol)
3877     {
3878         Dsymbol root = symbol;
3879 
3880         const(char)[] lref;
3881         while (symbol && symbol.ident && !symbol.isModule())
3882         {
3883             if (lref.length)
3884                 lref = '.' ~ lref;
3885             lref = symbol.ident.toString() ~ lref;
3886             symbol = symbol.parent;
3887         }
3888 
3889         const(char)[] path;
3890         if (symbol && symbol.ident && symbol.isModule() != _scope._module)
3891         {
3892             do
3893             {
3894                 root = symbol;
3895 
3896                 // If the module has a file name, we're done
3897                 if (const m = symbol.isModule())
3898                     if (m.docfile)
3899                     {
3900                         path = m.docfile.toString();
3901                         break;
3902                     }
3903 
3904                 if (path.length)
3905                     path = '_' ~ path;
3906                 path = symbol.ident.toString() ~ path;
3907                 symbol = symbol.parent;
3908             } while (symbol && symbol.ident);
3909 
3910             if (!symbol && path.length)
3911                 path ~= "$(DOC_EXTENSION)";
3912         }
3913 
3914         // Attempt an absolute URL if not in the same package
3915         while (root.parent)
3916             root = root.parent;
3917         Dsymbol scopeRoot = _scope._module;
3918         while (scopeRoot.parent)
3919             scopeRoot = scopeRoot.parent;
3920         if (scopeRoot != root)
3921         {
3922             path = "$(DOC_ROOT_" ~ root.ident.toString() ~ ')' ~ path;
3923             lref = '.' ~ lref;  // remote URIs like Phobos and Mir use .prefixes
3924         }
3925 
3926         return cast(string) (path ~ '#' ~ lref);
3927     }
3928 }
3929 
3930 private enum TableColumnAlignment
3931 {
3932     none,
3933     left,
3934     center,
3935     right
3936 }
3937 
3938 /****************************************************
3939  * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |`
3940  * where the example text has four columns with the following alignments:
3941  * default, left, center, and right. The first and last pipes are optional. If a
3942  * delimiter row is found it will be removed from `buf`.
3943  *
3944  * Params:
3945  *  buf     = an OutBuffer containing the DDoc
3946  *  iStart  = the index within `buf` that the delimiter row starts at
3947  *  inQuote   = whether the table is inside a quote
3948  *  columnAlignments = alignments to populate for each column
3949  * Returns: the index of the end of the parsed delimiter, or `0` if not found
3950  */
3951 private size_t parseTableDelimiterRow(ref OutBuffer buf, const size_t iStart, bool inQuote, ref TableColumnAlignment[] columnAlignments)
3952 {
3953     size_t i = skipChars(buf, iStart, inQuote ? ">| \t" : "| \t");
3954     while (i < buf.length && buf[i] != '\r' && buf[i] != '\n')
3955     {
3956         const leftColon = buf[i] == ':';
3957         if (leftColon)
3958             ++i;
3959 
3960         if (i >= buf.length || buf[i] != '-')
3961             break;
3962         i = skipChars(buf, i, "-");
3963 
3964         const rightColon = i < buf.length && buf[i] == ':';
3965         i = skipChars(buf, i, ": \t");
3966 
3967         if (i >= buf.length || (buf[i] != '|' && buf[i] != '\r' && buf[i] != '\n'))
3968             break;
3969         i = skipChars(buf, i, "| \t");
3970 
3971         columnAlignments ~= (leftColon && rightColon) ? TableColumnAlignment.center :
3972                 leftColon ? TableColumnAlignment.left :
3973                 rightColon ? TableColumnAlignment.right :
3974                 TableColumnAlignment.none;
3975     }
3976 
3977     if (i < buf.length && buf[i] != '\r' && buf[i] != '\n' && buf[i] != ')')
3978     {
3979         columnAlignments.length = 0;
3980         return 0;
3981     }
3982 
3983     if (i < buf.length && buf[i] == '\r') ++i;
3984     if (i < buf.length && buf[i] == '\n') ++i;
3985     return i;
3986 }
3987 
3988 /****************************************************
3989  * Look for a table delimiter row, and if found parse the previous row as a
3990  * table header row. If both exist with a matching number of columns, start a
3991  * table.
3992  *
3993  * Params:
3994  *  buf       = an OutBuffer containing the DDoc
3995  *  iStart    = the index within `buf` that the table header row starts at, inclusive
3996  *  iEnd      = the index within `buf` that the table header row ends at, exclusive
3997  *  loc       = the current location in the file
3998  *  inQuote   = whether the table is inside a quote
3999  *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
4000  *  columnAlignments = the parsed alignments for each column
4001  * Returns: the number of characters added by starting the table, or `0` if unchanged
4002  */
4003 private size_t startTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, bool inQuote, ref MarkdownDelimiter[] inlineDelimiters, out TableColumnAlignment[] columnAlignments)
4004 {
4005     const iDelimiterRowEnd = parseTableDelimiterRow(buf, iEnd + 1, inQuote, columnAlignments);
4006     if (iDelimiterRowEnd)
4007     {
4008         const delta = replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, true);
4009         if (delta)
4010         {
4011             buf.remove(iEnd + delta, iDelimiterRowEnd - iEnd);
4012             buf.insert(iEnd + delta, "$(TBODY ");
4013             buf.insert(iStart, "$(TABLE ");
4014             return delta + 15;
4015         }
4016     }
4017 
4018     columnAlignments.length = 0;
4019     return 0;
4020 }
4021 
4022 /****************************************************
4023  * Replace a Markdown table row in the form of table cells delimited by pipes:
4024  * `| cell | cell | cell`. The first and last pipes are optional.
4025  *
4026  * Params:
4027  *  buf       = an OutBuffer containing the DDoc
4028  *  iStart    = the index within `buf` that the table row starts at, inclusive
4029  *  iEnd      = the index within `buf` that the table row ends at, exclusive
4030  *  loc       = the current location in the file
4031  *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
4032  *  columnAlignments = alignments for each column
4033  *  headerRow = if `true` then the number of columns will be enforced to match
4034  *              `columnAlignments.length` and the row will be surrounded by a
4035  *              `THEAD` macro
4036  * Returns: the number of characters added by replacing the row, or `0` if unchanged
4037  */
4038 private size_t replaceTableRow(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, TableColumnAlignment[] columnAlignments, bool headerRow)
4039 {
4040     if (!columnAlignments.length || iStart == iEnd)
4041         return 0;
4042 
4043     iStart = skipChars(buf, iStart, " \t");
4044     int cellCount = 0;
4045     foreach (delimiter; inlineDelimiters)
4046         if (delimiter.type == '|' && !delimiter.leftFlanking)
4047             ++cellCount;
4048     bool ignoreLast = inlineDelimiters.length > 0 && inlineDelimiters[$-1].type == '|';
4049     if (ignoreLast)
4050     {
4051         const iLast = skipChars(buf, inlineDelimiters[$-1].iStart + inlineDelimiters[$-1].count, " \t");
4052         ignoreLast = iLast >= iEnd;
4053     }
4054     if (!ignoreLast)
4055         ++cellCount;
4056 
4057     if (headerRow && cellCount != columnAlignments.length)
4058         return 0;
4059 
4060     if (headerRow && global.params.vmarkdown)
4061     {
4062         const s = buf[][iStart..iEnd];
4063         message(loc, "Ddoc: formatting table '%.*s'", cast(int)s.length, s.ptr);
4064     }
4065 
4066     size_t delta = 0;
4067 
4068     void replaceTableCell(size_t iCellStart, size_t iCellEnd, int cellIndex, int di)
4069     {
4070         const eDelta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, di);
4071         delta += eDelta;
4072         iCellEnd += eDelta;
4073 
4074         // strip trailing whitespace and delimiter
4075         size_t i = iCellEnd - 1;
4076         while (i > iCellStart && (buf[i] == '|' || buf[i] == ' ' || buf[i] == '\t'))
4077             --i;
4078         ++i;
4079         buf.remove(i, iCellEnd - i);
4080         delta -= iCellEnd - i;
4081         iCellEnd = i;
4082 
4083         buf.insert(iCellEnd, ")");
4084         ++delta;
4085 
4086         // strip initial whitespace and delimiter
4087         i = skipChars(buf, iCellStart, "| \t");
4088         buf.remove(iCellStart, i - iCellStart);
4089         delta -= i - iCellStart;
4090 
4091         switch (columnAlignments[cellIndex])
4092         {
4093         case TableColumnAlignment.none:
4094             buf.insert(iCellStart, headerRow ? "$(TH " : "$(TD ");
4095             delta += 5;
4096             break;
4097         case TableColumnAlignment.left:
4098             buf.insert(iCellStart, "left, ");
4099             delta += 6;
4100             goto default;
4101         case TableColumnAlignment.center:
4102             buf.insert(iCellStart, "center, ");
4103             delta += 8;
4104             goto default;
4105         case TableColumnAlignment.right:
4106             buf.insert(iCellStart, "right, ");
4107             delta += 7;
4108             goto default;
4109         default:
4110             buf.insert(iCellStart, headerRow ? "$(TH_ALIGN " : "$(TD_ALIGN ");
4111             delta += 11;
4112             break;
4113         }
4114     }
4115 
4116     int cellIndex = cellCount - 1;
4117     size_t iCellEnd = iEnd;
4118     foreach_reverse (di, delimiter; inlineDelimiters)
4119     {
4120         if (delimiter.type == '|')
4121         {
4122             if (ignoreLast && di == inlineDelimiters.length-1)
4123             {
4124                 ignoreLast = false;
4125                 continue;
4126             }
4127 
4128             if (cellIndex >= columnAlignments.length)
4129             {
4130                 // kill any extra cells
4131                 buf.remove(delimiter.iStart, iEnd + delta - delimiter.iStart);
4132                 delta -= iEnd + delta - delimiter.iStart;
4133                 iCellEnd = iEnd + delta;
4134                 --cellIndex;
4135                 continue;
4136             }
4137 
4138             replaceTableCell(delimiter.iStart, iCellEnd, cellIndex, cast(int) di);
4139             iCellEnd = delimiter.iStart;
4140             --cellIndex;
4141         }
4142     }
4143 
4144     // if no starting pipe, replace from the start
4145     if (cellIndex >= 0)
4146         replaceTableCell(iStart, iCellEnd, cellIndex, 0);
4147 
4148     buf.insert(iEnd + delta, ")");
4149     buf.insert(iStart, "$(TR ");
4150     delta += 6;
4151 
4152     if (headerRow)
4153     {
4154         buf.insert(iEnd + delta, ")");
4155         buf.insert(iStart, "$(THEAD ");
4156         delta += 9;
4157     }
4158 
4159     return delta;
4160 }
4161 
4162 /****************************************************
4163  * End a table, if in one.
4164  *
4165  * Params:
4166  *  buf = an OutBuffer containing the DDoc
4167  *  i   = the index within `buf` to end the table at
4168  *  columnAlignments = alignments for each column; upon return is set to length `0`
4169  * Returns: the number of characters added by ending the table, or `0` if unchanged
4170  */
4171 private size_t endTable(ref OutBuffer buf, size_t i, ref TableColumnAlignment[] columnAlignments)
4172 {
4173     if (!columnAlignments.length)
4174         return 0;
4175 
4176     buf.insert(i, "))");
4177     columnAlignments.length = 0;
4178     return 2;
4179 }
4180 
4181 /****************************************************
4182  * End a table row and then the table itself.
4183  *
4184  * Params:
4185  *  buf       = an OutBuffer containing the DDoc
4186  *  iStart    = the index within `buf` that the table row starts at, inclusive
4187  *  iEnd      = the index within `buf` that the table row ends at, exclusive
4188  *  loc       = the current location in the file
4189  *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
4190  *  columnAlignments = alignments for each column; upon return is set to length `0`
4191  * Returns: the number of characters added by replacing the row, or `0` if unchanged
4192  */
4193 private size_t endRowAndTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, ref TableColumnAlignment[] columnAlignments)
4194 {
4195     size_t delta = replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, false);
4196     delta += endTable(buf, iEnd + delta, columnAlignments);
4197     return delta;
4198 }
4199 
4200 /**************************************************
4201  * Highlight text section.
4202  *
4203  * Params:
4204  *  scope = the current parse scope
4205  *  a     = an array of D symbols at the current scope
4206  *  loc   = source location of start of text. It is a mutable copy to allow incrementing its linenum, for printing the correct line number when an error is encountered in a multiline block of ddoc.
4207  *  buf   = an OutBuffer containing the DDoc
4208  *  offset = the index within buf to start highlighting
4209  */
4210 private void highlightText(Scope* sc, Dsymbols* a, Loc loc, ref OutBuffer buf, size_t offset)
4211 {
4212     const incrementLoc = loc.linnum == 0 ? 1 : 0;
4213     loc.linnum += incrementLoc;
4214     loc.charnum = 0;
4215     //printf("highlightText()\n");
4216     bool leadingBlank = true;
4217     size_t iParagraphStart = offset;
4218     size_t iPrecedingBlankLine = 0;
4219     int headingLevel = 0;
4220     int headingMacroLevel = 0;
4221     int quoteLevel = 0;
4222     bool lineQuoted = false;
4223     int quoteMacroLevel = 0;
4224     MarkdownList[] nestedLists;
4225     MarkdownDelimiter[] inlineDelimiters;
4226     MarkdownLinkReferences linkReferences;
4227     TableColumnAlignment[] columnAlignments;
4228     bool tableRowDetected = false;
4229     int inCode = 0;
4230     int inBacktick = 0;
4231     int macroLevel = 0;
4232     int previousMacroLevel = 0;
4233     int parenLevel = 0;
4234     size_t iCodeStart = 0; // start of code section
4235     size_t codeFenceLength = 0;
4236     size_t codeIndent = 0;
4237     string codeLanguage;
4238     size_t iLineStart = offset;
4239     linkReferences._scope = sc;
4240     for (size_t i = offset; i < buf.length; i++)
4241     {
4242         char c = buf[i];
4243     Lcont:
4244         switch (c)
4245         {
4246         case ' ':
4247         case '\t':
4248             break;
4249         case '\n':
4250             if (inBacktick)
4251             {
4252                 // `inline code` is only valid if contained on a single line
4253                 // otherwise, the backticks should be output literally.
4254                 //
4255                 // This lets things like `output from the linker' display
4256                 // unmolested while keeping the feature consistent with GitHub.
4257                 inBacktick = false;
4258                 inCode = false; // the backtick also assumes we're in code
4259                 // Nothing else is necessary since the DDOC_BACKQUOTED macro is
4260                 // inserted lazily at the close quote, meaning the rest of the
4261                 // text is already OK.
4262             }
4263             if (headingLevel)
4264             {
4265                 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
4266                 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
4267                 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4268                 ++i;
4269                 iParagraphStart = skipChars(buf, i, " \t\r\n");
4270             }
4271 
4272             if (tableRowDetected && !columnAlignments.length)
4273                 i += startTable(buf, iLineStart, i, loc, lineQuoted, inlineDelimiters, columnAlignments);
4274             else if (columnAlignments.length)
4275             {
4276                 const delta = replaceTableRow(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments, false);
4277                 if (delta)
4278                     i += delta;
4279                 else
4280                     i += endTable(buf, i, columnAlignments);
4281             }
4282 
4283             if (!inCode && nestedLists.length && !quoteLevel)
4284                 MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);
4285 
4286             iPrecedingBlankLine = 0;
4287             if (!inCode && i == iLineStart && i + 1 < buf.length) // if "\n\n"
4288             {
4289                 i += endTable(buf, i, columnAlignments);
4290                 if (!lineQuoted && quoteLevel)
4291                     endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
4292                 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
4293 
4294                 // if we don't already know about this paragraph break then
4295                 // insert a blank line and record the paragraph break
4296                 if (iParagraphStart <= i)
4297                 {
4298                     iPrecedingBlankLine = i;
4299                     i = buf.insert(i, "$(DDOC_BLANKLINE)");
4300                     iParagraphStart = i + 1;
4301                 }
4302             }
4303             else if (inCode &&
4304                 i == iLineStart &&
4305                 i + 1 < buf.length &&
4306                 !lineQuoted &&
4307                 quoteLevel) // if "\n\n" in quoted code
4308             {
4309                 inCode = false;
4310                 i = buf.insert(i, ")");
4311                 i += endAllMarkdownQuotes(buf, i, quoteLevel);
4312                 quoteMacroLevel = 0;
4313             }
4314             leadingBlank = true;
4315             lineQuoted = false;
4316             tableRowDetected = false;
4317             iLineStart = i + 1;
4318             loc.linnum += incrementLoc;
4319 
4320             // update the paragraph start if we just entered a macro
4321             if (previousMacroLevel < macroLevel && iParagraphStart < iLineStart)
4322                 iParagraphStart = iLineStart;
4323             previousMacroLevel = macroLevel;
4324             break;
4325 
4326         case '<':
4327             {
4328                 leadingBlank = false;
4329                 if (inCode)
4330                     break;
4331                 const slice = buf[];
4332                 auto p = &slice[i];
4333                 const se = sc._module.escapetable.escapeChar('<');
4334                 if (se == "&lt;")
4335                 {
4336                     // Generating HTML
4337                     // Skip over comments
4338                     if (p[1] == '!' && p[2] == '-' && p[3] == '-')
4339                     {
4340                         size_t j = i + 4;
4341                         p += 4;
4342                         while (1)
4343                         {
4344                             if (j == slice.length)
4345                                 goto L1;
4346                             if (p[0] == '-' && p[1] == '-' && p[2] == '>')
4347                             {
4348                                 i = j + 2; // place on closing '>'
4349                                 break;
4350                             }
4351                             j++;
4352                             p++;
4353                         }
4354                         break;
4355                     }
4356                     // Skip over HTML tag
4357                     if (isalpha(p[1]) || (p[1] == '/' && isalpha(p[2])))
4358                     {
4359                         size_t j = i + 2;
4360                         p += 2;
4361                         while (1)
4362                         {
4363                             if (j == slice.length)
4364                                 break;
4365                             if (p[0] == '>')
4366                             {
4367                                 i = j; // place on closing '>'
4368                                 break;
4369                             }
4370                             j++;
4371                             p++;
4372                         }
4373                         break;
4374                     }
4375                 }
4376             L1:
4377                 // Replace '<' with '&lt;' character entity
4378                 if (se.length)
4379                 {
4380                     buf.remove(i, 1);
4381                     i = buf.insert(i, se);
4382                     i--; // point to ';'
4383                 }
4384                 break;
4385             }
4386 
4387         case '>':
4388             {
4389                 if (leadingBlank && (!inCode || quoteLevel) && global.params.markdown)
4390                 {
4391                     if (!quoteLevel && global.params.vmarkdown)
4392                     {
4393                         size_t iEnd = i + 1;
4394                         while (iEnd < buf.length && buf[iEnd] != '\n')
4395                             ++iEnd;
4396                         const s = buf[][i .. iEnd];
4397                         message(loc, "Ddoc: starting quote block with '%.*s'", cast(int)s.length, s.ptr);
4398                     }
4399 
4400                     lineQuoted = true;
4401                     int lineQuoteLevel = 1;
4402                     size_t iAfterDelimiters = i + 1;
4403                     for (; iAfterDelimiters < buf.length; ++iAfterDelimiters)
4404                     {
4405                         const c0 = buf[iAfterDelimiters];
4406                         if (c0 == '>')
4407                             ++lineQuoteLevel;
4408                         else if (c0 != ' ' && c0 != '\t')
4409                             break;
4410                     }
4411                     if (!quoteMacroLevel)
4412                         quoteMacroLevel = macroLevel;
4413                     buf.remove(i, iAfterDelimiters - i);
4414 
4415                     if (quoteLevel < lineQuoteLevel)
4416                     {
4417                         i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4418                         if (nestedLists.length)
4419                         {
4420                             const indent = getMarkdownIndent(buf, iLineStart, i);
4421                             if (indent < nestedLists[$-1].contentIndent)
4422                                 i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
4423                         }
4424 
4425                         for (; quoteLevel < lineQuoteLevel; ++quoteLevel)
4426                         {
4427                             i = buf.insert(i, "$(BLOCKQUOTE\n");
4428                             iLineStart = iParagraphStart = i;
4429                         }
4430                         --i;
4431                     }
4432                     else
4433                     {
4434                         --i;
4435                         if (nestedLists.length)
4436                             MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);
4437                     }
4438                     break;
4439                 }
4440 
4441                 leadingBlank = false;
4442                 if (inCode)
4443                     break;
4444                 // Replace '>' with '&gt;' character entity
4445                 const se = sc._module.escapetable.escapeChar('>');
4446                 if (se.length)
4447                 {
4448                     buf.remove(i, 1);
4449                     i = buf.insert(i, se);
4450                     i--; // point to ';'
4451                 }
4452                 break;
4453             }
4454 
4455         case '&':
4456             {
4457                 leadingBlank = false;
4458                 if (inCode)
4459                     break;
4460                 char* p = cast(char*)&buf[].ptr[i];
4461                 if (p[1] == '#' || isalpha(p[1]))
4462                     break;
4463                 // already a character entity
4464                 // Replace '&' with '&amp;' character entity
4465                 const se = sc._module.escapetable.escapeChar('&');
4466                 if (se)
4467                 {
4468                     buf.remove(i, 1);
4469                     i = buf.insert(i, se);
4470                     i--; // point to ';'
4471                 }
4472                 break;
4473             }
4474 
4475         case '`':
4476             {
4477                 const iAfterDelimiter = skipChars(buf, i, "`");
4478                 const count = iAfterDelimiter - i;
4479 
4480                 if (inBacktick == count)
4481                 {
4482                     inBacktick = 0;
4483                     inCode = 0;
4484                     OutBuffer codebuf;
4485                     codebuf.write(buf[iCodeStart + count .. i]);
4486                     // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
4487                     highlightCode(sc, a, codebuf, 0);
4488                     escapeStrayParenthesis(loc, &codebuf, 0, false);
4489                     buf.remove(iCodeStart, i - iCodeStart + count); // also trimming off the current `
4490                     immutable pre = "$(DDOC_BACKQUOTED ";
4491                     i = buf.insert(iCodeStart, pre);
4492                     i = buf.insert(i, codebuf[]);
4493                     i = buf.insert(i, ")");
4494                     i--; // point to the ending ) so when the for loop does i++, it will see the next character
4495                     break;
4496                 }
4497 
4498                 // Perhaps we're starting or ending a Markdown code block
4499                 if (leadingBlank && global.params.markdown && count >= 3)
4500                 {
4501                     bool moreBackticks = false;
4502                     for (size_t j = iAfterDelimiter; !moreBackticks && j < buf.length; ++j)
4503                         if (buf[j] == '`')
4504                             moreBackticks = true;
4505                         else if (buf[j] == '\r' || buf[j] == '\n')
4506                             break;
4507                     if (!moreBackticks)
4508                         goto case '-';
4509                 }
4510 
4511                 if (inCode)
4512                 {
4513                     if (inBacktick)
4514                         i = iAfterDelimiter - 1;
4515                     break;
4516                 }
4517                 inCode = c;
4518                 inBacktick = cast(int) count;
4519                 codeIndent = 0; // inline code is not indented
4520                 // All we do here is set the code flags and record
4521                 // the location. The macro will be inserted lazily
4522                 // so we can easily cancel the inBacktick if we come
4523                 // across a newline character.
4524                 iCodeStart = i;
4525                 i = iAfterDelimiter - 1;
4526                 break;
4527             }
4528 
4529         case '#':
4530         {
4531             /* A line beginning with # indicates an ATX-style heading. */
4532             if (leadingBlank && !inCode)
4533             {
4534                 leadingBlank = false;
4535 
4536                 headingLevel = detectAtxHeadingLevel(buf, i);
4537                 if (!headingLevel)
4538                     break;
4539 
4540                 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4541                 if (!lineQuoted && quoteLevel)
4542                     i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4543 
4544                 // remove the ### prefix, including whitespace
4545                 i = skipChars(buf, i + headingLevel, " \t");
4546                 buf.remove(iLineStart, i - iLineStart);
4547                 i = iParagraphStart = iLineStart;
4548 
4549                 removeAnyAtxHeadingSuffix(buf, i);
4550                 --i;
4551 
4552                 headingMacroLevel = macroLevel;
4553             }
4554             break;
4555         }
4556 
4557         case '~':
4558             {
4559                 if (leadingBlank && global.params.markdown)
4560                 {
4561                     // Perhaps we're starting or ending a Markdown code block
4562                     const iAfterDelimiter = skipChars(buf, i, "~");
4563                     if (iAfterDelimiter - i >= 3)
4564                         goto case '-';
4565                 }
4566                 leadingBlank = false;
4567                 break;
4568             }
4569 
4570         case '-':
4571             /* A line beginning with --- delimits a code section.
4572              * inCode tells us if it is start or end of a code section.
4573              */
4574             if (leadingBlank)
4575             {
4576                 if (!inCode && c == '-')
4577                 {
4578                     const list = MarkdownList.parseItem(buf, iLineStart, i);
4579                     if (list.isValid)
4580                     {
4581                         if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4582                         {
4583                             removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4584                             iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4585                             break;
4586                         }
4587                         else
4588                             goto case '+';
4589                     }
4590                 }
4591 
4592                 size_t istart = i;
4593                 size_t eollen = 0;
4594                 leadingBlank = false;
4595                 const c0 = c; // if we jumped here from case '`' or case '~'
4596                 size_t iInfoString = 0;
4597                 if (!inCode)
4598                     codeLanguage.length = 0;
4599                 while (1)
4600                 {
4601                     ++i;
4602                     if (i >= buf.length)
4603                         break;
4604                     c = buf[i];
4605                     if (c == '\n')
4606                     {
4607                         eollen = 1;
4608                         break;
4609                     }
4610                     if (c == '\r')
4611                     {
4612                         eollen = 1;
4613                         if (i + 1 >= buf.length)
4614                             break;
4615                         if (buf[i + 1] == '\n')
4616                         {
4617                             eollen = 2;
4618                             break;
4619                         }
4620                     }
4621                     // BUG: handle UTF PS and LS too
4622                     if (c != c0 || iInfoString)
4623                     {
4624                         if (global.params.markdown && !iInfoString && !inCode && i - istart >= 3)
4625                         {
4626                             // Start a Markdown info string, like ```ruby
4627                             codeFenceLength = i - istart;
4628                             i = iInfoString = skipChars(buf, i, " \t");
4629                         }
4630                         else if (iInfoString && c != '`')
4631                         {
4632                             if (!codeLanguage.length && (c == ' ' || c == '\t'))
4633                                 codeLanguage = cast(string) buf[iInfoString..i].idup;
4634                         }
4635                         else
4636                         {
4637                             iInfoString = 0;
4638                             goto Lcont;
4639                         }
4640                     }
4641                 }
4642                 if (i - istart < 3 || (inCode && (inCode != c0 || (inCode != '-' && i - istart < codeFenceLength))))
4643                     goto Lcont;
4644                 if (iInfoString)
4645                 {
4646                     if (!codeLanguage.length)
4647                         codeLanguage = cast(string) buf[iInfoString..i].idup;
4648                 }
4649                 else
4650                     codeFenceLength = i - istart;
4651 
4652                 // We have the start/end of a code section
4653                 // Remove the entire --- line, including blanks and \n
4654                 buf.remove(iLineStart, i - iLineStart + eollen);
4655                 i = iLineStart;
4656                 if (eollen)
4657                     leadingBlank = true;
4658                 if (inCode && (i <= iCodeStart))
4659                 {
4660                     // Empty code section, just remove it completely.
4661                     inCode = 0;
4662                     break;
4663                 }
4664                 if (inCode)
4665                 {
4666                     inCode = 0;
4667                     // The code section is from iCodeStart to i
4668                     OutBuffer codebuf;
4669                     codebuf.write(buf[iCodeStart .. i]);
4670                     codebuf.writeByte(0);
4671                     // Remove leading indentations from all lines
4672                     bool lineStart = true;
4673                     char* endp = cast(char*)codebuf[].ptr + codebuf.length;
4674                     for (char* p = cast(char*)codebuf[].ptr; p < endp;)
4675                     {
4676                         if (lineStart)
4677                         {
4678                             size_t j = codeIndent;
4679                             char* q = p;
4680                             while (j-- > 0 && q < endp && isIndentWS(q))
4681                                 ++q;
4682                             codebuf.remove(p - cast(char*)codebuf[].ptr, q - p);
4683                             assert(cast(char*)codebuf[].ptr <= p);
4684                             assert(p < cast(char*)codebuf[].ptr + codebuf.length);
4685                             lineStart = false;
4686                             endp = cast(char*)codebuf[].ptr + codebuf.length; // update
4687                             continue;
4688                         }
4689                         if (*p == '\n')
4690                             lineStart = true;
4691                         ++p;
4692                     }
4693                     if (!codeLanguage.length || codeLanguage == "dlang" || codeLanguage == "d")
4694                         highlightCode2(sc, a, codebuf, 0);
4695                     else
4696                         codebuf.remove(codebuf.length-1, 1);    // remove the trailing 0 byte
4697                     escapeStrayParenthesis(loc, &codebuf, 0, false);
4698                     buf.remove(iCodeStart, i - iCodeStart);
4699                     i = buf.insert(iCodeStart, codebuf[]);
4700                     i = buf.insert(i, ")\n");
4701                     i -= 2; // in next loop, c should be '\n'
4702                 }
4703                 else
4704                 {
4705                     i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4706                     if (!lineQuoted && quoteLevel)
4707                     {
4708                         const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4709                         i += delta;
4710                         istart += delta;
4711                     }
4712 
4713                     inCode = c0;
4714                     codeIndent = istart - iLineStart; // save indent count
4715                     if (codeLanguage.length && codeLanguage != "dlang" && codeLanguage != "d")
4716                     {
4717                         // backslash-escape
4718                         for (size_t j; j < codeLanguage.length - 1; ++j)
4719                             if (codeLanguage[j] == '\\' && ispunct(codeLanguage[j + 1]))
4720                                 codeLanguage = codeLanguage[0..j] ~ codeLanguage[j + 1..$];
4721 
4722                         if (global.params.vmarkdown)
4723                             message(loc, "Ddoc: adding code block for language '%.*s'", cast(int)codeLanguage.length, codeLanguage.ptr);
4724 
4725                         i = buf.insert(i, "$(OTHER_CODE ");
4726                         i = buf.insert(i, codeLanguage);
4727                         i = buf.insert(i, ",");
4728                     }
4729                     else
4730                         i = buf.insert(i, "$(D_CODE ");
4731                     iCodeStart = i;
4732                     i--; // place i on >
4733                     leadingBlank = true;
4734                 }
4735             }
4736             break;
4737 
4738         case '_':
4739         {
4740             if (leadingBlank && !inCode && replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4741             {
4742                 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4743                 if (!lineQuoted && quoteLevel)
4744                     i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4745                 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4746                 iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4747                 break;
4748             }
4749             goto default;
4750         }
4751 
4752         case '+':
4753         case '0':
4754         ..
4755         case '9':
4756         {
4757             if (leadingBlank && !inCode)
4758             {
4759                 MarkdownList list = MarkdownList.parseItem(buf, iLineStart, i);
4760                 if (list.isValid)
4761                 {
4762                     // Avoid starting a numbered list in the middle of a paragraph
4763                     if (!nestedLists.length && list.orderedStart.length &&
4764                         iParagraphStart < iLineStart)
4765                     {
4766                         i += list.orderedStart.length - 1;
4767                         break;
4768                     }
4769 
4770                     i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4771                     if (!lineQuoted && quoteLevel)
4772                     {
4773                         const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4774                         i += delta;
4775                         list.iStart += delta;
4776                         list.iContentStart += delta;
4777                     }
4778 
4779                     list.macroLevel = macroLevel;
4780                     list.startItem(buf, iLineStart, i, iPrecedingBlankLine, nestedLists, loc);
4781                     break;
4782                 }
4783             }
4784             leadingBlank = false;
4785             break;
4786         }
4787 
4788         case '*':
4789         {
4790             if (inCode || inBacktick || !global.params.markdown)
4791             {
4792                 leadingBlank = false;
4793                 break;
4794             }
4795 
4796             if (leadingBlank)
4797             {
4798                 // Check for a thematic break
4799                 if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4800                 {
4801                     i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4802                     if (!lineQuoted && quoteLevel)
4803                         i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4804                     removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4805                     iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4806                     break;
4807                 }
4808 
4809                 // An initial * indicates a Markdown list item
4810                 const list = MarkdownList.parseItem(buf, iLineStart, i);
4811                 if (list.isValid)
4812                     goto case '+';
4813             }
4814 
4815             // Markdown emphasis
4816             const leftC = i > offset ? buf[i-1] : '\0';
4817             size_t iAfterEmphasis = skipChars(buf, i+1, "*");
4818             const rightC = iAfterEmphasis < buf.length ? buf[iAfterEmphasis] : '\0';
4819             int count = cast(int) (iAfterEmphasis - i);
4820             const leftFlanking = (rightC != '\0' && !isspace(rightC)) && (!ispunct(rightC) || leftC == '\0' || isspace(leftC) || ispunct(leftC));
4821             const rightFlanking = (leftC != '\0' && !isspace(leftC)) && (!ispunct(leftC) || rightC == '\0' || isspace(rightC) || ispunct(rightC));
4822             auto emphasis = MarkdownDelimiter(i, count, macroLevel, leftFlanking, rightFlanking, false, c);
4823 
4824             if (!emphasis.leftFlanking && !emphasis.rightFlanking)
4825             {
4826                 i = iAfterEmphasis - 1;
4827                 break;
4828             }
4829 
4830             inlineDelimiters ~= emphasis;
4831             i += emphasis.count;
4832             --i;
4833             break;
4834         }
4835 
4836         case '!':
4837         {
4838             leadingBlank = false;
4839 
4840             if (inCode || !global.params.markdown)
4841                 break;
4842 
4843             if (i < buf.length-1 && buf[i+1] == '[')
4844             {
4845                 const imageStart = MarkdownDelimiter(i, 2, macroLevel, false, false, false, c);
4846                 inlineDelimiters ~= imageStart;
4847                 ++i;
4848             }
4849             break;
4850         }
4851         case '[':
4852         {
4853             if (inCode || !global.params.markdown)
4854             {
4855                 leadingBlank = false;
4856                 break;
4857             }
4858 
4859             const leftC = i > offset ? buf[i-1] : '\0';
4860             const rightFlanking = leftC != '\0' && !isspace(leftC) && !ispunct(leftC);
4861             const atParagraphStart = leadingBlank && iParagraphStart >= iLineStart;
4862             const linkStart = MarkdownDelimiter(i, 1, macroLevel, false, rightFlanking, atParagraphStart, c);
4863             inlineDelimiters ~= linkStart;
4864             leadingBlank = false;
4865             break;
4866         }
4867         case ']':
4868         {
4869             leadingBlank = false;
4870 
4871             if (inCode || !global.params.markdown)
4872                 break;
4873 
4874             for (int d = cast(int) inlineDelimiters.length - 1; d >= 0; --d)
4875             {
4876                 const delimiter = inlineDelimiters[d];
4877                 if (delimiter.type == '[' || delimiter.type == '!')
4878                 {
4879                     if (delimiter.isValid &&
4880                         MarkdownLink.replaceLink(buf, i, loc, inlineDelimiters, d, linkReferences))
4881                     {
4882                         // if we removed a reference link then we're at line start
4883                         if (i <= delimiter.iStart)
4884                             leadingBlank = true;
4885 
4886                         // don't nest links
4887                         if (delimiter.type == '[')
4888                             for (--d; d >= 0; --d)
4889                                 if (inlineDelimiters[d].type == '[')
4890                                     inlineDelimiters[d].invalidate();
4891                     }
4892                     else
4893                     {
4894                         // nothing found, so kill the delimiter
4895                         inlineDelimiters = inlineDelimiters[0..d] ~ inlineDelimiters[d+1..$];
4896                     }
4897                     break;
4898                 }
4899             }
4900             break;
4901         }
4902 
4903         case '|':
4904         {
4905             if (inCode || !global.params.markdown)
4906             {
4907                 leadingBlank = false;
4908                 break;
4909             }
4910 
4911             tableRowDetected = true;
4912             inlineDelimiters ~= MarkdownDelimiter(i, 1, macroLevel, leadingBlank, false, false, c);
4913             leadingBlank = false;
4914             break;
4915         }
4916 
4917         case '\\':
4918         {
4919             leadingBlank = false;
4920             if (inCode || i+1 >= buf.length || !global.params.markdown)
4921                 break;
4922 
4923             /* Escape Markdown special characters */
4924             char c1 = buf[i+1];
4925             if (ispunct(c1))
4926             {
4927                 if (global.params.vmarkdown)
4928                     message(loc, "Ddoc: backslash-escaped %c", c1);
4929 
4930                 buf.remove(i, 1);
4931 
4932                 auto se = sc._module.escapetable.escapeChar(c1);
4933                 if (!se)
4934                     se = c1 == '$' ? "$(DOLLAR)" : c1 == ',' ? "$(COMMA)" : null;
4935                 if (se)
4936                 {
4937                     buf.remove(i, 1);
4938                     i = buf.insert(i, se);
4939                     i--; // point to escaped char
4940                 }
4941             }
4942             break;
4943         }
4944 
4945         case '$':
4946         {
4947             /* Look for the start of a macro, '$(Identifier'
4948              */
4949             leadingBlank = false;
4950             if (inCode || inBacktick)
4951                 break;
4952             const slice = buf[];
4953             auto p = &slice[i];
4954             if (p[1] == '(' && isIdStart(&p[2]))
4955                 ++macroLevel;
4956             break;
4957         }
4958 
4959         case '(':
4960         {
4961             if (!inCode && i > offset && buf[i-1] != '$')
4962                 ++parenLevel;
4963             break;
4964         }
4965 
4966         case ')':
4967         {   /* End of macro
4968              */
4969             leadingBlank = false;
4970             if (inCode || inBacktick)
4971                 break;
4972             if (parenLevel > 0)
4973                 --parenLevel;
4974             else if (macroLevel)
4975             {
4976                 int downToLevel = cast(int) inlineDelimiters.length;
4977                 while (downToLevel > 0 && inlineDelimiters[downToLevel - 1].macroLevel >= macroLevel)
4978                     --downToLevel;
4979                 if (headingLevel && headingMacroLevel >= macroLevel)
4980                 {
4981                     endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
4982                     removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4983                 }
4984                 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4985                 while (nestedLists.length && nestedLists[$-1].macroLevel >= macroLevel)
4986                 {
4987                     i = buf.insert(i, ")\n)");
4988                     --nestedLists.length;
4989                 }
4990                 if (quoteLevel && quoteMacroLevel >= macroLevel)
4991                     i += endAllMarkdownQuotes(buf, i, quoteLevel);
4992                 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters, downToLevel);
4993 
4994                 --macroLevel;
4995                 quoteMacroLevel = 0;
4996             }
4997             break;
4998         }
4999 
5000         default:
5001             leadingBlank = false;
5002             if (sc._module.isDocFile || inCode)
5003                 break;
5004             const start = cast(char*)buf[].ptr + i;
5005             if (isIdStart(start))
5006             {
5007                 size_t j = skippastident(buf, i);
5008                 if (i < j)
5009                 {
5010                     size_t k = skippastURL(buf, i);
5011                     if (i < k)
5012                     {
5013                         /* The URL is buf[i..k]
5014                          */
5015                         if (macroLevel)
5016                             /* Leave alone if already in a macro
5017                              */
5018                             i = k - 1;
5019                         else
5020                         {
5021                             /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)'
5022                              */
5023                             i = buf.bracket(i, "$(DDOC_LINK_AUTODETECT ", k, ")") - 1;
5024                         }
5025                         break;
5026                     }
5027                 }
5028                 else
5029                     break;
5030                 size_t len = j - i;
5031                 // leading '_' means no highlight unless it's a reserved symbol name
5032                 if (c == '_' && (i == 0 || !isdigit(*(start - 1))) && (i == buf.length - 1 || !isReservedName(start[0 .. len])))
5033                 {
5034                     buf.remove(i, 1);
5035                     i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j - 1, ")") - 1;
5036                     break;
5037                 }
5038                 if (isIdentifier(a, start, len))
5039                 {
5040                     i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL ", j, ")") - 1;
5041                     break;
5042                 }
5043                 if (isKeyword(start, len))
5044                 {
5045                     i = buf.bracket(i, "$(DDOC_AUTO_KEYWORD ", j, ")") - 1;
5046                     break;
5047                 }
5048                 if (isFunctionParameter(a, start, len))
5049                 {
5050                     //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5051                     i = buf.bracket(i, "$(DDOC_AUTO_PARAM ", j, ")") - 1;
5052                     break;
5053                 }
5054                 i = j - 1;
5055             }
5056             break;
5057         }
5058     }
5059 
5060     if (inCode == '-')
5061         error(loc, "unmatched `---` in DDoc comment");
5062     else if (inCode)
5063         buf.insert(buf.length, ")");
5064 
5065     size_t i = buf.length;
5066     if (headingLevel)
5067     {
5068         endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
5069         removeBlankLineMacro(buf, iPrecedingBlankLine, i);
5070     }
5071     i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
5072     i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
5073     endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
5074 }
5075 
5076 /**************************************************
5077  * Highlight code for DDOC section.
5078  */
5079 private void highlightCode(Scope* sc, Dsymbol s, ref OutBuffer buf, size_t offset)
5080 {
5081     auto imp = s.isImport();
5082     if (imp && imp.aliases.dim > 0)
5083     {
5084         // For example: `public import core.stdc.string : memcpy, memcmp;`
5085         for(int i = 0; i < imp.aliases.dim; i++)
5086         {
5087             // Need to distinguish between
5088             // `public import core.stdc.string : memcpy, memcmp;` and
5089             // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
5090             auto a = imp.aliases[i];
5091             auto id = a ? a : imp.names[i];
5092             auto loc = Loc.init;
5093             if (auto symFromId = sc.search(loc, id, null))
5094             {
5095                 highlightCode(sc, symFromId, buf, offset);
5096             }
5097         }
5098     }
5099     else
5100     {
5101         OutBuffer ancbuf;
5102         emitAnchor(ancbuf, s, sc);
5103         buf.insert(offset, ancbuf[]);
5104         offset += ancbuf.length;
5105 
5106         Dsymbols a;
5107         a.push(s);
5108         highlightCode(sc, &a, buf, offset);
5109     }
5110 }
5111 
5112 /****************************************************
5113  */
5114 private void highlightCode(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
5115 {
5116     //printf("highlightCode(a = '%s')\n", a.toChars());
5117     bool resolvedTemplateParameters = false;
5118 
5119     for (size_t i = offset; i < buf.length; i++)
5120     {
5121         char c = buf[i];
5122         const se = sc._module.escapetable.escapeChar(c);
5123         if (se.length)
5124         {
5125             buf.remove(i, 1);
5126             i = buf.insert(i, se);
5127             i--; // point to ';'
5128             continue;
5129         }
5130         char* start = cast(char*)buf[].ptr + i;
5131         if (isIdStart(start))
5132         {
5133             size_t j = skipPastIdentWithDots(buf, i);
5134             if (i < j)
5135             {
5136                 size_t len = j - i;
5137                 if (isIdentifier(a, start, len))
5138                 {
5139                     i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
5140                     continue;
5141                 }
5142             }
5143 
5144             j = skippastident(buf, i);
5145             if (i < j)
5146             {
5147                 size_t len = j - i;
5148                 if (isIdentifier(a, start, len))
5149                 {
5150                     i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
5151                     continue;
5152                 }
5153                 if (isFunctionParameter(a, start, len))
5154                 {
5155                     //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5156                     i = buf.bracket(i, "$(DDOC_PARAM ", j, ")") - 1;
5157                     continue;
5158                 }
5159                 i = j - 1;
5160             }
5161         }
5162         else if (!resolvedTemplateParameters)
5163         {
5164             size_t previ = i;
5165 
5166             // hunt for template declarations:
5167             foreach (symi; 0 .. a.dim)
5168             {
5169                 FuncDeclaration fd = (*a)[symi].isFuncDeclaration();
5170 
5171                 if (!fd || !fd.parent || !fd.parent.isTemplateDeclaration())
5172                 {
5173                     continue;
5174                 }
5175 
5176                 TemplateDeclaration td = fd.parent.isTemplateDeclaration();
5177 
5178                 // build the template parameters
5179                 Array!(size_t) paramLens;
5180                 paramLens.reserve(td.parameters.dim);
5181 
5182                 OutBuffer parametersBuf;
5183                 HdrGenState hgs;
5184 
5185                 parametersBuf.writeByte('(');
5186 
5187                 foreach (parami; 0 .. td.parameters.dim)
5188                 {
5189                     TemplateParameter tp = (*td.parameters)[parami];
5190 
5191                     if (parami)
5192                         parametersBuf.writestring(", ");
5193 
5194                     size_t lastOffset = parametersBuf.length;
5195 
5196                     .toCBuffer(tp, &parametersBuf, &hgs);
5197 
5198                     paramLens[parami] = parametersBuf.length - lastOffset;
5199                 }
5200                 parametersBuf.writeByte(')');
5201 
5202                 const templateParams = parametersBuf[];
5203 
5204                 //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start);
5205                 if (start[0 .. templateParams.length] == templateParams)
5206                 {
5207                     immutable templateParamListMacro = "$(DDOC_TEMPLATE_PARAM_LIST ";
5208                     buf.bracket(i, templateParamListMacro.ptr, i + templateParams.length, ")");
5209 
5210                     // We have the parameter list. While we're here we might
5211                     // as well wrap the parameters themselves as well
5212 
5213                     // + 1 here to take into account the opening paren of the
5214                     // template param list
5215                     i += templateParamListMacro.length + 1;
5216 
5217                     foreach (const len; paramLens)
5218                     {
5219                         i = buf.bracket(i, "$(DDOC_TEMPLATE_PARAM ", i + len, ")");
5220                         // increment two here for space + comma
5221                         i += 2;
5222                     }
5223 
5224                     resolvedTemplateParameters = true;
5225                     // reset i to be positioned back before we found the template
5226                     // param list this assures that anything within the template
5227                     // param list that needs to be escaped or otherwise altered
5228                     // has an opportunity for that to happen outside of this context
5229                     i = previ;
5230 
5231                     continue;
5232                 }
5233             }
5234         }
5235     }
5236 }
5237 
5238 /****************************************
5239  */
5240 private void highlightCode3(Scope* sc, ref OutBuffer buf, const(char)* p, const(char)* pend)
5241 {
5242     for (; p < pend; p++)
5243     {
5244         const se = sc._module.escapetable.escapeChar(*p);
5245         if (se.length)
5246             buf.writestring(se);
5247         else
5248             buf.writeByte(*p);
5249     }
5250 }
5251 
5252 /**************************************************
5253  * Highlight code for CODE section.
5254  */
5255 private void highlightCode2(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
5256 {
5257     import dmd.diagnostic : DefaultDiagnosticHandler;
5258 
5259     uint errorsave = global.startGagging();
5260 
5261     DefaultDiagnosticHandler diagnosticHandler;
5262     scope Lexer lex = new Lexer(null, cast(char*)buf[].ptr, 0, buf.length - 1, 0, 1, diagnosticHandler.diagnosticHandler);
5263     OutBuffer res;
5264     const(char)* lastp = cast(char*)buf[].ptr;
5265     //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr);
5266     res.reserve(buf.length);
5267     while (1)
5268     {
5269         Token tok;
5270         lex.scan(&tok);
5271         diagnosticHandler.report();
5272         highlightCode3(sc, res, lastp, tok.ptr);
5273         string highlight = null;
5274         switch (tok.value)
5275         {
5276         case TOK.identifier:
5277             {
5278                 if (!sc)
5279                     break;
5280                 size_t len = lex.p - tok.ptr;
5281                 if (isIdentifier(a, tok.ptr, len))
5282                 {
5283                     highlight = "$(D_PSYMBOL ";
5284                     break;
5285                 }
5286                 if (isFunctionParameter(a, tok.ptr, len))
5287                 {
5288                     //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5289                     highlight = "$(D_PARAM ";
5290                     break;
5291                 }
5292                 break;
5293             }
5294         case TOK.comment:
5295             highlight = "$(D_COMMENT ";
5296             break;
5297         case TOK.string_:
5298             highlight = "$(D_STRING ";
5299             break;
5300         default:
5301             if (tok.isKeyword())
5302                 highlight = "$(D_KEYWORD ";
5303             break;
5304         }
5305         if (highlight)
5306         {
5307             res.writestring(highlight);
5308             size_t o = res.length;
5309             highlightCode3(sc, res, tok.ptr, lex.p);
5310             if (tok.value == TOK.comment || tok.value == TOK.string_)
5311                 /* https://issues.dlang.org/show_bug.cgi?id=7656
5312                  * https://issues.dlang.org/show_bug.cgi?id=7715
5313                  * https://issues.dlang.org/show_bug.cgi?id=10519
5314                  */
5315                 escapeDdocString(&res, o);
5316             res.writeByte(')');
5317         }
5318         else
5319             highlightCode3(sc, res, tok.ptr, lex.p);
5320         if (tok.value == TOK.endOfFile)
5321             break;
5322         lastp = lex.p;
5323     }
5324     buf.setsize(offset);
5325     buf.write(&res);
5326     global.endGagging(errorsave);
5327 }
5328 
5329 /****************************************
5330  * Determine if p points to the start of a "..." parameter identifier.
5331  */
5332 private bool isCVariadicArg(const(char)[] p)
5333 {
5334     return p.length >= 3 && p[0 .. 3] == "...";
5335 }
5336 
5337 /****************************************
5338  * Determine if p points to the start of an identifier.
5339  */
5340 bool isIdStart(const(char)* p)
5341 {
5342     dchar c = *p;
5343     if (isalpha(c) || c == '_')
5344         return true;
5345     if (c >= 0x80)
5346     {
5347         size_t i = 0;
5348         if (utf_decodeChar(p[0 .. 4], i, c))
5349             return false; // ignore errors
5350         if (isUniAlpha(c))
5351             return true;
5352     }
5353     return false;
5354 }
5355 
5356 /****************************************
5357  * Determine if p points to the rest of an identifier.
5358  */
5359 bool isIdTail(const(char)* p)
5360 {
5361     dchar c = *p;
5362     if (isalnum(c) || c == '_')
5363         return true;
5364     if (c >= 0x80)
5365     {
5366         size_t i = 0;
5367         if (utf_decodeChar(p[0 .. 4], i, c))
5368             return false; // ignore errors
5369         if (isUniAlpha(c))
5370             return true;
5371     }
5372     return false;
5373 }
5374 
5375 /****************************************
5376  * Determine if p points to the indentation space.
5377  */
5378 private bool isIndentWS(const(char)* p)
5379 {
5380     return (*p == ' ') || (*p == '\t');
5381 }
5382 
5383 /*****************************************
5384  * Return number of bytes in UTF character.
5385  */
5386 int utfStride(const(char)* p)
5387 {
5388     dchar c = *p;
5389     if (c < 0x80)
5390         return 1;
5391     size_t i = 0;
5392     utf_decodeChar(p[0 .. 4], i, c); // ignore errors, but still consume input
5393     return cast(int)i;
5394 }
5395 
5396 private inout(char)* stripLeadingNewlines(inout(char)* s)
5397 {
5398     while (s && *s == '\n' || *s == '\r')
5399         s++;
5400 
5401     return s;
5402 }