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