
/* Compiler implementation of the D programming language
 * Copyright (c) 1999-2014 by Digital Mars
 * All Rights Reserved
 * written by Walter Bright
 * http://www.digitalmars.com
 * Distributed under the Boost Software License, Version 1.0.
 * http://www.boost.org/LICENSE_1_0.txt
 * https://github.com/D-Programming-Language/dmd/blob/master/src/doc.c
 */

// This implements the Ddoc capability.

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <assert.h>

#include "rmem.h"
#include "root.h"
#include "port.h"
#include "aav.h"

#include "attrib.h"
#include "cond.h"
#include "mars.h"
#include "dsymbol.h"
#include "macro.h"
#include "template.h"
#include "lexer.h"
#include "aggregate.h"
#include "declaration.h"
#include "statement.h"
#include "enum.h"
#include "id.h"
#include "module.h"
#include "scope.h"
#include "hdrgen.h"
#include "doc.h"
#include "mtype.h"
#include "utf.h"

void emitMemberComments(ScopeDsymbol *sds, OutBuffer *buf, Scope *sc);
void toDocBuffer(Dsymbol *s, OutBuffer *buf, Scope *sc);
void emitComment(Dsymbol *s, OutBuffer *buf, Scope *sc);

struct Escape
{
    const char *strings[256];

    const char *escapeChar(unsigned c);
};

class Section
{
public:
    const utf8_t *name;
    size_t namelen;

    const utf8_t *body;
    size_t bodylen;

    int nooutput;

    virtual void write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf);
};

class ParamSection : public Section
{
public:
    void write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf);
};

class MacroSection : public Section
{
public:
    void write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf);
};

typedef Array<Section *> Sections;

struct DocComment
{
    Sections sections;             // Section*[]

    Section *summary;
    Section *copyright;
    Section *macros;
    Macro **pmacrotable;
    Escape **pescapetable;

    Dsymbols a;

    DocComment() :
       summary(NULL), copyright(NULL), macros(NULL), pmacrotable(NULL), pescapetable(NULL)
    { }

    static DocComment *parse(Scope *sc, Dsymbol *s, const utf8_t *comment);
    static void parseMacros(Escape **pescapetable, Macro **pmacrotable, const utf8_t *m, size_t mlen);
    static void parseEscapes(Escape **pescapetable, const utf8_t *textstart, size_t textlen);

    void parseSections(const utf8_t *comment);
    void writeSections(Scope *sc, Dsymbols *a, OutBuffer *buf);
};


int cmp(const char *stringz, const void *s, size_t slen);
int icmp(const char *stringz, const void *s, size_t slen);
bool isDitto(const utf8_t *comment);
const utf8_t *skipwhitespace(const utf8_t *p);
size_t skiptoident(OutBuffer *buf, size_t i);
size_t skippastident(OutBuffer *buf, size_t i);
size_t skippastURL(OutBuffer *buf, size_t i);
void highlightText(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset);
void highlightCode(Scope *sc, Dsymbol *s, OutBuffer *buf, size_t offset);
void highlightCode(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset);
void highlightCode2(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset);
void highlightCode3(Scope *sc, OutBuffer *buf, const utf8_t *p, const utf8_t *pend);
TypeFunction *isTypeFunction(Dsymbol *s);
Parameter *isFunctionParameter(Dsymbols *a, const utf8_t *p, size_t len);
TemplateParameter *isTemplateParameter(Dsymbols *a, const utf8_t *p, size_t len);

bool isIdStart(const utf8_t *p);
bool isCVariadicArg(const utf8_t *p, size_t len);
bool isIdTail(const utf8_t *p);
bool isIndentWS(const utf8_t *p);
int utfStride(const utf8_t *p);

// Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one).
bool isCVariadicParameter(Dsymbols *a, const utf8_t *p, size_t len)
{
    for (size_t i = 0; i < a->dim; i++)
    {
        TypeFunction *tf = isTypeFunction((*a)[i]);
        if (tf && tf->varargs == 1 && cmp("...", p, len) == 0)
            return true;
    }
    return false;
}

static Dsymbol *getEponymousMember(TemplateDeclaration *td)
{
    if (!td->onemember)
        return NULL;

    if (AggregateDeclaration *ad = td->onemember->isAggregateDeclaration())
        return ad;
    if (FuncDeclaration *fd = td->onemember->isFuncDeclaration())
        return fd;
    if (VarDeclaration *vd = td->onemember->isVarDeclaration())
        return td->constraint ? NULL : vd;

    return NULL;
}

static TemplateDeclaration *getEponymousParent(Dsymbol *s)
{
    if (!s->parent)
        return NULL;
    TemplateDeclaration *td = s->parent->isTemplateDeclaration();
    return (td && getEponymousMember(td)) ? td : NULL;
}

static const char ddoc_default[] = "\
DDOC =  <html><head>\n\
        <META http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n\
        <title>$(TITLE)</title>\n\
        </head><body>\n\
        <h1>$(TITLE)</h1>\n\
        $(BODY)\n\
        <hr>$(SMALL Page generated by $(LINK2 http://dlang.org/ddoc.html, Ddoc). $(COPYRIGHT))\n\
        </body></html>\n\
\n\
B =     <b>$0</b>\n\
I =     <i>$0</i>\n\
U =     <u>$0</u>\n\
P =     <p>$0</p>\n\
DL =    <dl>$0</dl>\n\
DT =    <dt>$0</dt>\n\
DD =    <dd>$0</dd>\n\
TABLE = <table>$0</table>\n\
TR =    <tr>$0</tr>\n\
TH =    <th>$0</th>\n\
TD =    <td>$0</td>\n\
OL =    <ol>$0</ol>\n\
UL =    <ul>$0</ul>\n\
LI =    <li>$0</li>\n\
BIG =   <big>$0</big>\n\
SMALL = <small>$0</small>\n\
BR =    <br>\n\
LINK =  <a href=\"$0\">$0</a>\n\
LINK2 = <a href=\"$1\">$+</a>\n\
LPAREN= (\n\
RPAREN= )\n\
BACKTICK= `\n\
DOLLAR= $\n\
DEPRECATED= $0\n\
\n\
RED =   <font color=red>$0</font>\n\
BLUE =  <font color=blue>$0</font>\n\
GREEN = <font color=green>$0</font>\n\
YELLOW =<font color=yellow>$0</font>\n\
BLACK = <font color=black>$0</font>\n\
WHITE = <font color=white>$0</font>\n\
\n\
D_CODE = <pre class=\"d_code\">$0</pre>\n\
DDOC_BACKQUOTED = $(D_INLINECODE $0)\n\
D_INLINECODE = <pre style=\"display:inline;\" class=\"d_inline_code\">$0</pre>\n\
D_COMMENT = $(GREEN $0)\n\
D_STRING  = $(RED $0)\n\
D_KEYWORD = $(BLUE $0)\n\
D_PSYMBOL = $(U $0)\n\
D_PARAM   = $(I $0)\n\
\n\
DDOC_COMMENT   = <!-- $0 -->\n\
DDOC_DECL      = $(DT $(BIG $0))\n\
DDOC_DECL_DD   = $(DD $0)\n\
DDOC_DITTO     = $(BR)$0\n\
DDOC_SECTIONS  = $0\n\
DDOC_SUMMARY   = $0$(BR)$(BR)\n\
DDOC_DESCRIPTION = $0$(BR)$(BR)\n\
DDOC_AUTHORS   = $(B Authors:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_BUGS      = $(RED BUGS:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_COPYRIGHT = $(B Copyright:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_DATE      = $(B Date:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_DEPRECATED = $(RED Deprecated:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_EXAMPLES  = $(B Examples:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_HISTORY   = $(B History:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_LICENSE   = $(B License:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_RETURNS   = $(B Returns:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_SEE_ALSO  = $(B See Also:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_STANDARDS = $(B Standards:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_THROWS    = $(B Throws:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_VERSION   = $(B Version:)$(BR)\n$0$(BR)$(BR)\n\
DDOC_SECTION_H = $(B $0)$(BR)\n\
DDOC_SECTION   = $0$(BR)$(BR)\n\
DDOC_MEMBERS   = $(DL $0)\n\
DDOC_MODULE_MEMBERS = $(DDOC_MEMBERS $0)\n\
DDOC_CLASS_MEMBERS  = $(DDOC_MEMBERS $0)\n\
DDOC_STRUCT_MEMBERS = $(DDOC_MEMBERS $0)\n\
DDOC_ENUM_MEMBERS   = $(DDOC_MEMBERS $0)\n\
DDOC_TEMPLATE_MEMBERS = $(DDOC_MEMBERS $0)\n\
DDOC_ENUM_BASETYPE = $0\n\
DDOC_PARAMS    = $(B Params:)$(BR)\n$(TABLE $0)$(BR)\n\
DDOC_PARAM_ROW = $(TR $0)\n\
DDOC_PARAM_ID  = $(TD $0)\n\
DDOC_PARAM_DESC = $(TD $0)\n\
DDOC_BLANKLINE  = $(BR)$(BR)\n\
\n\
DDOC_ANCHOR     = <a name=\"$1\"></a>\n\
DDOC_PSYMBOL    = $(U $0)\n\
DDOC_PSUPER_SYMBOL = $(U $0)\n\
DDOC_KEYWORD    = $(B $0)\n\
DDOC_PARAM      = $(I $0)\n\
\n\
ESCAPES = /</&lt;/\n\
          />/&gt;/\n\
          /&/&amp;/\n\
";

static const char ddoc_decl_s[] = "$(DDOC_DECL ";
static const char ddoc_decl_e[] = ")\n";

static const char ddoc_decl_dd_s[] = "$(DDOC_DECL_DD ";
static const char ddoc_decl_dd_e[] = ")\n";


/****************************************************
 */

void gendocfile(Module *m)
{
    static OutBuffer mbuf;
    static int mbuf_done;

    OutBuffer buf;

    //printf("Module::gendocfile()\n");

    if (!mbuf_done)             // if not already read the ddoc files
    {
        mbuf_done = 1;

        // Use our internal default
        mbuf.write(ddoc_default, strlen(ddoc_default));

        // Override with DDOCFILE specified in the sc.ini file
        char *p = getenv("DDOCFILE");
        if (p)
            global.params.ddocfiles->shift(p);

        // Override with the ddoc macro files from the command line
        for (size_t i = 0; i < global.params.ddocfiles->dim; i++)
        {
            FileName f((*global.params.ddocfiles)[i]);
            File file(&f);
            readFile(m->loc, &file);
            // BUG: convert file contents to UTF-8 before use

            //printf("file: '%.*s'\n", file.len, file.buffer);
            mbuf.write(file.buffer, file.len);
        }
    }
    DocComment::parseMacros(&m->escapetable, &m->macrotable, (utf8_t *)mbuf.data, mbuf.offset);

    Scope *sc = Scope::createGlobal(m);      // create root scope

    DocComment *dc = DocComment::parse(sc, m, m->comment);
    dc->pmacrotable = &m->macrotable;
    dc->pescapetable = &m->escapetable;
    sc->lastdc = dc;

    // Generate predefined macros

    // Set the title to be the name of the module
    {
        const char *p = m->toPrettyChars();
        Macro::define(&m->macrotable, (utf8_t *)"TITLE", 5, (utf8_t *)p, strlen(p));
    }

    // Set time macros
    {
        time_t t;
        time(&t);
        char *p = ctime(&t);
        p = mem.xstrdup(p);
        Macro::define(&m->macrotable, (utf8_t *)"DATETIME", 8, (utf8_t *)p, strlen(p));
        Macro::define(&m->macrotable, (utf8_t *)"YEAR", 4, (utf8_t *)p + 20, 4);
    }

    char *srcfilename = m->srcfile->toChars();
    Macro::define(&m->macrotable, (utf8_t *)"SRCFILENAME", 11, (utf8_t *)srcfilename, strlen(srcfilename));

    char *docfilename = m->docfile->toChars();
    Macro::define(&m->macrotable, (utf8_t *)"DOCFILENAME", 11, (utf8_t *)docfilename, strlen(docfilename));

    if (dc->copyright)
    {
        dc->copyright->nooutput = 1;
        Macro::define(&m->macrotable, (utf8_t *)"COPYRIGHT", 9, dc->copyright->body, dc->copyright->bodylen);
    }

    buf.printf("$(DDOC_COMMENT Generated by Ddoc from %s)\n", m->srcfile->toChars());
    if (m->isDocFile)
    {
        Loc loc = m->md ? m->md->loc : m->loc;
        size_t commentlen = strlen((char *)m->comment);
        Dsymbols a;
        // Bugzilla 9764: Don't push m in a, to prevent emphasize ddoc file name.
        if (dc->macros)
        {
            commentlen = dc->macros->name - m->comment;
            dc->macros->write(loc, dc, sc, &a, &buf);
        }
        buf.write(m->comment, commentlen);
        highlightText(sc, &a, &buf, 0);
    }
    else
    {
        Dsymbols a;
        a.push(m);
        dc->writeSections(sc, &a, &buf);
        emitMemberComments(m, &buf, sc);
    }

    //printf("BODY= '%.*s'\n", buf.offset, buf.data);
    Macro::define(&m->macrotable, (utf8_t *)"BODY", 4, (utf8_t *)buf.data, buf.offset);

    OutBuffer buf2;
    buf2.writestring("$(DDOC)\n");
    size_t end = buf2.offset;
    m->macrotable->expand(&buf2, 0, &end, NULL, 0);

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

    // Transfer image to file
    assert(m->docfile);
    m->docfile->setbuffer(buf.data, buf.offset);
    m->docfile->ref = 1;
    ensurePathToNameExists(Loc(), m->docfile->toChars());
    writeFile(m->loc, m->docfile);
#else
    /* Remove all the escape sequences from buf2
     */
    {
        size_t i = 0;
        utf8_t *p = buf2.data;
        for (size_t j = 0; j < buf2.offset; j++)
        {
            if (p[j] == 0xFF && j + 1 < buf2.offset)
            {
                j++;
                continue;
            }
            p[i] = p[j];
            i++;
        }
        buf2.setsize(i);
    }

    // Transfer image to file
    m->docfile->setbuffer(buf2.data, buf2.offset);
    m->docfile->ref = 1;
    ensurePathToNameExists(Loc(), m->docfile->toChars());
    writeFile(m->loc, m->docfile);
#endif
}

/****************************************************
 * 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.
 */
void escapeDdocString(OutBuffer *buf, size_t start)
{
    for (size_t u = start; u < buf->offset; u++)
    {
        utf8_t c = buf->data[u];
        switch(c)
        {
            case '$':
                buf->remove(u, 1);
                buf->insert(u, (const char *)"$(DOLLAR)", 9);
                u += 8;
                break;

            case '(':
                buf->remove(u, 1); //remove the (
                buf->insert(u, (const char *)"$(LPAREN)", 9); //insert this instead
                u += 8; //skip over newly inserted macro
                break;

            case ')':
                buf->remove(u, 1); //remove the )
                buf->insert(u, (const char *)"$(RPAREN)", 9); //insert this instead
                u += 8; //skip over newly inserted macro
                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).
 */
void escapeStrayParenthesis(Loc loc, OutBuffer *buf, size_t start)
{
    unsigned par_open = 0;

    for (size_t u = start; u < buf->offset; u++)
    {
        utf8_t c = buf->data[u];
        switch(c)
        {
            case '(':
                par_open++;
                break;

            case ')':
                if (par_open == 0)
                {
                    //stray ')'
                    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, (const char *)"$(RPAREN)", 9); //insert this instead
                    u += 8; //skip over newly inserted macro
                }
                else
                    par_open--;
                break;
#if 0
            // 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)
            case '\n':
                loc.linnum++;
                break;
#endif
        }
    }

    if (par_open)                       // if any unmatched lparens
    {
        par_open = 0;
        for (size_t u = buf->offset; u > start;)
        {
            u--;
            utf8_t c = buf->data[u];
            switch(c)
            {
                case ')':
                    par_open++;
                    break;

                case '(':
                    if (par_open == 0)
                    {
                        //stray '('
                        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, (const char *)"$(LPAREN)", 9); //insert this instead
                    }
                    else
                        par_open--;
                    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.
static Scope *skipNonQualScopes(Scope *sc)
{
    while (sc && !sc->scopesym)
        sc = sc->enclosing;
    return sc;
}

static bool emitAnchorName(OutBuffer *buf, Dsymbol *s, Scope *sc)
{
    if (!s || s->isPackage() || s->isModule())
        return false;

    // Add parent names first
    bool dot = false;
    if (s->parent)
        dot = emitAnchorName(buf, s->parent, sc);
    else if (sc)
        dot = emitAnchorName(buf, sc->scopesym, skipNonQualScopes(sc->enclosing));

    // Eponymous template members can share the parent anchor name
    if (getEponymousParent(s))
        return dot;
    if (dot)
        buf->writeByte('.');

    // Use "this" not "__ctor"
    TemplateDeclaration *td;
    if (s->isCtorDeclaration() || ((td = s->isTemplateDeclaration()) != 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;
}

static void emitAnchor(OutBuffer *buf, Dsymbol *s, Scope *sc)
{
    Identifier *ident;
    {
        OutBuffer anc;
        emitAnchorName(&anc, s, skipNonQualScopes(sc));
        ident = Identifier::idPool(anc.peekString());
    }
    size_t *count = (size_t*)dmd_aaGet(&sc->anchorCounts, (void *)ident);
    TemplateDeclaration *td = getEponymousParent(s);
    // don't write an anchor for matching consecutive ditto symbols
    if (*count > 0 && sc->prevAnchor == ident &&
        sc->lastdc && (isDitto(s->comment) || (td && isDitto(td->comment))))
        return;

    (*count)++;
    // cache anchor name
    sc->prevAnchor = ident;

    buf->writestring("$(DDOC_ANCHOR ");
    buf->writestring(ident->string);
    // only append count once there's a duplicate
    if (*count != 1)
        buf->printf(".%u", *count);
    buf->writeByte(')');
}

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

/** Get leading indentation from 'src' which represents lines of code. */
static 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. */
static void expandTemplateMixinComments(TemplateMixin *tm, OutBuffer *buf, Scope *sc)
{
    if (!tm->semanticRun) tm->semantic(sc);
    TemplateDeclaration *td = (tm && tm->tempdecl) ?
        tm->tempdecl->isTemplateDeclaration() : NULL;
    if (td && td->members)
    {
        for (size_t i = 0; i < td->members->dim; 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, 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->offset;         // save starting offset
    buf->writestring(m);
    size_t offset2 = buf->offset;         // to see if we write anything

    sc = sc->push(sds);

    for (size_t i = 0; i < sds->members->dim; 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((TemplateMixin *)s, buf, sc);

        emitComment(s, buf, sc);
    }
    emitComment(NULL, buf, sc);

    sc->pop();

    if (buf->offset == offset2)
    {
        /* Didn't write out any members, so back out last write
         */
        buf->offset = offset1;
    }
    else
        buf->writestring(")\n");
}

void emitProtection(OutBuffer *buf, Prot prot)
{
    if (prot.kind != PROTundefined && prot.kind != PROTpublic)
    {
        protectionToBuffer(buf, prot);
        buf->writeByte(' ');
    }
}

void emitComment(Dsymbol *s, OutBuffer *buf, Scope *sc)
{
    class EmitComment : public Visitor
    {
    public:
        OutBuffer *buf;
        Scope *sc;

        EmitComment(OutBuffer *buf, Scope *sc)
            : buf(buf), sc(sc)
        {
        }

        void visit(Dsymbol *) {}
        void visit(InvariantDeclaration *) {}
        void visit(UnitTestDeclaration *) {}
        void visit(PostBlitDeclaration *) {}
        void visit(DtorDeclaration *) {}
        void visit(StaticCtorDeclaration *) {}
        void visit(StaticDtorDeclaration *) {}
        void visit(TypeInfoDeclaration *) {}

        void emit(Scope *sc, Dsymbol *s, const utf8_t *com)
        {
            if (s && sc->lastdc && isDitto(com))
            {
                sc->lastdc->a.push(s);
                return;
            }

            // Put previous doc comment if exists
            if (DocComment *dc = sc->lastdc)
            {
                // Put the declaration signatures as the document 'title'
                buf->writestring(ddoc_decl_s);
                for (size_t i = 0; i < dc->a.dim; i++)
                {
                    Dsymbol *sx = dc->a[i];

                    if (i == 0)
                    {
                        size_t o = buf->offset;
                        toDocBuffer(sx, buf, sc);
                        highlightCode(sc, sx, buf, o);
                        continue;
                    }

                    buf->writestring("$(DDOC_DITTO ");
                    {
                        size_t o = buf->offset;
                        toDocBuffer(sx, buf, sc);
                        highlightCode(sc, sx, buf, o);
                    }
                    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);
                //printf("buf.2 = [[%.*s]]\n", buf->offset - o0, buf->data + o0);
            }

            if (s)
            {
                DocComment *dc = DocComment::parse(sc, s, com);
                dc->pmacrotable = &sc->module->macrotable;
                sc->lastdc = dc;
            }
        }

        void visit(Declaration *d)
        {
            //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d->toChars(), d->comment);
            //printf("type = %p\n", d->type);
            const utf8_t *com = d->comment;
            if (TemplateDeclaration *td = getEponymousParent(d))
            {
                if (isDitto(td->comment))
                    com = td->comment;
                else
                    com = Lexer::combineComments(td->comment, com);
            }
            else
            {
                if (!d->ident)
                    return;
                if (!d->type && !d->isCtorDeclaration() && !d->isAliasDeclaration())
                    return;
                if (d->protection.kind == PROTprivate || sc->protection.kind == PROTprivate)
                    return;
            }
            if (!com)
                return;

            emit(sc, d, com);
        }

        void visit(AggregateDeclaration *ad)
        {
            //printf("AggregateDeclaration::emitComment() '%s'\n", ad->toChars());
            const utf8_t *com = ad->comment;
            if (TemplateDeclaration *td = getEponymousParent(ad))
            {
                if (isDitto(td->comment))
                    com = td->comment;
                else
                    com = Lexer::combineComments(td->comment, com);
            }
            else
            {
                if (ad->prot().kind == PROTprivate || sc->protection.kind == PROTprivate)
                    return;
                if (!ad->comment)
                    return;
            }
            if (!com)
                return;

            emit(sc, ad, com);
        }

        void visit(TemplateDeclaration *td)
        {
            //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td->toChars(), td->kind());
            if (td->prot().kind == PROTprivate || sc->protection.kind == PROTprivate)
                return;
            if (!td->comment)
                return;

            if (Dsymbol *ss = getEponymousMember(td))
            {
                ss->accept(this);
                return;
            }
            emit(sc, td, td->comment);
        }

        void visit(EnumDeclaration *ed)
        {
            if (ed->prot().kind == PROTprivate || sc->protection.kind == PROTprivate)
                return;
            if (ed->isAnonymous() && ed->members)
            {
                for (size_t i = 0; i < ed->members->dim; i++)
                {
                    Dsymbol *s = (*ed->members)[i];
                    emitComment(s, buf, sc);
                }
                return;
            }
            if (!ed->comment)
                return;
            if (ed->isAnonymous())
                return;

            emit(sc, ed, ed->comment);
        }

        void visit(EnumMember *em)
        {
            //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em->toChars(), em->comment);
            if (em->prot().kind == PROTprivate || sc->protection.kind == PROTprivate)
                return;
            if (!em->comment)
                return;

            emit(sc, em, em->comment);
        }

        void visit(AttribDeclaration *ad)
        {
            //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);

            /* A general problem with this, illustrated by BUGZILLA 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, NULL);

            if (d)
            {
                for (size_t i = 0; i < d->dim; i++)
                {
                    Dsymbol *s = (*d)[i];
                    //printf("AttribDeclaration::emitComment %s\n", s->toChars());
                    emitComment(s, buf, sc);
                }
            }
        }

        void visit(ProtDeclaration *pd)
        {
            if (pd->decl)
            {
                Scope *scx = sc;
                sc = sc->copy();
                sc->protection = pd->protection;
                visit((AttribDeclaration *)pd);
                scx->lastdc = sc->lastdc;
                sc = sc->pop();
            }
        }

        void visit(ConditionalDeclaration *cd)
        {
            //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
            if (cd->condition->inc)
            {
                visit((AttribDeclaration *)cd);
                return;
            }

            /* If generating doc comment, be careful because if we're inside
             * a template, then include(NULL, NULL) will fail.
             */
            Dsymbols *d = cd->decl ? cd->decl : cd->elsedecl;
            for (size_t i = 0; i < d->dim; i++)
            {
                Dsymbol *s = (*d)[i];
                emitComment(s, buf, sc);
            }
        }
    };

    EmitComment v(buf, sc);

    if (!s)
        v.emit(sc, NULL, NULL);
    else
        s->accept(&v);
}

/******************************* toDocBuffer **********************************/

void toDocBuffer(Dsymbol *s, OutBuffer *buf, Scope *sc)
{
    class ToDocBuffer : public Visitor
    {
    public:
        OutBuffer *buf;
        Scope *sc;

        ToDocBuffer(OutBuffer *buf, Scope *sc)
            : buf(buf), sc(sc)
        {
        }

        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())
            {
                emitProtection(buf, d->protection);

                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
                {
                    if (d->isConst())
                        buf->writestring("const ");
                    if (d->isImmutable())
                        buf->writestring("immutable ");
                    if (d->isSynchronized())
                        buf->writestring("synchronized ");

                    if (d->storage_class & STCmanifest)
                        buf->writestring("enum ");
                }
            }
        }

        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((TypeFunction *)origType, buf, d->ident, &hgs, td);
                }
                else
                    ::toCBuffer(origType, buf, d->ident, &hgs);
            }
            else
                buf->writestring(d->ident->toChars());

            if (d->isVarDeclaration() && td)
            {
                buf->writeByte('(');
                if (td->origParameters && td->origParameters->dim)
                {
                    for (size_t i = 0; i < td->origParameters->dim; 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)
            {
                buf->writestring(" if (");
                ::toCBuffer(td->constraint, buf, &hgs);
                buf->writeByte(')');
            }

            if (d->isDeprecated())
                buf->writestring(")");

            buf->writestring(";\n");
        }

        void visit(AliasDeclaration *ad)
        {
            //printf("AliasDeclaration::toDocbuffer() %s\n", ad->toChars());
            if (!ad->ident)
                return;

            if (ad->isDeprecated())
                buf->writestring("deprecated ");

            emitProtection(buf, ad->protection);
            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)
                {
                    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)
        {
            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());
            }
        }

        void visit(AggregateDeclaration *ad)
        {
            if (!ad->ident)
                return;

        #if 0
            emitProtection(buf, ad->protection);
        #endif
            buf->printf("%s %s", ad->kind(), ad->toChars());
            buf->writestring(";\n");
        }

        void visit(StructDeclaration *sd)
        {
            //printf("StructDeclaration::toDocbuffer() %s\n", sd->toChars());
            if (!sd->ident)
                return;

        #if 0
            emitProtection(buf, sd->protection);
        #endif
            if (TemplateDeclaration *td = getEponymousParent(sd))
            {
                toDocBuffer(td, buf, sc);
            }
            else
            {
                buf->printf("%s %s", sd->kind(), sd->toChars());
            }
            buf->writestring(";\n");
        }

        void visit(ClassDeclaration *cd)
        {
            //printf("ClassDeclaration::toDocbuffer() %s\n", cd->toChars());
            if (!cd->ident)
                return;

        #if 0
            emitProtection(buf, cd->protection);
        #endif
            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->dim; i++)
            {
                BaseClass *bc = (*cd->baseclasses)[i];

                if (bc->protection.kind == PROTprivate)
                    continue;
                if (bc->base && bc->base->ident == Id::Object)
                    continue;

                if (any)
                    buf->writestring(", ");
                else
                {
                    buf->writestring(": ");
                    any = 1;
                }
                emitProtection(buf, bc->protection);
                if (bc->base)
                {
                    buf->printf("$(DDOC_PSUPER_SYMBOL %s)", bc->base->toPrettyChars());
                }
                else
                {
                    HdrGenState hgs;
                    ::toCBuffer(bc->type, buf, NULL, &hgs);
                }
            }
            buf->writestring(";\n");
        }

        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");
        }

        void visit(EnumMember *em)
        {
            if (!em->ident)
                return;

            buf->writestring(em->toChars());
        }
    };

    ToDocBuffer v(buf, sc);
    s->accept(&v);
}

/********************************* DocComment *********************************/

DocComment *DocComment::parse(Scope *sc, Dsymbol *s, const utf8_t *comment)
{
    //printf("parse(%s): '%s'\n", s->toChars(), comment);
    DocComment *dc = new DocComment();
    dc->a.push(s);
    if (!comment)
        return dc;

    dc->parseSections(comment);

    for (size_t i = 0; i < dc->sections.dim; i++)
    {
        Section *sec = dc->sections[i];

        if (icmp("copyright", sec->name, sec->namelen) == 0)
        {
            dc->copyright = sec;
        }
        if (icmp("macros", sec->name, sec->namelen) == 0)
        {
            dc->macros = sec;
        }
    }

    return dc;
}

/*****************************************
 * 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 DocComment::parseSections(const utf8_t *comment)
{
    const utf8_t *p;
    const utf8_t *pstart;
    const utf8_t *pend;
    const utf8_t *idstart = NULL;       // dead-store to prevent spurious warning
    size_t idlen;

    const utf8_t *name = NULL;
    size_t namelen = 0;

    //printf("parseSections('%s')\n", comment);
    p = comment;
    while (*p)
    {
        const utf8_t *pstart0 = p;
        p = skipwhitespace(p);
        pstart = p;
        pend = p;

        /* 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 == '-')
            {
                if (!inCode)
                {
                    // restore leading indentation
                    while (pstart0 < pstart && isIndentWS(pstart-1)) --pstart;
                }

                int numdash = 0;
                while (*p == '-')
                {
                    ++numdash;
                    p++;
                }
                // BUG: handle UTF PS and LS too
                if ((!*p || *p == '\r' || *p == '\n') && numdash >= 3)
                    inCode ^= 1;
                pend = p;
            }

            if (!inCode && isIdStart(p))
            {
                const utf8_t *q = p + utfStride(p);
                while (isIdTail(q))
                    q += utfStride(q);
                if (*q == ':')  // identifier: ends it
                {
                    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 (icmp("Params", name, namelen) == 0)
                s = new ParamSection();
            else if (icmp("Macros", name, namelen) == 0)
                s = new MacroSection();
            else
                s = new Section();
            s->name = name;
            s->namelen = namelen;
            s->body = pstart;
            s->bodylen = pend - pstart;
            s->nooutput = 0;

            //printf("Section: '%.*s' = '%.*s'\n", s->namelen, s->name, 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 DocComment::writeSections(Scope *sc, Dsymbols *a, OutBuffer *buf)
{
    assert(a->dim);

    //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->offset;
    buf->writestring("$(DDOC_SECTIONS ");
    size_t offset2 = buf->offset;

    for (size_t i = 0; i < sections.dim; i++)
    {
        Section *sec = sections[i];
        if (sec->nooutput)
            continue;

        //printf("Section: '%.*s' = '%.*s'\n", sec->namelen, sec->name, sec->bodylen, sec->body);
        if (!sec->namelen && i == 0)
        {
            buf->writestring("$(DDOC_SUMMARY ");
            size_t o = buf->offset;
            buf->write(sec->body, sec->bodylen);
            escapeStrayParenthesis(loc, buf, o);
            highlightText(sc, a, buf, o);
            buf->writestring(")\n");
        }
        else
            sec->write(loc, this, sc, a, buf);
    }

    for (size_t i = 0; i < a->dim; i++)
    {
        Dsymbol *s = (*a)[i];
        if (Dsymbol *td = getEponymousParent(s))
            s = td;

        for (UnitTestDeclaration *utd = s->ddocUnittest; utd; utd = utd->ddocUnittest)
        {
            if (utd->protection.kind == PROTprivate || !utd->comment || !utd->fbody)
                continue;

            // Strip whitespaces to avoid showing empty summary
            const utf8_t *c = utd->comment;
            while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r') ++c;

            buf->writestring("$(DDOC_EXAMPLES ");

            size_t o = buf->offset;
            buf->writestring((char *)c);

            if (utd->codedoc)
            {
                size_t n = getCodeIndent(utd->codedoc);
                while (n--) buf->writeByte(' ');
                buf->writestring("----\n");
                buf->writestring(utd->codedoc);
                buf->writestring("----\n");
                highlightText(sc, a, buf, o);
            }

            buf->writestring(")");
        }
    }

    if (buf->offset == offset2)
    {
        /* Didn't write out any sections, so back out last write
         */
        buf->offset = offset1;
        buf->writestring("$(DDOC_BLANKLINE)\n");
    }
    else
        buf->writestring(")\n");
}

/***************************************************
 */

void Section::write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf)
{
    assert(a->dim);

    if (namelen)
    {
        static const char *table[] =
        {
            "AUTHORS", "BUGS", "COPYRIGHT", "DATE",
            "DEPRECATED", "EXAMPLES", "HISTORY", "LICENSE",
            "RETURNS", "SEE_ALSO", "STANDARDS", "THROWS",
            "VERSION", NULL
        };

        for (size_t i = 0; table[i]; i++)
        {
            if (icmp(table[i], name, namelen) == 0)
            {
                buf->printf("$(DDOC_%s ", table[i]);
                goto L1;
            }
        }

        buf->writestring("$(DDOC_SECTION ");

            // Replace _ characters with spaces
            buf->writestring("$(DDOC_SECTION_H ");
            size_t o = buf->offset;
            for (size_t u = 0; u < namelen; u++)
            {
                utf8_t c = name[u];
                buf->writeByte((c == '_') ? ' ' : c);
            }
            escapeStrayParenthesis(loc, buf, o);
            buf->writestring(":)\n");
    }
    else
    {
        buf->writestring("$(DDOC_DESCRIPTION ");
    }
  L1:
    size_t o = buf->offset;
    buf->write(body, bodylen);
    escapeStrayParenthesis(loc, buf, o);
    highlightText(sc, a, buf, o);
    buf->writestring(")\n");
}

/***************************************************
 */

void ParamSection::write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf)
{
    assert(a->dim);
    Dsymbol *s = (*a)[0];   // test

    const utf8_t *p = body;
    size_t len = bodylen;
    const utf8_t *pend = p + len;

    const utf8_t *tempstart = NULL;
    size_t templen = 0;

    const utf8_t *namestart = NULL;
    size_t namelen = 0;       // !=0 if line continuation

    const utf8_t *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, 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, 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", namelen, namestart, textlen, textstart);
            ++paramcount;
            HdrGenState hgs;
            buf->writestring("$(DDOC_PARAM_ROW ");
            {
                buf->writestring("$(DDOC_PARAM_ID ");
                {
                    size_t o = buf->offset;
                    Parameter *fparam = isFunctionParameter(a, namestart, namelen);
                    bool isCVariadic = isCVariadicParameter(a, namestart, 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)
                        {
                            warning(s->loc, "Ddoc: function declaration has no parameter '%.*s'", namelen, namestart);
                        }
                        buf->write(namestart, namelen);
                    }
                    escapeStrayParenthesis(loc, buf, o);
                    highlightCode(sc, a, buf, o);
                }
                buf->writestring(")\n");

                buf->writestring("$(DDOC_PARAM_DESC ");
                {
                    size_t o = buf->offset;
                    buf->write(textstart, textlen);
                    escapeStrayParenthesis(loc, buf, o);
                    highlightText(sc, a, buf, o);
                }
                buf->writestring(")");
            }
            buf->writestring(")\n");
            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(")\n");

    TypeFunction *tf = a->dim == 1 ? isTypeFunction(s) : NULL;
    if (tf)
    {
        size_t pcount = (tf->parameters ? tf->parameters->dim : 0) + (int)(tf->varargs == 1);
        if (pcount != paramcount)
        {
            warning(s->loc, "Ddoc: parameter count mismatch");
        }
    }
}

/***************************************************
 */

void MacroSection::write(Loc loc, DocComment *dc, Scope *sc, Dsymbols *a, OutBuffer *buf)
{
    //printf("MacroSection::write()\n");
    DocComment::parseMacros(dc->pescapetable, dc->pmacrotable, body, bodylen);
}

/************************************************
 * Parse macros out of Macros: section.
 * Macros are of the form:
 *      name1 = value1
 *
 *      name2 = value2
 */

void DocComment::parseMacros(Escape **pescapetable, Macro **pmacrotable, const utf8_t *m, size_t mlen)
{
    const utf8_t *p = m;
    size_t len = mlen;
    const utf8_t *pend = p + len;

    const utf8_t *tempstart = NULL;
    size_t templen = 0;

    const utf8_t *namestart = NULL;
    size_t namelen = 0;       // !=0 if line continuation

    const utf8_t *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", namelen, namestart, textlen, textstart);
            if (icmp("ESCAPES", namestart, namelen) == 0)
                parseEscapes(pescapetable, textstart, textlen);
            else
                Macro::define(pmacrotable, namestart, namelen, textstart, 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.
 */

void DocComment::parseEscapes(Escape **pescapetable, const utf8_t *textstart, size_t textlen)
{
    Escape *escapetable = *pescapetable;

    if (!escapetable)
    {
        escapetable = new Escape;
        memset(escapetable, 0, sizeof(Escape));
        *pescapetable = escapetable;
    }
    //printf("parseEscapes('%.*s') pescapetable = %p\n", textlen, textstart, pescapetable);
    const utf8_t *p = textstart;
    const utf8_t *pend = p + textlen;

    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;
        utf8_t c = p[1];
        p += 3;
        const utf8_t *start = p;
        while (1)
        {
            if (p >= pend)
                return;
            if (*p == '/')
                break;
            p++;
        }
        size_t len = p - start;
        char *s = (char *)memcpy(mem.xmalloc(len + 1), start, len);
        s[len] = 0;
        escapetable->strings[c] = s;
        //printf("\t%c = '%s'\n", c, s);
        p++;
    }
}


/******************************************
 * Compare 0-terminated string with length terminated string.
 * Return < 0, ==0, > 0
 */

int cmp(const char *stringz, const void *s, size_t slen)
{
    size_t len1 = strlen(stringz);

    if (len1 != slen)
        return (int)(len1 - slen);
    return memcmp(stringz, s, slen);
}

int icmp(const char *stringz, const void *s, size_t slen)
{
    size_t len1 = strlen(stringz);

    if (len1 != slen)
        return (int)(len1 - slen);
    return Port::memicmp(stringz, (char *)s, slen);
}

/*****************************************
 * Return true if comment consists entirely of "ditto".
 */

bool isDitto(const utf8_t *comment)
{
    if (comment)
    {
        const utf8_t *p = skipwhitespace(comment);

        if (Port::memicmp((const char *)p, "ditto", 5) == 0 && *skipwhitespace(p + 5) == 0)
            return true;
    }
    return false;
}

/**********************************************
 * Skip white space.
 */

const utf8_t *skipwhitespace(const utf8_t *p)
{
    for (; 1; p++)
    {
        switch (*p)
        {
            case ' ':
            case '\t':
            case '\n':
                continue;
        }
        break;
    }
    return p;
}


/************************************************
 * Scan forward to one of:
 *      start of identifier
 *      beginning of next line
 *      end of buf
 */

size_t skiptoident(OutBuffer *buf, size_t i)
{
    while (i < buf->offset)
    {
        dchar_t c;

        size_t oi = i;
        if (utf_decodeChar((utf8_t *)buf->data, buf->offset, &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(OutBuffer *buf, size_t i)
{
    while (i < buf->offset)
    {
        dchar_t c;

        size_t oi = i;
        if (utf_decodeChar((utf8_t *)buf->data, buf->offset, &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 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(OutBuffer *buf, size_t i)
{
    size_t length = buf->offset - i;
    utf8_t *p = (utf8_t *)&buf->data[i];
    size_t j;
    unsigned sawdot = 0;

    if (length > 7 && Port::memicmp((char *)p, "http://", 7) == 0)
    {
        j = 7;
    }
    else if (length > 8 && Port::memicmp((char *)p, "https://", 8) == 0)
    {
        j = 8;
    }
    else
        goto Lno;

    for (; j < length; j++)
    {
        utf8_t c = p[j];
        if (isalnum(c))
            continue;
        if (c == '-' || c == '_' || c == '?' ||
            c == '=' || c == '%' || c == '&' ||
            c == '/' || c == '+' || c == '#' ||
            c == '~')
            continue;
        if (c == '.')
        {
            sawdot = 1;
            continue;
        }
        break;
    }
    if (sawdot)
        return i + j;

Lno:
    return i;
}


/****************************************************
 */

bool isIdentifier(Dsymbols *a, const utf8_t *p, size_t len)
{
    for (size_t i = 0; i < a->dim; i++)
    {
        const char *s = (*a)[i]->ident->toChars();
        if (cmp(s, p, len) == 0)
            return true;
    }
    return false;
}

/****************************************************
 */

bool isKeyword(utf8_t *p, size_t len)
{
    static const char *table[] = { "true", "false", "null", NULL };

    for (int i = 0; table[i]; i++)
    {
        if (cmp(table[i], p, len) == 0)
            return true;
    }
    return false;
}

/****************************************************
 */

TypeFunction *isTypeFunction(Dsymbol *s)
{
    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 (TypeFunction *)t;
    }
    return NULL;
}

/****************************************************
 */

Parameter *isFunctionParameter(Dsymbols *a, const utf8_t *p, size_t len)
{
    for (size_t i = 0; i < a->dim; i++)
    {
        TypeFunction *tf = isTypeFunction((*a)[i]);
        if (tf && tf->parameters)
        {
            for (size_t k = 0; k < tf->parameters->dim; k++)
            {
                Parameter *fparam = (*tf->parameters)[k];
                if (fparam->ident && cmp(fparam->ident->toChars(), p, len) == 0)
                {
                    return fparam;
                }
            }
        }
    }
    return NULL;
}

/****************************************************
 */

TemplateParameter *isTemplateParameter(Dsymbols *a, const utf8_t *p, size_t len)
{
    for (size_t i = 0; i < a->dim; i++)
    {
        TemplateDeclaration *td = getEponymousParent((*a)[i]);
        if (td && td->origParameters)
        {
            for (size_t k = 0; k < td->origParameters->dim; k++)
            {
                TemplateParameter *tp = (*td->origParameters)[k];
                if (tp->ident && cmp(tp->ident->toChars(), p, len) == 0)
                {
                    return tp;
                }
            }
        }
    }
    return NULL;
}

/****************************************************
 * Return true if str is a reserved symbol name
 * that starts with a double underscore.
 */

bool isReservedName(utf8_t *str, size_t len)
{
    static const char *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__", "__LOCAL_SIZE", "___tls_get_addr", "__entrypoint", "__va_argsave_t",
        "__va_argsave", NULL };

    for (int i = 0; table[i]; i++)
    {
        if (cmp(table[i], str, len) == 0)
            return true;
    }
    return false;
}

/**************************************************
 * Highlight text section.
 */

void highlightText(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset)
{
    Dsymbol *s = a->dim ? (*a)[0] : NULL;   // test

    //printf("highlightText()\n");

    int leadingBlank = 1;
    int inCode = 0;
    int inBacktick = 0;
    //int inComment = 0;                  // in <!-- ... --> comment
    size_t iCodeStart = 0;                    // start of code section
    size_t codeIndent = 0;

    size_t iLineStart = offset;

    for (size_t i = offset; i < buf->offset; i++)
    {
        utf8_t c = buf->data[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 (!sc->module->isDocFile &&
                    !inCode && i == iLineStart && i + 1 < buf->offset)    // if "\n\n"
                {
                    static const char blankline[] = "$(DDOC_BLANKLINE)\n";

                    i = buf->insert(i, blankline, strlen(blankline));
                }
                leadingBlank = 1;
                iLineStart = i + 1;
                break;

            case '<':
            {
                leadingBlank = 0;
                if (inCode)
                    break;
                utf8_t *p = (utf8_t *)&buf->data[i];
                const char *se = sc->module->escapetable->escapeChar('<');
                if (se && strcmp(se, "&lt;") == 0)
                {
                    // Generating HTML
                    // Skip over comments
                    if (p[1] == '!' && p[2] == '-' && p[3] == '-')
                    {
                        size_t j = i + 4;
                        p += 4;
                        while (1)
                        {
                            if (j == buf->offset)
                                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 == buf->offset)
                                break;
                            if (p[0] == '>')
                            {
                                i = j;      // place on closing '>'
                                break;
                            }
                            j++;
                            p++;
                        }
                        break;
                    }
                }
            L1:
                // Replace '<' with '&lt;' character entity
                if (se)
                {
                    size_t len = strlen(se);
                    buf->remove(i, 1);
                    i = buf->insert(i, se, len);
                    i--;        // point to ';'
                }
                break;
            }
            case '>':
            {
                leadingBlank = 0;
                if (inCode)
                    break;
                // Replace '>' with '&gt;' character entity
                const char *se = sc->module->escapetable->escapeChar('>');
                if (se)
                {
                    size_t len = strlen(se);
                    buf->remove(i, 1);
                    i = buf->insert(i, se, len);
                    i--;        // point to ';'
                }
                break;
            }
            case '&':
            {
                leadingBlank = 0;
                if (inCode)
                    break;
                utf8_t *p = (utf8_t *)&buf->data[i];
                if (p[1] == '#' || isalpha(p[1]))
                    break;                      // already a character entity
                // Replace '&' with '&amp;' character entity
                const char *se = sc->module->escapetable->escapeChar('&');
                if (se)
                {
                    size_t len = strlen(se);
                    buf->remove(i, 1);
                    i = buf->insert(i, se, len);
                    i--;        // point to ';'
                }
                break;
            }
            case '`':
            {
                if (inBacktick)
                {
                    inBacktick = 0;
                    inCode = 0;

                    OutBuffer codebuf;

                    codebuf.write(buf->data + iCodeStart + 1, i - (iCodeStart + 1));

                    // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
                    highlightCode(sc, a, &codebuf, 0);

                    buf->remove(iCodeStart, i - iCodeStart + 1); // also trimming off the current `

                    static const char pre[] = "$(DDOC_BACKQUOTED ";
                    i = buf->insert(iCodeStart, pre, strlen(pre));
                    i = buf->insert(i, (char *)codebuf.data, codebuf.offset);
                    i = buf->insert(i, (char *)")", 1);

                    i--; // point to the ending ) so when the for loop does i++, it will see the next character

                    break;
                }

                if (inCode)
                    break;

                inCode = 1;
                inBacktick = 1;
                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;

                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)
                {
                    size_t istart = i;
                    size_t eollen = 0;

                    leadingBlank = 0;
                    while (1)
                    {
                        ++i;
                        if (i >= buf->offset)
                            break;
                        c = buf->data[i];
                        if (c == '\n')
                        {
                            eollen = 1;
                            break;
                        }
                        if (c == '\r')
                        {
                            eollen = 1;
                            if (i + 1 >= buf->offset)
                                break;
                            if (buf->data[i + 1] == '\n')
                            {
                                eollen = 2;
                                break;
                            }
                        }
                        // BUG: handle UTF PS and LS too
                        if (c != '-')
                            goto Lcont;
                    }
                    if (i - istart < 3)
                        goto Lcont;

                    // 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 (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->data + iCodeStart, i - iCodeStart);
                        codebuf.writeByte(0);

                        // Remove leading indentations from all lines
                        bool lineStart = true;
                        utf8_t *endp = (utf8_t *)codebuf.data + codebuf.offset;
                        for (utf8_t *p = (utf8_t *)codebuf.data; p < endp; )
                        {
                            if (lineStart)
                            {
                                size_t j = codeIndent;
                                utf8_t *q = p;
                                while (j-- > 0 && q < endp && isIndentWS(q))
                                    ++q;
                                codebuf.remove(p - (utf8_t *)codebuf.data, q - p);
                                assert((utf8_t *)codebuf.data <= p);
                                assert(p < (utf8_t *)codebuf.data + codebuf.offset);
                                lineStart = false;
                                endp = (utf8_t *)codebuf.data + codebuf.offset; // update
                                continue;
                            }
                            if (*p == '\n')
                                lineStart = true;
                            ++p;
                        }

                        highlightCode2(sc, a, &codebuf, 0);
                        buf->remove(iCodeStart, i - iCodeStart);
                        i = buf->insert(iCodeStart, codebuf.data, codebuf.offset);
                        i = buf->insert(i, (const char *)")\n", 2);
                        i -= 2; // in next loop, c should be '\n'
                    }
                    else
                    {
                        static const char d_code[] = "$(D_CODE ";

                        inCode = 1;
                        codeIndent = istart - iLineStart;  // save indent count
                        i = buf->insert(i, d_code, strlen(d_code));
                        iCodeStart = i;
                        i--;            // place i on >
                        leadingBlank = true;
                    }
                }
                break;

            default:
                leadingBlank = 0;
                if (sc->module->isDocFile || inCode)
                    break;

                utf8_t *start = (utf8_t *)buf->data + i;
                if (isIdStart(start))
                {
                    size_t j = skippastident(buf, i);
                    if (i < j)
                    {
                        size_t k = skippastURL(buf, i);
                        if (i < k)
                        {
                            i = 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->size - 1 || !isReservedName(start, len)))
                    {
                        buf->remove(i, 1);
                        i = j - 1;
                        break;
                    }
                    if (isIdentifier(a, start, len))
                    {
                        i = buf->bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
                        break;
                    }
                    if (isKeyword(start, len))
                    {
                        i = buf->bracket(i, "$(DDOC_KEYWORD ", j, ")") - 1;
                        break;
                    }
                    if (isFunctionParameter(a, start, len))
                    {
                        //printf("highlighting arg '%s', i = %d, j = %d\n", arg->ident->toChars(), i, j);
                        i = buf->bracket(i, "$(DDOC_PARAM ", j, ")") - 1;
                        break;
                    }

                    i = j - 1;
                }
                break;
        }
    }
    if (inCode)
        error(s ? s->loc : Loc(), "unmatched --- in DDoc comment");
}

/**************************************************
 * Highlight code for DDOC section.
 */

void highlightCode(Scope *sc, Dsymbol *s, OutBuffer *buf, size_t offset)
{
    //printf("highlightCode(s = %s '%s')\n", s->kind(), s->toChars());
    OutBuffer ancbuf;
    emitAnchor(&ancbuf, s, sc);
    buf->insert(offset, (char *)ancbuf.data, ancbuf.offset);
    offset += ancbuf.offset;

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

/****************************************************
 */

void highlightCode(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset)
{
    //printf("highlightCode(a = '%s')\n", a->toChars());

    for (size_t i = offset; i < buf->offset; i++)
    {
        utf8_t c = buf->data[i];
        const char *se = sc->module->escapetable->escapeChar(c);
        if (se)
        {
            size_t len = strlen(se);
            buf->remove(i, 1);
            i = buf->insert(i, se, len);
            i--;                // point to ';'
            continue;
        }

        utf8_t *start = (utf8_t *)buf->data + i;
        if (isIdStart(start))
        {
            size_t j = skippastident(buf, i);
            if (i < j)
            {
                size_t len = j - i;
                if (isIdentifier(a, start, len))
                {
                    i = buf->bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
                    continue;
                }
                if (isFunctionParameter(a, start, 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;
            }
        }
    }
}

/****************************************
 */

void highlightCode3(Scope *sc, OutBuffer *buf, const utf8_t *p, const utf8_t *pend)
{
    for (; p < pend; p++)
    {
        const char *s = sc->module->escapetable->escapeChar(*p);
        if (s)
            buf->writestring(s);
        else
            buf->writeByte(*p);
    }
}

/**************************************************
 * Highlight code for CODE section.
 */

void highlightCode2(Scope *sc, Dsymbols *a, OutBuffer *buf, size_t offset)
{
    unsigned errorsave = global.errors;
    Lexer lex(NULL, (utf8_t *)buf->data, 0, buf->offset - 1, 0, 1);
    OutBuffer res;
    const utf8_t *lastp = (utf8_t *)buf->data;

    //printf("highlightCode2('%.*s')\n", buf->offset - 1, buf->data);
    res.reserve(buf->offset);
    while (1)
    {
        Token tok;
        lex.scan(&tok);
        highlightCode3(sc, &res, lastp, tok.ptr);

        const char *highlight = NULL;
        switch (tok.value)
        {
            case TOKidentifier:
            {
                if (!sc)
                    break;
                size_t len = lex.p - tok.ptr;
                if (isIdentifier(a, tok.ptr, len))
                {
                    highlight = "$(D_PSYMBOL ";
                    break;
                }
                if (isFunctionParameter(a, tok.ptr, len))
                {
                    //printf("highlighting arg '%s', i = %d, j = %d\n", arg->ident->toChars(), i, j);
                    highlight = "$(D_PARAM ";
                    break;
                }
                break;
            }
            case TOKcomment:
                highlight = "$(D_COMMENT ";
                break;

            case TOKstring:
                highlight = "$(D_STRING ";
                break;

            default:
                if (tok.isKeyword())
                    highlight = "$(D_KEYWORD ";
                break;
        }
        if (highlight)
        {
            res.writestring(highlight);
            size_t o = res.offset;
            highlightCode3(sc, &res, tok.ptr, lex.p);
            if (tok.value == TOKcomment || tok.value == TOKstring)
                escapeDdocString(&res, o);  // Bugzilla 7656, 7715, and 10519
            res.writeByte(')');
        }
        else
            highlightCode3(sc, &res, tok.ptr, lex.p);
        if (tok.value == TOKeof)
            break;
        lastp = lex.p;
    }
    buf->setsize(offset);
    buf->write(&res);
    global.errors = errorsave;
}

/***************************************
 * Find character string to replace c with.
 */

const char *Escape::escapeChar(unsigned c)
{
#if 1
    assert(c < 256);
    //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c]);
    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;
#endif
}

/****************************************
 * Determine if p points to the start of a "..." parameter identifier.
 */

bool isCVariadicArg(const utf8_t *p, size_t len)
{
    return len >= 3 && cmp("...", p, 3) == 0;
}

/****************************************
 * Determine if p points to the start of an identifier.
 */

bool isIdStart(const utf8_t *p)
{
    unsigned c = *p;
    if (isalpha(c) || c == '_')
        return true;
    if (c >= 0x80)
    {
        size_t i = 0;
        if (utf_decodeChar(p, 4, &i, &c))
            return false;   // ignore errors
        if (isUniAlpha(c))
            return true;
    }
    return false;
}

/****************************************
 * Determine if p points to the rest of an identifier.
 */

bool isIdTail(const utf8_t *p)
{
    unsigned c = *p;
    if (isalnum(c) || c == '_')
        return true;
    if (c >= 0x80)
    {
        size_t i = 0;
        if (utf_decodeChar(p, 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 utf8_t *p)
{
    return (*p == ' ') || (*p == '\t');
}

/*****************************************
 * Return number of bytes in UTF character.
 */

int utfStride(const utf8_t *p)
{
    unsigned c = *p;
    if (c < 0x80)
        return 1;
    size_t i = 0;
    utf_decodeChar(p, 4, &i, &c);       // ignore errors, but still consume input
    return (int)i;
}
