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