| /** |
| * Ddoc documentation generation. |
| * |
| * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator) |
| * |
| * Copyright: Copyright (C) 1999-2022 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.errors; |
| import dmd.func; |
| import dmd.globals; |
| import dmd.hdrgen; |
| import dmd.id; |
| import dmd.identifier; |
| import dmd.lexer; |
| 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.utils; |
| import dmd.visitor; |
| |
| struct Escape |
| { |
| const(char)[][char.max] strings; |
| |
| /*************************************** |
| * Find character string to replace c with. |
| */ |
| const(char)[] escapeChar(char c) |
| { |
| version (all) |
| { |
| //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr); |
| return strings[c]; |
| } |
| else |
| { |
| const(char)[] s; |
| switch (c) |
| { |
| case '<': |
| s = "<"; |
| break; |
| case '>': |
| s = ">"; |
| break; |
| case '&': |
| s = "&"; |
| break; |
| default: |
| s = null; |
| break; |
| } |
| return s; |
| } |
| } |
| } |
| |
| /*********************************************************** |
| */ |
| private 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, OutBuffer* buf) |
| { |
| assert(a.dim); |
| 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); |
| buf.writestring(")"); |
| } |
| else |
| { |
| buf.writestring("$(DDOC_DESCRIPTION "); |
| } |
| L1: |
| size_t o = buf.length; |
| buf.write(body_); |
| escapeStrayParenthesis(loc, buf, o, true); |
| highlightText(sc, a, loc, *buf, o); |
| buf.writestring(")"); |
| } |
| } |
| |
| /*********************************************************** |
| */ |
| private final class ParamSection : Section |
| { |
| override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf) |
| { |
| assert(a.dim); |
| 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) |
| { |
| warning(s.loc, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen, namestart); |
| } |
| buf.write(namestart[0 .. namelen]); |
| } |
| escapeStrayParenthesis(loc, buf, o, true); |
| 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); |
| 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.dim == 1 ? isTypeFunction(s) : null; |
| if (tf) |
| { |
| size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.dim : 0) + |
| cast(int)(tf.parameterList.varargs == VarArg.variadic); |
| if (pcount != paramcount) |
| { |
| 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 |
| warningSupplemental(s.loc, "Note that the format is `param = description`"); |
| } |
| } |
| } |
| } |
| } |
| |
| /*********************************************************** |
| */ |
| private final class MacroSection : Section |
| { |
| override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf) |
| { |
| //printf("MacroSection::write()\n"); |
| DocComment.parseMacros(dc.escapetable, *dc.pmacrotable, body_); |
| } |
| } |
| |
| private alias Sections = Array!(Section); |
| |
| // Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one). |
| private 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; |
| } |
| |
| private 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; |
| } |
| |
| private TemplateDeclaration getEponymousParent(Dsymbol s) |
| { |
| if (!s.parent) |
| return null; |
| TemplateDeclaration td = s.parent.isTemplateDeclaration(); |
| return (td && getEponymousMember(td)) ? td : null; |
| } |
| |
| private immutable ddoc_default = import("default_ddoc_theme." ~ ddoc_ext); |
| private immutable ddoc_decl_s = "$(DDOC_DECL "; |
| private immutable ddoc_decl_e = ")\n"; |
| private immutable ddoc_decl_dd_s = "$(DDOC_DECL_DD "; |
| private immutable ddoc_decl_dd_e = ")\n"; |
| |
| /**************************************************** |
| */ |
| extern(C++) void gendocfile(Module m) |
| { |
| __gshared OutBuffer mbuf; |
| __gshared 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.writestring(ddoc_default); |
| // Override with DDOCFILE specified in the sc.ini file |
| char* p = getenv("DDOCFILE"); |
| if (p) |
| global.params.ddoc.files.shift(p); |
| // Override with the ddoc macro files from the command line |
| for (size_t i = 0; i < global.params.ddoc.files.dim; i++) |
| { |
| auto buffer = readFile(m.loc, global.params.ddoc.files[i]); |
| // BUG: convert file contents to UTF-8 before use |
| const data = buffer.data; |
| //printf("file: '%.*s'\n", cast(int)data.length, data.ptr); |
| mbuf.write(data); |
| } |
| } |
| DocComment.parseMacros(m.escapetable, m.macrotable, mbuf[]); |
| Scope* sc = Scope.createGlobal(m); // 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 |
| { |
| time_t t; |
| time(&t); |
| char* p = ctime(&t); |
| p = mem.xstrdup(p); |
| m.macrotable.define("DATETIME", p.toDString()); |
| m.macrotable.define("YEAR", p[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_); |
| } |
| if (m.filetype == FileType.ddoc) |
| { |
| const ploc = m.md ? &m.md.loc : &m.loc; |
| const loc = Loc(ploc.filename ? ploc.filename : srcfilename.ptr, |
| ploc.linnum, |
| ploc.charnum); |
| |
| size_t commentlen = strlen(cast(char*)m.comment); |
| 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; |
| |
| const success = m.macrotable.expand(buf2, 0, end, null, global.recursionLimit); |
| if (!success) |
| error(Loc.initial, "DDoc macro expansion limit exceeded; more than %d expansions.", global.recursionLimit); |
| |
| version (all) |
| { |
| /* Remove all the escape sequences from buf2, |
| * and make CR-LF the newline. |
| */ |
| { |
| const slice = buf2[]; |
| buf.setsize(0); |
| buf.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') |
| buf.writeByte('\r'); |
| else if (c == '\r') |
| { |
| buf.writestring("\r\n"); |
| if (j + 1 < slice.length && p[j + 1] == '\n') |
| { |
| j++; |
| } |
| continue; |
| } |
| buf.writeByte(c); |
| } |
| } |
| writeFile(m.loc, m.docfile.toString(), buf[]); |
| } |
| else |
| { |
| /* Remove all the escape sequences from buf2 |
| */ |
| { |
| size_t i = 0; |
| char* p = buf2.data; |
| for (size_t j = 0; j < buf2.length; j++) |
| { |
| if (p[j] == 0xFF && j + 1 < buf2.length) |
| { |
| j++; |
| continue; |
| } |
| p[i] = p[j]; |
| i++; |
| } |
| buf2.setsize(i); |
| } |
| writeFile(m.loc, m.docfile.toString(), buf2[]); |
| } |
| } |
| |
| /**************************************************** |
| * 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.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, OutBuffer* buf, size_t start, bool respectBackslashEscapes) |
| { |
| 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 ')' |
| 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 '(' |
| 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. |
| private Scope* skipNonQualScopes(Scope* sc) |
| { |
| while (sc && !sc.scopesym) |
| sc = sc.enclosing; |
| return sc; |
| } |
| |
| private 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; |
| } |
| |
| private 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.dim > 0) |
| { |
| for(int i = 0; i < imp.aliases.dim; 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; |
| if (auto symFromId = sc.search(loc, id, null)) |
| { |
| 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. */ |
| private 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. */ |
| private 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.dim; i++) |
| { |
| Dsymbol sm = (*td.members)[i]; |
| TemplateMixin tmc = sm.isTemplateMixin(); |
| if (tmc && tmc.comment) |
| expandTemplateMixinComments(tmc, buf, sc); |
| else |
| emitComment(sm, buf, sc); |
| } |
| } |
| } |
| |
| private 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.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(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(")"); |
| } |
| |
| private 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); |
| } |
| |
| private 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); |
| } |
| } |
| |
| private void emitVisibility(ref OutBuffer buf, Visibility vis) |
| { |
| visibilityToBuffer(&buf, vis); |
| buf.writeByte(' '); |
| } |
| |
| private 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) |
| { |
| 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.dim > 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.dim; 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.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); |
| } |
| |
| 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.dim; 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.dim; 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); |
| } |
| |
| private 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) |
| { |
| 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.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) |
| { |
| 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) |
| { |
| 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()); |
| } |
| } |
| |
| 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.dim; 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); |
| } |
| |
| /*********************************************************** |
| */ |
| 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.dim; 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, 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.length; |
| buf.writestring("$(DDOC_SECTIONS "); |
| size_t offset2 = buf.length; |
| for (size_t i = 0; i < sections.dim; 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); |
| highlightText(sc, a, loc, *buf, o); |
| buf.writestring(")"); |
| } |
| 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.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". |
| */ |
| private 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. |
| */ |
| private const(char)* skipwhitespace(const(char)* p) |
| { |
| return skipwhitespace(p.toDString).ptr; |
| } |
| |
| /// Ditto |
| private const(char)[] skipwhitespace(const(char)[] p) |
| { |
| 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. |
| */ |
| private size_t skipChars(ref OutBuffer buf, size_t i, string chars) |
| { |
| 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` |
| */ |
| private inout(char)[] replaceChar(inout(char)[] s, char c, string r) pure |
| { |
| 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 |
| */ |
| private string toLowercase(string s) pure |
| { |
| 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 |
| */ |
| private int getMarkdownIndent(ref OutBuffer buf, size_t from, size_t to) |
| { |
| 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) |
| { |
| 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. |
| */ |
| private size_t skippastident(ref OutBuffer buf, size_t i) |
| { |
| 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`) |
| */ |
| private size_t skipPastIdentWithDots(ref OutBuffer buf, size_t i) |
| { |
| 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 |
| */ |
| private 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. |
| */ |
| private 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 |
| */ |
| private 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 |
| */ |
| private int detectAtxHeadingLevel(ref OutBuffer buf, const size_t i) |
| { |
| 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 |
| */ |
| private 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. |
| */ |
| private 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 |
| */ |
| private 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 |
| */ |
| private 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 |
| */ |
| private 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; |
| } |
| |
| /**************************************************** |
| */ |
| private bool isIdentifier(Dsymbols* a, const(char)[] s) |
| { |
| 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; |
| } |
| |
| /**************************************************** |
| */ |
| private bool isKeyword(const(char)[] str) @safe |
| { |
| immutable string[3] table = ["true", "false", "null"]; |
| foreach (s; table) |
| { |
| if (str == s) |
| return true; |
| } |
| return false; |
| } |
| |
| /**************************************************** |
| */ |
| private 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; |
| } |
| |
| /**************************************************** |
| */ |
| private 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; |
| } |
| |
| /**************************************************** |
| */ |
| private Parameter isFunctionParameter(Dsymbols* a, const(char)[] p) @safe |
| { |
| foreach (Dsymbol sym; *a) |
| { |
| Parameter fparam = isFunctionParameter(sym, p); |
| if (fparam) |
| { |
| return fparam; |
| } |
| } |
| return null; |
| } |
| |
| /**************************************************** |
| */ |
| private 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; |
| } |
| |
| /**************************************************** |
| */ |
| private TemplateParameter isTemplateParameter(Dsymbols* a, const(char)* p, size_t len) |
| { |
| for (size_t i = 0; i < a.dim; 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. |
| */ |
| private bool isReservedName(const(char)[] str) |
| { |
| 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. |
| */ |
| private 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 { return count != 0; } |
| |
| /// flag this delimiter as invalid |
| void invalidate() { count = 0; } |
| } |
| |
| /**************************************************** |
| * Info about a Markdown list. |
| */ |
| private 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 { 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) |
| { |
| 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) |
| { |
| 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 |