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