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