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