/**
 * Ddoc documentation generation.
 *
 * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator)
 *
 * Copyright:   Copyright (C) 1999-2024 by The D Language Foundation, All Rights Reserved
 * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/doc.d, _doc.d)
 * Documentation:  https://dlang.org/phobos/dmd_doc.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/doc.d
 */

module dmd.doc;

import core.stdc.ctype;
import core.stdc.stdlib;
import core.stdc.stdio;
import core.stdc.string;
import core.stdc.time;
import dmd.aggregate;
import dmd.arraytypes;
import dmd.astenums;
import dmd.attrib;
import dmd.cond;
import dmd.dclass;
import dmd.declaration;
import dmd.denum;
import dmd.dimport;
import dmd.dmacro;
import dmd.dmodule;
import dmd.dscope;
import dmd.dstruct;
import dmd.dsymbol;
import dmd.dsymbolsem;
import dmd.dtemplate;
import dmd.errorsink;
import dmd.func;
import dmd.globals;
import dmd.hdrgen;
import dmd.id;
import dmd.identifier;
import dmd.lexer;
import dmd.location;
import dmd.mtype;
import dmd.root.array;
import dmd.root.file;
import dmd.root.filename;
import dmd.common.outbuffer;
import dmd.root.port;
import dmd.root.rmem;
import dmd.root.string;
import dmd.root.utf;
import dmd.tokens;
import dmd.visitor;

private:

public
struct Escape
{
    const(char)[][char.max] strings;

    /***************************************
     * Find character string to replace c with.
     */
    const(char)[] escapeChar(char c) @safe
    {
        version (all)
        {
            //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr);
            return strings[c];
        }
        else
        {
            const(char)[] s;
            switch (c)
            {
            case '<':
                s = "&lt;";
                break;
            case '>':
                s = "&gt;";
                break;
            case '&':
                s = "&amp;";
                break;
            default:
                s = null;
                break;
            }
            return s;
        }
    }
}

/***********************************************************
 */
class Section
{
    const(char)[] name;
    const(char)[] body_;
    int nooutput;

    override string toString() const
    {
        assert(0);
    }

    void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
    {
        assert(a.length);
        if (name.length)
        {
            static immutable table =
            [
                "AUTHORS",
                "BUGS",
                "COPYRIGHT",
                "DATE",
                "DEPRECATED",
                "EXAMPLES",
                "HISTORY",
                "LICENSE",
                "RETURNS",
                "SEE_ALSO",
                "STANDARDS",
                "THROWS",
                "VERSION",
            ];
            foreach (entry; table)
            {
                if (iequals(entry, name))
                {
                    buf.printf("$(DDOC_%s ", entry.ptr);
                    goto L1;
                }
            }
            buf.writestring("$(DDOC_SECTION ");
            // Replace _ characters with spaces
            buf.writestring("$(DDOC_SECTION_H ");
            size_t o = buf.length;
            foreach (char c; name)
                buf.writeByte((c == '_') ? ' ' : c);
            escapeStrayParenthesis(loc, buf, o, false, sc.eSink);
            buf.writestring(")");
        }
        else
        {
            buf.writestring("$(DDOC_DESCRIPTION ");
        }
    L1:
        size_t o = buf.length;
        buf.write(body_);
        escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
        highlightText(sc, a, loc, buf, o);
        buf.writestring(")");
    }
}

/***********************************************************
 */
final class ParamSection : Section
{
    override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
    {
        assert(a.length);
        Dsymbol s = (*a)[0]; // test
        const(char)* p = body_.ptr;
        size_t len = body_.length;
        const(char)* pend = p + len;
        const(char)* tempstart = null;
        size_t templen = 0;
        const(char)* namestart = null;
        size_t namelen = 0; // !=0 if line continuation
        const(char)* textstart = null;
        size_t textlen = 0;
        size_t paramcount = 0;
        buf.writestring("$(DDOC_PARAMS ");
        while (p < pend)
        {
            // Skip to start of macro
            while (1)
            {
                switch (*p)
                {
                case ' ':
                case '\t':
                    p++;
                    continue;
                case '\n':
                    p++;
                    goto Lcont;
                default:
                    if (isIdStart(p) || isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
                        break;
                    if (namelen)
                        goto Ltext;
                    // continuation of prev macro
                    goto Lskipline;
                }
                break;
            }
            tempstart = p;
            while (isIdTail(p))
                p += utfStride(p);
            if (isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
                p += 3;
            templen = p - tempstart;
            while (*p == ' ' || *p == '\t')
                p++;
            if (*p != '=')
            {
                if (namelen)
                    goto Ltext;
                // continuation of prev macro
                goto Lskipline;
            }
            p++;
            if (namelen)
            {
                // Output existing param
            L1:
                //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
                ++paramcount;
                HdrGenState hgs;
                buf.writestring("$(DDOC_PARAM_ROW ");
                {
                    buf.writestring("$(DDOC_PARAM_ID ");
                    {
                        size_t o = buf.length;
                        Parameter fparam = isFunctionParameter(a, namestart[0 .. namelen]);
                        if (!fparam)
                        {
                            // Comments on a template might refer to function parameters within.
                            // Search the parameters of nested eponymous functions (with the same name.)
                            fparam = isEponymousFunctionParameter(a, namestart[0 ..  namelen]);
                        }
                        bool isCVariadic = isCVariadicParameter(a, namestart[0 .. namelen]);
                        if (isCVariadic)
                        {
                            buf.writestring("...");
                        }
                        else if (fparam && fparam.type && fparam.ident)
                        {
                            toCBuffer(fparam.type, buf, fparam.ident, hgs);
                        }
                        else
                        {
                            if (isTemplateParameter(a, namestart, namelen))
                            {
                                // 10236: Don't count template parameters for params check
                                --paramcount;
                            }
                            else if (!fparam)
                            {
                                sc.eSink.warning(s.loc, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen, namestart);
                            }
                            buf.write(namestart[0 .. namelen]);
                        }
                        escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
                        highlightCode(sc, a, buf, o);
                    }
                    buf.writestring(")");
                    buf.writestring("$(DDOC_PARAM_DESC ");
                    {
                        size_t o = buf.length;
                        buf.write(textstart[0 .. textlen]);
                        escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
                        highlightText(sc, a, loc, buf, o);
                    }
                    buf.writestring(")");
                }
                buf.writestring(")");
                namelen = 0;
                if (p >= pend)
                    break;
            }
            namestart = tempstart;
            namelen = templen;
            while (*p == ' ' || *p == '\t')
                p++;
            textstart = p;
        Ltext:
            while (*p != '\n')
                p++;
            textlen = p - textstart;
            p++;
        Lcont:
            continue;
        Lskipline:
            // Ignore this line
            while (*p++ != '\n')
            {
            }
        }
        if (namelen)
            goto L1;
        // write out last one
        buf.writestring(")");
        TypeFunction tf = a.length == 1 ? isTypeFunction(s) : null;
        if (tf)
        {
            size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.length : 0) +
                            cast(int)(tf.parameterList.varargs == VarArg.variadic);
            if (pcount != paramcount)
            {
                sc.eSink.warning(s.loc, "Ddoc: parameter count mismatch, expected %llu, got %llu",
                        cast(ulong) pcount, cast(ulong) paramcount);
                if (paramcount == 0)
                {
                    // Chances are someone messed up the format
                    sc.eSink.warningSupplemental(s.loc, "Note that the format is `param = description`");
                }
            }
        }
    }
}

/***********************************************************
 */
final class MacroSection : Section
{
    override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
    {
        //printf("MacroSection::write()\n");
        DocComment.parseMacros(dc.escapetable, *dc.pmacrotable, body_);
    }
}

alias Sections = Array!(Section);

// Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one).
bool isCVariadicParameter(Dsymbols* a, const(char)[] p) @safe
{
    foreach (member; *a)
    {
        TypeFunction tf = isTypeFunction(member);
        if (tf && tf.parameterList.varargs == VarArg.variadic && p == "...")
            return true;
    }
    return false;
}

Dsymbol getEponymousMember(TemplateDeclaration td) @safe
{
    if (!td.onemember)
        return null;
    if (AggregateDeclaration ad = td.onemember.isAggregateDeclaration())
        return ad;
    if (FuncDeclaration fd = td.onemember.isFuncDeclaration())
        return fd;
    if (auto em = td.onemember.isEnumMember())
        return null;    // Keep backward compatibility. See compilable/ddoc9.d
    if (VarDeclaration vd = td.onemember.isVarDeclaration())
        return td.constraint ? null : vd;
    return null;
}

TemplateDeclaration getEponymousParent(Dsymbol s) @safe
{
    if (!s.parent)
        return null;
    TemplateDeclaration td = s.parent.isTemplateDeclaration();
    return (td && getEponymousMember(td)) ? td : null;
}

immutable ddoc_default = import("default_ddoc_theme." ~ ddoc_ext);
immutable ddoc_decl_s = "$(DDOC_DECL ";
immutable ddoc_decl_e = ")\n";
immutable ddoc_decl_dd_s = "$(DDOC_DECL_DD ";
immutable ddoc_decl_dd_e = ")\n";

/****************************************************
 * Generate Ddoc file for Module m.
 * Params:
 *      m = Module
 *      ddoctext_ptr = combined text of .ddoc files for macro definitions
 *      ddoctext_length = extant of ddoctext_ptr
 *      datetime = charz returned by ctime()
 *      eSink = send error messages to eSink
 *      outbuf = append the Ddoc text to this
 */
public
void gendocfile(Module m, const char* ddoctext_ptr, size_t ddoctext_length, const char* datetime, ErrorSink eSink, ref OutBuffer outbuf)
{
    gendocfile(m, ddoctext_ptr[0 .. ddoctext_length], datetime, eSink, outbuf);
}

/****************************************************
 * Generate Ddoc text for Module `m` and append it to `outbuf`.
 * Params:
 *      m = Module
 *      ddoctext = combined text of .ddoc files for macro definitions
 *      datetime = charz returned by ctime()
 *      eSink = send error messages to eSink
 *      outbuf = append the Ddoc text to this
 */
public
void gendocfile(Module m, const char[] ddoctext, const char* datetime, ErrorSink eSink, ref OutBuffer outbuf)
{
    // Load internal default macros first
    DocComment.parseMacros(m.escapetable, m.macrotable, ddoc_default[]);

    // Ddoc files override default macros
    DocComment.parseMacros(m.escapetable, m.macrotable, ddoctext);

    Scope* sc = Scope.createGlobal(m, eSink); // create root scope
    DocComment* dc = DocComment.parse(m, m.comment);
    dc.pmacrotable = &m.macrotable;
    dc.escapetable = m.escapetable;
    sc.lastdc = dc;
    // Generate predefined macros
    // Set the title to be the name of the module
    {
        const p = m.toPrettyChars().toDString;
        m.macrotable.define("TITLE", p);
    }
    // Set time macros
    m.macrotable.define("DATETIME", datetime[0 .. 26]);
    m.macrotable.define("YEAR", datetime[20 .. 20 + 4]);

    const srcfilename = m.srcfile.toString();
    m.macrotable.define("SRCFILENAME", srcfilename);
    const docfilename = m.docfile.toString();
    m.macrotable.define("DOCFILENAME", docfilename);
    if (dc.copyright)
    {
        dc.copyright.nooutput = 1;
        m.macrotable.define("COPYRIGHT", dc.copyright.body_);
    }

    OutBuffer buf;
    if (m.filetype == FileType.ddoc)
    {
        const ploc = m.md ? &m.md.loc : &m.loc;
        Loc loc = *ploc;
        if (!loc.filename)
            loc.filename = srcfilename.ptr;

        size_t commentlen = m.comment ? strlen(cast(char*)m.comment) : 0;
        Dsymbols a;
        // https://issues.dlang.org/show_bug.cgi?id=9764
        // Don't push m in a, to prevent emphasize ddoc file name.
        if (dc.macros)
        {
            commentlen = dc.macros.name.ptr - m.comment;
            dc.macros.write(loc, dc, sc, &a, buf);
        }
        buf.write(m.comment[0 .. commentlen]);
        highlightText(sc, &a, loc, buf, 0);
    }
    else
    {
        Dsymbols a;
        a.push(m);
        dc.writeSections(sc, &a, buf);
        emitMemberComments(m, buf, sc);
    }
    //printf("BODY= '%.*s'\n", cast(int)buf.length, buf.data);
    m.macrotable.define("BODY", buf[]);

    OutBuffer buf2;
    buf2.writestring("$(DDOC)");
    size_t end = buf2.length;

    // Expand buf in place with macro expansions
    const success = m.macrotable.expand(buf2, 0, end, null, global.recursionLimit, &isIdStart, &isIdTail);
    if (!success)
        eSink.error(Loc.initial, "DDoc macro expansion limit exceeded; more than %d expansions.", global.recursionLimit);

    /* Remove all the escape sequences from buf,
     * and make CR-LF the newline.
     */
    const slice = buf2[];
    outbuf.reserve(slice.length);
    auto p = slice.ptr;
    for (size_t j = 0; j < slice.length; j++)
    {
        char c = p[j];
        if (c == 0xFF && j + 1 < slice.length)
        {
            j++;
            continue;
        }
        if (c == '\n')
            outbuf.writeByte('\r');
        else if (c == '\r')
        {
            outbuf.writestring("\r\n");
            if (j + 1 < slice.length && p[j + 1] == '\n')
            {
                j++;
            }
            continue;
        }
        outbuf.writeByte(c);
    }
}

/****************************************************
 * Having unmatched parentheses can hose the output of Ddoc,
 * as the macros depend on properly nested parentheses.
 * This function replaces all ( with $(LPAREN) and ) with $(RPAREN)
 * to preserve text literally. This also means macros in the
 * text won't be expanded.
 */
public
void escapeDdocString(ref OutBuffer buf, size_t start)
{
    for (size_t u = start; u < buf.length; u++)
    {
        char c = buf[u];
        switch (c)
        {
        case '$':
            buf.remove(u, 1);
            buf.insert(u, "$(DOLLAR)");
            u += 8;
            break;
        case '(':
            buf.remove(u, 1); //remove the (
            buf.insert(u, "$(LPAREN)"); //insert this instead
            u += 8; //skip over newly inserted macro
            break;
        case ')':
            buf.remove(u, 1); //remove the )
            buf.insert(u, "$(RPAREN)"); //insert this instead
            u += 8; //skip over newly inserted macro
            break;
        default:
            break;
        }
    }
}

/****************************************************
 * Having unmatched parentheses can hose the output of Ddoc,
 * as the macros depend on properly nested parentheses.
 *
 * Fix by replacing unmatched ( with $(LPAREN) and unmatched ) with $(RPAREN).
 *
 * Params:
 *  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.
 *  buf   = an OutBuffer containing the DDoc
 *  start = the index within buf to start replacing unmatched parentheses
 *  respectBackslashEscapes = if true, always replace parentheses that are
 *    directly preceeded by a backslash with $(LPAREN) or $(RPAREN) instead of
 *    counting them as stray parentheses
 */
private void escapeStrayParenthesis(Loc loc, ref OutBuffer buf, size_t start, bool respectBackslashEscapes, ErrorSink eSink)
{
    uint par_open = 0;
    char inCode = 0;
    bool atLineStart = true;
    for (size_t u = start; u < buf.length; u++)
    {
        char c = buf[u];
        switch (c)
        {
        case '(':
            if (!inCode)
                par_open++;
            atLineStart = false;
            break;
        case ')':
            if (!inCode)
            {
                if (par_open == 0)
                {
                    //stray ')'
                    eSink.warning(loc, "Ddoc: Stray ')'. This may cause incorrect Ddoc output. Use $(RPAREN) instead for unpaired right parentheses.");
                    buf.remove(u, 1); //remove the )
                    buf.insert(u, "$(RPAREN)"); //insert this instead
                    u += 8; //skip over newly inserted macro
                }
                else
                    par_open--;
            }
            atLineStart = false;
            break;
        case '\n':
            atLineStart = true;
            version (none)
            {
                // For this to work, loc must be set to the beginning of the passed
                // text which is currently not possible
                // (loc is set to the Loc of the Dsymbol)
                loc.linnum++;
            }
            break;
        case ' ':
        case '\r':
        case '\t':
            break;
        case '-':
        case '`':
        case '~':
            // Issue 15465: don't try to escape unbalanced parens inside code
            // blocks.
            int numdash = 1;
            for (++u; u < buf.length && buf[u] == c; ++u)
                ++numdash;
            --u;
            if (c == '`' || (atLineStart && numdash >= 3))
            {
                if (inCode == c)
                    inCode = 0;
                else if (!inCode)
                    inCode = c;
            }
            atLineStart = false;
            break;
        case '\\':
            // replace backslash-escaped parens with their macros
            if (!inCode && respectBackslashEscapes && u+1 < buf.length)
            {
                if (buf[u+1] == '(' || buf[u+1] == ')')
                {
                    const paren = buf[u+1] == '(' ? "$(LPAREN)" : "$(RPAREN)";
                    buf.remove(u, 2); //remove the \)
                    buf.insert(u, paren); //insert this instead
                    u += 8; //skip over newly inserted macro
                }
                else if (buf[u+1] == '\\')
                    ++u;
            }
            break;
        default:
            atLineStart = false;
            break;
        }
    }
    if (par_open) // if any unmatched lparens
    {
        par_open = 0;
        for (size_t u = buf.length; u > start;)
        {
            u--;
            char c = buf[u];
            switch (c)
            {
            case ')':
                par_open++;
                break;
            case '(':
                if (par_open == 0)
                {
                    //stray '('
                    eSink.warning(loc, "Ddoc: Stray '('. This may cause incorrect Ddoc output. Use $(LPAREN) instead for unpaired left parentheses.");
                    buf.remove(u, 1); //remove the (
                    buf.insert(u, "$(LPAREN)"); //insert this instead
                }
                else
                    par_open--;
                break;
            default:
                break;
            }
        }
    }
}

// Basically, this is to skip over things like private{} blocks in a struct or
// class definition that don't add any components to the qualified name.
Scope* skipNonQualScopes(Scope* sc) @safe
{
    while (sc && !sc.scopesym)
        sc = sc.enclosing;
    return sc;
}

bool emitAnchorName(ref OutBuffer buf, Dsymbol s, Scope* sc, bool includeParent)
{
    if (!s || s.isPackage() || s.isModule())
        return false;
    // Add parent names first
    bool dot = false;
    auto eponymousParent = getEponymousParent(s);
    if (includeParent && s.parent || eponymousParent)
        dot = emitAnchorName(buf, s.parent, sc, includeParent);
    else if (includeParent && sc)
        dot = emitAnchorName(buf, sc.scopesym, skipNonQualScopes(sc.enclosing), includeParent);
    // Eponymous template members can share the parent anchor name
    if (eponymousParent)
        return dot;
    if (dot)
        buf.writeByte('.');
    // Use "this" not "__ctor"
    TemplateDeclaration td;
    if (s.isCtorDeclaration() || ((td = s.isTemplateDeclaration()) !is null && td.onemember && td.onemember.isCtorDeclaration()))
    {
        buf.writestring("this");
    }
    else
    {
        /* We just want the identifier, not overloads like TemplateDeclaration::toChars.
         * We don't want the template parameter list and constraints. */
        buf.writestring(s.Dsymbol.toChars());
    }
    return true;
}

void emitAnchor(ref OutBuffer buf, Dsymbol s, Scope* sc, bool forHeader = false)
{
    Identifier ident;
    {
        OutBuffer anc;
        emitAnchorName(anc, s, skipNonQualScopes(sc), true);
        ident = Identifier.idPool(anc[]);
    }

    auto pcount = cast(void*)ident in sc.anchorCounts;
    typeof(*pcount) count;
    if (!forHeader)
    {
        if (pcount)
        {
            // Existing anchor,
            // don't write an anchor for matching consecutive ditto symbols
            TemplateDeclaration td = getEponymousParent(s);
            if (sc.prevAnchor == ident && sc.lastdc && (isDitto(s.comment) || (td && isDitto(td.comment))))
                return;

            count = ++*pcount;
        }
        else
        {
            sc.anchorCounts[cast(void*)ident] = 1;
            count = 1;
        }
    }

    // cache anchor name
    sc.prevAnchor = ident;
    auto macroName = forHeader ? "DDOC_HEADER_ANCHOR" : "DDOC_ANCHOR";

    if (auto imp = s.isImport())
    {
        // For example: `public import core.stdc.string : memcpy, memcmp;`
        if (imp.aliases.length > 0)
        {
            for(int i = 0; i < imp.aliases.length; i++)
            {
                // Need to distinguish between
                // `public import core.stdc.string : memcpy, memcmp;` and
                // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
                auto a = imp.aliases[i];
                auto id = a ? a : imp.names[i];
                auto loc = Loc.init;
                Dsymbol pscopesym;
                if (auto symFromId = sc.search(loc, id, pscopesym))
                {
                    emitAnchor(buf, symFromId, sc, forHeader);
                }
            }
        }
        else
        {
            // For example: `public import str = core.stdc.string;`
            if (imp.aliasId)
            {
                auto symbolName = imp.aliasId.toString();

                buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
                    cast(int) symbolName.length, symbolName.ptr);

                if (forHeader)
                {
                    buf.printf(", %.*s", cast(int) symbolName.length, symbolName.ptr);
                }
            }
            else
            {
                // The general case:  `public import core.stdc.string;`

                // fully qualify imports so `core.stdc.string` doesn't appear as `core`
                void printFullyQualifiedImport()
                {
                    foreach (const pid; imp.packages)
                    {
                        buf.printf("%s.", pid.toChars());
                    }
                    buf.writestring(imp.id.toString());
                }

                buf.printf("$(%.*s ", cast(int) macroName.length, macroName.ptr);
                printFullyQualifiedImport();

                if (forHeader)
                {
                    buf.printf(", ");
                    printFullyQualifiedImport();
                }
            }

            buf.writeByte(')');
        }
    }
    else
    {
        auto symbolName = ident.toString();
        buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
            cast(int) symbolName.length, symbolName.ptr);

        // only append count once there's a duplicate
        if (count > 1)
            buf.printf(".%u", count);

        if (forHeader)
        {
            Identifier shortIdent;
            {
                OutBuffer anc;
                emitAnchorName(anc, s, skipNonQualScopes(sc), false);
                shortIdent = Identifier.idPool(anc[]);
            }

            auto shortName = shortIdent.toString();
            buf.printf(", %.*s", cast(int) shortName.length, shortName.ptr);
        }

        buf.writeByte(')');
    }
}

/******************************* emitComment **********************************/

/** Get leading indentation from 'src' which represents lines of code. */
size_t getCodeIndent(const(char)* src)
{
    while (src && (*src == '\r' || *src == '\n'))
        ++src; // skip until we find the first non-empty line
    size_t codeIndent = 0;
    while (src && (*src == ' ' || *src == '\t'))
    {
        codeIndent++;
        src++;
    }
    return codeIndent;
}

/** Recursively expand template mixin member docs into the scope. */
void expandTemplateMixinComments(TemplateMixin tm, ref OutBuffer buf, Scope* sc)
{
    if (!tm.semanticRun)
        tm.dsymbolSemantic(sc);
    TemplateDeclaration td = (tm && tm.tempdecl) ? tm.tempdecl.isTemplateDeclaration() : null;
    if (td && td.members)
    {
        for (size_t i = 0; i < td.members.length; i++)
        {
            Dsymbol sm = (*td.members)[i];
            TemplateMixin tmc = sm.isTemplateMixin();
            if (tmc && tmc.comment)
                expandTemplateMixinComments(tmc, buf, sc);
            else
                emitComment(sm, buf, sc);
        }
    }
}

void emitMemberComments(ScopeDsymbol sds, ref OutBuffer buf, Scope* sc)
{
    if (!sds.members)
        return;
    //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars());
    const(char)[] m = "$(DDOC_MEMBERS ";
    if (sds.isTemplateDeclaration())
        m = "$(DDOC_TEMPLATE_MEMBERS ";
    else if (sds.isClassDeclaration())
        m = "$(DDOC_CLASS_MEMBERS ";
    else if (sds.isStructDeclaration())
        m = "$(DDOC_STRUCT_MEMBERS ";
    else if (sds.isEnumDeclaration())
        m = "$(DDOC_ENUM_MEMBERS ";
    else if (sds.isModule())
        m = "$(DDOC_MODULE_MEMBERS ";
    size_t offset1 = buf.length; // save starting offset
    buf.writestring(m);
    size_t offset2 = buf.length; // to see if we write anything
    sc = sc.push(sds);
    for (size_t i = 0; i < sds.members.length; i++)
    {
        Dsymbol s = (*sds.members)[i];
        //printf("\ts = '%s'\n", s.toChars());
        // only expand if parent is a non-template (semantic won't work)
        if (s.comment && s.isTemplateMixin() && s.parent && !s.parent.isTemplateDeclaration())
            expandTemplateMixinComments(cast(TemplateMixin)s, buf, sc);
        emitComment(s, buf, sc);
    }
    emitComment(null, buf, sc);
    sc.pop();
    if (buf.length == offset2)
    {
        /* Didn't write out any members, so back out last write
         */
        buf.setsize(offset1);
    }
    else
        buf.writestring(")");
}

void emitVisibility(ref OutBuffer buf, Import i)
{
    // imports are private by default, which is different from other declarations
    // so they should explicitly show their visibility
    emitVisibility(buf, i.visibility);
}

void emitVisibility(ref OutBuffer buf, Declaration d)
{
    auto vis = d.visibility;
    if (vis.kind != Visibility.Kind.undefined && vis.kind != Visibility.Kind.public_)
    {
        emitVisibility(buf, vis);
    }
}

void emitVisibility(ref OutBuffer buf, Visibility vis)
{
    visibilityToBuffer(buf, vis);
    buf.writeByte(' ');
}

void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc)
{
    extern (C++) final class EmitComment : Visitor
    {
        alias visit = Visitor.visit;
    public:
        OutBuffer* buf;
        Scope* sc;

        extern (D) this(ref OutBuffer buf, Scope* sc) scope
        {
            this.buf = &buf;
            this.sc = sc;
        }

        override void visit(Dsymbol)
        {
        }

        override void visit(InvariantDeclaration)
        {
        }

        override void visit(UnitTestDeclaration)
        {
        }

        override void visit(PostBlitDeclaration)
        {
        }

        override void visit(DtorDeclaration)
        {
        }

        override void visit(StaticCtorDeclaration)
        {
        }

        override void visit(StaticDtorDeclaration)
        {
        }

        override void visit(TypeInfoDeclaration)
        {
        }

        void emit(Scope* sc, Dsymbol s, const(char)* com)
        {
            if (s && sc.lastdc && isDitto(com))
            {
                sc.lastdc.a.push(s);
                return;
            }
            // Put previous doc comment if exists
            if (DocComment* dc = sc.lastdc)
            {
                assert(dc.a.length > 0, "Expects at least one declaration for a" ~
                    "documentation comment");

                auto symbol = dc.a[0];

                buf.writestring("$(DDOC_MEMBER");
                buf.writestring("$(DDOC_MEMBER_HEADER");
                emitAnchor(*buf, symbol, sc, true);
                buf.writeByte(')');

                // Put the declaration signatures as the document 'title'
                buf.writestring(ddoc_decl_s);
                for (size_t i = 0; i < dc.a.length; i++)
                {
                    Dsymbol sx = dc.a[i];
                    // the added linebreaks in here make looking at multiple
                    // signatures more appealing
                    if (i == 0)
                    {
                        size_t o = buf.length;
                        toDocBuffer(sx, *buf, sc);
                        highlightCode(sc, sx, *buf, o);
                        buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
                        continue;
                    }
                    buf.writestring("$(DDOC_DITTO ");
                    {
                        size_t o = buf.length;
                        toDocBuffer(sx, *buf, sc);
                        highlightCode(sc, sx, *buf, o);
                    }
                    buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
                    buf.writeByte(')');
                }
                buf.writestring(ddoc_decl_e);
                // Put the ddoc comment as the document 'description'
                buf.writestring(ddoc_decl_dd_s);
                {
                    dc.writeSections(sc, &dc.a, *buf);
                    if (ScopeDsymbol sds = dc.a[0].isScopeDsymbol())
                        emitMemberComments(sds, *buf, sc);
                }
                buf.writestring(ddoc_decl_dd_e);
                buf.writeByte(')');
                //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0);
            }
            if (s)
            {
                DocComment* dc = DocComment.parse(s, com);
                dc.pmacrotable = &sc._module.macrotable;
                sc.lastdc = dc;
            }
        }

        override void visit(Import imp)
        {
            if (imp.visible().kind != Visibility.Kind.public_ && sc.visibility.kind != Visibility.Kind.export_)
                return;

            if (imp.comment)
                emit(sc, imp, imp.comment);
        }

        override void visit(Declaration d)
        {
            //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment);
            //printf("type = %p\n", d.type);
            const(char)* com = d.comment;
            if (TemplateDeclaration td = getEponymousParent(d))
            {
                if (isDitto(td.comment))
                    com = td.comment;
                else
                    com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
            }
            else
            {
                if (!d.ident)
                    return;
                if (!d.type)
                {
                    if (!d.isCtorDeclaration() &&
                        !d.isAliasDeclaration() &&
                        !d.isVarDeclaration())
                    {
                        return;
                    }
                }
                if (d.visibility.kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
                    return;
            }
            if (!com)
                return;
            emit(sc, d, com);
        }

        override void visit(AggregateDeclaration ad)
        {
            //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars());
            const(char)* com = ad.comment;
            if (TemplateDeclaration td = getEponymousParent(ad))
            {
                if (isDitto(td.comment))
                    com = td.comment;
                else
                    com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
            }
            else
            {
                if (ad.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
                    return;
                if (!ad.comment)
                    return;
            }
            if (!com)
                return;
            emit(sc, ad, com);
        }

        override void visit(TemplateDeclaration td)
        {
            //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind());
            if (td.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
                return;
            if (!td.comment)
                return;
            if (Dsymbol ss = getEponymousMember(td))
            {
                ss.accept(this);
                return;
            }
            emit(sc, td, td.comment);
        }

        override void visit(EnumDeclaration ed)
        {
            if (ed.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
                return;
            if (ed.isAnonymous() && ed.members)
            {
                for (size_t i = 0; i < ed.members.length; i++)
                {
                    Dsymbol s = (*ed.members)[i];
                    emitComment(s, *buf, sc);
                }
                return;
            }
            if (!ed.comment)
                return;
            if (ed.isAnonymous())
                return;
            emit(sc, ed, ed.comment);
        }

        override void visit(EnumMember em)
        {
            //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment);
            if (em.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
                return;
            if (!em.comment)
                return;
            emit(sc, em, em.comment);
        }

        override void visit(AttribDeclaration ad)
        {
            //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);
            /* A general problem with this,
             * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516
             * is that attributes are not transmitted through to the underlying
             * member declarations for template bodies, because semantic analysis
             * is not done for template declaration bodies
             * (only template instantiations).
             * Hence, Ddoc omits attributes from template members.
             */
            Dsymbols* d = ad.include(null);
            if (d)
            {
                for (size_t i = 0; i < d.length; i++)
                {
                    Dsymbol s = (*d)[i];
                    //printf("AttribDeclaration::emitComment %s\n", s.toChars());
                    emitComment(s, *buf, sc);
                }
            }
        }

        override void visit(VisibilityDeclaration pd)
        {
            if (pd.decl)
            {
                Scope* scx = sc;
                sc = sc.copy();
                sc.visibility = pd.visibility;
                visit(cast(AttribDeclaration)pd);
                scx.lastdc = sc.lastdc;
                sc = sc.pop();
            }
        }

        override void visit(ConditionalDeclaration cd)
        {
            //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
            if (cd.condition.inc != Include.notComputed)
            {
                visit(cast(AttribDeclaration)cd);
                return;
            }
            /* If generating doc comment, be careful because if we're inside
             * a template, then include(null) will fail.
             */
            Dsymbols* d = cd.decl ? cd.decl : cd.elsedecl;
            for (size_t i = 0; i < d.length; i++)
            {
                Dsymbol s = (*d)[i];
                emitComment(s, *buf, sc);
            }
        }
    }

    scope EmitComment v = new EmitComment(buf, sc);
    if (!s)
        v.emit(sc, null, null);
    else
        s.accept(v);
}

void toDocBuffer(Dsymbol s, ref OutBuffer buf, Scope* sc)
{
    extern (C++) final class ToDocBuffer : Visitor
    {
        alias visit = Visitor.visit;
    public:
        OutBuffer* buf;
        Scope* sc;

        extern (D) this(ref OutBuffer buf, Scope* sc) scope
        {
            this.buf = &buf;
            this.sc = sc;
        }

        override void visit(Dsymbol s)
        {
            //printf("Dsymbol::toDocbuffer() %s\n", s.toChars());
            HdrGenState hgs;
            hgs.ddoc = true;
            toCBuffer(s, *buf, hgs);
        }

        void prefix(Dsymbol s)
        {
            if (s.isDeprecated())
                buf.writestring("deprecated ");
            if (Declaration d = s.isDeclaration())
            {
                emitVisibility(*buf, d);
                if (d.isStatic())
                    buf.writestring("static ");
                else if (d.isFinal())
                    buf.writestring("final ");
                else if (d.isAbstract())
                    buf.writestring("abstract ");

                if (d.isFuncDeclaration())      // functionToBufferFull handles this
                    return;

                if (d.isImmutable())
                    buf.writestring("immutable ");
                if (d.storage_class & STC.shared_)
                    buf.writestring("shared ");
                if (d.isWild())
                    buf.writestring("inout ");
                if (d.isConst())
                    buf.writestring("const ");

                if (d.isSynchronized())
                    buf.writestring("synchronized ");

                if (d.storage_class & STC.manifest)
                    buf.writestring("enum ");

                // Add "auto" for the untyped variable in template members
                if (!d.type && d.isVarDeclaration() &&
                    !d.isImmutable() && !(d.storage_class & STC.shared_) && !d.isWild() && !d.isConst() &&
                    !d.isSynchronized())
                {
                    buf.writestring("auto ");
                }
            }
        }

        override void visit(Import i)
        {
            HdrGenState hgs;
            hgs.ddoc = true;
            emitVisibility(*buf, i);
            toCBuffer(i, *buf, hgs);
        }

        override void visit(Declaration d)
        {
            if (!d.ident)
                return;
            TemplateDeclaration td = getEponymousParent(d);
            //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--");
            HdrGenState hgs;
            hgs.ddoc = true;
            if (d.isDeprecated())
                buf.writestring("$(DEPRECATED ");
            prefix(d);
            if (d.type)
            {
                Type origType = d.originalType ? d.originalType : d.type;
                if (origType.ty == Tfunction)
                {
                    functionToBufferFull(cast(TypeFunction)origType, *buf, d.ident, hgs, td);
                }
                else
                    toCBuffer(origType, *buf, d.ident, hgs);
            }
            else
                buf.writestring(d.ident.toString());
            if (d.isVarDeclaration() && td)
            {
                buf.writeByte('(');
                if (td.origParameters && td.origParameters.length)
                {
                    for (size_t i = 0; i < td.origParameters.length; i++)
                    {
                        if (i)
                            buf.writestring(", ");
                        toCBuffer((*td.origParameters)[i], *buf, hgs);
                    }
                }
                buf.writeByte(')');
            }
            // emit constraints if declaration is a templated declaration
            if (td && td.constraint)
            {
                bool noFuncDecl = td.isFuncDeclaration() is null;
                if (noFuncDecl)
                {
                    buf.writestring("$(DDOC_CONSTRAINT ");
                }

                toCBuffer(td.constraint, *buf, hgs);

                if (noFuncDecl)
                {
                    buf.writestring(")");
                }
            }
            if (d.isDeprecated())
                buf.writestring(")");
            buf.writestring(";\n");
        }

        override void visit(AliasDeclaration ad)
        {
            //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars());
            if (!ad.ident)
                return;
            if (ad.isDeprecated())
                buf.writestring("deprecated ");
            emitVisibility(*buf, ad);
            buf.printf("alias %s = ", ad.toChars());
            if (Dsymbol s = ad.aliassym) // ident alias
            {
                prettyPrintDsymbol(s, ad.parent);
            }
            else if (Type type = ad.getType()) // type alias
            {
                if (type.ty == Tclass || type.ty == Tstruct || type.ty == Tenum)
                {
                    import dmd.typesem : toDsymbol;
                    if (Dsymbol s = type.toDsymbol(null)) // elaborate type
                        prettyPrintDsymbol(s, ad.parent);
                    else
                        buf.writestring(type.toChars());
                }
                else
                {
                    // simple type
                    buf.writestring(type.toChars());
                }
            }
            buf.writestring(";\n");
        }

        void parentToBuffer(Dsymbol s)
        {
            if (s && !s.isPackage() && !s.isModule())
            {
                parentToBuffer(s.parent);
                buf.writestring(s.toChars());
                buf.writestring(".");
            }
        }

        static bool inSameModule(Dsymbol s, Dsymbol p) @safe
        {
            for (; s; s = s.parent)
            {
                if (s.isModule())
                    break;
            }
            for (; p; p = p.parent)
            {
                if (p.isModule())
                    break;
            }
            return s == p;
        }

        void prettyPrintDsymbol(Dsymbol s, Dsymbol parent)
        {
            if (s.parent && (s.parent == parent)) // in current scope -> naked name
            {
                buf.writestring(s.toChars());
            }
            else if (!inSameModule(s, parent)) // in another module -> full name
            {
                buf.writestring(s.toPrettyChars());
            }
            else // nested in a type in this module -> full name w/o module name
            {
                // if alias is nested in a user-type use module-scope lookup
                if (!parent.isModule() && !parent.isPackage())
                    buf.writestring(".");
                parentToBuffer(s.parent);
                buf.writestring(s.toChars());
            }
        }

        override void visit(AggregateDeclaration ad)
        {
            if (!ad.ident)
                return;
            version (none)
            {
                emitVisibility(buf, ad);
            }
            buf.printf("%s %s", ad.kind(), ad.toChars());
            buf.writestring(";\n");
        }

        override void visit(StructDeclaration sd)
        {
            //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars());
            if (!sd.ident)
                return;
            version (none)
            {
                emitVisibility(buf, sd);
            }
            if (TemplateDeclaration td = getEponymousParent(sd))
            {
                toDocBuffer(td, *buf, sc);
            }
            else
            {
                buf.printf("%s %s", sd.kind(), sd.toChars());
            }
            buf.writestring(";\n");
        }

        override void visit(ClassDeclaration cd)
        {
            //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars());
            if (!cd.ident)
                return;
            version (none)
            {
                emitVisibility(*buf, cd);
            }
            if (TemplateDeclaration td = getEponymousParent(cd))
            {
                toDocBuffer(td, *buf, sc);
            }
            else
            {
                if (!cd.isInterfaceDeclaration() && cd.isAbstract())
                    buf.writestring("abstract ");
                buf.printf("%s %s", cd.kind(), cd.toChars());
            }
            int any = 0;
            for (size_t i = 0; i < cd.baseclasses.length; i++)
            {
                BaseClass* bc = (*cd.baseclasses)[i];
                if (bc.sym && bc.sym.ident == Id.Object)
                    continue;
                if (any)
                    buf.writestring(", ");
                else
                {
                    buf.writestring(": ");
                    any = 1;
                }

                if (bc.sym)
                {
                    buf.printf("$(DDOC_PSUPER_SYMBOL %s)", bc.sym.toPrettyChars());
                }
                else
                {
                    HdrGenState hgs;
                    toCBuffer(bc.type, *buf, null, hgs);
                }
            }
            buf.writestring(";\n");
        }

        override void visit(EnumDeclaration ed)
        {
            if (!ed.ident)
                return;
            buf.printf("%s %s", ed.kind(), ed.toChars());
            if (ed.memtype)
            {
                buf.writestring(": $(DDOC_ENUM_BASETYPE ");
                HdrGenState hgs;
                toCBuffer(ed.memtype, *buf, null, hgs);
                buf.writestring(")");
            }
            buf.writestring(";\n");
        }

        override void visit(EnumMember em)
        {
            if (!em.ident)
                return;
            buf.writestring(em.toChars());
        }
    }

    scope ToDocBuffer v = new ToDocBuffer(buf, sc);
    s.accept(v);
}

/***********************************************************
 */
public
struct DocComment
{
    Sections sections;      // Section*[]
    Section summary;
    Section copyright;
    Section macros;
    MacroTable* pmacrotable;
    Escape* escapetable;
    Dsymbols a;

    static DocComment* parse(Dsymbol s, const(char)* comment)
    {
        //printf("parse(%s): '%s'\n", s.toChars(), comment);
        auto dc = new DocComment();
        dc.a.push(s);
        if (!comment)
            return dc;
        dc.parseSections(comment);
        for (size_t i = 0; i < dc.sections.length; i++)
        {
            Section sec = dc.sections[i];
            if (iequals("copyright", sec.name))
            {
                dc.copyright = sec;
            }
            if (iequals("macros", sec.name))
            {
                dc.macros = sec;
            }
        }
        return dc;
    }

    /************************************************
     * Parse macros out of Macros: section.
     * Macros are of the form:
     *      name1 = value1
     *
     *      name2 = value2
     */
    extern(D) static void parseMacros(
        Escape* escapetable, ref MacroTable pmacrotable, const(char)[] m)
    {
        const(char)* p = m.ptr;
        size_t len = m.length;
        const(char)* pend = p + len;
        const(char)* tempstart = null;
        size_t templen = 0;
        const(char)* namestart = null;
        size_t namelen = 0; // !=0 if line continuation
        const(char)* textstart = null;
        size_t textlen = 0;
        while (p < pend)
        {
            // Skip to start of macro
            while (1)
            {
                if (p >= pend)
                    goto Ldone;
                switch (*p)
                {
                case ' ':
                case '\t':
                    p++;
                    continue;
                case '\r':
                case '\n':
                    p++;
                    goto Lcont;
                default:
                    if (isIdStart(p))
                        break;
                    if (namelen)
                        goto Ltext; // continuation of prev macro
                    goto Lskipline;
                }
                break;
            }
            tempstart = p;
            while (1)
            {
                if (p >= pend)
                    goto Ldone;
                if (!isIdTail(p))
                    break;
                p += utfStride(p);
            }
            templen = p - tempstart;
            while (1)
            {
                if (p >= pend)
                    goto Ldone;
                if (!(*p == ' ' || *p == '\t'))
                    break;
                p++;
            }
            if (*p != '=')
            {
                if (namelen)
                    goto Ltext; // continuation of prev macro
                goto Lskipline;
            }
            p++;
            if (p >= pend)
                goto Ldone;
            if (namelen)
            {
                // Output existing macro
            L1:
                //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
                if (iequals("ESCAPES", namestart[0 .. namelen]))
                    parseEscapes(escapetable, textstart[0 .. textlen]);
                else
                    pmacrotable.define(namestart[0 .. namelen], textstart[0 .. textlen]);
                namelen = 0;
                if (p >= pend)
                    break;
            }
            namestart = tempstart;
            namelen = templen;
            while (p < pend && (*p == ' ' || *p == '\t'))
                p++;
            textstart = p;
        Ltext:
            while (p < pend && *p != '\r' && *p != '\n')
                p++;
            textlen = p - textstart;
            p++;
            //printf("p = %p, pend = %p\n", p, pend);
        Lcont:
            continue;
        Lskipline:
            // Ignore this line
            while (p < pend && *p != '\r' && *p != '\n')
                p++;
        }
    Ldone:
        if (namelen)
            goto L1; // write out last one
    }

    /**************************************
     * Parse escapes of the form:
     *      /c/string/
     * where c is a single character.
     * Multiple escapes can be separated
     * by whitespace and/or commas.
     */
    static void parseEscapes(Escape* escapetable, const(char)[] text)
    {
        if (!escapetable)
        {
            escapetable = new Escape();
            memset(escapetable, 0, Escape.sizeof);
        }
        //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable);
        const(char)* p = text.ptr;
        const(char)* pend = p + text.length;
        while (1)
        {
            while (1)
            {
                if (p + 4 >= pend)
                    return;
                if (!(*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ','))
                    break;
                p++;
            }
            if (p[0] != '/' || p[2] != '/')
                return;
            char c = p[1];
            p += 3;
            const(char)* start = p;
            while (1)
            {
                if (p >= pend)
                    return;
                if (*p == '/')
                    break;
                p++;
            }
            size_t len = p - start;
            char* s = cast(char*)memcpy(mem.xmalloc(len + 1), start, len);
            s[len] = 0;
            escapetable.strings[c] = s[0 .. len];
            //printf("\t%c = '%s'\n", c, s);
            p++;
        }
    }

    /*****************************************
     * Parse next paragraph out of *pcomment.
     * Update *pcomment to point past paragraph.
     * Returns NULL if no more paragraphs.
     * If paragraph ends in 'identifier:',
     * then (*pcomment)[0 .. idlen] is the identifier.
     */
    void parseSections(const(char)* comment)
    {
        const(char)* p;
        const(char)* pstart;
        const(char)* pend;
        const(char)* idstart = null; // dead-store to prevent spurious warning
        size_t idlen;
        const(char)* name = null;
        size_t namelen = 0;
        //printf("parseSections('%s')\n", comment);
        p = comment;
        while (*p)
        {
            const(char)* pstart0 = p;
            p = skipwhitespace(p);
            pstart = p;
            pend = p;

            // Undo indent if starting with a list item
            if ((*p == '-' || *p == '+' || *p == '*') && (*(p+1) == ' ' || *(p+1) == '\t'))
                pstart = pstart0;
            else
            {
                const(char)* pitem = p;
                while (*pitem >= '0' && *pitem <= '9')
                    ++pitem;
                if (pitem > p && *pitem == '.' && (*(pitem+1) == ' ' || *(pitem+1) == '\t'))
                    pstart = pstart0;
            }

            /* Find end of section, which is ended by one of:
             *      'identifier:' (but not inside a code section)
             *      '\0'
             */
            idlen = 0;
            int inCode = 0;
            while (1)
            {
                // Check for start/end of a code section
                if (*p == '-' || *p == '`' || *p == '~')
                {
                    char c = *p;
                    int numdash = 0;
                    while (*p == c)
                    {
                        ++numdash;
                        p++;
                    }
                    // BUG: handle UTF PS and LS too
                    if ((!*p || *p == '\r' || *p == '\n' || (!inCode && c != '-')) && numdash >= 3)
                    {
                        inCode = inCode == c ? false : c;
                        if (inCode)
                        {
                            // restore leading indentation
                            while (pstart0 < pstart && isIndentWS(pstart - 1))
                                --pstart;
                        }
                    }
                    pend = p;
                }
                if (!inCode && isIdStart(p))
                {
                    const(char)* q = p + utfStride(p);
                    while (isIdTail(q))
                        q += utfStride(q);

                    // Detected tag ends it
                    if (*q == ':' && isupper(*p)
                            && (isspace(q[1]) || q[1] == 0))
                    {
                        idlen = q - p;
                        idstart = p;
                        for (pend = p; pend > pstart; pend--)
                        {
                            if (pend[-1] == '\n')
                                break;
                        }
                        p = q + 1;
                        break;
                    }
                }
                while (1)
                {
                    if (!*p)
                        goto L1;
                    if (*p == '\n')
                    {
                        p++;
                        if (*p == '\n' && !summary && !namelen && !inCode)
                        {
                            pend = p;
                            p++;
                            goto L1;
                        }
                        break;
                    }
                    p++;
                    pend = p;
                }
                p = skipwhitespace(p);
            }
        L1:
            if (namelen || pstart < pend)
            {
                Section s;
                if (iequals("Params", name[0 .. namelen]))
                    s = new ParamSection();
                else if (iequals("Macros", name[0 .. namelen]))
                    s = new MacroSection();
                else
                    s = new Section();
                s.name = name[0 .. namelen];
                s.body_ = pstart[0 .. pend - pstart];
                s.nooutput = 0;
                //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body);
                sections.push(s);
                if (!summary && !namelen)
                    summary = s;
            }
            if (idlen)
            {
                name = idstart;
                namelen = idlen;
            }
            else
            {
                name = null;
                namelen = 0;
                if (!*p)
                    break;
            }
        }
    }

    void writeSections(Scope* sc, Dsymbols* a, ref OutBuffer buf)
    {
        assert(a.length);
        //printf("DocComment::writeSections()\n");
        Loc loc = (*a)[0].loc;
        if (Module m = (*a)[0].isModule())
        {
            if (m.md)
                loc = m.md.loc;
        }
        size_t offset1 = buf.length;
        buf.writestring("$(DDOC_SECTIONS ");
        size_t offset2 = buf.length;
        for (size_t i = 0; i < sections.length; i++)
        {
            Section sec = sections[i];
            if (sec.nooutput)
                continue;
            //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body);
            if (!sec.name.length && i == 0)
            {
                buf.writestring("$(DDOC_SUMMARY ");
                size_t o = buf.length;
                buf.write(sec.body_);
                escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
                highlightText(sc, a, loc, buf, o);
                buf.writestring(")");
            }
            else
                sec.write(loc, &this, sc, a, buf);
        }
        for (size_t i = 0; i < a.length; i++)
        {
            Dsymbol s = (*a)[i];
            if (Dsymbol td = getEponymousParent(s))
                s = td;
            for (UnitTestDeclaration utd = s.ddocUnittest; utd; utd = utd.ddocUnittest)
            {
                if (utd.visibility.kind == Visibility.Kind.private_ || !utd.comment || !utd.fbody)
                    continue;
                // Strip whitespaces to avoid showing empty summary
                const(char)* c = utd.comment;
                while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r')
                    ++c;
                buf.writestring("$(DDOC_EXAMPLES ");
                size_t o = buf.length;
                buf.writestring(cast(char*)c);
                if (utd.codedoc)
                {
                    auto codedoc = utd.codedoc.stripLeadingNewlines;
                    size_t n = getCodeIndent(codedoc);
                    while (n--)
                        buf.writeByte(' ');
                    buf.writestring("----\n");
                    buf.writestring(codedoc);
                    buf.writestring("----\n");
                    highlightText(sc, a, loc, buf, o);
                }
                buf.writestring(")");
            }
        }
        if (buf.length == offset2)
        {
            /* Didn't write out any sections, so back out last write
             */
            buf.setsize(offset1);
            buf.writestring("\n");
        }
        else
            buf.writestring(")");
    }
}

/*****************************************
 * Return true if comment consists entirely of "ditto".
 */
bool isDitto(const(char)* comment)
{
    if (comment)
    {
        const(char)* p = skipwhitespace(comment);
        if (Port.memicmp(p, "ditto", 5) == 0 && *skipwhitespace(p + 5) == 0)
            return true;
    }
    return false;
}

/**********************************************
 * Skip white space.
 */
const(char)* skipwhitespace(const(char)* p)
{
    return skipwhitespace(p.toDString).ptr;
}

/// Ditto
const(char)[] skipwhitespace(const(char)[] p) @safe
{
    foreach (idx, char c; p)
    {
        switch (c)
        {
        case ' ':
        case '\t':
        case '\n':
            continue;
        default:
            return p[idx .. $];
        }
    }
    return p[$ .. $];
}

/************************************************
 * Scan past all instances of the given characters.
 * Params:
 *  buf           = an OutBuffer containing the DDoc
 *  i             = the index within `buf` to start scanning from
 *  chars         = the characters to skip; order is unimportant
 * Returns: the index after skipping characters.
 */
size_t skipChars(ref OutBuffer buf, size_t i, string chars) @safe
{
    Outer:
    foreach (j, c; buf[][i..$])
    {
        foreach (d; chars)
        {
            if (d == c)
                continue Outer;
        }
        return i + j;
    }
    return buf.length;
}

unittest {
    OutBuffer buf;
    string data = "test ---\r\n\r\nend";
    buf.write(data);

    assert(skipChars(buf, 0, "-") == 0);
    assert(skipChars(buf, 4, "-") == 4);
    assert(skipChars(buf, 4, " -") == 8);
    assert(skipChars(buf, 8, "\r\n") == 12);
    assert(skipChars(buf, 12, "dne") == 15);
}

/****************************************************
 * Replace all instances of `c` with `r` in the given string
 * Params:
 *  s = the string to do replacements in
 *  c = the character to look for
 *  r = the string to replace `c` with
 * Returns: `s` with `c` replaced with `r`
 */
inout(char)[] replaceChar(inout(char)[] s, char c, string r) pure @safe
{
    int count = 0;
    foreach (char sc; s)
        if (sc == c)
            ++count;
    if (count == 0)
        return s;

    char[] result;
    result.reserve(s.length - count + (r.length * count));
    size_t start = 0;
    foreach (i, char sc; s)
    {
        if (sc == c)
        {
            result ~= s[start..i];
            result ~= r;
            start = i+1;
        }
    }
    result ~= s[start..$];
    return result;
}

///
unittest
{
    assert("".replaceChar(',', "$(COMMA)") == "");
    assert("ab".replaceChar(',', "$(COMMA)") == "ab");
    assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b");
    assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b");
    assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab");
    assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)");
}

/**
 * Return a lowercased copy of a string.
 * Params:
 *  s = the string to lowercase
 * Returns: the lowercase version of the string or the original if already lowercase
 */
string toLowercase(string s) pure @safe
{
    string lower;
    foreach (size_t i; 0..s.length)
    {
        char c = s[i];
// TODO: maybe unicode lowercase, somehow
        if (c >= 'A' && c <= 'Z')
        {
            if (!lower.length) {
                lower.reserve(s.length);
            }
            lower ~= s[lower.length..i];
            c += 'a' - 'A';
            lower ~= c;
        }
    }
    if (lower.length)
        lower ~= s[lower.length..$];
    else
        lower = s;
    return lower;
}

///
unittest
{
    assert("".toLowercase == "");
    assert("abc".toLowercase == "abc");
    assert("ABC".toLowercase == "abc");
    assert("aBc".toLowercase == "abc");
}

/************************************************
 * Get the indent from one index to another, counting tab stops as four spaces wide
 * per the Markdown spec.
 * Params:
 *  buf   = an OutBuffer containing the DDoc
 *  from  = the index within `buf` to start counting from, inclusive
 *  to    = the index within `buf` to stop counting at, exclusive
 * Returns: the indent
 */
int getMarkdownIndent(ref OutBuffer buf, size_t from, size_t to) @safe
{
    const slice = buf[];
    if (to > slice.length)
        to = slice.length;
    int indent = 0;
    foreach (const c; slice[from..to])
        indent += (c == '\t') ? 4 - (indent % 4) : 1;
    return indent;
}

/************************************************
 * Scan forward to one of:
 *      start of identifier
 *      beginning of next line
 *      end of buf
 */
size_t skiptoident(ref OutBuffer buf, size_t i) @safe
{
    const slice = buf[];
    while (i < slice.length)
    {
        dchar c;
        size_t oi = i;
        if (utf_decodeChar(slice, i, c))
        {
            /* Ignore UTF errors, but still consume input
             */
            break;
        }
        if (c >= 0x80)
        {
            if (!isUniAlpha(c))
                continue;
        }
        else if (!(isalpha(c) || c == '_' || c == '\n'))
            continue;
        i = oi;
        break;
    }
    return i;
}

/************************************************
 * Scan forward past end of identifier.
 */
size_t skippastident(ref OutBuffer buf, size_t i) @safe
{
    const slice = buf[];
    while (i < slice.length)
    {
        dchar c;
        size_t oi = i;
        if (utf_decodeChar(slice, i, c))
        {
            /* Ignore UTF errors, but still consume input
             */
            break;
        }
        if (c >= 0x80)
        {
            if (isUniAlpha(c))
                continue;
        }
        else if (isalnum(c) || c == '_')
            continue;
        i = oi;
        break;
    }
    return i;
}

/************************************************
 * Scan forward past end of an identifier that might
 * contain dots (e.g. `abc.def`)
 */
size_t skipPastIdentWithDots(ref OutBuffer buf, size_t i) @safe
{
    const slice = buf[];
    bool lastCharWasDot;
    while (i < slice.length)
    {
        dchar c;
        size_t oi = i;
        if (utf_decodeChar(slice, i, c))
        {
            /* Ignore UTF errors, but still consume input
             */
            break;
        }
        if (c == '.')
        {
            // We need to distinguish between `abc.def`, abc..def`, and `abc.`
            // Only `abc.def` is a valid identifier

            if (lastCharWasDot)
            {
                i = oi;
                break;
            }

            lastCharWasDot = true;
            continue;
        }
        else
        {
            if (c >= 0x80)
            {
                if (isUniAlpha(c))
                {
                    lastCharWasDot = false;
                    continue;
                }
            }
            else if (isalnum(c) || c == '_')
            {
                lastCharWasDot = false;
                continue;
            }
            i = oi;
            break;
        }
    }

    // if `abc.`
    if (lastCharWasDot)
        return i - 1;

    return i;
}

/************************************************
 * Scan forward past URL starting at i.
 * We don't want to highlight parts of a URL.
 * Returns:
 *      i if not a URL
 *      index just past it if it is a URL
 */
size_t skippastURL(ref OutBuffer buf, size_t i)
{
    const slice = buf[][i .. $];
    size_t j;
    bool sawdot = false;
    if (slice.length > 7 && Port.memicmp(slice.ptr, "http://", 7) == 0)
    {
        j = 7;
    }
    else if (slice.length > 8 && Port.memicmp(slice.ptr, "https://", 8) == 0)
    {
        j = 8;
    }
    else
        goto Lno;
    for (; j < slice.length; j++)
    {
        const c = slice[j];
        if (isalnum(c))
            continue;
        if (c == '-' || c == '_' || c == '?' || c == '=' || c == '%' ||
            c == '&' || c == '/' || c == '+' || c == '#' || c == '~')
            continue;
        if (c == '.')
        {
            sawdot = true;
            continue;
        }
        break;
    }
    if (sawdot)
        return i + j;
Lno:
    return i;
}

/****************************************************
 * Remove a previously-inserted blank line macro.
 * Params:
 *  buf           = an OutBuffer containing the DDoc
 *  iAt           = the index within `buf` of the start of the `$(DDOC_BLANKLINE)`
 *                  macro. Upon function return its value is set to `0`.
 *  i             = an index within `buf`. If `i` is after `iAt` then it gets
 *                  reduced by the length of the removed macro.
 */
void removeBlankLineMacro(ref OutBuffer buf, ref size_t iAt, ref size_t i)
{
    if (!iAt)
        return;

    enum macroLength = "$(DDOC_BLANKLINE)".length;
    buf.remove(iAt, macroLength);
    if (i > iAt)
        i -= macroLength;
    iAt = 0;
}

/****************************************************
 * Attempt to detect and replace a Markdown thematic break (HR). These are three
 * or more of the same delimiter, optionally with spaces or tabs between any of
 * them, e.g. `\n- - -\n` becomes `\n$(HR)\n`
 * Params:
 *  buf         = an OutBuffer containing the DDoc
 *  i           = the index within `buf` of the first character of a potential
 *                thematic break. If the replacement is made `i` changes to
 *                point to the closing parenthesis of the `$(HR)` macro.
 *  iLineStart  = the index within `buf` that the thematic break's line starts at
 *  loc         = the current location within the file
 * Returns: whether a thematic break was replaced
 */
bool replaceMarkdownThematicBreak(ref OutBuffer buf, ref size_t i, size_t iLineStart, const ref Loc loc)
{

    const slice = buf[];
    const c = buf[i];
    size_t j = i + 1;
    int repeat = 1;
    for (; j < slice.length; j++)
    {
        if (buf[j] == c)
            ++repeat;
        else if (buf[j] != ' ' && buf[j] != '\t')
            break;
    }
    if (repeat >= 3)
    {
        if (j >= buf.length || buf[j] == '\n' || buf[j] == '\r')
        {
            buf.remove(iLineStart, j - iLineStart);
            i = buf.insert(iLineStart, "$(HR)") - 1;
            return true;
        }
    }
    return false;
}

/****************************************************
 * Detect the level of an ATX-style heading, e.g. `## This is a heading` would
 * have a level of `2`.
 * Params:
 *  buf   = an OutBuffer containing the DDoc
 *  i     = the index within `buf` of the first `#` character
 * Returns:
 *          the detected heading level from 1 to 6, or
 *          0 if not at an ATX heading
 */
int detectAtxHeadingLevel(ref OutBuffer buf, const size_t i) @safe
{
    const iHeadingStart = i;
    const iAfterHashes = skipChars(buf, i, "#");
    const headingLevel = cast(int) (iAfterHashes - iHeadingStart);
    if (headingLevel > 6)
        return 0;

    const iTextStart = skipChars(buf, iAfterHashes, " \t");
    const emptyHeading = buf[iTextStart] == '\r' || buf[iTextStart] == '\n';

    // require whitespace
    if (!emptyHeading && iTextStart == iAfterHashes)
        return 0;

    return headingLevel;
}

/****************************************************
 * Remove any trailing `##` suffix from an ATX-style heading.
 * Params:
 *  buf   = an OutBuffer containing the DDoc
 *  i     = the index within `buf` to start looking for a suffix at
 */
void removeAnyAtxHeadingSuffix(ref OutBuffer buf, size_t i)
{
    size_t j = i;
    size_t iSuffixStart = 0;
    size_t iWhitespaceStart = j;
    const slice = buf[];
    for (; j < slice.length; j++)
    {
        switch (slice[j])
        {
        case '#':
            if (iWhitespaceStart && !iSuffixStart)
                iSuffixStart = j;
            continue;
        case ' ':
        case '\t':
            if (!iWhitespaceStart)
                iWhitespaceStart = j;
            continue;
        case '\r':
        case '\n':
            break;
        default:
            iSuffixStart = 0;
            iWhitespaceStart = 0;
            continue;
        }
        break;
    }
    if (iSuffixStart)
        buf.remove(iWhitespaceStart, j - iWhitespaceStart);
}

/****************************************************
 * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`).
 * Params:
 *  buf           = an OutBuffer containing the DDoc
 *  iStart        = the index within `buf` that the Markdown heading starts at
 *  iEnd          = the index within `buf` of the character after the last
 *                  heading character. Is incremented by the length of the
 *                  inserted heading macro when this function ends.
 *  loc           = the location of the Ddoc within the file
 *  headingLevel  = the level (1-6) of heading to end. Is set to `0` when this
 *                  function ends.
 */
void endMarkdownHeading(ref OutBuffer buf, size_t iStart, ref size_t iEnd, const ref Loc loc, ref int headingLevel)
{
    char[5] heading = "$(H0 ";
    heading[3] = cast(char) ('0' + headingLevel);
    buf.insert(iStart, heading);
    iEnd += 5;
    size_t iBeforeNewline = iEnd;
    while (buf[iBeforeNewline-1] == '\r' || buf[iBeforeNewline-1] == '\n')
        --iBeforeNewline;
    buf.insert(iBeforeNewline, ")");
    headingLevel = 0;
}

/****************************************************
 * End all nested Markdown quotes, if inside any.
 * Params:
 *  buf         = an OutBuffer containing the DDoc
 *  i           = the index within `buf` of the character after the quote text.
 *  quoteLevel  = the current quote level. Is set to `0` when this function ends.
 * Returns: the amount that `i` was moved
 */
size_t endAllMarkdownQuotes(ref OutBuffer buf, size_t i, ref int quoteLevel)
{
    const length = quoteLevel;
    for (; quoteLevel > 0; --quoteLevel)
        i = buf.insert(i, ")");
    return length;
}

/****************************************************
 * Convenience function to end all Markdown lists and quotes, if inside any, and
 * set `quoteMacroLevel` to `0`.
 * Params:
 *  buf         = an OutBuffer containing the DDoc
 *  i           = the index within `buf` of the character after the list and/or
 *                quote text. Is adjusted when this function ends if any lists
 *                and/or quotes were ended.
 *  nestedLists = a set of nested lists. Upon return it will be empty.
 *  quoteLevel  = the current quote level. Is set to `0` when this function ends.
 *  quoteMacroLevel   = the macro level that the quote was started at. Is set to
 *                      `0` when this function ends.
 * Returns: the amount that `i` was moved
 */
size_t endAllListsAndQuotes(ref OutBuffer buf, ref size_t i, ref MarkdownList[] nestedLists, ref int quoteLevel, out int quoteMacroLevel)
{
    quoteMacroLevel = 0;
    const i0 = i;
    i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
    i += endAllMarkdownQuotes(buf, i, quoteLevel);
    return i - i0;
}

/****************************************************
 * Replace Markdown emphasis with the appropriate macro,
 * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`.
 * Params:
 *  buf               = an OutBuffer containing the DDoc
 *  loc               = the current location within the file
 *  inlineDelimiters  = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`.
 *  downToLevel       = the length within `inlineDelimiters`` to reduce emphasis to
 * Returns: the number of characters added to the buffer by the replacements
 */
size_t replaceMarkdownEmphasis(ref OutBuffer buf, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int downToLevel = 0)
{
    size_t replaceEmphasisPair(ref MarkdownDelimiter start, ref MarkdownDelimiter end)
    {
        immutable count = start.count == 1 || end.count == 1 ? 1 : 2;

        size_t iStart = start.iStart;
        size_t iEnd = end.iStart;
        end.count -= count;
        start.count -= count;
        iStart += start.count;

        if (!start.count)
            start.type = 0;
        if (!end.count)
            end.type = 0;

        buf.remove(iStart, count);
        iEnd -= count;
        buf.remove(iEnd, count);

        string macroName = count >= 2 ? "$(STRONG " : "$(EM ";
        buf.insert(iEnd, ")");
        buf.insert(iStart, macroName);

        const delta = 1 + macroName.length - (count + count);
        end.iStart += count;
        return delta;
    }

    size_t delta = 0;
    int start = (cast(int) inlineDelimiters.length) - 1;
    while (start >= downToLevel)
    {
        // find start emphasis
        while (start >= downToLevel &&
            (inlineDelimiters[start].type != '*' || !inlineDelimiters[start].leftFlanking))
            --start;
        if (start < downToLevel)
            break;

        // find the nearest end emphasis
        int end = start + 1;
        while (end < inlineDelimiters.length &&
            (inlineDelimiters[end].type != inlineDelimiters[start].type ||
                inlineDelimiters[end].macroLevel != inlineDelimiters[start].macroLevel ||
                !inlineDelimiters[end].rightFlanking))
            ++end;
        if (end == inlineDelimiters.length)
        {
            // the start emphasis has no matching end; if it isn't an end itself then kill it
            if (!inlineDelimiters[start].rightFlanking)
                inlineDelimiters[start].type = 0;
            --start;
            continue;
        }

        // multiple-of-3 rule
        if (((inlineDelimiters[start].leftFlanking && inlineDelimiters[start].rightFlanking) ||
                (inlineDelimiters[end].leftFlanking && inlineDelimiters[end].rightFlanking)) &&
            (inlineDelimiters[start].count + inlineDelimiters[end].count) % 3 == 0)
        {
            --start;
            continue;
        }

        immutable delta0 = replaceEmphasisPair(inlineDelimiters[start], inlineDelimiters[end]);

        for (; end < inlineDelimiters.length; ++end)
            inlineDelimiters[end].iStart += delta0;
        delta += delta0;
    }

    inlineDelimiters.length = downToLevel;
    return delta;
}

/****************************************************
 */
bool isIdentifier(Dsymbols* a, const(char)[] s) @safe
{
    foreach (member; *a)
    {
        if (auto imp = member.isImport())
        {
            // For example: `public import str = core.stdc.string;`
            // This checks if `s` is equal to `str`
            if (imp.aliasId)
            {
                if (s == imp.aliasId.toString())
                    return true;
            }
            else
            {
                // The general case:  `public import core.stdc.string;`

                // fully qualify imports so `core.stdc.string` doesn't appear as `core`
                string fullyQualifiedImport;
                foreach (const pid; imp.packages)
                {
                    fullyQualifiedImport ~= pid.toString() ~ ".";
                }
                fullyQualifiedImport ~= imp.id.toString();

                // Check if `s` == `core.stdc.string`
                if (s == fullyQualifiedImport)
                    return true;
            }
        }
        else if (member.ident)
        {
            if (s == member.ident.toString())
                return true;
        }

    }
    return false;
}

/****************************************************
 */
bool isKeyword(const(char)[] str) @safe
{
    immutable string[3] table = ["true", "false", "null"];
    foreach (s; table)
    {
        if (str == s)
            return true;
    }
    return false;
}

/****************************************************
 */
TypeFunction isTypeFunction(Dsymbol s) @safe
{
    FuncDeclaration f = s.isFuncDeclaration();
    /* f.type may be NULL for template members.
     */
    if (f && f.type)
    {
        Type t = f.originalType ? f.originalType : f.type;
        if (t.ty == Tfunction)
            return cast(TypeFunction)t;
    }
    return null;
}

/****************************************************
 */
Parameter isFunctionParameter(Dsymbol s, const(char)[] str) @safe
{
    TypeFunction tf = isTypeFunction(s);
    if (tf && tf.parameterList.parameters)
    {
        foreach (fparam; *tf.parameterList.parameters)
        {
            if (fparam.ident && str == fparam.ident.toString())
            {
                return fparam;
            }
        }
    }
    return null;
}

/****************************************************
 */
Parameter isFunctionParameter(Dsymbols* a, const(char)[] p) @safe
{
    foreach (Dsymbol sym; *a)
    {
        Parameter fparam = isFunctionParameter(sym, p);
        if (fparam)
        {
            return fparam;
        }
    }
    return null;
}

/****************************************************
 */
Parameter isEponymousFunctionParameter(Dsymbols *a, const(char)[] p) @safe
{
    foreach (Dsymbol dsym; *a)
    {
        TemplateDeclaration td = dsym.isTemplateDeclaration();
        if (td && td.onemember)
        {
            /* Case 1: we refer to a template declaration inside the template

               /// ...ddoc...
               template case1(T) {
                 void case1(R)() {}
               }
             */
            td = td.onemember.isTemplateDeclaration();
        }
        if (!td)
        {
            /* Case 2: we're an alias to a template declaration

               /// ...ddoc...
               alias case2 = case1!int;
             */
            AliasDeclaration ad = dsym.isAliasDeclaration();
            if (ad && ad.aliassym)
            {
                td = ad.aliassym.isTemplateDeclaration();
            }
        }
        while (td)
        {
            Dsymbol sym = getEponymousMember(td);
            if (sym)
            {
                Parameter fparam = isFunctionParameter(sym, p);
                if (fparam)
                {
                    return fparam;
                }
            }
            td = td.overnext;
        }
    }
    return null;
}

/****************************************************
 */
TemplateParameter isTemplateParameter(Dsymbols* a, const(char)* p, size_t len)
{
    for (size_t i = 0; i < a.length; i++)
    {
        TemplateDeclaration td = (*a)[i].isTemplateDeclaration();
        // Check for the parent, if the current symbol is not a template declaration.
        if (!td)
            td = getEponymousParent((*a)[i]);
        if (td && td.origParameters)
        {
            foreach (tp; *td.origParameters)
            {
                if (tp.ident && p[0 .. len] == tp.ident.toString())
                {
                    return tp;
                }
            }
        }
    }
    return null;
}

/****************************************************
 * Return true if str is a reserved symbol name
 * that starts with a double underscore.
 */
bool isReservedName(const(char)[] str) @safe
{
    immutable string[] table =
    [
        "__ctor",
        "__dtor",
        "__postblit",
        "__invariant",
        "__unitTest",
        "__require",
        "__ensure",
        "__dollar",
        "__ctfe",
        "__withSym",
        "__result",
        "__returnLabel",
        "__vptr",
        "__monitor",
        "__gate",
        "__xopEquals",
        "__xopCmp",
        "__LINE__",
        "__FILE__",
        "__MODULE__",
        "__FUNCTION__",
        "__PRETTY_FUNCTION__",
        "__DATE__",
        "__TIME__",
        "__TIMESTAMP__",
        "__VENDOR__",
        "__VERSION__",
        "__EOF__",
        "__CXXLIB__",
        "__LOCAL_SIZE",
        "__entrypoint",
    ];
    foreach (s; table)
    {
        if (str == s)
            return true;
    }
    return false;
}

/****************************************************
 * A delimiter for Markdown inline content like emphasis and links.
 */
struct MarkdownDelimiter
{
    size_t iStart;  /// the index where this delimiter starts
    int count;      /// the length of this delimeter's start sequence
    int macroLevel; /// the count of nested DDoc macros when the delimiter is started
    bool leftFlanking;  /// whether the delimiter is left-flanking, as defined by the CommonMark spec
    bool rightFlanking; /// whether the delimiter is right-flanking, as defined by the CommonMark spec
    bool atParagraphStart;  /// whether the delimiter is at the start of a paragraph
    char type;      /// the type of delimiter, defined by its starting character

    /// whether this describes a valid delimiter
    @property bool isValid() const @safe { return count != 0; }

    /// flag this delimiter as invalid
    void invalidate() @safe { count = 0; }
}

/****************************************************
 * Info about a Markdown list.
 */
struct MarkdownList
{
    string orderedStart;    /// an optional start number--if present then the list starts at this number
    size_t iStart;          /// the index where the list item starts
    size_t iContentStart;   /// the index where the content starts after the list delimiter
    int delimiterIndent;    /// the level of indent the list delimiter starts at
    int contentIndent;      /// the level of indent the content starts at
    int macroLevel;         /// the count of nested DDoc macros when the list is started
    char type;              /// the type of list, defined by its starting character

    /// whether this describes a valid list
    @property bool isValid() const @safe { return type != type.init; }

    /****************************************************
     * Try to parse a list item, returning whether successful.
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  iLineStart    = the index within `buf` of the first character of the line
     *  i             = the index within `buf` of the potential list item
     * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded.
     */
    static MarkdownList parseItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
    {
        if (buf[i] == '+' || buf[i] == '-' || buf[i] == '*')
            return parseUnorderedListItem(buf, iLineStart, i);
        else
            return parseOrderedListItem(buf, iLineStart, i);
    }

    /****************************************************
     * Return whether the context is at a list item of the same type as this list.
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  iLineStart    = the index within `buf` of the first character of the line
     *  i             = the index within `buf` of the list item
     * Returns: whether `i` is at a list item of the same type as this list
     */
    private bool isAtItemInThisList(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
    {
        MarkdownList item = (type == '.' || type == ')') ?
            parseOrderedListItem(buf, iLineStart, i) :
            parseUnorderedListItem(buf, iLineStart, i);
        if (item.type == type)
            return item.delimiterIndent < contentIndent && item.contentIndent > delimiterIndent;
        return false;
    }

    /****************************************************
     * Start a Markdown list item by creating/deleting nested lists and starting the item.
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  iLineStart    = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`.
     *  i             = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro.
     *  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`.
     *  nestedLists   = a set of nested lists. If this function succeeds it may contain a new nested list.
     *  loc           = the location of the Ddoc within the file
     * Returns: `true` if a list was created
     */
    bool startItem(ref OutBuffer buf, ref size_t iLineStart, ref size_t i, ref size_t iPrecedingBlankLine, ref MarkdownList[] nestedLists, const ref Loc loc)
    {
        buf.remove(iStart, iContentStart - iStart);

        if (!nestedLists.length ||
            delimiterIndent >= nestedLists[$-1].contentIndent ||
            buf[iLineStart - 4..iLineStart] == "$(LI")
        {
            // start a list macro
            nestedLists ~= this;
            if (type == '.')
            {
                if (orderedStart.length)
                {
                    iStart = buf.insert(iStart, "$(OL_START ");
                    iStart = buf.insert(iStart, orderedStart);
                    iStart = buf.insert(iStart, ",\n");
                }
                else
                    iStart = buf.insert(iStart, "$(OL\n");
            }
            else
                iStart = buf.insert(iStart, "$(UL\n");

            removeBlankLineMacro(buf, iPrecedingBlankLine, iStart);
        }
        else if (nestedLists.length)
        {
            nestedLists[$-1].delimiterIndent = delimiterIndent;
            nestedLists[$-1].contentIndent = contentIndent;
        }

        iStart = buf.insert(iStart, "$(LI\n");
        i = iStart - 1;
        iLineStart = i;

        return true;
    }

    /****************************************************
     * End all nested Markdown lists.
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  i             = the index within `buf` to end lists at.
     *  nestedLists   = a set of nested lists. Upon return it will be empty.
     * Returns: the amount that `i` changed
     */
    static size_t endAllNestedLists(ref OutBuffer buf, size_t i, ref MarkdownList[] nestedLists)
    {
        const iStart = i;
        for (; nestedLists.length; --nestedLists.length)
            i = buf.insert(i, ")\n)");
        return i - iStart;
    }

    /****************************************************
     * Look for a sibling list item or the end of nested list(s).
     * Params:
     *  buf               = an OutBuffer containing the DDoc
     *  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.
     *  iParagraphStart   = the index within `buf` to start the next paragraph at at. May be adjusted upon return.
     *  nestedLists       = a set of nested lists. Some nested lists may have been removed from it upon return.
     */
    static void handleSiblingOrEndingList(ref OutBuffer buf, ref size_t i, ref size_t iParagraphStart, ref MarkdownList[] nestedLists)
    {
        size_t iAfterSpaces = skipChars(buf, i + 1, " \t");

        if (nestedLists[$-1].isAtItemInThisList(buf, i + 1, iAfterSpaces))
        {
            // end a sibling list item
            i = buf.insert(i, ")");
            iParagraphStart = skipChars(buf, i, " \t\r\n");
        }
        else if (iAfterSpaces >= buf.length || (buf[iAfterSpaces] != '\r' && buf[iAfterSpaces] != '\n'))
        {
            // end nested lists that are indented more than this content
            const indent = getMarkdownIndent(buf, i + 1, iAfterSpaces);
            while (nestedLists.length && nestedLists[$-1].contentIndent > indent)
            {
                i = buf.insert(i, ")\n)");
                --nestedLists.length;
                iParagraphStart = skipChars(buf, i, " \t\r\n");

                if (nestedLists.length && nestedLists[$-1].isAtItemInThisList(buf, i + 1, iParagraphStart))
                {
                    i = buf.insert(i, ")");
                    ++iParagraphStart;
                    break;
                }
            }
        }
    }

    /****************************************************
     * Parse an unordered list item at the current position
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  iLineStart    = the index within `buf` of the first character of the line
     *  i             = the index within `buf` of the list item
     * Returns: the parsed list item, or a list item with type `.init` if no list item is available
     */
    private static MarkdownList parseUnorderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
    {
        if (i+1 < buf.length &&
                (buf[i] == '-' ||
                buf[i] == '*' ||
                buf[i] == '+') &&
            (buf[i+1] == ' ' ||
                buf[i+1] == '\t' ||
                buf[i+1] == '\r' ||
                buf[i+1] == '\n'))
        {
            const iContentStart = skipChars(buf, i + 1, " \t");
            const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
            const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
            auto list = MarkdownList(null, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[i]);
            return list;
        }
        return MarkdownList();
    }

    /****************************************************
     * Parse an ordered list item at the current position
     * Params:
     *  buf           = an OutBuffer containing the DDoc
     *  iLineStart    = the index within `buf` of the first character of the line
     *  i             = the index within `buf` of the list item
     * Returns: the parsed list item, or a list item with type `.init` if no list item is available
     */
    private static MarkdownList parseOrderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
    {
        size_t iAfterNumbers = skipChars(buf, i, "0123456789");
        if (iAfterNumbers - i > 0 &&
            iAfterNumbers - i <= 9 &&
            iAfterNumbers + 1 < buf.length &&
            buf[iAfterNumbers] == '.' &&
            (buf[iAfterNumbers+1] == ' ' ||
                buf[iAfterNumbers+1] == '\t' ||
                buf[iAfterNumbers+1] == '\r' ||
                buf[iAfterNumbers+1] == '\n'))
        {
            const iContentStart = skipChars(buf, iAfterNumbers + 1, " \t");
            const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
            const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
            size_t iNumberStart = skipChars(buf, i, "0");
            if (iNumberStart == iAfterNumbers)
                --iNumberStart;
            auto orderedStart = buf[][iNumberStart .. iAfterNumbers];
            if (orderedStart == "1")
                orderedStart = null;
            return MarkdownList(orderedStart.idup, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[iAfterNumbers]);
        }
        return MarkdownList();
    }
}

/****************************************************
 * A Markdown link.
 */
struct MarkdownLink
{
    string href;    /// the link destination
    string title;   /// an optional title for the link
    string label;   /// an optional label for the link
    Dsymbol symbol; /// an optional symbol to link to

    /****************************************************
     * Replace a Markdown link or link definition in the form of:
     * - Inline link: `[foo](url/ 'optional title')`
     * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]`
     * - Link reference definition: `[bar]: url/ 'optional title'`
     * Params:
     *  buf               = an OutBuffer containing the DDoc
     *  i                 = the index within `buf` that points to the `]` character of the potential link.
     *                      If this function succeeds it will be adjusted to fit the inserted link macro.
     *  loc               = the current location within the file
     *  inlineDelimiters  = previously parsed Markdown delimiters, including emphasis and link/image starts
     *  delimiterIndex    = the index within `inlineDelimiters` of the nearest link/image starting delimiter
     *  linkReferences    = previously parsed link references. When this function returns it may contain
     *                      additional previously unparsed references.
     * Returns: whether a reference link was found and replaced at `i`
     */
    static bool replaceLink(ref OutBuffer buf, ref size_t i, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences)
    {
        const delimiter = inlineDelimiters[delimiterIndex];
        MarkdownLink link;

        size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
        if (iEnd > i)
        {
            i = delimiter.iStart;
            link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
            inlineDelimiters.length = delimiterIndex;
            return true;
        }

        iEnd = link.parseInlineLink(buf, i);
        if (iEnd == i)
        {
            iEnd = link.parseReferenceLink(buf, i, delimiter);
            if (iEnd > i)
            {
                const label = link.label;
                link = linkReferences.lookupReference(label, buf, i, loc);
                // check rightFlanking to avoid replacing things like int[string]
                if (!link.href.length && !delimiter.rightFlanking)
                    link = linkReferences.lookupSymbol(label);
                if (!link.href.length)
                    return false;
            }
        }

        if (iEnd == i)
            return false;

        immutable delta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, delimiterIndex);
        iEnd += delta;
        i += delta;
        link.replaceLink(buf, i, iEnd, delimiter);
        return true;
    }

    /****************************************************
     * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'`
     * Params:
     *  buf               = an OutBuffer containing the DDoc
     *  i                 = the index within `buf` that points to the `]` character of the potential link.
     *                      If this function succeeds it will be adjusted to fit the inserted link macro.
     *  inlineDelimiters  = previously parsed Markdown delimiters, including emphasis and link/image starts
     *  delimiterIndex    = the index within `inlineDelimiters` of the nearest link/image starting delimiter
     *  linkReferences    = previously parsed link references. When this function returns it may contain
     *                      additional previously unparsed references.
     *  loc               = the current location in the file
     * Returns: whether a reference link was found and replaced at `i`
     */
    static bool replaceReferenceDefinition(ref OutBuffer buf, ref size_t i, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
    {
        const delimiter = inlineDelimiters[delimiterIndex];
        MarkdownLink link;
        size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
        if (iEnd == i)
            return false;

        i = delimiter.iStart;
        link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
        inlineDelimiters.length = delimiterIndex;
        return true;
    }

    /****************************************************
     * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')`
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` that points to the `]` character of the inline link.
     * Returns: the index at the end of parsing the link, or `i` if parsing failed.
     */
    private size_t parseInlineLink(ref OutBuffer buf, size_t i)
    {
        size_t iEnd = i + 1;
        if (iEnd >= buf.length || buf[iEnd] != '(')
            return i;
        ++iEnd;

        if (!parseHref(buf, iEnd))
            return i;

        iEnd = skipChars(buf, iEnd, " \t\r\n");
        if (buf[iEnd] != ')')
        {
            if (parseTitle(buf, iEnd))
                iEnd = skipChars(buf, iEnd, " \t\r\n");
        }

        if (buf[iEnd] != ')')
            return i;

        return iEnd + 1;
    }

    /****************************************************
     * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]`
     * Params:
     *  buf       = an OutBuffer containing the DDoc
     *  i         = the index within `buf` that points to the `]` character of the inline link.
     *  delimiter = the delimiter that starts this link
     * Returns: the index at the end of parsing the link, or `i` if parsing failed.
     */
    private size_t parseReferenceLink(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) @safe
    {
        size_t iStart = i + 1;
        size_t iEnd = iStart;
        if (iEnd >= buf.length || buf[iEnd] != '[' || (iEnd+1 < buf.length && buf[iEnd+1] == ']'))
        {
            // collapsed reference [foo][] or shortcut reference [foo]
            iStart = delimiter.iStart + delimiter.count - 1;
            if (buf[iEnd] == '[')
                iEnd += 2;
        }

        parseLabel(buf, iStart);
        if (!label.length)
            return i;

        if (iEnd < iStart)
            iEnd = iStart;
        return iEnd;
    }

    /****************************************************
     * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'`
     * Params:
     *  buf               = an OutBuffer containing the DDoc
     *  i                 = the index within `buf` that points to the `]` character of the inline link.
     *  delimiter = the delimiter that starts this link
     * Returns: the index at the end of parsing the link, or `i` if parsing failed.
     */
    private size_t parseReferenceDefinition(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter)
    {
        if (!delimiter.atParagraphStart || delimiter.type != '[' ||
            i+1 >= buf.length || buf[i+1] != ':')
            return i;

        size_t iEnd = delimiter.iStart;
        parseLabel(buf, iEnd);
        if (label.length == 0 || iEnd != i + 1)
            return i;

        ++iEnd;
        iEnd = skipChars(buf, iEnd, " \t");
        skipOneNewline(buf, iEnd);

        if (!parseHref(buf, iEnd) || href.length == 0)
            return i;

        iEnd = skipChars(buf, iEnd, " \t");
        const requireNewline = !skipOneNewline(buf, iEnd);
        const iBeforeTitle = iEnd;

        if (parseTitle(buf, iEnd))
        {
            iEnd = skipChars(buf, iEnd, " \t");
            if (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n')
            {
                // the title must end with a newline
                title.length = 0;
                iEnd = iBeforeTitle;
            }
        }

        iEnd = skipChars(buf, iEnd, " \t");
        if (requireNewline && iEnd < buf.length-1 && buf[iEnd] != '\r' && buf[iEnd] != '\n')
            return i;

        return iEnd;
    }

    /****************************************************
     * Parse and normalize a Markdown reference label
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` that points to the `[` character at the start of the label.
     *          If this function returns a non-empty label then `i` will point just after the ']' at the end of the label.
     * Returns: the parsed and normalized label, possibly empty
     */
    private bool parseLabel(ref OutBuffer buf, ref size_t i) @safe
    {
        if (buf[i] != '[')
            return false;

        const slice = buf[];
        size_t j = i + 1;

        // Some labels have already been en-symboled; handle that
        const inSymbol = j+15 < slice.length && slice[j..j+15] == "$(DDOC_PSYMBOL ";
        if (inSymbol)
            j += 15;

        for (; j < slice.length; ++j)
        {
            const c = slice[j];
            switch (c)
            {
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                if (label.length && label[$-1] != ' ')
                    label ~= ' ';
                break;
            case ')':
                if (inSymbol && j+1 < slice.length && slice[j+1] == ']')
                {
                    ++j;
                    goto case ']';
                }
                goto default;
            case '[':
                if (slice[j-1] != '\\')
                {
                    label.length = 0;
                    return false;
                }
                break;
            case ']':
                if (label.length && label[$-1] == ' ')
                    --label.length;
                if (label.length)
                {
                    i = j + 1;
                    return true;
                }
                return false;
            default:
                label ~= c;
                break;
            }
        }
        label.length = 0;
        return false;
    }

    /****************************************************
     * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` that points to the first character of the URL.
     *          If this function succeeds `i` will point just after the end of the URL.
     * Returns: whether a URL was found and parsed
     */
    private bool parseHref(ref OutBuffer buf, ref size_t i)
    {
        size_t j = skipChars(buf, i, " \t");

        size_t iHrefStart = j;
        size_t parenDepth = 1;
        bool inPointy = false;
        const slice = buf[];
        for (; j < slice.length; j++)
        {
            switch (slice[j])
            {
            case '<':
                if (!inPointy && j == iHrefStart)
                {
                    inPointy = true;
                    ++iHrefStart;
                }
                break;
            case '>':
                if (inPointy && slice[j-1] != '\\')
                    goto LReturnHref;
                break;
            case '(':
                if (!inPointy && slice[j-1] != '\\')
                    ++parenDepth;
                break;
            case ')':
                if (!inPointy && slice[j-1] != '\\')
                {
                    --parenDepth;
                    if (!parenDepth)
                        goto LReturnHref;
                }
                break;
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                if (inPointy)
                {
                    // invalid link
                    return false;
                }
                goto LReturnHref;
            default:
                break;
            }
        }
        if (inPointy)
            return false;
    LReturnHref:
        auto href = slice[iHrefStart .. j].dup;
        this.href = cast(string) percentEncode(removeEscapeBackslashes(href)).replaceChar(',', "$(COMMA)");
        i = j;
        if (inPointy)
            ++i;
        return true;
    }

    /****************************************************
     * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` that points to the first character of the title.
     *          If this function succeeds `i` will point just after the end of the title.
     * Returns: whether a title was found and parsed
     */
    private bool parseTitle(ref OutBuffer buf, ref size_t i)
    {
        size_t j = skipChars(buf, i, " \t");
        if (j >= buf.length)
            return false;

        char type = buf[j];
        if (type != '"' && type != '\'' && type != '(')
            return false;
        if (type == '(')
            type = ')';

        const iTitleStart = j + 1;
        size_t iNewline = 0;
        const slice = buf[];
        for (j = iTitleStart; j < slice.length; j++)
        {
            const c = slice[j];
            switch (c)
            {
            case ')':
            case '"':
            case '\'':
                if (type == c && slice[j-1] != '\\')
                    goto LEndTitle;
                iNewline = 0;
                break;
            case ' ':
            case '\t':
            case '\r':
                break;
            case '\n':
                if (iNewline)
                {
                    // no blank lines in titles
                    return false;
                }
                iNewline = j;
                break;
            default:
                iNewline = 0;
                break;
            }
        }
        return false;
    LEndTitle:
        auto title = slice[iTitleStart .. j].dup;
        this.title = cast(string) removeEscapeBackslashes(title).
            replaceChar(',', "$(COMMA)").
            replaceChar('"', "$(QUOTE)");
        i = j + 1;
        return true;
    }

    /****************************************************
     * Replace a Markdown link or image with the appropriate macro
     * Params:
     *  buf       = an OutBuffer containing the DDoc
     *  i         = the index within `buf` that points to the `]` character of the inline link.
     *              When this function returns it will be adjusted to the end of the inserted macro.
     *  iLinkEnd  = the index within `buf` that points just after the last character of the link
     *  delimiter = the Markdown delimiter that started the link or image
     */
    private void replaceLink(ref OutBuffer buf, ref size_t i, size_t iLinkEnd, MarkdownDelimiter delimiter)
    {
        size_t iAfterLink = i - delimiter.count;
        string macroName;
        if (symbol)
        {
            macroName = "$(SYMBOL_LINK ";
        }
        else if (title.length)
        {
            if (delimiter.type == '[')
                macroName = "$(LINK_TITLE ";
            else
                macroName = "$(IMAGE_TITLE ";
        }
        else
        {
            if (delimiter.type == '[')
                macroName = "$(LINK2 ";
            else
                macroName = "$(IMAGE ";
        }
        buf.remove(delimiter.iStart, delimiter.count);
        buf.remove(i - delimiter.count, iLinkEnd - i);
        iLinkEnd = buf.insert(delimiter.iStart, macroName);
        iLinkEnd = buf.insert(iLinkEnd, href);
        iLinkEnd = buf.insert(iLinkEnd, ", ");
        iAfterLink += macroName.length + href.length + 2;
        if (title.length)
        {
            iLinkEnd = buf.insert(iLinkEnd, title);
            iLinkEnd = buf.insert(iLinkEnd, ", ");
            iAfterLink += title.length + 2;

            // Link macros with titles require escaping commas
            for (size_t j = iLinkEnd; j < iAfterLink; ++j)
                if (buf[j] == ',')
                {
                    buf.remove(j, 1);
                    j = buf.insert(j, "$(COMMA)") - 1;
                    iAfterLink += 7;
                }
        }
// TODO: if image, remove internal macros, leaving only text
        buf.insert(iAfterLink, ")");
        i = iAfterLink;
    }

    /****************************************************
     * Store the Markdown link definition and remove it from `buf`
     * Params:
     *  buf               = an OutBuffer containing the DDoc
     *  i                 = the index within `buf` that points to the `[` character at the start of the link definition.
     *                      When this function returns it will be adjusted to exclude the link definition.
     *  iEnd              = the index within `buf` that points just after the end of the definition
     *  linkReferences    = previously parsed link references. When this function returns it may contain
     *                      an additional reference.
     *  loc               = the current location in the file
     */
    private void storeAndReplaceDefinition(ref OutBuffer buf, ref size_t i, size_t iEnd, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
    {
        // Remove the definition and trailing whitespace
        iEnd = skipChars(buf, iEnd, " \t\r\n");
        buf.remove(i, iEnd - i);
        i -= 2;

        string lowercaseLabel = label.toLowercase();
        if (lowercaseLabel !in linkReferences.references)
            linkReferences.references[lowercaseLabel] = this;
    }

    /****************************************************
     * Remove Markdown escaping backslashes from the given string
     * Params:
     *  s = the string to remove escaping backslashes from
     * Returns: `s` without escaping backslashes in it
     */
    private static char[] removeEscapeBackslashes(char[] s) @safe
    {
        if (!s.length)
            return s;

        // avoid doing anything if there isn't anything to escape
        size_t i;
        for (i = 0; i < s.length-1; ++i)
            if (s[i] == '\\' && ispunct(s[i+1]))
                break;
        if (i == s.length-1)
            return s;

        // copy characters backwards, then truncate
        size_t j = i + 1;
        s[i] = s[j];
        for (++i, ++j; j < s.length; ++i, ++j)
        {
            if (j < s.length-1 && s[j] == '\\' && ispunct(s[j+1]))
                ++j;
            s[i] = s[j];
        }
        s.length -= (j - i);
        return s;
    }

    ///
    unittest
    {
        assert(removeEscapeBackslashes("".dup) == "");
        assert(removeEscapeBackslashes(`\a`.dup) == `\a`);
        assert(removeEscapeBackslashes(`.\`.dup) == `.\`);
        assert(removeEscapeBackslashes(`\.\`.dup) == `.\`);
        assert(removeEscapeBackslashes(`\.`.dup) == `.`);
        assert(removeEscapeBackslashes(`\.\.`.dup) == `..`);
        assert(removeEscapeBackslashes(`a\.b\.c`.dup) == `a.b.c`);
    }

    /****************************************************
     * Percent-encode (AKA URL-encode) the given string
     * Params:
     *  s = the string to percent-encode
     * Returns: `s` with special characters percent-encoded
     */
    private static inout(char)[] percentEncode(inout(char)[] s) pure @safe
    {
        static bool shouldEncode(char c)
        {
            return ((c < '0' && c != '!' && c != '#' && c != '$' && c != '%' && c != '&' && c != '\'' && c != '(' &&
                    c != ')' && c != '*' && c != '+' && c != ',' && c != '-' && c != '.' && c != '/')
                || (c > '9' && c < 'A' && c != ':' && c != ';' && c != '=' && c != '?' && c != '@')
                || (c > 'Z' && c < 'a' && c != '[' && c != ']' && c != '_')
                || (c > 'z' && c != '~'));
        }

        for (size_t i = 0; i < s.length; ++i)
        {
            if (shouldEncode(s[i]))
            {
                immutable static hexDigits = "0123456789ABCDEF";
                immutable encoded1 = hexDigits[s[i] >> 4];
                immutable encoded2 = hexDigits[s[i] & 0x0F];
                s = s[0..i] ~ '%' ~ encoded1 ~ encoded2 ~ s[i+1..$];
                i += 2;
            }
        }
        return s;
    }

    ///
    unittest
    {
        assert(percentEncode("") == "");
        assert(percentEncode("aB12-._~/?") == "aB12-._~/?");
        assert(percentEncode("<\n>") == "%3C%0A%3E");
    }

    /**************************************************
     * Skip a single newline at `i`
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` to start looking at.
     *          If this function succeeds `i` will point after the newline.
     * Returns: whether a newline was skipped
     */
    private static bool skipOneNewline(ref OutBuffer buf, ref size_t i) pure @safe
    {
        if (i < buf.length && buf[i] == '\r')
            ++i;
        if (i < buf.length && buf[i] == '\n')
        {
            ++i;
            return true;
        }
        return false;
    }
}

/**************************************************
 * A set of Markdown link references.
 */
struct MarkdownLinkReferences
{
    MarkdownLink[string] references;    // link references keyed by normalized label
    MarkdownLink[string] symbols;       // link symbols keyed by name
    Scope* _scope;      // the current scope
    bool extractedAll;  // the index into the buffer of the last-parsed reference

    /**************************************************
     * Look up a reference by label, searching through the rest of the buffer if needed.
     * Symbols in the current scope are searched for if the DDoc doesn't define the reference.
     * Params:
     *  label = the label to find the reference for
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` to start searching for references at
     *  loc   = the current location in the file
     * Returns: a link. If the `href` member has a value then the reference is valid.
     */
    MarkdownLink lookupReference(string label, ref OutBuffer buf, size_t i, const ref Loc loc)
    {
        const lowercaseLabel = label.toLowercase();
        if (lowercaseLabel !in references)
            extractReferences(buf, i, loc);

        if (lowercaseLabel in references)
            return references[lowercaseLabel];

        return MarkdownLink();
    }

    /**
     * Look up the link for the D symbol with the given name.
     * If found, the link is cached in the `symbols` member.
     * Params:
     *  name  = the name of the symbol
     * Returns: the link for the symbol or a link with a `null` href
     */
    MarkdownLink lookupSymbol(string name)
    {
        if (name in symbols)
            return symbols[name];

        const ids = split(name, '.');

        MarkdownLink link;
        auto id = Identifier.lookup(ids[0].ptr, ids[0].length);
        if (id)
        {
            auto loc = Loc();
            Dsymbol pscopesym;
            auto symbol = _scope.search(loc, id, pscopesym, SearchOpt.ignoreErrors);
            for (size_t i = 1; symbol && i < ids.length; ++i)
            {
                id = Identifier.lookup(ids[i].ptr, ids[i].length);
                symbol = id !is null ? symbol.search(loc, id, SearchOpt.ignoreErrors) : null;
            }
            if (symbol)
                link = MarkdownLink(createHref(symbol), null, name, symbol);
        }

        symbols[name] = link;
        return link;
    }

    /**************************************************
     * Remove and store all link references from the document, in the form of
     * `[label]: href "optional title"`
     * Params:
     *  buf   = an OutBuffer containing the DDoc
     *  i     = the index within `buf` to start looking at
     *  loc   = the current location in the file
     * Returns: whether a reference was extracted
     */
    private void extractReferences(ref OutBuffer buf, size_t i, const ref Loc loc)
    {
        static bool isFollowedBySpace(ref OutBuffer buf, size_t i)
        {
            return i+1 < buf.length && (buf[i+1] == ' ' || buf[i+1] == '\t');
        }

        if (extractedAll)
            return;

        bool leadingBlank = false;
        int inCode = false;
        bool newParagraph = true;
        MarkdownDelimiter[] delimiters;
        for (; i < buf.length; ++i)
        {
            const c = buf[i];
            switch (c)
            {
            case ' ':
            case '\t':
                break;
            case '\n':
                if (leadingBlank && !inCode)
                    newParagraph = true;
                leadingBlank = true;
                break;
            case '\\':
                ++i;
                break;
            case '#':
                if (leadingBlank && !inCode)
                    newParagraph = true;
                leadingBlank = false;
                break;
            case '>':
                if (leadingBlank && !inCode)
                    newParagraph = true;
                break;
            case '+':
                if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
                    newParagraph = true;
                else
                    leadingBlank = false;
                break;
            case '0':
            ..
            case '9':
                if (leadingBlank && !inCode)
                {
                    i = skipChars(buf, i, "0123456789");
                    if (i < buf.length &&
                        (buf[i] == '.' || buf[i] == ')') &&
                        isFollowedBySpace(buf, i))
                        newParagraph = true;
                    else
                        leadingBlank = false;
                }
                break;
            case '*':
                if (leadingBlank && !inCode)
                {
                    newParagraph = true;
                    if (!isFollowedBySpace(buf, i))
                        leadingBlank = false;
                }
                break;
            case '`':
            case '~':
                if (leadingBlank && i+2 < buf.length && buf[i+1] == c && buf[i+2] == c)
                {
                    inCode = inCode == c ? false : c;
                    i = skipChars(buf, i, [c]) - 1;
                    newParagraph = true;
                }
                leadingBlank = false;
                break;
            case '-':
                if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
                    goto case '+';
                else
                    goto case '`';
            case '[':
                if (leadingBlank && !inCode && newParagraph)
                    delimiters ~= MarkdownDelimiter(i, 1, 0, false, false, true, c);
                break;
            case ']':
                if (delimiters.length && !inCode &&
                    MarkdownLink.replaceReferenceDefinition(buf, i, delimiters, cast(int) delimiters.length - 1, this, loc))
                    --i;
                break;
            default:
                if (leadingBlank)
                    newParagraph = false;
                leadingBlank = false;
                break;
            }
        }
        extractedAll = true;
    }

    /**
     * Split a string by a delimiter, excluding the delimiter.
     * Params:
     *  s         = the string to split
     *  delimiter = the character to split by
     * Returns: the resulting array of strings
     */
    private static string[] split(string s, char delimiter) pure @safe
    {
        string[] result;
        size_t iStart = 0;
        foreach (size_t i; 0..s.length)
            if (s[i] == delimiter)
            {
                result ~= s[iStart..i];
                iStart = i + 1;
            }
        result ~= s[iStart..$];
        return result;
    }

    ///
    unittest
    {
        assert(split("", ',') == [""]);
        assert(split("ab", ',') == ["ab"]);
        assert(split("a,b", ',') == ["a", "b"]);
        assert(split("a,,b", ',') == ["a", "", "b"]);
        assert(split(",ab", ',') == ["", "ab"]);
        assert(split("ab,", ',') == ["ab", ""]);
    }

    /**
     * Create a HREF for the given D symbol.
     * The HREF is relative to the current location if possible.
     * Params:
     *  symbol    = the symbol to create a HREF for.
     * Returns: the resulting href
     */
    private string createHref(Dsymbol symbol)
    {
        Dsymbol root = symbol;

        const(char)[] lref;
        while (symbol && symbol.ident && !symbol.isModule())
        {
            if (lref.length)
                lref = '.' ~ lref;
            lref = symbol.ident.toString() ~ lref;
            symbol = symbol.parent;
        }

        const(char)[] path;
        if (symbol && symbol.ident && symbol.isModule() != _scope._module)
        {
            do
            {
                root = symbol;

                // If the module has a file name, we're done
                if (const m = symbol.isModule())
                    if (m.docfile)
                    {
                        path = m.docfile.toString();
                        break;
                    }

                if (path.length)
                    path = '_' ~ path;
                path = symbol.ident.toString() ~ path;
                symbol = symbol.parent;
            } while (symbol && symbol.ident);

            if (!symbol && path.length)
                path ~= "$(DOC_EXTENSION)";
        }

        // Attempt an absolute URL if not in the same package
        while (root.parent)
            root = root.parent;
        Dsymbol scopeRoot = _scope._module;
        while (scopeRoot.parent)
            scopeRoot = scopeRoot.parent;
        if (scopeRoot != root)
        {
            path = "$(DOC_ROOT_" ~ root.ident.toString() ~ ')' ~ path;
            lref = '.' ~ lref;  // remote URIs like Phobos and Mir use .prefixes
        }

        return cast(string) (path ~ '#' ~ lref);
    }
}

enum TableColumnAlignment
{
    none,
    left,
    center,
    right
}

/****************************************************
 * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |`
 * where the example text has four columns with the following alignments:
 * default, left, center, and right. The first and last pipes are optional. If a
 * delimiter row is found it will be removed from `buf`.
 *
 * Params:
 *  buf     = an OutBuffer containing the DDoc
 *  iStart  = the index within `buf` that the delimiter row starts at
 *  inQuote   = whether the table is inside a quote
 *  columnAlignments = alignments to populate for each column
 * Returns: the index of the end of the parsed delimiter, or `0` if not found
 */
size_t parseTableDelimiterRow(ref OutBuffer buf, const size_t iStart, bool inQuote, ref TableColumnAlignment[] columnAlignments) @safe
{
    size_t i = skipChars(buf, iStart, inQuote ? ">| \t" : "| \t");
    while (i < buf.length && buf[i] != '\r' && buf[i] != '\n')
    {
        const leftColon = buf[i] == ':';
        if (leftColon)
            ++i;

        if (i >= buf.length || buf[i] != '-')
            break;
        i = skipChars(buf, i, "-");

        const rightColon = i < buf.length && buf[i] == ':';
        i = skipChars(buf, i, ": \t");

        if (i >= buf.length || (buf[i] != '|' && buf[i] != '\r' && buf[i] != '\n'))
            break;
        i = skipChars(buf, i, "| \t");

        columnAlignments ~= (leftColon && rightColon) ? TableColumnAlignment.center :
                leftColon ? TableColumnAlignment.left :
                rightColon ? TableColumnAlignment.right :
                TableColumnAlignment.none;
    }

    if (i < buf.length && buf[i] != '\r' && buf[i] != '\n' && buf[i] != ')')
    {
        columnAlignments.length = 0;
        return 0;
    }

    if (i < buf.length && buf[i] == '\r') ++i;
    if (i < buf.length && buf[i] == '\n') ++i;
    return i;
}

/****************************************************
 * Look for a table delimiter row, and if found parse the previous row as a
 * table header row. If both exist with a matching number of columns, start a
 * table.
 *
 * Params:
 *  buf       = an OutBuffer containing the DDoc
 *  iStart    = the index within `buf` that the table header row starts at, inclusive
 *  iEnd      = the index within `buf` that the table header row ends at, exclusive
 *  loc       = the current location in the file
 *  inQuote   = whether the table is inside a quote
 *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
 *  columnAlignments = the parsed alignments for each column
 * Returns: the number of characters added by starting the table, or `0` if unchanged
 */
size_t startTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, bool inQuote, ref MarkdownDelimiter[] inlineDelimiters, out TableColumnAlignment[] columnAlignments)
{
    const iDelimiterRowEnd = parseTableDelimiterRow(buf, iEnd + 1, inQuote, columnAlignments);
    if (iDelimiterRowEnd)
    {
        size_t delta;
        if (replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, true, delta))
        {
            buf.remove(iEnd + delta, iDelimiterRowEnd - iEnd);
            buf.insert(iEnd + delta, "$(TBODY ");
            buf.insert(iStart, "$(TABLE ");
            return delta + 15;
        }
    }

    columnAlignments.length = 0;
    return 0;
}

/****************************************************
 * Replace a Markdown table row in the form of table cells delimited by pipes:
 * `| cell | cell | cell`. The first and last pipes are optional.
 *
 * Params:
 *  buf       = an OutBuffer containing the DDoc
 *  iStart    = the index within `buf` that the table row starts at, inclusive
 *  iEnd      = the index within `buf` that the table row ends at, exclusive
 *  loc       = the current location in the file
 *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
 *  columnAlignments = alignments for each column
 *  headerRow = if `true` then the number of columns will be enforced to match
 *              `columnAlignments.length` and the row will be surrounded by a
 *              `THEAD` macro
 *  delta     = the number of characters added by replacing the row, or `0` if unchanged
 * Returns: `true` if a table row was found and replaced
 */
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)
{
    delta = 0;

    if (!columnAlignments.length || iStart == iEnd)
        return false;

    iStart = skipChars(buf, iStart, " \t");
    int cellCount = 0;
    foreach (delimiter; inlineDelimiters)
        if (delimiter.type == '|' && !delimiter.leftFlanking)
            ++cellCount;
    bool ignoreLast = inlineDelimiters.length > 0 && inlineDelimiters[$-1].type == '|';
    if (ignoreLast)
    {
        const iLast = skipChars(buf, inlineDelimiters[$-1].iStart + inlineDelimiters[$-1].count, " \t");
        ignoreLast = iLast >= iEnd;
    }
    if (!ignoreLast)
        ++cellCount;

    if (headerRow && cellCount != columnAlignments.length)
        return false;

    void replaceTableCell(size_t iCellStart, size_t iCellEnd, int cellIndex, int di)
    {
        const eDelta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, di);
        delta += eDelta;
        iCellEnd += eDelta;

        // strip trailing whitespace and delimiter
        size_t i = iCellEnd - 1;
        while (i > iCellStart && (buf[i] == '|' || buf[i] == ' ' || buf[i] == '\t'))
            --i;
        ++i;
        buf.remove(i, iCellEnd - i);
        delta -= iCellEnd - i;
        iCellEnd = i;

        buf.insert(iCellEnd, ")");
        ++delta;

        // strip initial whitespace and delimiter
        i = skipChars(buf, iCellStart, "| \t");
        buf.remove(iCellStart, i - iCellStart);
        delta -= i - iCellStart;

        switch (columnAlignments[cellIndex])
        {
        case TableColumnAlignment.none:
            buf.insert(iCellStart, headerRow ? "$(TH " : "$(TD ");
            delta += 5;
            break;
        case TableColumnAlignment.left:
            buf.insert(iCellStart, "left, ");
            delta += 6;
            goto default;
        case TableColumnAlignment.center:
            buf.insert(iCellStart, "center, ");
            delta += 8;
            goto default;
        case TableColumnAlignment.right:
            buf.insert(iCellStart, "right, ");
            delta += 7;
            goto default;
        default:
            buf.insert(iCellStart, headerRow ? "$(TH_ALIGN " : "$(TD_ALIGN ");
            delta += 11;
            break;
        }
    }

    int cellIndex = cellCount - 1;
    size_t iCellEnd = iEnd;
    foreach_reverse (di, delimiter; inlineDelimiters)
    {
        if (delimiter.type == '|')
        {
            if (ignoreLast && di == inlineDelimiters.length-1)
            {
                ignoreLast = false;
                continue;
            }

            if (cellIndex >= columnAlignments.length)
            {
                // kill any extra cells
                buf.remove(delimiter.iStart, iEnd + delta - delimiter.iStart);
                delta -= iEnd + delta - delimiter.iStart;
                iCellEnd = iEnd + delta;
                --cellIndex;
                continue;
            }

            replaceTableCell(delimiter.iStart, iCellEnd, cellIndex, cast(int) di);
            iCellEnd = delimiter.iStart;
            --cellIndex;
        }
    }

    // if no starting pipe, replace from the start
    if (cellIndex >= 0)
        replaceTableCell(iStart, iCellEnd, cellIndex, 0);

    buf.insert(iEnd + delta, ")");
    buf.insert(iStart, "$(TR ");
    delta += 6;

    if (headerRow)
    {
        buf.insert(iEnd + delta, ")");
        buf.insert(iStart, "$(THEAD ");
        delta += 9;
    }

    return true;
}

/****************************************************
 * End a table, if in one.
 *
 * Params:
 *  buf = an OutBuffer containing the DDoc
 *  i   = the index within `buf` to end the table at
 *  columnAlignments = alignments for each column; upon return is set to length `0`
 * Returns: the number of characters added by ending the table, or `0` if unchanged
 */
size_t endTable(ref OutBuffer buf, size_t i, ref TableColumnAlignment[] columnAlignments)
{
    if (!columnAlignments.length)
        return 0;

    buf.insert(i, "))");
    columnAlignments.length = 0;
    return 2;
}

/****************************************************
 * End a table row and then the table itself.
 *
 * Params:
 *  buf       = an OutBuffer containing the DDoc
 *  iStart    = the index within `buf` that the table row starts at, inclusive
 *  iEnd      = the index within `buf` that the table row ends at, exclusive
 *  loc       = the current location in the file
 *  inlineDelimiters = delimiters containing columns separators and any inline emphasis
 *  columnAlignments = alignments for each column; upon return is set to length `0`
 * Returns: the number of characters added by replacing the row, or `0` if unchanged
 */
size_t endRowAndTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, ref TableColumnAlignment[] columnAlignments)
{
    size_t delta;
    replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, false, delta);
    delta += endTable(buf, iEnd + delta, columnAlignments);
    return delta;
}

/**************************************************
 * Highlight text section.
 *
 * Params:
 *  scope = the current parse scope
 *  a     = an array of D symbols at the current scope
 *  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.
 *  buf   = an OutBuffer containing the DDoc
 *  offset = the index within buf to start highlighting
 */
void highlightText(Scope* sc, Dsymbols* a, Loc loc, ref OutBuffer buf, size_t offset)
{
    const incrementLoc = loc.linnum == 0 ? 1 : 0;
    loc.linnum = loc.linnum + incrementLoc;
    loc.charnum = 0;
    //printf("highlightText()\n");
    bool leadingBlank = true;
    size_t iParagraphStart = offset;
    size_t iPrecedingBlankLine = 0;
    int headingLevel = 0;
    int headingMacroLevel = 0;
    int quoteLevel = 0;
    bool lineQuoted = false;
    int quoteMacroLevel = 0;
    MarkdownList[] nestedLists;
    MarkdownDelimiter[] inlineDelimiters;
    MarkdownLinkReferences linkReferences;
    TableColumnAlignment[] columnAlignments;
    bool tableRowDetected = false;
    int inCode = 0;
    int inBacktick = 0;
    int macroLevel = 0;
    int previousMacroLevel = 0;
    int parenLevel = 0;
    size_t iCodeStart = 0; // start of code section
    size_t codeFenceLength = 0;
    size_t codeIndent = 0;
    string codeLanguage;
    size_t iLineStart = offset;
    linkReferences._scope = sc;
    for (size_t i = offset; i < buf.length; i++)
    {
        char c = buf[i];
    Lcont:
        switch (c)
        {
        case ' ':
        case '\t':
            break;
        case '\n':
            if (inBacktick)
            {
                // `inline code` is only valid if contained on a single line
                // otherwise, the backticks should be output literally.
                //
                // This lets things like `output from the linker' display
                // unmolested while keeping the feature consistent with GitHub.
                inBacktick = false;
                inCode = false; // the backtick also assumes we're in code
                // Nothing else is necessary since the DDOC_BACKQUOTED macro is
                // inserted lazily at the close quote, meaning the rest of the
                // text is already OK.
            }
            if (headingLevel)
            {
                i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
                endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
                removeBlankLineMacro(buf, iPrecedingBlankLine, i);
                ++i;
                iParagraphStart = skipChars(buf, i, " \t\r\n");
            }

            if (tableRowDetected && !columnAlignments.length)
                i += startTable(buf, iLineStart, i, loc, lineQuoted, inlineDelimiters, columnAlignments);
            else if (columnAlignments.length)
            {
                size_t delta;
                if (replaceTableRow(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments, false, delta))
                    i += delta;
                else
                    i += endTable(buf, i, columnAlignments);
            }

            if (!inCode && nestedLists.length && !quoteLevel)
                MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);

            iPrecedingBlankLine = 0;
            if (!inCode && i == iLineStart && i + 1 < buf.length) // if "\n\n"
            {
                i += endTable(buf, i, columnAlignments);
                if (!lineQuoted && quoteLevel)
                    endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
                i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);

                // if we don't already know about this paragraph break then
                // insert a blank line and record the paragraph break
                if (iParagraphStart <= i)
                {
                    iPrecedingBlankLine = i;
                    i = buf.insert(i, "$(DDOC_BLANKLINE)");
                    iParagraphStart = i + 1;
                }
            }
            else if (inCode &&
                i == iLineStart &&
                i + 1 < buf.length &&
                !lineQuoted &&
                quoteLevel) // if "\n\n" in quoted code
            {
                inCode = false;
                i = buf.insert(i, ")");
                i += endAllMarkdownQuotes(buf, i, quoteLevel);
                quoteMacroLevel = 0;
            }
            leadingBlank = true;
            lineQuoted = false;
            tableRowDetected = false;
            iLineStart = i + 1;
            loc.linnum = loc.linnum + incrementLoc;

            // update the paragraph start if we just entered a macro
            if (previousMacroLevel < macroLevel && iParagraphStart < iLineStart)
                iParagraphStart = iLineStart;
            previousMacroLevel = macroLevel;
            break;

        case '<':
            {
                leadingBlank = false;
                if (inCode)
                    break;
                const slice = buf[];
                auto p = &slice[i];
                const se = sc._module.escapetable.escapeChar('<');
                if (se == "&lt;")
                {
                    // Generating HTML
                    // Skip over comments
                    if (p[1] == '!' && p[2] == '-' && p[3] == '-')
                    {
                        size_t j = i + 4;
                        p += 4;
                        while (1)
                        {
                            if (j == slice.length)
                                goto L1;
                            if (p[0] == '-' && p[1] == '-' && p[2] == '>')
                            {
                                i = j + 2; // place on closing '>'
                                break;
                            }
                            j++;
                            p++;
                        }
                        break;
                    }
                    // Skip over HTML tag
                    if (isalpha(p[1]) || (p[1] == '/' && isalpha(p[2])))
                    {
                        size_t j = i + 2;
                        p += 2;
                        while (1)
                        {
                            if (j == slice.length)
                                break;
                            if (p[0] == '>')
                            {
                                i = j; // place on closing '>'
                                break;
                            }
                            j++;
                            p++;
                        }
                        break;
                    }
                }
            L1:
                // Replace '<' with '&lt;' character entity
                if (se.length)
                {
                    buf.remove(i, 1);
                    i = buf.insert(i, se);
                    i--; // point to ';'
                }
                break;
            }

        case '>':
            {
                if (leadingBlank && (!inCode || quoteLevel))
                {
                    lineQuoted = true;
                    int lineQuoteLevel = 1;
                    size_t iAfterDelimiters = i + 1;
                    for (; iAfterDelimiters < buf.length; ++iAfterDelimiters)
                    {
                        const c0 = buf[iAfterDelimiters];
                        if (c0 == '>')
                            ++lineQuoteLevel;
                        else if (c0 != ' ' && c0 != '\t')
                            break;
                    }
                    if (!quoteMacroLevel)
                        quoteMacroLevel = macroLevel;
                    buf.remove(i, iAfterDelimiters - i);

                    if (quoteLevel < lineQuoteLevel)
                    {
                        i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                        if (nestedLists.length)
                        {
                            const indent = getMarkdownIndent(buf, iLineStart, i);
                            if (indent < nestedLists[$-1].contentIndent)
                                i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
                        }

                        for (; quoteLevel < lineQuoteLevel; ++quoteLevel)
                        {
                            i = buf.insert(i, "$(BLOCKQUOTE\n");
                            iLineStart = iParagraphStart = i;
                        }
                        --i;
                    }
                    else
                    {
                        --i;
                        if (nestedLists.length)
                            MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);
                    }
                    break;
                }

                leadingBlank = false;
                if (inCode)
                    break;
                // Replace '>' with '&gt;' character entity
                const se = sc._module.escapetable.escapeChar('>');
                if (se.length)
                {
                    buf.remove(i, 1);
                    i = buf.insert(i, se);
                    i--; // point to ';'
                }
                break;
            }

        case '&':
            {
                leadingBlank = false;
                if (inCode)
                    break;
                char* p = cast(char*)&buf[].ptr[i];
                if (p[1] == '#' || isalpha(p[1]))
                    break;
                // already a character entity
                // Replace '&' with '&amp;' character entity
                const se = sc._module.escapetable.escapeChar('&');
                if (se)
                {
                    buf.remove(i, 1);
                    i = buf.insert(i, se);
                    i--; // point to ';'
                }
                break;
            }

        case '`':
            {
                const iAfterDelimiter = skipChars(buf, i, "`");
                const count = iAfterDelimiter - i;

                if (inBacktick == count)
                {
                    inBacktick = 0;
                    inCode = 0;
                    OutBuffer codebuf;
                    codebuf.write(buf[iCodeStart + count .. i]);
                    // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
                    highlightCode(sc, a, codebuf, 0);
                    escapeStrayParenthesis(loc, codebuf, 0, false, sc.eSink);
                    buf.remove(iCodeStart, i - iCodeStart + count); // also trimming off the current `
                    immutable pre = "$(DDOC_BACKQUOTED ";
                    i = buf.insert(iCodeStart, pre);
                    i = buf.insert(i, codebuf[]);
                    i = buf.insert(i, ")");
                    i--; // point to the ending ) so when the for loop does i++, it will see the next character
                    break;
                }

                // Perhaps we're starting or ending a Markdown code block
                if (leadingBlank && count >= 3)
                {
                    bool moreBackticks = false;
                    for (size_t j = iAfterDelimiter; !moreBackticks && j < buf.length; ++j)
                        if (buf[j] == '`')
                            moreBackticks = true;
                        else if (buf[j] == '\r' || buf[j] == '\n')
                            break;
                    if (!moreBackticks)
                        goto case '-';
                }

                if (inCode)
                {
                    if (inBacktick)
                        i = iAfterDelimiter - 1;
                    break;
                }
                inCode = c;
                inBacktick = cast(int) count;
                codeIndent = 0; // inline code is not indented
                // All we do here is set the code flags and record
                // the location. The macro will be inserted lazily
                // so we can easily cancel the inBacktick if we come
                // across a newline character.
                iCodeStart = i;
                i = iAfterDelimiter - 1;
                break;
            }

        case '#':
        {
            /* A line beginning with # indicates an ATX-style heading. */
            if (leadingBlank && !inCode)
            {
                leadingBlank = false;

                headingLevel = detectAtxHeadingLevel(buf, i);
                if (!headingLevel)
                    break;

                i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                if (!lineQuoted && quoteLevel)
                    i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);

                // remove the ### prefix, including whitespace
                i = skipChars(buf, i + headingLevel, " \t");
                buf.remove(iLineStart, i - iLineStart);
                i = iParagraphStart = iLineStart;

                removeAnyAtxHeadingSuffix(buf, i);
                --i;

                headingMacroLevel = macroLevel;
            }
            break;
        }

        case '~':
            {
                if (leadingBlank)
                {
                    // Perhaps we're starting or ending a Markdown code block
                    const iAfterDelimiter = skipChars(buf, i, "~");
                    if (iAfterDelimiter - i >= 3)
                        goto case '-';
                }
                leadingBlank = false;
                break;
            }

        case '-':
            /* A line beginning with --- delimits a code section.
             * inCode tells us if it is start or end of a code section.
             */
            if (leadingBlank)
            {
                if (!inCode && c == '-')
                {
                    const list = MarkdownList.parseItem(buf, iLineStart, i);
                    if (list.isValid)
                    {
                        if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
                        {
                            removeBlankLineMacro(buf, iPrecedingBlankLine, i);
                            iParagraphStart = skipChars(buf, i+1, " \t\r\n");
                            break;
                        }
                        else
                            goto case '+';
                    }
                }

                size_t istart = i;
                size_t eollen = 0;
                leadingBlank = false;
                const c0 = c; // if we jumped here from case '`' or case '~'
                size_t iInfoString = 0;
                if (!inCode)
                    codeLanguage.length = 0;
                while (1)
                {
                    ++i;
                    if (i >= buf.length)
                        break;
                    c = buf[i];
                    if (c == '\n')
                    {
                        eollen = 1;
                        break;
                    }
                    if (c == '\r')
                    {
                        eollen = 1;
                        if (i + 1 >= buf.length)
                            break;
                        if (buf[i + 1] == '\n')
                        {
                            eollen = 2;
                            break;
                        }
                    }
                    // BUG: handle UTF PS and LS too
                    if (c != c0 || iInfoString)
                    {
                        if (!iInfoString && !inCode && i - istart >= 3)
                        {
                            // Start a Markdown info string, like ```ruby
                            codeFenceLength = i - istart;
                            i = iInfoString = skipChars(buf, i, " \t");
                        }
                        else if (iInfoString && c != '`')
                        {
                            if (!codeLanguage.length && (c == ' ' || c == '\t'))
                                codeLanguage = cast(string) buf[iInfoString..i].idup;
                        }
                        else
                        {
                            iInfoString = 0;
                            goto Lcont;
                        }
                    }
                }
                if (i - istart < 3 || (inCode && (inCode != c0 || (inCode != '-' && i - istart < codeFenceLength))))
                    goto Lcont;
                if (iInfoString)
                {
                    if (!codeLanguage.length)
                        codeLanguage = cast(string) buf[iInfoString..i].idup;
                }
                else
                    codeFenceLength = i - istart;

                // We have the start/end of a code section
                // Remove the entire --- line, including blanks and \n
                buf.remove(iLineStart, i - iLineStart + eollen);
                i = iLineStart;
                if (eollen)
                    leadingBlank = true;
                if (inCode && (i <= iCodeStart))
                {
                    // Empty code section, just remove it completely.
                    inCode = 0;
                    break;
                }
                if (inCode)
                {
                    inCode = 0;
                    // The code section is from iCodeStart to i
                    OutBuffer codebuf;
                    codebuf.write(buf[iCodeStart .. i]);
                    codebuf.writeByte(0);
                    // Remove leading indentations from all lines
                    bool lineStart = true;
                    char* endp = cast(char*)codebuf[].ptr + codebuf.length;
                    for (char* p = cast(char*)codebuf[].ptr; p < endp;)
                    {
                        if (lineStart)
                        {
                            size_t j = codeIndent;
                            char* q = p;
                            while (j-- > 0 && q < endp && isIndentWS(q))
                                ++q;
                            codebuf.remove(p - cast(char*)codebuf[].ptr, q - p);
                            assert(cast(char*)codebuf[].ptr <= p);
                            assert(p < cast(char*)codebuf[].ptr + codebuf.length);
                            lineStart = false;
                            endp = cast(char*)codebuf[].ptr + codebuf.length; // update
                            continue;
                        }
                        if (*p == '\n')
                            lineStart = true;
                        ++p;
                    }
                    if (!codeLanguage.length || codeLanguage == "dlang" || codeLanguage == "d")
                        highlightCode2(sc, a, codebuf, 0);
                    else
                        codebuf.remove(codebuf.length-1, 1);    // remove the trailing 0 byte
                    escapeStrayParenthesis(loc, codebuf, 0, false, sc.eSink);
                    buf.remove(iCodeStart, i - iCodeStart);
                    i = buf.insert(iCodeStart, codebuf[]);
                    i = buf.insert(i, ")\n");
                    i -= 2; // in next loop, c should be '\n'
                }
                else
                {
                    i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                    if (!lineQuoted && quoteLevel)
                    {
                        const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
                        i += delta;
                        istart += delta;
                    }

                    inCode = c0;
                    codeIndent = istart - iLineStart; // save indent count
                    if (codeLanguage.length && codeLanguage != "dlang" && codeLanguage != "d")
                    {
                        // backslash-escape
                        for (size_t j; j < codeLanguage.length - 1; ++j)
                            if (codeLanguage[j] == '\\' && ispunct(codeLanguage[j + 1]))
                                codeLanguage = codeLanguage[0..j] ~ codeLanguage[j + 1..$];

                        i = buf.insert(i, "$(OTHER_CODE ");
                        i = buf.insert(i, codeLanguage);
                        i = buf.insert(i, ",");
                    }
                    else
                        i = buf.insert(i, "$(D_CODE ");
                    iCodeStart = i;
                    i--; // place i on >
                    leadingBlank = true;
                }
            }
            break;

        case '_':
        {
            if (leadingBlank && !inCode && replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
            {
                i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                if (!lineQuoted && quoteLevel)
                    i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
                removeBlankLineMacro(buf, iPrecedingBlankLine, i);
                iParagraphStart = skipChars(buf, i+1, " \t\r\n");
                break;
            }
            goto default;
        }

        case '+':
        case '0':
        ..
        case '9':
        {
            if (leadingBlank && !inCode)
            {
                MarkdownList list = MarkdownList.parseItem(buf, iLineStart, i);
                if (list.isValid)
                {
                    // Avoid starting a numbered list in the middle of a paragraph
                    if (!nestedLists.length && list.orderedStart.length &&
                        iParagraphStart < iLineStart)
                    {
                        i += list.orderedStart.length - 1;
                        break;
                    }

                    i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                    if (!lineQuoted && quoteLevel)
                    {
                        const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
                        i += delta;
                        list.iStart += delta;
                        list.iContentStart += delta;
                    }

                    list.macroLevel = macroLevel;
                    list.startItem(buf, iLineStart, i, iPrecedingBlankLine, nestedLists, loc);
                    break;
                }
            }
            leadingBlank = false;
            break;
        }

        case '*':
        {
            if (inCode || inBacktick)
            {
                leadingBlank = false;
                break;
            }

            if (leadingBlank)
            {
                // Check for a thematic break
                if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
                {
                    i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                    if (!lineQuoted && quoteLevel)
                        i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
                    removeBlankLineMacro(buf, iPrecedingBlankLine, i);
                    iParagraphStart = skipChars(buf, i+1, " \t\r\n");
                    break;
                }

                // An initial * indicates a Markdown list item
                const list = MarkdownList.parseItem(buf, iLineStart, i);
                if (list.isValid)
                    goto case '+';
            }

            // Markdown emphasis
            const leftC = i > offset ? buf[i-1] : '\0';
            size_t iAfterEmphasis = skipChars(buf, i+1, "*");
            const rightC = iAfterEmphasis < buf.length ? buf[iAfterEmphasis] : '\0';
            int count = cast(int) (iAfterEmphasis - i);
            const leftFlanking = (rightC != '\0' && !isspace(rightC)) && (!ispunct(rightC) || leftC == '\0' || isspace(leftC) || ispunct(leftC));
            const rightFlanking = (leftC != '\0' && !isspace(leftC)) && (!ispunct(leftC) || rightC == '\0' || isspace(rightC) || ispunct(rightC));
            auto emphasis = MarkdownDelimiter(i, count, macroLevel, leftFlanking, rightFlanking, false, c);

            if (!emphasis.leftFlanking && !emphasis.rightFlanking)
            {
                i = iAfterEmphasis - 1;
                break;
            }

            inlineDelimiters ~= emphasis;
            i += emphasis.count;
            --i;
            break;
        }

        case '!':
        {
            leadingBlank = false;

            if (inCode)
                break;

            if (i < buf.length-1 && buf[i+1] == '[')
            {
                const imageStart = MarkdownDelimiter(i, 2, macroLevel, false, false, false, c);
                inlineDelimiters ~= imageStart;
                ++i;
            }
            break;
        }
        case '[':
        {
            if (inCode)
            {
                leadingBlank = false;
                break;
            }

            const leftC = i > offset ? buf[i-1] : '\0';
            const rightFlanking = leftC != '\0' && !isspace(leftC) && !ispunct(leftC);
            const atParagraphStart = leadingBlank && iParagraphStart >= iLineStart;
            const linkStart = MarkdownDelimiter(i, 1, macroLevel, false, rightFlanking, atParagraphStart, c);
            inlineDelimiters ~= linkStart;
            leadingBlank = false;
            break;
        }
        case ']':
        {
            leadingBlank = false;

            if (inCode)
                break;

            for (int d = cast(int) inlineDelimiters.length - 1; d >= 0; --d)
            {
                const delimiter = inlineDelimiters[d];
                if (delimiter.type == '[' || delimiter.type == '!')
                {
                    if (delimiter.isValid &&
                        MarkdownLink.replaceLink(buf, i, loc, inlineDelimiters, d, linkReferences))
                    {
                        // if we removed a reference link then we're at line start
                        if (i <= delimiter.iStart)
                            leadingBlank = true;

                        // don't nest links
                        if (delimiter.type == '[')
                            for (--d; d >= 0; --d)
                                if (inlineDelimiters[d].type == '[')
                                    inlineDelimiters[d].invalidate();
                    }
                    else
                    {
                        // nothing found, so kill the delimiter
                        inlineDelimiters = inlineDelimiters[0..d] ~ inlineDelimiters[d+1..$];
                    }
                    break;
                }
            }
            break;
        }

        case '|':
        {
            if (inCode)
            {
                leadingBlank = false;
                break;
            }

            tableRowDetected = true;
            inlineDelimiters ~= MarkdownDelimiter(i, 1, macroLevel, leadingBlank, false, false, c);
            leadingBlank = false;
            break;
        }

        case '\\':
        {
            leadingBlank = false;
            if (inCode || i+1 >= buf.length)
                break;

            /* Escape Markdown special characters */
            char c1 = buf[i+1];
            if (ispunct(c1))
            {
                buf.remove(i, 1);

                auto se = sc._module.escapetable.escapeChar(c1);
                if (!se)
                    se = c1 == '$' ? "$(DOLLAR)" : c1 == ',' ? "$(COMMA)" : null;
                if (se)
                {
                    buf.remove(i, 1);
                    i = buf.insert(i, se);
                    i--; // point to escaped char
                }
            }
            break;
        }

        case '$':
        {
            /* Look for the start of a macro, '$(Identifier'
             */
            leadingBlank = false;
            if (inCode || inBacktick)
                break;
            const slice = buf[];
            auto p = &slice[i];
            if (p[1] == '(' && isIdStart(&p[2]))
                ++macroLevel;
            break;
        }

        case '(':
        {
            if (!inCode && i > offset && buf[i-1] != '$')
                ++parenLevel;
            break;
        }

        case ')':
        {   /* End of macro
             */
            leadingBlank = false;
            if (inCode || inBacktick)
                break;
            if (parenLevel > 0)
                --parenLevel;
            else if (macroLevel)
            {
                int downToLevel = cast(int) inlineDelimiters.length;
                while (downToLevel > 0 && inlineDelimiters[downToLevel - 1].macroLevel >= macroLevel)
                    --downToLevel;
                if (headingLevel && headingMacroLevel >= macroLevel)
                {
                    endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
                    removeBlankLineMacro(buf, iPrecedingBlankLine, i);
                }
                i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
                while (nestedLists.length && nestedLists[$-1].macroLevel >= macroLevel)
                {
                    i = buf.insert(i, ")\n)");
                    --nestedLists.length;
                }
                if (quoteLevel && quoteMacroLevel >= macroLevel)
                    i += endAllMarkdownQuotes(buf, i, quoteLevel);
                i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters, downToLevel);

                --macroLevel;
                quoteMacroLevel = 0;
            }
            break;
        }

        default:
            leadingBlank = false;
            if (sc._module.filetype == FileType.ddoc || inCode)
                break;
            const start = cast(char*)buf[].ptr + i;
            if (isIdStart(start))
            {
                size_t j = skippastident(buf, i);
                if (i < j)
                {
                    size_t k = skippastURL(buf, i);
                    if (i < k)
                    {
                        /* The URL is buf[i..k]
                         */
                        if (macroLevel)
                            /* Leave alone if already in a macro
                             */
                            i = k - 1;
                        else
                        {
                            /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)'
                             */
                            i = buf.bracket(i, "$(DDOC_LINK_AUTODETECT ", k, ")") - 1;
                        }
                        break;
                    }
                }
                else
                    break;
                size_t len = j - i;
                // leading '_' means no highlight unless it's a reserved symbol name
                if (c == '_' && (i == 0 || !isdigit(*(start - 1))) && (i == buf.length - 1 || !isReservedName(start[0 .. len])))
                {
                    buf.remove(i, 1);
                    i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j - 1, ")") - 1;
                    break;
                }
                if (isIdentifier(a, start[0 .. len]))
                {
                    i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL ", j, ")") - 1;
                    break;
                }
                if (isKeyword(start[0 .. len]))
                {
                    i = buf.bracket(i, "$(DDOC_AUTO_KEYWORD ", j, ")") - 1;
                    break;
                }
                if (isFunctionParameter(a, start[0 .. len]))
                {
                    //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
                    i = buf.bracket(i, "$(DDOC_AUTO_PARAM ", j, ")") - 1;
                    break;
                }
                i = j - 1;
            }
            break;
        }
    }

    if (inCode == '-')
        sc.eSink.error(loc, "unmatched `---` in DDoc comment");
    else if (inCode)
        buf.insert(buf.length, ")");

    size_t i = buf.length;
    if (headingLevel)
    {
        endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
        removeBlankLineMacro(buf, iPrecedingBlankLine, i);
    }
    i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
    i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
    endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
}

/**************************************************
 * Highlight code for DDOC section.
 */
void highlightCode(Scope* sc, Dsymbol s, ref OutBuffer buf, size_t offset)
{
    auto imp = s.isImport();
    if (imp && imp.aliases.length > 0)
    {
        // For example: `public import core.stdc.string : memcpy, memcmp;`
        for(int i = 0; i < imp.aliases.length; i++)
        {
            // Need to distinguish between
            // `public import core.stdc.string : memcpy, memcmp;` and
            // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
            auto a = imp.aliases[i];
            auto id = a ? a : imp.names[i];
            auto loc = Loc.init;
            Dsymbol pscopesym;
            if (auto symFromId = sc.search(loc, id, pscopesym))
            {
                highlightCode(sc, symFromId, buf, offset);
            }
        }
    }
    else
    {
        OutBuffer ancbuf;
        emitAnchor(ancbuf, s, sc);
        buf.insert(offset, ancbuf[]);
        offset += ancbuf.length;

        Dsymbols a;
        a.push(s);
        highlightCode(sc, &a, buf, offset);
    }
}

/****************************************************
 */
void highlightCode(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
{
    //printf("highlightCode(a = '%s')\n", a.toChars());
    bool resolvedTemplateParameters = false;

    for (size_t i = offset; i < buf.length; i++)
    {
        char c = buf[i];
        const se = sc._module.escapetable.escapeChar(c);
        if (se.length)
        {
            buf.remove(i, 1);
            i = buf.insert(i, se);
            i--; // point to ';'
            continue;
        }
        char* start = cast(char*)buf[].ptr + i;
        if (isIdStart(start))
        {
            size_t j = skipPastIdentWithDots(buf, i);
            if (i < j)
            {
                size_t len = j - i;
                if (isIdentifier(a, start[0 .. len]))
                {
                    i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
                    continue;
                }
            }

            j = skippastident(buf, i);
            if (i < j)
            {
                size_t len = j - i;
                if (isIdentifier(a, start[0 .. len]))
                {
                    i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
                    continue;
                }
                if (isFunctionParameter(a, start[0 .. len]))
                {
                    //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
                    i = buf.bracket(i, "$(DDOC_PARAM ", j, ")") - 1;
                    continue;
                }
                i = j - 1;
            }
        }
        else if (!resolvedTemplateParameters)
        {
            size_t previ = i;

            // hunt for template declarations:
            foreach (symi; 0 .. a.length)
            {
                FuncDeclaration fd = (*a)[symi].isFuncDeclaration();

                if (!fd || !fd.parent || !fd.parent.isTemplateDeclaration())
                {
                    continue;
                }

                TemplateDeclaration td = fd.parent.isTemplateDeclaration();

                // build the template parameters
                Array!(size_t) paramLens;
                paramLens.reserve(td.parameters.length);

                OutBuffer parametersBuf;
                HdrGenState hgs;

                parametersBuf.writeByte('(');

                foreach (parami; 0 .. td.parameters.length)
                {
                    TemplateParameter tp = (*td.parameters)[parami];

                    if (parami)
                        parametersBuf.writestring(", ");

                    size_t lastOffset = parametersBuf.length;

                    toCBuffer(tp, parametersBuf, hgs);

                    paramLens[parami] = parametersBuf.length - lastOffset;
                }
                parametersBuf.writeByte(')');

                const templateParams = parametersBuf[];

                //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start);
                if (start[0 .. templateParams.length] == templateParams)
                {
                    immutable templateParamListMacro = "$(DDOC_TEMPLATE_PARAM_LIST ";
                    buf.bracket(i, templateParamListMacro.ptr, i + templateParams.length, ")");

                    // We have the parameter list. While we're here we might
                    // as well wrap the parameters themselves as well

                    // + 1 here to take into account the opening paren of the
                    // template param list
                    i += templateParamListMacro.length + 1;

                    foreach (const len; paramLens)
                    {
                        i = buf.bracket(i, "$(DDOC_TEMPLATE_PARAM ", i + len, ")");
                        // increment two here for space + comma
                        i += 2;
                    }

                    resolvedTemplateParameters = true;
                    // reset i to be positioned back before we found the template
                    // param list this assures that anything within the template
                    // param list that needs to be escaped or otherwise altered
                    // has an opportunity for that to happen outside of this context
                    i = previ;

                    continue;
                }
            }
        }
    }
}

/****************************************
 */
void highlightCode3(Scope* sc, ref OutBuffer buf, const(char)* p, const(char)* pend)
{
    for (; p < pend; p++)
    {
        const se = sc._module.escapetable.escapeChar(*p);
        if (se.length)
            buf.writestring(se);
        else
            buf.writeByte(*p);
    }
}

/**************************************************
 * Highlight code for CODE section.
 */
void highlightCode2(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
{
    scope eSinkNull = new ErrorSinkNull();

    scope Lexer lex = new Lexer(null, cast(char*)buf[].ptr, 0, buf.length - 1, 0, 1,
        eSinkNull,  // ignore errors
        &global.compileEnv);
    OutBuffer res;
    const(char)* lastp = cast(char*)buf[].ptr;
    //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr);
    res.reserve(buf.length);
    while (1)
    {
        Token tok;
        lex.scan(&tok);
        highlightCode3(sc, res, lastp, tok.ptr);
        string highlight = null;
        switch (tok.value)
        {
        case TOK.identifier:
            {
                if (!sc)
                    break;
                size_t len = lex.p - tok.ptr;
                if (isIdentifier(a, tok.ptr[0 .. len]))
                {
                    highlight = "$(D_PSYMBOL ";
                    break;
                }
                if (isFunctionParameter(a, tok.ptr[0 .. len]))
                {
                    //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
                    highlight = "$(D_PARAM ";
                    break;
                }
                break;
            }
        case TOK.comment:
            highlight = "$(D_COMMENT ";
            break;
        case TOK.string_:
        case TOK.interpolated:
            highlight = "$(D_STRING ";
            break;
        default:
            if (tok.isKeyword())
                highlight = "$(D_KEYWORD ";
            break;
        }
        if (highlight)
        {
            res.writestring(highlight);
            size_t o = res.length;
            highlightCode3(sc, res, tok.ptr, lex.p);
            if (tok.value == TOK.comment || tok.value == TOK.string_ || tok.value == TOK.interpolated)
                /* https://issues.dlang.org/show_bug.cgi?id=7656
                 * https://issues.dlang.org/show_bug.cgi?id=7715
                 * https://issues.dlang.org/show_bug.cgi?id=10519
                 */
                escapeDdocString(res, o);
            res.writeByte(')');
        }
        else
            highlightCode3(sc, res, tok.ptr, lex.p);
        if (tok.value == TOK.endOfFile)
            break;
        lastp = lex.p;
    }
    buf.setsize(offset);
    buf.write(&res);
}

/****************************************
 * Determine if p points to the start of a "..." parameter identifier.
 */
bool isCVariadicArg(const(char)[] p) @nogc nothrow pure @safe
{
    return p.length >= 3 && p[0 .. 3] == "...";
}

/****************************************
 * Determine if p points to the start of an identifier.
 */
@trusted
bool isIdStart(const(char)* p) @nogc nothrow pure
{
    dchar c = *p;
    if (isalpha(c) || c == '_')
        return true;
    if (c >= 0x80)
    {
        size_t i = 0;
        if (utf_decodeChar(p[0 .. 4], i, c))
            return false; // ignore errors
        if (isUniAlpha(c))
            return true;
    }
    return false;
}

/****************************************
 * Determine if p points to the rest of an identifier.
 */
@trusted
bool isIdTail(const(char)* p) @nogc nothrow pure
{
    dchar c = *p;
    if (isalnum(c) || c == '_')
        return true;
    if (c >= 0x80)
    {
        size_t i = 0;
        if (utf_decodeChar(p[0 .. 4], i, c))
            return false; // ignore errors
        if (isUniAlpha(c))
            return true;
    }
    return false;
}

/****************************************
 * Determine if p points to the indentation space.
 */
bool isIndentWS(const(char)* p) @nogc nothrow pure @safe
{
    return (*p == ' ') || (*p == '\t');
}

/*****************************************
 * Return number of bytes in UTF character.
 */
int utfStride(const(char)* p) @nogc nothrow pure
{
    dchar c = *p;
    if (c < 0x80)
        return 1;
    size_t i = 0;
    utf_decodeChar(p[0 .. 4], i, c); // ignore errors, but still consume input
    return cast(int)i;
}

inout(char)* stripLeadingNewlines(inout(char)* s) @nogc nothrow pure
{
    while (s && *s == '\n' || *s == '\r')
        s++;

    return s;
}
