1 /** 2 * Ddoc documentation generation. 3 * 4 * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator) 5 * 6 * Copyright: Copyright (C) 1999-2021 by The D Language Foundation, All Rights Reserved 7 * Authors: $(LINK2 http://www.digitalmars.com, Walter Bright) 8 * License: $(LINK2 http://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 12 */ 13 14 module dmd.doc; 15 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.attrib; 24 import dmd.cond; 25 import dmd.dclass; 26 import dmd.declaration; 27 import dmd.denum; 28 import dmd.dimport; 29 import dmd.dmacro; 30 import dmd.dmodule; 31 import dmd.dscope; 32 import dmd.dstruct; 33 import dmd.dsymbol; 34 import dmd.dsymbolsem; 35 import dmd.dtemplate; 36 import dmd.errors; 37 import dmd.func; 38 import dmd.globals; 39 import dmd.hdrgen; 40 import dmd.id; 41 import dmd.identifier; 42 import dmd.lexer; 43 import dmd.mtype; 44 import dmd.root.array; 45 import dmd.root.file; 46 import dmd.root.filename; 47 import dmd.root.outbuffer; 48 import dmd.root.port; 49 import dmd.root.rmem; 50 import dmd.root.string; 51 import dmd.tokens; 52 import dmd.utf; 53 import dmd.utils; 54 import dmd.visitor; 55 56 struct Escape 57 { 58 const(char)[][char.max] strings; 59 60 /*************************************** 61 * Find character string to replace c with. 62 */ 63 const(char)[] escapeChar(char c) 64 { 65 version (all) 66 { 67 //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr); 68 return strings[c]; 69 } 70 else 71 { 72 const(char)[] s; 73 switch (c) 74 { 75 case '<': 76 s = "<"; 77 break; 78 case '>': 79 s = ">"; 80 break; 81 case '&': 82 s = "&"; 83 break; 84 default: 85 s = null; 86 break; 87 } 88 return s; 89 } 90 } 91 } 92 93 /*********************************************************** 94 */ 95 private class Section 96 { 97 const(char)[] name; 98 const(char)[] body_; 99 int nooutput; 100 101 override string toString() const 102 { 103 assert(0); 104 } 105 106 void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf) 107 { 108 assert(a.dim); 109 if (name.length) 110 { 111 static immutable table = 112 [ 113 "AUTHORS", 114 "BUGS", 115 "COPYRIGHT", 116 "DATE", 117 "DEPRECATED", 118 "EXAMPLES", 119 "HISTORY", 120 "LICENSE", 121 "RETURNS", 122 "SEE_ALSO", 123 "STANDARDS", 124 "THROWS", 125 "VERSION", 126 ]; 127 foreach (entry; table) 128 { 129 if (iequals(entry, name)) 130 { 131 buf.printf("$(DDOC_%s ", entry.ptr); 132 goto L1; 133 } 134 } 135 buf.writestring("$(DDOC_SECTION "); 136 // Replace _ characters with spaces 137 buf.writestring("$(DDOC_SECTION_H "); 138 size_t o = buf.length; 139 foreach (char c; name) 140 buf.writeByte((c == '_') ? ' ' : c); 141 escapeStrayParenthesis(loc, buf, o, false); 142 buf.writestring(")"); 143 } 144 else 145 { 146 buf.writestring("$(DDOC_DESCRIPTION "); 147 } 148 L1: 149 size_t o = buf.length; 150 buf.write(body_); 151 escapeStrayParenthesis(loc, buf, o, true); 152 highlightText(sc, a, loc, *buf, o); 153 buf.writestring(")"); 154 } 155 } 156 157 /*********************************************************** 158 */ 159 private final class ParamSection : Section 160 { 161 override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf) 162 { 163 assert(a.dim); 164 Dsymbol s = (*a)[0]; // test 165 const(char)* p = body_.ptr; 166 size_t len = body_.length; 167 const(char)* pend = p + len; 168 const(char)* tempstart = null; 169 size_t templen = 0; 170 const(char)* namestart = null; 171 size_t namelen = 0; // !=0 if line continuation 172 const(char)* textstart = null; 173 size_t textlen = 0; 174 size_t paramcount = 0; 175 buf.writestring("$(DDOC_PARAMS "); 176 while (p < pend) 177 { 178 // Skip to start of macro 179 while (1) 180 { 181 switch (*p) 182 { 183 case ' ': 184 case '\t': 185 p++; 186 continue; 187 case '\n': 188 p++; 189 goto Lcont; 190 default: 191 if (isIdStart(p) || isCVariadicArg(p[0 .. cast(size_t)(pend - p)])) 192 break; 193 if (namelen) 194 goto Ltext; 195 // continuation of prev macro 196 goto Lskipline; 197 } 198 break; 199 } 200 tempstart = p; 201 while (isIdTail(p)) 202 p += utfStride(p); 203 if (isCVariadicArg(p[0 .. cast(size_t)(pend - p)])) 204 p += 3; 205 templen = p - tempstart; 206 while (*p == ' ' || *p == '\t') 207 p++; 208 if (*p != '=') 209 { 210 if (namelen) 211 goto Ltext; 212 // continuation of prev macro 213 goto Lskipline; 214 } 215 p++; 216 if (namelen) 217 { 218 // Output existing param 219 L1: 220 //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart); 221 ++paramcount; 222 HdrGenState hgs; 223 buf.writestring("$(DDOC_PARAM_ROW "); 224 { 225 buf.writestring("$(DDOC_PARAM_ID "); 226 { 227 size_t o = buf.length; 228 Parameter fparam = isFunctionParameter(a, namestart, namelen); 229 if (!fparam) 230 { 231 // Comments on a template might refer to function parameters within. 232 // Search the parameters of nested eponymous functions (with the same name.) 233 fparam = isEponymousFunctionParameter(a, namestart, namelen); 234 } 235 bool isCVariadic = isCVariadicParameter(a, namestart[0 .. namelen]); 236 if (isCVariadic) 237 { 238 buf.writestring("..."); 239 } 240 else if (fparam && fparam.type && fparam.ident) 241 { 242 .toCBuffer(fparam.type, buf, fparam.ident, &hgs); 243 } 244 else 245 { 246 if (isTemplateParameter(a, namestart, namelen)) 247 { 248 // 10236: Don't count template parameters for params check 249 --paramcount; 250 } 251 else if (!fparam) 252 { 253 warning(s.loc, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen, namestart); 254 } 255 buf.write(namestart[0 .. namelen]); 256 } 257 escapeStrayParenthesis(loc, buf, o, true); 258 highlightCode(sc, a, *buf, o); 259 } 260 buf.writestring(")"); 261 buf.writestring("$(DDOC_PARAM_DESC "); 262 { 263 size_t o = buf.length; 264 buf.write(textstart[0 .. textlen]); 265 escapeStrayParenthesis(loc, buf, o, true); 266 highlightText(sc, a, loc, *buf, o); 267 } 268 buf.writestring(")"); 269 } 270 buf.writestring(")"); 271 namelen = 0; 272 if (p >= pend) 273 break; 274 } 275 namestart = tempstart; 276 namelen = templen; 277 while (*p == ' ' || *p == '\t') 278 p++; 279 textstart = p; 280 Ltext: 281 while (*p != '\n') 282 p++; 283 textlen = p - textstart; 284 p++; 285 Lcont: 286 continue; 287 Lskipline: 288 // Ignore this line 289 while (*p++ != '\n') 290 { 291 } 292 } 293 if (namelen) 294 goto L1; 295 // write out last one 296 buf.writestring(")"); 297 TypeFunction tf = a.dim == 1 ? isTypeFunction(s) : null; 298 if (tf) 299 { 300 size_t pcount = (tf.parameterList.parameters ? tf.parameterList.parameters.dim : 0) + 301 cast(int)(tf.parameterList.varargs == VarArg.variadic); 302 if (pcount != paramcount) 303 { 304 warning(s.loc, "Ddoc: parameter count mismatch, expected %llu, got %llu", 305 cast(ulong) pcount, cast(ulong) paramcount); 306 if (paramcount == 0) 307 { 308 // Chances are someone messed up the format 309 warningSupplemental(s.loc, "Note that the format is `param = description`"); 310 } 311 } 312 } 313 } 314 } 315 316 /*********************************************************** 317 */ 318 private final class MacroSection : Section 319 { 320 override void write(Loc loc, DocComment* dc, Scope* sc, Dsymbols* a, OutBuffer* buf) 321 { 322 //printf("MacroSection::write()\n"); 323 DocComment.parseMacros(dc.escapetable, *dc.pmacrotable, body_); 324 } 325 } 326 327 private alias Sections = Array!(Section); 328 329 // Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one). 330 private bool isCVariadicParameter(Dsymbols* a, const(char)[] p) 331 { 332 foreach (member; *a) 333 { 334 TypeFunction tf = isTypeFunction(member); 335 if (tf && tf.parameterList.varargs == VarArg.variadic && p == "...") 336 return true; 337 } 338 return false; 339 } 340 341 private Dsymbol getEponymousMember(TemplateDeclaration td) 342 { 343 if (!td.onemember) 344 return null; 345 if (AggregateDeclaration ad = td.onemember.isAggregateDeclaration()) 346 return ad; 347 if (FuncDeclaration fd = td.onemember.isFuncDeclaration()) 348 return fd; 349 if (auto em = td.onemember.isEnumMember()) 350 return null; // Keep backward compatibility. See compilable/ddoc9.d 351 if (VarDeclaration vd = td.onemember.isVarDeclaration()) 352 return td.constraint ? null : vd; 353 return null; 354 } 355 356 private TemplateDeclaration getEponymousParent(Dsymbol s) 357 { 358 if (!s.parent) 359 return null; 360 TemplateDeclaration td = s.parent.isTemplateDeclaration(); 361 return (td && getEponymousMember(td)) ? td : null; 362 } 363 364 private immutable ddoc_default = import("default_ddoc_theme.ddoc"); 365 private immutable ddoc_decl_s = "$(DDOC_DECL "; 366 private immutable ddoc_decl_e = ")\n"; 367 private immutable ddoc_decl_dd_s = "$(DDOC_DECL_DD "; 368 private immutable ddoc_decl_dd_e = ")\n"; 369 370 /**************************************************** 371 */ 372 extern(C++) void gendocfile(Module m) 373 { 374 __gshared OutBuffer mbuf; 375 __gshared int mbuf_done; 376 OutBuffer buf; 377 //printf("Module::gendocfile()\n"); 378 if (!mbuf_done) // if not already read the ddoc files 379 { 380 mbuf_done = 1; 381 // Use our internal default 382 mbuf.writestring(ddoc_default); 383 // Override with DDOCFILE specified in the sc.ini file 384 char* p = getenv("DDOCFILE"); 385 if (p) 386 global.params.ddocfiles.shift(p); 387 // Override with the ddoc macro files from the command line 388 for (size_t i = 0; i < global.params.ddocfiles.dim; i++) 389 { 390 auto buffer = readFile(m.loc, global.params.ddocfiles[i]); 391 // BUG: convert file contents to UTF-8 before use 392 const data = buffer.data; 393 //printf("file: '%.*s'\n", cast(int)data.length, data.ptr); 394 mbuf.write(data); 395 } 396 } 397 DocComment.parseMacros(m.escapetable, m.macrotable, mbuf[]); 398 Scope* sc = Scope.createGlobal(m); // create root scope 399 DocComment* dc = DocComment.parse(m, m.comment); 400 dc.pmacrotable = &m.macrotable; 401 dc.escapetable = m.escapetable; 402 sc.lastdc = dc; 403 // Generate predefined macros 404 // Set the title to be the name of the module 405 { 406 const p = m.toPrettyChars().toDString; 407 m.macrotable.define("TITLE", p); 408 } 409 // Set time macros 410 { 411 time_t t; 412 time(&t); 413 char* p = ctime(&t); 414 p = mem.xstrdup(p); 415 m.macrotable.define("DATETIME", p.toDString()); 416 m.macrotable.define("YEAR", p[20 .. 20 + 4]); 417 } 418 const srcfilename = m.srcfile.toString(); 419 m.macrotable.define("SRCFILENAME", srcfilename); 420 const docfilename = m.docfile.toString(); 421 m.macrotable.define("DOCFILENAME", docfilename); 422 if (dc.copyright) 423 { 424 dc.copyright.nooutput = 1; 425 m.macrotable.define("COPYRIGHT", dc.copyright.body_); 426 } 427 if (m.isDocFile) 428 { 429 const ploc = m.md ? &m.md.loc : &m.loc; 430 const loc = Loc(ploc.filename ? ploc.filename : srcfilename.ptr, 431 ploc.linnum, 432 ploc.charnum); 433 434 size_t commentlen = strlen(cast(char*)m.comment); 435 Dsymbols a; 436 // https://issues.dlang.org/show_bug.cgi?id=9764 437 // Don't push m in a, to prevent emphasize ddoc file name. 438 if (dc.macros) 439 { 440 commentlen = dc.macros.name.ptr - m.comment; 441 dc.macros.write(loc, dc, sc, &a, &buf); 442 } 443 buf.write(m.comment[0 .. commentlen]); 444 highlightText(sc, &a, loc, buf, 0); 445 } 446 else 447 { 448 Dsymbols a; 449 a.push(m); 450 dc.writeSections(sc, &a, &buf); 451 emitMemberComments(m, buf, sc); 452 } 453 //printf("BODY= '%.*s'\n", cast(int)buf.length, buf.data); 454 m.macrotable.define("BODY", buf[]); 455 OutBuffer buf2; 456 buf2.writestring("$(DDOC)"); 457 size_t end = buf2.length; 458 m.macrotable.expand(buf2, 0, end, null); 459 version (all) 460 { 461 /* Remove all the escape sequences from buf2, 462 * and make CR-LF the newline. 463 */ 464 { 465 const slice = buf2[]; 466 buf.setsize(0); 467 buf.reserve(slice.length); 468 auto p = slice.ptr; 469 for (size_t j = 0; j < slice.length; j++) 470 { 471 char c = p[j]; 472 if (c == 0xFF && j + 1 < slice.length) 473 { 474 j++; 475 continue; 476 } 477 if (c == '\n') 478 buf.writeByte('\r'); 479 else if (c == '\r') 480 { 481 buf.writestring("\r\n"); 482 if (j + 1 < slice.length && p[j + 1] == '\n') 483 { 484 j++; 485 } 486 continue; 487 } 488 buf.writeByte(c); 489 } 490 } 491 writeFile(m.loc, m.docfile.toString(), buf[]); 492 } 493 else 494 { 495 /* Remove all the escape sequences from buf2 496 */ 497 { 498 size_t i = 0; 499 char* p = buf2.data; 500 for (size_t j = 0; j < buf2.length; j++) 501 { 502 if (p[j] == 0xFF && j + 1 < buf2.length) 503 { 504 j++; 505 continue; 506 } 507 p[i] = p[j]; 508 i++; 509 } 510 buf2.setsize(i); 511 } 512 writeFile(m.loc, m.docfile.toString(), buf2[]); 513 } 514 } 515 516 /**************************************************** 517 * Having unmatched parentheses can hose the output of Ddoc, 518 * as the macros depend on properly nested parentheses. 519 * This function replaces all ( with $(LPAREN) and ) with $(RPAREN) 520 * to preserve text literally. This also means macros in the 521 * text won't be expanded. 522 */ 523 void escapeDdocString(OutBuffer* buf, size_t start) 524 { 525 for (size_t u = start; u < buf.length; u++) 526 { 527 char c = (*buf)[u]; 528 switch (c) 529 { 530 case '$': 531 buf.remove(u, 1); 532 buf.insert(u, "$(DOLLAR)"); 533 u += 8; 534 break; 535 case '(': 536 buf.remove(u, 1); //remove the ( 537 buf.insert(u, "$(LPAREN)"); //insert this instead 538 u += 8; //skip over newly inserted macro 539 break; 540 case ')': 541 buf.remove(u, 1); //remove the ) 542 buf.insert(u, "$(RPAREN)"); //insert this instead 543 u += 8; //skip over newly inserted macro 544 break; 545 default: 546 break; 547 } 548 } 549 } 550 551 /**************************************************** 552 * Having unmatched parentheses can hose the output of Ddoc, 553 * as the macros depend on properly nested parentheses. 554 * 555 * Fix by replacing unmatched ( with $(LPAREN) and unmatched ) with $(RPAREN). 556 * 557 * Params: 558 * 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. 559 * buf = an OutBuffer containing the DDoc 560 * start = the index within buf to start replacing unmatched parentheses 561 * respectBackslashEscapes = if true, always replace parentheses that are 562 * directly preceeded by a backslash with $(LPAREN) or $(RPAREN) instead of 563 * counting them as stray parentheses 564 */ 565 private void escapeStrayParenthesis(Loc loc, OutBuffer* buf, size_t start, bool respectBackslashEscapes) 566 { 567 uint par_open = 0; 568 char inCode = 0; 569 bool atLineStart = true; 570 for (size_t u = start; u < buf.length; u++) 571 { 572 char c = (*buf)[u]; 573 switch (c) 574 { 575 case '(': 576 if (!inCode) 577 par_open++; 578 atLineStart = false; 579 break; 580 case ')': 581 if (!inCode) 582 { 583 if (par_open == 0) 584 { 585 //stray ')' 586 warning(loc, "Ddoc: Stray ')'. This may cause incorrect Ddoc output. Use $(RPAREN) instead for unpaired right parentheses."); 587 buf.remove(u, 1); //remove the ) 588 buf.insert(u, "$(RPAREN)"); //insert this instead 589 u += 8; //skip over newly inserted macro 590 } 591 else 592 par_open--; 593 } 594 atLineStart = false; 595 break; 596 case '\n': 597 atLineStart = true; 598 version (none) 599 { 600 // For this to work, loc must be set to the beginning of the passed 601 // text which is currently not possible 602 // (loc is set to the Loc of the Dsymbol) 603 loc.linnum++; 604 } 605 break; 606 case ' ': 607 case '\r': 608 case '\t': 609 break; 610 case '-': 611 case '`': 612 case '~': 613 // Issue 15465: don't try to escape unbalanced parens inside code 614 // blocks. 615 int numdash = 1; 616 for (++u; u < buf.length && (*buf)[u] == c; ++u) 617 ++numdash; 618 --u; 619 if (c == '`' || (atLineStart && numdash >= 3)) 620 { 621 if (inCode == c) 622 inCode = 0; 623 else if (!inCode) 624 inCode = c; 625 } 626 atLineStart = false; 627 break; 628 case '\\': 629 // replace backslash-escaped parens with their macros 630 if (!inCode && respectBackslashEscapes && u+1 < buf.length && global.params.markdown) 631 { 632 if ((*buf)[u+1] == '(' || (*buf)[u+1] == ')') 633 { 634 const paren = (*buf)[u+1] == '(' ? "$(LPAREN)" : "$(RPAREN)"; 635 buf.remove(u, 2); //remove the \) 636 buf.insert(u, paren); //insert this instead 637 u += 8; //skip over newly inserted macro 638 } 639 else if ((*buf)[u+1] == '\\') 640 ++u; 641 } 642 break; 643 default: 644 atLineStart = false; 645 break; 646 } 647 } 648 if (par_open) // if any unmatched lparens 649 { 650 par_open = 0; 651 for (size_t u = buf.length; u > start;) 652 { 653 u--; 654 char c = (*buf)[u]; 655 switch (c) 656 { 657 case ')': 658 par_open++; 659 break; 660 case '(': 661 if (par_open == 0) 662 { 663 //stray '(' 664 warning(loc, "Ddoc: Stray '('. This may cause incorrect Ddoc output. Use $(LPAREN) instead for unpaired left parentheses."); 665 buf.remove(u, 1); //remove the ( 666 buf.insert(u, "$(LPAREN)"); //insert this instead 667 } 668 else 669 par_open--; 670 break; 671 default: 672 break; 673 } 674 } 675 } 676 } 677 678 // Basically, this is to skip over things like private{} blocks in a struct or 679 // class definition that don't add any components to the qualified name. 680 private Scope* skipNonQualScopes(Scope* sc) 681 { 682 while (sc && !sc.scopesym) 683 sc = sc.enclosing; 684 return sc; 685 } 686 687 private bool emitAnchorName(ref OutBuffer buf, Dsymbol s, Scope* sc, bool includeParent) 688 { 689 if (!s || s.isPackage() || s.isModule()) 690 return false; 691 // Add parent names first 692 bool dot = false; 693 auto eponymousParent = getEponymousParent(s); 694 if (includeParent && s.parent || eponymousParent) 695 dot = emitAnchorName(buf, s.parent, sc, includeParent); 696 else if (includeParent && sc) 697 dot = emitAnchorName(buf, sc.scopesym, skipNonQualScopes(sc.enclosing), includeParent); 698 // Eponymous template members can share the parent anchor name 699 if (eponymousParent) 700 return dot; 701 if (dot) 702 buf.writeByte('.'); 703 // Use "this" not "__ctor" 704 TemplateDeclaration td; 705 if (s.isCtorDeclaration() || ((td = s.isTemplateDeclaration()) !is null && td.onemember && td.onemember.isCtorDeclaration())) 706 { 707 buf.writestring("this"); 708 } 709 else 710 { 711 /* We just want the identifier, not overloads like TemplateDeclaration::toChars. 712 * We don't want the template parameter list and constraints. */ 713 buf.writestring(s.Dsymbol.toChars()); 714 } 715 return true; 716 } 717 718 private void emitAnchor(ref OutBuffer buf, Dsymbol s, Scope* sc, bool forHeader = false) 719 { 720 Identifier ident; 721 { 722 OutBuffer anc; 723 emitAnchorName(anc, s, skipNonQualScopes(sc), true); 724 ident = Identifier.idPool(anc[]); 725 } 726 727 auto pcount = cast(void*)ident in sc.anchorCounts; 728 typeof(*pcount) count; 729 if (!forHeader) 730 { 731 if (pcount) 732 { 733 // Existing anchor, 734 // don't write an anchor for matching consecutive ditto symbols 735 TemplateDeclaration td = getEponymousParent(s); 736 if (sc.prevAnchor == ident && sc.lastdc && (isDitto(s.comment) || (td && isDitto(td.comment)))) 737 return; 738 739 count = ++*pcount; 740 } 741 else 742 { 743 sc.anchorCounts[cast(void*)ident] = 1; 744 count = 1; 745 } 746 } 747 748 // cache anchor name 749 sc.prevAnchor = ident; 750 auto macroName = forHeader ? "DDOC_HEADER_ANCHOR" : "DDOC_ANCHOR"; 751 752 if (auto imp = s.isImport()) 753 { 754 // For example: `public import core.stdc.string : memcpy, memcmp;` 755 if (imp.aliases.dim > 0) 756 { 757 for(int i = 0; i < imp.aliases.dim; i++) 758 { 759 // Need to distinguish between 760 // `public import core.stdc.string : memcpy, memcmp;` and 761 // `public import core.stdc.string : copy = memcpy, compare = memcmp;` 762 auto a = imp.aliases[i]; 763 auto id = a ? a : imp.names[i]; 764 auto loc = Loc.init; 765 if (auto symFromId = sc.search(loc, id, null)) 766 { 767 emitAnchor(buf, symFromId, sc, forHeader); 768 } 769 } 770 } 771 else 772 { 773 // For example: `public import str = core.stdc.string;` 774 if (imp.aliasId) 775 { 776 auto symbolName = imp.aliasId.toString(); 777 778 buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr, 779 cast(int) symbolName.length, symbolName.ptr); 780 781 if (forHeader) 782 { 783 buf.printf(", %.*s", cast(int) symbolName.length, symbolName.ptr); 784 } 785 } 786 else 787 { 788 // The general case: `public import core.stdc.string;` 789 790 // fully qualify imports so `core.stdc.string` doesn't appear as `core` 791 void printFullyQualifiedImport() 792 { 793 foreach (const pid; imp.packages) 794 { 795 buf.printf("%s.", pid.toChars()); 796 } 797 buf.writestring(imp.id.toString()); 798 } 799 800 buf.printf("$(%.*s ", cast(int) macroName.length, macroName.ptr); 801 printFullyQualifiedImport(); 802 803 if (forHeader) 804 { 805 buf.printf(", "); 806 printFullyQualifiedImport(); 807 } 808 } 809 810 buf.writeByte(')'); 811 } 812 } 813 else 814 { 815 auto symbolName = ident.toString(); 816 buf.printf("$(%.*s %.*s", cast(int) macroName.length, macroName.ptr, 817 cast(int) symbolName.length, symbolName.ptr); 818 819 // only append count once there's a duplicate 820 if (count > 1) 821 buf.printf(".%u", count); 822 823 if (forHeader) 824 { 825 Identifier shortIdent; 826 { 827 OutBuffer anc; 828 emitAnchorName(anc, s, skipNonQualScopes(sc), false); 829 shortIdent = Identifier.idPool(anc[]); 830 } 831 832 auto shortName = shortIdent.toString(); 833 buf.printf(", %.*s", cast(int) shortName.length, shortName.ptr); 834 } 835 836 buf.writeByte(')'); 837 } 838 } 839 840 /******************************* emitComment **********************************/ 841 842 /** Get leading indentation from 'src' which represents lines of code. */ 843 private size_t getCodeIndent(const(char)* src) 844 { 845 while (src && (*src == '\r' || *src == '\n')) 846 ++src; // skip until we find the first non-empty line 847 size_t codeIndent = 0; 848 while (src && (*src == ' ' || *src == '\t')) 849 { 850 codeIndent++; 851 src++; 852 } 853 return codeIndent; 854 } 855 856 /** Recursively expand template mixin member docs into the scope. */ 857 private void expandTemplateMixinComments(TemplateMixin tm, ref OutBuffer buf, Scope* sc) 858 { 859 if (!tm.semanticRun) 860 tm.dsymbolSemantic(sc); 861 TemplateDeclaration td = (tm && tm.tempdecl) ? tm.tempdecl.isTemplateDeclaration() : null; 862 if (td && td.members) 863 { 864 for (size_t i = 0; i < td.members.dim; i++) 865 { 866 Dsymbol sm = (*td.members)[i]; 867 TemplateMixin tmc = sm.isTemplateMixin(); 868 if (tmc && tmc.comment) 869 expandTemplateMixinComments(tmc, buf, sc); 870 else 871 emitComment(sm, buf, sc); 872 } 873 } 874 } 875 876 private void emitMemberComments(ScopeDsymbol sds, ref OutBuffer buf, Scope* sc) 877 { 878 if (!sds.members) 879 return; 880 //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars()); 881 const(char)[] m = "$(DDOC_MEMBERS "; 882 if (sds.isTemplateDeclaration()) 883 m = "$(DDOC_TEMPLATE_MEMBERS "; 884 else if (sds.isClassDeclaration()) 885 m = "$(DDOC_CLASS_MEMBERS "; 886 else if (sds.isStructDeclaration()) 887 m = "$(DDOC_STRUCT_MEMBERS "; 888 else if (sds.isEnumDeclaration()) 889 m = "$(DDOC_ENUM_MEMBERS "; 890 else if (sds.isModule()) 891 m = "$(DDOC_MODULE_MEMBERS "; 892 size_t offset1 = buf.length; // save starting offset 893 buf.writestring(m); 894 size_t offset2 = buf.length; // to see if we write anything 895 sc = sc.push(sds); 896 for (size_t i = 0; i < sds.members.dim; i++) 897 { 898 Dsymbol s = (*sds.members)[i]; 899 //printf("\ts = '%s'\n", s.toChars()); 900 // only expand if parent is a non-template (semantic won't work) 901 if (s.comment && s.isTemplateMixin() && s.parent && !s.parent.isTemplateDeclaration()) 902 expandTemplateMixinComments(cast(TemplateMixin)s, buf, sc); 903 emitComment(s, buf, sc); 904 } 905 emitComment(null, buf, sc); 906 sc.pop(); 907 if (buf.length == offset2) 908 { 909 /* Didn't write out any members, so back out last write 910 */ 911 buf.setsize(offset1); 912 } 913 else 914 buf.writestring(")"); 915 } 916 917 private void emitVisibility(ref OutBuffer buf, Import i) 918 { 919 // imports are private by default, which is different from other declarations 920 // so they should explicitly show their visibility 921 emitVisibility(buf, i.visibility); 922 } 923 924 private void emitVisibility(ref OutBuffer buf, Declaration d) 925 { 926 auto vis = d.visibility; 927 if (vis.kind != Visibility.Kind.undefined && vis.kind != Visibility.Kind.public_) 928 { 929 emitVisibility(buf, vis); 930 } 931 } 932 933 private void emitVisibility(ref OutBuffer buf, Visibility vis) 934 { 935 visibilityToBuffer(&buf, vis); 936 buf.writeByte(' '); 937 } 938 939 private void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc) 940 { 941 extern (C++) final class EmitComment : Visitor 942 { 943 alias visit = Visitor.visit; 944 public: 945 OutBuffer* buf; 946 Scope* sc; 947 948 extern (D) this(ref OutBuffer buf, Scope* sc) 949 { 950 this.buf = &buf; 951 this.sc = sc; 952 } 953 954 override void visit(Dsymbol) 955 { 956 } 957 958 override void visit(InvariantDeclaration) 959 { 960 } 961 962 override void visit(UnitTestDeclaration) 963 { 964 } 965 966 override void visit(PostBlitDeclaration) 967 { 968 } 969 970 override void visit(DtorDeclaration) 971 { 972 } 973 974 override void visit(StaticCtorDeclaration) 975 { 976 } 977 978 override void visit(StaticDtorDeclaration) 979 { 980 } 981 982 override void visit(TypeInfoDeclaration) 983 { 984 } 985 986 void emit(Scope* sc, Dsymbol s, const(char)* com) 987 { 988 if (s && sc.lastdc && isDitto(com)) 989 { 990 sc.lastdc.a.push(s); 991 return; 992 } 993 // Put previous doc comment if exists 994 if (DocComment* dc = sc.lastdc) 995 { 996 assert(dc.a.dim > 0, "Expects at least one declaration for a" ~ 997 "documentation comment"); 998 999 auto symbol = dc.a[0]; 1000 1001 buf.writestring("$(DDOC_MEMBER"); 1002 buf.writestring("$(DDOC_MEMBER_HEADER"); 1003 emitAnchor(*buf, symbol, sc, true); 1004 buf.writeByte(')'); 1005 1006 // Put the declaration signatures as the document 'title' 1007 buf.writestring(ddoc_decl_s); 1008 for (size_t i = 0; i < dc.a.dim; i++) 1009 { 1010 Dsymbol sx = dc.a[i]; 1011 // the added linebreaks in here make looking at multiple 1012 // signatures more appealing 1013 if (i == 0) 1014 { 1015 size_t o = buf.length; 1016 toDocBuffer(sx, *buf, sc); 1017 highlightCode(sc, sx, *buf, o); 1018 buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)"); 1019 continue; 1020 } 1021 buf.writestring("$(DDOC_DITTO "); 1022 { 1023 size_t o = buf.length; 1024 toDocBuffer(sx, *buf, sc); 1025 highlightCode(sc, sx, *buf, o); 1026 } 1027 buf.writestring("$(DDOC_OVERLOAD_SEPARATOR)"); 1028 buf.writeByte(')'); 1029 } 1030 buf.writestring(ddoc_decl_e); 1031 // Put the ddoc comment as the document 'description' 1032 buf.writestring(ddoc_decl_dd_s); 1033 { 1034 dc.writeSections(sc, &dc.a, buf); 1035 if (ScopeDsymbol sds = dc.a[0].isScopeDsymbol()) 1036 emitMemberComments(sds, *buf, sc); 1037 } 1038 buf.writestring(ddoc_decl_dd_e); 1039 buf.writeByte(')'); 1040 //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0); 1041 } 1042 if (s) 1043 { 1044 DocComment* dc = DocComment.parse(s, com); 1045 dc.pmacrotable = &sc._module.macrotable; 1046 sc.lastdc = dc; 1047 } 1048 } 1049 1050 override void visit(Import imp) 1051 { 1052 if (imp.visible().kind != Visibility.Kind.public_ && sc.visibility.kind != Visibility.Kind.export_) 1053 return; 1054 1055 if (imp.comment) 1056 emit(sc, imp, imp.comment); 1057 } 1058 1059 override void visit(Declaration d) 1060 { 1061 //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment); 1062 //printf("type = %p\n", d.type); 1063 const(char)* com = d.comment; 1064 if (TemplateDeclaration td = getEponymousParent(d)) 1065 { 1066 if (isDitto(td.comment)) 1067 com = td.comment; 1068 else 1069 com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true); 1070 } 1071 else 1072 { 1073 if (!d.ident) 1074 return; 1075 if (!d.type) 1076 { 1077 if (!d.isCtorDeclaration() && 1078 !d.isAliasDeclaration() && 1079 !d.isVarDeclaration()) 1080 { 1081 return; 1082 } 1083 } 1084 if (d.visibility.kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) 1085 return; 1086 } 1087 if (!com) 1088 return; 1089 emit(sc, d, com); 1090 } 1091 1092 override void visit(AggregateDeclaration ad) 1093 { 1094 //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars()); 1095 const(char)* com = ad.comment; 1096 if (TemplateDeclaration td = getEponymousParent(ad)) 1097 { 1098 if (isDitto(td.comment)) 1099 com = td.comment; 1100 else 1101 com = Lexer.combineComments(td.comment.toDString(), com.toDString(), true); 1102 } 1103 else 1104 { 1105 if (ad.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) 1106 return; 1107 if (!ad.comment) 1108 return; 1109 } 1110 if (!com) 1111 return; 1112 emit(sc, ad, com); 1113 } 1114 1115 override void visit(TemplateDeclaration td) 1116 { 1117 //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind()); 1118 if (td.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) 1119 return; 1120 if (!td.comment) 1121 return; 1122 if (Dsymbol ss = getEponymousMember(td)) 1123 { 1124 ss.accept(this); 1125 return; 1126 } 1127 emit(sc, td, td.comment); 1128 } 1129 1130 override void visit(EnumDeclaration ed) 1131 { 1132 if (ed.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) 1133 return; 1134 if (ed.isAnonymous() && ed.members) 1135 { 1136 for (size_t i = 0; i < ed.members.dim; i++) 1137 { 1138 Dsymbol s = (*ed.members)[i]; 1139 emitComment(s, *buf, sc); 1140 } 1141 return; 1142 } 1143 if (!ed.comment) 1144 return; 1145 if (ed.isAnonymous()) 1146 return; 1147 emit(sc, ed, ed.comment); 1148 } 1149 1150 override void visit(EnumMember em) 1151 { 1152 //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment); 1153 if (em.visible().kind == Visibility.Kind.private_ || sc.visibility.kind == Visibility.Kind.private_) 1154 return; 1155 if (!em.comment) 1156 return; 1157 emit(sc, em, em.comment); 1158 } 1159 1160 override void visit(AttribDeclaration ad) 1161 { 1162 //printf("AttribDeclaration::emitComment(sc = %p)\n", sc); 1163 /* A general problem with this, 1164 * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516 1165 * is that attributes are not transmitted through to the underlying 1166 * member declarations for template bodies, because semantic analysis 1167 * is not done for template declaration bodies 1168 * (only template instantiations). 1169 * Hence, Ddoc omits attributes from template members. 1170 */ 1171 Dsymbols* d = ad.include(null); 1172 if (d) 1173 { 1174 for (size_t i = 0; i < d.dim; i++) 1175 { 1176 Dsymbol s = (*d)[i]; 1177 //printf("AttribDeclaration::emitComment %s\n", s.toChars()); 1178 emitComment(s, *buf, sc); 1179 } 1180 } 1181 } 1182 1183 override void visit(VisibilityDeclaration pd) 1184 { 1185 if (pd.decl) 1186 { 1187 Scope* scx = sc; 1188 sc = sc.copy(); 1189 sc.visibility = pd.visibility; 1190 visit(cast(AttribDeclaration)pd); 1191 scx.lastdc = sc.lastdc; 1192 sc = sc.pop(); 1193 } 1194 } 1195 1196 override void visit(ConditionalDeclaration cd) 1197 { 1198 //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc); 1199 if (cd.condition.inc != Include.notComputed) 1200 { 1201 visit(cast(AttribDeclaration)cd); 1202 return; 1203 } 1204 /* If generating doc comment, be careful because if we're inside 1205 * a template, then include(null) will fail. 1206 */ 1207 Dsymbols* d = cd.decl ? cd.decl : cd.elsedecl; 1208 for (size_t i = 0; i < d.dim; i++) 1209 { 1210 Dsymbol s = (*d)[i]; 1211 emitComment(s, *buf, sc); 1212 } 1213 } 1214 } 1215 1216 scope EmitComment v = new EmitComment(buf, sc); 1217 if (!s) 1218 v.emit(sc, null, null); 1219 else 1220 s.accept(v); 1221 } 1222 1223 private void toDocBuffer(Dsymbol s, ref OutBuffer buf, Scope* sc) 1224 { 1225 extern (C++) final class ToDocBuffer : Visitor 1226 { 1227 alias visit = Visitor.visit; 1228 public: 1229 OutBuffer* buf; 1230 Scope* sc; 1231 1232 extern (D) this(ref OutBuffer buf, Scope* sc) 1233 { 1234 this.buf = &buf; 1235 this.sc = sc; 1236 } 1237 1238 override void visit(Dsymbol s) 1239 { 1240 //printf("Dsymbol::toDocbuffer() %s\n", s.toChars()); 1241 HdrGenState hgs; 1242 hgs.ddoc = true; 1243 .toCBuffer(s, buf, &hgs); 1244 } 1245 1246 void prefix(Dsymbol s) 1247 { 1248 if (s.isDeprecated()) 1249 buf.writestring("deprecated "); 1250 if (Declaration d = s.isDeclaration()) 1251 { 1252 emitVisibility(*buf, d); 1253 if (d.isStatic()) 1254 buf.writestring("static "); 1255 else if (d.isFinal()) 1256 buf.writestring("final "); 1257 else if (d.isAbstract()) 1258 buf.writestring("abstract "); 1259 1260 if (d.isFuncDeclaration()) // functionToBufferFull handles this 1261 return; 1262 1263 if (d.isImmutable()) 1264 buf.writestring("immutable "); 1265 if (d.storage_class & STC.shared_) 1266 buf.writestring("shared "); 1267 if (d.isWild()) 1268 buf.writestring("inout "); 1269 if (d.isConst()) 1270 buf.writestring("const "); 1271 1272 if (d.isSynchronized()) 1273 buf.writestring("synchronized "); 1274 1275 if (d.storage_class & STC.manifest) 1276 buf.writestring("enum "); 1277 1278 // Add "auto" for the untyped variable in template members 1279 if (!d.type && d.isVarDeclaration() && 1280 !d.isImmutable() && !(d.storage_class & STC.shared_) && !d.isWild() && !d.isConst() && 1281 !d.isSynchronized()) 1282 { 1283 buf.writestring("auto "); 1284 } 1285 } 1286 } 1287 1288 override void visit(Import i) 1289 { 1290 HdrGenState hgs; 1291 hgs.ddoc = true; 1292 emitVisibility(*buf, i); 1293 .toCBuffer(i, buf, &hgs); 1294 } 1295 1296 override void visit(Declaration d) 1297 { 1298 if (!d.ident) 1299 return; 1300 TemplateDeclaration td = getEponymousParent(d); 1301 //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--"); 1302 HdrGenState hgs; 1303 hgs.ddoc = true; 1304 if (d.isDeprecated()) 1305 buf.writestring("$(DEPRECATED "); 1306 prefix(d); 1307 if (d.type) 1308 { 1309 Type origType = d.originalType ? d.originalType : d.type; 1310 if (origType.ty == Tfunction) 1311 { 1312 functionToBufferFull(cast(TypeFunction)origType, buf, d.ident, &hgs, td); 1313 } 1314 else 1315 .toCBuffer(origType, buf, d.ident, &hgs); 1316 } 1317 else 1318 buf.writestring(d.ident.toString()); 1319 if (d.isVarDeclaration() && td) 1320 { 1321 buf.writeByte('('); 1322 if (td.origParameters && td.origParameters.dim) 1323 { 1324 for (size_t i = 0; i < td.origParameters.dim; i++) 1325 { 1326 if (i) 1327 buf.writestring(", "); 1328 toCBuffer((*td.origParameters)[i], buf, &hgs); 1329 } 1330 } 1331 buf.writeByte(')'); 1332 } 1333 // emit constraints if declaration is a templated declaration 1334 if (td && td.constraint) 1335 { 1336 bool noFuncDecl = td.isFuncDeclaration() is null; 1337 if (noFuncDecl) 1338 { 1339 buf.writestring("$(DDOC_CONSTRAINT "); 1340 } 1341 1342 .toCBuffer(td.constraint, buf, &hgs); 1343 1344 if (noFuncDecl) 1345 { 1346 buf.writestring(")"); 1347 } 1348 } 1349 if (d.isDeprecated()) 1350 buf.writestring(")"); 1351 buf.writestring(";\n"); 1352 } 1353 1354 override void visit(AliasDeclaration ad) 1355 { 1356 //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars()); 1357 if (!ad.ident) 1358 return; 1359 if (ad.isDeprecated()) 1360 buf.writestring("deprecated "); 1361 emitVisibility(*buf, ad); 1362 buf.printf("alias %s = ", ad.toChars()); 1363 if (Dsymbol s = ad.aliassym) // ident alias 1364 { 1365 prettyPrintDsymbol(s, ad.parent); 1366 } 1367 else if (Type type = ad.getType()) // type alias 1368 { 1369 if (type.ty == Tclass || type.ty == Tstruct || type.ty == Tenum) 1370 { 1371 if (Dsymbol s = type.toDsymbol(null)) // elaborate type 1372 prettyPrintDsymbol(s, ad.parent); 1373 else 1374 buf.writestring(type.toChars()); 1375 } 1376 else 1377 { 1378 // simple type 1379 buf.writestring(type.toChars()); 1380 } 1381 } 1382 buf.writestring(";\n"); 1383 } 1384 1385 void parentToBuffer(Dsymbol s) 1386 { 1387 if (s && !s.isPackage() && !s.isModule()) 1388 { 1389 parentToBuffer(s.parent); 1390 buf.writestring(s.toChars()); 1391 buf.writestring("."); 1392 } 1393 } 1394 1395 static bool inSameModule(Dsymbol s, Dsymbol p) 1396 { 1397 for (; s; s = s.parent) 1398 { 1399 if (s.isModule()) 1400 break; 1401 } 1402 for (; p; p = p.parent) 1403 { 1404 if (p.isModule()) 1405 break; 1406 } 1407 return s == p; 1408 } 1409 1410 void prettyPrintDsymbol(Dsymbol s, Dsymbol parent) 1411 { 1412 if (s.parent && (s.parent == parent)) // in current scope -> naked name 1413 { 1414 buf.writestring(s.toChars()); 1415 } 1416 else if (!inSameModule(s, parent)) // in another module -> full name 1417 { 1418 buf.writestring(s.toPrettyChars()); 1419 } 1420 else // nested in a type in this module -> full name w/o module name 1421 { 1422 // if alias is nested in a user-type use module-scope lookup 1423 if (!parent.isModule() && !parent.isPackage()) 1424 buf.writestring("."); 1425 parentToBuffer(s.parent); 1426 buf.writestring(s.toChars()); 1427 } 1428 } 1429 1430 override void visit(AggregateDeclaration ad) 1431 { 1432 if (!ad.ident) 1433 return; 1434 version (none) 1435 { 1436 emitVisibility(buf, ad); 1437 } 1438 buf.printf("%s %s", ad.kind(), ad.toChars()); 1439 buf.writestring(";\n"); 1440 } 1441 1442 override void visit(StructDeclaration sd) 1443 { 1444 //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars()); 1445 if (!sd.ident) 1446 return; 1447 version (none) 1448 { 1449 emitVisibility(buf, sd); 1450 } 1451 if (TemplateDeclaration td = getEponymousParent(sd)) 1452 { 1453 toDocBuffer(td, *buf, sc); 1454 } 1455 else 1456 { 1457 buf.printf("%s %s", sd.kind(), sd.toChars()); 1458 } 1459 buf.writestring(";\n"); 1460 } 1461 1462 override void visit(ClassDeclaration cd) 1463 { 1464 //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars()); 1465 if (!cd.ident) 1466 return; 1467 version (none) 1468 { 1469 emitVisibility(*buf, cd); 1470 } 1471 if (TemplateDeclaration td = getEponymousParent(cd)) 1472 { 1473 toDocBuffer(td, *buf, sc); 1474 } 1475 else 1476 { 1477 if (!cd.isInterfaceDeclaration() && cd.isAbstract()) 1478 buf.writestring("abstract "); 1479 buf.printf("%s %s", cd.kind(), cd.toChars()); 1480 } 1481 int any = 0; 1482 for (size_t i = 0; i < cd.baseclasses.dim; i++) 1483 { 1484 BaseClass* bc = (*cd.baseclasses)[i]; 1485 if (bc.sym && bc.sym.ident == Id.Object) 1486 continue; 1487 if (any) 1488 buf.writestring(", "); 1489 else 1490 { 1491 buf.writestring(": "); 1492 any = 1; 1493 } 1494 1495 if (bc.sym) 1496 { 1497 buf.printf("$(DDOC_PSUPER_SYMBOL %s)", bc.sym.toPrettyChars()); 1498 } 1499 else 1500 { 1501 HdrGenState hgs; 1502 .toCBuffer(bc.type, buf, null, &hgs); 1503 } 1504 } 1505 buf.writestring(";\n"); 1506 } 1507 1508 override void visit(EnumDeclaration ed) 1509 { 1510 if (!ed.ident) 1511 return; 1512 buf.printf("%s %s", ed.kind(), ed.toChars()); 1513 if (ed.memtype) 1514 { 1515 buf.writestring(": $(DDOC_ENUM_BASETYPE "); 1516 HdrGenState hgs; 1517 .toCBuffer(ed.memtype, buf, null, &hgs); 1518 buf.writestring(")"); 1519 } 1520 buf.writestring(";\n"); 1521 } 1522 1523 override void visit(EnumMember em) 1524 { 1525 if (!em.ident) 1526 return; 1527 buf.writestring(em.toChars()); 1528 } 1529 } 1530 1531 scope ToDocBuffer v = new ToDocBuffer(buf, sc); 1532 s.accept(v); 1533 } 1534 1535 /*********************************************************** 1536 */ 1537 struct DocComment 1538 { 1539 Sections sections; // Section*[] 1540 Section summary; 1541 Section copyright; 1542 Section macros; 1543 MacroTable* pmacrotable; 1544 Escape* escapetable; 1545 Dsymbols a; 1546 1547 static DocComment* parse(Dsymbol s, const(char)* comment) 1548 { 1549 //printf("parse(%s): '%s'\n", s.toChars(), comment); 1550 auto dc = new DocComment(); 1551 dc.a.push(s); 1552 if (!comment) 1553 return dc; 1554 dc.parseSections(comment); 1555 for (size_t i = 0; i < dc.sections.dim; i++) 1556 { 1557 Section sec = dc.sections[i]; 1558 if (iequals("copyright", sec.name)) 1559 { 1560 dc.copyright = sec; 1561 } 1562 if (iequals("macros", sec.name)) 1563 { 1564 dc.macros = sec; 1565 } 1566 } 1567 return dc; 1568 } 1569 1570 /************************************************ 1571 * Parse macros out of Macros: section. 1572 * Macros are of the form: 1573 * name1 = value1 1574 * 1575 * name2 = value2 1576 */ 1577 extern(D) static void parseMacros( 1578 Escape* escapetable, ref MacroTable pmacrotable, const(char)[] m) 1579 { 1580 const(char)* p = m.ptr; 1581 size_t len = m.length; 1582 const(char)* pend = p + len; 1583 const(char)* tempstart = null; 1584 size_t templen = 0; 1585 const(char)* namestart = null; 1586 size_t namelen = 0; // !=0 if line continuation 1587 const(char)* textstart = null; 1588 size_t textlen = 0; 1589 while (p < pend) 1590 { 1591 // Skip to start of macro 1592 while (1) 1593 { 1594 if (p >= pend) 1595 goto Ldone; 1596 switch (*p) 1597 { 1598 case ' ': 1599 case '\t': 1600 p++; 1601 continue; 1602 case '\r': 1603 case '\n': 1604 p++; 1605 goto Lcont; 1606 default: 1607 if (isIdStart(p)) 1608 break; 1609 if (namelen) 1610 goto Ltext; // continuation of prev macro 1611 goto Lskipline; 1612 } 1613 break; 1614 } 1615 tempstart = p; 1616 while (1) 1617 { 1618 if (p >= pend) 1619 goto Ldone; 1620 if (!isIdTail(p)) 1621 break; 1622 p += utfStride(p); 1623 } 1624 templen = p - tempstart; 1625 while (1) 1626 { 1627 if (p >= pend) 1628 goto Ldone; 1629 if (!(*p == ' ' || *p == '\t')) 1630 break; 1631 p++; 1632 } 1633 if (*p != '=') 1634 { 1635 if (namelen) 1636 goto Ltext; // continuation of prev macro 1637 goto Lskipline; 1638 } 1639 p++; 1640 if (p >= pend) 1641 goto Ldone; 1642 if (namelen) 1643 { 1644 // Output existing macro 1645 L1: 1646 //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart); 1647 if (iequals("ESCAPES", namestart[0 .. namelen])) 1648 parseEscapes(escapetable, textstart[0 .. textlen]); 1649 else 1650 pmacrotable.define(namestart[0 .. namelen], textstart[0 .. textlen]); 1651 namelen = 0; 1652 if (p >= pend) 1653 break; 1654 } 1655 namestart = tempstart; 1656 namelen = templen; 1657 while (p < pend && (*p == ' ' || *p == '\t')) 1658 p++; 1659 textstart = p; 1660 Ltext: 1661 while (p < pend && *p != '\r' && *p != '\n') 1662 p++; 1663 textlen = p - textstart; 1664 p++; 1665 //printf("p = %p, pend = %p\n", p, pend); 1666 Lcont: 1667 continue; 1668 Lskipline: 1669 // Ignore this line 1670 while (p < pend && *p != '\r' && *p != '\n') 1671 p++; 1672 } 1673 Ldone: 1674 if (namelen) 1675 goto L1; // write out last one 1676 } 1677 1678 /************************************** 1679 * Parse escapes of the form: 1680 * /c/string/ 1681 * where c is a single character. 1682 * Multiple escapes can be separated 1683 * by whitespace and/or commas. 1684 */ 1685 static void parseEscapes(Escape* escapetable, const(char)[] text) 1686 { 1687 if (!escapetable) 1688 { 1689 escapetable = new Escape(); 1690 memset(escapetable, 0, Escape.sizeof); 1691 } 1692 //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable); 1693 const(char)* p = text.ptr; 1694 const(char)* pend = p + text.length; 1695 while (1) 1696 { 1697 while (1) 1698 { 1699 if (p + 4 >= pend) 1700 return; 1701 if (!(*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ',')) 1702 break; 1703 p++; 1704 } 1705 if (p[0] != '/' || p[2] != '/') 1706 return; 1707 char c = p[1]; 1708 p += 3; 1709 const(char)* start = p; 1710 while (1) 1711 { 1712 if (p >= pend) 1713 return; 1714 if (*p == '/') 1715 break; 1716 p++; 1717 } 1718 size_t len = p - start; 1719 char* s = cast(char*)memcpy(mem.xmalloc(len + 1), start, len); 1720 s[len] = 0; 1721 escapetable.strings[c] = s[0 .. len]; 1722 //printf("\t%c = '%s'\n", c, s); 1723 p++; 1724 } 1725 } 1726 1727 /***************************************** 1728 * Parse next paragraph out of *pcomment. 1729 * Update *pcomment to point past paragraph. 1730 * Returns NULL if no more paragraphs. 1731 * If paragraph ends in 'identifier:', 1732 * then (*pcomment)[0 .. idlen] is the identifier. 1733 */ 1734 void parseSections(const(char)* comment) 1735 { 1736 const(char)* p; 1737 const(char)* pstart; 1738 const(char)* pend; 1739 const(char)* idstart = null; // dead-store to prevent spurious warning 1740 size_t idlen; 1741 const(char)* name = null; 1742 size_t namelen = 0; 1743 //printf("parseSections('%s')\n", comment); 1744 p = comment; 1745 while (*p) 1746 { 1747 const(char)* pstart0 = p; 1748 p = skipwhitespace(p); 1749 pstart = p; 1750 pend = p; 1751 1752 // Undo indent if starting with a list item 1753 if ((*p == '-' || *p == '+' || *p == '*') && (*(p+1) == ' ' || *(p+1) == '\t')) 1754 pstart = pstart0; 1755 else 1756 { 1757 const(char)* pitem = p; 1758 while (*pitem >= '0' && *pitem <= '9') 1759 ++pitem; 1760 if (pitem > p && *pitem == '.' && (*(pitem+1) == ' ' || *(pitem+1) == '\t')) 1761 pstart = pstart0; 1762 } 1763 1764 /* Find end of section, which is ended by one of: 1765 * 'identifier:' (but not inside a code section) 1766 * '\0' 1767 */ 1768 idlen = 0; 1769 int inCode = 0; 1770 while (1) 1771 { 1772 // Check for start/end of a code section 1773 if (*p == '-' || *p == '`' || *p == '~') 1774 { 1775 char c = *p; 1776 int numdash = 0; 1777 while (*p == c) 1778 { 1779 ++numdash; 1780 p++; 1781 } 1782 // BUG: handle UTF PS and LS too 1783 if ((!*p || *p == '\r' || *p == '\n' || (!inCode && c != '-')) && numdash >= 3) 1784 { 1785 inCode = inCode == c ? false : c; 1786 if (inCode) 1787 { 1788 // restore leading indentation 1789 while (pstart0 < pstart && isIndentWS(pstart - 1)) 1790 --pstart; 1791 } 1792 } 1793 pend = p; 1794 } 1795 if (!inCode && isIdStart(p)) 1796 { 1797 const(char)* q = p + utfStride(p); 1798 while (isIdTail(q)) 1799 q += utfStride(q); 1800 1801 // Detected tag ends it 1802 if (*q == ':' && isupper(*p) 1803 && (isspace(q[1]) || q[1] == 0)) 1804 { 1805 idlen = q - p; 1806 idstart = p; 1807 for (pend = p; pend > pstart; pend--) 1808 { 1809 if (pend[-1] == '\n') 1810 break; 1811 } 1812 p = q + 1; 1813 break; 1814 } 1815 } 1816 while (1) 1817 { 1818 if (!*p) 1819 goto L1; 1820 if (*p == '\n') 1821 { 1822 p++; 1823 if (*p == '\n' && !summary && !namelen && !inCode) 1824 { 1825 pend = p; 1826 p++; 1827 goto L1; 1828 } 1829 break; 1830 } 1831 p++; 1832 pend = p; 1833 } 1834 p = skipwhitespace(p); 1835 } 1836 L1: 1837 if (namelen || pstart < pend) 1838 { 1839 Section s; 1840 if (iequals("Params", name[0 .. namelen])) 1841 s = new ParamSection(); 1842 else if (iequals("Macros", name[0 .. namelen])) 1843 s = new MacroSection(); 1844 else 1845 s = new Section(); 1846 s.name = name[0 .. namelen]; 1847 s.body_ = pstart[0 .. pend - pstart]; 1848 s.nooutput = 0; 1849 //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body); 1850 sections.push(s); 1851 if (!summary && !namelen) 1852 summary = s; 1853 } 1854 if (idlen) 1855 { 1856 name = idstart; 1857 namelen = idlen; 1858 } 1859 else 1860 { 1861 name = null; 1862 namelen = 0; 1863 if (!*p) 1864 break; 1865 } 1866 } 1867 } 1868 1869 void writeSections(Scope* sc, Dsymbols* a, OutBuffer* buf) 1870 { 1871 assert(a.dim); 1872 //printf("DocComment::writeSections()\n"); 1873 Loc loc = (*a)[0].loc; 1874 if (Module m = (*a)[0].isModule()) 1875 { 1876 if (m.md) 1877 loc = m.md.loc; 1878 } 1879 size_t offset1 = buf.length; 1880 buf.writestring("$(DDOC_SECTIONS "); 1881 size_t offset2 = buf.length; 1882 for (size_t i = 0; i < sections.dim; i++) 1883 { 1884 Section sec = sections[i]; 1885 if (sec.nooutput) 1886 continue; 1887 //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body); 1888 if (!sec.name.length && i == 0) 1889 { 1890 buf.writestring("$(DDOC_SUMMARY "); 1891 size_t o = buf.length; 1892 buf.write(sec.body_); 1893 escapeStrayParenthesis(loc, buf, o, true); 1894 highlightText(sc, a, loc, *buf, o); 1895 buf.writestring(")"); 1896 } 1897 else 1898 sec.write(loc, &this, sc, a, buf); 1899 } 1900 for (size_t i = 0; i < a.dim; i++) 1901 { 1902 Dsymbol s = (*a)[i]; 1903 if (Dsymbol td = getEponymousParent(s)) 1904 s = td; 1905 for (UnitTestDeclaration utd = s.ddocUnittest; utd; utd = utd.ddocUnittest) 1906 { 1907 if (utd.visibility.kind == Visibility.Kind.private_ || !utd.comment || !utd.fbody) 1908 continue; 1909 // Strip whitespaces to avoid showing empty summary 1910 const(char)* c = utd.comment; 1911 while (*c == ' ' || *c == '\t' || *c == '\n' || *c == '\r') 1912 ++c; 1913 buf.writestring("$(DDOC_EXAMPLES "); 1914 size_t o = buf.length; 1915 buf.writestring(cast(char*)c); 1916 if (utd.codedoc) 1917 { 1918 auto codedoc = utd.codedoc.stripLeadingNewlines; 1919 size_t n = getCodeIndent(codedoc); 1920 while (n--) 1921 buf.writeByte(' '); 1922 buf.writestring("----\n"); 1923 buf.writestring(codedoc); 1924 buf.writestring("----\n"); 1925 highlightText(sc, a, loc, *buf, o); 1926 } 1927 buf.writestring(")"); 1928 } 1929 } 1930 if (buf.length == offset2) 1931 { 1932 /* Didn't write out any sections, so back out last write 1933 */ 1934 buf.setsize(offset1); 1935 buf.writestring("\n"); 1936 } 1937 else 1938 buf.writestring(")"); 1939 } 1940 } 1941 1942 /***************************************** 1943 * Return true if comment consists entirely of "ditto". 1944 */ 1945 private bool isDitto(const(char)* comment) 1946 { 1947 if (comment) 1948 { 1949 const(char)* p = skipwhitespace(comment); 1950 if (Port.memicmp(p, "ditto", 5) == 0 && *skipwhitespace(p + 5) == 0) 1951 return true; 1952 } 1953 return false; 1954 } 1955 1956 /********************************************** 1957 * Skip white space. 1958 */ 1959 private const(char)* skipwhitespace(const(char)* p) 1960 { 1961 return skipwhitespace(p.toDString).ptr; 1962 } 1963 1964 /// Ditto 1965 private const(char)[] skipwhitespace(const(char)[] p) 1966 { 1967 foreach (idx, char c; p) 1968 { 1969 switch (c) 1970 { 1971 case ' ': 1972 case '\t': 1973 case '\n': 1974 continue; 1975 default: 1976 return p[idx .. $]; 1977 } 1978 } 1979 return p[$ .. $]; 1980 } 1981 1982 /************************************************ 1983 * Scan past all instances of the given characters. 1984 * Params: 1985 * buf = an OutBuffer containing the DDoc 1986 * i = the index within `buf` to start scanning from 1987 * chars = the characters to skip; order is unimportant 1988 * Returns: the index after skipping characters. 1989 */ 1990 private size_t skipChars(ref OutBuffer buf, size_t i, string chars) 1991 { 1992 Outer: 1993 foreach (j, c; buf[][i..$]) 1994 { 1995 foreach (d; chars) 1996 { 1997 if (d == c) 1998 continue Outer; 1999 } 2000 return i + j; 2001 } 2002 return buf.length; 2003 } 2004 2005 unittest { 2006 OutBuffer buf; 2007 string data = "test ---\r\n\r\nend"; 2008 buf.write(data); 2009 2010 assert(skipChars(buf, 0, "-") == 0); 2011 assert(skipChars(buf, 4, "-") == 4); 2012 assert(skipChars(buf, 4, " -") == 8); 2013 assert(skipChars(buf, 8, "\r\n") == 12); 2014 assert(skipChars(buf, 12, "dne") == 15); 2015 } 2016 2017 /**************************************************** 2018 * Replace all instances of `c` with `r` in the given string 2019 * Params: 2020 * s = the string to do replacements in 2021 * c = the character to look for 2022 * r = the string to replace `c` with 2023 * Returns: `s` with `c` replaced with `r` 2024 */ 2025 private inout(char)[] replaceChar(inout(char)[] s, char c, string r) pure 2026 { 2027 int count = 0; 2028 foreach (char sc; s) 2029 if (sc == c) 2030 ++count; 2031 if (count == 0) 2032 return s; 2033 2034 char[] result; 2035 result.reserve(s.length - count + (r.length * count)); 2036 size_t start = 0; 2037 foreach (i, char sc; s) 2038 { 2039 if (sc == c) 2040 { 2041 result ~= s[start..i]; 2042 result ~= r; 2043 start = i+1; 2044 } 2045 } 2046 result ~= s[start..$]; 2047 return result; 2048 } 2049 2050 /// 2051 unittest 2052 { 2053 assert("".replaceChar(',', "$(COMMA)") == ""); 2054 assert("ab".replaceChar(',', "$(COMMA)") == "ab"); 2055 assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b"); 2056 assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b"); 2057 assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab"); 2058 assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)"); 2059 } 2060 2061 /** 2062 * Return a lowercased copy of a string. 2063 * Params: 2064 * s = the string to lowercase 2065 * Returns: the lowercase version of the string or the original if already lowercase 2066 */ 2067 private string toLowercase(string s) pure 2068 { 2069 string lower; 2070 foreach (size_t i; 0..s.length) 2071 { 2072 char c = s[i]; 2073 // TODO: maybe unicode lowercase, somehow 2074 if (c >= 'A' && c <= 'Z') 2075 { 2076 if (!lower.length) { 2077 lower.reserve(s.length); 2078 } 2079 lower ~= s[lower.length..i]; 2080 c += 'a' - 'A'; 2081 lower ~= c; 2082 } 2083 } 2084 if (lower.length) 2085 lower ~= s[lower.length..$]; 2086 else 2087 lower = s; 2088 return lower; 2089 } 2090 2091 /// 2092 unittest 2093 { 2094 assert("".toLowercase == ""); 2095 assert("abc".toLowercase == "abc"); 2096 assert("ABC".toLowercase == "abc"); 2097 assert("aBc".toLowercase == "abc"); 2098 } 2099 2100 /************************************************ 2101 * Get the indent from one index to another, counting tab stops as four spaces wide 2102 * per the Markdown spec. 2103 * Params: 2104 * buf = an OutBuffer containing the DDoc 2105 * from = the index within `buf` to start counting from, inclusive 2106 * to = the index within `buf` to stop counting at, exclusive 2107 * Returns: the indent 2108 */ 2109 private int getMarkdownIndent(ref OutBuffer buf, size_t from, size_t to) 2110 { 2111 const slice = buf[]; 2112 if (to > slice.length) 2113 to = slice.length; 2114 int indent = 0; 2115 foreach (const c; slice[from..to]) 2116 indent += (c == '\t') ? 4 - (indent % 4) : 1; 2117 return indent; 2118 } 2119 2120 /************************************************ 2121 * Scan forward to one of: 2122 * start of identifier 2123 * beginning of next line 2124 * end of buf 2125 */ 2126 size_t skiptoident(ref OutBuffer buf, size_t i) 2127 { 2128 const slice = buf[]; 2129 while (i < slice.length) 2130 { 2131 dchar c; 2132 size_t oi = i; 2133 if (utf_decodeChar(slice, i, c)) 2134 { 2135 /* Ignore UTF errors, but still consume input 2136 */ 2137 break; 2138 } 2139 if (c >= 0x80) 2140 { 2141 if (!isUniAlpha(c)) 2142 continue; 2143 } 2144 else if (!(isalpha(c) || c == '_' || c == '\n')) 2145 continue; 2146 i = oi; 2147 break; 2148 } 2149 return i; 2150 } 2151 2152 /************************************************ 2153 * Scan forward past end of identifier. 2154 */ 2155 private size_t skippastident(ref OutBuffer buf, size_t i) 2156 { 2157 const slice = buf[]; 2158 while (i < slice.length) 2159 { 2160 dchar c; 2161 size_t oi = i; 2162 if (utf_decodeChar(slice, i, c)) 2163 { 2164 /* Ignore UTF errors, but still consume input 2165 */ 2166 break; 2167 } 2168 if (c >= 0x80) 2169 { 2170 if (isUniAlpha(c)) 2171 continue; 2172 } 2173 else if (isalnum(c) || c == '_') 2174 continue; 2175 i = oi; 2176 break; 2177 } 2178 return i; 2179 } 2180 2181 /************************************************ 2182 * Scan forward past end of an identifier that might 2183 * contain dots (e.g. `abc.def`) 2184 */ 2185 private size_t skipPastIdentWithDots(ref OutBuffer buf, size_t i) 2186 { 2187 const slice = buf[]; 2188 bool lastCharWasDot; 2189 while (i < slice.length) 2190 { 2191 dchar c; 2192 size_t oi = i; 2193 if (utf_decodeChar(slice, i, c)) 2194 { 2195 /* Ignore UTF errors, but still consume input 2196 */ 2197 break; 2198 } 2199 if (c == '.') 2200 { 2201 // We need to distinguish between `abc.def`, abc..def`, and `abc.` 2202 // Only `abc.def` is a valid identifier 2203 2204 if (lastCharWasDot) 2205 { 2206 i = oi; 2207 break; 2208 } 2209 2210 lastCharWasDot = true; 2211 continue; 2212 } 2213 else 2214 { 2215 if (c >= 0x80) 2216 { 2217 if (isUniAlpha(c)) 2218 { 2219 lastCharWasDot = false; 2220 continue; 2221 } 2222 } 2223 else if (isalnum(c) || c == '_') 2224 { 2225 lastCharWasDot = false; 2226 continue; 2227 } 2228 i = oi; 2229 break; 2230 } 2231 } 2232 2233 // if `abc.` 2234 if (lastCharWasDot) 2235 return i - 1; 2236 2237 return i; 2238 } 2239 2240 /************************************************ 2241 * Scan forward past URL starting at i. 2242 * We don't want to highlight parts of a URL. 2243 * Returns: 2244 * i if not a URL 2245 * index just past it if it is a URL 2246 */ 2247 private size_t skippastURL(ref OutBuffer buf, size_t i) 2248 { 2249 const slice = buf[][i .. $]; 2250 size_t j; 2251 bool sawdot = false; 2252 if (slice.length > 7 && Port.memicmp(slice.ptr, "http://", 7) == 0) 2253 { 2254 j = 7; 2255 } 2256 else if (slice.length > 8 && Port.memicmp(slice.ptr, "https://", 8) == 0) 2257 { 2258 j = 8; 2259 } 2260 else 2261 goto Lno; 2262 for (; j < slice.length; j++) 2263 { 2264 const c = slice[j]; 2265 if (isalnum(c)) 2266 continue; 2267 if (c == '-' || c == '_' || c == '?' || c == '=' || c == '%' || 2268 c == '&' || c == '/' || c == '+' || c == '#' || c == '~') 2269 continue; 2270 if (c == '.') 2271 { 2272 sawdot = true; 2273 continue; 2274 } 2275 break; 2276 } 2277 if (sawdot) 2278 return i + j; 2279 Lno: 2280 return i; 2281 } 2282 2283 /**************************************************** 2284 * Remove a previously-inserted blank line macro. 2285 * Params: 2286 * buf = an OutBuffer containing the DDoc 2287 * iAt = the index within `buf` of the start of the `$(DDOC_BLANKLINE)` 2288 * macro. Upon function return its value is set to `0`. 2289 * i = an index within `buf`. If `i` is after `iAt` then it gets 2290 * reduced by the length of the removed macro. 2291 */ 2292 private void removeBlankLineMacro(ref OutBuffer buf, ref size_t iAt, ref size_t i) 2293 { 2294 if (!iAt) 2295 return; 2296 2297 enum macroLength = "$(DDOC_BLANKLINE)".length; 2298 buf.remove(iAt, macroLength); 2299 if (i > iAt) 2300 i -= macroLength; 2301 iAt = 0; 2302 } 2303 2304 /**************************************************** 2305 * Attempt to detect and replace a Markdown thematic break (HR). These are three 2306 * or more of the same delimiter, optionally with spaces or tabs between any of 2307 * them, e.g. `\n- - -\n` becomes `\n$(HR)\n` 2308 * Params: 2309 * buf = an OutBuffer containing the DDoc 2310 * i = the index within `buf` of the first character of a potential 2311 * thematic break. If the replacement is made `i` changes to 2312 * point to the closing parenthesis of the `$(HR)` macro. 2313 * iLineStart = the index within `buf` that the thematic break's line starts at 2314 * loc = the current location within the file 2315 * Returns: whether a thematic break was replaced 2316 */ 2317 private bool replaceMarkdownThematicBreak(ref OutBuffer buf, ref size_t i, size_t iLineStart, const ref Loc loc) 2318 { 2319 if (!global.params.markdown) 2320 return false; 2321 2322 const slice = buf[]; 2323 const c = buf[i]; 2324 size_t j = i + 1; 2325 int repeat = 1; 2326 for (; j < slice.length; j++) 2327 { 2328 if (buf[j] == c) 2329 ++repeat; 2330 else if (buf[j] != ' ' && buf[j] != '\t') 2331 break; 2332 } 2333 if (repeat >= 3) 2334 { 2335 if (j >= buf.length || buf[j] == '\n' || buf[j] == '\r') 2336 { 2337 if (global.params.vmarkdown) 2338 { 2339 const s = buf[][i..j]; 2340 message(loc, "Ddoc: converted '%.*s' to a thematic break", cast(int)s.length, s.ptr); 2341 } 2342 2343 buf.remove(iLineStart, j - iLineStart); 2344 i = buf.insert(iLineStart, "$(HR)") - 1; 2345 return true; 2346 } 2347 } 2348 return false; 2349 } 2350 2351 /**************************************************** 2352 * Detect the level of an ATX-style heading, e.g. `## This is a heading` would 2353 * have a level of `2`. 2354 * Params: 2355 * buf = an OutBuffer containing the DDoc 2356 * i = the index within `buf` of the first `#` character 2357 * Returns: 2358 * the detected heading level from 1 to 6, or 2359 * 0 if not at an ATX heading 2360 */ 2361 private int detectAtxHeadingLevel(ref OutBuffer buf, const size_t i) 2362 { 2363 if (!global.params.markdown) 2364 return 0; 2365 2366 const iHeadingStart = i; 2367 const iAfterHashes = skipChars(buf, i, "#"); 2368 const headingLevel = cast(int) (iAfterHashes - iHeadingStart); 2369 if (headingLevel > 6) 2370 return 0; 2371 2372 const iTextStart = skipChars(buf, iAfterHashes, " \t"); 2373 const emptyHeading = buf[iTextStart] == '\r' || buf[iTextStart] == '\n'; 2374 2375 // require whitespace 2376 if (!emptyHeading && iTextStart == iAfterHashes) 2377 return 0; 2378 2379 return headingLevel; 2380 } 2381 2382 /**************************************************** 2383 * Remove any trailing `##` suffix from an ATX-style heading. 2384 * Params: 2385 * buf = an OutBuffer containing the DDoc 2386 * i = the index within `buf` to start looking for a suffix at 2387 */ 2388 private void removeAnyAtxHeadingSuffix(ref OutBuffer buf, size_t i) 2389 { 2390 size_t j = i; 2391 size_t iSuffixStart = 0; 2392 size_t iWhitespaceStart = j; 2393 const slice = buf[]; 2394 for (; j < slice.length; j++) 2395 { 2396 switch (slice[j]) 2397 { 2398 case '#': 2399 if (iWhitespaceStart && !iSuffixStart) 2400 iSuffixStart = j; 2401 continue; 2402 case ' ': 2403 case '\t': 2404 if (!iWhitespaceStart) 2405 iWhitespaceStart = j; 2406 continue; 2407 case '\r': 2408 case '\n': 2409 break; 2410 default: 2411 iSuffixStart = 0; 2412 iWhitespaceStart = 0; 2413 continue; 2414 } 2415 break; 2416 } 2417 if (iSuffixStart) 2418 buf.remove(iWhitespaceStart, j - iWhitespaceStart); 2419 } 2420 2421 /**************************************************** 2422 * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`). 2423 * Params: 2424 * buf = an OutBuffer containing the DDoc 2425 * iStart = the index within `buf` that the Markdown heading starts at 2426 * iEnd = the index within `buf` of the character after the last 2427 * heading character. Is incremented by the length of the 2428 * inserted heading macro when this function ends. 2429 * loc = the location of the Ddoc within the file 2430 * headingLevel = the level (1-6) of heading to end. Is set to `0` when this 2431 * function ends. 2432 */ 2433 private void endMarkdownHeading(ref OutBuffer buf, size_t iStart, ref size_t iEnd, const ref Loc loc, ref int headingLevel) 2434 { 2435 if (!global.params.markdown) 2436 return; 2437 if (global.params.vmarkdown) 2438 { 2439 const s = buf[][iStart..iEnd]; 2440 message(loc, "Ddoc: added heading '%.*s'", cast(int)s.length, s.ptr); 2441 } 2442 2443 char[5] heading = "$(H0 "; 2444 heading[3] = cast(char) ('0' + headingLevel); 2445 buf.insert(iStart, heading); 2446 iEnd += 5; 2447 size_t iBeforeNewline = iEnd; 2448 while (buf[iBeforeNewline-1] == '\r' || buf[iBeforeNewline-1] == '\n') 2449 --iBeforeNewline; 2450 buf.insert(iBeforeNewline, ")"); 2451 headingLevel = 0; 2452 } 2453 2454 /**************************************************** 2455 * End all nested Markdown quotes, if inside any. 2456 * Params: 2457 * buf = an OutBuffer containing the DDoc 2458 * i = the index within `buf` of the character after the quote text. 2459 * quoteLevel = the current quote level. Is set to `0` when this function ends. 2460 * Returns: the amount that `i` was moved 2461 */ 2462 private size_t endAllMarkdownQuotes(ref OutBuffer buf, size_t i, ref int quoteLevel) 2463 { 2464 const length = quoteLevel; 2465 for (; quoteLevel > 0; --quoteLevel) 2466 i = buf.insert(i, ")"); 2467 return length; 2468 } 2469 2470 /**************************************************** 2471 * Convenience function to end all Markdown lists and quotes, if inside any, and 2472 * set `quoteMacroLevel` to `0`. 2473 * Params: 2474 * buf = an OutBuffer containing the DDoc 2475 * i = the index within `buf` of the character after the list and/or 2476 * quote text. Is adjusted when this function ends if any lists 2477 * and/or quotes were ended. 2478 * nestedLists = a set of nested lists. Upon return it will be empty. 2479 * quoteLevel = the current quote level. Is set to `0` when this function ends. 2480 * quoteMacroLevel = the macro level that the quote was started at. Is set to 2481 * `0` when this function ends. 2482 * Returns: the amount that `i` was moved 2483 */ 2484 private size_t endAllListsAndQuotes(ref OutBuffer buf, ref size_t i, ref MarkdownList[] nestedLists, ref int quoteLevel, out int quoteMacroLevel) 2485 { 2486 quoteMacroLevel = 0; 2487 const i0 = i; 2488 i += MarkdownList.endAllNestedLists(buf, i, nestedLists); 2489 i += endAllMarkdownQuotes(buf, i, quoteLevel); 2490 return i - i0; 2491 } 2492 2493 /**************************************************** 2494 * Replace Markdown emphasis with the appropriate macro, 2495 * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`. 2496 * Params: 2497 * buf = an OutBuffer containing the DDoc 2498 * loc = the current location within the file 2499 * inlineDelimiters = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`. 2500 * downToLevel = the length within `inlineDelimiters`` to reduce emphasis to 2501 * Returns: the number of characters added to the buffer by the replacements 2502 */ 2503 private size_t replaceMarkdownEmphasis(ref OutBuffer buf, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int downToLevel = 0) 2504 { 2505 if (!global.params.markdown) 2506 return 0; 2507 2508 size_t replaceEmphasisPair(ref MarkdownDelimiter start, ref MarkdownDelimiter end) 2509 { 2510 immutable count = start.count == 1 || end.count == 1 ? 1 : 2; 2511 2512 size_t iStart = start.iStart; 2513 size_t iEnd = end.iStart; 2514 end.count -= count; 2515 start.count -= count; 2516 iStart += start.count; 2517 2518 if (!start.count) 2519 start.type = 0; 2520 if (!end.count) 2521 end.type = 0; 2522 2523 if (global.params.vmarkdown) 2524 { 2525 const s = buf[][iStart + count..iEnd]; 2526 message(loc, "Ddoc: emphasized text '%.*s'", cast(int)s.length, s.ptr); 2527 } 2528 2529 buf.remove(iStart, count); 2530 iEnd -= count; 2531 buf.remove(iEnd, count); 2532 2533 string macroName = count >= 2 ? "$(STRONG " : "$(EM "; 2534 buf.insert(iEnd, ")"); 2535 buf.insert(iStart, macroName); 2536 2537 const delta = 1 + macroName.length - (count + count); 2538 end.iStart += count; 2539 return delta; 2540 } 2541 2542 size_t delta = 0; 2543 int start = (cast(int) inlineDelimiters.length) - 1; 2544 while (start >= downToLevel) 2545 { 2546 // find start emphasis 2547 while (start >= downToLevel && 2548 (inlineDelimiters[start].type != '*' || !inlineDelimiters[start].leftFlanking)) 2549 --start; 2550 if (start < downToLevel) 2551 break; 2552 2553 // find the nearest end emphasis 2554 int end = start + 1; 2555 while (end < inlineDelimiters.length && 2556 (inlineDelimiters[end].type != inlineDelimiters[start].type || 2557 inlineDelimiters[end].macroLevel != inlineDelimiters[start].macroLevel || 2558 !inlineDelimiters[end].rightFlanking)) 2559 ++end; 2560 if (end == inlineDelimiters.length) 2561 { 2562 // the start emphasis has no matching end; if it isn't an end itself then kill it 2563 if (!inlineDelimiters[start].rightFlanking) 2564 inlineDelimiters[start].type = 0; 2565 --start; 2566 continue; 2567 } 2568 2569 // multiple-of-3 rule 2570 if (((inlineDelimiters[start].leftFlanking && inlineDelimiters[start].rightFlanking) || 2571 (inlineDelimiters[end].leftFlanking && inlineDelimiters[end].rightFlanking)) && 2572 (inlineDelimiters[start].count + inlineDelimiters[end].count) % 3 == 0) 2573 { 2574 --start; 2575 continue; 2576 } 2577 2578 immutable delta0 = replaceEmphasisPair(inlineDelimiters[start], inlineDelimiters[end]); 2579 2580 for (; end < inlineDelimiters.length; ++end) 2581 inlineDelimiters[end].iStart += delta0; 2582 delta += delta0; 2583 } 2584 2585 inlineDelimiters.length = downToLevel; 2586 return delta; 2587 } 2588 2589 /**************************************************** 2590 */ 2591 private bool isIdentifier(Dsymbols* a, const(char)* p, size_t len) 2592 { 2593 foreach (member; *a) 2594 { 2595 if (auto imp = member.isImport()) 2596 { 2597 // For example: `public import str = core.stdc.string;` 2598 // This checks if `p` is equal to `str` 2599 if (imp.aliasId) 2600 { 2601 if (p[0 .. len] == imp.aliasId.toString()) 2602 return true; 2603 } 2604 else 2605 { 2606 // The general case: `public import core.stdc.string;` 2607 2608 // fully qualify imports so `core.stdc.string` doesn't appear as `core` 2609 string fullyQualifiedImport; 2610 foreach (const pid; imp.packages) 2611 { 2612 fullyQualifiedImport ~= pid.toString() ~ "."; 2613 } 2614 fullyQualifiedImport ~= imp.id.toString(); 2615 2616 // Check if `p` == `core.stdc.string` 2617 if (p[0 .. len] == fullyQualifiedImport) 2618 return true; 2619 } 2620 } 2621 else if (member.ident) 2622 { 2623 if (p[0 .. len] == member.ident.toString()) 2624 return true; 2625 } 2626 2627 } 2628 return false; 2629 } 2630 2631 /**************************************************** 2632 */ 2633 private bool isKeyword(const(char)* p, size_t len) 2634 { 2635 immutable string[3] table = ["true", "false", "null"]; 2636 foreach (s; table) 2637 { 2638 if (p[0 .. len] == s) 2639 return true; 2640 } 2641 return false; 2642 } 2643 2644 /**************************************************** 2645 */ 2646 private TypeFunction isTypeFunction(Dsymbol s) 2647 { 2648 FuncDeclaration f = s.isFuncDeclaration(); 2649 /* f.type may be NULL for template members. 2650 */ 2651 if (f && f.type) 2652 { 2653 Type t = f.originalType ? f.originalType : f.type; 2654 if (t.ty == Tfunction) 2655 return cast(TypeFunction)t; 2656 } 2657 return null; 2658 } 2659 2660 /**************************************************** 2661 */ 2662 private Parameter isFunctionParameter(Dsymbol s, const(char)* p, size_t len) 2663 { 2664 TypeFunction tf = isTypeFunction(s); 2665 if (tf && tf.parameterList.parameters) 2666 { 2667 foreach (fparam; *tf.parameterList.parameters) 2668 { 2669 if (fparam.ident && p[0 .. len] == fparam.ident.toString()) 2670 { 2671 return fparam; 2672 } 2673 } 2674 } 2675 return null; 2676 } 2677 2678 /**************************************************** 2679 */ 2680 private Parameter isFunctionParameter(Dsymbols* a, const(char)* p, size_t len) 2681 { 2682 for (size_t i = 0; i < a.dim; i++) 2683 { 2684 Parameter fparam = isFunctionParameter((*a)[i], p, len); 2685 if (fparam) 2686 { 2687 return fparam; 2688 } 2689 } 2690 return null; 2691 } 2692 2693 /**************************************************** 2694 */ 2695 private Parameter isEponymousFunctionParameter(Dsymbols *a, const(char) *p, size_t len) 2696 { 2697 for (size_t i = 0; i < a.dim; i++) 2698 { 2699 TemplateDeclaration td = (*a)[i].isTemplateDeclaration(); 2700 if (td && td.onemember) 2701 { 2702 /* Case 1: we refer to a template declaration inside the template 2703 2704 /// ...ddoc... 2705 template case1(T) { 2706 void case1(R)() {} 2707 } 2708 */ 2709 td = td.onemember.isTemplateDeclaration(); 2710 } 2711 if (!td) 2712 { 2713 /* Case 2: we're an alias to a template declaration 2714 2715 /// ...ddoc... 2716 alias case2 = case1!int; 2717 */ 2718 AliasDeclaration ad = (*a)[i].isAliasDeclaration(); 2719 if (ad && ad.aliassym) 2720 { 2721 td = ad.aliassym.isTemplateDeclaration(); 2722 } 2723 } 2724 while (td) 2725 { 2726 Dsymbol sym = getEponymousMember(td); 2727 if (sym) 2728 { 2729 Parameter fparam = isFunctionParameter(sym, p, len); 2730 if (fparam) 2731 { 2732 return fparam; 2733 } 2734 } 2735 td = td.overnext; 2736 } 2737 } 2738 return null; 2739 } 2740 2741 /**************************************************** 2742 */ 2743 private TemplateParameter isTemplateParameter(Dsymbols* a, const(char)* p, size_t len) 2744 { 2745 for (size_t i = 0; i < a.dim; i++) 2746 { 2747 TemplateDeclaration td = (*a)[i].isTemplateDeclaration(); 2748 // Check for the parent, if the current symbol is not a template declaration. 2749 if (!td) 2750 td = getEponymousParent((*a)[i]); 2751 if (td && td.origParameters) 2752 { 2753 foreach (tp; *td.origParameters) 2754 { 2755 if (tp.ident && p[0 .. len] == tp.ident.toString()) 2756 { 2757 return tp; 2758 } 2759 } 2760 } 2761 } 2762 return null; 2763 } 2764 2765 /**************************************************** 2766 * Return true if str is a reserved symbol name 2767 * that starts with a double underscore. 2768 */ 2769 private bool isReservedName(const(char)[] str) 2770 { 2771 immutable string[] table = 2772 [ 2773 "__ctor", 2774 "__dtor", 2775 "__postblit", 2776 "__invariant", 2777 "__unitTest", 2778 "__require", 2779 "__ensure", 2780 "__dollar", 2781 "__ctfe", 2782 "__withSym", 2783 "__result", 2784 "__returnLabel", 2785 "__vptr", 2786 "__monitor", 2787 "__gate", 2788 "__xopEquals", 2789 "__xopCmp", 2790 "__LINE__", 2791 "__FILE__", 2792 "__MODULE__", 2793 "__FUNCTION__", 2794 "__PRETTY_FUNCTION__", 2795 "__DATE__", 2796 "__TIME__", 2797 "__TIMESTAMP__", 2798 "__VENDOR__", 2799 "__VERSION__", 2800 "__EOF__", 2801 "__CXXLIB__", 2802 "__LOCAL_SIZE", 2803 "__entrypoint", 2804 ]; 2805 foreach (s; table) 2806 { 2807 if (str == s) 2808 return true; 2809 } 2810 return false; 2811 } 2812 2813 /**************************************************** 2814 * A delimiter for Markdown inline content like emphasis and links. 2815 */ 2816 private struct MarkdownDelimiter 2817 { 2818 size_t iStart; /// the index where this delimiter starts 2819 int count; /// the length of this delimeter's start sequence 2820 int macroLevel; /// the count of nested DDoc macros when the delimiter is started 2821 bool leftFlanking; /// whether the delimiter is left-flanking, as defined by the CommonMark spec 2822 bool rightFlanking; /// whether the delimiter is right-flanking, as defined by the CommonMark spec 2823 bool atParagraphStart; /// whether the delimiter is at the start of a paragraph 2824 char type; /// the type of delimiter, defined by its starting character 2825 2826 /// whether this describes a valid delimiter 2827 @property bool isValid() const { return count != 0; } 2828 2829 /// flag this delimiter as invalid 2830 void invalidate() { count = 0; } 2831 } 2832 2833 /**************************************************** 2834 * Info about a Markdown list. 2835 */ 2836 private struct MarkdownList 2837 { 2838 string orderedStart; /// an optional start number--if present then the list starts at this number 2839 size_t iStart; /// the index where the list item starts 2840 size_t iContentStart; /// the index where the content starts after the list delimiter 2841 int delimiterIndent; /// the level of indent the list delimiter starts at 2842 int contentIndent; /// the level of indent the content starts at 2843 int macroLevel; /// the count of nested DDoc macros when the list is started 2844 char type; /// the type of list, defined by its starting character 2845 2846 /// whether this describes a valid list 2847 @property bool isValid() const { return type != type.init; } 2848 2849 /**************************************************** 2850 * Try to parse a list item, returning whether successful. 2851 * Params: 2852 * buf = an OutBuffer containing the DDoc 2853 * iLineStart = the index within `buf` of the first character of the line 2854 * i = the index within `buf` of the potential list item 2855 * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded. 2856 */ 2857 static MarkdownList parseItem(ref OutBuffer buf, size_t iLineStart, size_t i) 2858 { 2859 if (!global.params.markdown) 2860 return MarkdownList(); 2861 2862 if (buf[i] == '+' || buf[i] == '-' || buf[i] == '*') 2863 return parseUnorderedListItem(buf, iLineStart, i); 2864 else 2865 return parseOrderedListItem(buf, iLineStart, i); 2866 } 2867 2868 /**************************************************** 2869 * Return whether the context is at a list item of the same type as this list. 2870 * Params: 2871 * buf = an OutBuffer containing the DDoc 2872 * iLineStart = the index within `buf` of the first character of the line 2873 * i = the index within `buf` of the list item 2874 * Returns: whether `i` is at a list item of the same type as this list 2875 */ 2876 private bool isAtItemInThisList(ref OutBuffer buf, size_t iLineStart, size_t i) 2877 { 2878 MarkdownList item = (type == '.' || type == ')') ? 2879 parseOrderedListItem(buf, iLineStart, i) : 2880 parseUnorderedListItem(buf, iLineStart, i); 2881 if (item.type == type) 2882 return item.delimiterIndent < contentIndent && item.contentIndent > delimiterIndent; 2883 return false; 2884 } 2885 2886 /**************************************************** 2887 * Start a Markdown list item by creating/deleting nested lists and starting the item. 2888 * Params: 2889 * buf = an OutBuffer containing the DDoc 2890 * iLineStart = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`. 2891 * i = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro. 2892 * 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`. 2893 * nestedLists = a set of nested lists. If this function succeeds it may contain a new nested list. 2894 * loc = the location of the Ddoc within the file 2895 * Returns: `true` if a list was created 2896 */ 2897 bool startItem(ref OutBuffer buf, ref size_t iLineStart, ref size_t i, ref size_t iPrecedingBlankLine, ref MarkdownList[] nestedLists, const ref Loc loc) 2898 { 2899 buf.remove(iStart, iContentStart - iStart); 2900 2901 if (!nestedLists.length || 2902 delimiterIndent >= nestedLists[$-1].contentIndent || 2903 buf[iLineStart - 4..iLineStart] == "$(LI") 2904 { 2905 // start a list macro 2906 nestedLists ~= this; 2907 if (type == '.') 2908 { 2909 if (orderedStart.length) 2910 { 2911 iStart = buf.insert(iStart, "$(OL_START "); 2912 iStart = buf.insert(iStart, orderedStart); 2913 iStart = buf.insert(iStart, ",\n"); 2914 } 2915 else 2916 iStart = buf.insert(iStart, "$(OL\n"); 2917 } 2918 else 2919 iStart = buf.insert(iStart, "$(UL\n"); 2920 2921 removeBlankLineMacro(buf, iPrecedingBlankLine, iStart); 2922 } 2923 else if (nestedLists.length) 2924 { 2925 nestedLists[$-1].delimiterIndent = delimiterIndent; 2926 nestedLists[$-1].contentIndent = contentIndent; 2927 } 2928 2929 iStart = buf.insert(iStart, "$(LI\n"); 2930 i = iStart - 1; 2931 iLineStart = i; 2932 2933 if (global.params.vmarkdown) 2934 { 2935 size_t iEnd = iStart; 2936 while (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n') 2937 ++iEnd; 2938 const s = buf[][iStart..iEnd]; 2939 message(loc, "Ddoc: starting list item '%.*s'", cast(int)s.length, s.ptr); 2940 } 2941 2942 return true; 2943 } 2944 2945 /**************************************************** 2946 * End all nested Markdown lists. 2947 * Params: 2948 * buf = an OutBuffer containing the DDoc 2949 * i = the index within `buf` to end lists at. 2950 * nestedLists = a set of nested lists. Upon return it will be empty. 2951 * Returns: the amount that `i` changed 2952 */ 2953 static size_t endAllNestedLists(ref OutBuffer buf, size_t i, ref MarkdownList[] nestedLists) 2954 { 2955 const iStart = i; 2956 for (; nestedLists.length; --nestedLists.length) 2957 i = buf.insert(i, ")\n)"); 2958 return i - iStart; 2959 } 2960 2961 /**************************************************** 2962 * Look for a sibling list item or the end of nested list(s). 2963 * Params: 2964 * buf = an OutBuffer containing the DDoc 2965 * 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. 2966 * iParagraphStart = the index within `buf` to start the next paragraph at at. May be adjusted upon return. 2967 * nestedLists = a set of nested lists. Some nested lists may have been removed from it upon return. 2968 */ 2969 static void handleSiblingOrEndingList(ref OutBuffer buf, ref size_t i, ref size_t iParagraphStart, ref MarkdownList[] nestedLists) 2970 { 2971 size_t iAfterSpaces = skipChars(buf, i + 1, " \t"); 2972 2973 if (nestedLists[$-1].isAtItemInThisList(buf, i + 1, iAfterSpaces)) 2974 { 2975 // end a sibling list item 2976 i = buf.insert(i, ")"); 2977 iParagraphStart = skipChars(buf, i, " \t\r\n"); 2978 } 2979 else if (iAfterSpaces >= buf.length || (buf[iAfterSpaces] != '\r' && buf[iAfterSpaces] != '\n')) 2980 { 2981 // end nested lists that are indented more than this content 2982 const indent = getMarkdownIndent(buf, i + 1, iAfterSpaces); 2983 while (nestedLists.length && nestedLists[$-1].contentIndent > indent) 2984 { 2985 i = buf.insert(i, ")\n)"); 2986 --nestedLists.length; 2987 iParagraphStart = skipChars(buf, i, " \t\r\n"); 2988 2989 if (nestedLists.length && nestedLists[$-1].isAtItemInThisList(buf, i + 1, iParagraphStart)) 2990 { 2991 i = buf.insert(i, ")"); 2992 ++iParagraphStart; 2993 break; 2994 } 2995 } 2996 } 2997 } 2998 2999 /**************************************************** 3000 * Parse an unordered list item at the current position 3001 * Params: 3002 * buf = an OutBuffer containing the DDoc 3003 * iLineStart = the index within `buf` of the first character of the line 3004 * i = the index within `buf` of the list item 3005 * Returns: the parsed list item, or a list item with type `.init` if no list item is available 3006 */ 3007 private static MarkdownList parseUnorderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) 3008 { 3009 if (i+1 < buf.length && 3010 (buf[i] == '-' || 3011 buf[i] == '*' || 3012 buf[i] == '+') && 3013 (buf[i+1] == ' ' || 3014 buf[i+1] == '\t' || 3015 buf[i+1] == '\r' || 3016 buf[i+1] == '\n')) 3017 { 3018 const iContentStart = skipChars(buf, i + 1, " \t"); 3019 const delimiterIndent = getMarkdownIndent(buf, iLineStart, i); 3020 const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart); 3021 auto list = MarkdownList(null, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[i]); 3022 return list; 3023 } 3024 return MarkdownList(); 3025 } 3026 3027 /**************************************************** 3028 * Parse an ordered list item at the current position 3029 * Params: 3030 * buf = an OutBuffer containing the DDoc 3031 * iLineStart = the index within `buf` of the first character of the line 3032 * i = the index within `buf` of the list item 3033 * Returns: the parsed list item, or a list item with type `.init` if no list item is available 3034 */ 3035 private static MarkdownList parseOrderedListItem(ref OutBuffer buf, size_t iLineStart, size_t i) 3036 { 3037 size_t iAfterNumbers = skipChars(buf, i, "0123456789"); 3038 if (iAfterNumbers - i > 0 && 3039 iAfterNumbers - i <= 9 && 3040 iAfterNumbers + 1 < buf.length && 3041 buf[iAfterNumbers] == '.' && 3042 (buf[iAfterNumbers+1] == ' ' || 3043 buf[iAfterNumbers+1] == '\t' || 3044 buf[iAfterNumbers+1] == '\r' || 3045 buf[iAfterNumbers+1] == '\n')) 3046 { 3047 const iContentStart = skipChars(buf, iAfterNumbers + 1, " \t"); 3048 const delimiterIndent = getMarkdownIndent(buf, iLineStart, i); 3049 const contentIndent = getMarkdownIndent(buf, iLineStart, iContentStart); 3050 size_t iNumberStart = skipChars(buf, i, "0"); 3051 if (iNumberStart == iAfterNumbers) 3052 --iNumberStart; 3053 auto orderedStart = buf[][iNumberStart .. iAfterNumbers]; 3054 if (orderedStart == "1") 3055 orderedStart = null; 3056 return MarkdownList(orderedStart.idup, iLineStart, iContentStart, delimiterIndent, contentIndent, 0, buf[iAfterNumbers]); 3057 } 3058 return MarkdownList(); 3059 } 3060 } 3061 3062 /**************************************************** 3063 * A Markdown link. 3064 */ 3065 private struct MarkdownLink 3066 { 3067 string href; /// the link destination 3068 string title; /// an optional title for the link 3069 string label; /// an optional label for the link 3070 Dsymbol symbol; /// an optional symbol to link to 3071 3072 /**************************************************** 3073 * Replace a Markdown link or link definition in the form of: 3074 * - Inline link: `[foo](url/ 'optional title')` 3075 * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]` 3076 * - Link reference definition: `[bar]: url/ 'optional title'` 3077 * Params: 3078 * buf = an OutBuffer containing the DDoc 3079 * i = the index within `buf` that points to the `]` character of the potential link. 3080 * If this function succeeds it will be adjusted to fit the inserted link macro. 3081 * loc = the current location within the file 3082 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts 3083 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter 3084 * linkReferences = previously parsed link references. When this function returns it may contain 3085 * additional previously unparsed references. 3086 * Returns: whether a reference link was found and replaced at `i` 3087 */ 3088 static bool replaceLink(ref OutBuffer buf, ref size_t i, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences) 3089 { 3090 const delimiter = inlineDelimiters[delimiterIndex]; 3091 MarkdownLink link; 3092 3093 size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter); 3094 if (iEnd > i) 3095 { 3096 i = delimiter.iStart; 3097 link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc); 3098 inlineDelimiters.length = delimiterIndex; 3099 return true; 3100 } 3101 3102 iEnd = link.parseInlineLink(buf, i); 3103 if (iEnd == i) 3104 { 3105 iEnd = link.parseReferenceLink(buf, i, delimiter); 3106 if (iEnd > i) 3107 { 3108 const label = link.label; 3109 link = linkReferences.lookupReference(label, buf, i, loc); 3110 // check rightFlanking to avoid replacing things like int[string] 3111 if (!link.href.length && !delimiter.rightFlanking) 3112 link = linkReferences.lookupSymbol(label); 3113 if (!link.href.length) 3114 return false; 3115 } 3116 } 3117 3118 if (iEnd == i) 3119 return false; 3120 3121 immutable delta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, delimiterIndex); 3122 iEnd += delta; 3123 i += delta; 3124 3125 if (global.params.vmarkdown) 3126 { 3127 const s = buf[][delimiter.iStart..iEnd]; 3128 message(loc, "Ddoc: linking '%.*s' to '%.*s'", cast(int)s.length, s.ptr, cast(int)link.href.length, link.href.ptr); 3129 } 3130 3131 link.replaceLink(buf, i, iEnd, delimiter); 3132 return true; 3133 } 3134 3135 /**************************************************** 3136 * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'` 3137 * Params: 3138 * buf = an OutBuffer containing the DDoc 3139 * i = the index within `buf` that points to the `]` character of the potential link. 3140 * If this function succeeds it will be adjusted to fit the inserted link macro. 3141 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts 3142 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter 3143 * linkReferences = previously parsed link references. When this function returns it may contain 3144 * additional previously unparsed references. 3145 * loc = the current location in the file 3146 * Returns: whether a reference link was found and replaced at `i` 3147 */ 3148 static bool replaceReferenceDefinition(ref OutBuffer buf, ref size_t i, ref MarkdownDelimiter[] inlineDelimiters, int delimiterIndex, ref MarkdownLinkReferences linkReferences, const ref Loc loc) 3149 { 3150 const delimiter = inlineDelimiters[delimiterIndex]; 3151 MarkdownLink link; 3152 size_t iEnd = link.parseReferenceDefinition(buf, i, delimiter); 3153 if (iEnd == i) 3154 return false; 3155 3156 i = delimiter.iStart; 3157 link.storeAndReplaceDefinition(buf, i, iEnd, linkReferences, loc); 3158 inlineDelimiters.length = delimiterIndex; 3159 return true; 3160 } 3161 3162 /**************************************************** 3163 * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')` 3164 * Params: 3165 * buf = an OutBuffer containing the DDoc 3166 * i = the index within `buf` that points to the `]` character of the inline link. 3167 * Returns: the index at the end of parsing the link, or `i` if parsing failed. 3168 */ 3169 private size_t parseInlineLink(ref OutBuffer buf, size_t i) 3170 { 3171 size_t iEnd = i + 1; 3172 if (iEnd >= buf.length || buf[iEnd] != '(') 3173 return i; 3174 ++iEnd; 3175 3176 if (!parseHref(buf, iEnd)) 3177 return i; 3178 3179 iEnd = skipChars(buf, iEnd, " \t\r\n"); 3180 if (buf[iEnd] != ')') 3181 { 3182 if (parseTitle(buf, iEnd)) 3183 iEnd = skipChars(buf, iEnd, " \t\r\n"); 3184 } 3185 3186 if (buf[iEnd] != ')') 3187 return i; 3188 3189 return iEnd + 1; 3190 } 3191 3192 /**************************************************** 3193 * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]` 3194 * Params: 3195 * buf = an OutBuffer containing the DDoc 3196 * i = the index within `buf` that points to the `]` character of the inline link. 3197 * delimiter = the delimiter that starts this link 3198 * Returns: the index at the end of parsing the link, or `i` if parsing failed. 3199 */ 3200 private size_t parseReferenceLink(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) 3201 { 3202 size_t iStart = i + 1; 3203 size_t iEnd = iStart; 3204 if (iEnd >= buf.length || buf[iEnd] != '[' || (iEnd+1 < buf.length && buf[iEnd+1] == ']')) 3205 { 3206 // collapsed reference [foo][] or shortcut reference [foo] 3207 iStart = delimiter.iStart + delimiter.count - 1; 3208 if (buf[iEnd] == '[') 3209 iEnd += 2; 3210 } 3211 3212 parseLabel(buf, iStart); 3213 if (!label.length) 3214 return i; 3215 3216 if (iEnd < iStart) 3217 iEnd = iStart; 3218 return iEnd; 3219 } 3220 3221 /**************************************************** 3222 * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'` 3223 * Params: 3224 * buf = an OutBuffer containing the DDoc 3225 * i = the index within `buf` that points to the `]` character of the inline link. 3226 * delimiter = the delimiter that starts this link 3227 * Returns: the index at the end of parsing the link, or `i` if parsing failed. 3228 */ 3229 private size_t parseReferenceDefinition(ref OutBuffer buf, size_t i, MarkdownDelimiter delimiter) 3230 { 3231 if (!delimiter.atParagraphStart || delimiter.type != '[' || 3232 i+1 >= buf.length || buf[i+1] != ':') 3233 return i; 3234 3235 size_t iEnd = delimiter.iStart; 3236 parseLabel(buf, iEnd); 3237 if (label.length == 0 || iEnd != i + 1) 3238 return i; 3239 3240 ++iEnd; 3241 iEnd = skipChars(buf, iEnd, " \t"); 3242 skipOneNewline(buf, iEnd); 3243 3244 if (!parseHref(buf, iEnd) || href.length == 0) 3245 return i; 3246 3247 iEnd = skipChars(buf, iEnd, " \t"); 3248 const requireNewline = !skipOneNewline(buf, iEnd); 3249 const iBeforeTitle = iEnd; 3250 3251 if (parseTitle(buf, iEnd)) 3252 { 3253 iEnd = skipChars(buf, iEnd, " \t"); 3254 if (iEnd < buf.length && buf[iEnd] != '\r' && buf[iEnd] != '\n') 3255 { 3256 // the title must end with a newline 3257 title.length = 0; 3258 iEnd = iBeforeTitle; 3259 } 3260 } 3261 3262 iEnd = skipChars(buf, iEnd, " \t"); 3263 if (requireNewline && iEnd < buf.length-1 && buf[iEnd] != '\r' && buf[iEnd] != '\n') 3264 return i; 3265 3266 return iEnd; 3267 } 3268 3269 /**************************************************** 3270 * Parse and normalize a Markdown reference label 3271 * Params: 3272 * buf = an OutBuffer containing the DDoc 3273 * i = the index within `buf` that points to the `[` character at the start of the label. 3274 * If this function returns a non-empty label then `i` will point just after the ']' at the end of the label. 3275 * Returns: the parsed and normalized label, possibly empty 3276 */ 3277 private bool parseLabel(ref OutBuffer buf, ref size_t i) 3278 { 3279 if (buf[i] != '[') 3280 return false; 3281 3282 const slice = buf[]; 3283 size_t j = i + 1; 3284 3285 // Some labels have already been en-symboled; handle that 3286 const inSymbol = j+15 < slice.length && slice[j..j+15] == "$(DDOC_PSYMBOL "; 3287 if (inSymbol) 3288 j += 15; 3289 3290 for (; j < slice.length; ++j) 3291 { 3292 const c = slice[j]; 3293 switch (c) 3294 { 3295 case ' ': 3296 case '\t': 3297 case '\r': 3298 case '\n': 3299 if (label.length && label[$-1] != ' ') 3300 label ~= ' '; 3301 break; 3302 case ')': 3303 if (inSymbol && j+1 < slice.length && slice[j+1] == ']') 3304 { 3305 ++j; 3306 goto case ']'; 3307 } 3308 goto default; 3309 case '[': 3310 if (slice[j-1] != '\\') 3311 { 3312 label.length = 0; 3313 return false; 3314 } 3315 break; 3316 case ']': 3317 if (label.length && label[$-1] == ' ') 3318 --label.length; 3319 if (label.length) 3320 { 3321 i = j + 1; 3322 return true; 3323 } 3324 return false; 3325 default: 3326 label ~= c; 3327 break; 3328 } 3329 } 3330 label.length = 0; 3331 return false; 3332 } 3333 3334 /**************************************************** 3335 * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets 3336 * Params: 3337 * buf = an OutBuffer containing the DDoc 3338 * i = the index within `buf` that points to the first character of the URL. 3339 * If this function succeeds `i` will point just after the the end of the URL. 3340 * Returns: whether a URL was found and parsed 3341 */ 3342 private bool parseHref(ref OutBuffer buf, ref size_t i) 3343 { 3344 size_t j = skipChars(buf, i, " \t"); 3345 3346 size_t iHrefStart = j; 3347 size_t parenDepth = 1; 3348 bool inPointy = false; 3349 const slice = buf[]; 3350 for (; j < slice.length; j++) 3351 { 3352 switch (slice[j]) 3353 { 3354 case '<': 3355 if (!inPointy && j == iHrefStart) 3356 { 3357 inPointy = true; 3358 ++iHrefStart; 3359 } 3360 break; 3361 case '>': 3362 if (inPointy && slice[j-1] != '\\') 3363 goto LReturnHref; 3364 break; 3365 case '(': 3366 if (!inPointy && slice[j-1] != '\\') 3367 ++parenDepth; 3368 break; 3369 case ')': 3370 if (!inPointy && slice[j-1] != '\\') 3371 { 3372 --parenDepth; 3373 if (!parenDepth) 3374 goto LReturnHref; 3375 } 3376 break; 3377 case ' ': 3378 case '\t': 3379 case '\r': 3380 case '\n': 3381 if (inPointy) 3382 { 3383 // invalid link 3384 return false; 3385 } 3386 goto LReturnHref; 3387 default: 3388 break; 3389 } 3390 } 3391 if (inPointy) 3392 return false; 3393 LReturnHref: 3394 auto href = slice[iHrefStart .. j].dup; 3395 this.href = cast(string) percentEncode(removeEscapeBackslashes(href)).replaceChar(',', "$(COMMA)"); 3396 i = j; 3397 if (inPointy) 3398 ++i; 3399 return true; 3400 } 3401 3402 /**************************************************** 3403 * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes 3404 * Params: 3405 * buf = an OutBuffer containing the DDoc 3406 * i = the index within `buf` that points to the first character of the title. 3407 * If this function succeeds `i` will point just after the the end of the title. 3408 * Returns: whether a title was found and parsed 3409 */ 3410 private bool parseTitle(ref OutBuffer buf, ref size_t i) 3411 { 3412 size_t j = skipChars(buf, i, " \t"); 3413 if (j >= buf.length) 3414 return false; 3415 3416 char type = buf[j]; 3417 if (type != '"' && type != '\'' && type != '(') 3418 return false; 3419 if (type == '(') 3420 type = ')'; 3421 3422 const iTitleStart = j + 1; 3423 size_t iNewline = 0; 3424 const slice = buf[]; 3425 for (j = iTitleStart; j < slice.length; j++) 3426 { 3427 const c = slice[j]; 3428 switch (c) 3429 { 3430 case ')': 3431 case '"': 3432 case '\'': 3433 if (type == c && slice[j-1] != '\\') 3434 goto LEndTitle; 3435 iNewline = 0; 3436 break; 3437 case ' ': 3438 case '\t': 3439 case '\r': 3440 break; 3441 case '\n': 3442 if (iNewline) 3443 { 3444 // no blank lines in titles 3445 return false; 3446 } 3447 iNewline = j; 3448 break; 3449 default: 3450 iNewline = 0; 3451 break; 3452 } 3453 } 3454 return false; 3455 LEndTitle: 3456 auto title = slice[iTitleStart .. j].dup; 3457 this.title = cast(string) removeEscapeBackslashes(title). 3458 replaceChar(',', "$(COMMA)"). 3459 replaceChar('"', "$(QUOTE)"); 3460 i = j + 1; 3461 return true; 3462 } 3463 3464 /**************************************************** 3465 * Replace a Markdown link or image with the appropriate macro 3466 * Params: 3467 * buf = an OutBuffer containing the DDoc 3468 * i = the index within `buf` that points to the `]` character of the inline link. 3469 * When this function returns it will be adjusted to the end of the inserted macro. 3470 * iLinkEnd = the index within `buf` that points just after the last character of the link 3471 * delimiter = the Markdown delimiter that started the link or image 3472 */ 3473 private void replaceLink(ref OutBuffer buf, ref size_t i, size_t iLinkEnd, MarkdownDelimiter delimiter) 3474 { 3475 size_t iAfterLink = i - delimiter.count; 3476 string macroName; 3477 if (symbol) 3478 { 3479 macroName = "$(SYMBOL_LINK "; 3480 } 3481 else if (title.length) 3482 { 3483 if (delimiter.type == '[') 3484 macroName = "$(LINK_TITLE "; 3485 else 3486 macroName = "$(IMAGE_TITLE "; 3487 } 3488 else 3489 { 3490 if (delimiter.type == '[') 3491 macroName = "$(LINK2 "; 3492 else 3493 macroName = "$(IMAGE "; 3494 } 3495 buf.remove(delimiter.iStart, delimiter.count); 3496 buf.remove(i - delimiter.count, iLinkEnd - i); 3497 iLinkEnd = buf.insert(delimiter.iStart, macroName); 3498 iLinkEnd = buf.insert(iLinkEnd, href); 3499 iLinkEnd = buf.insert(iLinkEnd, ", "); 3500 iAfterLink += macroName.length + href.length + 2; 3501 if (title.length) 3502 { 3503 iLinkEnd = buf.insert(iLinkEnd, title); 3504 iLinkEnd = buf.insert(iLinkEnd, ", "); 3505 iAfterLink += title.length + 2; 3506 3507 // Link macros with titles require escaping commas 3508 for (size_t j = iLinkEnd; j < iAfterLink; ++j) 3509 if (buf[j] == ',') 3510 { 3511 buf.remove(j, 1); 3512 j = buf.insert(j, "$(COMMA)") - 1; 3513 iAfterLink += 7; 3514 } 3515 } 3516 // TODO: if image, remove internal macros, leaving only text 3517 buf.insert(iAfterLink, ")"); 3518 i = iAfterLink; 3519 } 3520 3521 /**************************************************** 3522 * Store the Markdown link definition and remove it from `buf` 3523 * Params: 3524 * buf = an OutBuffer containing the DDoc 3525 * i = the index within `buf` that points to the `[` character at the start of the link definition. 3526 * When this function returns it will be adjusted to exclude the link definition. 3527 * iEnd = the index within `buf` that points just after the end of the definition 3528 * linkReferences = previously parsed link references. When this function returns it may contain 3529 * an additional reference. 3530 * loc = the current location in the file 3531 */ 3532 private void storeAndReplaceDefinition(ref OutBuffer buf, ref size_t i, size_t iEnd, ref MarkdownLinkReferences linkReferences, const ref Loc loc) 3533 { 3534 if (global.params.vmarkdown) 3535 message(loc, "Ddoc: found link reference '%.*s' to '%.*s'", cast(int)label.length, label.ptr, cast(int)href.length, href.ptr); 3536 3537 // Remove the definition and trailing whitespace 3538 iEnd = skipChars(buf, iEnd, " \t\r\n"); 3539 buf.remove(i, iEnd - i); 3540 i -= 2; 3541 3542 string lowercaseLabel = label.toLowercase(); 3543 if (lowercaseLabel !in linkReferences.references) 3544 linkReferences.references[lowercaseLabel] = this; 3545 } 3546 3547 /**************************************************** 3548 * Remove Markdown escaping backslashes from the given string 3549 * Params: 3550 * s = the string to remove escaping backslashes from 3551 * Returns: `s` without escaping backslashes in it 3552 */ 3553 private static char[] removeEscapeBackslashes(char[] s) 3554 { 3555 if (!s.length) 3556 return s; 3557 3558 // avoid doing anything if there isn't anything to escape 3559 size_t i; 3560 for (i = 0; i < s.length-1; ++i) 3561 if (s[i] == '\\' && ispunct(s[i+1])) 3562 break; 3563 if (i == s.length-1) 3564 return s; 3565 3566 // copy characters backwards, then truncate 3567 size_t j = i + 1; 3568 s[i] = s[j]; 3569 for (++i, ++j; j < s.length; ++i, ++j) 3570 { 3571 if (j < s.length-1 && s[j] == '\\' && ispunct(s[j+1])) 3572 ++j; 3573 s[i] = s[j]; 3574 } 3575 s.length -= (j - i); 3576 return s; 3577 } 3578 3579 /// 3580 unittest 3581 { 3582 assert(removeEscapeBackslashes("".dup) == ""); 3583 assert(removeEscapeBackslashes(`\a`.dup) == `\a`); 3584 assert(removeEscapeBackslashes(`.\`.dup) == `.\`); 3585 assert(removeEscapeBackslashes(`\.\`.dup) == `.\`); 3586 assert(removeEscapeBackslashes(`\.`.dup) == `.`); 3587 assert(removeEscapeBackslashes(`\.\.`.dup) == `..`); 3588 assert(removeEscapeBackslashes(`a\.b\.c`.dup) == `a.b.c`); 3589 } 3590 3591 /**************************************************** 3592 * Percent-encode (AKA URL-encode) the given string 3593 * Params: 3594 * s = the string to percent-encode 3595 * Returns: `s` with special characters percent-encoded 3596 */ 3597 private static inout(char)[] percentEncode(inout(char)[] s) pure 3598 { 3599 static bool shouldEncode(char c) 3600 { 3601 return ((c < '0' && c != '!' && c != '#' && c != '$' && c != '%' && c != '&' && c != '\'' && c != '(' && 3602 c != ')' && c != '*' && c != '+' && c != ',' && c != '-' && c != '.' && c != '/') 3603 || (c > '9' && c < 'A' && c != ':' && c != ';' && c != '=' && c != '?' && c != '@') 3604 || (c > 'Z' && c < 'a' && c != '[' && c != ']' && c != '_') 3605 || (c > 'z' && c != '~')); 3606 } 3607 3608 for (size_t i = 0; i < s.length; ++i) 3609 { 3610 if (shouldEncode(s[i])) 3611 { 3612 immutable static hexDigits = "0123456789ABCDEF"; 3613 immutable encoded1 = hexDigits[s[i] >> 4]; 3614 immutable encoded2 = hexDigits[s[i] & 0x0F]; 3615 s = s[0..i] ~ '%' ~ encoded1 ~ encoded2 ~ s[i+1..$]; 3616 i += 2; 3617 } 3618 } 3619 return s; 3620 } 3621 3622 /// 3623 unittest 3624 { 3625 assert(percentEncode("") == ""); 3626 assert(percentEncode("aB12-._~/?") == "aB12-._~/?"); 3627 assert(percentEncode("<\n>") == "%3C%0A%3E"); 3628 } 3629 3630 /************************************************** 3631 * Skip a single newline at `i` 3632 * Params: 3633 * buf = an OutBuffer containing the DDoc 3634 * i = the index within `buf` to start looking at. 3635 * If this function succeeds `i` will point after the newline. 3636 * Returns: whether a newline was skipped 3637 */ 3638 private static bool skipOneNewline(ref OutBuffer buf, ref size_t i) pure 3639 { 3640 if (i < buf.length && buf[i] == '\r') 3641 ++i; 3642 if (i < buf.length && buf[i] == '\n') 3643 { 3644 ++i; 3645 return true; 3646 } 3647 return false; 3648 } 3649 } 3650 3651 /************************************************** 3652 * A set of Markdown link references. 3653 */ 3654 private struct MarkdownLinkReferences 3655 { 3656 MarkdownLink[string] references; // link references keyed by normalized label 3657 MarkdownLink[string] symbols; // link symbols keyed by name 3658 Scope* _scope; // the current scope 3659 bool extractedAll; // the index into the buffer of the last-parsed reference 3660 3661 /************************************************** 3662 * Look up a reference by label, searching through the rest of the buffer if needed. 3663 * Symbols in the current scope are searched for if the DDoc doesn't define the reference. 3664 * Params: 3665 * label = the label to find the reference for 3666 * buf = an OutBuffer containing the DDoc 3667 * i = the index within `buf` to start searching for references at 3668 * loc = the current location in the file 3669 * Returns: a link. If the `href` member has a value then the reference is valid. 3670 */ 3671 MarkdownLink lookupReference(string label, ref OutBuffer buf, size_t i, const ref Loc loc) 3672 { 3673 const lowercaseLabel = label.toLowercase(); 3674 if (lowercaseLabel !in references) 3675 extractReferences(buf, i, loc); 3676 3677 if (lowercaseLabel in references) 3678 return references[lowercaseLabel]; 3679 3680 return MarkdownLink(); 3681 } 3682 3683 /** 3684 * Look up the link for the D symbol with the given name. 3685 * If found, the link is cached in the `symbols` member. 3686 * Params: 3687 * name = the name of the symbol 3688 * Returns: the link for the symbol or a link with a `null` href 3689 */ 3690 MarkdownLink lookupSymbol(string name) 3691 { 3692 if (name in symbols) 3693 return symbols[name]; 3694 3695 const ids = split(name, '.'); 3696 3697 MarkdownLink link; 3698 auto id = Identifier.lookup(ids[0].ptr, ids[0].length); 3699 if (id) 3700 { 3701 auto loc = Loc(); 3702 auto symbol = _scope.search(loc, id, null, IgnoreErrors); 3703 for (size_t i = 1; symbol && i < ids.length; ++i) 3704 { 3705 id = Identifier.lookup(ids[i].ptr, ids[i].length); 3706 symbol = id !is null ? symbol.search(loc, id, IgnoreErrors) : null; 3707 } 3708 if (symbol) 3709 link = MarkdownLink(createHref(symbol), null, name, symbol); 3710 } 3711 3712 symbols[name] = link; 3713 return link; 3714 } 3715 3716 /************************************************** 3717 * Remove and store all link references from the document, in the form of 3718 * `[label]: href "optional title"` 3719 * Params: 3720 * buf = an OutBuffer containing the DDoc 3721 * i = the index within `buf` to start looking at 3722 * loc = the current location in the file 3723 * Returns: whether a reference was extracted 3724 */ 3725 private void extractReferences(ref OutBuffer buf, size_t i, const ref Loc loc) 3726 { 3727 static bool isFollowedBySpace(ref OutBuffer buf, size_t i) 3728 { 3729 return i+1 < buf.length && (buf[i+1] == ' ' || buf[i+1] == '\t'); 3730 } 3731 3732 if (extractedAll) 3733 return; 3734 3735 bool leadingBlank = false; 3736 int inCode = false; 3737 bool newParagraph = true; 3738 MarkdownDelimiter[] delimiters; 3739 for (; i < buf.length; ++i) 3740 { 3741 const c = buf[i]; 3742 switch (c) 3743 { 3744 case ' ': 3745 case '\t': 3746 break; 3747 case '\n': 3748 if (leadingBlank && !inCode) 3749 newParagraph = true; 3750 leadingBlank = true; 3751 break; 3752 case '\\': 3753 ++i; 3754 break; 3755 case '#': 3756 if (leadingBlank && !inCode) 3757 newParagraph = true; 3758 leadingBlank = false; 3759 break; 3760 case '>': 3761 if (leadingBlank && !inCode) 3762 newParagraph = true; 3763 break; 3764 case '+': 3765 if (leadingBlank && !inCode && isFollowedBySpace(buf, i)) 3766 newParagraph = true; 3767 else 3768 leadingBlank = false; 3769 break; 3770 case '0': 3771 .. 3772 case '9': 3773 if (leadingBlank && !inCode) 3774 { 3775 i = skipChars(buf, i, "0123456789"); 3776 if (i < buf.length && 3777 (buf[i] == '.' || buf[i] == ')') && 3778 isFollowedBySpace(buf, i)) 3779 newParagraph = true; 3780 else 3781 leadingBlank = false; 3782 } 3783 break; 3784 case '*': 3785 if (leadingBlank && !inCode) 3786 { 3787 newParagraph = true; 3788 if (!isFollowedBySpace(buf, i)) 3789 leadingBlank = false; 3790 } 3791 break; 3792 case '`': 3793 case '~': 3794 if (leadingBlank && i+2 < buf.length && buf[i+1] == c && buf[i+2] == c) 3795 { 3796 inCode = inCode == c ? false : c; 3797 i = skipChars(buf, i, [c]) - 1; 3798 newParagraph = true; 3799 } 3800 leadingBlank = false; 3801 break; 3802 case '-': 3803 if (leadingBlank && !inCode && isFollowedBySpace(buf, i)) 3804 goto case '+'; 3805 else 3806 goto case '`'; 3807 case '[': 3808 if (leadingBlank && !inCode && newParagraph) 3809 delimiters ~= MarkdownDelimiter(i, 1, 0, false, false, true, c); 3810 break; 3811 case ']': 3812 if (delimiters.length && !inCode && 3813 MarkdownLink.replaceReferenceDefinition(buf, i, delimiters, cast(int) delimiters.length - 1, this, loc)) 3814 --i; 3815 break; 3816 default: 3817 if (leadingBlank) 3818 newParagraph = false; 3819 leadingBlank = false; 3820 break; 3821 } 3822 } 3823 extractedAll = true; 3824 } 3825 3826 /** 3827 * Split a string by a delimiter, excluding the delimiter. 3828 * Params: 3829 * s = the string to split 3830 * delimiter = the character to split by 3831 * Returns: the resulting array of strings 3832 */ 3833 private static string[] split(string s, char delimiter) pure 3834 { 3835 string[] result; 3836 size_t iStart = 0; 3837 foreach (size_t i; 0..s.length) 3838 if (s[i] == delimiter) 3839 { 3840 result ~= s[iStart..i]; 3841 iStart = i + 1; 3842 } 3843 result ~= s[iStart..$]; 3844 return result; 3845 } 3846 3847 /// 3848 unittest 3849 { 3850 assert(split("", ',') == [""]); 3851 assert(split("ab", ',') == ["ab"]); 3852 assert(split("a,b", ',') == ["a", "b"]); 3853 assert(split("a,,b", ',') == ["a", "", "b"]); 3854 assert(split(",ab", ',') == ["", "ab"]); 3855 assert(split("ab,", ',') == ["ab", ""]); 3856 } 3857 3858 /** 3859 * Create a HREF for the given D symbol. 3860 * The HREF is relative to the current location if possible. 3861 * Params: 3862 * symbol = the symbol to create a HREF for. 3863 * Returns: the resulting href 3864 */ 3865 private string createHref(Dsymbol symbol) 3866 { 3867 Dsymbol root = symbol; 3868 3869 const(char)[] lref; 3870 while (symbol && symbol.ident && !symbol.isModule()) 3871 { 3872 if (lref.length) 3873 lref = '.' ~ lref; 3874 lref = symbol.ident.toString() ~ lref; 3875 symbol = symbol.parent; 3876 } 3877 3878 const(char)[] path; 3879 if (symbol && symbol.ident && symbol.isModule() != _scope._module) 3880 { 3881 do 3882 { 3883 root = symbol; 3884 3885 // If the module has a file name, we're done 3886 if (const m = symbol.isModule()) 3887 if (m.docfile) 3888 { 3889 path = m.docfile.toString(); 3890 break; 3891 } 3892 3893 if (path.length) 3894 path = '_' ~ path; 3895 path = symbol.ident.toString() ~ path; 3896 symbol = symbol.parent; 3897 } while (symbol && symbol.ident); 3898 3899 if (!symbol && path.length) 3900 path ~= "$(DOC_EXTENSION)"; 3901 } 3902 3903 // Attempt an absolute URL if not in the same package 3904 while (root.parent) 3905 root = root.parent; 3906 Dsymbol scopeRoot = _scope._module; 3907 while (scopeRoot.parent) 3908 scopeRoot = scopeRoot.parent; 3909 if (scopeRoot != root) 3910 { 3911 path = "$(DOC_ROOT_" ~ root.ident.toString() ~ ')' ~ path; 3912 lref = '.' ~ lref; // remote URIs like Phobos and Mir use .prefixes 3913 } 3914 3915 return cast(string) (path ~ '#' ~ lref); 3916 } 3917 } 3918 3919 private enum TableColumnAlignment 3920 { 3921 none, 3922 left, 3923 center, 3924 right 3925 } 3926 3927 /**************************************************** 3928 * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |` 3929 * where the example text has four columns with the following alignments: 3930 * default, left, center, and right. The first and last pipes are optional. If a 3931 * delimiter row is found it will be removed from `buf`. 3932 * 3933 * Params: 3934 * buf = an OutBuffer containing the DDoc 3935 * iStart = the index within `buf` that the delimiter row starts at 3936 * inQuote = whether the table is inside a quote 3937 * columnAlignments = alignments to populate for each column 3938 * Returns: the index of the end of the parsed delimiter, or `0` if not found 3939 */ 3940 private size_t parseTableDelimiterRow(ref OutBuffer buf, const size_t iStart, bool inQuote, ref TableColumnAlignment[] columnAlignments) 3941 { 3942 size_t i = skipChars(buf, iStart, inQuote ? ">| \t" : "| \t"); 3943 while (i < buf.length && buf[i] != '\r' && buf[i] != '\n') 3944 { 3945 const leftColon = buf[i] == ':'; 3946 if (leftColon) 3947 ++i; 3948 3949 if (i >= buf.length || buf[i] != '-') 3950 break; 3951 i = skipChars(buf, i, "-"); 3952 3953 const rightColon = i < buf.length && buf[i] == ':'; 3954 i = skipChars(buf, i, ": \t"); 3955 3956 if (i >= buf.length || (buf[i] != '|' && buf[i] != '\r' && buf[i] != '\n')) 3957 break; 3958 i = skipChars(buf, i, "| \t"); 3959 3960 columnAlignments ~= (leftColon && rightColon) ? TableColumnAlignment.center : 3961 leftColon ? TableColumnAlignment.left : 3962 rightColon ? TableColumnAlignment.right : 3963 TableColumnAlignment.none; 3964 } 3965 3966 if (i < buf.length && buf[i] != '\r' && buf[i] != '\n' && buf[i] != ')') 3967 { 3968 columnAlignments.length = 0; 3969 return 0; 3970 } 3971 3972 if (i < buf.length && buf[i] == '\r') ++i; 3973 if (i < buf.length && buf[i] == '\n') ++i; 3974 return i; 3975 } 3976 3977 /**************************************************** 3978 * Look for a table delimiter row, and if found parse the previous row as a 3979 * table header row. If both exist with a matching number of columns, start a 3980 * table. 3981 * 3982 * Params: 3983 * buf = an OutBuffer containing the DDoc 3984 * iStart = the index within `buf` that the table header row starts at, inclusive 3985 * iEnd = the index within `buf` that the table header row ends at, exclusive 3986 * loc = the current location in the file 3987 * inQuote = whether the table is inside a quote 3988 * inlineDelimiters = delimiters containing columns separators and any inline emphasis 3989 * columnAlignments = the parsed alignments for each column 3990 * Returns: the number of characters added by starting the table, or `0` if unchanged 3991 */ 3992 private size_t startTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, bool inQuote, ref MarkdownDelimiter[] inlineDelimiters, out TableColumnAlignment[] columnAlignments) 3993 { 3994 const iDelimiterRowEnd = parseTableDelimiterRow(buf, iEnd + 1, inQuote, columnAlignments); 3995 if (iDelimiterRowEnd) 3996 { 3997 const delta = replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, true); 3998 if (delta) 3999 { 4000 buf.remove(iEnd + delta, iDelimiterRowEnd - iEnd); 4001 buf.insert(iEnd + delta, "$(TBODY "); 4002 buf.insert(iStart, "$(TABLE "); 4003 return delta + 15; 4004 } 4005 } 4006 4007 columnAlignments.length = 0; 4008 return 0; 4009 } 4010 4011 /**************************************************** 4012 * Replace a Markdown table row in the form of table cells delimited by pipes: 4013 * `| cell | cell | cell`. The first and last pipes are optional. 4014 * 4015 * Params: 4016 * buf = an OutBuffer containing the DDoc 4017 * iStart = the index within `buf` that the table row starts at, inclusive 4018 * iEnd = the index within `buf` that the table row ends at, exclusive 4019 * loc = the current location in the file 4020 * inlineDelimiters = delimiters containing columns separators and any inline emphasis 4021 * columnAlignments = alignments for each column 4022 * headerRow = if `true` then the number of columns will be enforced to match 4023 * `columnAlignments.length` and the row will be surrounded by a 4024 * `THEAD` macro 4025 * Returns: the number of characters added by replacing the row, or `0` if unchanged 4026 */ 4027 private size_t replaceTableRow(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, TableColumnAlignment[] columnAlignments, bool headerRow) 4028 { 4029 if (!columnAlignments.length || iStart == iEnd) 4030 return 0; 4031 4032 iStart = skipChars(buf, iStart, " \t"); 4033 int cellCount = 0; 4034 foreach (delimiter; inlineDelimiters) 4035 if (delimiter.type == '|' && !delimiter.leftFlanking) 4036 ++cellCount; 4037 bool ignoreLast = inlineDelimiters.length > 0 && inlineDelimiters[$-1].type == '|'; 4038 if (ignoreLast) 4039 { 4040 const iLast = skipChars(buf, inlineDelimiters[$-1].iStart + inlineDelimiters[$-1].count, " \t"); 4041 ignoreLast = iLast >= iEnd; 4042 } 4043 if (!ignoreLast) 4044 ++cellCount; 4045 4046 if (headerRow && cellCount != columnAlignments.length) 4047 return 0; 4048 4049 if (headerRow && global.params.vmarkdown) 4050 { 4051 const s = buf[][iStart..iEnd]; 4052 message(loc, "Ddoc: formatting table '%.*s'", cast(int)s.length, s.ptr); 4053 } 4054 4055 size_t delta = 0; 4056 4057 void replaceTableCell(size_t iCellStart, size_t iCellEnd, int cellIndex, int di) 4058 { 4059 const eDelta = replaceMarkdownEmphasis(buf, loc, inlineDelimiters, di); 4060 delta += eDelta; 4061 iCellEnd += eDelta; 4062 4063 // strip trailing whitespace and delimiter 4064 size_t i = iCellEnd - 1; 4065 while (i > iCellStart && (buf[i] == '|' || buf[i] == ' ' || buf[i] == '\t')) 4066 --i; 4067 ++i; 4068 buf.remove(i, iCellEnd - i); 4069 delta -= iCellEnd - i; 4070 iCellEnd = i; 4071 4072 buf.insert(iCellEnd, ")"); 4073 ++delta; 4074 4075 // strip initial whitespace and delimiter 4076 i = skipChars(buf, iCellStart, "| \t"); 4077 buf.remove(iCellStart, i - iCellStart); 4078 delta -= i - iCellStart; 4079 4080 switch (columnAlignments[cellIndex]) 4081 { 4082 case TableColumnAlignment.none: 4083 buf.insert(iCellStart, headerRow ? "$(TH " : "$(TD "); 4084 delta += 5; 4085 break; 4086 case TableColumnAlignment.left: 4087 buf.insert(iCellStart, "left, "); 4088 delta += 6; 4089 goto default; 4090 case TableColumnAlignment.center: 4091 buf.insert(iCellStart, "center, "); 4092 delta += 8; 4093 goto default; 4094 case TableColumnAlignment.right: 4095 buf.insert(iCellStart, "right, "); 4096 delta += 7; 4097 goto default; 4098 default: 4099 buf.insert(iCellStart, headerRow ? "$(TH_ALIGN " : "$(TD_ALIGN "); 4100 delta += 11; 4101 break; 4102 } 4103 } 4104 4105 int cellIndex = cellCount - 1; 4106 size_t iCellEnd = iEnd; 4107 foreach_reverse (di, delimiter; inlineDelimiters) 4108 { 4109 if (delimiter.type == '|') 4110 { 4111 if (ignoreLast && di == inlineDelimiters.length-1) 4112 { 4113 ignoreLast = false; 4114 continue; 4115 } 4116 4117 if (cellIndex >= columnAlignments.length) 4118 { 4119 // kill any extra cells 4120 buf.remove(delimiter.iStart, iEnd + delta - delimiter.iStart); 4121 delta -= iEnd + delta - delimiter.iStart; 4122 iCellEnd = iEnd + delta; 4123 --cellIndex; 4124 continue; 4125 } 4126 4127 replaceTableCell(delimiter.iStart, iCellEnd, cellIndex, cast(int) di); 4128 iCellEnd = delimiter.iStart; 4129 --cellIndex; 4130 } 4131 } 4132 4133 // if no starting pipe, replace from the start 4134 if (cellIndex >= 0) 4135 replaceTableCell(iStart, iCellEnd, cellIndex, 0); 4136 4137 buf.insert(iEnd + delta, ")"); 4138 buf.insert(iStart, "$(TR "); 4139 delta += 6; 4140 4141 if (headerRow) 4142 { 4143 buf.insert(iEnd + delta, ")"); 4144 buf.insert(iStart, "$(THEAD "); 4145 delta += 9; 4146 } 4147 4148 return delta; 4149 } 4150 4151 /**************************************************** 4152 * End a table, if in one. 4153 * 4154 * Params: 4155 * buf = an OutBuffer containing the DDoc 4156 * i = the index within `buf` to end the table at 4157 * columnAlignments = alignments for each column; upon return is set to length `0` 4158 * Returns: the number of characters added by ending the table, or `0` if unchanged 4159 */ 4160 private size_t endTable(ref OutBuffer buf, size_t i, ref TableColumnAlignment[] columnAlignments) 4161 { 4162 if (!columnAlignments.length) 4163 return 0; 4164 4165 buf.insert(i, "))"); 4166 columnAlignments.length = 0; 4167 return 2; 4168 } 4169 4170 /**************************************************** 4171 * End a table row and then the table itself. 4172 * 4173 * Params: 4174 * buf = an OutBuffer containing the DDoc 4175 * iStart = the index within `buf` that the table row starts at, inclusive 4176 * iEnd = the index within `buf` that the table row ends at, exclusive 4177 * loc = the current location in the file 4178 * inlineDelimiters = delimiters containing columns separators and any inline emphasis 4179 * columnAlignments = alignments for each column; upon return is set to length `0` 4180 * Returns: the number of characters added by replacing the row, or `0` if unchanged 4181 */ 4182 private size_t endRowAndTable(ref OutBuffer buf, size_t iStart, size_t iEnd, const ref Loc loc, ref MarkdownDelimiter[] inlineDelimiters, ref TableColumnAlignment[] columnAlignments) 4183 { 4184 size_t delta = replaceTableRow(buf, iStart, iEnd, loc, inlineDelimiters, columnAlignments, false); 4185 delta += endTable(buf, iEnd + delta, columnAlignments); 4186 return delta; 4187 } 4188 4189 /************************************************** 4190 * Highlight text section. 4191 * 4192 * Params: 4193 * scope = the current parse scope 4194 * a = an array of D symbols at the current scope 4195 * 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. 4196 * buf = an OutBuffer containing the DDoc 4197 * offset = the index within buf to start highlighting 4198 */ 4199 private void highlightText(Scope* sc, Dsymbols* a, Loc loc, ref OutBuffer buf, size_t offset) 4200 { 4201 const incrementLoc = loc.linnum == 0 ? 1 : 0; 4202 loc.linnum += incrementLoc; 4203 loc.charnum = 0; 4204 //printf("highlightText()\n"); 4205 bool leadingBlank = true; 4206 size_t iParagraphStart = offset; 4207 size_t iPrecedingBlankLine = 0; 4208 int headingLevel = 0; 4209 int headingMacroLevel = 0; 4210 int quoteLevel = 0; 4211 bool lineQuoted = false; 4212 int quoteMacroLevel = 0; 4213 MarkdownList[] nestedLists; 4214 MarkdownDelimiter[] inlineDelimiters; 4215 MarkdownLinkReferences linkReferences; 4216 TableColumnAlignment[] columnAlignments; 4217 bool tableRowDetected = false; 4218 int inCode = 0; 4219 int inBacktick = 0; 4220 int macroLevel = 0; 4221 int previousMacroLevel = 0; 4222 int parenLevel = 0; 4223 size_t iCodeStart = 0; // start of code section 4224 size_t codeFenceLength = 0; 4225 size_t codeIndent = 0; 4226 string codeLanguage; 4227 size_t iLineStart = offset; 4228 linkReferences._scope = sc; 4229 for (size_t i = offset; i < buf.length; i++) 4230 { 4231 char c = buf[i]; 4232 Lcont: 4233 switch (c) 4234 { 4235 case ' ': 4236 case '\t': 4237 break; 4238 case '\n': 4239 if (inBacktick) 4240 { 4241 // `inline code` is only valid if contained on a single line 4242 // otherwise, the backticks should be output literally. 4243 // 4244 // This lets things like `output from the linker' display 4245 // unmolested while keeping the feature consistent with GitHub. 4246 inBacktick = false; 4247 inCode = false; // the backtick also assumes we're in code 4248 // Nothing else is necessary since the DDOC_BACKQUOTED macro is 4249 // inserted lazily at the close quote, meaning the rest of the 4250 // text is already OK. 4251 } 4252 if (headingLevel) 4253 { 4254 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); 4255 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); 4256 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 4257 ++i; 4258 iParagraphStart = skipChars(buf, i, " \t\r\n"); 4259 } 4260 4261 if (tableRowDetected && !columnAlignments.length) 4262 i += startTable(buf, iLineStart, i, loc, lineQuoted, inlineDelimiters, columnAlignments); 4263 else if (columnAlignments.length) 4264 { 4265 const delta = replaceTableRow(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments, false); 4266 if (delta) 4267 i += delta; 4268 else 4269 i += endTable(buf, i, columnAlignments); 4270 } 4271 4272 if (!inCode && nestedLists.length && !quoteLevel) 4273 MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists); 4274 4275 iPrecedingBlankLine = 0; 4276 if (!inCode && i == iLineStart && i + 1 < buf.length) // if "\n\n" 4277 { 4278 i += endTable(buf, i, columnAlignments); 4279 if (!lineQuoted && quoteLevel) 4280 endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel); 4281 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); 4282 4283 // if we don't already know about this paragraph break then 4284 // insert a blank line and record the paragraph break 4285 if (iParagraphStart <= i) 4286 { 4287 iPrecedingBlankLine = i; 4288 i = buf.insert(i, "$(DDOC_BLANKLINE)"); 4289 iParagraphStart = i + 1; 4290 } 4291 } 4292 else if (inCode && 4293 i == iLineStart && 4294 i + 1 < buf.length && 4295 !lineQuoted && 4296 quoteLevel) // if "\n\n" in quoted code 4297 { 4298 inCode = false; 4299 i = buf.insert(i, ")"); 4300 i += endAllMarkdownQuotes(buf, i, quoteLevel); 4301 quoteMacroLevel = 0; 4302 } 4303 leadingBlank = true; 4304 lineQuoted = false; 4305 tableRowDetected = false; 4306 iLineStart = i + 1; 4307 loc.linnum += incrementLoc; 4308 4309 // update the paragraph start if we just entered a macro 4310 if (previousMacroLevel < macroLevel && iParagraphStart < iLineStart) 4311 iParagraphStart = iLineStart; 4312 previousMacroLevel = macroLevel; 4313 break; 4314 4315 case '<': 4316 { 4317 leadingBlank = false; 4318 if (inCode) 4319 break; 4320 const slice = buf[]; 4321 auto p = &slice[i]; 4322 const se = sc._module.escapetable.escapeChar('<'); 4323 if (se == "<") 4324 { 4325 // Generating HTML 4326 // Skip over comments 4327 if (p[1] == '!' && p[2] == '-' && p[3] == '-') 4328 { 4329 size_t j = i + 4; 4330 p += 4; 4331 while (1) 4332 { 4333 if (j == slice.length) 4334 goto L1; 4335 if (p[0] == '-' && p[1] == '-' && p[2] == '>') 4336 { 4337 i = j + 2; // place on closing '>' 4338 break; 4339 } 4340 j++; 4341 p++; 4342 } 4343 break; 4344 } 4345 // Skip over HTML tag 4346 if (isalpha(p[1]) || (p[1] == '/' && isalpha(p[2]))) 4347 { 4348 size_t j = i + 2; 4349 p += 2; 4350 while (1) 4351 { 4352 if (j == slice.length) 4353 break; 4354 if (p[0] == '>') 4355 { 4356 i = j; // place on closing '>' 4357 break; 4358 } 4359 j++; 4360 p++; 4361 } 4362 break; 4363 } 4364 } 4365 L1: 4366 // Replace '<' with '<' character entity 4367 if (se.length) 4368 { 4369 buf.remove(i, 1); 4370 i = buf.insert(i, se); 4371 i--; // point to ';' 4372 } 4373 break; 4374 } 4375 4376 case '>': 4377 { 4378 if (leadingBlank && (!inCode || quoteLevel) && global.params.markdown) 4379 { 4380 if (!quoteLevel && global.params.vmarkdown) 4381 { 4382 size_t iEnd = i + 1; 4383 while (iEnd < buf.length && buf[iEnd] != '\n') 4384 ++iEnd; 4385 const s = buf[][i .. iEnd]; 4386 message(loc, "Ddoc: starting quote block with '%.*s'", cast(int)s.length, s.ptr); 4387 } 4388 4389 lineQuoted = true; 4390 int lineQuoteLevel = 1; 4391 size_t iAfterDelimiters = i + 1; 4392 for (; iAfterDelimiters < buf.length; ++iAfterDelimiters) 4393 { 4394 const c0 = buf[iAfterDelimiters]; 4395 if (c0 == '>') 4396 ++lineQuoteLevel; 4397 else if (c0 != ' ' && c0 != '\t') 4398 break; 4399 } 4400 if (!quoteMacroLevel) 4401 quoteMacroLevel = macroLevel; 4402 buf.remove(i, iAfterDelimiters - i); 4403 4404 if (quoteLevel < lineQuoteLevel) 4405 { 4406 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4407 if (nestedLists.length) 4408 { 4409 const indent = getMarkdownIndent(buf, iLineStart, i); 4410 if (indent < nestedLists[$-1].contentIndent) 4411 i += MarkdownList.endAllNestedLists(buf, i, nestedLists); 4412 } 4413 4414 for (; quoteLevel < lineQuoteLevel; ++quoteLevel) 4415 { 4416 i = buf.insert(i, "$(BLOCKQUOTE\n"); 4417 iLineStart = iParagraphStart = i; 4418 } 4419 --i; 4420 } 4421 else 4422 { 4423 --i; 4424 if (nestedLists.length) 4425 MarkdownList.handleSiblingOrEndingList(buf, i, iParagraphStart, nestedLists); 4426 } 4427 break; 4428 } 4429 4430 leadingBlank = false; 4431 if (inCode) 4432 break; 4433 // Replace '>' with '>' character entity 4434 const se = sc._module.escapetable.escapeChar('>'); 4435 if (se.length) 4436 { 4437 buf.remove(i, 1); 4438 i = buf.insert(i, se); 4439 i--; // point to ';' 4440 } 4441 break; 4442 } 4443 4444 case '&': 4445 { 4446 leadingBlank = false; 4447 if (inCode) 4448 break; 4449 char* p = cast(char*)&buf[].ptr[i]; 4450 if (p[1] == '#' || isalpha(p[1])) 4451 break; 4452 // already a character entity 4453 // Replace '&' with '&' character entity 4454 const se = sc._module.escapetable.escapeChar('&'); 4455 if (se) 4456 { 4457 buf.remove(i, 1); 4458 i = buf.insert(i, se); 4459 i--; // point to ';' 4460 } 4461 break; 4462 } 4463 4464 case '`': 4465 { 4466 const iAfterDelimiter = skipChars(buf, i, "`"); 4467 const count = iAfterDelimiter - i; 4468 4469 if (inBacktick == count) 4470 { 4471 inBacktick = 0; 4472 inCode = 0; 4473 OutBuffer codebuf; 4474 codebuf.write(buf[iCodeStart + count .. i]); 4475 // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL 4476 highlightCode(sc, a, codebuf, 0); 4477 escapeStrayParenthesis(loc, &codebuf, 0, false); 4478 buf.remove(iCodeStart, i - iCodeStart + count); // also trimming off the current ` 4479 immutable pre = "$(DDOC_BACKQUOTED "; 4480 i = buf.insert(iCodeStart, pre); 4481 i = buf.insert(i, codebuf[]); 4482 i = buf.insert(i, ")"); 4483 i--; // point to the ending ) so when the for loop does i++, it will see the next character 4484 break; 4485 } 4486 4487 // Perhaps we're starting or ending a Markdown code block 4488 if (leadingBlank && global.params.markdown && count >= 3) 4489 { 4490 bool moreBackticks = false; 4491 for (size_t j = iAfterDelimiter; !moreBackticks && j < buf.length; ++j) 4492 if (buf[j] == '`') 4493 moreBackticks = true; 4494 else if (buf[j] == '\r' || buf[j] == '\n') 4495 break; 4496 if (!moreBackticks) 4497 goto case '-'; 4498 } 4499 4500 if (inCode) 4501 { 4502 if (inBacktick) 4503 i = iAfterDelimiter - 1; 4504 break; 4505 } 4506 inCode = c; 4507 inBacktick = cast(int) count; 4508 codeIndent = 0; // inline code is not indented 4509 // All we do here is set the code flags and record 4510 // the location. The macro will be inserted lazily 4511 // so we can easily cancel the inBacktick if we come 4512 // across a newline character. 4513 iCodeStart = i; 4514 i = iAfterDelimiter - 1; 4515 break; 4516 } 4517 4518 case '#': 4519 { 4520 /* A line beginning with # indicates an ATX-style heading. */ 4521 if (leadingBlank && !inCode) 4522 { 4523 leadingBlank = false; 4524 4525 headingLevel = detectAtxHeadingLevel(buf, i); 4526 if (!headingLevel) 4527 break; 4528 4529 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4530 if (!lineQuoted && quoteLevel) 4531 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); 4532 4533 // remove the ### prefix, including whitespace 4534 i = skipChars(buf, i + headingLevel, " \t"); 4535 buf.remove(iLineStart, i - iLineStart); 4536 i = iParagraphStart = iLineStart; 4537 4538 removeAnyAtxHeadingSuffix(buf, i); 4539 --i; 4540 4541 headingMacroLevel = macroLevel; 4542 } 4543 break; 4544 } 4545 4546 case '~': 4547 { 4548 if (leadingBlank && global.params.markdown) 4549 { 4550 // Perhaps we're starting or ending a Markdown code block 4551 const iAfterDelimiter = skipChars(buf, i, "~"); 4552 if (iAfterDelimiter - i >= 3) 4553 goto case '-'; 4554 } 4555 leadingBlank = false; 4556 break; 4557 } 4558 4559 case '-': 4560 /* A line beginning with --- delimits a code section. 4561 * inCode tells us if it is start or end of a code section. 4562 */ 4563 if (leadingBlank) 4564 { 4565 if (!inCode && c == '-') 4566 { 4567 const list = MarkdownList.parseItem(buf, iLineStart, i); 4568 if (list.isValid) 4569 { 4570 if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) 4571 { 4572 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 4573 iParagraphStart = skipChars(buf, i+1, " \t\r\n"); 4574 break; 4575 } 4576 else 4577 goto case '+'; 4578 } 4579 } 4580 4581 size_t istart = i; 4582 size_t eollen = 0; 4583 leadingBlank = false; 4584 const c0 = c; // if we jumped here from case '`' or case '~' 4585 size_t iInfoString = 0; 4586 if (!inCode) 4587 codeLanguage.length = 0; 4588 while (1) 4589 { 4590 ++i; 4591 if (i >= buf.length) 4592 break; 4593 c = buf[i]; 4594 if (c == '\n') 4595 { 4596 eollen = 1; 4597 break; 4598 } 4599 if (c == '\r') 4600 { 4601 eollen = 1; 4602 if (i + 1 >= buf.length) 4603 break; 4604 if (buf[i + 1] == '\n') 4605 { 4606 eollen = 2; 4607 break; 4608 } 4609 } 4610 // BUG: handle UTF PS and LS too 4611 if (c != c0 || iInfoString) 4612 { 4613 if (global.params.markdown && !iInfoString && !inCode && i - istart >= 3) 4614 { 4615 // Start a Markdown info string, like ```ruby 4616 codeFenceLength = i - istart; 4617 i = iInfoString = skipChars(buf, i, " \t"); 4618 } 4619 else if (iInfoString && c != '`') 4620 { 4621 if (!codeLanguage.length && (c == ' ' || c == '\t')) 4622 codeLanguage = cast(string) buf[iInfoString..i].idup; 4623 } 4624 else 4625 { 4626 iInfoString = 0; 4627 goto Lcont; 4628 } 4629 } 4630 } 4631 if (i - istart < 3 || (inCode && (inCode != c0 || (inCode != '-' && i - istart < codeFenceLength)))) 4632 goto Lcont; 4633 if (iInfoString) 4634 { 4635 if (!codeLanguage.length) 4636 codeLanguage = cast(string) buf[iInfoString..i].idup; 4637 } 4638 else 4639 codeFenceLength = i - istart; 4640 4641 // We have the start/end of a code section 4642 // Remove the entire --- line, including blanks and \n 4643 buf.remove(iLineStart, i - iLineStart + eollen); 4644 i = iLineStart; 4645 if (eollen) 4646 leadingBlank = true; 4647 if (inCode && (i <= iCodeStart)) 4648 { 4649 // Empty code section, just remove it completely. 4650 inCode = 0; 4651 break; 4652 } 4653 if (inCode) 4654 { 4655 inCode = 0; 4656 // The code section is from iCodeStart to i 4657 OutBuffer codebuf; 4658 codebuf.write(buf[iCodeStart .. i]); 4659 codebuf.writeByte(0); 4660 // Remove leading indentations from all lines 4661 bool lineStart = true; 4662 char* endp = cast(char*)codebuf[].ptr + codebuf.length; 4663 for (char* p = cast(char*)codebuf[].ptr; p < endp;) 4664 { 4665 if (lineStart) 4666 { 4667 size_t j = codeIndent; 4668 char* q = p; 4669 while (j-- > 0 && q < endp && isIndentWS(q)) 4670 ++q; 4671 codebuf.remove(p - cast(char*)codebuf[].ptr, q - p); 4672 assert(cast(char*)codebuf[].ptr <= p); 4673 assert(p < cast(char*)codebuf[].ptr + codebuf.length); 4674 lineStart = false; 4675 endp = cast(char*)codebuf[].ptr + codebuf.length; // update 4676 continue; 4677 } 4678 if (*p == '\n') 4679 lineStart = true; 4680 ++p; 4681 } 4682 if (!codeLanguage.length || codeLanguage == "dlang" || codeLanguage == "d") 4683 highlightCode2(sc, a, codebuf, 0); 4684 else 4685 codebuf.remove(codebuf.length-1, 1); // remove the trailing 0 byte 4686 escapeStrayParenthesis(loc, &codebuf, 0, false); 4687 buf.remove(iCodeStart, i - iCodeStart); 4688 i = buf.insert(iCodeStart, codebuf[]); 4689 i = buf.insert(i, ")\n"); 4690 i -= 2; // in next loop, c should be '\n' 4691 } 4692 else 4693 { 4694 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4695 if (!lineQuoted && quoteLevel) 4696 { 4697 const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); 4698 i += delta; 4699 istart += delta; 4700 } 4701 4702 inCode = c0; 4703 codeIndent = istart - iLineStart; // save indent count 4704 if (codeLanguage.length && codeLanguage != "dlang" && codeLanguage != "d") 4705 { 4706 // backslash-escape 4707 for (size_t j; j < codeLanguage.length - 1; ++j) 4708 if (codeLanguage[j] == '\\' && ispunct(codeLanguage[j + 1])) 4709 codeLanguage = codeLanguage[0..j] ~ codeLanguage[j + 1..$]; 4710 4711 if (global.params.vmarkdown) 4712 message(loc, "Ddoc: adding code block for language '%.*s'", cast(int)codeLanguage.length, codeLanguage.ptr); 4713 4714 i = buf.insert(i, "$(OTHER_CODE "); 4715 i = buf.insert(i, codeLanguage); 4716 i = buf.insert(i, ","); 4717 } 4718 else 4719 i = buf.insert(i, "$(D_CODE "); 4720 iCodeStart = i; 4721 i--; // place i on > 4722 leadingBlank = true; 4723 } 4724 } 4725 break; 4726 4727 case '_': 4728 { 4729 if (leadingBlank && !inCode && replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) 4730 { 4731 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4732 if (!lineQuoted && quoteLevel) 4733 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); 4734 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 4735 iParagraphStart = skipChars(buf, i+1, " \t\r\n"); 4736 break; 4737 } 4738 goto default; 4739 } 4740 4741 case '+': 4742 case '0': 4743 .. 4744 case '9': 4745 { 4746 if (leadingBlank && !inCode) 4747 { 4748 MarkdownList list = MarkdownList.parseItem(buf, iLineStart, i); 4749 if (list.isValid) 4750 { 4751 // Avoid starting a numbered list in the middle of a paragraph 4752 if (!nestedLists.length && list.orderedStart.length && 4753 iParagraphStart < iLineStart) 4754 { 4755 i += list.orderedStart.length - 1; 4756 break; 4757 } 4758 4759 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4760 if (!lineQuoted && quoteLevel) 4761 { 4762 const delta = endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); 4763 i += delta; 4764 list.iStart += delta; 4765 list.iContentStart += delta; 4766 } 4767 4768 list.macroLevel = macroLevel; 4769 list.startItem(buf, iLineStart, i, iPrecedingBlankLine, nestedLists, loc); 4770 break; 4771 } 4772 } 4773 leadingBlank = false; 4774 break; 4775 } 4776 4777 case '*': 4778 { 4779 if (inCode || inBacktick || !global.params.markdown) 4780 { 4781 leadingBlank = false; 4782 break; 4783 } 4784 4785 if (leadingBlank) 4786 { 4787 // Check for a thematic break 4788 if (replaceMarkdownThematicBreak(buf, i, iLineStart, loc)) 4789 { 4790 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4791 if (!lineQuoted && quoteLevel) 4792 i += endAllListsAndQuotes(buf, iLineStart, nestedLists, quoteLevel, quoteMacroLevel); 4793 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 4794 iParagraphStart = skipChars(buf, i+1, " \t\r\n"); 4795 break; 4796 } 4797 4798 // An initial * indicates a Markdown list item 4799 const list = MarkdownList.parseItem(buf, iLineStart, i); 4800 if (list.isValid) 4801 goto case '+'; 4802 } 4803 4804 // Markdown emphasis 4805 const leftC = i > offset ? buf[i-1] : '\0'; 4806 size_t iAfterEmphasis = skipChars(buf, i+1, "*"); 4807 const rightC = iAfterEmphasis < buf.length ? buf[iAfterEmphasis] : '\0'; 4808 int count = cast(int) (iAfterEmphasis - i); 4809 const leftFlanking = (rightC != '\0' && !isspace(rightC)) && (!ispunct(rightC) || leftC == '\0' || isspace(leftC) || ispunct(leftC)); 4810 const rightFlanking = (leftC != '\0' && !isspace(leftC)) && (!ispunct(leftC) || rightC == '\0' || isspace(rightC) || ispunct(rightC)); 4811 auto emphasis = MarkdownDelimiter(i, count, macroLevel, leftFlanking, rightFlanking, false, c); 4812 4813 if (!emphasis.leftFlanking && !emphasis.rightFlanking) 4814 { 4815 i = iAfterEmphasis - 1; 4816 break; 4817 } 4818 4819 inlineDelimiters ~= emphasis; 4820 i += emphasis.count; 4821 --i; 4822 break; 4823 } 4824 4825 case '!': 4826 { 4827 leadingBlank = false; 4828 4829 if (inCode || !global.params.markdown) 4830 break; 4831 4832 if (i < buf.length-1 && buf[i+1] == '[') 4833 { 4834 const imageStart = MarkdownDelimiter(i, 2, macroLevel, false, false, false, c); 4835 inlineDelimiters ~= imageStart; 4836 ++i; 4837 } 4838 break; 4839 } 4840 case '[': 4841 { 4842 if (inCode || !global.params.markdown) 4843 { 4844 leadingBlank = false; 4845 break; 4846 } 4847 4848 const leftC = i > offset ? buf[i-1] : '\0'; 4849 const rightFlanking = leftC != '\0' && !isspace(leftC) && !ispunct(leftC); 4850 const atParagraphStart = leadingBlank && iParagraphStart >= iLineStart; 4851 const linkStart = MarkdownDelimiter(i, 1, macroLevel, false, rightFlanking, atParagraphStart, c); 4852 inlineDelimiters ~= linkStart; 4853 leadingBlank = false; 4854 break; 4855 } 4856 case ']': 4857 { 4858 leadingBlank = false; 4859 4860 if (inCode || !global.params.markdown) 4861 break; 4862 4863 for (int d = cast(int) inlineDelimiters.length - 1; d >= 0; --d) 4864 { 4865 const delimiter = inlineDelimiters[d]; 4866 if (delimiter.type == '[' || delimiter.type == '!') 4867 { 4868 if (delimiter.isValid && 4869 MarkdownLink.replaceLink(buf, i, loc, inlineDelimiters, d, linkReferences)) 4870 { 4871 // if we removed a reference link then we're at line start 4872 if (i <= delimiter.iStart) 4873 leadingBlank = true; 4874 4875 // don't nest links 4876 if (delimiter.type == '[') 4877 for (--d; d >= 0; --d) 4878 if (inlineDelimiters[d].type == '[') 4879 inlineDelimiters[d].invalidate(); 4880 } 4881 else 4882 { 4883 // nothing found, so kill the delimiter 4884 inlineDelimiters = inlineDelimiters[0..d] ~ inlineDelimiters[d+1..$]; 4885 } 4886 break; 4887 } 4888 } 4889 break; 4890 } 4891 4892 case '|': 4893 { 4894 if (inCode || !global.params.markdown) 4895 { 4896 leadingBlank = false; 4897 break; 4898 } 4899 4900 tableRowDetected = true; 4901 inlineDelimiters ~= MarkdownDelimiter(i, 1, macroLevel, leadingBlank, false, false, c); 4902 leadingBlank = false; 4903 break; 4904 } 4905 4906 case '\\': 4907 { 4908 leadingBlank = false; 4909 if (inCode || i+1 >= buf.length || !global.params.markdown) 4910 break; 4911 4912 /* Escape Markdown special characters */ 4913 char c1 = buf[i+1]; 4914 if (ispunct(c1)) 4915 { 4916 if (global.params.vmarkdown) 4917 message(loc, "Ddoc: backslash-escaped %c", c1); 4918 4919 buf.remove(i, 1); 4920 4921 auto se = sc._module.escapetable.escapeChar(c1); 4922 if (!se) 4923 se = c1 == '$' ? "$(DOLLAR)" : c1 == ',' ? "$(COMMA)" : null; 4924 if (se) 4925 { 4926 buf.remove(i, 1); 4927 i = buf.insert(i, se); 4928 i--; // point to escaped char 4929 } 4930 } 4931 break; 4932 } 4933 4934 case '$': 4935 { 4936 /* Look for the start of a macro, '$(Identifier' 4937 */ 4938 leadingBlank = false; 4939 if (inCode || inBacktick) 4940 break; 4941 const slice = buf[]; 4942 auto p = &slice[i]; 4943 if (p[1] == '(' && isIdStart(&p[2])) 4944 ++macroLevel; 4945 break; 4946 } 4947 4948 case '(': 4949 { 4950 if (!inCode && i > offset && buf[i-1] != '$') 4951 ++parenLevel; 4952 break; 4953 } 4954 4955 case ')': 4956 { /* End of macro 4957 */ 4958 leadingBlank = false; 4959 if (inCode || inBacktick) 4960 break; 4961 if (parenLevel > 0) 4962 --parenLevel; 4963 else if (macroLevel) 4964 { 4965 int downToLevel = cast(int) inlineDelimiters.length; 4966 while (downToLevel > 0 && inlineDelimiters[downToLevel - 1].macroLevel >= macroLevel) 4967 --downToLevel; 4968 if (headingLevel && headingMacroLevel >= macroLevel) 4969 { 4970 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); 4971 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 4972 } 4973 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 4974 while (nestedLists.length && nestedLists[$-1].macroLevel >= macroLevel) 4975 { 4976 i = buf.insert(i, ")\n)"); 4977 --nestedLists.length; 4978 } 4979 if (quoteLevel && quoteMacroLevel >= macroLevel) 4980 i += endAllMarkdownQuotes(buf, i, quoteLevel); 4981 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters, downToLevel); 4982 4983 --macroLevel; 4984 quoteMacroLevel = 0; 4985 } 4986 break; 4987 } 4988 4989 default: 4990 leadingBlank = false; 4991 if (sc._module.isDocFile || inCode) 4992 break; 4993 const start = cast(char*)buf[].ptr + i; 4994 if (isIdStart(start)) 4995 { 4996 size_t j = skippastident(buf, i); 4997 if (i < j) 4998 { 4999 size_t k = skippastURL(buf, i); 5000 if (i < k) 5001 { 5002 /* The URL is buf[i..k] 5003 */ 5004 if (macroLevel) 5005 /* Leave alone if already in a macro 5006 */ 5007 i = k - 1; 5008 else 5009 { 5010 /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)' 5011 */ 5012 i = buf.bracket(i, "$(DDOC_LINK_AUTODETECT ", k, ")") - 1; 5013 } 5014 break; 5015 } 5016 } 5017 else 5018 break; 5019 size_t len = j - i; 5020 // leading '_' means no highlight unless it's a reserved symbol name 5021 if (c == '_' && (i == 0 || !isdigit(*(start - 1))) && (i == buf.length - 1 || !isReservedName(start[0 .. len]))) 5022 { 5023 buf.remove(i, 1); 5024 i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j - 1, ")") - 1; 5025 break; 5026 } 5027 if (isIdentifier(a, start, len)) 5028 { 5029 i = buf.bracket(i, "$(DDOC_AUTO_PSYMBOL ", j, ")") - 1; 5030 break; 5031 } 5032 if (isKeyword(start, len)) 5033 { 5034 i = buf.bracket(i, "$(DDOC_AUTO_KEYWORD ", j, ")") - 1; 5035 break; 5036 } 5037 if (isFunctionParameter(a, start, len)) 5038 { 5039 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); 5040 i = buf.bracket(i, "$(DDOC_AUTO_PARAM ", j, ")") - 1; 5041 break; 5042 } 5043 i = j - 1; 5044 } 5045 break; 5046 } 5047 } 5048 5049 if (inCode == '-') 5050 error(loc, "unmatched `---` in DDoc comment"); 5051 else if (inCode) 5052 buf.insert(buf.length, ")"); 5053 5054 size_t i = buf.length; 5055 if (headingLevel) 5056 { 5057 endMarkdownHeading(buf, iParagraphStart, i, loc, headingLevel); 5058 removeBlankLineMacro(buf, iPrecedingBlankLine, i); 5059 } 5060 i += endRowAndTable(buf, iLineStart, i, loc, inlineDelimiters, columnAlignments); 5061 i += replaceMarkdownEmphasis(buf, loc, inlineDelimiters); 5062 endAllListsAndQuotes(buf, i, nestedLists, quoteLevel, quoteMacroLevel); 5063 } 5064 5065 /************************************************** 5066 * Highlight code for DDOC section. 5067 */ 5068 private void highlightCode(Scope* sc, Dsymbol s, ref OutBuffer buf, size_t offset) 5069 { 5070 auto imp = s.isImport(); 5071 if (imp && imp.aliases.dim > 0) 5072 { 5073 // For example: `public import core.stdc.string : memcpy, memcmp;` 5074 for(int i = 0; i < imp.aliases.dim; i++) 5075 { 5076 // Need to distinguish between 5077 // `public import core.stdc.string : memcpy, memcmp;` and 5078 // `public import core.stdc.string : copy = memcpy, compare = memcmp;` 5079 auto a = imp.aliases[i]; 5080 auto id = a ? a : imp.names[i]; 5081 auto loc = Loc.init; 5082 if (auto symFromId = sc.search(loc, id, null)) 5083 { 5084 highlightCode(sc, symFromId, buf, offset); 5085 } 5086 } 5087 } 5088 else 5089 { 5090 OutBuffer ancbuf; 5091 emitAnchor(ancbuf, s, sc); 5092 buf.insert(offset, ancbuf[]); 5093 offset += ancbuf.length; 5094 5095 Dsymbols a; 5096 a.push(s); 5097 highlightCode(sc, &a, buf, offset); 5098 } 5099 } 5100 5101 /**************************************************** 5102 */ 5103 private void highlightCode(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset) 5104 { 5105 //printf("highlightCode(a = '%s')\n", a.toChars()); 5106 bool resolvedTemplateParameters = false; 5107 5108 for (size_t i = offset; i < buf.length; i++) 5109 { 5110 char c = buf[i]; 5111 const se = sc._module.escapetable.escapeChar(c); 5112 if (se.length) 5113 { 5114 buf.remove(i, 1); 5115 i = buf.insert(i, se); 5116 i--; // point to ';' 5117 continue; 5118 } 5119 char* start = cast(char*)buf[].ptr + i; 5120 if (isIdStart(start)) 5121 { 5122 size_t j = skipPastIdentWithDots(buf, i); 5123 if (i < j) 5124 { 5125 size_t len = j - i; 5126 if (isIdentifier(a, start, len)) 5127 { 5128 i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1; 5129 continue; 5130 } 5131 } 5132 5133 j = skippastident(buf, i); 5134 if (i < j) 5135 { 5136 size_t len = j - i; 5137 if (isIdentifier(a, start, len)) 5138 { 5139 i = buf.bracket(i, "$(DDOC_PSYMBOL ", j, ")") - 1; 5140 continue; 5141 } 5142 if (isFunctionParameter(a, start, len)) 5143 { 5144 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); 5145 i = buf.bracket(i, "$(DDOC_PARAM ", j, ")") - 1; 5146 continue; 5147 } 5148 i = j - 1; 5149 } 5150 } 5151 else if (!resolvedTemplateParameters) 5152 { 5153 size_t previ = i; 5154 5155 // hunt for template declarations: 5156 foreach (symi; 0 .. a.dim) 5157 { 5158 FuncDeclaration fd = (*a)[symi].isFuncDeclaration(); 5159 5160 if (!fd || !fd.parent || !fd.parent.isTemplateDeclaration()) 5161 { 5162 continue; 5163 } 5164 5165 TemplateDeclaration td = fd.parent.isTemplateDeclaration(); 5166 5167 // build the template parameters 5168 Array!(size_t) paramLens; 5169 paramLens.reserve(td.parameters.dim); 5170 5171 OutBuffer parametersBuf; 5172 HdrGenState hgs; 5173 5174 parametersBuf.writeByte('('); 5175 5176 foreach (parami; 0 .. td.parameters.dim) 5177 { 5178 TemplateParameter tp = (*td.parameters)[parami]; 5179 5180 if (parami) 5181 parametersBuf.writestring(", "); 5182 5183 size_t lastOffset = parametersBuf.length; 5184 5185 .toCBuffer(tp, ¶metersBuf, &hgs); 5186 5187 paramLens[parami] = parametersBuf.length - lastOffset; 5188 } 5189 parametersBuf.writeByte(')'); 5190 5191 const templateParams = parametersBuf[]; 5192 5193 //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start); 5194 if (start[0 .. templateParams.length] == templateParams) 5195 { 5196 immutable templateParamListMacro = "$(DDOC_TEMPLATE_PARAM_LIST "; 5197 buf.bracket(i, templateParamListMacro.ptr, i + templateParams.length, ")"); 5198 5199 // We have the parameter list. While we're here we might 5200 // as well wrap the parameters themselves as well 5201 5202 // + 1 here to take into account the opening paren of the 5203 // template param list 5204 i += templateParamListMacro.length + 1; 5205 5206 foreach (const len; paramLens) 5207 { 5208 i = buf.bracket(i, "$(DDOC_TEMPLATE_PARAM ", i + len, ")"); 5209 // increment two here for space + comma 5210 i += 2; 5211 } 5212 5213 resolvedTemplateParameters = true; 5214 // reset i to be positioned back before we found the template 5215 // param list this assures that anything within the template 5216 // param list that needs to be escaped or otherwise altered 5217 // has an opportunity for that to happen outside of this context 5218 i = previ; 5219 5220 continue; 5221 } 5222 } 5223 } 5224 } 5225 } 5226 5227 /**************************************** 5228 */ 5229 private void highlightCode3(Scope* sc, ref OutBuffer buf, const(char)* p, const(char)* pend) 5230 { 5231 for (; p < pend; p++) 5232 { 5233 const se = sc._module.escapetable.escapeChar(*p); 5234 if (se.length) 5235 buf.writestring(se); 5236 else 5237 buf.writeByte(*p); 5238 } 5239 } 5240 5241 /************************************************** 5242 * Highlight code for CODE section. 5243 */ 5244 private void highlightCode2(Scope* sc, Dsymbols* a, ref OutBuffer buf, size_t offset) 5245 { 5246 import dmd.diagnostic : DefaultDiagnosticHandler; 5247 5248 uint errorsave = global.startGagging(); 5249 5250 DefaultDiagnosticHandler diagnosticHandler; 5251 scope Lexer lex = new Lexer(null, cast(char*)buf[].ptr, 0, buf.length - 1, 0, 1, diagnosticHandler.diagnosticHandler); 5252 OutBuffer res; 5253 const(char)* lastp = cast(char*)buf[].ptr; 5254 //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr); 5255 res.reserve(buf.length); 5256 while (1) 5257 { 5258 Token tok; 5259 lex.scan(&tok); 5260 diagnosticHandler.report(); 5261 highlightCode3(sc, res, lastp, tok.ptr); 5262 string highlight = null; 5263 switch (tok.value) 5264 { 5265 case TOK.identifier: 5266 { 5267 if (!sc) 5268 break; 5269 size_t len = lex.p - tok.ptr; 5270 if (isIdentifier(a, tok.ptr, len)) 5271 { 5272 highlight = "$(D_PSYMBOL "; 5273 break; 5274 } 5275 if (isFunctionParameter(a, tok.ptr, len)) 5276 { 5277 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j); 5278 highlight = "$(D_PARAM "; 5279 break; 5280 } 5281 break; 5282 } 5283 case TOK.comment: 5284 highlight = "$(D_COMMENT "; 5285 break; 5286 case TOK.string_: 5287 highlight = "$(D_STRING "; 5288 break; 5289 default: 5290 if (tok.isKeyword()) 5291 highlight = "$(D_KEYWORD "; 5292 break; 5293 } 5294 if (highlight) 5295 { 5296 res.writestring(highlight); 5297 size_t o = res.length; 5298 highlightCode3(sc, res, tok.ptr, lex.p); 5299 if (tok.value == TOK.comment || tok.value == TOK.string_) 5300 /* https://issues.dlang.org/show_bug.cgi?id=7656 5301 * https://issues.dlang.org/show_bug.cgi?id=7715 5302 * https://issues.dlang.org/show_bug.cgi?id=10519 5303 */ 5304 escapeDdocString(&res, o); 5305 res.writeByte(')'); 5306 } 5307 else 5308 highlightCode3(sc, res, tok.ptr, lex.p); 5309 if (tok.value == TOK.endOfFile) 5310 break; 5311 lastp = lex.p; 5312 } 5313 buf.setsize(offset); 5314 buf.write(&res); 5315 global.endGagging(errorsave); 5316 } 5317 5318 /**************************************** 5319 * Determine if p points to the start of a "..." parameter identifier. 5320 */ 5321 private bool isCVariadicArg(const(char)[] p) 5322 { 5323 return p.length >= 3 && p[0 .. 3] == "..."; 5324 } 5325 5326 /**************************************** 5327 * Determine if p points to the start of an identifier. 5328 */ 5329 bool isIdStart(const(char)* p) 5330 { 5331 dchar c = *p; 5332 if (isalpha(c) || c == '_') 5333 return true; 5334 if (c >= 0x80) 5335 { 5336 size_t i = 0; 5337 if (utf_decodeChar(p[0 .. 4], i, c)) 5338 return false; // ignore errors 5339 if (isUniAlpha(c)) 5340 return true; 5341 } 5342 return false; 5343 } 5344 5345 /**************************************** 5346 * Determine if p points to the rest of an identifier. 5347 */ 5348 bool isIdTail(const(char)* p) 5349 { 5350 dchar c = *p; 5351 if (isalnum(c) || c == '_') 5352 return true; 5353 if (c >= 0x80) 5354 { 5355 size_t i = 0; 5356 if (utf_decodeChar(p[0 .. 4], i, c)) 5357 return false; // ignore errors 5358 if (isUniAlpha(c)) 5359 return true; 5360 } 5361 return false; 5362 } 5363 5364 /**************************************** 5365 * Determine if p points to the indentation space. 5366 */ 5367 private bool isIndentWS(const(char)* p) 5368 { 5369 return (*p == ' ') || (*p == '\t'); 5370 } 5371 5372 /***************************************** 5373 * Return number of bytes in UTF character. 5374 */ 5375 int utfStride(const(char)* p) 5376 { 5377 dchar c = *p; 5378 if (c < 0x80) 5379 return 1; 5380 size_t i = 0; 5381 utf_decodeChar(p[0 .. 4], i, c); // ignore errors, but still consume input 5382 return cast(int)i; 5383 } 5384 5385 private inout(char)* stripLeadingNewlines(inout(char)* s) 5386 { 5387 while (s && *s == '\n' || *s == '\r') 5388 s++; 5389 5390 return s; 5391 }