d: Merge dmd. druntime e770945277, phobos 6d6e0b9b9
[official-gcc.git] / gcc / d / dmd / doc.d
blobbcf358cb5e9dbec3dced2a41861f2f74425471b3
1 /**
2 * Ddoc documentation generation.
4 * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator)
6 * Copyright: Copyright (C) 1999-2024 by The D Language Foundation, All Rights Reserved
7 * Authors: $(LINK2 https://www.digitalmars.com, Walter Bright)
8 * License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
9 * Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/doc.d, _doc.d)
10 * Documentation: https://dlang.org/phobos/dmd_doc.html
11 * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/doc.d
14 module dmd.doc;
16 import core.stdc.ctype;
17 import core.stdc.stdlib;
18 import core.stdc.stdio;
19 import core.stdc.string;
20 import core.stdc.time;
21 import dmd.aggregate;
22 import dmd.arraytypes;
23 import dmd.astenums;
24 import dmd.attrib;
25 import dmd.cond;
26 import dmd.dclass;
27 import dmd.declaration;
28 import dmd.denum;
29 import dmd.dimport;
30 import dmd.dmacro;
31 import dmd.dmodule;
32 import dmd.dscope;
33 import dmd.dstruct;
34 import dmd.dsymbol;
35 import dmd.dsymbolsem;
36 import dmd.dtemplate;
37 import dmd.errorsink;
38 import dmd.func;
39 import dmd.globals;
40 import dmd.hdrgen;
41 import dmd.id;
42 import dmd.identifier;
43 import dmd.lexer;
44 import dmd.location;
45 import dmd.mtype;
46 import dmd.root.array;
47 import dmd.root.file;
48 import dmd.root.filename;
49 import dmd.common.outbuffer;
50 import dmd.root.port;
51 import dmd.root.rmem;
52 import dmd.root.string;
53 import dmd.root.utf;
54 import dmd.tokens;
55 import dmd.visitor;
57 private:
59 public
60 struct Escape
62 const(char)[][char.max] strings;
64 /***************************************
65 * Find character string to replace c with.
67 const(char)[] escapeChar(char c) @safe
69 version (all)
71 //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr);
72 return strings[c];
74 else
76 const(char)[] s;
77 switch (c)
79 case '<':
80 s = "&lt;";
81 break;
82 case '>':
83 s = "&gt;";
84 break;
85 case '&':
86 s = "&amp;";
87 break;
88 default:
89 s = null;
90 break;
92 return s;
97 /***********************************************************
99 class Section
101 const(char)[] name;
102 const(char)[] body_;
103 int nooutput;
105 override string toString() const
107 assert(0);
110 void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
112 assert(a.length);
113 if (name.length)
115 static immutable table =
117 "AUTHORS",
118 "BUGS",
119 "COPYRIGHT",
120 "DATE",
121 "DEPRECATED",
122 "EXAMPLES",
123 "HISTORY",
124 "LICENSE",
125 "RETURNS",
126 "SEE_ALSO",
127 "STANDARDS",
128 "THROWS",
129 "VERSION",
131 foreach (entry; table)
133 if (iequals(entry, name))
135 buf.printf("$(DDOC_%s ", entry.ptr);
136 goto L1;
139 buf.writestring("$(DDOC_SECTION ");
140 // Replace _ characters with spaces
141 buf.writestring("$(DDOC_SECTION_H ");
142 size_t o = buf.length;
143 foreach (char c; name)
144 buf.writeByte((c == '_') ? ' ' : c);
145 escapeStrayParenthesis(loc, buf, o, false, sc.eSink);
146 buf.writestring(")");
148 else
150 buf.writestring("$(DDOC_DESCRIPTION ");
153 size_t o = buf.length;
154 buf.write(body_);
155 escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
156 highlightText(sc, a, loc, buf, o);
157 buf.writestring(")");
161 /***********************************************************
163 final class ParamSection : Section
165 override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
167 assert(a.length);
168 Dsymbol s = (*a)[0]; // test
169 const(char)* p = body_.ptr;
170 size_t len = body_.length;
171 const(char)* pend = p + len;
172 const(char)* tempstart = null;
173 size_t templen = 0;
174 const(char)* namestart = null;
175 size_t namelen = 0; // !=0 if line continuation
176 const(char)* textstart = null;
177 size_t textlen = 0;
178 size_t paramcount = 0;
179 buf.writestring("$(DDOC_PARAMS ");
180 while (p < pend)
182 // Skip to start of macro
183 while (1)
185 switch (*p)
187 case ' ':
188 case '\t':
189 p++;
190 continue;
191 case '\n':
192 p++;
193 goto Lcont;
194 default:
195 if (isIdStart(p) || isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
196 break;
197 if (namelen)
198 goto Ltext;
199 // continuation of prev macro
200 goto Lskipline;
202 break;
204 tempstart = p;
205 while (isIdTail(p))
206 p += utfStride(p);
207 if (isCVariadicArg(p[0 .. cast(size_t)(pend - p)]))
208 p += 3;
209 templen = p - tempstart;
210 while (*p == ' ' || *p == '\t')
211 p++;
212 if (*p != '=')
214 if (namelen)
215 goto Ltext;
216 // continuation of prev macro
217 goto Lskipline;
219 p++;
220 if (namelen)
222 // Output existing param
224 //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
225 ++paramcount;
226 HdrGenState hgs;
227 buf.writestring("$(DDOC_PARAM_ROW ");
229 buf.writestring("$(DDOC_PARAM_ID ");
231 size_t o = buf.length;
232 Parameter fparam = isFunctionParameter(a, namestart[0 .. namelen]);
233 if (!fparam)
235 // Comments on a template might refer to function parameters within.
236 // Search the parameters of nested eponymous functions (with the same name.)
237 fparam = isEponymousFunctionParameter(a, namestart[0 .. namelen]);
239 bool isCVariadic = isCVariadicParameter(a, namestart[0 .. namelen]);
240 if (isCVariadic)
242 buf.writestring("...");
244 else if (fparam && fparam.type && fparam.ident)
246 toCBuffer(fparam.type, buf, fparam.ident, hgs);
248 else
250 if (isTemplateParameter(a, namestart, namelen))
252 // 10236: Don't count template parameters for params check
253 --paramcount;
255 else if (!fparam)
257 sc.eSink.warning(s.loc, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen, namestart);
259 buf.write(namestart[0 .. namelen]);
261 escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
262 highlightCode(sc, a, buf, o);
264 buf.writestring(")");
265 buf.writestring("$(DDOC_PARAM_DESC ");
267 size_t o = buf.length;
268 buf.write(textstart[0 .. textlen]);
269 escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
270 highlightText(sc, a, loc, buf, o);
272 buf.writestring(")");
274 buf.writestring(")");
275 namelen = 0;
276 if (p >= pend)
277 break;
279 namestart = tempstart;
280 namelen = templen;
281 while (*p == ' ' || *p == '\t')
282 p++;
283 textstart = p;
284 Ltext:
285 while (*p != '\n')
286 p++;
287 textlen = p - textstart;
288 p++;
289 Lcont:
290 continue;
291 Lskipline:
292 // Ignore this line
293 while (*p++ != '\n')
297 if (namelen)
298 goto L1;
299 // write out last one
300 buf.writestring(")");
301 TypeFunction tf = a.length == 1 ? isTypeFunction(s) : null;
302 if (tf)
304 size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.length : 0) +
305 cast(int)(tf.parameterList.varargs == VarArg.variadic);
306 if (pcount != paramcount)
308 sc.eSink.warning(s.loc, "Ddoc: parameter count mismatch, expected %llu, got %llu",
309 cast(ulong) pcount, cast(ulong) paramcount);
310 if (paramcount == 0)
312 // Chances are someone messed up the format
313 sc.eSink.warningSupplemental(s.loc, "Note that the format is `param = description`");
320 /***********************************************************
322 final class MacroSection : Section
324 override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, ref OutBuffer buf)
326 //printf("MacroSection::write()\n");
327 DocComment.parseMacros(dc.escapetable, *dc.pmacrotable, body_);
331 alias Sections = Array!(Section);
333 // Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one).
334 bool isCVariadicParameter(Dsymbols* a, const(char)[] p) @safe
336 foreach (member; *a)
338 TypeFunction tf = isTypeFunction(member);
339 if (tf && tf.parameterList.varargs == VarArg.variadic && p == "...")
340 return true;
342 return false;
345 Dsymbol getEponymousMember(TemplateDeclaration td) @safe
347 if (!td.onemember)
348 return null;
349 if (AggregateDeclaration ad = td.onemember.isAggregateDeclaration())
350 return ad;
351 if (FuncDeclaration fd = td.onemember.isFuncDeclaration())
352 return fd;
353 if (auto em = td.onemember.isEnumMember())
354 return null; // Keep backward compatibility. See compilable/ddoc9.d
355 if (VarDeclaration vd = td.onemember.isVarDeclaration())
356 return td.constraint ? null : vd;
357 return null;
360 TemplateDeclaration getEponymousParent(Dsymbol s) @safe
362 if (!s.parent)
363 return null;
364 TemplateDeclaration td = s.parent.isTemplateDeclaration();
365 return (td && getEponymousMember(td)) ? td : null;
368 immutable ddoc_default = import("default_ddoc_theme." ~ ddoc_ext);
369 immutable ddoc_decl_s = "$(DDOC_DECL ";
370 immutable ddoc_decl_e = ")\n";
371 immutable ddoc_decl_dd_s = "$(DDOC_DECL_DD ";
372 immutable ddoc_decl_dd_e = ")\n";
374 /****************************************************
375 * Generate Ddoc file for Module m.
376 * Params:
377 * m = Module
378 * ddoctext_ptr = combined text of .ddoc files for macro definitions
379 * ddoctext_length = extant of ddoctext_ptr
380 * datetime = charz returned by ctime()
381 * eSink = send error messages to eSink
382 * outbuf = append the Ddoc text to this
384 public
385 extern(C++) void gendocfile(Module m, const char* ddoctext_ptr, size_t ddoctext_length, const char* datetime, ErrorSink eSink, ref OutBuffer outbuf)
387 gendocfile(m, ddoctext_ptr[0 .. ddoctext_length], datetime, eSink, outbuf);
390 /****************************************************
391 * Generate Ddoc text for Module `m` and append it to `outbuf`.
392 * Params:
393 * m = Module
394 * ddoctext = combined text of .ddoc files for macro definitions
395 * datetime = charz returned by ctime()
396 * eSink = send error messages to eSink
397 * outbuf = append the Ddoc text to this
399 public
400 void gendocfile(Module m, const char[] ddoctext, const char* datetime, ErrorSink eSink, ref OutBuffer outbuf)
402 // Load internal default macros first
403 DocComment.parseMacros(m.escapetable, m.macrotable, ddoc_default[]);
405 // Ddoc files override default macros
406 DocComment.parseMacros(m.escapetable, m.macrotable, ddoctext);
408 Scope* sc = Scope.createGlobal(m, eSink); // create root scope
409 DocComment* dc = DocComment.parse(m, m.comment);
410 dc.pmacrotable = &m.macrotable;
411 dc.escapetable = m.escapetable;
412 sc.lastdc = dc;
413 // Generate predefined macros
414 // Set the title to be the name of the module
416 const p = m.toPrettyChars().toDString;
417 m.macrotable.define("TITLE", p);
419 // Set time macros
420 m.macrotable.define("DATETIME", datetime[0 .. 26]);
421 m.macrotable.define("YEAR", datetime[20 .. 20 + 4]);
423 const srcfilename = m.srcfile.toString();
424 m.macrotable.define("SRCFILENAME", srcfilename);
425 const docfilename = m.docfile.toString();
426 m.macrotable.define("DOCFILENAME", docfilename);
427 if (dc.copyright)
429 dc.copyright.nooutput = 1;
430 m.macrotable.define("COPYRIGHT", dc.copyright.body_);
433 OutBuffer buf;
434 if (m.filetype == FileType.ddoc)
436 const ploc = m.md ? &m.md.loc : &m.loc;
437 Loc loc = *ploc;
438 if (!loc.filename)
439 loc.filename = srcfilename.ptr;
441 size_t commentlen = m.comment ? strlen(cast(char*)m.comment) : 0;
442 Dsymbols a;
443 // https://issues.dlang.org/show_bug.cgi?id=9764
444 // Don't push m in a, to prevent emphasize ddoc file name.
445 if (dc.macros)
447 commentlen = dc.macros.name.ptr - m.comment;
448 dc.macros.write(loc, dc, sc, &a, buf);
450 buf.write(m.comment[0 .. commentlen]);
451 highlightText(sc, &a, loc, buf, 0);
453 else
455 Dsymbols a;
456 a.push(m);
457 dc.writeSections(sc, &a, buf);
458 emitMemberComments(m, buf, sc);
460 //printf("BODY= '%.*s'\n", cast(int)buf.length, buf.data);
461 m.macrotable.define("BODY", buf[]);
463 OutBuffer buf2;
464 buf2.writestring("$(DDOC)");
465 size_t end = buf2.length;
467 // Expand buf in place with macro expansions
468 const success = m.macrotable.expand(buf2, 0, end, null, global.recursionLimit, &isIdStart, &isIdTail);
469 if (!success)
470 eSink.error(Loc.initial, "DDoc macro expansion limit exceeded; more than %d expansions.", global.recursionLimit);
472 /* Remove all the escape sequences from buf,
473 * and make CR-LF the newline.
475 const slice = buf2[];
476 outbuf.reserve(slice.length);
477 auto p = slice.ptr;
478 for (size_t j = 0; j < slice.length; j++)
480 char c = p[j];
481 if (c == 0xFF && j + 1 < slice.length)
483 j++;
484 continue;
486 if (c == '\n')
487 outbuf.writeByte('\r');
488 else if (c == '\r')
490 outbuf.writestring("\r\n");
491 if (j + 1 < slice.length && p[j + 1] == '\n')
493 j++;
495 continue;
497 outbuf.writeByte(c);
501 /****************************************************
502 * Having unmatched parentheses can hose the output of Ddoc,
503 * as the macros depend on properly nested parentheses.
504 * This function replaces all ( with $(LPAREN) and ) with $(RPAREN)
505 * to preserve text literally. This also means macros in the
506 * text won't be expanded.
508 public
509 void escapeDdocString(ref OutBuffer buf, size_t start)
511 for (size_t u = start; u < buf.length; u++)
513 char c = buf[u];
514 switch (c)
516 case '$':
517 buf.remove(u, 1);
518 buf.insert(u, "$(DOLLAR)");
519 u += 8;
520 break;
521 case '(':
522 buf.remove(u, 1); //remove the (
523 buf.insert(u, "$(LPAREN)"); //insert this instead
524 u += 8; //skip over newly inserted macro
525 break;
526 case ')':
527 buf.remove(u, 1); //remove the )
528 buf.insert(u, "$(RPAREN)"); //insert this instead
529 u += 8; //skip over newly inserted macro
530 break;
531 default:
532 break;
537 /****************************************************
538 * Having unmatched parentheses can hose the output of Ddoc,
539 * as the macros depend on properly nested parentheses.
541 * Fix by replacing unmatched ( with $(LPAREN) and unmatched ) with $(RPAREN).
543 * Params:
544 * 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.
545 * buf = an OutBuffer containing the DDoc
546 * start = the index within buf to start replacing unmatched parentheses
547 * respectBackslashEscapes = if true, always replace parentheses that are
548 * directly preceeded by a backslash with $(LPAREN) or $(RPAREN) instead of
549 * counting them as stray parentheses
551 private void escapeStrayParenthesis(Loc loc, ref OutBuffer buf, size_t start, bool respectBackslashEscapes, ErrorSink eSink)
553 uint par_open = 0;
554 char inCode = 0;
555 bool atLineStart = true;
556 for (size_t u = start; u < buf.length; u++)
558 char c = buf[u];
559 switch (c)
561 case '(':
562 if (!inCode)
563 par_open++;
564 atLineStart = false;
565 break;
566 case ')':
567 if (!inCode)
569 if (par_open == 0)
571 //stray ')'
572 eSink.warning(loc, "Ddoc: Stray ')'. This may cause incorrect Ddoc output. Use $(RPAREN) instead for unpaired right parentheses.");
573 buf.remove(u, 1); //remove the )
574 buf.insert(u, "$(RPAREN)"); //insert this instead
575 u += 8; //skip over newly inserted macro
577 else
578 par_open--;
580 atLineStart = false;
581 break;
582 case '\n':
583 atLineStart = true;
584 version (none)
586 // For this to work, loc must be set to the beginning of the passed
587 // text which is currently not possible
588 // (loc is set to the Loc of the Dsymbol)
589 loc.linnum++;
591 break;
592 case ' ':
593 case '\r':
594 case '\t':
595 break;
596 case '-':
597 case '`':
598 case '~':
599 // Issue 15465: don't try to escape unbalanced parens inside code
600 // blocks.
601 int numdash = 1;
602 for (++u; u < buf.length && buf[u] == c; ++u)
603 ++numdash;
604 --u;
605 if (c == '`' || (atLineStart && numdash >= 3))
607 if (inCode == c)
608 inCode = 0;
609 else if (!inCode)
610 inCode = c;
612 atLineStart = false;
613 break;
614 case '\\':
615 // replace backslash-escaped parens with their macros
616 if (!inCode && respectBackslashEscapes && u+1 < buf.length)
618 if (buf[u+1] == '(' || buf[u+1] == ')')
620 const paren = buf[u+1] == '(' ? "$(LPAREN)" : "$(RPAREN)";
621 buf.remove(u, 2); //remove the \)
622 buf.insert(u, paren); //insert this instead
623 u += 8; //skip over newly inserted macro
625 else if (buf[u+1] == '\\')
626 ++u;
628 break;
629 default:
630 atLineStart = false;
631 break;
634 if (par_open) // if any unmatched lparens
636 par_open = 0;
637 for (size_t u = buf.length; u > start;)
639 u--;
640 char c = buf[u];
641 switch (c)
643 case ')':
644 par_open++;
645 break;
646 case '(':
647 if (par_open == 0)
649 //stray '('
650 eSink.warning(loc, "Ddoc: Stray '('. This may cause incorrect Ddoc output. Use $(LPAREN) instead for unpaired left parentheses.");
651 buf.remove(u, 1); //remove the (
652 buf.insert(u, "$(LPAREN)"); //insert this instead
654 else
655 par_open--;
656 break;
657 default:
658 break;
664 // Basically, this is to skip over things like private{} blocks in a struct or
665 // class definition that don't add any components to the qualified name.
666 Scope* skipNonQualScopes(Scope* sc) @safe
668 while (sc && !sc.scopesym)
669 sc = sc.enclosing;
670 return sc;
673 bool emitAnchorName(ref OutBuffer buf, Dsymbol s, Scope* sc, bool includeParent)
675 if (!s || s.isPackage() || s.isModule())
676 return false;
677 // Add parent names first
678 bool dot = false;
679 auto eponymousParent = getEponymousParent(s);
680 if (includeParent && s.parent || eponymousParent)
681 dot = emitAnchorName(buf, s.parent, sc, includeParent);
682 else if (includeParent && sc)
683 dot = emitAnchorName(buf, sc.scopesym, skipNonQualScopes(sc.enclosing), includeParent);
684 // Eponymous template members can share the parent anchor name
685 if (eponymousParent)
686 return dot;
687 if (dot)
688 buf.writeByte('.');
689 // Use "this" not "__ctor"
690 TemplateDeclaration td;
691 if (s.isCtorDeclaration() || ((td = s.isTemplateDeclaration()) !is null && td.onemember && td.onemember.isCtorDeclaration()))
693 buf.writestring("this");
695 else
697 /* We just want the identifier, not overloads like TemplateDeclaration::toChars.
698 * We don't want the template parameter list and constraints. */
699 buf.writestring(s.Dsymbol.toChars());
701 return true;
704 void emitAnchor(ref OutBuffer buf, Dsymbol s, Scope* sc, bool forHeader = false)
706 Identifier ident;
708 OutBuffer anc;
709 emitAnchorName(anc, s, skipNonQualScopes(sc), true);
710 ident = Identifier.idPool(anc[]);
713 auto pcount = cast(void*)ident in sc.anchorCounts;
714 typeof(*pcount) count;
715 if (!forHeader)
717 if (pcount)
719 // Existing anchor,
720 // don't write an anchor for matching consecutive ditto symbols
721 TemplateDeclaration td = getEponymousParent(s);
722 if (sc.prevAnchor == ident && sc.lastdc && (isDitto(s.comment) || (td && isDitto(td.comment))))
723 return;
725 count = ++*pcount;
727 else
729 sc.anchorCounts[cast(void*)ident] = 1;
730 count = 1;
734 // cache anchor name
735 sc.prevAnchor = ident;
736 auto macroName = forHeader ? "DDOC_HEADER_ANCHOR" : "DDOC_ANCHOR";
738 if (auto imp = s.isImport())
740 // For example: `public import core.stdc.string : memcpy, memcmp;`
741 if (imp.aliases.length > 0)
743 for(int i = 0; i < imp.aliases.length; i++)
745 // Need to distinguish between
746 // `public import core.stdc.string : memcpy, memcmp;` and
747 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
748 auto a = imp.aliases[i];
749 auto id = a ? a : imp.names[i];
750 auto loc = Loc.init;
751 Dsymbol pscopesym;
752 if (auto symFromId = sc.search(loc, id, pscopesym))
754 emitAnchor(buf, symFromId, sc, forHeader);
758 else
760 // For example: `public import str = core.stdc.string;`
761 if (imp.aliasId)
763 auto symbolName = imp.aliasId.toString();
765 buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
766 cast(int) symbolName.length, symbolName.ptr);
768 if (forHeader)
770 buf.printf(", %.*s", cast(int) symbolName.length, symbolName.ptr);
773 else
775 // The general case: `public import core.stdc.string;`
777 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
778 void printFullyQualifiedImport()
780 foreach (const pid; imp.packages)
782 buf.printf("%s.", pid.toChars());
784 buf.writestring(imp.id.toString());
787 buf.printf("$(%.*s ", cast(int) macroName.length, macroName.ptr);
788 printFullyQualifiedImport();
790 if (forHeader)
792 buf.printf(", ");
793 printFullyQualifiedImport();
797 buf.writeByte(')');
800 else
802 auto symbolName = ident.toString();
803 buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr,
804 cast(int) symbolName.length, symbolName.ptr);
806 // only append count once there's a duplicate
807 if (count > 1)
808 buf.printf(".%u", count);
810 if (forHeader)
812 Identifier shortIdent;
814 OutBuffer anc;
815 emitAnchorName(anc, s, skipNonQualScopes(sc), false);
816 shortIdent = Identifier.idPool(anc[]);
819 auto shortName = shortIdent.toString();
820 buf.printf(", %.*s", cast(int) shortName.length, shortName.ptr);
823 buf.writeByte(')');
827 /******************************* emitComment **********************************/
829 /** Get leading indentation from 'src' which represents lines of code. */
830 size_t getCodeIndent(const(char)* src)
832 while (src && (*src == '\r' || *src == '\n'))
833 ++src; // skip until we find the first non-empty line
834 size_t codeIndent = 0;
835 while (src && (*src == ' ' || *src == '\t'))
837 codeIndent++;
838 src++;
840 return codeIndent;
843 /** Recursively expand template mixin member docs into the scope. */
844 void expandTemplateMixinComments(TemplateMixin tm, ref OutBuffer buf, Scope* sc)
846 if (!tm.semanticRun)
847 tm.dsymbolSemantic(sc);
848 TemplateDeclaration td = (tm && tm.tempdecl) ? tm.tempdecl.isTemplateDeclaration() : null;
849 if (td && td.members)
851 for (size_t i = 0; i < td.members.length; i++)
853 Dsymbol sm = (*td.members)[i];
854 TemplateMixin tmc = sm.isTemplateMixin();
855 if (tmc && tmc.comment)
856 expandTemplateMixinComments(tmc, buf, sc);
857 else
858 emitComment(sm, buf, sc);
863 void emitMemberComments(ScopeDsymbol sds, ref OutBuffer buf, Scope* sc)
865 if (!sds.members)
866 return;
867 //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars());
868 const(char)[] m = "$(DDOC_MEMBERS ";
869 if (sds.isTemplateDeclaration())
870 m = "$(DDOC_TEMPLATE_MEMBERS ";
871 else if (sds.isClassDeclaration())
872 m = "$(DDOC_CLASS_MEMBERS ";
873 else if (sds.isStructDeclaration())
874 m = "$(DDOC_STRUCT_MEMBERS ";
875 else if (sds.isEnumDeclaration())
876 m = "$(DDOC_ENUM_MEMBERS ";
877 else if (sds.isModule())
878 m = "$(DDOC_MODULE_MEMBERS ";
879 size_t offset1 = buf.length; // save starting offset
880 buf.writestring(m);
881 size_t offset2 = buf.length; // to see if we write anything
882 sc = sc.push(sds);
883 for (size_t i = 0; i < sds.members.length; i++)
885 Dsymbol s = (*sds.members)[i];
886 //printf("\ts = '%s'\n", s.toChars());
887 // only expand if parent is a non-template (semantic won't work)
888 if (s.comment && s.isTemplateMixin() && s.parent && !s.parent.isTemplateDeclaration())
889 expandTemplateMixinComments(cast(TemplateMixin)s, buf, sc);
890 emitComment(s, buf, sc);
892 emitComment(null, buf, sc);
893 sc.pop();
894 if (buf.length == offset2)
896 /* Didn't write out any members, so back out last write
898 buf.setsize(offset1);
900 else
901 buf.writestring(")");
904 void emitVisibility(ref OutBuffer buf, Import i)
906 // imports are private by default, which is different from other declarations
907 // so they should explicitly show their visibility
908 emitVisibility(buf, i.visibility);
911 void emitVisibility(ref OutBuffer buf, Declaration d)
913 auto vis = d.visibility;
914 if (vis.kind != Visibility.Kind.undefined && vis.kind != Visibility.Kind.public_)
916 emitVisibility(buf, vis);
920 void emitVisibility(ref OutBuffer buf, Visibility vis)
922 visibilityToBuffer(buf, vis);
923 buf.writeByte(' ');
926 void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc)
928 extern (C++) final class EmitComment : Visitor
930 alias visit = Visitor.visit;
931 public:
932 OutBuffer* buf;
933 Scope* sc;
935 extern (D) this(ref OutBuffer buf, Scope* sc) scope
937 this.buf = &buf;
938 this.sc = sc;
941 override void visit(Dsymbol)
945 override void visit(InvariantDeclaration)
949 override void visit(UnitTestDeclaration)
953 override void visit(PostBlitDeclaration)
957 override void visit(DtorDeclaration)
961 override void visit(StaticCtorDeclaration)
965 override void visit(StaticDtorDeclaration)
969 override void visit(TypeInfoDeclaration)
973 void emit(Scope* sc, Dsymbol s, const(char)* com)
975 if (s && sc.lastdc && isDitto(com))
977 sc.lastdc.a.push(s);
978 return;
980 // Put previous doc comment if exists
981 if (DocComment* dc = sc.lastdc)
983 assert(dc.a.length > 0, "Expects at least one declaration for a" ~
984 "documentation comment");
986 auto symbol = dc.a[0];
988 buf.writestring("$(DDOC_MEMBER");
989 buf.writestring("$(DDOC_MEMBER_HEADER");
990 emitAnchor(*buf, symbol, sc, true);
991 buf.writeByte(')');
993 // Put the declaration signatures as the document 'title'
994 buf.writestring(ddoc_decl_s);
995 for (size_t i = 0; i < dc.a.length; i++)
997 Dsymbol sx = dc.a[i];
998 // the added linebreaks in here make looking at multiple
999 // signatures more appealing
1000 if (i == 0)
1002 size_t o = buf.length;
1003 toDocBuffer(sx, *buf, sc);
1004 highlightCode(sc, sx, *buf, o);
1005 buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1006 continue;
1008 buf.writestring("$(DDOC_DITTO ");
1010 size_t o = buf.length;
1011 toDocBuffer(sx, *buf, sc);
1012 highlightCode(sc, sx, *buf, o);
1014 buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1015 buf.writeByte(')');
1017 buf.writestring(ddoc_decl_e);
1018 // Put the ddoc comment as the document 'description'
1019 buf.writestring(ddoc_decl_dd_s);
1021 dc.writeSections(sc, &dc.a, *buf);
1022 if (ScopeDsymbol sds = dc.a[0].isScopeDsymbol())
1023 emitMemberComments(sds, *buf, sc);
1025 buf.writestring(ddoc_decl_dd_e);
1026 buf.writeByte(')');
1027 //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0);
1029 if (s)
1031 DocComment* dc = DocComment.parse(s, com);
1032 dc.pmacrotable = &sc._module.macrotable;
1033 sc.lastdc = dc;
1037 override void visit(Import imp)
1039 if (imp.visible().kind != Visibility.Kind.public_ && sc.visibility.kind != Visibility.Kind.export_)
1040 return;
1042 if (imp.comment)
1043 emit(sc, imp, imp.comment);
1046 override void visit(Declaration d)
1048 //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment);
1049 //printf("type = %p\n", d.type);
1050 const(char)* com = d.comment;
1051 if (TemplateDeclaration td = getEponymousParent(d))
1053 if (isDitto(td.comment))
1054 com = td.comment;
1055 else
1056 com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
1058 else
1060 if (!d.ident)
1061 return;
1062 if (!d.type)
1064 if (!d.isCtorDeclaration() &&
1065 !d.isAliasDeclaration() &&
1066 !d.isVarDeclaration())
1068 return;
1071 if (d.visibility.kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
1072 return;
1074 if (!com)
1075 return;
1076 emit(sc, d, com);
1079 override void visit(AggregateDeclaration ad)
1081 //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars());
1082 const(char)* com = ad.comment;
1083 if (TemplateDeclaration td = getEponymousParent(ad))
1085 if (isDitto(td.comment))
1086 com = td.comment;
1087 else
1088 com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true);
1090 else
1092 if (ad.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
1093 return;
1094 if (!ad.comment)
1095 return;
1097 if (!com)
1098 return;
1099 emit(sc, ad, com);
1102 override void visit(TemplateDeclaration td)
1104 //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind());
1105 if (td.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
1106 return;
1107 if (!td.comment)
1108 return;
1109 if (Dsymbol ss = getEponymousMember(td))
1111 ss.accept(this);
1112 return;
1114 emit(sc, td, td.comment);
1117 override void visit(EnumDeclaration ed)
1119 if (ed.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
1120 return;
1121 if (ed.isAnonymous() && ed.members)
1123 for (size_t i = 0; i < ed.members.length; i++)
1125 Dsymbol s = (*ed.members)[i];
1126 emitComment(s, *buf, sc);
1128 return;
1130 if (!ed.comment)
1131 return;
1132 if (ed.isAnonymous())
1133 return;
1134 emit(sc, ed, ed.comment);
1137 override void visit(EnumMember em)
1139 //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment);
1140 if (em.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_)
1141 return;
1142 if (!em.comment)
1143 return;
1144 emit(sc, em, em.comment);
1147 override void visit(AttribDeclaration ad)
1149 //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);
1150 /* A general problem with this,
1151 * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516
1152 * is that attributes are not transmitted through to the underlying
1153 * member declarations for template bodies, because semantic analysis
1154 * is not done for template declaration bodies
1155 * (only template instantiations).
1156 * Hence, Ddoc omits attributes from template members.
1158 Dsymbols* d = ad.include(null);
1159 if (d)
1161 for (size_t i = 0; i < d.length; i++)
1163 Dsymbol s = (*d)[i];
1164 //printf("AttribDeclaration::emitComment %s\n", s.toChars());
1165 emitComment(s, *buf, sc);
1170 override void visit(VisibilityDeclaration pd)
1172 if (pd.decl)
1174 Scope* scx = sc;
1175 sc = sc.copy();
1176 sc.visibility = pd.visibility;
1177 visit(cast(AttribDeclaration)pd);
1178 scx.lastdc = sc.lastdc;
1179 sc = sc.pop();
1183 override void visit(ConditionalDeclaration cd)
1185 //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
1186 if (cd.condition.inc != Include.notComputed)
1188 visit(cast(AttribDeclaration)cd);
1189 return;
1191 /* If generating doc comment, be careful because if we're inside
1192 * a template, then include(null) will fail.
1194 Dsymbols* d = cd.decl ? cd.decl : cd.elsedecl;
1195 for (size_t i = 0; i < d.length; i++)
1197 Dsymbol s = (*d)[i];
1198 emitComment(s, *buf, sc);
1203 scope EmitComment v = new EmitComment(buf, sc);
1204 if (!s)
1205 v.emit(sc, null, null);
1206 else
1207 s.accept(v);
1210 void toDocBuffer(Dsymbol s, ref OutBuffer buf, Scope* sc)
1212 extern (C++) final class ToDocBuffer : Visitor
1214 alias visit = Visitor.visit;
1215 public:
1216 OutBuffer* buf;
1217 Scope* sc;
1219 extern (D) this(ref OutBuffer buf, Scope* sc) scope
1221 this.buf = &buf;
1222 this.sc = sc;
1225 override void visit(Dsymbol s)
1227 //printf("Dsymbol::toDocbuffer() %s\n", s.toChars());
1228 HdrGenState hgs;
1229 hgs.ddoc = true;
1230 toCBuffer(s, *buf, hgs);
1233 void prefix(Dsymbol s)
1235 if (s.isDeprecated())
1236 buf.writestring("deprecated ");
1237 if (Declaration d = s.isDeclaration())
1239 emitVisibility(*buf, d);
1240 if (d.isStatic())
1241 buf.writestring("static ");
1242 else if (d.isFinal())
1243 buf.writestring("final ");
1244 else if (d.isAbstract())
1245 buf.writestring("abstract ");
1247 if (d.isFuncDeclaration()) // functionToBufferFull handles this
1248 return;
1250 if (d.isImmutable())
1251 buf.writestring("immutable ");
1252 if (d.storage_class & STC.shared_)
1253 buf.writestring("shared ");
1254 if (d.isWild())
1255 buf.writestring("inout ");
1256 if (d.isConst())
1257 buf.writestring("const ");
1259 if (d.isSynchronized())
1260 buf.writestring("synchronized ");
1262 if (d.storage_class & STC.manifest)
1263 buf.writestring("enum ");
1265 // Add "auto" for the untyped variable in template members
1266 if (!d.type && d.isVarDeclaration() &&
1267 !d.isImmutable() && !(d.storage_class & STC.shared_) && !d.isWild() && !d.isConst() &&
1268 !d.isSynchronized())
1270 buf.writestring("auto ");
1275 override void visit(Import i)
1277 HdrGenState hgs;
1278 hgs.ddoc = true;
1279 emitVisibility(*buf, i);
1280 toCBuffer(i, *buf, hgs);
1283 override void visit(Declaration d)
1285 if (!d.ident)
1286 return;
1287 TemplateDeclaration td = getEponymousParent(d);
1288 //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--");
1289 HdrGenState hgs;
1290 hgs.ddoc = true;
1291 if (d.isDeprecated())
1292 buf.writestring("$(DEPRECATED ");
1293 prefix(d);
1294 if (d.type)
1296 Type origType = d.originalType ? d.originalType : d.type;
1297 if (origType.ty == Tfunction)
1299 functionToBufferFull(cast(TypeFunction)origType, *buf, d.ident, hgs, td);
1301 else
1302 toCBuffer(origType, *buf, d.ident, hgs);
1304 else
1305 buf.writestring(d.ident.toString());
1306 if (d.isVarDeclaration() && td)
1308 buf.writeByte('(');
1309 if (td.origParameters && td.origParameters.length)
1311 for (size_t i = 0; i < td.origParameters.length; i++)
1313 if (i)
1314 buf.writestring(", ");
1315 toCBuffer((*td.origParameters)[i], *buf, hgs);
1318 buf.writeByte(')');
1320 // emit constraints if declaration is a templated declaration
1321 if (td && td.constraint)
1323 bool noFuncDecl = td.isFuncDeclaration() is null;
1324 if (noFuncDecl)
1326 buf.writestring("$(DDOC_CONSTRAINT ");
1329 toCBuffer(td.constraint, *buf, hgs);
1331 if (noFuncDecl)
1333 buf.writestring(")");
1336 if (d.isDeprecated())
1337 buf.writestring(")");
1338 buf.writestring(";\n");
1341 override void visit(AliasDeclaration ad)
1343 //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars());
1344 if (!ad.ident)
1345 return;
1346 if (ad.isDeprecated())
1347 buf.writestring("deprecated ");
1348 emitVisibility(*buf, ad);
1349 buf.printf("alias %s = ", ad.toChars());
1350 if (Dsymbol s = ad.aliassym) // ident alias
1352 prettyPrintDsymbol(s, ad.parent);
1354 else if (Type type = ad.getType()) // type alias
1356 if (type.ty == Tclass || type.ty == Tstruct || type.ty == Tenum)
1358 import dmd.typesem : toDsymbol;
1359 if (Dsymbol s = type.toDsymbol(null)) // elaborate type
1360 prettyPrintDsymbol(s, ad.parent);
1361 else
1362 buf.writestring(type.toChars());
1364 else
1366 // simple type
1367 buf.writestring(type.toChars());
1370 buf.writestring(";\n");
1373 void parentToBuffer(Dsymbol s)
1375 if (s && !s.isPackage() && !s.isModule())
1377 parentToBuffer(s.parent);
1378 buf.writestring(s.toChars());
1379 buf.writestring(".");
1383 static bool inSameModule(Dsymbol s, Dsymbol p) @safe
1385 for (; s; s = s.parent)
1387 if (s.isModule())
1388 break;
1390 for (; p; p = p.parent)
1392 if (p.isModule())
1393 break;
1395 return s == p;
1398 void prettyPrintDsymbol(Dsymbol s, Dsymbol parent)
1400 if (s.parent && (s.parent == parent)) // in current scope -> naked name
1402 buf.writestring(s.toChars());
1404 else if (!inSameModule(s, parent)) // in another module -> full name
1406 buf.writestring(s.toPrettyChars());
1408 else // nested in a type in this module -> full name w/o module name
1410 // if alias is nested in a user-type use module-scope lookup
1411 if (!parent.isModule() && !parent.isPackage())
1412 buf.writestring(".");
1413 parentToBuffer(s.parent);
1414 buf.writestring(s.toChars());
1418 override void visit(AggregateDeclaration ad)
1420 if (!ad.ident)
1421 return;
1422 version (none)
1424 emitVisibility(buf, ad);
1426 buf.printf("%s %s", ad.kind(), ad.toChars());
1427 buf.writestring(";\n");
1430 override void visit(StructDeclaration sd)
1432 //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars());
1433 if (!sd.ident)
1434 return;
1435 version (none)
1437 emitVisibility(buf, sd);
1439 if (TemplateDeclaration td = getEponymousParent(sd))
1441 toDocBuffer(td, *buf, sc);
1443 else
1445 buf.printf("%s %s", sd.kind(), sd.toChars());
1447 buf.writestring(";\n");
1450 override void visit(ClassDeclaration cd)
1452 //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars());
1453 if (!cd.ident)
1454 return;
1455 version (none)
1457 emitVisibility(*buf, cd);
1459 if (TemplateDeclaration td = getEponymousParent(cd))
1461 toDocBuffer(td, *buf, sc);
1463 else
1465 if (!cd.isInterfaceDeclaration() && cd.isAbstract())
1466 buf.writestring("abstract ");
1467 buf.printf("%s %s", cd.kind(), cd.toChars());
1469 int any = 0;
1470 for (size_t i = 0; i < cd.baseclasses.length; i++)
1472 BaseClass* bc = (*cd.baseclasses)[i];
1473 if (bc.sym && bc.sym.ident == Id.Object)
1474 continue;
1475 if (any)
1476 buf.writestring(", ");
1477 else
1479 buf.writestring(": ");
1480 any = 1;
1483 if (bc.sym)
1485 buf.printf("$(DDOC_PSUPER_SYMBOL %s)", bc.sym.toPrettyChars());
1487 else
1489 HdrGenState hgs;
1490 toCBuffer(bc.type, *buf, null, hgs);
1493 buf.writestring(";\n");
1496 override void visit(EnumDeclaration ed)
1498 if (!ed.ident)
1499 return;
1500 buf.printf("%s %s", ed.kind(), ed.toChars());
1501 if (ed.memtype)
1503 buf.writestring(": $(DDOC_ENUM_BASETYPE ");
1504 HdrGenState hgs;
1505 toCBuffer(ed.memtype, *buf, null, hgs);
1506 buf.writestring(")");
1508 buf.writestring(";\n");
1511 override void visit(EnumMember em)
1513 if (!em.ident)
1514 return;
1515 buf.writestring(em.toChars());
1519 scope ToDocBuffer v = new ToDocBuffer(buf, sc);
1520 s.accept(v);
1523 /***********************************************************
1525 public
1526 struct DocComment
1528 Sections sections; // Section*[]
1529 Section summary;
1530 Section copyright;
1531 Section macros;
1532 MacroTable* pmacrotable;
1533 Escape* escapetable;
1534 Dsymbols a;
1536 static DocComment* parse(Dsymbol s, const(char)* comment)
1538 //printf("parse(%s): '%s'\n", s.toChars(), comment);
1539 auto dc = new DocComment();
1540 dc.a.push(s);
1541 if (!comment)
1542 return dc;
1543 dc.parseSections(comment);
1544 for (size_t i = 0; i < dc.sections.length; i++)
1546 Section sec = dc.sections[i];
1547 if (iequals("copyright", sec.name))
1549 dc.copyright = sec;
1551 if (iequals("macros", sec.name))
1553 dc.macros = sec;
1556 return dc;
1559 /************************************************
1560 * Parse macros out of Macros: section.
1561 * Macros are of the form:
1562 * name1 = value1
1564 * name2 = value2
1566 extern(D) static void parseMacros(
1567 Escape* escapetable, ref MacroTable pmacrotable, const(char)[] m)
1569 const(char)* p = m.ptr;
1570 size_t len = m.length;
1571 const(char)* pend = p + len;
1572 const(char)* tempstart = null;
1573 size_t templen = 0;
1574 const(char)* namestart = null;
1575 size_t namelen = 0; // !=0 if line continuation
1576 const(char)* textstart = null;
1577 size_t textlen = 0;
1578 while (p < pend)
1580 // Skip to start of macro
1581 while (1)
1583 if (p >= pend)
1584 goto Ldone;
1585 switch (*p)
1587 case ' ':
1588 case '\t':
1589 p++;
1590 continue;
1591 case '\r':
1592 case '\n':
1593 p++;
1594 goto Lcont;
1595 default:
1596 if (isIdStart(p))
1597 break;
1598 if (namelen)
1599 goto Ltext; // continuation of prev macro
1600 goto Lskipline;
1602 break;
1604 tempstart = p;
1605 while (1)
1607 if (p >= pend)
1608 goto Ldone;
1609 if (!isIdTail(p))
1610 break;
1611 p += utfStride(p);
1613 templen = p - tempstart;
1614 while (1)
1616 if (p >= pend)
1617 goto Ldone;
1618 if (!(*p == ' ' || *p == '\t'))
1619 break;
1620 p++;
1622 if (*p != '=')
1624 if (namelen)
1625 goto Ltext; // continuation of prev macro
1626 goto Lskipline;
1628 p++;
1629 if (p >= pend)
1630 goto Ldone;
1631 if (namelen)
1633 // Output existing macro
1635 //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
1636 if (iequals("ESCAPES", namestart[0 .. namelen]))
1637 parseEscapes(escapetable, textstart[0 .. textlen]);
1638 else
1639 pmacrotable.define(namestart[0 .. namelen], textstart[0 .. textlen]);
1640 namelen = 0;
1641 if (p >= pend)
1642 break;
1644 namestart = tempstart;
1645 namelen = templen;
1646 while (p < pend && (*p == ' ' || *p == '\t'))
1647 p++;
1648 textstart = p;
1649 Ltext:
1650 while (p < pend && *p != '\r' && *p != '\n')
1651 p++;
1652 textlen = p - textstart;
1653 p++;
1654 //printf("p = %p, pend = %p\n", p, pend);
1655 Lcont:
1656 continue;
1657 Lskipline:
1658 // Ignore this line
1659 while (p < pend && *p != '\r' && *p != '\n')
1660 p++;
1662 Ldone:
1663 if (namelen)
1664 goto L1; // write out last one
1667 /**************************************
1668 * Parse escapes of the form:
1669 * /c/string/
1670 * where c is a single character.
1671 * Multiple escapes can be separated
1672 * by whitespace and/or commas.
1674 static void parseEscapes(Escape* escapetable, const(char)[] text)
1676 if (!escapetable)
1678 escapetable = new Escape();
1679 memset(escapetable, 0, Escape.sizeof);
1681 //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable);
1682 const(char)* p = text.ptr;
1683 const(char)* pend = p + text.length;
1684 while (1)
1686 while (1)
1688 if (p + 4 >= pend)
1689 return;
1690 if (!(*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ','))
1691 break;
1692 p++;
1694 if (p[0] != '/' || p[2] != '/')
1695 return;
1696 char c = p[1];
1697 p += 3;
1698 const(char)* start = p;
1699 while (1)
1701 if (p >= pend)
1702 return;
1703 if (*p == '/')
1704 break;
1705 p++;
1707 size_t len = p - start;
1708 char* s = cast(char*)memcpy(mem.xmalloc(len + 1), start, len);
1709 s[len] = 0;
1710 escapetable.strings[c] = s[0 .. len];
1711 //printf("\t%c = '%s'\n", c, s);
1712 p++;
1716 /*****************************************
1717 * Parse next paragraph out of *pcomment.
1718 * Update *pcomment to point past paragraph.
1719 * Returns NULL if no more paragraphs.
1720 * If paragraph ends in 'identifier:',
1721 * then (*pcomment)[0 .. idlen] is the identifier.
1723 void parseSections(const(char)* comment)
1725 const(char)* p;
1726 const(char)* pstart;
1727 const(char)* pend;
1728 const(char)* idstart = null; // dead-store to prevent spurious warning
1729 size_t idlen;
1730 const(char)* name = null;
1731 size_t namelen = 0;
1732 //printf("parseSections('%s')\n", comment);
1733 p = comment;
1734 while (*p)
1736 const(char)* pstart0 = p;
1737 p = skipwhitespace(p);
1738 pstart = p;
1739 pend = p;
1741 // Undo indent if starting with a list item
1742 if ((*p == '-' || *p == '+' || *p == '*') && (*(p+1) == ' ' || *(p+1) == '\t'))
1743 pstart = pstart0;
1744 else
1746 const(char)* pitem = p;
1747 while (*pitem >= '0' && *pitem <= '9')
1748 ++pitem;
1749 if (pitem > p && *pitem == '.' && (*(pitem+1) == ' ' || *(pitem+1) == '\t'))
1750 pstart = pstart0;
1753 /* Find end of section, which is ended by one of:
1754 * 'identifier:' (but not inside a code section)
1755 * '\0'
1757 idlen = 0;
1758 int inCode = 0;
1759 while (1)
1761 // Check for start/end of a code section
1762 if (*p == '-' || *p == '`' || *p == '~')
1764 char c = *p;
1765 int numdash = 0;
1766 while (*p == c)
1768 ++numdash;
1769 p++;
1771 // BUG: handle UTF PS and LS too
1772 if ((!*p || *p == '\r' || *p == '\n' || (!inCode && c != '-')) && numdash >= 3)
1774 inCode = inCode == c ? false : c;
1775 if (inCode)
1777 // restore leading indentation
1778 while (pstart0 < pstart && isIndentWS(pstart - 1))
1779 --pstart;
1782 pend = p;
1784 if (!inCode && isIdStart(p))
1786 const(char)* q = p + utfStride(p);
1787 while (isIdTail(q))
1788 q += utfStride(q);
1790 // Detected tag ends it
1791 if (*q == ':' && isupper(*p)
1792 && (isspace(q[1]) || q[1] == 0))
1794 idlen = q - p;
1795 idstart = p;
1796 for (pend = p; pend > pstart; pend--)
1798 if (pend[-1] == '\n')
1799 break;
1801 p = q + 1;
1802 break;
1805 while (1)
1807 if (!*p)
1808 goto L1;
1809 if (*p == '\n')
1811 p++;
1812 if (*p == '\n' && !summary && !namelen && !inCode)
1814 pend = p;
1815 p++;
1816 goto L1;
1818 break;
1820 p++;
1821 pend = p;
1823 p = skipwhitespace(p);
1826 if (namelen || pstart < pend)
1828 Section s;
1829 if (iequals("Params", name[0 .. namelen]))
1830 s = new ParamSection();
1831 else if (iequals("Macros", name[0 .. namelen]))
1832 s = new MacroSection();
1833 else
1834 s = new Section();
1835 s.name = name[0 .. namelen];
1836 s.body_ = pstart[0 .. pend - pstart];
1837 s.nooutput = 0;
1838 //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body);
1839 sections.push(s);
1840 if (!summary && !namelen)
1841 summary = s;
1843 if (idlen)
1845 name = idstart;
1846 namelen = idlen;
1848 else
1850 name = null;
1851 namelen = 0;
1852 if (!*p)
1853 break;
1858 void writeSections(Scope* sc, Dsymbols* a, ref OutBuffer buf)
1860 assert(a.length);
1861 //printf("DocComment::writeSections()\n");
1862 Loc loc = (*a)[0].loc;
1863 if (Module m = (*a)[0].isModule())
1865 if (m.md)
1866 loc = m.md.loc;
1868 size_t offset1 = buf.length;
1869 buf.writestring("$(DDOC_SECTIONS ");
1870 size_t offset2 = buf.length;
1871 for (size_t i = 0; i < sections.length; i++)
1873 Section sec = sections[i];
1874 if (sec.nooutput)
1875 continue;
1876 //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body);
1877 if (!sec.name.length && i == 0)
1879 buf.writestring("$(DDOC_SUMMARY ");
1880 size_t o = buf.length;
1881 buf.write(sec.body_);
1882 escapeStrayParenthesis(loc, buf, o, true, sc.eSink);
1883 highlightText(sc, a, loc, buf, o);
1884 buf.writestring(")");
1886 else
1887 sec.write(loc, &this, sc, a, buf);
1889 for (size_t i = 0; i < a.length; i++)
1891 Dsymbol s = (*a)[i];
1892 if (Dsymbol td = getEponymousParent(s))
1893 s = td;
1894 for (UnitTestDeclaration utd = s.ddocUnittest; utd; utd = utd.ddocUnittest)
1896 if (utd.visibility.kind == Visibility.Kind.private_ || !utd.comment || !utd.fbody)
1897 continue;
1898 // Strip whitespaces to avoid showing empty summary
1899 const(char)* c = utd.comment;
1900 while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r')
1901 ++c;
1902 buf.writestring("$(DDOC_EXAMPLES ");
1903 size_t o = buf.length;
1904 buf.writestring(cast(char*)c);
1905 if (utd.codedoc)
1907 auto codedoc = utd.codedoc.stripLeadingNewlines;
1908 size_t n = getCodeIndent(codedoc);
1909 while (n--)
1910 buf.writeByte(' ');
1911 buf.writestring("----\n");
1912 buf.writestring(codedoc);
1913 buf.writestring("----\n");
1914 highlightText(sc, a, loc, buf, o);
1916 buf.writestring(")");
1919 if (buf.length == offset2)
1921 /* Didn't write out any sections, so back out last write
1923 buf.setsize(offset1);
1924 buf.writestring("\n");
1926 else
1927 buf.writestring(")");
1931 /*****************************************
1932 * Return true if comment consists entirely of "ditto".
1934 bool isDitto(const(char)* comment)
1936 if (comment)
1938 const(char)* p = skipwhitespace(comment);
1939 if (Port.memicmp(p, "ditto", 5) == 0 && *skipwhitespace(p + 5) == 0)
1940 return true;
1942 return false;
1945 /**********************************************
1946 * Skip white space.
1948 const(char)* skipwhitespace(const(char)* p)
1950 return skipwhitespace(p.toDString).ptr;
1953 /// Ditto
1954 const(char)[] skipwhitespace(const(char)[] p) @safe
1956 foreach (idx, char c; p)
1958 switch (c)
1960 case ' ':
1961 case '\t':
1962 case '\n':
1963 continue;
1964 default:
1965 return p[idx .. $];
1968 return p[$ .. $];
1971 /************************************************
1972 * Scan past all instances of the given characters.
1973 * Params:
1974 * buf = an OutBuffer containing the DDoc
1975 * i = the index within `buf` to start scanning from
1976 * chars = the characters to skip; order is unimportant
1977 * Returns: the index after skipping characters.
1979 size_t skipChars(ref OutBuffer buf, size_t i, string chars) @safe
1981 Outer:
1982 foreach (j, c; buf[][i..$])
1984 foreach (d; chars)
1986 if (d == c)
1987 continue Outer;
1989 return i + j;
1991 return buf.length;
1994 unittest {
1995 OutBuffer buf;
1996 string data = "test ---\r\n\r\nend";
1997 buf.write(data);
1999 assert(skipChars(buf, 0, "-") == 0);
2000 assert(skipChars(buf, 4, "-") == 4);
2001 assert(skipChars(buf, 4, " -") == 8);
2002 assert(skipChars(buf, 8, "\r\n") == 12);
2003 assert(skipChars(buf, 12, "dne") == 15);
2006 /****************************************************
2007 * Replace all instances of `c` with `r` in the given string
2008 * Params:
2009 * s = the string to do replacements in
2010 * c = the character to look for
2011 * r = the string to replace `c` with
2012 * Returns: `s` with `c` replaced with `r`
2014 inout(char)[] replaceChar(inout(char)[] s, char c, string r) pure @safe
2016 int count = 0;
2017 foreach (char sc; s)
2018 if (sc == c)
2019 ++count;
2020 if (count == 0)
2021 return s;
2023 char[] result;
2024 result.reserve(s.length - count + (r.length * count));
2025 size_t start = 0;
2026 foreach (i, char sc; s)
2028 if (sc == c)
2030 result ~= s[start..i];
2031 result ~= r;
2032 start = i+1;
2035 result ~= s[start..$];
2036 return result;
2040 unittest
2042 assert("".replaceChar(',', "$(COMMA)") == "");
2043 assert("ab".replaceChar(',', "$(COMMA)") == "ab");
2044 assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b");
2045 assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b");
2046 assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab");
2047 assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)");
2051 * Return a lowercased copy of a string.
2052 * Params:
2053 * s = the string to lowercase
2054 * Returns: the lowercase version of the string or the original if already lowercase
2056 string toLowercase(string s) pure @safe
2058 string lower;
2059 foreach (size_t i; 0..s.length)
2061 char c = s[i];
2062 // TODO: maybe unicode lowercase, somehow
2063 if (c >= 'A' && c <= 'Z')
2065 if (!lower.length) {
2066 lower.reserve(s.length);
2068 lower ~= s[lower.length..i];
2069 c += 'a' - 'A';
2070 lower ~= c;
2073 if (lower.length)
2074 lower ~= s[lower.length..$];
2075 else
2076 lower = s;
2077 return lower;
2081 unittest
2083 assert("".toLowercase == "");
2084 assert("abc".toLowercase == "abc");
2085 assert("ABC".toLowercase == "abc");
2086 assert("aBc".toLowercase == "abc");
2089 /************************************************
2090 * Get the indent from one index to another, counting tab stops as four spaces wide
2091 * per the Markdown spec.
2092 * Params:
2093 * buf = an OutBuffer containing the DDoc
2094 * from = the index within `buf` to start counting from, inclusive
2095 * to = the index within `buf` to stop counting at, exclusive
2096 * Returns: the indent
2098 int getMarkdownIndent(ref OutBuffer buf, size_t from, size_t to) @safe
2100 const slice = buf[];
2101 if (to > slice.length)
2102 to = slice.length;
2103 int indent = 0;
2104 foreach (const c; slice[from..to])
2105 indent += (c == '\t') ? 4 - (indent % 4) : 1;
2106 return indent;
2109 /************************************************
2110 * Scan forward to one of:
2111 * start of identifier
2112 * beginning of next line
2113 * end of buf
2115 size_t skiptoident(ref OutBuffer buf, size_t i) @safe
2117 const slice = buf[];
2118 while (i < slice.length)
2120 dchar c;
2121 size_t oi = i;
2122 if (utf_decodeChar(slice, i, c))
2124 /* Ignore UTF errors, but still consume input
2126 break;
2128 if (c >= 0x80)
2130 if (!isUniAlpha(c))
2131 continue;
2133 else if (!(isalpha(c) || c == '_' || c == '\n'))
2134 continue;
2135 i = oi;
2136 break;
2138 return i;
2141 /************************************************
2142 * Scan forward past end of identifier.
2144 size_t skippastident(ref OutBuffer buf, size_t i) @safe
2146 const slice = buf[];
2147 while (i < slice.length)
2149 dchar c;
2150 size_t oi = i;
2151 if (utf_decodeChar(slice, i, c))
2153 /* Ignore UTF errors, but still consume input
2155 break;
2157 if (c >= 0x80)
2159 if (isUniAlpha(c))
2160 continue;
2162 else if (isalnum(c) || c == '_')
2163 continue;
2164 i = oi;
2165 break;
2167 return i;
2170 /************************************************
2171 * Scan forward past end of an identifier that might
2172 * contain dots (e.g. `abc.def`)
2174 size_t skipPastIdentWithDots(ref OutBuffer buf, size_t i) @safe
2176 const slice = buf[];
2177 bool lastCharWasDot;
2178 while (i < slice.length)
2180 dchar c;
2181 size_t oi = i;
2182 if (utf_decodeChar(slice, i, c))
2184 /* Ignore UTF errors, but still consume input
2186 break;
2188 if (c == '.')
2190 // We need to distinguish between `abc.def`, abc..def`, and `abc.`
2191 // Only `abc.def` is a valid identifier
2193 if (lastCharWasDot)
2195 i = oi;
2196 break;
2199 lastCharWasDot = true;
2200 continue;
2202 else
2204 if (c >= 0x80)
2206 if (isUniAlpha(c))
2208 lastCharWasDot = false;
2209 continue;
2212 else if (isalnum(c) || c == '_')
2214 lastCharWasDot = false;
2215 continue;
2217 i = oi;
2218 break;
2222 // if `abc.`
2223 if (lastCharWasDot)
2224 return i - 1;
2226 return i;
2229 /************************************************
2230 * Scan forward past URL starting at i.
2231 * We don't want to highlight parts of a URL.
2232 * Returns:
2233 * i if not a URL
2234 * index just past it if it is a URL
2236 size_t skippastURL(ref OutBuffer buf, size_t i)
2238 const slice = buf[][i .. $];
2239 size_t j;
2240 bool sawdot = false;
2241 if (slice.length > 7 && Port.memicmp(slice.ptr, "http://", 7) == 0)
2243 j = 7;
2245 else if (slice.length > 8 && Port.memicmp(slice.ptr, "https://", 8) == 0)
2247 j = 8;
2249 else
2250 goto Lno;
2251 for (; j < slice.length; j++)
2253 const c = slice[j];
2254 if (isalnum(c))
2255 continue;
2256 if (c == '-' || c == '_' || c == '?' || c == '=' || c == '%' ||
2257 c == '&' || c == '/' || c == '+' || c == '#' || c == '~')
2258 continue;
2259 if (c == '.')
2261 sawdot = true;
2262 continue;
2264 break;
2266 if (sawdot)
2267 return i + j;
2268 Lno:
2269 return i;
2272 /****************************************************
2273 * Remove a previously-inserted blank line macro.
2274 * Params:
2275 * buf = an OutBuffer containing the DDoc
2276 * iAt = the index within `buf` of the start of the `$(DDOC_BLANKLINE)`
2277 * macro. Upon function return its value is set to `0`.
2278 * i = an index within `buf`. If `i` is after `iAt` then it gets
2279 * reduced by the length of the removed macro.
2281 void removeBlankLineMacro(ref OutBuffer buf, ref size_t iAt, ref size_t i)
2283 if (!iAt)
2284 return;
2286 enum macroLength = "$(DDOC_BLANKLINE)".length;
2287 buf.remove(iAt, macroLength);
2288 if (i > iAt)
2289 i -= macroLength;
2290 iAt = 0;
2293 /****************************************************
2294 * Attempt to detect and replace a Markdown thematic break (HR). These are three
2295 * or more of the same delimiter, optionally with spaces or tabs between any of
2296 * them, e.g. `\n- - -\n` becomes `\n$(HR)\n`
2297 * Params:
2298 * buf = an OutBuffer containing the DDoc
2299 * i = the index within `buf` of the first character of a potential
2300 * thematic break. If the replacement is made `i` changes to
2301 * point to the closing parenthesis of the `$(HR)` macro.
2302 * iLineStart = the index within `buf` that the thematic break's line starts at
2303 * loc = the current location within the file
2304 * Returns: whether a thematic break was replaced
2306 bool replaceMarkdownThematicBreak(ref OutBuffer buf, ref size_t i, size_t iLineStart, const ref Loc loc)
2309 const slice = buf[];
2310 const c = buf[i];
2311 size_t j = i + 1;
2312 int repeat = 1;
2313 for (; j < slice.length; j++)
2315 if (buf[j] == c)
2316 ++repeat;
2317 else if (buf[j] != ' ' && buf[j] != '\t')
2318 break;
2320 if (repeat >= 3)
2322 if (j >= buf.length || buf[j] == '\n' || buf[j] == '\r')
2324 buf.remove(iLineStart, j - iLineStart);
2325 i = buf.insert(iLineStart, "$(HR)") - 1;
2326 return true;
2329 return false;
2332 /****************************************************
2333 * Detect the level of an ATX-style heading, e.g. `## This is a heading` would
2334 * have a level of `2`.
2335 * Params:
2336 * buf = an OutBuffer containing the DDoc
2337 * i = the index within `buf` of the first `#` character
2338 * Returns:
2339 * the detected heading level from 1 to 6, or
2340 * 0 if not at an ATX heading
2342 int detectAtxHeadingLevel(ref OutBuffer buf, const size_t i) @safe
2344 const iHeadingStart = i;
2345 const iAfterHashes = skipChars(buf, i, "#");
2346 const headingLevel = cast(int) (iAfterHashes - iHeadingStart);
2347 if (headingLevel > 6)
2348 return 0;
2350 const iTextStart = skipChars(buf, iAfterHashes, " \t");
2351 const emptyHeading = buf[iTextStart] == '\r' || buf[iTextStart] == '\n';
2353 // require whitespace
2354 if (!emptyHeading && iTextStart == iAfterHashes)
2355 return 0;
2357 return headingLevel;
2360 /****************************************************
2361 * Remove any trailing `##` suffix from an ATX-style heading.
2362 * Params:
2363 * buf = an OutBuffer containing the DDoc
2364 * i = the index within `buf` to start looking for a suffix at
2366 void removeAnyAtxHeadingSuffix(ref OutBuffer buf, size_t i)
2368 size_t j = i;
2369 size_t iSuffixStart = 0;
2370 size_t iWhitespaceStart = j;
2371 const slice = buf[];
2372 for (; j < slice.length; j++)
2374 switch (slice[j])
2376 case '#':
2377 if (iWhitespaceStart && !iSuffixStart)
2378 iSuffixStart = j;
2379 continue;
2380 case ' ':
2381 case '\t':
2382 if (!iWhitespaceStart)
2383 iWhitespaceStart = j;
2384 continue;
2385 case '\r':
2386 case '\n':
2387 break;
2388 default:
2389 iSuffixStart = 0;
2390 iWhitespaceStart = 0;
2391 continue;
2393 break;
2395 if (iSuffixStart)
2396 buf.remove(iWhitespaceStart, j - iWhitespaceStart);
2399 /****************************************************
2400 * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`).
2401 * Params:
2402 * buf = an OutBuffer containing the DDoc
2403 * iStart = the index within `buf` that the Markdown heading starts at
2404 * iEnd = the index within `buf` of the character after the last
2405 * heading character. Is incremented by the length of the
2406 * inserted heading macro when this function ends.
2407 * loc = the location of the Ddoc within the file
2408 * headingLevel = the level (1-6) of heading to end. Is set to `0` when this
2409 * function ends.
2411 void endMarkdownHeading(ref OutBuffer buf, size_t iStart, ref size_t iEnd, const ref Loc loc, ref int headingLevel)
2413 char[5] heading = "$(H0 ";
2414 heading[3] = cast(char) ('0' + headingLevel);
2415 buf.insert(iStart, heading);
2416 iEnd += 5;
2417 size_t iBeforeNewline = iEnd;
2418 while (buf[iBeforeNewline-1] == '\r' || buf[iBeforeNewline-1] == '\n')
2419 --iBeforeNewline;
2420 buf.insert(iBeforeNewline, ")");
2421 headingLevel = 0;
2424 /****************************************************
2425 * End all nested Markdown quotes, if inside any.
2426 * Params:
2427 * buf = an OutBuffer containing the DDoc
2428 * i = the index within `buf` of the character after the quote text.
2429 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2430 * Returns: the amount that `i` was moved
2432 size_t endAllMarkdownQuotes(ref OutBuffer buf, size_t i, ref int quoteLevel)
2434 const length = quoteLevel;
2435 for (; quoteLevel > 0; --quoteLevel)
2436 i = buf.insert(i, ")");
2437 return length;
2440 /****************************************************
2441 * Convenience function to end all Markdown lists and quotes, if inside any, and
2442 * set `quoteMacroLevel` to `0`.
2443 * Params:
2444 * buf = an OutBuffer containing the DDoc
2445 * i = the index within `buf` of the character after the list and/or
2446 * quote text. Is adjusted when this function ends if any lists
2447 * and/or quotes were ended.
2448 * nestedLists = a set of nested lists. Upon return it will be empty.
2449 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2450 * quoteMacroLevel = the macro level that the quote was started at. Is set to
2451 * `0` when this function ends.
2452 * Returns: the amount that `i` was moved
2454 size_t endAllListsAndQuotes(ref OutBuffer buf, ref size_t i, ref MarkdownList[] nestedLists, ref int quoteLevel, out int quoteMacroLevel)
2456 quoteMacroLevel = 0;
2457 const i0 = i;
2458 i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
2459 i += endAllMarkdownQuotes(buf, i, quoteLevel);
2460 return i - i0;
2463 /****************************************************
2464 * Replace Markdown emphasis with the appropriate macro,
2465 * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`.
2466 * Params:
2467 * buf = an OutBuffer containing the DDoc
2468 * loc = the current location within the file
2469 * inlineDelimiters = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`.
2470 * downToLevel = the length within `inlineDelimiters`` to reduce emphasis to
2471 * Returns: the number of characters added to the buffer by the replacements
2473 size_t replaceMarkdownEmphasis(ref OutBuffer buf, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int downToLevel = 0)
2475 size_t replaceEmphasisPair(ref MarkdownDelimiter start, ref MarkdownDelimiter end)
2477 immutable count = start.count == 1 || end.count == 1 ? 1 : 2;
2479 size_t iStart = start.iStart;
2480 size_t iEnd = end.iStart;
2481 end.count -= count;
2482 start.count -= count;
2483 iStart += start.count;
2485 if (!start.count)
2486 start.type = 0;
2487 if (!end.count)
2488 end.type = 0;
2490 buf.remove(iStart, count);
2491 iEnd -= count;
2492 buf.remove(iEnd, count);
2494 string macroName = count >= 2 ? "$(STRONG " : "$(EM ";
2495 buf.insert(iEnd, ")");
2496 buf.insert(iStart, macroName);
2498 const delta = 1 + macroName.length - (count + count);
2499 end.iStart += count;
2500 return delta;
2503 size_t delta = 0;
2504 int start = (cast(int) inlineDelimiters.length) - 1;
2505 while (start >= downToLevel)
2507 // find start emphasis
2508 while (start >= downToLevel &&
2509 (inlineDelimiters[start].type != '*' || !inlineDelimiters[start].leftFlanking))
2510 --start;
2511 if (start < downToLevel)
2512 break;
2514 // find the nearest end emphasis
2515 int end = start + 1;
2516 while (end < inlineDelimiters.length &&
2517 (inlineDelimiters[end].type != inlineDelimiters[start].type ||
2518 inlineDelimiters[end].macroLevel != inlineDelimiters[start].macroLevel ||
2519 !inlineDelimiters[end].rightFlanking))
2520 ++end;
2521 if (end == inlineDelimiters.length)
2523 // the start emphasis has no matching end; if it isn't an end itself then kill it
2524 if (!inlineDelimiters[start].rightFlanking)
2525 inlineDelimiters[start].type = 0;
2526 --start;
2527 continue;
2530 // multiple-of-3 rule
2531 if (((inlineDelimiters[start].leftFlanking && inlineDelimiters[start].rightFlanking) ||
2532 (inlineDelimiters[end].leftFlanking && inlineDelimiters[end].rightFlanking)) &&
2533 (inlineDelimiters[start].count + inlineDelimiters[end].count) % 3 == 0)
2535 --start;
2536 continue;
2539 immutable delta0 = replaceEmphasisPair(inlineDelimiters[start], inlineDelimiters[end]);
2541 for (; end < inlineDelimiters.length; ++end)
2542 inlineDelimiters[end].iStart += delta0;
2543 delta += delta0;
2546 inlineDelimiters.length = downToLevel;
2547 return delta;
2550 /****************************************************
2552 bool isIdentifier(Dsymbols* a, const(char)[] s) @safe
2554 foreach (member; *a)
2556 if (auto imp = member.isImport())
2558 // For example: `public import str = core.stdc.string;`
2559 // This checks if `s` is equal to `str`
2560 if (imp.aliasId)
2562 if (s == imp.aliasId.toString())
2563 return true;
2565 else
2567 // The general case: `public import core.stdc.string;`
2569 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
2570 string fullyQualifiedImport;
2571 foreach (const pid; imp.packages)
2573 fullyQualifiedImport ~= pid.toString() ~ ".";
2575 fullyQualifiedImport ~= imp.id.toString();
2577 // Check if `s` == `core.stdc.string`
2578 if (s == fullyQualifiedImport)
2579 return true;
2582 else if (member.ident)
2584 if (s == member.ident.toString())
2585 return true;
2589 return false;
2592 /****************************************************
2594 bool isKeyword(const(char)[] str) @safe
2596 immutable string[3] table = ["true", "false", "null"];
2597 foreach (s; table)
2599 if (str == s)
2600 return true;
2602 return false;
2605 /****************************************************
2607 TypeFunction isTypeFunction(Dsymbol s) @safe
2609 FuncDeclaration f = s.isFuncDeclaration();
2610 /* f.type may be NULL for template members.
2612 if (f && f.type)
2614 Type t = f.originalType ? f.originalType : f.type;
2615 if (t.ty == Tfunction)
2616 return cast(TypeFunction)t;
2618 return null;
2621 /****************************************************
2623 Parameter isFunctionParameter(Dsymbol s, const(char)[] str) @safe
2625 TypeFunction tf = isTypeFunction(s);
2626 if (tf && tf.parameterList.parameters)
2628 foreach (fparam; *tf.parameterList.parameters)
2630 if (fparam.ident && str == fparam.ident.toString())
2632 return fparam;
2636 return null;
2639 /****************************************************
2641 Parameter isFunctionParameter(Dsymbols* a, const(char)[] p) @safe
2643 foreach (Dsymbol sym; *a)
2645 Parameter fparam = isFunctionParameter(sym, p);
2646 if (fparam)
2648 return fparam;
2651 return null;
2654 /****************************************************
2656 Parameter isEponymousFunctionParameter(Dsymbols *a, const(char)[] p) @safe
2658 foreach (Dsymbol dsym; *a)
2660 TemplateDeclaration td = dsym.isTemplateDeclaration();
2661 if (td && td.onemember)
2663 /* Case 1: we refer to a template declaration inside the template
2665 /// ...ddoc...
2666 template case1(T) {
2667 void case1(R)() {}
2670 td = td.onemember.isTemplateDeclaration();
2672 if (!td)
2674 /* Case 2: we're an alias to a template declaration
2676 /// ...ddoc...
2677 alias case2 = case1!int;
2679 AliasDeclaration ad = dsym.isAliasDeclaration();
2680 if (ad && ad.aliassym)
2682 td = ad.aliassym.isTemplateDeclaration();
2685 while (td)
2687 Dsymbol sym = getEponymousMember(td);
2688 if (sym)
2690 Parameter fparam = isFunctionParameter(sym, p);
2691 if (fparam)
2693 return fparam;
2696 td = td.overnext;
2699 return null;
2702 /****************************************************
2704 TemplateParameter isTemplateParameter(Dsymbols* a, const(char)* p, size_t len)
2706 for (size_t i = 0; i < a.length; i++)
2708 TemplateDeclaration td = (*a)[i].isTemplateDeclaration();
2709 // Check for the parent, if the current symbol is not a template declaration.
2710 if (!td)
2711 td = getEponymousParent((*a)[i]);
2712 if (td && td.origParameters)
2714 foreach (tp; *td.origParameters)
2716 if (tp.ident && p[0 .. len] == tp.ident.toString())
2718 return tp;
2723 return null;
2726 /****************************************************
2727 * Return true if str is a reserved symbol name
2728 * that starts with a double underscore.
2730 bool isReservedName(const(char)[] str) @safe
2732 immutable string[] table =
2734 "__ctor",
2735 "__dtor",
2736 "__postblit",
2737 "__invariant",
2738 "__unitTest",
2739 "__require",
2740 "__ensure",
2741 "__dollar",
2742 "__ctfe",
2743 "__withSym",
2744 "__result",
2745 "__returnLabel",
2746 "__vptr",
2747 "__monitor",
2748 "__gate",
2749 "__xopEquals",
2750 "__xopCmp",
2751 "__LINE__",
2752 "__FILE__",
2753 "__MODULE__",
2754 "__FUNCTION__",
2755 "__PRETTY_FUNCTION__",
2756 "__DATE__",
2757 "__TIME__",
2758 "__TIMESTAMP__",
2759 "__VENDOR__",
2760 "__VERSION__",
2761 "__EOF__",
2762 "__CXXLIB__",
2763 "__LOCAL_SIZE",
2764 "__entrypoint",
2766 foreach (s; table)
2768 if (str == s)
2769 return true;
2771 return false;
2774 /****************************************************
2775 * A delimiter for Markdown inline content like emphasis and links.
2777 struct MarkdownDelimiter
2779 size_t iStart; /// the index where this delimiter starts
2780 int count; /// the length of this delimeter's start sequence
2781 int macroLevel; /// the count of nested DDoc macros when the delimiter is started
2782 bool leftFlanking; /// whether the delimiter is left-flanking, as defined by the CommonMark spec
2783 bool rightFlanking; /// whether the delimiter is right-flanking, as defined by the CommonMark spec
2784 bool atParagraphStart; /// whether the delimiter is at the start of a paragraph
2785 char type; /// the type of delimiter, defined by its starting character
2787 /// whether this describes a valid delimiter
2788 @property bool isValid() const @safe { return count != 0; }
2790 /// flag this delimiter as invalid
2791 void invalidate() @safe { count = 0; }
2794 /****************************************************
2795 * Info about a Markdown list.
2797 struct MarkdownList
2799 string orderedStart; /// an optional start number--if present then the list starts at this number
2800 size_t iStart; /// the index where the list item starts
2801 size_t iContentStart; /// the index where the content starts after the list delimiter
2802 int delimiterIndent; /// the level of indent the list delimiter starts at
2803 int contentIndent; /// the level of indent the content starts at
2804 int macroLevel; /// the count of nested DDoc macros when the list is started
2805 char type; /// the type of list, defined by its starting character
2807 /// whether this describes a valid list
2808 @property bool isValid() const @safe { return type != type.init; }
2810 /****************************************************
2811 * Try to parse a list item, returning whether successful.
2812 * Params:
2813 * buf = an OutBuffer containing the DDoc
2814 * iLineStart = the index within `buf` of the first character of the line
2815 * i = the index within `buf` of the potential list item
2816 * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded.
2818 static MarkdownList parseItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
2820 if (buf[i] == '+' || buf[i] == '-' || buf[i] == '*')
2821 return parseUnorderedListItem(buf, iLineStart, i);
2822 else
2823 return parseOrderedListItem(buf, iLineStart, i);
2826 /****************************************************
2827 * Return whether the context is at a list item of the same type as this list.
2828 * Params:
2829 * buf = an OutBuffer containing the DDoc
2830 * iLineStart = the index within `buf` of the first character of the line
2831 * i = the index within `buf` of the list item
2832 * Returns: whether `i` is at a list item of the same type as this list
2834 private bool isAtItemInThisList(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
2836 MarkdownList item = (type == '.' || type == ')') ?
2837 parseOrderedListItem(buf, iLineStart, i) :
2838 parseUnorderedListItem(buf, iLineStart, i);
2839 if (item.type == type)
2840 return item.delimiterIndent < contentIndent && item.contentIndent > delimiterIndent;
2841 return false;
2844 /****************************************************
2845 * Start a Markdown list item by creating/deleting nested lists and starting the item.
2846 * Params:
2847 * buf = an OutBuffer containing the DDoc
2848 * iLineStart = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`.
2849 * i = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro.
2850 * 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`.
2851 * nestedLists = a set of nested lists. If this function succeeds it may contain a new nested list.
2852 * loc = the location of the Ddoc within the file
2853 * Returns: `true` if a list was created
2855 bool startItem(ref OutBuffer buf, ref size_t iLineStart, ref size_t i, ref size_t iPrecedingBlankLine, ref MarkdownList[] nestedLists, const ref Loc loc)
2857 buf.remove(iStart, iContentStart - iStart);
2859 if (!nestedLists.length ||
2860 delimiterIndent >= nestedLists[$-1].contentIndent ||
2861 buf[iLineStart - 4..iLineStart] == "$(LI")
2863 // start a list macro
2864 nestedLists ~= this;
2865 if (type == '.')
2867 if (orderedStart.length)
2869 iStart = buf.insert(iStart, "$(OL_START ");
2870 iStart = buf.insert(iStart, orderedStart);
2871 iStart = buf.insert(iStart, ",\n");
2873 else
2874 iStart = buf.insert(iStart, "$(OL\n");
2876 else
2877 iStart = buf.insert(iStart, "$(UL\n");
2879 removeBlankLineMacro(buf, iPrecedingBlankLine, iStart);
2881 else if (nestedLists.length)
2883 nestedLists[$-1].delimiterIndent = delimiterIndent;
2884 nestedLists[$-1].contentIndent = contentIndent;
2887 iStart = buf.insert(iStart, "$(LI\n");
2888 i = iStart - 1;
2889 iLineStart = i;
2891 return true;
2894 /****************************************************
2895 * End all nested Markdown lists.
2896 * Params:
2897 * buf = an OutBuffer containing the DDoc
2898 * i = the index within `buf` to end lists at.
2899 * nestedLists = a set of nested lists. Upon return it will be empty.
2900 * Returns: the amount that `i` changed
2902 static size_t endAllNestedLists(ref OutBuffer buf, size_t i, ref MarkdownList[] nestedLists)
2904 const iStart = i;
2905 for (; nestedLists.length; --nestedLists.length)
2906 i = buf.insert(i, ")\n)");
2907 return i - iStart;
2910 /****************************************************
2911 * Look for a sibling list item or the end of nested list(s).
2912 * Params:
2913 * buf = an OutBuffer containing the DDoc
2914 * 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.
2915 * iParagraphStart = the index within `buf` to start the next paragraph at at. May be adjusted upon return.
2916 * nestedLists = a set of nested lists. Some nested lists may have been removed from it upon return.
2918 static void handleSiblingOrEndingList(ref OutBuffer buf, ref size_t i, ref size_t iParagraphStart, ref MarkdownList[] nestedLists)
2920 size_t iAfterSpaces = skipChars(buf, i + 1, " \t");
2922 if (nestedLists[$-1].isAtItemInThisList(buf, i + 1, iAfterSpaces))
2924 // end a sibling list item
2925 i = buf.insert(i, ")");
2926 iParagraphStart = skipChars(buf, i, " \t\r\n");
2928 else if (iAfterSpaces >= buf.length || (buf[iAfterSpaces] != '\r' && buf[iAfterSpaces] != '\n'))
2930 // end nested lists that are indented more than this content
2931 const indent = getMarkdownIndent(buf, i + 1, iAfterSpaces);
2932 while (nestedLists.length && nestedLists[$-1].contentIndent > indent)
2934 i = buf.insert(i, ")\n)");
2935 --nestedLists.length;
2936 iParagraphStart = skipChars(buf, i, " \t\r\n");
2938 if (nestedLists.length && nestedLists[$-1].isAtItemInThisList(buf, i + 1, iParagraphStart))
2940 i = buf.insert(i, ")");
2941 ++iParagraphStart;
2942 break;
2948 /****************************************************
2949 * Parse an unordered list item at the current position
2950 * Params:
2951 * buf = an OutBuffer containing the DDoc
2952 * iLineStart = the index within `buf` of the first character of the line
2953 * i = the index within `buf` of the list item
2954 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2956 private static MarkdownList parseUnorderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
2958 if (i+1 < buf.length &&
2959 (buf[i] == '-' ||
2960 buf[i] == '*' ||
2961 buf[i] == '+') &&
2962 (buf[i+1] == ' ' ||
2963 buf[i+1] == '\t' ||
2964 buf[i+1] == '\r' ||
2965 buf[i+1] == '\n'))
2967 const iContentStart = skipChars(buf, i + 1, " \t");
2968 const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
2969 const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
2970 auto list = MarkdownList(null, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[i]);
2971 return list;
2973 return MarkdownList();
2976 /****************************************************
2977 * Parse an ordered list item at the current position
2978 * Params:
2979 * buf = an OutBuffer containing the DDoc
2980 * iLineStart = the index within `buf` of the first character of the line
2981 * i = the index within `buf` of the list item
2982 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2984 private static MarkdownList parseOrderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) @safe
2986 size_t iAfterNumbers = skipChars(buf, i, "0123456789");
2987 if (iAfterNumbers - i > 0 &&
2988 iAfterNumbers - i <= 9 &&
2989 iAfterNumbers + 1 < buf.length &&
2990 buf[iAfterNumbers] == '.' &&
2991 (buf[iAfterNumbers+1] == ' ' ||
2992 buf[iAfterNumbers+1] == '\t' ||
2993 buf[iAfterNumbers+1] == '\r' ||
2994 buf[iAfterNumbers+1] == '\n'))
2996 const iContentStart = skipChars(buf, iAfterNumbers + 1, " \t");
2997 const delimiterIndent = getMarkdownIndent(buf, iLineStart, i);
2998 const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart);
2999 size_t iNumberStart = skipChars(buf, i, "0");
3000 if (iNumberStart == iAfterNumbers)
3001 --iNumberStart;
3002 auto orderedStart = buf[][iNumberStart .. iAfterNumbers];
3003 if (orderedStart == "1")
3004 orderedStart = null;
3005 return MarkdownList(orderedStart.idup, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[iAfterNumbers]);
3007 return MarkdownList();
3011 /****************************************************
3012 * A Markdown link.
3014 struct MarkdownLink
3016 string href; /// the link destination
3017 string title; /// an optional title for the link
3018 string label; /// an optional label for the link
3019 Dsymbol symbol; /// an optional symbol to link to
3021 /****************************************************
3022 * Replace a Markdown link or link definition in the form of:
3023 * - Inline link: `[foo](url/ 'optional title')`
3024 * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]`
3025 * - Link reference definition: `[bar]: url/ 'optional title'`
3026 * Params:
3027 * buf = an OutBuffer containing the DDoc
3028 * i = the index within `buf` that points to the `]` character of the potential link.
3029 * If this function succeeds it will be adjusted to fit the inserted link macro.
3030 * loc = the current location within the file
3031 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3032 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3033 * linkReferences = previously parsed link references. When this function returns it may contain
3034 * additional previously unparsed references.
3035 * Returns: whether a reference link was found and replaced at `i`
3037 static bool replaceLink(ref OutBuffer buf, ref size_t i, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences)
3039 const delimiter = inlineDelimiters[delimiterIndex];
3040 MarkdownLink link;
3042 size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
3043 if (iEnd > i)
3045 i = delimiter.iStart;
3046 link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
3047 inlineDelimiters.length = delimiterIndex;
3048 return true;
3051 iEnd = link.parseInlineLink(buf, i);
3052 if (iEnd == i)
3054 iEnd = link.parseReferenceLink(buf, i, delimiter);
3055 if (iEnd > i)
3057 const label = link.label;
3058 link = linkReferences.lookupReference(label, buf, i, loc);
3059 // check rightFlanking to avoid replacing things like int[string]
3060 if (!link.href.length && !delimiter.rightFlanking)
3061 link = linkReferences.lookupSymbol(label);
3062 if (!link.href.length)
3063 return false;
3067 if (iEnd == i)
3068 return false;
3070 immutable delta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, delimiterIndex);
3071 iEnd += delta;
3072 i += delta;
3073 link.replaceLink(buf, i, iEnd, delimiter);
3074 return true;
3077 /****************************************************
3078 * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'`
3079 * Params:
3080 * buf = an OutBuffer containing the DDoc
3081 * i = the index within `buf` that points to the `]` character of the potential link.
3082 * If this function succeeds it will be adjusted to fit the inserted link macro.
3083 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3084 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3085 * linkReferences = previously parsed link references. When this function returns it may contain
3086 * additional previously unparsed references.
3087 * loc = the current location in the file
3088 * Returns: whether a reference link was found and replaced at `i`
3090 static bool replaceReferenceDefinition(ref OutBuffer buf, ref size_t i, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
3092 const delimiter = inlineDelimiters[delimiterIndex];
3093 MarkdownLink link;
3094 size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter);
3095 if (iEnd == i)
3096 return false;
3098 i = delimiter.iStart;
3099 link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc);
3100 inlineDelimiters.length = delimiterIndex;
3101 return true;
3104 /****************************************************
3105 * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')`
3106 * Params:
3107 * buf = an OutBuffer containing the DDoc
3108 * i = the index within `buf` that points to the `]` character of the inline link.
3109 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3111 private size_t parseInlineLink(ref OutBuffer buf, size_t i)
3113 size_t iEnd = i + 1;
3114 if (iEnd >= buf.length || buf[iEnd] != '(')
3115 return i;
3116 ++iEnd;
3118 if (!parseHref(buf, iEnd))
3119 return i;
3121 iEnd = skipChars(buf, iEnd, " \t\r\n");
3122 if (buf[iEnd] != ')')
3124 if (parseTitle(buf, iEnd))
3125 iEnd = skipChars(buf, iEnd, " \t\r\n");
3128 if (buf[iEnd] != ')')
3129 return i;
3131 return iEnd + 1;
3134 /****************************************************
3135 * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]`
3136 * Params:
3137 * buf = an OutBuffer containing the DDoc
3138 * i = the index within `buf` that points to the `]` character of the inline link.
3139 * delimiter = the delimiter that starts this link
3140 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3142 private size_t parseReferenceLink(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) @safe
3144 size_t iStart = i + 1;
3145 size_t iEnd = iStart;
3146 if (iEnd >= buf.length || buf[iEnd] != '[' || (iEnd+1 < buf.length && buf[iEnd+1] == ']'))
3148 // collapsed reference [foo][] or shortcut reference [foo]
3149 iStart = delimiter.iStart + delimiter.count - 1;
3150 if (buf[iEnd] == '[')
3151 iEnd += 2;
3154 parseLabel(buf, iStart);
3155 if (!label.length)
3156 return i;
3158 if (iEnd < iStart)
3159 iEnd = iStart;
3160 return iEnd;
3163 /****************************************************
3164 * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'`
3165 * Params:
3166 * buf = an OutBuffer containing the DDoc
3167 * i = the index within `buf` that points to the `]` character of the inline link.
3168 * delimiter = the delimiter that starts this link
3169 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3171 private size_t parseReferenceDefinition(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter)
3173 if (!delimiter.atParagraphStart || delimiter.type != '[' ||
3174 i+1 >= buf.length || buf[i+1] != ':')
3175 return i;
3177 size_t iEnd = delimiter.iStart;
3178 parseLabel(buf, iEnd);
3179 if (label.length == 0 || iEnd != i + 1)
3180 return i;
3182 ++iEnd;
3183 iEnd = skipChars(buf, iEnd, " \t");
3184 skipOneNewline(buf, iEnd);
3186 if (!parseHref(buf, iEnd) || href.length == 0)
3187 return i;
3189 iEnd = skipChars(buf, iEnd, " \t");
3190 const requireNewline = !skipOneNewline(buf, iEnd);
3191 const iBeforeTitle = iEnd;
3193 if (parseTitle(buf, iEnd))
3195 iEnd = skipChars(buf, iEnd, " \t");
3196 if (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n')
3198 // the title must end with a newline
3199 title.length = 0;
3200 iEnd = iBeforeTitle;
3204 iEnd = skipChars(buf, iEnd, " \t");
3205 if (requireNewline && iEnd < buf.length-1 && buf[iEnd] != '\r' && buf[iEnd] != '\n')
3206 return i;
3208 return iEnd;
3211 /****************************************************
3212 * Parse and normalize a Markdown reference label
3213 * Params:
3214 * buf = an OutBuffer containing the DDoc
3215 * i = the index within `buf` that points to the `[` character at the start of the label.
3216 * If this function returns a non-empty label then `i` will point just after the ']' at the end of the label.
3217 * Returns: the parsed and normalized label, possibly empty
3219 private bool parseLabel(ref OutBuffer buf, ref size_t i) @safe
3221 if (buf[i] != '[')
3222 return false;
3224 const slice = buf[];
3225 size_t j = i + 1;
3227 // Some labels have already been en-symboled; handle that
3228 const inSymbol = j+15 < slice.length && slice[j..j+15] == "$(DDOC_PSYMBOL ";
3229 if (inSymbol)
3230 j += 15;
3232 for (; j < slice.length; ++j)
3234 const c = slice[j];
3235 switch (c)
3237 case ' ':
3238 case '\t':
3239 case '\r':
3240 case '\n':
3241 if (label.length && label[$-1] != ' ')
3242 label ~= ' ';
3243 break;
3244 case ')':
3245 if (inSymbol && j+1 < slice.length && slice[j+1] == ']')
3247 ++j;
3248 goto case ']';
3250 goto default;
3251 case '[':
3252 if (slice[j-1] != '\\')
3254 label.length = 0;
3255 return false;
3257 break;
3258 case ']':
3259 if (label.length && label[$-1] == ' ')
3260 --label.length;
3261 if (label.length)
3263 i = j + 1;
3264 return true;
3266 return false;
3267 default:
3268 label ~= c;
3269 break;
3272 label.length = 0;
3273 return false;
3276 /****************************************************
3277 * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets
3278 * Params:
3279 * buf = an OutBuffer containing the DDoc
3280 * i = the index within `buf` that points to the first character of the URL.
3281 * If this function succeeds `i` will point just after the end of the URL.
3282 * Returns: whether a URL was found and parsed
3284 private bool parseHref(ref OutBuffer buf, ref size_t i)
3286 size_t j = skipChars(buf, i, " \t");
3288 size_t iHrefStart = j;
3289 size_t parenDepth = 1;
3290 bool inPointy = false;
3291 const slice = buf[];
3292 for (; j < slice.length; j++)
3294 switch (slice[j])
3296 case '<':
3297 if (!inPointy && j == iHrefStart)
3299 inPointy = true;
3300 ++iHrefStart;
3302 break;
3303 case '>':
3304 if (inPointy && slice[j-1] != '\\')
3305 goto LReturnHref;
3306 break;
3307 case '(':
3308 if (!inPointy && slice[j-1] != '\\')
3309 ++parenDepth;
3310 break;
3311 case ')':
3312 if (!inPointy && slice[j-1] != '\\')
3314 --parenDepth;
3315 if (!parenDepth)
3316 goto LReturnHref;
3318 break;
3319 case ' ':
3320 case '\t':
3321 case '\r':
3322 case '\n':
3323 if (inPointy)
3325 // invalid link
3326 return false;
3328 goto LReturnHref;
3329 default:
3330 break;
3333 if (inPointy)
3334 return false;
3335 LReturnHref:
3336 auto href = slice[iHrefStart .. j].dup;
3337 this.href = cast(string) percentEncode(removeEscapeBackslashes(href)).replaceChar(',', "$(COMMA)");
3338 i = j;
3339 if (inPointy)
3340 ++i;
3341 return true;
3344 /****************************************************
3345 * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes
3346 * Params:
3347 * buf = an OutBuffer containing the DDoc
3348 * i = the index within `buf` that points to the first character of the title.
3349 * If this function succeeds `i` will point just after the end of the title.
3350 * Returns: whether a title was found and parsed
3352 private bool parseTitle(ref OutBuffer buf, ref size_t i)
3354 size_t j = skipChars(buf, i, " \t");
3355 if (j >= buf.length)
3356 return false;
3358 char type = buf[j];
3359 if (type != '"' && type != '\'' && type != '(')
3360 return false;
3361 if (type == '(')
3362 type = ')';
3364 const iTitleStart = j + 1;
3365 size_t iNewline = 0;
3366 const slice = buf[];
3367 for (j = iTitleStart; j < slice.length; j++)
3369 const c = slice[j];
3370 switch (c)
3372 case ')':
3373 case '"':
3374 case '\'':
3375 if (type == c && slice[j-1] != '\\')
3376 goto LEndTitle;
3377 iNewline = 0;
3378 break;
3379 case ' ':
3380 case '\t':
3381 case '\r':
3382 break;
3383 case '\n':
3384 if (iNewline)
3386 // no blank lines in titles
3387 return false;
3389 iNewline = j;
3390 break;
3391 default:
3392 iNewline = 0;
3393 break;
3396 return false;
3397 LEndTitle:
3398 auto title = slice[iTitleStart .. j].dup;
3399 this.title = cast(string) removeEscapeBackslashes(title).
3400 replaceChar(',', "$(COMMA)").
3401 replaceChar('"', "$(QUOTE)");
3402 i = j + 1;
3403 return true;
3406 /****************************************************
3407 * Replace a Markdown link or image with the appropriate macro
3408 * Params:
3409 * buf = an OutBuffer containing the DDoc
3410 * i = the index within `buf` that points to the `]` character of the inline link.
3411 * When this function returns it will be adjusted to the end of the inserted macro.
3412 * iLinkEnd = the index within `buf` that points just after the last character of the link
3413 * delimiter = the Markdown delimiter that started the link or image
3415 private void replaceLink(ref OutBuffer buf, ref size_t i, size_t iLinkEnd, MarkdownDelimiter delimiter)
3417 size_t iAfterLink = i - delimiter.count;
3418 string macroName;
3419 if (symbol)
3421 macroName = "$(SYMBOL_LINK ";
3423 else if (title.length)
3425 if (delimiter.type == '[')
3426 macroName = "$(LINK_TITLE ";
3427 else
3428 macroName = "$(IMAGE_TITLE ";
3430 else
3432 if (delimiter.type == '[')
3433 macroName = "$(LINK2 ";
3434 else
3435 macroName = "$(IMAGE ";
3437 buf.remove(delimiter.iStart, delimiter.count);
3438 buf.remove(i - delimiter.count, iLinkEnd - i);
3439 iLinkEnd = buf.insert(delimiter.iStart, macroName);
3440 iLinkEnd = buf.insert(iLinkEnd, href);
3441 iLinkEnd = buf.insert(iLinkEnd, ", ");
3442 iAfterLink += macroName.length + href.length + 2;
3443 if (title.length)
3445 iLinkEnd = buf.insert(iLinkEnd, title);
3446 iLinkEnd = buf.insert(iLinkEnd, ", ");
3447 iAfterLink += title.length + 2;
3449 // Link macros with titles require escaping commas
3450 for (size_t j = iLinkEnd; j < iAfterLink; ++j)
3451 if (buf[j] == ',')
3453 buf.remove(j, 1);
3454 j = buf.insert(j, "$(COMMA)") - 1;
3455 iAfterLink += 7;
3458 // TODO: if image, remove internal macros, leaving only text
3459 buf.insert(iAfterLink, ")");
3460 i = iAfterLink;
3463 /****************************************************
3464 * Store the Markdown link definition and remove it from `buf`
3465 * Params:
3466 * buf = an OutBuffer containing the DDoc
3467 * i = the index within `buf` that points to the `[` character at the start of the link definition.
3468 * When this function returns it will be adjusted to exclude the link definition.
3469 * iEnd = the index within `buf` that points just after the end of the definition
3470 * linkReferences = previously parsed link references. When this function returns it may contain
3471 * an additional reference.
3472 * loc = the current location in the file
3474 private void storeAndReplaceDefinition(ref OutBuffer buf, ref size_t i, size_t iEnd, ref MarkdownLinkReferences linkReferences, const ref Loc loc)
3476 // Remove the definition and trailing whitespace
3477 iEnd = skipChars(buf, iEnd, " \t\r\n");
3478 buf.remove(i, iEnd - i);
3479 i -= 2;
3481 string lowercaseLabel = label.toLowercase();
3482 if (lowercaseLabel !in linkReferences.references)
3483 linkReferences.references[lowercaseLabel] = this;
3486 /****************************************************
3487 * Remove Markdown escaping backslashes from the given string
3488 * Params:
3489 * s = the string to remove escaping backslashes from
3490 * Returns: `s` without escaping backslashes in it
3492 private static char[] removeEscapeBackslashes(char[] s) @safe
3494 if (!s.length)
3495 return s;
3497 // avoid doing anything if there isn't anything to escape
3498 size_t i;
3499 for (i = 0; i < s.length-1; ++i)
3500 if (s[i] == '\\' && ispunct(s[i+1]))
3501 break;
3502 if (i == s.length-1)
3503 return s;
3505 // copy characters backwards, then truncate
3506 size_t j = i + 1;
3507 s[i] = s[j];
3508 for (++i, ++j; j < s.length; ++i, ++j)
3510 if (j < s.length-1 && s[j] == '\\' && ispunct(s[j+1]))
3511 ++j;
3512 s[i] = s[j];
3514 s.length -= (j - i);
3515 return s;
3519 unittest
3521 assert(removeEscapeBackslashes("".dup) == "");
3522 assert(removeEscapeBackslashes(`\a`.dup) == `\a`);
3523 assert(removeEscapeBackslashes(`.\`.dup) == `.\`);
3524 assert(removeEscapeBackslashes(`\.\`.dup) == `.\`);
3525 assert(removeEscapeBackslashes(`\.`.dup) == `.`);
3526 assert(removeEscapeBackslashes(`\.\.`.dup) == `..`);
3527 assert(removeEscapeBackslashes(`a\.b\.c`.dup) == `a.b.c`);
3530 /****************************************************
3531 * Percent-encode (AKA URL-encode) the given string
3532 * Params:
3533 * s = the string to percent-encode
3534 * Returns: `s` with special characters percent-encoded
3536 private static inout(char)[] percentEncode(inout(char)[] s) pure @safe
3538 static bool shouldEncode(char c)
3540 return ((c < '0' && c != '!' && c != '#' && c != '$' && c != '%' && c != '&' && c != '\'' && c != '(' &&
3541 c != ')' && c != '*' && c != '+' && c != ',' && c != '-' && c != '.' && c != '/')
3542 || (c > '9' && c < 'A' && c != ':' && c != ';' && c != '=' && c != '?' && c != '@')
3543 || (c > 'Z' && c < 'a' && c != '[' && c != ']' && c != '_')
3544 || (c > 'z' && c != '~'));
3547 for (size_t i = 0; i < s.length; ++i)
3549 if (shouldEncode(s[i]))
3551 immutable static hexDigits = "0123456789ABCDEF";
3552 immutable encoded1 = hexDigits[s[i] >> 4];
3553 immutable encoded2 = hexDigits[s[i] & 0x0F];
3554 s = s[0..i] ~ '%' ~ encoded1 ~ encoded2 ~ s[i+1..$];
3555 i += 2;
3558 return s;
3562 unittest
3564 assert(percentEncode("") == "");
3565 assert(percentEncode("aB12-._~/?") == "aB12-._~/?");
3566 assert(percentEncode("<\n>") == "%3C%0A%3E");
3569 /**************************************************
3570 * Skip a single newline at `i`
3571 * Params:
3572 * buf = an OutBuffer containing the DDoc
3573 * i = the index within `buf` to start looking at.
3574 * If this function succeeds `i` will point after the newline.
3575 * Returns: whether a newline was skipped
3577 private static bool skipOneNewline(ref OutBuffer buf, ref size_t i) pure @safe
3579 if (i < buf.length && buf[i] == '\r')
3580 ++i;
3581 if (i < buf.length && buf[i] == '\n')
3583 ++i;
3584 return true;
3586 return false;
3590 /**************************************************
3591 * A set of Markdown link references.
3593 struct MarkdownLinkReferences
3595 MarkdownLink[string] references; // link references keyed by normalized label
3596 MarkdownLink[string] symbols; // link symbols keyed by name
3597 Scope* _scope; // the current scope
3598 bool extractedAll; // the index into the buffer of the last-parsed reference
3600 /**************************************************
3601 * Look up a reference by label, searching through the rest of the buffer if needed.
3602 * Symbols in the current scope are searched for if the DDoc doesn't define the reference.
3603 * Params:
3604 * label = the label to find the reference for
3605 * buf = an OutBuffer containing the DDoc
3606 * i = the index within `buf` to start searching for references at
3607 * loc = the current location in the file
3608 * Returns: a link. If the `href` member has a value then the reference is valid.
3610 MarkdownLink lookupReference(string label, ref OutBuffer buf, size_t i, const ref Loc loc)
3612 const lowercaseLabel = label.toLowercase();
3613 if (lowercaseLabel !in references)
3614 extractReferences(buf, i, loc);
3616 if (lowercaseLabel in references)
3617 return references[lowercaseLabel];
3619 return MarkdownLink();
3623 * Look up the link for the D symbol with the given name.
3624 * If found, the link is cached in the `symbols` member.
3625 * Params:
3626 * name = the name of the symbol
3627 * Returns: the link for the symbol or a link with a `null` href
3629 MarkdownLink lookupSymbol(string name)
3631 if (name in symbols)
3632 return symbols[name];
3634 const ids = split(name, '.');
3636 MarkdownLink link;
3637 auto id = Identifier.lookup(ids[0].ptr, ids[0].length);
3638 if (id)
3640 auto loc = Loc();
3641 Dsymbol pscopesym;
3642 auto symbol = _scope.search(loc, id, pscopesym, SearchOpt.ignoreErrors);
3643 for (size_t i = 1; symbol && i < ids.length; ++i)
3645 id = Identifier.lookup(ids[i].ptr, ids[i].length);
3646 symbol = id !is null ? symbol.search(loc, id, SearchOpt.ignoreErrors) : null;
3648 if (symbol)
3649 link = MarkdownLink(createHref(symbol), null, name, symbol);
3652 symbols[name] = link;
3653 return link;
3656 /**************************************************
3657 * Remove and store all link references from the document, in the form of
3658 * `[label]: href "optional title"`
3659 * Params:
3660 * buf = an OutBuffer containing the DDoc
3661 * i = the index within `buf` to start looking at
3662 * loc = the current location in the file
3663 * Returns: whether a reference was extracted
3665 private void extractReferences(ref OutBuffer buf, size_t i, const ref Loc loc)
3667 static bool isFollowedBySpace(ref OutBuffer buf, size_t i)
3669 return i+1 < buf.length && (buf[i+1] == ' ' || buf[i+1] == '\t');
3672 if (extractedAll)
3673 return;
3675 bool leadingBlank = false;
3676 int inCode = false;
3677 bool newParagraph = true;
3678 MarkdownDelimiter[] delimiters;
3679 for (; i < buf.length; ++i)
3681 const c = buf[i];
3682 switch (c)
3684 case ' ':
3685 case '\t':
3686 break;
3687 case '\n':
3688 if (leadingBlank && !inCode)
3689 newParagraph = true;
3690 leadingBlank = true;
3691 break;
3692 case '\\':
3693 ++i;
3694 break;
3695 case '#':
3696 if (leadingBlank && !inCode)
3697 newParagraph = true;
3698 leadingBlank = false;
3699 break;
3700 case '>':
3701 if (leadingBlank && !inCode)
3702 newParagraph = true;
3703 break;
3704 case '+':
3705 if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
3706 newParagraph = true;
3707 else
3708 leadingBlank = false;
3709 break;
3710 case '0':
3712 case '9':
3713 if (leadingBlank && !inCode)
3715 i = skipChars(buf, i, "0123456789");
3716 if (i < buf.length &&
3717 (buf[i] == '.' || buf[i] == ')') &&
3718 isFollowedBySpace(buf, i))
3719 newParagraph = true;
3720 else
3721 leadingBlank = false;
3723 break;
3724 case '*':
3725 if (leadingBlank && !inCode)
3727 newParagraph = true;
3728 if (!isFollowedBySpace(buf, i))
3729 leadingBlank = false;
3731 break;
3732 case '`':
3733 case '~':
3734 if (leadingBlank && i+2 < buf.length && buf[i+1] == c && buf[i+2] == c)
3736 inCode = inCode == c ? false : c;
3737 i = skipChars(buf, i, [c]) - 1;
3738 newParagraph = true;
3740 leadingBlank = false;
3741 break;
3742 case '-':
3743 if (leadingBlank && !inCode && isFollowedBySpace(buf, i))
3744 goto case '+';
3745 else
3746 goto case '`';
3747 case '[':
3748 if (leadingBlank && !inCode && newParagraph)
3749 delimiters ~= MarkdownDelimiter(i, 1, 0, false, false, true, c);
3750 break;
3751 case ']':
3752 if (delimiters.length && !inCode &&
3753 MarkdownLink.replaceReferenceDefinition(buf, i, delimiters, cast(int) delimiters.length - 1, this, loc))
3754 --i;
3755 break;
3756 default:
3757 if (leadingBlank)
3758 newParagraph = false;
3759 leadingBlank = false;
3760 break;
3763 extractedAll = true;
3767 * Split a string by a delimiter, excluding the delimiter.
3768 * Params:
3769 * s = the string to split
3770 * delimiter = the character to split by
3771 * Returns: the resulting array of strings
3773 private static string[] split(string s, char delimiter) pure @safe
3775 string[] result;
3776 size_t iStart = 0;
3777 foreach (size_t i; 0..s.length)
3778 if (s[i] == delimiter)
3780 result ~= s[iStart..i];
3781 iStart = i + 1;
3783 result ~= s[iStart..$];
3784 return result;
3788 unittest
3790 assert(split("", ',') == [""]);
3791 assert(split("ab", ',') == ["ab"]);
3792 assert(split("a,b", ',') == ["a", "b"]);
3793 assert(split("a,,b", ',') == ["a", "", "b"]);
3794 assert(split(",ab", ',') == ["", "ab"]);
3795 assert(split("ab,", ',') == ["ab", ""]);
3799 * Create a HREF for the given D symbol.
3800 * The HREF is relative to the current location if possible.
3801 * Params:
3802 * symbol = the symbol to create a HREF for.
3803 * Returns: the resulting href
3805 private string createHref(Dsymbol symbol)
3807 Dsymbol root = symbol;
3809 const(char)[] lref;
3810 while (symbol && symbol.ident && !symbol.isModule())
3812 if (lref.length)
3813 lref = '.' ~ lref;
3814 lref = symbol.ident.toString() ~ lref;
3815 symbol = symbol.parent;
3818 const(char)[] path;
3819 if (symbol && symbol.ident && symbol.isModule() != _scope._module)
3823 root = symbol;
3825 // If the module has a file name, we're done
3826 if (const m = symbol.isModule())
3827 if (m.docfile)
3829 path = m.docfile.toString();
3830 break;
3833 if (path.length)
3834 path = '_' ~ path;
3835 path = symbol.ident.toString() ~ path;
3836 symbol = symbol.parent;
3837 } while (symbol && symbol.ident);
3839 if (!symbol && path.length)
3840 path ~= "$(DOC_EXTENSION)";
3843 // Attempt an absolute URL if not in the same package
3844 while (root.parent)
3845 root = root.parent;
3846 Dsymbol scopeRoot = _scope._module;
3847 while (scopeRoot.parent)
3848 scopeRoot = scopeRoot.parent;
3849 if (scopeRoot != root)
3851 path = "$(DOC_ROOT_" ~ root.ident.toString() ~ ')' ~ path;
3852 lref = '.' ~ lref; // remote URIs like Phobos and Mir use .prefixes
3855 return cast(string) (path ~ '#' ~ lref);
3859 enum TableColumnAlignment
3861 none,
3862 left,
3863 center,
3864 right
3867 /****************************************************
3868 * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |`
3869 * where the example text has four columns with the following alignments:
3870 * default, left, center, and right. The first and last pipes are optional. If a
3871 * delimiter row is found it will be removed from `buf`.
3873 * Params:
3874 * buf = an OutBuffer containing the DDoc
3875 * iStart = the index within `buf` that the delimiter row starts at
3876 * inQuote = whether the table is inside a quote
3877 * columnAlignments = alignments to populate for each column
3878 * Returns: the index of the end of the parsed delimiter, or `0` if not found
3880 size_t parseTableDelimiterRow(ref OutBuffer buf, const size_t iStart, bool inQuote, ref TableColumnAlignment[] columnAlignments) @safe
3882 size_t i = skipChars(buf, iStart, inQuote ? ">| \t" : "| \t");
3883 while (i < buf.length && buf[i] != '\r' && buf[i] != '\n')
3885 const leftColon = buf[i] == ':';
3886 if (leftColon)
3887 ++i;
3889 if (i >= buf.length || buf[i] != '-')
3890 break;
3891 i = skipChars(buf, i, "-");
3893 const rightColon = i < buf.length && buf[i] == ':';
3894 i = skipChars(buf, i, ": \t");
3896 if (i >= buf.length || (buf[i] != '|' && buf[i] != '\r' && buf[i] != '\n'))
3897 break;
3898 i = skipChars(buf, i, "| \t");
3900 columnAlignments ~= (leftColon && rightColon) ? TableColumnAlignment.center :
3901 leftColon ? TableColumnAlignment.left :
3902 rightColon ? TableColumnAlignment.right :
3903 TableColumnAlignment.none;
3906 if (i < buf.length && buf[i] != '\r' && buf[i] != '\n' && buf[i] != ')')
3908 columnAlignments.length = 0;
3909 return 0;
3912 if (i < buf.length && buf[i] == '\r') ++i;
3913 if (i < buf.length && buf[i] == '\n') ++i;
3914 return i;
3917 /****************************************************
3918 * Look for a table delimiter row, and if found parse the previous row as a
3919 * table header row. If both exist with a matching number of columns, start a
3920 * table.
3922 * Params:
3923 * buf = an OutBuffer containing the DDoc
3924 * iStart = the index within `buf` that the table header row starts at, inclusive
3925 * iEnd = the index within `buf` that the table header row ends at, exclusive
3926 * loc = the current location in the file
3927 * inQuote = whether the table is inside a quote
3928 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3929 * columnAlignments = the parsed alignments for each column
3930 * Returns: the number of characters added by starting the table, or `0` if unchanged
3932 size_t startTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, bool inQuote, ref MarkdownDelimiter[] inlineDelimiters, out TableColumnAlignment[] columnAlignments)
3934 const iDelimiterRowEnd = parseTableDelimiterRow(buf, iEnd + 1, inQuote, columnAlignments);
3935 if (iDelimiterRowEnd)
3937 size_t delta;
3938 if (replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, true, delta))
3940 buf.remove(iEnd + delta, iDelimiterRowEnd - iEnd);
3941 buf.insert(iEnd + delta, "$(TBODY ");
3942 buf.insert(iStart, "$(TABLE ");
3943 return delta + 15;
3947 columnAlignments.length = 0;
3948 return 0;
3951 /****************************************************
3952 * Replace a Markdown table row in the form of table cells delimited by pipes:
3953 * `| cell | cell | cell`. The first and last pipes are optional.
3955 * Params:
3956 * buf = an OutBuffer containing the DDoc
3957 * iStart = the index within `buf` that the table row starts at, inclusive
3958 * iEnd = the index within `buf` that the table row ends at, exclusive
3959 * loc = the current location in the file
3960 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3961 * columnAlignments = alignments for each column
3962 * headerRow = if `true` then the number of columns will be enforced to match
3963 * `columnAlignments.length` and the row will be surrounded by a
3964 * `THEAD` macro
3965 * delta = the number of characters added by replacing the row, or `0` if unchanged
3966 * Returns: `true` if a table row was found and replaced
3968 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)
3970 delta = 0;
3972 if (!columnAlignments.length || iStart == iEnd)
3973 return false;
3975 iStart = skipChars(buf, iStart, " \t");
3976 int cellCount = 0;
3977 foreach (delimiter; inlineDelimiters)
3978 if (delimiter.type == '|' && !delimiter.leftFlanking)
3979 ++cellCount;
3980 bool ignoreLast = inlineDelimiters.length > 0 && inlineDelimiters[$-1].type == '|';
3981 if (ignoreLast)
3983 const iLast = skipChars(buf, inlineDelimiters[$-1].iStart + inlineDelimiters[$-1].count, " \t");
3984 ignoreLast = iLast >= iEnd;
3986 if (!ignoreLast)
3987 ++cellCount;
3989 if (headerRow && cellCount != columnAlignments.length)
3990 return false;
3992 void replaceTableCell(size_t iCellStart, size_t iCellEnd, int cellIndex, int di)
3994 const eDelta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, di);
3995 delta += eDelta;
3996 iCellEnd += eDelta;
3998 // strip trailing whitespace and delimiter
3999 size_t i = iCellEnd - 1;
4000 while (i > iCellStart && (buf[i] == '|' || buf[i] == ' ' || buf[i] == '\t'))
4001 --i;
4002 ++i;
4003 buf.remove(i, iCellEnd - i);
4004 delta -= iCellEnd - i;
4005 iCellEnd = i;
4007 buf.insert(iCellEnd, ")");
4008 ++delta;
4010 // strip initial whitespace and delimiter
4011 i = skipChars(buf, iCellStart, "| \t");
4012 buf.remove(iCellStart, i - iCellStart);
4013 delta -= i - iCellStart;
4015 switch (columnAlignments[cellIndex])
4017 case TableColumnAlignment.none:
4018 buf.insert(iCellStart, headerRow ? "$(TH " : "$(TD ");
4019 delta += 5;
4020 break;
4021 case TableColumnAlignment.left:
4022 buf.insert(iCellStart, "left, ");
4023 delta += 6;
4024 goto default;
4025 case TableColumnAlignment.center:
4026 buf.insert(iCellStart, "center, ");
4027 delta += 8;
4028 goto default;
4029 case TableColumnAlignment.right:
4030 buf.insert(iCellStart, "right, ");
4031 delta += 7;
4032 goto default;
4033 default:
4034 buf.insert(iCellStart, headerRow ? "$(TH_ALIGN " : "$(TD_ALIGN ");
4035 delta += 11;
4036 break;
4040 int cellIndex = cellCount - 1;
4041 size_t iCellEnd = iEnd;
4042 foreach_reverse (di, delimiter; inlineDelimiters)
4044 if (delimiter.type == '|')
4046 if (ignoreLast && di == inlineDelimiters.length-1)
4048 ignoreLast = false;
4049 continue;
4052 if (cellIndex >= columnAlignments.length)
4054 // kill any extra cells
4055 buf.remove(delimiter.iStart, iEnd + delta - delimiter.iStart);
4056 delta -= iEnd + delta - delimiter.iStart;
4057 iCellEnd = iEnd + delta;
4058 --cellIndex;
4059 continue;
4062 replaceTableCell(delimiter.iStart, iCellEnd, cellIndex, cast(int) di);
4063 iCellEnd = delimiter.iStart;
4064 --cellIndex;
4068 // if no starting pipe, replace from the start
4069 if (cellIndex >= 0)
4070 replaceTableCell(iStart, iCellEnd, cellIndex, 0);
4072 buf.insert(iEnd + delta, ")");
4073 buf.insert(iStart, "$(TR ");
4074 delta += 6;
4076 if (headerRow)
4078 buf.insert(iEnd + delta, ")");
4079 buf.insert(iStart, "$(THEAD ");
4080 delta += 9;
4083 return true;
4086 /****************************************************
4087 * End a table, if in one.
4089 * Params:
4090 * buf = an OutBuffer containing the DDoc
4091 * i = the index within `buf` to end the table at
4092 * columnAlignments = alignments for each column; upon return is set to length `0`
4093 * Returns: the number of characters added by ending the table, or `0` if unchanged
4095 size_t endTable(ref OutBuffer buf, size_t i, ref TableColumnAlignment[] columnAlignments)
4097 if (!columnAlignments.length)
4098 return 0;
4100 buf.insert(i, "))");
4101 columnAlignments.length = 0;
4102 return 2;
4105 /****************************************************
4106 * End a table row and then the table itself.
4108 * Params:
4109 * buf = an OutBuffer containing the DDoc
4110 * iStart = the index within `buf` that the table row starts at, inclusive
4111 * iEnd = the index within `buf` that the table row ends at, exclusive
4112 * loc = the current location in the file
4113 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
4114 * columnAlignments = alignments for each column; upon return is set to length `0`
4115 * Returns: the number of characters added by replacing the row, or `0` if unchanged
4117 size_t endRowAndTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, ref TableColumnAlignment[] columnAlignments)
4119 size_t delta;
4120 replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, false, delta);
4121 delta += endTable(buf, iEnd + delta, columnAlignments);
4122 return delta;
4125 /**************************************************
4126 * Highlight text section.
4128 * Params:
4129 * scope = the current parse scope
4130 * a = an array of D symbols at the current scope
4131 * 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.
4132 * buf = an OutBuffer containing the DDoc
4133 * offset = the index within buf to start highlighting
4135 void highlightText(Scope* sc, Dsymbols* a, Loc loc, ref OutBuffer buf, size_t offset)
4137 const incrementLoc = loc.linnum == 0 ? 1 : 0;
4138 loc.linnum = loc.linnum + incrementLoc;
4139 loc.charnum = 0;
4140 //printf("highlightText()\n");
4141 bool leadingBlank = true;
4142 size_t iParagraphStart = offset;
4143 size_t iPrecedingBlankLine = 0;
4144 int headingLevel = 0;
4145 int headingMacroLevel = 0;
4146 int quoteLevel = 0;
4147 bool lineQuoted = false;
4148 int quoteMacroLevel = 0;
4149 MarkdownList[] nestedLists;
4150 MarkdownDelimiter[] inlineDelimiters;
4151 MarkdownLinkReferences linkReferences;
4152 TableColumnAlignment[] columnAlignments;
4153 bool tableRowDetected = false;
4154 int inCode = 0;
4155 int inBacktick = 0;
4156 int macroLevel = 0;
4157 int previousMacroLevel = 0;
4158 int parenLevel = 0;
4159 size_t iCodeStart = 0; // start of code section
4160 size_t codeFenceLength = 0;
4161 size_t codeIndent = 0;
4162 string codeLanguage;
4163 size_t iLineStart = offset;
4164 linkReferences._scope = sc;
4165 for (size_t i = offset; i < buf.length; i++)
4167 char c = buf[i];
4168 Lcont:
4169 switch (c)
4171 case ' ':
4172 case '\t':
4173 break;
4174 case '\n':
4175 if (inBacktick)
4177 // `inline code` is only valid if contained on a single line
4178 // otherwise, the backticks should be output literally.
4180 // This lets things like `output from the linker' display
4181 // unmolested while keeping the feature consistent with GitHub.
4182 inBacktick = false;
4183 inCode = false; // the backtick also assumes we're in code
4184 // Nothing else is necessary since the DDOC_BACKQUOTED macro is
4185 // inserted lazily at the close quote, meaning the rest of the
4186 // text is already OK.
4188 if (headingLevel)
4190 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
4191 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
4192 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4193 ++i;
4194 iParagraphStart = skipChars(buf, i, " \t\r\n");
4197 if (tableRowDetected && !columnAlignments.length)
4198 i += startTable(buf, iLineStart, i, loc, lineQuoted, inlineDelimiters, columnAlignments);
4199 else if (columnAlignments.length)
4201 size_t delta;
4202 if (replaceTableRow(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments, false, delta))
4203 i += delta;
4204 else
4205 i += endTable(buf, i, columnAlignments);
4208 if (!inCode && nestedLists.length && !quoteLevel)
4209 MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);
4211 iPrecedingBlankLine = 0;
4212 if (!inCode && i == iLineStart && i + 1 < buf.length) // if "\n\n"
4214 i += endTable(buf, i, columnAlignments);
4215 if (!lineQuoted && quoteLevel)
4216 endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
4217 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
4219 // if we don't already know about this paragraph break then
4220 // insert a blank line and record the paragraph break
4221 if (iParagraphStart <= i)
4223 iPrecedingBlankLine = i;
4224 i = buf.insert(i, "$(DDOC_BLANKLINE)");
4225 iParagraphStart = i + 1;
4228 else if (inCode &&
4229 i == iLineStart &&
4230 i + 1 < buf.length &&
4231 !lineQuoted &&
4232 quoteLevel) // if "\n\n" in quoted code
4234 inCode = false;
4235 i = buf.insert(i, ")");
4236 i += endAllMarkdownQuotes(buf, i, quoteLevel);
4237 quoteMacroLevel = 0;
4239 leadingBlank = true;
4240 lineQuoted = false;
4241 tableRowDetected = false;
4242 iLineStart = i + 1;
4243 loc.linnum = loc.linnum + incrementLoc;
4245 // update the paragraph start if we just entered a macro
4246 if (previousMacroLevel < macroLevel && iParagraphStart < iLineStart)
4247 iParagraphStart = iLineStart;
4248 previousMacroLevel = macroLevel;
4249 break;
4251 case '<':
4253 leadingBlank = false;
4254 if (inCode)
4255 break;
4256 const slice = buf[];
4257 auto p = &slice[i];
4258 const se = sc._module.escapetable.escapeChar('<');
4259 if (se == "&lt;")
4261 // Generating HTML
4262 // Skip over comments
4263 if (p[1] == '!' && p[2] == '-' && p[3] == '-')
4265 size_t j = i + 4;
4266 p += 4;
4267 while (1)
4269 if (j == slice.length)
4270 goto L1;
4271 if (p[0] == '-' && p[1] == '-' && p[2] == '>')
4273 i = j + 2; // place on closing '>'
4274 break;
4276 j++;
4277 p++;
4279 break;
4281 // Skip over HTML tag
4282 if (isalpha(p[1]) || (p[1] == '/' && isalpha(p[2])))
4284 size_t j = i + 2;
4285 p += 2;
4286 while (1)
4288 if (j == slice.length)
4289 break;
4290 if (p[0] == '>')
4292 i = j; // place on closing '>'
4293 break;
4295 j++;
4296 p++;
4298 break;
4302 // Replace '<' with '&lt;' character entity
4303 if (se.length)
4305 buf.remove(i, 1);
4306 i = buf.insert(i, se);
4307 i--; // point to ';'
4309 break;
4312 case '>':
4314 if (leadingBlank && (!inCode || quoteLevel))
4316 lineQuoted = true;
4317 int lineQuoteLevel = 1;
4318 size_t iAfterDelimiters = i + 1;
4319 for (; iAfterDelimiters < buf.length; ++iAfterDelimiters)
4321 const c0 = buf[iAfterDelimiters];
4322 if (c0 == '>')
4323 ++lineQuoteLevel;
4324 else if (c0 != ' ' && c0 != '\t')
4325 break;
4327 if (!quoteMacroLevel)
4328 quoteMacroLevel = macroLevel;
4329 buf.remove(i, iAfterDelimiters - i);
4331 if (quoteLevel < lineQuoteLevel)
4333 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4334 if (nestedLists.length)
4336 const indent = getMarkdownIndent(buf, iLineStart, i);
4337 if (indent < nestedLists[$-1].contentIndent)
4338 i += MarkdownList.endAllNestedLists(buf, i, nestedLists);
4341 for (; quoteLevel < lineQuoteLevel; ++quoteLevel)
4343 i = buf.insert(i, "$(BLOCKQUOTE\n");
4344 iLineStart = iParagraphStart = i;
4346 --i;
4348 else
4350 --i;
4351 if (nestedLists.length)
4352 MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists);
4354 break;
4357 leadingBlank = false;
4358 if (inCode)
4359 break;
4360 // Replace '>' with '&gt;' character entity
4361 const se = sc._module.escapetable.escapeChar('>');
4362 if (se.length)
4364 buf.remove(i, 1);
4365 i = buf.insert(i, se);
4366 i--; // point to ';'
4368 break;
4371 case '&':
4373 leadingBlank = false;
4374 if (inCode)
4375 break;
4376 char* p = cast(char*)&buf[].ptr[i];
4377 if (p[1] == '#' || isalpha(p[1]))
4378 break;
4379 // already a character entity
4380 // Replace '&' with '&amp;' character entity
4381 const se = sc._module.escapetable.escapeChar('&');
4382 if (se)
4384 buf.remove(i, 1);
4385 i = buf.insert(i, se);
4386 i--; // point to ';'
4388 break;
4391 case '`':
4393 const iAfterDelimiter = skipChars(buf, i, "`");
4394 const count = iAfterDelimiter - i;
4396 if (inBacktick == count)
4398 inBacktick = 0;
4399 inCode = 0;
4400 OutBuffer codebuf;
4401 codebuf.write(buf[iCodeStart + count .. i]);
4402 // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
4403 highlightCode(sc, a, codebuf, 0);
4404 escapeStrayParenthesis(loc, codebuf, 0, false, sc.eSink);
4405 buf.remove(iCodeStart, i - iCodeStart + count); // also trimming off the current `
4406 immutable pre = "$(DDOC_BACKQUOTED ";
4407 i = buf.insert(iCodeStart, pre);
4408 i = buf.insert(i, codebuf[]);
4409 i = buf.insert(i, ")");
4410 i--; // point to the ending ) so when the for loop does i++, it will see the next character
4411 break;
4414 // Perhaps we're starting or ending a Markdown code block
4415 if (leadingBlank && count >= 3)
4417 bool moreBackticks = false;
4418 for (size_t j = iAfterDelimiter; !moreBackticks && j < buf.length; ++j)
4419 if (buf[j] == '`')
4420 moreBackticks = true;
4421 else if (buf[j] == '\r' || buf[j] == '\n')
4422 break;
4423 if (!moreBackticks)
4424 goto case '-';
4427 if (inCode)
4429 if (inBacktick)
4430 i = iAfterDelimiter - 1;
4431 break;
4433 inCode = c;
4434 inBacktick = cast(int) count;
4435 codeIndent = 0; // inline code is not indented
4436 // All we do here is set the code flags and record
4437 // the location. The macro will be inserted lazily
4438 // so we can easily cancel the inBacktick if we come
4439 // across a newline character.
4440 iCodeStart = i;
4441 i = iAfterDelimiter - 1;
4442 break;
4445 case '#':
4447 /* A line beginning with # indicates an ATX-style heading. */
4448 if (leadingBlank && !inCode)
4450 leadingBlank = false;
4452 headingLevel = detectAtxHeadingLevel(buf, i);
4453 if (!headingLevel)
4454 break;
4456 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4457 if (!lineQuoted && quoteLevel)
4458 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4460 // remove the ### prefix, including whitespace
4461 i = skipChars(buf, i + headingLevel, " \t");
4462 buf.remove(iLineStart, i - iLineStart);
4463 i = iParagraphStart = iLineStart;
4465 removeAnyAtxHeadingSuffix(buf, i);
4466 --i;
4468 headingMacroLevel = macroLevel;
4470 break;
4473 case '~':
4475 if (leadingBlank)
4477 // Perhaps we're starting or ending a Markdown code block
4478 const iAfterDelimiter = skipChars(buf, i, "~");
4479 if (iAfterDelimiter - i >= 3)
4480 goto case '-';
4482 leadingBlank = false;
4483 break;
4486 case '-':
4487 /* A line beginning with --- delimits a code section.
4488 * inCode tells us if it is start or end of a code section.
4490 if (leadingBlank)
4492 if (!inCode && c == '-')
4494 const list = MarkdownList.parseItem(buf, iLineStart, i);
4495 if (list.isValid)
4497 if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4499 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4500 iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4501 break;
4503 else
4504 goto case '+';
4508 size_t istart = i;
4509 size_t eollen = 0;
4510 leadingBlank = false;
4511 const c0 = c; // if we jumped here from case '`' or case '~'
4512 size_t iInfoString = 0;
4513 if (!inCode)
4514 codeLanguage.length = 0;
4515 while (1)
4517 ++i;
4518 if (i >= buf.length)
4519 break;
4520 c = buf[i];
4521 if (c == '\n')
4523 eollen = 1;
4524 break;
4526 if (c == '\r')
4528 eollen = 1;
4529 if (i + 1 >= buf.length)
4530 break;
4531 if (buf[i + 1] == '\n')
4533 eollen = 2;
4534 break;
4537 // BUG: handle UTF PS and LS too
4538 if (c != c0 || iInfoString)
4540 if (!iInfoString && !inCode && i - istart >= 3)
4542 // Start a Markdown info string, like ```ruby
4543 codeFenceLength = i - istart;
4544 i = iInfoString = skipChars(buf, i, " \t");
4546 else if (iInfoString && c != '`')
4548 if (!codeLanguage.length && (c == ' ' || c == '\t'))
4549 codeLanguage = cast(string) buf[iInfoString..i].idup;
4551 else
4553 iInfoString = 0;
4554 goto Lcont;
4558 if (i - istart < 3 || (inCode && (inCode != c0 || (inCode != '-' && i - istart < codeFenceLength))))
4559 goto Lcont;
4560 if (iInfoString)
4562 if (!codeLanguage.length)
4563 codeLanguage = cast(string) buf[iInfoString..i].idup;
4565 else
4566 codeFenceLength = i - istart;
4568 // We have the start/end of a code section
4569 // Remove the entire --- line, including blanks and \n
4570 buf.remove(iLineStart, i - iLineStart + eollen);
4571 i = iLineStart;
4572 if (eollen)
4573 leadingBlank = true;
4574 if (inCode && (i <= iCodeStart))
4576 // Empty code section, just remove it completely.
4577 inCode = 0;
4578 break;
4580 if (inCode)
4582 inCode = 0;
4583 // The code section is from iCodeStart to i
4584 OutBuffer codebuf;
4585 codebuf.write(buf[iCodeStart .. i]);
4586 codebuf.writeByte(0);
4587 // Remove leading indentations from all lines
4588 bool lineStart = true;
4589 char* endp = cast(char*)codebuf[].ptr + codebuf.length;
4590 for (char* p = cast(char*)codebuf[].ptr; p < endp;)
4592 if (lineStart)
4594 size_t j = codeIndent;
4595 char* q = p;
4596 while (j-- > 0 && q < endp && isIndentWS(q))
4597 ++q;
4598 codebuf.remove(p - cast(char*)codebuf[].ptr, q - p);
4599 assert(cast(char*)codebuf[].ptr <= p);
4600 assert(p < cast(char*)codebuf[].ptr + codebuf.length);
4601 lineStart = false;
4602 endp = cast(char*)codebuf[].ptr + codebuf.length; // update
4603 continue;
4605 if (*p == '\n')
4606 lineStart = true;
4607 ++p;
4609 if (!codeLanguage.length || codeLanguage == "dlang" || codeLanguage == "d")
4610 highlightCode2(sc, a, codebuf, 0);
4611 else
4612 codebuf.remove(codebuf.length-1, 1); // remove the trailing 0 byte
4613 escapeStrayParenthesis(loc, codebuf, 0, false, sc.eSink);
4614 buf.remove(iCodeStart, i - iCodeStart);
4615 i = buf.insert(iCodeStart, codebuf[]);
4616 i = buf.insert(i, ")\n");
4617 i -= 2; // in next loop, c should be '\n'
4619 else
4621 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4622 if (!lineQuoted && quoteLevel)
4624 const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4625 i += delta;
4626 istart += delta;
4629 inCode = c0;
4630 codeIndent = istart - iLineStart; // save indent count
4631 if (codeLanguage.length && codeLanguage != "dlang" && codeLanguage != "d")
4633 // backslash-escape
4634 for (size_t j; j < codeLanguage.length - 1; ++j)
4635 if (codeLanguage[j] == '\\' && ispunct(codeLanguage[j + 1]))
4636 codeLanguage = codeLanguage[0..j] ~ codeLanguage[j + 1..$];
4638 i = buf.insert(i, "$(OTHER_CODE ");
4639 i = buf.insert(i, codeLanguage);
4640 i = buf.insert(i, ",");
4642 else
4643 i = buf.insert(i, "$(D_CODE ");
4644 iCodeStart = i;
4645 i--; // place i on >
4646 leadingBlank = true;
4649 break;
4651 case '_':
4653 if (leadingBlank && !inCode && replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4655 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4656 if (!lineQuoted && quoteLevel)
4657 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4658 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4659 iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4660 break;
4662 goto default;
4665 case '+':
4666 case '0':
4668 case '9':
4670 if (leadingBlank && !inCode)
4672 MarkdownList list = MarkdownList.parseItem(buf, iLineStart, i);
4673 if (list.isValid)
4675 // Avoid starting a numbered list in the middle of a paragraph
4676 if (!nestedLists.length && list.orderedStart.length &&
4677 iParagraphStart < iLineStart)
4679 i += list.orderedStart.length - 1;
4680 break;
4683 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4684 if (!lineQuoted && quoteLevel)
4686 const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4687 i += delta;
4688 list.iStart += delta;
4689 list.iContentStart += delta;
4692 list.macroLevel = macroLevel;
4693 list.startItem(buf, iLineStart, i, iPrecedingBlankLine, nestedLists, loc);
4694 break;
4697 leadingBlank = false;
4698 break;
4701 case '*':
4703 if (inCode || inBacktick)
4705 leadingBlank = false;
4706 break;
4709 if (leadingBlank)
4711 // Check for a thematic break
4712 if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc))
4714 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4715 if (!lineQuoted && quoteLevel)
4716 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel);
4717 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4718 iParagraphStart = skipChars(buf, i+1, " \t\r\n");
4719 break;
4722 // An initial * indicates a Markdown list item
4723 const list = MarkdownList.parseItem(buf, iLineStart, i);
4724 if (list.isValid)
4725 goto case '+';
4728 // Markdown emphasis
4729 const leftC = i > offset ? buf[i-1] : '\0';
4730 size_t iAfterEmphasis = skipChars(buf, i+1, "*");
4731 const rightC = iAfterEmphasis < buf.length ? buf[iAfterEmphasis] : '\0';
4732 int count = cast(int) (iAfterEmphasis - i);
4733 const leftFlanking = (rightC != '\0' && !isspace(rightC)) && (!ispunct(rightC) || leftC == '\0' || isspace(leftC) || ispunct(leftC));
4734 const rightFlanking = (leftC != '\0' && !isspace(leftC)) && (!ispunct(leftC) || rightC == '\0' || isspace(rightC) || ispunct(rightC));
4735 auto emphasis = MarkdownDelimiter(i, count, macroLevel, leftFlanking, rightFlanking, false, c);
4737 if (!emphasis.leftFlanking && !emphasis.rightFlanking)
4739 i = iAfterEmphasis - 1;
4740 break;
4743 inlineDelimiters ~= emphasis;
4744 i += emphasis.count;
4745 --i;
4746 break;
4749 case '!':
4751 leadingBlank = false;
4753 if (inCode)
4754 break;
4756 if (i < buf.length-1 && buf[i+1] == '[')
4758 const imageStart = MarkdownDelimiter(i, 2, macroLevel, false, false, false, c);
4759 inlineDelimiters ~= imageStart;
4760 ++i;
4762 break;
4764 case '[':
4766 if (inCode)
4768 leadingBlank = false;
4769 break;
4772 const leftC = i > offset ? buf[i-1] : '\0';
4773 const rightFlanking = leftC != '\0' && !isspace(leftC) && !ispunct(leftC);
4774 const atParagraphStart = leadingBlank && iParagraphStart >= iLineStart;
4775 const linkStart = MarkdownDelimiter(i, 1, macroLevel, false, rightFlanking, atParagraphStart, c);
4776 inlineDelimiters ~= linkStart;
4777 leadingBlank = false;
4778 break;
4780 case ']':
4782 leadingBlank = false;
4784 if (inCode)
4785 break;
4787 for (int d = cast(int) inlineDelimiters.length - 1; d >= 0; --d)
4789 const delimiter = inlineDelimiters[d];
4790 if (delimiter.type == '[' || delimiter.type == '!')
4792 if (delimiter.isValid &&
4793 MarkdownLink.replaceLink(buf, i, loc, inlineDelimiters, d, linkReferences))
4795 // if we removed a reference link then we're at line start
4796 if (i <= delimiter.iStart)
4797 leadingBlank = true;
4799 // don't nest links
4800 if (delimiter.type == '[')
4801 for (--d; d >= 0; --d)
4802 if (inlineDelimiters[d].type == '[')
4803 inlineDelimiters[d].invalidate();
4805 else
4807 // nothing found, so kill the delimiter
4808 inlineDelimiters = inlineDelimiters[0..d] ~ inlineDelimiters[d+1..$];
4810 break;
4813 break;
4816 case '|':
4818 if (inCode)
4820 leadingBlank = false;
4821 break;
4824 tableRowDetected = true;
4825 inlineDelimiters ~= MarkdownDelimiter(i, 1, macroLevel, leadingBlank, false, false, c);
4826 leadingBlank = false;
4827 break;
4830 case '\\':
4832 leadingBlank = false;
4833 if (inCode || i+1 >= buf.length)
4834 break;
4836 /* Escape Markdown special characters */
4837 char c1 = buf[i+1];
4838 if (ispunct(c1))
4840 buf.remove(i, 1);
4842 auto se = sc._module.escapetable.escapeChar(c1);
4843 if (!se)
4844 se = c1 == '$' ? "$(DOLLAR)" : c1 == ',' ? "$(COMMA)" : null;
4845 if (se)
4847 buf.remove(i, 1);
4848 i = buf.insert(i, se);
4849 i--; // point to escaped char
4852 break;
4855 case '$':
4857 /* Look for the start of a macro, '$(Identifier'
4859 leadingBlank = false;
4860 if (inCode || inBacktick)
4861 break;
4862 const slice = buf[];
4863 auto p = &slice[i];
4864 if (p[1] == '(' && isIdStart(&p[2]))
4865 ++macroLevel;
4866 break;
4869 case '(':
4871 if (!inCode && i > offset && buf[i-1] != '$')
4872 ++parenLevel;
4873 break;
4876 case ')':
4877 { /* End of macro
4879 leadingBlank = false;
4880 if (inCode || inBacktick)
4881 break;
4882 if (parenLevel > 0)
4883 --parenLevel;
4884 else if (macroLevel)
4886 int downToLevel = cast(int) inlineDelimiters.length;
4887 while (downToLevel > 0 && inlineDelimiters[downToLevel - 1].macroLevel >= macroLevel)
4888 --downToLevel;
4889 if (headingLevel && headingMacroLevel >= macroLevel)
4891 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
4892 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4894 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4895 while (nestedLists.length && nestedLists[$-1].macroLevel >= macroLevel)
4897 i = buf.insert(i, ")\n)");
4898 --nestedLists.length;
4900 if (quoteLevel && quoteMacroLevel >= macroLevel)
4901 i += endAllMarkdownQuotes(buf, i, quoteLevel);
4902 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters, downToLevel);
4904 --macroLevel;
4905 quoteMacroLevel = 0;
4907 break;
4910 default:
4911 leadingBlank = false;
4912 if (sc._module.filetype == FileType.ddoc || inCode)
4913 break;
4914 const start = cast(char*)buf[].ptr + i;
4915 if (isIdStart(start))
4917 size_t j = skippastident(buf, i);
4918 if (i < j)
4920 size_t k = skippastURL(buf, i);
4921 if (i < k)
4923 /* The URL is buf[i..k]
4925 if (macroLevel)
4926 /* Leave alone if already in a macro
4928 i = k - 1;
4929 else
4931 /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)'
4933 i = buf.bracket(i, "$(DDOC_LINK_AUTODETECT ", k, ")") - 1;
4935 break;
4938 else
4939 break;
4940 size_t len = j - i;
4941 // leading '_' means no highlight unless it's a reserved symbol name
4942 if (c == '_' && (i == 0 || !isdigit(*(start - 1))) && (i == buf.length - 1 || !isReservedName(start[0 .. len])))
4944 buf.remove(i, 1);
4945 i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j - 1, ")") - 1;
4946 break;
4948 if (isIdentifier(a, start[0 .. len]))
4950 i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL ", j, ")") - 1;
4951 break;
4953 if (isKeyword(start[0 .. len]))
4955 i = buf.bracket(i, "$(DDOC_AUTO_KEYWORD ", j, ")") - 1;
4956 break;
4958 if (isFunctionParameter(a, start[0 .. len]))
4960 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
4961 i = buf.bracket(i, "$(DDOC_AUTO_PARAM ", j, ")") - 1;
4962 break;
4964 i = j - 1;
4966 break;
4970 if (inCode == '-')
4971 sc.eSink.error(loc, "unmatched `---` in DDoc comment");
4972 else if (inCode)
4973 buf.insert(buf.length, ")");
4975 size_t i = buf.length;
4976 if (headingLevel)
4978 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel);
4979 removeBlankLineMacro(buf, iPrecedingBlankLine, i);
4981 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments);
4982 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters);
4983 endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel);
4986 /**************************************************
4987 * Highlight code for DDOC section.
4989 void highlightCode(Scope* sc, Dsymbol s, ref OutBuffer buf, size_t offset)
4991 auto imp = s.isImport();
4992 if (imp && imp.aliases.length > 0)
4994 // For example: `public import core.stdc.string : memcpy, memcmp;`
4995 for(int i = 0; i < imp.aliases.length; i++)
4997 // Need to distinguish between
4998 // `public import core.stdc.string : memcpy, memcmp;` and
4999 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
5000 auto a = imp.aliases[i];
5001 auto id = a ? a : imp.names[i];
5002 auto loc = Loc.init;
5003 Dsymbol pscopesym;
5004 if (auto symFromId = sc.search(loc, id, pscopesym))
5006 highlightCode(sc, symFromId, buf, offset);
5010 else
5012 OutBuffer ancbuf;
5013 emitAnchor(ancbuf, s, sc);
5014 buf.insert(offset, ancbuf[]);
5015 offset += ancbuf.length;
5017 Dsymbols a;
5018 a.push(s);
5019 highlightCode(sc, &a, buf, offset);
5023 /****************************************************
5025 void highlightCode(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
5027 //printf("highlightCode(a = '%s')\n", a.toChars());
5028 bool resolvedTemplateParameters = false;
5030 for (size_t i = offset; i < buf.length; i++)
5032 char c = buf[i];
5033 const se = sc._module.escapetable.escapeChar(c);
5034 if (se.length)
5036 buf.remove(i, 1);
5037 i = buf.insert(i, se);
5038 i--; // point to ';'
5039 continue;
5041 char* start = cast(char*)buf[].ptr + i;
5042 if (isIdStart(start))
5044 size_t j = skipPastIdentWithDots(buf, i);
5045 if (i < j)
5047 size_t len = j - i;
5048 if (isIdentifier(a, start[0 .. len]))
5050 i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
5051 continue;
5055 j = skippastident(buf, i);
5056 if (i < j)
5058 size_t len = j - i;
5059 if (isIdentifier(a, start[0 .. len]))
5061 i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1;
5062 continue;
5064 if (isFunctionParameter(a, start[0 .. len]))
5066 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5067 i = buf.bracket(i, "$(DDOC_PARAM ", j, ")") - 1;
5068 continue;
5070 i = j - 1;
5073 else if (!resolvedTemplateParameters)
5075 size_t previ = i;
5077 // hunt for template declarations:
5078 foreach (symi; 0 .. a.length)
5080 FuncDeclaration fd = (*a)[symi].isFuncDeclaration();
5082 if (!fd || !fd.parent || !fd.parent.isTemplateDeclaration())
5084 continue;
5087 TemplateDeclaration td = fd.parent.isTemplateDeclaration();
5089 // build the template parameters
5090 Array!(size_t) paramLens;
5091 paramLens.reserve(td.parameters.length);
5093 OutBuffer parametersBuf;
5094 HdrGenState hgs;
5096 parametersBuf.writeByte('(');
5098 foreach (parami; 0 .. td.parameters.length)
5100 TemplateParameter tp = (*td.parameters)[parami];
5102 if (parami)
5103 parametersBuf.writestring(", ");
5105 size_t lastOffset = parametersBuf.length;
5107 toCBuffer(tp, parametersBuf, hgs);
5109 paramLens[parami] = parametersBuf.length - lastOffset;
5111 parametersBuf.writeByte(')');
5113 const templateParams = parametersBuf[];
5115 //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start);
5116 if (start[0 .. templateParams.length] == templateParams)
5118 immutable templateParamListMacro = "$(DDOC_TEMPLATE_PARAM_LIST ";
5119 buf.bracket(i, templateParamListMacro.ptr, i + templateParams.length, ")");
5121 // We have the parameter list. While we're here we might
5122 // as well wrap the parameters themselves as well
5124 // + 1 here to take into account the opening paren of the
5125 // template param list
5126 i += templateParamListMacro.length + 1;
5128 foreach (const len; paramLens)
5130 i = buf.bracket(i, "$(DDOC_TEMPLATE_PARAM ", i + len, ")");
5131 // increment two here for space + comma
5132 i += 2;
5135 resolvedTemplateParameters = true;
5136 // reset i to be positioned back before we found the template
5137 // param list this assures that anything within the template
5138 // param list that needs to be escaped or otherwise altered
5139 // has an opportunity for that to happen outside of this context
5140 i = previ;
5142 continue;
5149 /****************************************
5151 void highlightCode3(Scope* sc, ref OutBuffer buf, const(char)* p, const(char)* pend)
5153 for (; p < pend; p++)
5155 const se = sc._module.escapetable.escapeChar(*p);
5156 if (se.length)
5157 buf.writestring(se);
5158 else
5159 buf.writeByte(*p);
5163 /**************************************************
5164 * Highlight code for CODE section.
5166 void highlightCode2(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset)
5168 scope eSinkNull = new ErrorSinkNull();
5170 scope Lexer lex = new Lexer(null, cast(char*)buf[].ptr, 0, buf.length - 1, 0, 1,
5171 eSinkNull, // ignore errors
5172 &global.compileEnv);
5173 OutBuffer res;
5174 const(char)* lastp = cast(char*)buf[].ptr;
5175 //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr);
5176 res.reserve(buf.length);
5177 while (1)
5179 Token tok;
5180 lex.scan(&tok);
5181 highlightCode3(sc, res, lastp, tok.ptr);
5182 string highlight = null;
5183 switch (tok.value)
5185 case TOK.identifier:
5187 if (!sc)
5188 break;
5189 size_t len = lex.p - tok.ptr;
5190 if (isIdentifier(a, tok.ptr[0 .. len]))
5192 highlight = "$(D_PSYMBOL ";
5193 break;
5195 if (isFunctionParameter(a, tok.ptr[0 .. len]))
5197 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5198 highlight = "$(D_PARAM ";
5199 break;
5201 break;
5203 case TOK.comment:
5204 highlight = "$(D_COMMENT ";
5205 break;
5206 case TOK.string_:
5207 case TOK.interpolated:
5208 highlight = "$(D_STRING ";
5209 break;
5210 default:
5211 if (tok.isKeyword())
5212 highlight = "$(D_KEYWORD ";
5213 break;
5215 if (highlight)
5217 res.writestring(highlight);
5218 size_t o = res.length;
5219 highlightCode3(sc, res, tok.ptr, lex.p);
5220 if (tok.value == TOK.comment || tok.value == TOK.string_ || tok.value == TOK.interpolated)
5221 /* https://issues.dlang.org/show_bug.cgi?id=7656
5222 * https://issues.dlang.org/show_bug.cgi?id=7715
5223 * https://issues.dlang.org/show_bug.cgi?id=10519
5225 escapeDdocString(res, o);
5226 res.writeByte(')');
5228 else
5229 highlightCode3(sc, res, tok.ptr, lex.p);
5230 if (tok.value == TOK.endOfFile)
5231 break;
5232 lastp = lex.p;
5234 buf.setsize(offset);
5235 buf.write(&res);
5238 /****************************************
5239 * Determine if p points to the start of a "..." parameter identifier.
5241 bool isCVariadicArg(const(char)[] p) @nogc nothrow pure @safe
5243 return p.length >= 3 && p[0 .. 3] == "...";
5246 /****************************************
5247 * Determine if p points to the start of an identifier.
5249 @trusted
5250 bool isIdStart(const(char)* p) @nogc nothrow pure
5252 dchar c = *p;
5253 if (isalpha(c) || c == '_')
5254 return true;
5255 if (c >= 0x80)
5257 size_t i = 0;
5258 if (utf_decodeChar(p[0 .. 4], i, c))
5259 return false; // ignore errors
5260 if (isUniAlpha(c))
5261 return true;
5263 return false;
5266 /****************************************
5267 * Determine if p points to the rest of an identifier.
5269 @trusted
5270 bool isIdTail(const(char)* p) @nogc nothrow pure
5272 dchar c = *p;
5273 if (isalnum(c) || c == '_')
5274 return true;
5275 if (c >= 0x80)
5277 size_t i = 0;
5278 if (utf_decodeChar(p[0 .. 4], i, c))
5279 return false; // ignore errors
5280 if (isUniAlpha(c))
5281 return true;
5283 return false;
5286 /****************************************
5287 * Determine if p points to the indentation space.
5289 bool isIndentWS(const(char)* p) @nogc nothrow pure @safe
5291 return (*p == ' ') || (*p == '\t');
5294 /*****************************************
5295 * Return number of bytes in UTF character.
5297 int utfStride(const(char)* p) @nogc nothrow pure
5299 dchar c = *p;
5300 if (c < 0x80)
5301 return 1;
5302 size_t i = 0;
5303 utf_decodeChar(p[0 .. 4], i, c); // ignore errors, but still consume input
5304 return cast(int)i;
5307 inout(char)* stripLeadingNewlines(inout(char)* s) @nogc nothrow pure
5309 while (s && *s == '\n' || *s == '\r')
5310 s++;
5312 return s;