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