| /** |
| * Ddoc documentation generation. |
| * |
| * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator) |
| * |
| * Copyright: Copyright (C) 1999-2023 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.location; |
| import dmd.mtype; |
| import dmd.root.array; |
| import dmd.root.file; |
| import dmd.root.filename; |
| import dmd.common.outbuffer; |
| import dmd.root.port; |
| import dmd.root.rmem; |
| import dmd.root.string; |
| import dmd.root.utf; |
| import dmd.tokens; |
| import dmd.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.length); |
| if (name.length) |
| { |
| static immutable table = |
| [ |
| "AUTHORS", |
| "BUGS", |
| "COPYRIGHT", |
| "DATE", |
| "DEPRECATED", |
| "EXAMPLES", |
| "HISTORY", |
| "LICENSE", |
| "RETURNS", |
| "SEE_ALSO", |
| "STANDARDS", |
| "THROWS", |
| "VERSION", |
| ]; |
| foreach (entry; table) |
| { |
| if (iequals(entry, name)) |
| { |
| buf.printf("$(DDOC_%s ", entry.ptr); |
| goto L1; |
| } |
| } |
| buf.writestring("$(DDOC_SECTION "); |
| // Replace _ characters with spaces |
| buf.writestring("$(DDOC_SECTION_H "); |
| size_t o = buf.length; |
| foreach (char c; name) |
| buf.writeByte((c == '_') ? ' ' : c); |
| escapeStrayParenthesis(loc, buf, o, false); |
| 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.length); |
| Dsymbol s = (*a)[0]; // test |
| const(char)* p = body_.ptr; |
| size_t len = body_.length; |
| const(char)* pend = p + len; |
| const(char)* tempstart = null; |
| size_t templen = 0; |
| const(char)* namestart = null; |
| size_t namelen = 0; // !=0 if line continuation |
| const(char)* textstart = null; |
| size_t textlen = 0; |
| size_t paramcount = 0; |
| buf.writestring("$(DDOC_PARAMS "); |
| while (p < pend) |
| { |
| // Skip to start of macro |
| while (1) |
| { |
| switch (*p) |
| { |
| case ' ': |
| case '\t': |
| p++; |
| continue; |
| case '\n': |
| p++; |
| goto Lcont; |
| default: |
| if (isIdStart(p) || isCVariadicArg(p[0 .. cast(size_t)(pend - p)])) |
| break; |
| if (namelen) |
| goto Ltext; |
| // continuation of prev macro |
| goto Lskipline; |
| } |
| break; |
| } |
| tempstart = p; |
| while (isIdTail(p)) |
| p += utfStride(p); |
| if (isCVariadicArg(p[0 .. cast(size_t)(pend - p)])) |
| p += 3; |
| templen = p - tempstart; |
| while (*p == ' ' || *p == '\t') |
| p++; |
| if (*p != '=') |
| { |
| if (namelen) |
| goto Ltext; |
| // continuation of prev macro |
| goto Lskipline; |
| } |
| p++; |
| if (namelen) |
| { |
| // Output existing param |
| L1: |
| //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart); |
| ++paramcount; |
| HdrGenState hgs; |
| buf.writestring("$(DDOC_PARAM_ROW "); |
| { |
| buf.writestring("$(DDOC_PARAM_ID "); |
| { |
| size_t o = buf.length; |
| Parameter fparam = isFunctionParameter(a, namestart[0 .. namelen]); |
| if (!fparam) |
| { |
| // Comments on a template might refer to function parameters within. |
| // Search the parameters of nested eponymous functions (with the same name.) |
| fparam = isEponymousFunctionParameter(a, namestart[0 .. namelen]); |
| } |
| bool isCVariadic = isCVariadicParameter(a, namestart[0 .. namelen]); |
| if (isCVariadic) |
| { |
| buf.writestring("..."); |
| } |
| else if (fparam && fparam.type && fparam.ident) |
| { |
| .toCBuffer(fparam.type, buf, fparam.ident, &hgs); |
| } |
| else |
| { |
| if (isTemplateParameter(a, namestart, namelen)) |
| { |
| // 10236: Don't count template parameters for params check |
| --paramcount; |
| } |
| else if (!fparam) |
| { |
| 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.length == 1 ? isTypeFunction(s) : null; |
| if (tf) |
| { |
| size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.length : 0) + |
| cast(int)(tf.parameterList.varargs == VarArg.variadic); |
| if (pcount != paramcount) |
| { |
| 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.length; 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.length > 0) |
| { |
| for(int i = 0; i < imp.aliases.length; i++) |
| { |
| // Need to distinguish between |
| // `public import core.stdc.string : memcpy, memcmp;` and |
| // `public import core.stdc.string : copy = memcpy, compare = memcmp;` |
| auto a = imp.aliases[i]; |
| auto id = a ? a : imp.names[i]; |
| auto loc = Loc.init; |
| 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.length; 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.length; i++) |
| { |
| Dsymbol s = (*sds.members)[i]; |
| //printf("\ts = '%s'\n", s.toChars()); |
| // only expand if parent is a non-template (semantic won't work) |
| if (s.comment && s.isTemplateMixin() && s.parent && !s.parent.isTemplateDeclaration()) |
| expandTemplateMixinComments(cast(TemplateMixin)s, buf, sc); |
| emitComment(s, buf, sc); |
| } |
| emitComment(null, buf, sc); |
| sc.pop(); |
| if (buf.length == offset2) |
| { |
| /* Didn't write out any members, so back out last write |
| */ |
| buf.setsize(offset1); |
| } |
| else |
| buf.writestring(")"); |
| } |
| |
| 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) scope |
| { |
| this.buf = &buf; |
| this.sc = sc; |
| } |
| |
| override void visit(Dsymbol) |
| { |
| } |
| |
| override void visit(InvariantDeclaration) |
| { |
| } |
| |
| override void visit(UnitTestDeclaration) |
| { |
| } |
| |
| override void visit(PostBlitDeclaration) |
| { |
| } |
| |
| override void visit(DtorDeclaration) |
| { |
| } |
| |
| override void visit(StaticCtorDeclaration) |
| { |
| } |
| |
| override void visit(StaticDtorDeclaration) |
| { |
| } |
| |
| override void visit(TypeInfoDeclaration) |
| { |
| } |
| |
| void emit(Scope* sc, Dsymbol s, const(char)* com) |
| { |
| if (s && sc.lastdc && isDitto(com)) |
| { |
| sc.lastdc.a.push(s); |
| return; |
| } |
| // Put previous doc comment if exists |
| if (DocComment* dc = sc.lastdc) |
| { |
| assert(dc.a.length > 0, "Expects at least one declaration for a" ~ |
| "documentation comment"); |
| |
| auto symbol = dc.a[0]; |
| |
| buf.writestring("$(DDOC_MEMBER"); |
| buf.writestring("$(DDOC_MEMBER_HEADER"); |
| emitAnchor(*buf, symbol, sc, true); |
| buf.writeByte(')'); |
| |
| // Put the declaration signatures as the document 'title' |
| buf.writestring(ddoc_decl_s); |
| for (size_t i = 0; i < dc.a.length; i++) |
| { |
| Dsymbol sx = dc.a[i]; |
| // the added linebreaks in here make looking at multiple |
| // signatures more appealing |
| if (i == 0) |
| { |
| size_t o = buf.length; |
| toDocBuffer(sx, *buf, sc); |
| highlightCode(sc, sx, *buf, o); |
| buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)"); |
| continue; |
| } |
| buf.writestring("$(DDOC_DITTO "); |
| { |
| size_t o = buf.length; |
| toDocBuffer(sx, *buf, sc); |
| highlightCode(sc, sx, *buf, o); |
| } |
| buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)"); |
| buf.writeByte(')'); |
| } |
| buf.writestring(ddoc_decl_e); |
| // Put the ddoc comment as the document 'description' |
| buf.writestring(ddoc_decl_dd_s); |
| { |
| dc.writeSections(sc, &dc.a, buf); |
| if (ScopeDsymbol sds = dc.a[0].isScopeDsymbol()) |
| emitMemberComments(sds, *buf, sc); |
| } |
| buf.writestring(ddoc_decl_dd_e); |
| buf.writeByte(')'); |
| //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0); |
| } |
| if (s) |
| { |
| DocComment* dc = DocComment.parse(s, com); |
| dc.pmacrotable = &sc._module.macrotable; |
| sc.lastdc = dc; |
| } |
| } |
| |
| override void visit(Import imp) |
| { |
| if (imp.visible().kind != Visibility.Kind.public_ && sc.visibility.kind != Visibility.Kind.export_) |
| return; |
| |
| if (imp.comment) |
| emit(sc, imp, imp.comment); |
| } |
| |
| override void visit(Declaration d) |
| { |
| //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment); |
| //printf("type = %p\n", d.type); |
| const(char)* com = d.comment; |
| if (TemplateDeclaration td = getEponymousParent(d)) |
| { |
| if (isDitto(td.comment)) |
| com = td.comment; |
| else |
| com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true); |
| } |
| else |
| { |
| if (!d.ident) |
| return; |
| if (!d.type) |
| { |
| if (!d.isCtorDeclaration() && |
| !d.isAliasDeclaration() && |
| !d.isVarDeclaration()) |
| { |
| return; |
| } |
| } |
| if (d.visibility.kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) |
| return; |
| } |
| if (!com) |
| return; |
| emit(sc, d, com); |
| } |
| |
| override void visit(AggregateDeclaration ad) |
| { |
| //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars()); |
| const(char)* com = ad.comment; |
| if (TemplateDeclaration td = getEponymousParent(ad)) |
| { |
| if (isDitto(td.comment)) |
| com = td.comment; |
| else |
| com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true); |
| } |
| else |
| { |
| if (ad.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) |
| return; |
| if (!ad.comment) |
| return; |
| } |
| if (!com) |
| return; |
| emit(sc, ad, com); |
| } |
| |
| override void visit(TemplateDeclaration td) |
| { |
| //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind()); |
| if (td.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) |
| return; |
| if (!td.comment) |
| return; |
| if (Dsymbol ss = getEponymousMember(td)) |
| { |
| ss.accept(this); |
| return; |
| } |
| emit(sc, td, td.comment); |
| } |
| |
| override void visit(EnumDeclaration ed) |
| { |
| if (ed.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) |
| return; |
| if (ed.isAnonymous() && ed.members) |
| { |
| for (size_t i = 0; i < ed.members.length; i++) |
| { |
| Dsymbol s = (*ed.members)[i]; |
| emitComment(s, *buf, sc); |
| } |
| return; |
| } |
| if (!ed.comment) |
| return; |
| if (ed.isAnonymous()) |
| return; |
| emit(sc, ed, ed.comment); |
| } |
| |
| override void visit(EnumMember em) |
| { |
| //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment); |
| if (em.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) |
| return; |
| if (!em.comment) |
| return; |
| emit(sc, em, em.comment); |
| } |
| |
| override void visit(AttribDeclaration ad) |
| { |
| //printf("AttribDeclaration::emitComment(sc = %p)\n", sc); |
| /* A general problem with this, |
| * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516 |
| * is that attributes are not transmitted through to the underlying |
| * member declarations for template bodies, because semantic analysis |
| * is not done for template declaration bodies |
| * (only template instantiations). |
| * Hence, Ddoc omits attributes from template members. |
| */ |
| Dsymbols* d = ad.include(null); |
| if (d) |
| { |
| for (size_t i = 0; i < d.length; i++) |
| { |
| Dsymbol s = (*d)[i]; |
| //printf("AttribDeclaration::emitComment %s\n", s.toChars()); |
| emitComment(s, *buf, sc); |
| } |
| } |
| } |
| |
| override void visit(VisibilityDeclaration pd) |
| { |
| if (pd.decl) |
| { |
| Scope* scx = sc; |
| sc = sc.copy(); |
| sc.visibility = pd.visibility; |
| visit(cast(AttribDeclaration)pd); |
| scx.lastdc = sc.lastdc; |
| sc = sc.pop(); |
| } |
| } |
| |
| override void visit(ConditionalDeclaration cd) |
| { |
| //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc); |
| if (cd.condition.inc != Include.notComputed) |
| { |
| visit(cast(AttribDeclaration)cd); |
| return; |
| } |
| /* If generating doc comment, be careful because if we're inside |
| * a template, then include(null) will fail. |
| */ |
| Dsymbols* d = cd.decl ? cd.decl : cd.elsedecl; |
| for (size_t i = 0; i < d.length; i++) |
| { |
| Dsymbol s = (*d)[i]; |
| emitComment(s, *buf, sc); |
| } |
| } |
| } |
| |
| scope EmitComment v = new EmitComment(buf, sc); |
| if (!s) |
| v.emit(sc, null, null); |
| else |
| s.accept(v); |
| } |
| |
| 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) scope |
| { |
| this.buf = &buf; |
| this.sc = sc; |
| } |
| |
| override void visit(Dsymbol s) |
| { |
| //printf("Dsymbol::toDocbuffer() %s\n", s.toChars()); |
| HdrGenState hgs; |
| hgs.ddoc = true; |
| .toCBuffer(s, buf, &hgs); |
| } |
| |
| void prefix(Dsymbol s) |
| { |
| if (s.isDeprecated()) |
| buf.writestring("deprecated "); |
| if (Declaration d = s.isDeclaration()) |
| { |
| emitVisibility(*buf, d); |
| if (d.isStatic()) |
| buf.writestring("static "); |
| else if (d.isFinal()) |
| buf.writestring("final "); |
| else if (d.isAbstract()) |
| buf.writestring("abstract "); |
| |
| if (d.isFuncDeclaration()) // functionToBufferFull handles this |
| return; |
| |
| if (d.isImmutable()) |
| buf.writestring("immutable "); |
| if (d.storage_class & STC.shared_) |
| buf.writestring("shared "); |
| if (d.isWild()) |
| buf.writestring("inout "); |
| if (d.isConst()) |
| buf.writestring("const "); |
| |
| if (d.isSynchronized()) |
| buf.writestring("synchronized "); |
| |
| if (d.storage_class & STC.manifest) |
| buf.writestring("enum "); |
| |
| // Add "auto" for the untyped variable in template members |
| if (!d.type && d.isVarDeclaration() && |
| !d.isImmutable() && !(d.storage_class & STC.shared_) && !d.isWild() && !d.isConst() && |
| !d.isSynchronized()) |
| { |
| buf.writestring("auto "); |
| } |
| } |
| } |
| |
| override void visit(Import i) |
| { |
| HdrGenState hgs; |
| hgs.ddoc = true; |
| emitVisibility(*buf, i); |
| .toCBuffer(i, buf, &hgs); |
| } |
| |
| override void visit(Declaration d) |
| { |
| if (!d.ident) |
| return; |
| TemplateDeclaration td = getEponymousParent(d); |
| //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--"); |
| HdrGenState hgs; |
| hgs.ddoc = true; |
| if (d.isDeprecated()) |
| buf.writestring("$(DEPRECATED "); |
| prefix(d); |
| if (d.type) |
| { |
| Type origType = d.originalType ? d.originalType : d.type; |
| if (origType.ty == Tfunction) |
| { |
| functionToBufferFull(cast(TypeFunction)origType, buf, d.ident, &hgs, td); |
| } |
| else |
| .toCBuffer(origType, buf, d.ident, &hgs); |
| } |
| else |
| buf.writestring(d.ident.toString()); |
| if (d.isVarDeclaration() && td) |
| { |
| buf.writeByte('('); |
| if (td.origParameters && td.origParameters.length) |
| { |
| for (size_t i = 0; i < td.origParameters.length; i++) |
| { |
| if (i) |
| buf.writestring(", "); |
| toCBuffer((*td.origParameters)[i], buf, &hgs); |
| } |
| } |
| buf.writeByte(')'); |
| } |
| // emit constraints if declaration is a templated declaration |
| if (td && td.constraint) |
| { |
| bool noFuncDecl = td.isFuncDeclaration() is null; |
| if (noFuncDecl) |
| { |
| buf.writestring("$(DDOC_CONSTRAINT "); |
| } |
| |
| .toCBuffer(td.constraint, buf, &hgs); |
| |
| if (noFuncDecl) |
| { |
| buf.writestring(")"); |
| } |
| } |
| if (d.isDeprecated()) |
| buf.writestring(")"); |
| buf.writestring(";\n"); |
| } |
| |
| override void visit(AliasDeclaration ad) |
| { |
| //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars()); |
| if (!ad.ident) |
| return; |
| if (ad.isDeprecated()) |
| buf.writestring("deprecated "); |
| emitVisibility(*buf, ad); |
| buf.printf("alias %s = ", ad.toChars()); |
| if (Dsymbol s = ad.aliassym) // ident alias |
| { |
| prettyPrintDsymbol(s, ad.parent); |
| } |
| else if (Type type = ad.getType()) // type alias |
| { |
| if (type.ty == Tclass || type.ty == Tstruct || type.ty == Tenum) |
| { |
| 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.length; i++) |
| { |
| BaseClass* bc = (*cd.baseclasses)[i]; |
| if (bc.sym && bc.sym.ident == Id.Object) |
| continue; |
| if (any) |
| buf.writestring(", "); |
| else |
| { |
| buf.writestring(": "); |
| any = 1; |
| } |
| |
| if (bc.sym) |
| { |
| buf.printf("$(DDOC_PSUPER_SYMBOL %s)", bc.sym.toPrettyChars()); |
| } |
| else |
| { |
| HdrGenState hgs; |
| .toCBuffer(bc.type, buf, null, &hgs); |
| } |
| } |
| buf.writestring(";\n"); |
| } |
| |
| override void visit(EnumDeclaration ed) |
| { |
| if (!ed.ident) |
| return; |
| buf.printf("%s %s", ed.kind(), ed.toChars()); |
| if (ed.memtype) |
| { |
| buf.writestring(": $(DDOC_ENUM_BASETYPE "); |
| HdrGenState hgs; |
| .toCBuffer(ed.memtype, buf, null, &hgs); |
| buf.writestring(")"); |
| } |
| buf.writestring(";\n"); |
| } |
| |
| override void visit(EnumMember em) |
| { |
| if (!em.ident) |
| return; |
| buf.writestring(em.toChars()); |
| } |
| } |
| |
| scope ToDocBuffer v = new ToDocBuffer(buf, sc); |
| s.accept(v); |
| } |
| |
| /*********************************************************** |
| */ |
| struct DocComment |
| { |
| Sections sections; // Section*[] |
| Section summary; |
| Section copyright; |
| Section macros; |
| MacroTable* pmacrotable; |
| Escape* escapetable; |
| Dsymbols a; |
| |
| static DocComment* parse(Dsymbol s, const(char)* comment) |
| { |
| //printf("parse(%s): '%s'\n", s.toChars(), comment); |
| auto dc = new DocComment(); |
| dc.a.push(s); |
| if (!comment) |
| return dc; |
| dc.parseSections(comment); |
| for (size_t i = 0; i < dc.sections.length; i++) |
| { |
| Section sec = dc.sections[i]; |
| if (iequals("copyright", sec.name)) |
| { |
| dc.copyright = sec; |
| } |
| if (iequals("macros", sec.name)) |
| { |
| dc.macros = sec; |
| } |
| } |
| return dc; |
| } |
| |
| /************************************************ |
| * Parse macros out of Macros: section. |
| * Macros are of the form: |
| * name1 = value1 |
| * |
| * name2 = value2 |
| */ |
| extern(D) static void parseMacros( |
| Escape* escapetable, ref MacroTable pmacrotable, const(char)[] m) |
| { |
| const(char)* p = m.ptr; |
| size_t len = m.length; |
| const(char)* pend = p + len; |
| const(char)* tempstart = null; |
| size_t templen = 0; |
| const(char)* namestart = null; |
| size_t namelen = 0; // !=0 if line continuation |
| const(char)* textstart = null; |
| size_t textlen = 0; |
| while (p < pend) |
| { |
| // Skip to start of macro |
| while (1) |
| { |
| if (p >= pend) |
| goto Ldone; |
| switch (*p) |
| { |
| case ' ': |
| case '\t': |
| p++; |
| continue; |
| case '\r': |
| case '\n': |
| p++; |
| goto Lcont; |
| default: |
| if (isIdStart(p)) |
| break; |
| if (namelen) |
| goto Ltext; // continuation of prev macro |
| goto Lskipline; |
| } |
| break; |
| } |
| tempstart = p; |
| while (1) |
| { |
| if (p >= pend) |
| goto Ldone; |
| if (!isIdTail(p)) |
| break; |
| p += utfStride(p); |
| } |
| templen = p - tempstart; |
| while (1) |
| { |
| if (p >= pend) |
| goto Ldone; |
| if (!(*p == ' ' || *p == '\t')) |
| break; |
| p++; |
| } |
| if (*p != '=') |
| { |
| if (namelen) |
| goto Ltext; // continuation of prev macro |
| goto Lskipline; |
| } |
| p++; |
| if (p >= pend) |
| goto Ldone; |
| if (namelen) |
| { |
| // Output existing macro |
| L1: |
| //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart); |
| if (iequals("ESCAPES", namestart[0 .. namelen])) |
| parseEscapes(escapetable, textstart[0 .. textlen]); |
| else |
| pmacrotable.define(namestart[0 .. namelen], textstart[0 .. textlen]); |
| namelen = 0; |
| if (p >= pend) |
| break; |
| } |
| namestart = tempstart; |
| namelen = templen; |
| while (p < pend && (*p == ' ' || *p == '\t')) |
| p++; |
| textstart = p; |
| Ltext: |
| while (p < pend && *p != '\r' && *p != '\n') |
| p++; |
| textlen = p - textstart; |
| p++; |
| //printf("p = %p, pend = %p\n", p, pend); |
| Lcont: |
| continue; |
| Lskipline: |
| // Ignore this line |
| while (p < pend && *p != '\r' && *p != '\n') |
| p++; |
| } |
| Ldone: |
| if (namelen) |
| goto L1; // write out last one |
| } |
| |
| /************************************** |
| * Parse escapes of the form: |
| * /c/string/ |
| * where c is a single character. |
| * Multiple escapes can be separated |
| * by whitespace and/or commas. |
| */ |
| static void parseEscapes(Escape* escapetable, const(char)[] text) |
| { |
| if (!escapetable) |
| { |
| escapetable = new Escape(); |
| memset(escapetable, 0, Escape.sizeof); |
| } |
| //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable); |
| const(char)* p = text.ptr; |
| const(char)* pend = p + text.length; |
| while (1) |
| { |
| while (1) |
| { |
| if (p + 4 >= pend) |
| return; |
| if (!(*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ',')) |
| break; |
| p++; |
| } |
| if (p[0] != '/' || p[2] != '/') |
| return; |
| char c = p[1]; |
| p += 3; |
| const(char)* start = p; |
| while (1) |
| { |
| if (p >= pend) |
| return; |
| if (*p == '/') |
| break; |
| p++; |
| } |
| size_t len = p - start; |
| char* s = cast(char*)memcpy(mem.xmalloc(len + 1), start, len); |
| s[len] = 0; |
| escapetable.strings[c] = s[0 .. len]; |
| //printf("\t%c = '%s'\n", c, s); |
| p++; |
| } |
| } |
| |
| /***************************************** |
| * Parse next paragraph out of *pcomment. |
| * Update *pcomment to point past paragraph. |
| * Returns NULL if no more paragraphs. |
| * If paragraph ends in 'identifier:', |
| * then (*pcomment)[0 .. idlen] is the identifier. |
| */ |
| void parseSections(const(char)* comment) |
| { |
| const(char)* p; |
| const(char)* pstart; |
| const(char)* pend; |
| const(char)* idstart = null; // dead-store to prevent spurious warning |
| size_t idlen; |
| const(char)* name = null; |
| size_t namelen = 0; |
| //printf("parseSections('%s')\n", comment); |
| p = comment; |
| while (*p) |
| { |
| const(char)* pstart0 = p; |
| p = skipwhitespace(p); |
| pstart = p; |
| pend = p; |
| |
| // Undo indent if starting with a list item |
| if ((*p == '-' || *p == '+' || *p == '*') && (*(p+1) == ' ' || *(p+1) == '\t')) |
| pstart = pstart0; |
| else |
| { |
| const(char)* pitem = p; |
| while (*pitem >= '0' && *pitem <= '9') |
| ++pitem; |
| if (pitem > p && *pitem == '.' && (*(pitem+1) == ' ' || *(pitem+1) == '\t')) |
| pstart = pstart0; |
| } |
| |
| /* Find end of section, which is ended by one of: |
| * 'identifier:' (but not inside a code section) |
| * '\0' |
| */ |
| idlen = 0; |
| int inCode = 0; |
| while (1) |
| { |
| // Check for start/end of a code section |
| if (*p == '-' || *p == '`' || *p == '~') |
| { |
| char c = *p; |
| int numdash = 0; |
| while (*p == c) |
| { |
| ++numdash; |
| p++; |
| } |
| // BUG: handle UTF PS and LS too |
| if ((!*p || *p == '\r' || *p == '\n' || (!inCode && c != '-')) && numdash >= 3) |
| { |
| inCode = inCode == c ? false : c; |
| if (inCode) |
| { |
| // restore leading indentation |
| while (pstart0 < pstart && isIndentWS(pstart - 1)) |
| --pstart; |
| } |
| } |
| pend = p; |
| } |
| if (!inCode && isIdStart(p)) |
| { |
| const(char)* q = p + utfStride(p); |
| while (isIdTail(q)) |
| q += utfStride(q); |
| |
| // Detected tag ends it |
| if (*q == ':' && isupper(*p) |
| && (isspace(q[1]) || q[1] == 0)) |
| { |
| idlen = q - p; |
| idstart = p; |
| for (pend = p; pend > pstart; pend--) |
| { |
| if (pend[-1] == '\n') |
| break; |
| } |
| p = q + 1; |
| break; |
| } |
| } |
| while (1) |
| { |
| if (!*p) |
| goto L1; |
| if (*p == '\n') |
| { |
| p++; |
| if (*p == '\n' && !summary && !namelen && !inCode) |
| { |
| pend = p; |
| p++; |
| goto L1; |
| } |
| break; |
| } |
| p++; |
| pend = p; |
| } |
| p = skipwhitespace(p); |
| } |
| L1: |
| if (namelen || pstart < pend) |
| { |
| Section s; |
| if (iequals("Params", name[0 .. namelen])) |
| s = new ParamSection(); |
| else if (iequals("Macros", name[0 .. namelen])) |
| s = new MacroSection(); |
| else |
| s = new Section(); |
| s.name = name[0 .. namelen]; |
| s.body_ = pstart[0 .. pend - pstart]; |
| s.nooutput = 0; |
| //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body); |
| sections.push(s); |
| if (!summary && !namelen) |
| summary = s; |
| } |
| if (idlen) |
| { |
| name = idstart; |
| namelen = idlen; |
| } |
| else |
| { |
| name = null; |
| namelen = 0; |
| if (!*p) |
| break; |
| } |
| } |
| } |
| |
| void writeSections(Scope* sc, Dsymbols* a, OutBuffer* buf) |
| { |
| assert(a.length); |
| //printf("DocComment::writeSections()\n"); |
| Loc loc = (*a)[0].loc; |
| if (Module m = (*a)[0].isModule()) |
| { |
| if (m.md) |
| loc = m.md.loc; |
| } |
| size_t offset1 = buf.length; |
| buf.writestring("$(DDOC_SECTIONS "); |
| size_t offset2 = buf.length; |
| for (size_t i = 0; i < sections.length; i++) |
| { |
| Section sec = sections[i]; |
| if (sec.nooutput) |
| continue; |
| //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body); |
| if (!sec.name.length && i == 0) |
| { |
| buf.writestring("$(DDOC_SUMMARY "); |
| size_t o = buf.length; |
| buf.write(sec.body_); |
| escapeStrayParenthesis(loc, buf, o, true); |
| highlightText(sc, a, loc, *buf, o); |
| buf.writestring(")"); |
| } |
| else |
| sec.write(loc, &this, sc, a, buf); |
| } |
| for (size_t i = 0; i < a.length; i++) |
| { |
| Dsymbol s = (*a)[i]; |
| if (Dsymbol td = getEponymousParent(s)) |
| s = td; |
| for (UnitTestDeclaration utd = s.ddocUnittest; utd; utd = utd.ddocUnittest) |
| { |
| if (utd.visibility.kind == Visibility.Kind.private_ || !utd.comment || !utd.fbody) |
| continue; |
| // Strip whitespaces to avoid showing empty summary |
| const(char)* c = utd.comment; |
| while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r') |
| ++c; |
| buf.writestring("$(DDOC_EXAMPLES "); |
| size_t o = buf.length; |
| buf.writestring(cast(char*)c); |
| if (utd.codedoc) |
| { |
| auto codedoc = utd.codedoc.stripLeadingNewlines; |
| size_t n = getCodeIndent(codedoc); |
| while (n--) |
| buf.writeByte(' '); |
| buf.writestring("----\n"); |
| buf.writestring(codedoc); |
| buf.writestring("----\n"); |
| highlightText(sc, a, loc, *buf, o); |
| } |
| buf.writestring(")"); |
| } |
| } |
| if (buf.length == offset2) |
| { |
| /* Didn't write out any sections, so back out last write |
| */ |
| buf.setsize(offset1); |
| buf.writestring("\n"); |
| } |
| else |
| buf.writestring(")"); |
| } |
| } |
| |
| /***************************************** |
| * Return true if comment consists entirely of "ditto". |
| */ |
| 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.length; i++) |
| { |
| TemplateDeclaration td = (*a)[i].isTemplateDeclaration(); |
| // Check for the parent, if the current symbol is not a template declaration. |
| if (!td) |
| td = getEponymousParent((*a)[i]); |
| if (td && td.origParameters) |
| { |
| foreach (tp; *td.origParameters) |
| { |
| if (tp.ident && p[0 .. len] == tp.ident.toString()) |
| { |
| return tp; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /**************************************************** |
| * Return true if str is a reserved symbol name |
| * that starts with a double underscore. |
| */ |
| 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].isAtItemInThisList(buf, i + 1, iAfterSpaces)) |
| { |
| // end a sibling list item |
| i = buf.insert(i, ")"); |
| iParagraphStart = skipChars(buf, i, " \t\r\n"); |
| } |
| else if (iAfterSpaces >= buf.length || (buf[iAfterSpaces] != '\r' && buf[iAfterSpaces] != '\n')) |
| { |
| // end nested lists that are indented more than this content |
| const indent = getMarkdownIndent(buf, i + 1, iAfterSpaces); |
| while (nestedLists.length && nestedLists[$-1].contentIndent > indent) |
| { |
| i = buf.insert(i, ")\n)"); |
| --nestedLists.length; |
| iParagraphStart = skipChars(buf, i, " \t\r\n"); |
| |
| if (nestedLists.length && nestedLists[$-1].isAtItemInThisList(buf, i + 1, iParagraphStart)) |
| { |
| i = buf.insert(i, ")"); |
| ++iParagraphStart; |
| break; |
| } |
| } |
| } |
| } |
| |
| /**************************************************** |
| * Parse an unordered list item at the current position |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iLineStart = the index within `buf` of the first character of the line |
| * i = the index within `buf` of the list item |
| * Returns: the parsed list item, or a list item with type `.init` if no list item is available |
| */ |
| private static MarkdownList parseUnorderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) |
| { |
| if (i+1 < buf.length && |
| (buf[i] == '-' || |
| buf[i] == '*' || |
| buf[i] == '+') && |
| (buf[i+1] == ' ' || |
| buf[i+1] == '\t' || |
| buf[i+1] == '\r' || |
| buf[i+1] == '\n')) |
| { |
| const iContentStart = skipChars(buf, i + 1, " \t"); |
| const delimiterIndent = getMarkdownIndent(buf, iLineStart, i); |
| const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart); |
| auto list = MarkdownList(null, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[i]); |
| return list; |
| } |
| return MarkdownList(); |
| } |
| |
| /**************************************************** |
| * Parse an ordered list item at the current position |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iLineStart = the index within `buf` of the first character of the line |
| * i = the index within `buf` of the list item |
| * Returns: the parsed list item, or a list item with type `.init` if no list item is available |
| */ |
| private static MarkdownList parseOrderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) |
| { |
| size_t iAfterNumbers = skipChars(buf, i, "0123456789"); |
| if (iAfterNumbers - i > 0 && |
| iAfterNumbers - i <= 9 && |
| iAfterNumbers + 1 < buf.length && |
| buf[iAfterNumbers] == '.' && |
| (buf[iAfterNumbers+1] == ' ' || |
| buf[iAfterNumbers+1] == '\t' || |
| buf[iAfterNumbers+1] == '\r' || |
| buf[iAfterNumbers+1] == '\n')) |
| { |
| const iContentStart = skipChars(buf, iAfterNumbers + 1, " \t"); |
| const delimiterIndent = getMarkdownIndent(buf, iLineStart, i); |
| const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart); |
| size_t iNumberStart = skipChars(buf, i, "0"); |
| if (iNumberStart == iAfterNumbers) |
| --iNumberStart; |
| auto orderedStart = buf[][iNumberStart .. iAfterNumbers]; |
| if (orderedStart == "1") |
| orderedStart = null; |
| return MarkdownList(orderedStart.idup, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[iAfterNumbers]); |
| } |
| return MarkdownList(); |
| } |
| } |
| |
| /**************************************************** |
| * A Markdown link. |
| */ |
| private struct MarkdownLink |
| { |
| string href; /// the link destination |
| string title; /// an optional title for the link |
| string label; /// an optional label for the link |
| Dsymbol symbol; /// an optional symbol to link to |
| |
| /**************************************************** |
| * Replace a Markdown link or link definition in the form of: |
| * - Inline link: `[foo](url/ 'optional title')` |
| * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]` |
| * - Link reference definition: `[bar]: url/ 'optional title'` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the potential link. |
| * If this function succeeds it will be adjusted to fit the inserted link macro. |
| * loc = the current location within the file |
| * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts |
| * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter |
| * linkReferences = previously parsed link references. When this function returns it may contain |
| * additional previously unparsed references. |
| * Returns: whether a reference link was found and replaced at `i` |
| */ |
| static bool replaceLink(ref OutBuffer buf, ref size_t i, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences) |
| { |
| const delimiter = inlineDelimiters[delimiterIndex]; |
| MarkdownLink link; |
| |
| size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter); |
| if (iEnd > i) |
| { |
| i = delimiter.iStart; |
| link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc); |
| inlineDelimiters.length = delimiterIndex; |
| return true; |
| } |
| |
| iEnd = link.parseInlineLink(buf, i); |
| if (iEnd == i) |
| { |
| iEnd = link.parseReferenceLink(buf, i, delimiter); |
| if (iEnd > i) |
| { |
| const label = link.label; |
| link = linkReferences.lookupReference(label, buf, i, loc); |
| // check rightFlanking to avoid replacing things like int[string] |
| if (!link.href.length && !delimiter.rightFlanking) |
| link = linkReferences.lookupSymbol(label); |
| if (!link.href.length) |
| return false; |
| } |
| } |
| |
| if (iEnd == i) |
| return false; |
| |
| immutable delta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, delimiterIndex); |
| iEnd += delta; |
| i += delta; |
| link.replaceLink(buf, i, iEnd, delimiter); |
| return true; |
| } |
| |
| /**************************************************** |
| * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the potential link. |
| * If this function succeeds it will be adjusted to fit the inserted link macro. |
| * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts |
| * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter |
| * linkReferences = previously parsed link references. When this function returns it may contain |
| * additional previously unparsed references. |
| * loc = the current location in the file |
| * Returns: whether a reference link was found and replaced at `i` |
| */ |
| static bool replaceReferenceDefinition(ref OutBuffer buf, ref size_t i, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences, const ref Loc loc) |
| { |
| const delimiter = inlineDelimiters[delimiterIndex]; |
| MarkdownLink link; |
| size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter); |
| if (iEnd == i) |
| return false; |
| |
| i = delimiter.iStart; |
| link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc); |
| inlineDelimiters.length = delimiterIndex; |
| return true; |
| } |
| |
| /**************************************************** |
| * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the inline link. |
| * Returns: the index at the end of parsing the link, or `i` if parsing failed. |
| */ |
| private size_t parseInlineLink(ref OutBuffer buf, size_t i) |
| { |
| size_t iEnd = i + 1; |
| if (iEnd >= buf.length || buf[iEnd] != '(') |
| return i; |
| ++iEnd; |
| |
| if (!parseHref(buf, iEnd)) |
| return i; |
| |
| iEnd = skipChars(buf, iEnd, " \t\r\n"); |
| if (buf[iEnd] != ')') |
| { |
| if (parseTitle(buf, iEnd)) |
| iEnd = skipChars(buf, iEnd, " \t\r\n"); |
| } |
| |
| if (buf[iEnd] != ')') |
| return i; |
| |
| return iEnd + 1; |
| } |
| |
| /**************************************************** |
| * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the inline link. |
| * delimiter = the delimiter that starts this link |
| * Returns: the index at the end of parsing the link, or `i` if parsing failed. |
| */ |
| private size_t parseReferenceLink(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) |
| { |
| size_t iStart = i + 1; |
| size_t iEnd = iStart; |
| if (iEnd >= buf.length || buf[iEnd] != '[' || (iEnd+1 < buf.length && buf[iEnd+1] == ']')) |
| { |
| // collapsed reference [foo][] or shortcut reference [foo] |
| iStart = delimiter.iStart + delimiter.count - 1; |
| if (buf[iEnd] == '[') |
| iEnd += 2; |
| } |
| |
| parseLabel(buf, iStart); |
| if (!label.length) |
| return i; |
| |
| if (iEnd < iStart) |
| iEnd = iStart; |
| return iEnd; |
| } |
| |
| /**************************************************** |
| * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the inline link. |
| * delimiter = the delimiter that starts this link |
| * Returns: the index at the end of parsing the link, or `i` if parsing failed. |
| */ |
| private size_t parseReferenceDefinition(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) |
| { |
| if (!delimiter.atParagraphStart || delimiter.type != '[' || |
| i+1 >= buf.length || buf[i+1] != ':') |
| return i; |
| |
| size_t iEnd = delimiter.iStart; |
| parseLabel(buf, iEnd); |
| if (label.length == 0 || iEnd != i + 1) |
| return i; |
| |
| ++iEnd; |
| iEnd = skipChars(buf, iEnd, " \t"); |
| skipOneNewline(buf, iEnd); |
| |
| if (!parseHref(buf, iEnd) || href.length == 0) |
| return i; |
| |
| iEnd = skipChars(buf, iEnd, " \t"); |
| const requireNewline = !skipOneNewline(buf, iEnd); |
| const iBeforeTitle = iEnd; |
| |
| if (parseTitle(buf, iEnd)) |
| { |
| iEnd = skipChars(buf, iEnd, " \t"); |
| if (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n') |
| { |
| // the title must end with a newline |
| title.length = 0; |
| iEnd = iBeforeTitle; |
| } |
| } |
| |
| iEnd = skipChars(buf, iEnd, " \t"); |
| if (requireNewline && iEnd < buf.length-1 && buf[iEnd] != '\r' && buf[iEnd] != '\n') |
| return i; |
| |
| return iEnd; |
| } |
| |
| /**************************************************** |
| * Parse and normalize a Markdown reference label |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `[` character at the start of the label. |
| * If this function returns a non-empty label then `i` will point just after the ']' at the end of the label. |
| * Returns: the parsed and normalized label, possibly empty |
| */ |
| private bool parseLabel(ref OutBuffer buf, ref size_t i) |
| { |
| if (buf[i] != '[') |
| return false; |
| |
| const slice = buf[]; |
| size_t j = i + 1; |
| |
| // Some labels have already been en-symboled; handle that |
| const inSymbol = j+15 < slice.length && slice[j..j+15] == "$(DDOC_PSYMBOL "; |
| if (inSymbol) |
| j += 15; |
| |
| for (; j < slice.length; ++j) |
| { |
| const c = slice[j]; |
| switch (c) |
| { |
| case ' ': |
| case '\t': |
| case '\r': |
| case '\n': |
| if (label.length && label[$-1] != ' ') |
| label ~= ' '; |
| break; |
| case ')': |
| if (inSymbol && j+1 < slice.length && slice[j+1] == ']') |
| { |
| ++j; |
| goto case ']'; |
| } |
| goto default; |
| case '[': |
| if (slice[j-1] != '\\') |
| { |
| label.length = 0; |
| return false; |
| } |
| break; |
| case ']': |
| if (label.length && label[$-1] == ' ') |
| --label.length; |
| if (label.length) |
| { |
| i = j + 1; |
| return true; |
| } |
| return false; |
| default: |
| label ~= c; |
| break; |
| } |
| } |
| label.length = 0; |
| return false; |
| } |
| |
| /**************************************************** |
| * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the first character of the URL. |
| * If this function succeeds `i` will point just after the end of the URL. |
| * Returns: whether a URL was found and parsed |
| */ |
| private bool parseHref(ref OutBuffer buf, ref size_t i) |
| { |
| size_t j = skipChars(buf, i, " \t"); |
| |
| size_t iHrefStart = j; |
| size_t parenDepth = 1; |
| bool inPointy = false; |
| const slice = buf[]; |
| for (; j < slice.length; j++) |
| { |
| switch (slice[j]) |
| { |
| case '<': |
| if (!inPointy && j == iHrefStart) |
| { |
| inPointy = true; |
| ++iHrefStart; |
| } |
| break; |
| case '>': |
| if (inPointy && slice[j-1] != '\\') |
| goto LReturnHref; |
| break; |
| case '(': |
| if (!inPointy && slice[j-1] != '\\') |
| ++parenDepth; |
| break; |
| case ')': |
| if (!inPointy && slice[j-1] != '\\') |
| { |
| --parenDepth; |
| if (!parenDepth) |
| goto LReturnHref; |
| } |
| break; |
| case ' ': |
| case '\t': |
| case '\r': |
| case '\n': |
| if (inPointy) |
| { |
| // invalid link |
| return false; |
| } |
| goto LReturnHref; |
| default: |
| break; |
| } |
| } |
| if (inPointy) |
| return false; |
| LReturnHref: |
| auto href = slice[iHrefStart .. j].dup; |
| this.href = cast(string) percentEncode(removeEscapeBackslashes(href)).replaceChar(',', "$(COMMA)"); |
| i = j; |
| if (inPointy) |
| ++i; |
| return true; |
| } |
| |
| /**************************************************** |
| * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the first character of the title. |
| * If this function succeeds `i` will point just after the end of the title. |
| * Returns: whether a title was found and parsed |
| */ |
| private bool parseTitle(ref OutBuffer buf, ref size_t i) |
| { |
| size_t j = skipChars(buf, i, " \t"); |
| if (j >= buf.length) |
| return false; |
| |
| char type = buf[j]; |
| if (type != '"' && type != '\'' && type != '(') |
| return false; |
| if (type == '(') |
| type = ')'; |
| |
| const iTitleStart = j + 1; |
| size_t iNewline = 0; |
| const slice = buf[]; |
| for (j = iTitleStart; j < slice.length; j++) |
| { |
| const c = slice[j]; |
| switch (c) |
| { |
| case ')': |
| case '"': |
| case '\'': |
| if (type == c && slice[j-1] != '\\') |
| goto LEndTitle; |
| iNewline = 0; |
| break; |
| case ' ': |
| case '\t': |
| case '\r': |
| break; |
| case '\n': |
| if (iNewline) |
| { |
| // no blank lines in titles |
| return false; |
| } |
| iNewline = j; |
| break; |
| default: |
| iNewline = 0; |
| break; |
| } |
| } |
| return false; |
| LEndTitle: |
| auto title = slice[iTitleStart .. j].dup; |
| this.title = cast(string) removeEscapeBackslashes(title). |
| replaceChar(',', "$(COMMA)"). |
| replaceChar('"', "$(QUOTE)"); |
| i = j + 1; |
| return true; |
| } |
| |
| /**************************************************** |
| * Replace a Markdown link or image with the appropriate macro |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `]` character of the inline link. |
| * When this function returns it will be adjusted to the end of the inserted macro. |
| * iLinkEnd = the index within `buf` that points just after the last character of the link |
| * delimiter = the Markdown delimiter that started the link or image |
| */ |
| private void replaceLink(ref OutBuffer buf, ref size_t i, size_t iLinkEnd, MarkdownDelimiter delimiter) |
| { |
| size_t iAfterLink = i - delimiter.count; |
| string macroName; |
| if (symbol) |
| { |
| macroName = "$(SYMBOL_LINK "; |
| } |
| else if (title.length) |
| { |
| if (delimiter.type == '[') |
| macroName = "$(LINK_TITLE "; |
| else |
| macroName = "$(IMAGE_TITLE "; |
| } |
| else |
| { |
| if (delimiter.type == '[') |
| macroName = "$(LINK2 "; |
| else |
| macroName = "$(IMAGE "; |
| } |
| buf.remove(delimiter.iStart, delimiter.count); |
| buf.remove(i - delimiter.count, iLinkEnd - i); |
| iLinkEnd = buf.insert(delimiter.iStart, macroName); |
| iLinkEnd = buf.insert(iLinkEnd, href); |
| iLinkEnd = buf.insert(iLinkEnd, ", "); |
| iAfterLink += macroName.length + href.length + 2; |
| if (title.length) |
| { |
| iLinkEnd = buf.insert(iLinkEnd, title); |
| iLinkEnd = buf.insert(iLinkEnd, ", "); |
| iAfterLink += title.length + 2; |
| |
| // Link macros with titles require escaping commas |
| for (size_t j = iLinkEnd; j < iAfterLink; ++j) |
| if (buf[j] == ',') |
| { |
| buf.remove(j, 1); |
| j = buf.insert(j, "$(COMMA)") - 1; |
| iAfterLink += 7; |
| } |
| } |
| // TODO: if image, remove internal macros, leaving only text |
| buf.insert(iAfterLink, ")"); |
| i = iAfterLink; |
| } |
| |
| /**************************************************** |
| * Store the Markdown link definition and remove it from `buf` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` that points to the `[` character at the start of the link definition. |
| * When this function returns it will be adjusted to exclude the link definition. |
| * iEnd = the index within `buf` that points just after the end of the definition |
| * linkReferences = previously parsed link references. When this function returns it may contain |
| * an additional reference. |
| * loc = the current location in the file |
| */ |
| private void storeAndReplaceDefinition(ref OutBuffer buf, ref size_t i, size_t iEnd, ref MarkdownLinkReferences linkReferences, const ref Loc loc) |
| { |
| // Remove the definition and trailing whitespace |
| iEnd = skipChars(buf, iEnd, " \t\r\n"); |
| buf.remove(i, iEnd - i); |
| i -= 2; |
| |
| string lowercaseLabel = label.toLowercase(); |
| if (lowercaseLabel !in linkReferences.references) |
| linkReferences.references[lowercaseLabel] = this; |
| } |
| |
| /**************************************************** |
| * Remove Markdown escaping backslashes from the given string |
| * Params: |
| * s = the string to remove escaping backslashes from |
| * Returns: `s` without escaping backslashes in it |
| */ |
| private static char[] removeEscapeBackslashes(char[] s) |
| { |
| if (!s.length) |
| return s; |
| |
| // avoid doing anything if there isn't anything to escape |
| size_t i; |
| for (i = 0; i < s.length-1; ++i) |
| if (s[i] == '\\' && ispunct(s[i+1])) |
| break; |
| if (i == s.length-1) |
| return s; |
| |
| // copy characters backwards, then truncate |
| size_t j = i + 1; |
| s[i] = s[j]; |
| for (++i, ++j; j < s.length; ++i, ++j) |
| { |
| if (j < s.length-1 && s[j] == '\\' && ispunct(s[j+1])) |
| ++j; |
| s[i] = s[j]; |
| } |
| s.length -= (j - i); |
| return s; |
| } |
| |
| /// |
| unittest |
| { |
| assert(removeEscapeBackslashes("".dup) == ""); |
| assert(removeEscapeBackslashes(`\a`.dup) == `\a`); |
| assert(removeEscapeBackslashes(`.\`.dup) == `.\`); |
| assert(removeEscapeBackslashes(`\.\`.dup) == `.\`); |
| assert(removeEscapeBackslashes(`\.`.dup) == `.`); |
| assert(removeEscapeBackslashes(`\.\.`.dup) == `..`); |
| assert(removeEscapeBackslashes(`a\.b\.c`.dup) == `a.b.c`); |
| } |
| |
| /**************************************************** |
| * Percent-encode (AKA URL-encode) the given string |
| * Params: |
| * s = the string to percent-encode |
| * Returns: `s` with special characters percent-encoded |
| */ |
| private static inout(char)[] percentEncode(inout(char)[] s) pure |
| { |
| static bool shouldEncode(char c) |
| { |
| return ((c < '0' && c != '!' && c != '#' && c != '$' && c != '%' && c != '&' && c != '\'' && c != '(' && |
| c != ')' && c != '*' && c != '+' && c != ',' && c != '-' && c != '.' && c != '/') |
| || (c > '9' && c < 'A' && c != ':' && c != ';' && c != '=' && c != '?' && c != '@') |
| || (c > 'Z' && c < 'a' && c != '[' && c != ']' && c != '_') |
| || (c > 'z' && c != '~')); |
| } |
| |
| for (size_t i = 0; i < s.length; ++i) |
| { |
| if (shouldEncode(s[i])) |
| { |
| immutable static hexDigits = "0123456789ABCDEF"; |
| immutable encoded1 = hexDigits[s[i] >> 4]; |
| immutable encoded2 = hexDigits[s[i] & 0x0F]; |
| s = s[0..i] ~ '%' ~ encoded1 ~ encoded2 ~ s[i+1..$]; |
| i += 2; |
| } |
| } |
| return s; |
| } |
| |
| /// |
| unittest |
| { |
| assert(percentEncode("") == ""); |
| assert(percentEncode("aB12-._~/?") == "aB12-._~/?"); |
| assert(percentEncode("<\n>") == "%3C%0A%3E"); |
| } |
| |
| /************************************************** |
| * Skip a single newline at `i` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` to start looking at. |
| * If this function succeeds `i` will point after the newline. |
| * Returns: whether a newline was skipped |
| */ |
| private static bool skipOneNewline(ref OutBuffer buf, ref size_t i) pure |
| { |
| if (i < buf.length && buf[i] == '\r') |
| ++i; |
| if (i < buf.length && buf[i] == '\n') |
| { |
| ++i; |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /************************************************** |
| * A set of Markdown link references. |
| */ |
| private struct MarkdownLinkReferences |
| { |
| MarkdownLink[string] references; // link references keyed by normalized label |
| MarkdownLink[string] symbols; // link symbols keyed by name |
| Scope* _scope; // the current scope |
| bool extractedAll; // the index into the buffer of the last-parsed reference |
| |
| /************************************************** |
| * Look up a reference by label, searching through the rest of the buffer if needed. |
| * Symbols in the current scope are searched for if the DDoc doesn't define the reference. |
| * Params: |
| * label = the label to find the reference for |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` to start searching for references at |
| * loc = the current location in the file |
| * Returns: a link. If the `href` member has a value then the reference is valid. |
| */ |
| MarkdownLink lookupReference(string label, ref OutBuffer buf, size_t i, const ref Loc loc) |
| { |
| const lowercaseLabel = label.toLowercase(); |
| if (lowercaseLabel !in references) |
| extractReferences(buf, i, loc); |
| |
| if (lowercaseLabel in references) |
| return references[lowercaseLabel]; |
| |
| return MarkdownLink(); |
| } |
| |
| /** |
| * Look up the link for the D symbol with the given name. |
| * If found, the link is cached in the `symbols` member. |
| * Params: |
| * name = the name of the symbol |
| * Returns: the link for the symbol or a link with a `null` href |
| */ |
| MarkdownLink lookupSymbol(string name) |
| { |
| if (name in symbols) |
| return symbols[name]; |
| |
| const ids = split(name, '.'); |
| |
| MarkdownLink link; |
| auto id = Identifier.lookup(ids[0].ptr, ids[0].length); |
| if (id) |
| { |
| auto loc = Loc(); |
| auto symbol = _scope.search(loc, id, null, IgnoreErrors); |
| for (size_t i = 1; symbol && i < ids.length; ++i) |
| { |
| id = Identifier.lookup(ids[i].ptr, ids[i].length); |
| symbol = id !is null ? symbol.search(loc, id, IgnoreErrors) : null; |
| } |
| if (symbol) |
| link = MarkdownLink(createHref(symbol), null, name, symbol); |
| } |
| |
| symbols[name] = link; |
| return link; |
| } |
| |
| /************************************************** |
| * Remove and store all link references from the document, in the form of |
| * `[label]: href "optional title"` |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` to start looking at |
| * loc = the current location in the file |
| * Returns: whether a reference was extracted |
| */ |
| private void extractReferences(ref OutBuffer buf, size_t i, const ref Loc loc) |
| { |
| static bool isFollowedBySpace(ref OutBuffer buf, size_t i) |
| { |
| return i+1 < buf.length && (buf[i+1] == ' ' || buf[i+1] == '\t'); |
| } |
| |
| if (extractedAll) |
| return; |
| |
| bool leadingBlank = false; |
| int inCode = false; |
| bool newParagraph = true; |
| MarkdownDelimiter[] delimiters; |
| for (; i < buf.length; ++i) |
| { |
| const c = buf[i]; |
| switch (c) |
| { |
| case ' ': |
| case '\t': |
| break; |
| case '\n': |
| if (leadingBlank && !inCode) |
| newParagraph = true; |
| leadingBlank = true; |
| break; |
| case '\\': |
| ++i; |
| break; |
| case '#': |
| if (leadingBlank && !inCode) |
| newParagraph = true; |
| leadingBlank = false; |
| break; |
| case '>': |
| if (leadingBlank && !inCode) |
| newParagraph = true; |
| break; |
| case '+': |
| if (leadingBlank && !inCode && isFollowedBySpace(buf, i)) |
| newParagraph = true; |
| else |
| leadingBlank = false; |
| break; |
| case '0': |
| .. |
| case '9': |
| if (leadingBlank && !inCode) |
| { |
| i = skipChars(buf, i, "0123456789"); |
| if (i < buf.length && |
| (buf[i] == '.' || buf[i] == ')') && |
| isFollowedBySpace(buf, i)) |
| newParagraph = true; |
| else |
| leadingBlank = false; |
| } |
| break; |
| case '*': |
| if (leadingBlank && !inCode) |
| { |
| newParagraph = true; |
| if (!isFollowedBySpace(buf, i)) |
| leadingBlank = false; |
| } |
| break; |
| case '`': |
| case '~': |
| if (leadingBlank && i+2 < buf.length && buf[i+1] == c && buf[i+2] == c) |
| { |
| inCode = inCode == c ? false : c; |
| i = skipChars(buf, i, [c]) - 1; |
| newParagraph = true; |
| } |
| leadingBlank = false; |
| break; |
| case '-': |
| if (leadingBlank && !inCode && isFollowedBySpace(buf, i)) |
| goto case '+'; |
| else |
| goto case '`'; |
| case '[': |
| if (leadingBlank && !inCode && newParagraph) |
| delimiters ~= MarkdownDelimiter(i, 1, 0, false, false, true, c); |
| break; |
| case ']': |
| if (delimiters.length && !inCode && |
| MarkdownLink.replaceReferenceDefinition(buf, i, delimiters, cast(int) delimiters.length - 1, this, loc)) |
| --i; |
| break; |
| default: |
| if (leadingBlank) |
| newParagraph = false; |
| leadingBlank = false; |
| break; |
| } |
| } |
| extractedAll = true; |
| } |
| |
| /** |
| * Split a string by a delimiter, excluding the delimiter. |
| * Params: |
| * s = the string to split |
| * delimiter = the character to split by |
| * Returns: the resulting array of strings |
| */ |
| private static string[] split(string s, char delimiter) pure |
| { |
| string[] result; |
| size_t iStart = 0; |
| foreach (size_t i; 0..s.length) |
| if (s[i] == delimiter) |
| { |
| result ~= s[iStart..i]; |
| iStart = i + 1; |
| } |
| result ~= s[iStart..$]; |
| return result; |
| } |
| |
| /// |
| unittest |
| { |
| assert(split("", ',') == [""]); |
| assert(split("ab", ',') == ["ab"]); |
| assert(split("a,b", ',') == ["a", "b"]); |
| assert(split("a,,b", ',') == ["a", "", "b"]); |
| assert(split(",ab", ',') == ["", "ab"]); |
| assert(split("ab,", ',') == ["ab", ""]); |
| } |
| |
| /** |
| * Create a HREF for the given D symbol. |
| * The HREF is relative to the current location if possible. |
| * Params: |
| * symbol = the symbol to create a HREF for. |
| * Returns: the resulting href |
| */ |
| private string createHref(Dsymbol symbol) |
| { |
| Dsymbol root = symbol; |
| |
| const(char)[] lref; |
| while (symbol && symbol.ident && !symbol.isModule()) |
| { |
| if (lref.length) |
| lref = '.' ~ lref; |
| lref = symbol.ident.toString() ~ lref; |
| symbol = symbol.parent; |
| } |
| |
| const(char)[] path; |
| if (symbol && symbol.ident && symbol.isModule() != _scope._module) |
| { |
| do |
| { |
| root = symbol; |
| |
| // If the module has a file name, we're done |
| if (const m = symbol.isModule()) |
| if (m.docfile) |
| { |
| path = m.docfile.toString(); |
| break; |
| } |
| |
| if (path.length) |
| path = '_' ~ path; |
| path = symbol.ident.toString() ~ path; |
| symbol = symbol.parent; |
| } while (symbol && symbol.ident); |
| |
| if (!symbol && path.length) |
| path ~= "$(DOC_EXTENSION)"; |
| } |
| |
| // Attempt an absolute URL if not in the same package |
| while (root.parent) |
| root = root.parent; |
| Dsymbol scopeRoot = _scope._module; |
| while (scopeRoot.parent) |
| scopeRoot = scopeRoot.parent; |
| if (scopeRoot != root) |
| { |
| path = "$(DOC_ROOT_" ~ root.ident.toString() ~ ')' ~ path; |
| lref = '.' ~ lref; // remote URIs like Phobos and Mir use .prefixes |
| } |
| |
| return cast(string) (path ~ '#' ~ lref); |
| } |
| } |
| |
| private enum TableColumnAlignment |
| { |
| none, |
| left, |
| center, |
| right |
| } |
| |
| /**************************************************** |
| * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |` |
| * where the example text has four columns with the following alignments: |
| * default, left, center, and right. The first and last pipes are optional. If a |
| * delimiter row is found it will be removed from `buf`. |
| * |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iStart = the index within `buf` that the delimiter row starts at |
| * inQuote = whether the table is inside a quote |
| * columnAlignments = alignments to populate for each column |
| * Returns: the index of the end of the parsed delimiter, or `0` if not found |
| */ |
| private size_t parseTableDelimiterRow(ref OutBuffer buf, const size_t iStart, bool inQuote, ref TableColumnAlignment[] columnAlignments) |
| { |
| size_t i = skipChars(buf, iStart, inQuote ? ">| \t" : "| \t"); |
| while (i < buf.length && buf[i] != '\r' && buf[i] != '\n') |
| { |
| const leftColon = buf[i] == ':'; |
| if (leftColon) |
| ++i; |
| |
| if (i >= buf.length || buf[i] != '-') |
| break; |
| i = skipChars(buf, i, "-"); |
| |
| const rightColon = i < buf.length && buf[i] == ':'; |
| i = skipChars(buf, i, ": \t"); |
| |
| if (i >= buf.length || (buf[i] != '|' && buf[i] != '\r' && buf[i] != '\n')) |
| break; |
| i = skipChars(buf, i, "| \t"); |
| |
| columnAlignments ~= (leftColon && rightColon) ? TableColumnAlignment.center : |
| leftColon ? TableColumnAlignment.left : |
| rightColon ? TableColumnAlignment.right : |
| TableColumnAlignment.none; |
| } |
| |
| if (i < buf.length && buf[i] != '\r' && buf[i] != '\n' && buf[i] != ')') |
| { |
| columnAlignments.length = 0; |
| return 0; |
| } |
| |
| if (i < buf.length && buf[i] == '\r') ++i; |
| if (i < buf.length && buf[i] == '\n') ++i; |
| return i; |
| } |
| |
| /**************************************************** |
| * Look for a table delimiter row, and if found parse the previous row as a |
| * table header row. If both exist with a matching number of columns, start a |
| * table. |
| * |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iStart = the index within `buf` that the table header row starts at, inclusive |
| * iEnd = the index within `buf` that the table header row ends at, exclusive |
| * loc = the current location in the file |
| * inQuote = whether the table is inside a quote |
| * inlineDelimiters = delimiters containing columns separators and any inline emphasis |
| * columnAlignments = the parsed alignments for each column |
| * Returns: the number of characters added by starting the table, or `0` if unchanged |
| */ |
| private size_t startTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, bool inQuote, ref MarkdownDelimiter[] inlineDelimiters, out TableColumnAlignment[] columnAlignments) |
| { |
| const iDelimiterRowEnd = parseTableDelimiterRow(buf, iEnd + 1, inQuote, columnAlignments); |
| if (iDelimiterRowEnd) |
| { |
| size_t delta; |
| if (replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, true, delta)) |
| { |
| buf.remove(iEnd + delta, iDelimiterRowEnd - iEnd); |
| buf.insert(iEnd + delta, "$(TBODY "); |
| buf.insert(iStart, "$(TABLE "); |
| return delta + 15; |
| } |
| } |
| |
| columnAlignments.length = 0; |
| return 0; |
| } |
| |
| /**************************************************** |
| * Replace a Markdown table row in the form of table cells delimited by pipes: |
| * `| cell | cell | cell`. The first and last pipes are optional. |
| * |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iStart = the index within `buf` that the table row starts at, inclusive |
| * iEnd = the index within `buf` that the table row ends at, exclusive |
| * loc = the current location in the file |
| * inlineDelimiters = delimiters containing columns separators and any inline emphasis |
| * columnAlignments = alignments for each column |
| * headerRow = if `true` then the number of columns will be enforced to match |
| * `columnAlignments.length` and the row will be surrounded by a |
| * `THEAD` macro |
| * delta = the number of characters added by replacing the row, or `0` if unchanged |
| * Returns: `true` if a table row was found and replaced |
| */ |
| private bool replaceTableRow(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, TableColumnAlignment[] columnAlignments, bool headerRow, out size_t delta) |
| { |
| delta = 0; |
| |
| if (!columnAlignments.length || iStart == iEnd) |
| return false; |
| |
| iStart = skipChars(buf, iStart, " \t"); |
| int cellCount = 0; |
| foreach (delimiter; inlineDelimiters) |
| if (delimiter.type == '|' && !delimiter.leftFlanking) |
| ++cellCount; |
| bool ignoreLast = inlineDelimiters.length > 0 && inlineDelimiters[$-1].type == '|'; |
| if (ignoreLast) |
| { |
| const iLast = skipChars(buf, inlineDelimiters[$-1].iStart + inlineDelimiters[$-1].count, " \t"); |
| ignoreLast = iLast >= iEnd; |
| } |
| if (!ignoreLast) |
| ++cellCount; |
| |
| if (headerRow && cellCount != columnAlignments.length) |
| return false; |
| |
| void replaceTableCell(size_t iCellStart, size_t iCellEnd, int cellIndex, int di) |
| { |
| const eDelta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, di); |
| delta += eDelta; |
| iCellEnd += eDelta; |
| |
| // strip trailing whitespace and delimiter |
| size_t i = iCellEnd - 1; |
| while (i > iCellStart && (buf[i] == '|' || buf[i] == ' ' || buf[i] == '\t')) |
| --i; |
| ++i; |
| buf.remove(i, iCellEnd - i); |
| delta -= iCellEnd - i; |
| iCellEnd = i; |
| |
| buf.insert(iCellEnd, ")"); |
| ++delta; |
| |
| // strip initial whitespace and delimiter |
| i = skipChars(buf, iCellStart, "| \t"); |
| buf.remove(iCellStart, i - iCellStart); |
| delta -= i - iCellStart; |
| |
| switch (columnAlignments[cellIndex]) |
| { |
| case TableColumnAlignment.none: |
| buf.insert(iCellStart, headerRow ? "$(TH " : "$(TD "); |
| delta += 5; |
| break; |
| case TableColumnAlignment.left: |
| buf.insert(iCellStart, "left, "); |
| delta += 6; |
| goto default; |
| case TableColumnAlignment.center: |
| buf.insert(iCellStart, "center, "); |
| delta += 8; |
| goto default; |
| case TableColumnAlignment.right: |
| buf.insert(iCellStart, "right, "); |
| delta += 7; |
| goto default; |
| default: |
| buf.insert(iCellStart, headerRow ? "$(TH_ALIGN " : "$(TD_ALIGN "); |
| delta += 11; |
| break; |
| } |
| } |
| |
| int cellIndex = cellCount - 1; |
| size_t iCellEnd = iEnd; |
| foreach_reverse (di, delimiter; inlineDelimiters) |
| { |
| if (delimiter.type == '|') |
| { |
| if (ignoreLast && di == inlineDelimiters.length-1) |
| { |
| ignoreLast = false; |
| continue; |
| } |
| |
| if (cellIndex >= columnAlignments.length) |
| { |
| // kill any extra cells |
| buf.remove(delimiter.iStart, iEnd + delta - delimiter.iStart); |
| delta -= iEnd + delta - delimiter.iStart; |
| iCellEnd = iEnd + delta; |
| --cellIndex; |
| continue; |
| } |
| |
| replaceTableCell(delimiter.iStart, iCellEnd, cellIndex, cast(int) di); |
| iCellEnd = delimiter.iStart; |
| --cellIndex; |
| } |
| } |
| |
| // if no starting pipe, replace from the start |
| if (cellIndex >= 0) |
| replaceTableCell(iStart, iCellEnd, cellIndex, 0); |
| |
| buf.insert(iEnd + delta, ")"); |
| buf.insert(iStart, "$(TR "); |
| delta += 6; |
| |
| if (headerRow) |
| { |
| buf.insert(iEnd + delta, ")"); |
| buf.insert(iStart, "$(THEAD "); |
| delta += 9; |
| } |
| |
| return true; |
| } |
| |
| /**************************************************** |
| * End a table, if in one. |
| * |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * i = the index within `buf` to end the table at |
| * columnAlignments = alignments for each column; upon return is set to length `0` |
| * Returns: the number of characters added by ending the table, or `0` if unchanged |
| */ |
| private size_t endTable(ref OutBuffer buf, size_t i, ref TableColumnAlignment[] columnAlignments) |
| { |
| if (!columnAlignments.length) |
| return 0; |
| |
| buf.insert(i, "))"); |
| columnAlignments.length = 0; |
| return 2; |
| } |
| |
| /**************************************************** |
| * End a table row and then the table itself. |
| * |
| * Params: |
| * buf = an OutBuffer containing the DDoc |
| * iStart = the index within `buf` that the table row starts at, inclusive |
| * iEnd = the index within `buf` that the table row ends at, exclusive |
| * loc = the current location in the file |
| * inlineDelimiters = delimiters containing columns separators and any inline emphasis |
| * columnAlignments = alignments for each column; upon return is set to length `0` |
| * Returns: the number of characters added by replacing the row, or `0` if unchanged |
| */ |
| private size_t endRowAndTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, ref TableColumnAlignment[] columnAlignments) |
| { |
| size_t delta; |
| replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, false, delta); |
| delta += endTable(buf, iEnd + delta, columnAlignments); |
| return delta; |
| } |
| |
| /************************************************** |
| * Highlight text section. |
| * |
| * Params: |
| * scope = the current parse scope |
| * a = an array of D symbols at the current scope |
| * loc = source location of start of text. It is a mutable copy to allow incrementing its linenum, for printing the correct line number when an error is encountered in a multiline block of ddoc. |
| * buf = an OutBuffer containing the DDoc |
| * offset = the index within buf to start highlighting |
| */ |
| private void highlightText(Scope* sc, Dsymbols* a, Loc loc, ref OutBuffer buf, size_t offset) |
| { |
| const incrementLoc = loc.linnum == 0 ? 1 : 0; |
| loc.linnum += incrementLoc; |
| loc.charnum = 0; |
| //printf("highlightText()\n"); |
| bool leadingBlank = true; |
| size_t iParagraphStart = offset; |
| size_t iPrecedingBlankLine = 0; |
| int headingLevel = 0; |
| int headingMacroLevel = 0; |
| int quoteLevel = 0; |
| bool lineQuoted = false; |
| int quoteMacroLevel = 0; |
| MarkdownList[] nestedLists; |
| MarkdownDelimiter[] inlineDelimiters; |
| MarkdownLinkReferences linkReferences; |
| TableColumnAlignment[] columnAlignments; |
| bool tableRowDetected = false; |
| int inCode = 0; |
| int inBacktick = 0; |
| int macroLevel = 0; |
| int previousMacroLevel = 0; |
| int parenLevel = 0; |
| size_t iCodeStart = 0; // start of code section |
| size_t codeFenceLength = 0; |
| size_t codeIndent = 0; |
| string codeLanguage; |
| size_t iLineStart = offset; |
| linkReferences._scope = sc; |
| for (size_t i = offset; i < buf.length; i++) |
| { |
| char c = buf[i]; |
| Lcont: |
| switch (c) |
| { |
| case ' ': |
| case '\t': |
| break; |
| case '\n': |
| if (inBacktick) |
| { |
| // `inline code` is only valid if contained on a single line |
| // otherwise, the backticks should be output literally. |
| // |
| // This lets things like `output from the linker' display |
| // unmolested while keeping the feature consistent with GitHub. |
| inBacktick = false; |
| inCode = false; // the backtick also assumes we're in code |
| // Nothing else is necessary since the DDOC_BACKQUOTED macro is |
| // inserted lazily at the close quote, meaning the rest of the |
| // text is already OK. |
| } |
| if (headingLevel) |
| { |
| i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); |
| endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| ++i; |
| iParagraphStart = skipChars(buf, i, " \t\r\n"); |
| } |
| |
| if (tableRowDetected && !columnAlignments.length) |
| i += startTable(buf, iLineStart, i, loc, lineQuoted, inlineDelimiters, columnAlignments); |
| else if (columnAlignments.length) |
| { |
| size_t delta; |
| if (replaceTableRow(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments, false, delta)) |
| i += delta; |
| else |
| i += endTable(buf, i, columnAlignments); |
| } |
| |
| if (!inCode && nestedLists.length && !quoteLevel) |
| MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists); |
| |
| iPrecedingBlankLine = 0; |
| if (!inCode && i == iLineStart && i + 1 < buf.length) // if "\n\n" |
| { |
| i += endTable(buf, i, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel); |
| i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); |
| |
| // if we don't already know about this paragraph break then |
| // insert a blank line and record the paragraph break |
| if (iParagraphStart <= i) |
| { |
| iPrecedingBlankLine = i; |
| i = buf.insert(i, "$(DDOC_BLANKLINE)"); |
| iParagraphStart = i + 1; |
| } |
| } |
| else if (inCode && |
| i == iLineStart && |
| i + 1 < buf.length && |
| !lineQuoted && |
| quoteLevel) // if "\n\n" in quoted code |
| { |
| inCode = false; |
| i = buf.insert(i, ")"); |
| i += endAllMarkdownQuotes(buf, i, quoteLevel); |
| quoteMacroLevel = 0; |
| } |
| leadingBlank = true; |
| lineQuoted = false; |
| tableRowDetected = false; |
| iLineStart = i + 1; |
| loc.linnum += incrementLoc; |
| |
| // update the paragraph start if we just entered a macro |
| if (previousMacroLevel < macroLevel && iParagraphStart < iLineStart) |
| iParagraphStart = iLineStart; |
| previousMacroLevel = macroLevel; |
| break; |
| |
| case '<': |
| { |
| leadingBlank = false; |
| if (inCode) |
| break; |
| const slice = buf[]; |
| auto p = &slice[i]; |
| const se = sc._module.escapetable.escapeChar('<'); |
| if (se == "<") |
| { |
| // Generating HTML |
| // Skip over comments |
| if (p[1] == '!' && p[2] == '-' && p[3] == '-') |
| { |
| size_t j = i + 4; |
| p += 4; |
| while (1) |
| { |
| if (j == slice.length) |
| goto L1; |
| if (p[0] == '-' && p[1] == '-' && p[2] == '>') |
| { |
| i = j + 2; // place on closing '>' |
| break; |
| } |
| j++; |
| p++; |
| } |
| break; |
| } |
| // Skip over HTML tag |
| if (isalpha(p[1]) || (p[1] == '/' && isalpha(p[2]))) |
| { |
| size_t j = i + 2; |
| p += 2; |
| while (1) |
| { |
| if (j == slice.length) |
| break; |
| if (p[0] == '>') |
| { |
| i = j; // place on closing '>' |
| break; |
| } |
| j++; |
| p++; |
| } |
| break; |
| } |
| } |
| L1: |
| // Replace '<' with '<' character entity |
| if (se.length) |
| { |
| buf.remove(i, 1); |
| i = buf.insert(i, se); |
| i--; // point to ';' |
| } |
| break; |
| } |
| |
| case '>': |
| { |
| if (leadingBlank && (!inCode || quoteLevel)) |
| { |
| lineQuoted = true; |
| int lineQuoteLevel = 1; |
| size_t iAfterDelimiters = i + 1; |
| for (; iAfterDelimiters < buf.length; ++iAfterDelimiters) |
| { |
| const c0 = buf[iAfterDelimiters]; |
| if (c0 == '>') |
| ++lineQuoteLevel; |
| else if (c0 != ' ' && c0 != '\t') |
| break; |
| } |
| if (!quoteMacroLevel) |
| quoteMacroLevel = macroLevel; |
| buf.remove(i, iAfterDelimiters - i); |
| |
| if (quoteLevel < lineQuoteLevel) |
| { |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (nestedLists.length) |
| { |
| const indent = getMarkdownIndent(buf, iLineStart, i); |
| if (indent < nestedLists[$-1].contentIndent) |
| i += MarkdownList.endAllNestedLists(buf, i, nestedLists); |
| } |
| |
| for (; quoteLevel < lineQuoteLevel; ++quoteLevel) |
| { |
| i = buf.insert(i, "$(BLOCKQUOTE\n"); |
| iLineStart = iParagraphStart = i; |
| } |
| --i; |
| } |
| else |
| { |
| --i; |
| if (nestedLists.length) |
| MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists); |
| } |
| break; |
| } |
| |
| leadingBlank = false; |
| if (inCode) |
| break; |
| // Replace '>' with '>' character entity |
| const se = sc._module.escapetable.escapeChar('>'); |
| if (se.length) |
| { |
| buf.remove(i, 1); |
| i = buf.insert(i, se); |
| i--; // point to ';' |
| } |
| break; |
| } |
| |
| case '&': |
| { |
| leadingBlank = false; |
| if (inCode) |
| break; |
| char* p = cast(char*)&buf[].ptr[i]; |
| if (p[1] == '#' || isalpha(p[1])) |
| break; |
| // already a character entity |
| // Replace '&' with '&' character entity |
| const se = sc._module.escapetable.escapeChar('&'); |
| if (se) |
| { |
| buf.remove(i, 1); |
| i = buf.insert(i, se); |
| i--; // point to ';' |
| } |
| break; |
| } |
| |
| case '`': |
| { |
| const iAfterDelimiter = skipChars(buf, i, "`"); |
| const count = iAfterDelimiter - i; |
| |
| if (inBacktick == count) |
| { |
| inBacktick = 0; |
| inCode = 0; |
| OutBuffer codebuf; |
| codebuf.write(buf[iCodeStart + count .. i]); |
| // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL |
| highlightCode(sc, a, codebuf, 0); |
| escapeStrayParenthesis(loc, &codebuf, 0, false); |
| buf.remove(iCodeStart, i - iCodeStart + count); // also trimming off the current ` |
| immutable pre = "$(DDOC_BACKQUOTED "; |
| i = buf.insert(iCodeStart, pre); |
| i = buf.insert(i, codebuf[]); |
| i = buf.insert(i, ")"); |
| i--; // point to the ending ) so when the for loop does i++, it will see the next character |
| break; |
| } |
| |
| // Perhaps we're starting or ending a Markdown code block |
| if (leadingBlank && count >= 3) |
| { |
| bool moreBackticks = false; |
| for (size_t j = iAfterDelimiter; !moreBackticks && j < buf.length; ++j) |
| if (buf[j] == '`') |
| moreBackticks = true; |
| else if (buf[j] == '\r' || buf[j] == '\n') |
| break; |
| if (!moreBackticks) |
| goto case '-'; |
| } |
| |
| if (inCode) |
| { |
| if (inBacktick) |
| i = iAfterDelimiter - 1; |
| break; |
| } |
| inCode = c; |
| inBacktick = cast(int) count; |
| codeIndent = 0; // inline code is not indented |
| // All we do here is set the code flags and record |
| // the location. The macro will be inserted lazily |
| // so we can easily cancel the inBacktick if we come |
| // across a newline character. |
| iCodeStart = i; |
| i = iAfterDelimiter - 1; |
| break; |
| } |
| |
| case '#': |
| { |
| /* A line beginning with # indicates an ATX-style heading. */ |
| if (leadingBlank && !inCode) |
| { |
| leadingBlank = false; |
| |
| headingLevel = detectAtxHeadingLevel(buf, i); |
| if (!headingLevel) |
| break; |
| |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); |
| |
| // remove the ### prefix, including whitespace |
| i = skipChars(buf, i + headingLevel, " \t"); |
| buf.remove(iLineStart, i - iLineStart); |
| i = iParagraphStart = iLineStart; |
| |
| removeAnyAtxHeadingSuffix(buf, i); |
| --i; |
| |
| headingMacroLevel = macroLevel; |
| } |
| break; |
| } |
| |
| case '~': |
| { |
| if (leadingBlank) |
| { |
| // Perhaps we're starting or ending a Markdown code block |
| const iAfterDelimiter = skipChars(buf, i, "~"); |
| if (iAfterDelimiter - i >= 3) |
| goto case '-'; |
| } |
| leadingBlank = false; |
| break; |
| } |
| |
| case '-': |
| /* A line beginning with --- delimits a code section. |
| * inCode tells us if it is start or end of a code section. |
| */ |
| if (leadingBlank) |
| { |
| if (!inCode && c == '-') |
| { |
| const list = MarkdownList.parseItem(buf, iLineStart, i); |
| if (list.isValid) |
| { |
| if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) |
| { |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| iParagraphStart = skipChars(buf, i+1, " \t\r\n"); |
| break; |
| } |
| else |
| goto case '+'; |
| } |
| } |
| |
| size_t istart = i; |
| size_t eollen = 0; |
| leadingBlank = false; |
| const c0 = c; // if we jumped here from case '`' or case '~' |
| size_t iInfoString = 0; |
| if (!inCode) |
| codeLanguage.length = 0; |
| while (1) |
| { |
| ++i; |
| if (i >= buf.length) |
| break; |
| c = buf[i]; |
| if (c == '\n') |
| { |
| eollen = 1; |
| break; |
| } |
| if (c == '\r') |
| { |
| eollen = 1; |
| if (i + 1 >= buf.length) |
| break; |
| if (buf[i + 1] == '\n') |
| { |
| eollen = 2; |
| break; |
| } |
| } |
| // BUG: handle UTF PS and LS too |
| if (c != c0 || iInfoString) |
| { |
| if (!iInfoString && !inCode && i - istart >= 3) |
| { |
| // Start a Markdown info string, like ```ruby |
| codeFenceLength = i - istart; |
| i = iInfoString = skipChars(buf, i, " \t"); |
| } |
| else if (iInfoString && c != '`') |
| { |
| if (!codeLanguage.length && (c == ' ' || c == '\t')) |
| codeLanguage = cast(string) buf[iInfoString..i].idup; |
| } |
| else |
| { |
| iInfoString = 0; |
| goto Lcont; |
| } |
| } |
| } |
| if (i - istart < 3 || (inCode && (inCode != c0 || (inCode != '-' && i - istart < codeFenceLength)))) |
| goto Lcont; |
| if (iInfoString) |
| { |
| if (!codeLanguage.length) |
| codeLanguage = cast(string) buf[iInfoString..i].idup; |
| } |
| else |
| codeFenceLength = i - istart; |
| |
| // We have the start/end of a code section |
| // Remove the entire --- line, including blanks and \n |
| buf.remove(iLineStart, i - iLineStart + eollen); |
| i = iLineStart; |
| if (eollen) |
| leadingBlank = true; |
| if (inCode && (i <= iCodeStart)) |
| { |
| // Empty code section, just remove it completely. |
| inCode = 0; |
| break; |
| } |
| if (inCode) |
| { |
| inCode = 0; |
| // The code section is from iCodeStart to i |
| OutBuffer codebuf; |
| codebuf.write(buf[iCodeStart .. i]); |
| codebuf.writeByte(0); |
| // Remove leading indentations from all lines |
| bool lineStart = true; |
| char* endp = cast(char*)codebuf[].ptr + codebuf.length; |
| for (char* p = cast(char*)codebuf[].ptr; p < endp;) |
| { |
| if (lineStart) |
| { |
| size_t j = codeIndent; |
| char* q = p; |
| while (j-- > 0 && q < endp && isIndentWS(q)) |
| ++q; |
| codebuf.remove(p - cast(char*)codebuf[].ptr, q - p); |
| assert(cast(char*)codebuf[].ptr <= p); |
| assert(p < cast(char*)codebuf[].ptr + codebuf.length); |
| lineStart = false; |
| endp = cast(char*)codebuf[].ptr + codebuf.length; // update |
| continue; |
| } |
| if (*p == '\n') |
| lineStart = true; |
| ++p; |
| } |
| if (!codeLanguage.length || codeLanguage == "dlang" || codeLanguage == "d") |
| highlightCode2(sc, a, codebuf, 0); |
| else |
| codebuf.remove(codebuf.length-1, 1); // remove the trailing 0 byte |
| escapeStrayParenthesis(loc, &codebuf, 0, false); |
| buf.remove(iCodeStart, i - iCodeStart); |
| i = buf.insert(iCodeStart, codebuf[]); |
| i = buf.insert(i, ")\n"); |
| i -= 2; // in next loop, c should be '\n' |
| } |
| else |
| { |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| { |
| const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); |
| i += delta; |
| istart += delta; |
| } |
| |
| inCode = c0; |
| codeIndent = istart - iLineStart; // save indent count |
| if (codeLanguage.length && codeLanguage != "dlang" && codeLanguage != "d") |
| { |
| // backslash-escape |
| for (size_t j; j < codeLanguage.length - 1; ++j) |
| if (codeLanguage[j] == '\\' && ispunct(codeLanguage[j + 1])) |
| codeLanguage = codeLanguage[0..j] ~ codeLanguage[j + 1..$]; |
| |
| i = buf.insert(i, "$(OTHER_CODE "); |
| i = buf.insert(i, codeLanguage); |
| i = buf.insert(i, ","); |
| } |
| else |
| i = buf.insert(i, "$(D_CODE "); |
| iCodeStart = i; |
| i--; // place i on > |
| leadingBlank = true; |
| } |
| } |
| break; |
| |
| case '_': |
| { |
| if (leadingBlank && !inCode && replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) |
| { |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| iParagraphStart = skipChars(buf, i+1, " \t\r\n"); |
| break; |
| } |
| goto default; |
| } |
| |
| case '+': |
| case '0': |
| .. |
| case '9': |
| { |
| if (leadingBlank && !inCode) |
| { |
| MarkdownList list = MarkdownList.parseItem(buf, iLineStart, i); |
| if (list.isValid) |
| { |
| // Avoid starting a numbered list in the middle of a paragraph |
| if (!nestedLists.length && list.orderedStart.length && |
| iParagraphStart < iLineStart) |
| { |
| i += list.orderedStart.length - 1; |
| break; |
| } |
| |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| { |
| const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); |
| i += delta; |
| list.iStart += delta; |
| list.iContentStart += delta; |
| } |
| |
| list.macroLevel = macroLevel; |
| list.startItem(buf, iLineStart, i, iPrecedingBlankLine, nestedLists, loc); |
| break; |
| } |
| } |
| leadingBlank = false; |
| break; |
| } |
| |
| case '*': |
| { |
| if (inCode || inBacktick) |
| { |
| leadingBlank = false; |
| break; |
| } |
| |
| if (leadingBlank) |
| { |
| // Check for a thematic break |
| if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) |
| { |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| if (!lineQuoted && quoteLevel) |
| i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| iParagraphStart = skipChars(buf, i+1, " \t\r\n"); |
| break; |
| } |
| |
| // An initial * indicates a Markdown list item |
| const list = MarkdownList.parseItem(buf, iLineStart, i); |
| if (list.isValid) |
| goto case '+'; |
| } |
| |
| // Markdown emphasis |
| const leftC = i > offset ? buf[i-1] : '\0'; |
| size_t iAfterEmphasis = skipChars(buf, i+1, "*"); |
| const rightC = iAfterEmphasis < buf.length ? buf[iAfterEmphasis] : '\0'; |
| int count = cast(int) (iAfterEmphasis - i); |
| const leftFlanking = (rightC != '\0' && !isspace(rightC)) && (!ispunct(rightC) || leftC == '\0' || isspace(leftC) || ispunct(leftC)); |
| const rightFlanking = (leftC != '\0' && !isspace(leftC)) && (!ispunct(leftC) || rightC == '\0' || isspace(rightC) || ispunct(rightC)); |
| auto emphasis = MarkdownDelimiter(i, count, macroLevel, leftFlanking, rightFlanking, false, c); |
| |
| if (!emphasis.leftFlanking && !emphasis.rightFlanking) |
| { |
| i = iAfterEmphasis - 1; |
| break; |
| } |
| |
| inlineDelimiters ~= emphasis; |
| i += emphasis.count; |
| --i; |
| break; |
| } |
| |
| case '!': |
| { |
| leadingBlank = false; |
| |
| if (inCode) |
| break; |
| |
| if (i < buf.length-1 && buf[i+1] == '[') |
| { |
| const imageStart = MarkdownDelimiter(i, 2, macroLevel, false, false, false, c); |
| inlineDelimiters ~= imageStart; |
| ++i; |
| } |
| break; |
| } |
| case '[': |
| { |
| if (inCode) |
| { |
| leadingBlank = false; |
| break; |
| } |
| |
| const leftC = i > offset ? buf[i-1] : '\0'; |
| const rightFlanking = leftC != '\0' && !isspace(leftC) && !ispunct(leftC); |
| const atParagraphStart = leadingBlank && iParagraphStart >= iLineStart; |
| const linkStart = MarkdownDelimiter(i, 1, macroLevel, false, rightFlanking, atParagraphStart, c); |
| inlineDelimiters ~= linkStart; |
| leadingBlank = false; |
| break; |
| } |
| case ']': |
| { |
| leadingBlank = false; |
| |
| if (inCode) |
| break; |
| |
| for (int d = cast(int) inlineDelimiters.length - 1; d >= 0; --d) |
| { |
| const delimiter = inlineDelimiters[d]; |
| if (delimiter.type == '[' || delimiter.type == '!') |
| { |
| if (delimiter.isValid && |
| MarkdownLink.replaceLink(buf, i, loc, inlineDelimiters, d, linkReferences)) |
| { |
| // if we removed a reference link then we're at line start |
| if (i <= delimiter.iStart) |
| leadingBlank = true; |
| |
| // don't nest links |
| if (delimiter.type == '[') |
| for (--d; d >= 0; --d) |
| if (inlineDelimiters[d].type == '[') |
| inlineDelimiters[d].invalidate(); |
| } |
| else |
| { |
| // nothing found, so kill the delimiter |
| inlineDelimiters = inlineDelimiters[0..d] ~ inlineDelimiters[d+1..$]; |
| } |
| break; |
| } |
| } |
| break; |
| } |
| |
| case '|': |
| { |
| if (inCode) |
| { |
| leadingBlank = false; |
| break; |
| } |
| |
| tableRowDetected = true; |
| inlineDelimiters ~= MarkdownDelimiter(i, 1, macroLevel, leadingBlank, false, false, c); |
| leadingBlank = false; |
| break; |
| } |
| |
| case '\\': |
| { |
| leadingBlank = false; |
| if (inCode || i+1 >= buf.length) |
| break; |
| |
| /* Escape Markdown special characters */ |
| char c1 = buf[i+1]; |
| if (ispunct(c1)) |
| { |
| buf.remove(i, 1); |
| |
| auto se = sc._module.escapetable.escapeChar(c1); |
| if (!se) |
| se = c1 == '$' ? "$(DOLLAR)" : c1 == ',' ? "$(COMMA)" : null; |
| if (se) |
| { |
| buf.remove(i, 1); |
| i = buf.insert(i, se); |
| i--; // point to escaped char |
| } |
| } |
| break; |
| } |
| |
| case '$': |
| { |
| /* Look for the start of a macro, '$(Identifier' |
| */ |
| leadingBlank = false; |
| if (inCode || inBacktick) |
| break; |
| const slice = buf[]; |
| auto p = &slice[i]; |
| if (p[1] == '(' && isIdStart(&p[2])) |
| ++macroLevel; |
| break; |
| } |
| |
| case '(': |
| { |
| if (!inCode && i > offset && buf[i-1] != '$') |
| ++parenLevel; |
| break; |
| } |
| |
| case ')': |
| { /* End of macro |
| */ |
| leadingBlank = false; |
| if (inCode || inBacktick) |
| break; |
| if (parenLevel > 0) |
| --parenLevel; |
| else if (macroLevel) |
| { |
| int downToLevel = cast(int) inlineDelimiters.length; |
| while (downToLevel > 0 && inlineDelimiters[downToLevel - 1].macroLevel >= macroLevel) |
| --downToLevel; |
| if (headingLevel && headingMacroLevel >= macroLevel) |
| { |
| endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| } |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| while (nestedLists.length && nestedLists[$-1].macroLevel >= macroLevel) |
| { |
| i = buf.insert(i, ")\n)"); |
| --nestedLists.length; |
| } |
| if (quoteLevel && quoteMacroLevel >= macroLevel) |
| i += endAllMarkdownQuotes(buf, i, quoteLevel); |
| i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters, downToLevel); |
| |
| --macroLevel; |
| quoteMacroLevel = 0; |
| } |
| break; |
| } |
| |
| default: |
| leadingBlank = false; |
| if (sc._module.filetype == FileType.ddoc || inCode) |
| break; |
| const start = cast(char*)buf[].ptr + i; |
| if (isIdStart(start)) |
| { |
| size_t j = skippastident(buf, i); |
| if (i < j) |
| { |
| size_t k = skippastURL(buf, i); |
| if (i < k) |
| { |
| /* The URL is buf[i..k] |
| */ |
| if (macroLevel) |
| /* Leave alone if already in a macro |
| */ |
| i = k - 1; |
| else |
| { |
| /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)' |
| */ |
| i = buf.bracket(i, "$(DDOC_LINK_AUTODETECT ", k, ")") - 1; |
| } |
| break; |
| } |
| } |
| else |
| break; |
| size_t len = j - i; |
| // leading '_' means no highlight unless it's a reserved symbol name |
| if (c == '_' && (i == 0 || !isdigit(*(start - 1))) && (i == buf.length - 1 || !isReservedName(start[0 .. len]))) |
| { |
| buf.remove(i, 1); |
| i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j - 1, ")") - 1; |
| break; |
| } |
| if (isIdentifier(a, start[0 .. len])) |
| { |
| i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL ", j, ")") - 1; |
| break; |
| } |
| if (isKeyword(start[0 .. len])) |
| { |
| i = buf.bracket(i, "$(DDOC_AUTO_KEYWORD ", j, ")") - 1; |
| break; |
| } |
| if (isFunctionParameter(a, start[0 .. len])) |
| { |
| //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); |
| i = buf.bracket(i, "$(DDOC_AUTO_PARAM ", j, ")") - 1; |
| break; |
| } |
| i = j - 1; |
| } |
| break; |
| } |
| } |
| |
| if (inCode == '-') |
| error(loc, "unmatched `---` in DDoc comment"); |
| else if (inCode) |
| buf.insert(buf.length, ")"); |
| |
| size_t i = buf.length; |
| if (headingLevel) |
| { |
| endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); |
| removeBlankLineMacro(buf, iPrecedingBlankLine, i); |
| } |
| i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); |
| i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); |
| endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel); |
| } |
| |
| /************************************************** |
| * Highlight code for DDOC section. |
| */ |
| private void highlightCode(Scope* sc, Dsymbol s, ref OutBuffer buf, size_t offset) |
| { |
| auto imp = s.isImport(); |
| if (imp && imp.aliases.length > 0) |
| { |
| // For example: `public import core.stdc.string : memcpy, memcmp;` |
| for(int i = 0; i < imp.aliases.length; i++) |
| { |
| // Need to distinguish between |
| // `public import core.stdc.string : memcpy, memcmp;` and |
| // `public import core.stdc.string : copy = memcpy, compare = memcmp;` |
| auto a = imp.aliases[i]; |
| auto id = a ? a : imp.names[i]; |
| auto loc = Loc.init; |
| if (auto symFromId = sc.search(loc, id, null)) |
| { |
| highlightCode(sc, symFromId, buf, offset); |
| } |
| } |
| } |
| else |
| { |
| OutBuffer ancbuf; |
| emitAnchor(ancbuf, s, sc); |
| buf.insert(offset, ancbuf[]); |
| offset += ancbuf.length; |
| |
| Dsymbols a; |
| a.push(s); |
| highlightCode(sc, &a, buf, offset); |
| } |
| } |
| |
| /**************************************************** |
| */ |
| private void highlightCode(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset) |
| { |
| //printf("highlightCode(a = '%s')\n", a.toChars()); |
| bool resolvedTemplateParameters = false; |
| |
| for (size_t i = offset; i < buf.length; i++) |
| { |
| char c = buf[i]; |
| const se = sc._module.escapetable.escapeChar(c); |
| if (se.length) |
| { |
| buf.remove(i, 1); |
| i = buf.insert(i, se); |
| i--; // point to ';' |
| continue; |
| } |
| char* start = cast(char*)buf[].ptr + i; |
| if (isIdStart(start)) |
| { |
| size_t j = skipPastIdentWithDots(buf, i); |
| if (i < j) |
| { |
| size_t len = j - i; |
| if (isIdentifier(a, start[0 .. len])) |
| { |
| i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1; |
| continue; |
| } |
| } |
| |
| j = skippastident(buf, i); |
| if (i < j) |
| { |
| size_t len = j - i; |
| if (isIdentifier(a, start[0 .. len])) |
| { |
| i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1; |
| continue; |
| } |
| if (isFunctionParameter(a, start[0 .. len])) |
| { |
| //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); |
| i = buf.bracket(i, "$(DDOC_PARAM ", j, ")") - 1; |
| continue; |
| } |
| i = j - 1; |
| } |
| } |
| else if (!resolvedTemplateParameters) |
| { |
| size_t previ = i; |
| |
| // hunt for template declarations: |
| foreach (symi; 0 .. a.length) |
| { |
| FuncDeclaration fd = (*a)[symi].isFuncDeclaration(); |
| |
| if (!fd || !fd.parent || !fd.parent.isTemplateDeclaration()) |
| { |
| continue; |
| } |
| |
| TemplateDeclaration td = fd.parent.isTemplateDeclaration(); |
| |
| // build the template parameters |
| Array!(size_t) paramLens; |
| paramLens.reserve(td.parameters.length); |
| |
| OutBuffer parametersBuf; |
| HdrGenState hgs; |
| |
| parametersBuf.writeByte('('); |
| |
| foreach (parami; 0 .. td.parameters.length) |
| { |
| TemplateParameter tp = (*td.parameters)[parami]; |
| |
| if (parami) |
| parametersBuf.writestring(", "); |
| |
| size_t lastOffset = parametersBuf.length; |
| |
| .toCBuffer(tp, ¶metersBuf, &hgs); |
| |
| paramLens[parami] = parametersBuf.length - lastOffset; |
| } |
| parametersBuf.writeByte(')'); |
| |
| const templateParams = parametersBuf[]; |
| |
| //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start); |
| if (start[0 .. templateParams.length] == templateParams) |
| { |
| immutable templateParamListMacro = "$(DDOC_TEMPLATE_PARAM_LIST "; |
| buf.bracket(i, templateParamListMacro.ptr, i + templateParams.length, ")"); |
| |
| // We have the parameter list. While we're here we might |
| // as well wrap the parameters themselves as well |
| |
| // + 1 here to take into account the opening paren of the |
| // template param list |
| i += templateParamListMacro.length + 1; |
| |
| foreach (const len; paramLens) |
| { |
| i = buf.bracket(i, "$(DDOC_TEMPLATE_PARAM ", i + len, ")"); |
| // increment two here for space + comma |
| i += 2; |
| } |
| |
| resolvedTemplateParameters = true; |
| // reset i to be positioned back before we found the template |
| // param list this assures that anything within the template |
| // param list that needs to be escaped or otherwise altered |
| // has an opportunity for that to happen outside of this context |
| i = previ; |
| |
| continue; |
| } |
| } |
| } |
| } |
| } |
| |
| /**************************************** |
| */ |
| private void highlightCode3(Scope* sc, ref OutBuffer buf, const(char)* p, const(char)* pend) |
| { |
| for (; p < pend; p++) |
| { |
| const se = sc._module.escapetable.escapeChar(*p); |
| if (se.length) |
| buf.writestring(se); |
| else |
| buf.writeByte(*p); |
| } |
| } |
| |
| /************************************************** |
| * Highlight code for CODE section. |
| */ |
| private void highlightCode2(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset) |
| { |
| uint errorsave = global.startGagging(); |
| |
| scope Lexer lex = new Lexer(null, cast(char*)buf[].ptr, 0, buf.length - 1, 0, 1, |
| global.errorSink, |
| global.vendor, global.versionNumber()); |
| OutBuffer res; |
| const(char)* lastp = cast(char*)buf[].ptr; |
| //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr); |
| res.reserve(buf.length); |
| while (1) |
| { |
| Token tok; |
| lex.scan(&tok); |
| highlightCode3(sc, res, lastp, tok.ptr); |
| string highlight = null; |
| switch (tok.value) |
| { |
| case TOK.identifier: |
| { |
| if (!sc) |
| break; |
| size_t len = lex.p - tok.ptr; |
| if (isIdentifier(a, tok.ptr[0 .. len])) |
| { |
| highlight = "$(D_PSYMBOL "; |
| break; |
| } |
| if (isFunctionParameter(a, tok.ptr[0 .. len])) |
| { |
| //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); |
| highlight = "$(D_PARAM "; |
| break; |
| } |
| break; |
| } |
| case TOK.comment: |
| highlight = "$(D_COMMENT "; |
| break; |
| case TOK.string_: |
| highlight = "$(D_STRING "; |
| break; |
| default: |
| if (tok.isKeyword()) |
| highlight = "$(D_KEYWORD "; |
| break; |
| } |
| if (highlight) |
| { |
| res.writestring(highlight); |
| size_t o = res.length; |
| highlightCode3(sc, res, tok.ptr, lex.p); |
| if (tok.value == TOK.comment || tok.value == TOK.string_) |
| /* https://issues.dlang.org/show_bug.cgi?id=7656 |
| * https://issues.dlang.org/show_bug.cgi?id=7715 |
| * https://issues.dlang.org/show_bug.cgi?id=10519 |
| */ |
| escapeDdocString(&res, o); |
| res.writeByte(')'); |
| } |
| else |
| highlightCode3(sc, res, tok.ptr, lex.p); |
| if (tok.value == TOK.endOfFile) |
| break; |
| lastp = lex.p; |
| } |
| buf.setsize(offset); |
| buf.write(&res); |
| global.endGagging(errorsave); |
| } |
| |
| /**************************************** |
| * Determine if p points to the start of a "..." parameter identifier. |
| */ |
| private bool isCVariadicArg(const(char)[] p) @nogc nothrow pure @safe |
| { |
| return p.length >= 3 && p[0 .. 3] == "..."; |
| } |
| |
| /**************************************** |
| * Determine if p points to the start of an identifier. |
| */ |
| bool isIdStart(const(char)* p) @nogc nothrow pure |
| { |
| dchar c = *p; |
| if (isalpha(c) || c == '_') |
| return true; |
| if (c >= 0x80) |
| { |
| size_t i = 0; |
| if (utf_decodeChar(p[0 .. 4], i, c)) |
| return false; // ignore errors |
| if (isUniAlpha(c)) |
| return true; |
| } |
| return false; |
| } |
| |
| /**************************************** |
| * Determine if p points to the rest of an identifier. |
| */ |
| bool isIdTail(const(char)* p) @nogc nothrow pure |
| { |
| dchar c = *p; |
| if (isalnum(c) || c == '_') |
| return true; |
| if (c >= 0x80) |
| { |
| size_t i = 0; |
| if (utf_decodeChar(p[0 .. 4], i, c)) |
| return false; // ignore errors |
| if (isUniAlpha(c)) |
| return true; |
| } |
| return false; |
| } |
| |
| /**************************************** |
| * Determine if p points to the indentation space. |
| */ |
| private bool isIndentWS(const(char)* p) @nogc nothrow pure @safe |
| { |
| return (*p == ' ') || (*p == '\t'); |
| } |
| |
| /***************************************** |
| * Return number of bytes in UTF character. |
| */ |
| int utfStride(const(char)* p) @nogc nothrow pure |
| { |
| dchar c = *p; |
| if (c < 0x80) |
| return 1; |
| size_t i = 0; |
| utf_decodeChar(p[0 .. 4], i, c); // ignore errors, but still consume input |
| return cast(int)i; |
| } |
| |
| private inout(char)* stripLeadingNewlines(inout(char)* s) @nogc nothrow pure |
| { |
| while (s && *s == '\n' || *s == '\r') |
| s++; |
| |
| return s; |
| } |