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