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