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