1 /** 2 DietDoc/DDOC support routines 3 4 Copyright: © 2012-2015 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module ddox.ddoc; 9 10 import vibe.core.log; 11 import vibe.utils.string; 12 13 import hyphenate : Hyphenator; 14 15 import std.algorithm : canFind, countUntil, map, min, remove; 16 import std.array; 17 import std.conv; 18 import std.string; 19 import std.uni : isAlpha; 20 21 // TODO: support escapes section 22 23 24 shared static this() 25 { 26 s_standardMacros = 27 [ 28 `B`: `<b>$0</b>`, 29 `I`: `<i>$0</i>`, 30 `U`: `<u>$0</u>`, 31 `P` : `<p>$0</p>`, 32 `DL` : `<dl>$0</dl>`, 33 `DT` : `<dt>$0</dt>`, 34 `DD` : `<dd>$0</dd>`, 35 `TABLE` : `<table>$0</table>`, 36 `TR` : `<tr>$0</tr>`, 37 `TH` : `<th>$0</th>`, 38 `TD` : `<td>$0</td>`, 39 `OL` : `<ol>$0</ol>`, 40 `UL` : `<ul>$0</ul>`, 41 `LI` : `<li>$0</li>`, 42 `LINK` : `<a href="$0">$0</a>`, 43 `LINK2` : `<a href="$1">$+</a>`, 44 `LPAREN` : `(`, 45 `RPAREN` : `)`, 46 47 `RED` : `<font color=red>$0</font>`, 48 `BLUE` : `<font color=blue>$0</font>`, 49 `GREEN` : `<font color=green>$0</font>`, 50 `YELLOW` : `<font color=yellow>$0</font>`, 51 `BLACK` : `<font color=black>$0</font>`, 52 `WHITE` : `<font color=white>$0</font>`, 53 54 `D_CODE` : `<pre class="d_code">$0</pre>`, 55 `D_COMMENT` : `$(GREEN $0)`, 56 `D_STRING` : `$(RED $0)`, 57 `D_KEYWORD` : `$(BLUE $0)`, 58 `D_PSYMBOL` : `$(U $0)`, 59 `D_PARAM` : `$(I $0)`, 60 `BACKTICK`: "`", 61 `DDOC_BACKQUOTED`: `$(D_INLINECODE $0)`, 62 //`D_INLINECODE`: `<pre style="display:inline;" class="d_inline_code">$0</pre>`, 63 `D_INLINECODE`: `<code class="lang-d">$0</code>`, 64 65 `DDOC` : `<html> 66 <head> 67 <META http-equiv="content-type" content="text/html; charset=utf-8"> 68 <title>$(TITLE)</title> 69 </head> 70 <body> 71 <h1>$(TITLE)</h1> 72 $(BODY) 73 </body> 74 </html>`, 75 76 `DDOC_COMMENT` : `<!-- $0 -->`, 77 `DDOC_DECL` : `$(DT $(BIG $0))`, 78 `DDOC_DECL_DD` : `$(DD $0)`, 79 `DDOC_DITTO` : `$(BR)$0`, 80 `DDOC_SECTIONS` : `$0`, 81 `DDOC_SUMMARY` : `$0$(BR)$(BR)`, 82 `DDOC_DESCRIPTION` : `$0$(BR)$(BR)`, 83 `DDOC_AUTHORS` : "$(B Authors:)$(BR)\n$0$(BR)$(BR)", 84 `DDOC_BUGS` : "$(RED BUGS:)$(BR)\n$0$(BR)$(BR)", 85 `DDOC_COPYRIGHT` : "$(B Copyright:)$(BR)\n$0$(BR)$(BR)", 86 `DDOC_DATE` : "$(B Date:)$(BR)\n$0$(BR)$(BR)", 87 `DDOC_DEPRECATED` : "$(RED Deprecated:)$(BR)\n$0$(BR)$(BR)", 88 `DDOC_EXAMPLES` : "$(B Examples:)$(BR)\n$0$(BR)$(BR)", 89 `DDOC_HISTORY` : "$(B History:)$(BR)\n$0$(BR)$(BR)", 90 `DDOC_LICENSE` : "$(B License:)$(BR)\n$0$(BR)$(BR)", 91 `DDOC_RETURNS` : "$(B Returns:)$(BR)\n$0$(BR)$(BR)", 92 `DDOC_SEE_ALSO` : "$(B See Also:)$(BR)\n$0$(BR)$(BR)", 93 `DDOC_STANDARDS` : "$(B Standards:)$(BR)\n$0$(BR)$(BR)", 94 `DDOC_THROWS` : "$(B Throws:)$(BR)\n$0$(BR)$(BR)", 95 `DDOC_VERSION` : "$(B Version:)$(BR)\n$0$(BR)$(BR)", 96 `DDOC_SECTION_H` : `$(B $0)$(BR)$(BR)`, 97 `DDOC_SECTION` : `$0$(BR)$(BR)`, 98 `DDOC_MEMBERS` : `$(DL $0)`, 99 `DDOC_MODULE_MEMBERS` : `$(DDOC_MEMBERS $0)`, 100 `DDOC_CLASS_MEMBERS` : `$(DDOC_MEMBERS $0)`, 101 `DDOC_STRUCT_MEMBERS` : `$(DDOC_MEMBERS $0)`, 102 `DDOC_ENUM_MEMBERS` : `$(DDOC_MEMBERS $0)`, 103 `DDOC_TEMPLATE_MEMBERS` : `$(DDOC_MEMBERS $0)`, 104 `DDOC_PARAMS` : "$(B Params:)$(BR)\n$(TABLE $0)$(BR)", 105 `DDOC_PARAM_ROW` : `$(TR $0)`, 106 `DDOC_PARAM_ID` : `$(TD $0)`, 107 `DDOC_PARAM_DESC` : `$(TD $0)`, 108 `DDOC_BLANKLINE` : `$(BR)$(BR)`, 109 110 `DDOC_ANCHOR` : `<a name="$1"></a>`, 111 `DDOC_PSYMBOL` : `$(U $0)`, 112 `DDOC_KEYWORD` : `$(B $0)`, 113 `DDOC_PARAM` : `$(I $0)`, 114 ]; 115 import std.datetime : Clock; 116 auto now = Clock.currTime(); 117 s_standardMacros["DATETIME"] = "%s %s %s %s:%s:%s %s".format( 118 now.dayOfWeek.to!string.capitalize, now.month.to!string.capitalize, 119 now.day, now.hour, now.minute, now.second, now.year); 120 s_standardMacros["YEAR"] = now.year.to!string; 121 } 122 123 124 /** 125 Takes a DDOC string and outputs formatted HTML. 126 127 The hlevel parameter specifies the header level used for section names (<h2> by default). 128 By specifying a display_section callback it is also possible to output only certain sections. 129 */ 130 string formatDdocComment(string ddoc_, int hlevel = 2, bool delegate(string) display_section = null) 131 { 132 return formatDdocComment(ddoc_, new BareContext, hlevel, display_section); 133 } 134 /// ditto 135 string formatDdocComment(string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null) 136 { 137 auto dst = appender!string(); 138 filterDdocComment(dst, text, context, hlevel, display_section); 139 return dst.data; 140 } 141 /// ditto 142 void filterDdocComment(R)(ref R dst, string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null) 143 { 144 auto comment = new DdocComment(text); 145 comment.renderSectionsR(dst, context, display_section, hlevel); 146 } 147 148 149 /** 150 Sets a set of macros that will be available to all calls to formatDdocComment. 151 */ 152 void setDefaultDdocMacroFiles(string[] filenames) 153 { 154 import vibe.core.file; 155 import vibe.stream.operations; 156 s_defaultMacros = null; 157 foreach (filename; filenames) { 158 auto text = readAllUTF8(openFile(filename)); 159 parseMacros(s_defaultMacros, splitLines(text)); 160 } 161 } 162 163 164 /** 165 Sets a set of macros that will be available to all calls to formatDdocComment and override local macro definitions. 166 */ 167 void setOverrideDdocMacroFiles(string[] filenames) 168 { 169 import vibe.core.file; 170 import vibe.stream.operations; 171 s_overrideMacros = null; 172 foreach (filename; filenames) { 173 auto text = readAllUTF8(openFile(filename)); 174 parseMacros(s_overrideMacros, splitLines(text)); 175 } 176 } 177 178 179 /** 180 Enable hyphenation of doc text. 181 */ 182 void enableHyphenation() 183 { 184 s_hyphenator = Hyphenator(import("hyphen.tex")); // en-US 185 s_enableHyphenation = true; 186 } 187 188 189 void hyphenate(R)(in char[] word, R orng) 190 { 191 s_hyphenator.hyphenate(word, "\­", s => orng.put(s)); 192 } 193 194 /** 195 Holds a DDOC comment and formats it sectionwise as HTML. 196 */ 197 class DdocComment { 198 private { 199 Section[] m_sections; 200 string[string] m_macros; 201 bool m_isDitto = false; 202 bool m_isPrivate = false; 203 } 204 205 this(string text) 206 { 207 208 if (text.strip.icmp("ditto") == 0) { m_isDitto = true; return; } 209 if (text.strip.icmp("private") == 0) { m_isPrivate = true; return; } 210 211 auto lines = splitLines(text); 212 if( !lines.length ) return; 213 214 int getLineType(int i) 215 { 216 auto ln = strip(lines[i]); 217 if( ln.length == 0 ) return BLANK; 218 else if( ln.length >= 3 && ln.allOf("-") ) return CODE; 219 else if( ln.indexOf(':') > 0 && isIdent(ln[0 .. ln.indexOf(':')]) ) return SECTION; 220 return TEXT; 221 } 222 223 int skipCodeBlock(int start) 224 { 225 do { 226 start++; 227 } while(start < lines.length && getLineType(start) != CODE); 228 if (start >= lines.length) return start; // unterminated code section 229 return start+1; 230 } 231 232 int skipSection(int start) 233 { 234 while (start < lines.length) { 235 if (getLineType(start) == SECTION) break; 236 if (getLineType(start) == CODE) 237 start = skipCodeBlock(start); 238 else start++; 239 } 240 return start; 241 } 242 243 int skipBlock(int start) 244 { 245 do { 246 start++; 247 } while(start < lines.length && getLineType(start) == TEXT); 248 return start; 249 } 250 251 252 int i = 0; 253 254 // special case short description on the first line 255 while( i < lines.length && getLineType(i) == BLANK ) i++; 256 if( i < lines.length && getLineType(i) == TEXT ){ 257 auto j = skipBlock(i); 258 m_sections ~= Section("$Short", lines[i .. j]); 259 i = j; 260 } 261 262 // first section is implicitly the long description 263 { 264 auto j = skipSection(i); 265 if( j > i ){ 266 m_sections ~= Section("$Long", lines[i .. j]); 267 i = j; 268 } 269 } 270 271 // parse all other sections 272 while( i < lines.length ){ 273 assert(getLineType(i) == SECTION); 274 auto j = skipSection(i+1); 275 assert(j <= lines.length); 276 auto pidx = lines[i].indexOf(':'); 277 auto sect = strip(lines[i][0 .. pidx]); 278 lines[i] = stripLeftDD(lines[i][pidx+1 .. $]); 279 if (lines[i].empty && i < lines.length) i++; 280 if (sect == "Macros") parseMacros(m_macros, lines[i .. j]); 281 else { 282 m_sections ~= Section(sect, lines[i .. j]); 283 } 284 i = j; 285 } 286 } 287 288 @property bool isDitto() const { return m_isDitto; } 289 @property bool isPrivate() const { return m_isPrivate; } 290 291 /// The macros contained in the "Macros" section (if any) 292 @property const(string[string]) macros() const { return m_macros; } 293 294 bool hasSection(string name) const { return m_sections.canFind!(s => s.name == name); } 295 296 void renderSectionR(R)(ref R dst, DdocContext context, string name, int hlevel = 2) 297 { 298 renderSectionsR(dst, context, s => s == name, hlevel); 299 } 300 301 void renderSectionsR(R)(ref R dst, DdocContext context, scope bool delegate(string) display_section, int hlevel) 302 { 303 string[string] allmacros; 304 foreach (k, v; context.defaultMacroDefinitions) allmacros[k] = v; 305 foreach (k, v; m_macros) allmacros[k] = v; 306 foreach (k, v; context.overrideMacroDefinitions) allmacros[k] = v; 307 308 foreach (s; m_sections) { 309 if (display_section && !display_section(s.name)) continue; 310 parseSection(dst, s.name, s.lines, context, hlevel, allmacros); 311 } 312 } 313 314 string renderSection(DdocContext context, string name, int hlevel = 2) 315 { 316 auto dst = appender!string(); 317 renderSectionR(dst, context, name, hlevel); 318 return dst.data; 319 } 320 321 string renderSections(DdocContext context, bool delegate(string) display_section, int hlevel) 322 { 323 auto dst = appender!string(); 324 renderSectionsR(dst, context, display_section, hlevel); 325 return dst.data; 326 } 327 } 328 329 330 /** 331 Provides context information about the documented element. 332 */ 333 interface DdocContext { 334 /// A line array with macro definitions 335 @property string[string] defaultMacroDefinitions(); 336 337 /// Line array with macro definitions that take precedence over local macros 338 @property string[string] overrideMacroDefinitions(); 339 340 /// Looks up a symbol in the scope of the documented element and returns a link to it. 341 string lookupScopeSymbolLink(string name); 342 } 343 344 345 private class BareContext : DdocContext { 346 @property string[string] defaultMacroDefinitions() { return null; } 347 @property string[string] overrideMacroDefinitions() { return null; } 348 string lookupScopeSymbolLink(string name) { return null; } 349 } 350 351 private enum { 352 BLANK, 353 TEXT, 354 CODE, 355 SECTION 356 } 357 358 private struct Section { 359 string name; 360 string[] lines; 361 362 this(string name, string[] lines...) 363 { 364 this.name = name; 365 this.lines = lines; 366 } 367 } 368 369 private { 370 immutable string[string] s_standardMacros; 371 string[string] s_defaultMacros; 372 string[string] s_overrideMacros; 373 bool s_enableHyphenation; 374 Hyphenator s_hyphenator; 375 } 376 377 /// private 378 private void parseSection(R)(ref R dst, string sect, string[] lines, DdocContext context, int hlevel, string[string] macros) 379 { 380 if( sect == "$Short" ) hlevel = -1; 381 382 void putHeader(string hdr){ 383 if( hlevel <= 0 ) return; 384 dst.put("<section>"); 385 if( sect.length > 0 && sect[0] != '$' ){ 386 dst.put("<h"~to!string(hlevel)~">"); 387 foreach( ch; hdr ) dst.put(ch == '_' ? ' ' : ch); 388 dst.put("</h"~to!string(hlevel)~">\n"); 389 } 390 } 391 392 void putFooter(){ 393 if( hlevel <= 0 ) return; 394 dst.put("</section>\n"); 395 } 396 397 int getLineType(int i) 398 { 399 auto ln = strip(lines[i]); 400 if( ln.length == 0 ) return BLANK; 401 else if( ln.length >= 3 &&ln.allOf("-") ) return CODE; 402 else if( ln.indexOf(':') > 0 && !ln[0 .. ln.indexOf(':')].anyOf(" \t") ) return SECTION; 403 return TEXT; 404 } 405 406 int skipBlock(int start) 407 { 408 do { 409 start++; 410 } while(start < lines.length && getLineType(start) == TEXT); 411 return start; 412 } 413 414 int skipCodeBlock(int start) 415 { 416 do { 417 start++; 418 } while(start < lines.length && getLineType(start) != CODE); 419 return start; 420 } 421 422 // handle backtick inline-code 423 for (int i = 0; i < lines.length; i++) { 424 int lntype = getLineType(i); 425 if (lntype == CODE) i = skipCodeBlock(i); 426 else if (sect == "Params") { 427 auto idx = lines[i].indexOf('='); 428 if (idx > 0 && isIdent(lines[i][0 .. idx].strip)) { 429 lines[i] = lines[i][0 .. idx+1] ~ lines[i][idx+1 .. $].highlightAndCrossLink(context); 430 } else { 431 lines[i] = lines[i].highlightAndCrossLink(context); 432 } 433 } else lines[i] = lines[i].highlightAndCrossLink(context); 434 } 435 lines = renderMacros(lines.join("\n").stripDD, context, macros).splitLines(); 436 437 switch( sect ){ 438 default: 439 putHeader(sect); 440 int i = 0; 441 while( i < lines.length ){ 442 int lntype = getLineType(i); 443 444 switch( lntype ){ 445 default: assert(false, "Unexpected line type "~to!string(lntype)~": "~lines[i]); 446 case BLANK: 447 dst.put('\n'); 448 i++; 449 continue; 450 case SECTION: 451 case TEXT: 452 if( hlevel >= 0 ) dst.put("<p>"); 453 auto j = skipBlock(i); 454 bool first = true; 455 renderTextLine(dst, lines[i .. j].join("\n")/*.stripDD*/, context); 456 dst.put('\n'); 457 if( hlevel >= 0 ) dst.put("</p>\n"); 458 i = j; 459 break; 460 case CODE: 461 dst.put("<pre class=\"code\"><code class=\"lang-d\">"); 462 auto j = skipCodeBlock(i); 463 auto base_indent = baseIndent(lines[i+1 .. j]); 464 renderCodeLine(dst, lines[i+1 .. j].map!(ln => ln.unindent(base_indent)).join("\n"), context); 465 dst.put("</code></pre>\n"); 466 i = j+1; 467 break; 468 } 469 } 470 putFooter(); 471 break; 472 case "Params": 473 putHeader("Parameters"); 474 dst.put("<table><col class=\"caption\"><tr><th>Name</th><th>Description</th></tr>\n"); 475 bool in_parameter = false; 476 string desc; 477 foreach( string ln; lines ){ 478 // check if the line starts a parameter documentation 479 string name; 480 auto eidx = ln.indexOf("="); 481 if( eidx > 0 ) name = ln[0 .. eidx].strip(); 482 if( !isIdent(name) ) name = null; 483 484 // if it does, start a new row 485 if( name.length ){ 486 if( in_parameter ){ 487 renderTextLine(dst, desc, context); 488 dst.put("</td></tr>\n"); 489 } 490 491 dst.put("<tr><td id=\""); 492 dst.put(name); 493 dst.put("\">"); 494 dst.put(name); 495 dst.put("</td><td>"); 496 497 desc = ln[eidx+1 .. $]; 498 in_parameter = true; 499 } else if( in_parameter ) desc ~= "\n" ~ ln; 500 } 501 502 if( in_parameter ){ 503 renderTextLine(dst, desc, context); 504 dst.put("</td></tr>\n"); 505 } 506 507 dst.put("</table>\n"); 508 putFooter(); 509 break; 510 } 511 } 512 513 private string highlightAndCrossLink(string line, DdocContext context) 514 { 515 auto dst = appender!string; 516 highlightAndCrossLink(dst, line, context); 517 return dst.data; 518 } 519 520 private void highlightAndCrossLink(R)(ref R dst, string line, DdocContext context) 521 { 522 int inCode; 523 while( line.length > 0 ){ 524 switch( line[0] ){ 525 default: 526 dst.put(line[0]); 527 line = line[1 .. $]; 528 break; 529 case '<': 530 auto res = skipHtmlTag(line); 531 if (res.startsWith("<code")) 532 ++inCode; 533 else if (res == "</code>") 534 --inCode; 535 dst.put(res); 536 break; 537 case '_': 538 line = line[1 .. $]; 539 auto ident = skipIdent(line); 540 if( ident.length ) 541 { 542 if (s_enableHyphenation && !inCode) 543 hyphenate(ident, dst); 544 else 545 dst.put(ident); 546 } 547 else dst.put('_'); 548 break; 549 case '`': 550 line.popFront(); 551 auto idx = line.indexOf('`'); 552 if (idx < 0) break; 553 dst.put("<code class=\"lang-d\">"); 554 dst.renderCodeLine(line[0 .. idx], context); 555 dst.put("</code>"); 556 line = line[idx+1 .. $]; 557 break; 558 case '.': 559 if (line.length > 1 && (line[1 .. $].front.isAlpha || line[1] == '_')) goto case; 560 else goto default; 561 case 'a': .. case 'z': 562 case 'A': .. case 'Z': 563 564 auto url = skipUrl(line); 565 if( url.length ){ 566 /*dst.put("<a href=\""); 567 dst.put(url); 568 dst.put("\">");*/ 569 dst.put(url); 570 //dst.put("</a>"); 571 break; 572 } 573 574 auto ident = skipIdent(line); 575 auto link = context.lookupScopeSymbolLink(ident); 576 if (link.length && inCode) { 577 import ddox.highlight : highlightDCode; 578 if( link != "#" ){ 579 dst.put("<a href=\""); 580 dst.put(link); 581 dst.put("\">"); 582 } 583 dst.highlightDCode(ident, null); 584 if( link != "#" ) dst.put("</a>"); 585 } else { 586 ident = ident.replace("._", "."); 587 if (s_enableHyphenation && !inCode) 588 hyphenate(ident, dst); 589 else 590 dst.put(ident); 591 } 592 break; 593 } 594 } 595 } 596 597 /// private 598 private void renderTextLine(R)(ref R dst, string line, DdocContext context) 599 { 600 while( line.length > 0 ){ 601 switch( line[0] ){ 602 default: 603 dst.put(line[0]); 604 line = line[1 .. $]; 605 break; 606 case '<': 607 dst.put(skipHtmlTag(line)); 608 break; 609 case '>': 610 dst.put(">"); 611 line.popFront(); 612 break; 613 case '&': 614 if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&'); 615 else dst.put("&"); 616 line.popFront(); 617 break; 618 } 619 } 620 } 621 622 /// private 623 private void renderCodeLine(R)(ref R dst, string line, DdocContext context) 624 { 625 import ddox.highlight : highlightDCode; 626 dst.highlightDCode(line, (string ident, scope void delegate(bool) insert_ident) { 627 auto link = context.lookupScopeSymbolLink(ident); 628 if (link.length && link != "#") { 629 dst.put("<a href=\""); 630 dst.put(link); 631 dst.put("\">"); 632 insert_ident(true); 633 dst.put("</a>"); 634 } else insert_ident(false); 635 }); 636 } 637 638 /// private 639 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 640 { 641 while( !line.empty ){ 642 auto idx = line.indexOf('$'); 643 if( idx < 0 ){ 644 dst.put(line); 645 return; 646 } 647 dst.put(line[0 .. idx]); 648 line = line[idx .. $]; 649 renderMacro(dst, line, context, macros, params, callstack); 650 } 651 } 652 653 /// private 654 private string renderMacros(string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 655 { 656 auto app = appender!string; 657 renderMacros(app, line, context, macros, params, callstack); 658 return app.data; 659 } 660 661 /// private 662 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params, MacroInvocation[] callstack) 663 { 664 assert(line[0] == '$'); 665 line = line[1 .. $]; 666 if( line.length < 1) { 667 dst.put("$"); 668 return; 669 } 670 671 if( line[0] >= '0' && line[0] <= '9' ){ 672 int pidx = line[0]-'0'; 673 if( pidx < params.length ) 674 dst.put(params[pidx]); 675 line = line[1 .. $]; 676 } else if( line[0] == '+' ){ 677 if( params.length ){ 678 auto idx = params[0].indexOf(','); 679 if( idx >= 0 ) dst.put(params[0][idx+1 .. $].specialStrip()); 680 } 681 line = line[1 .. $]; 682 } else if( line[0] == '(' ){ 683 line = line[1 .. $]; 684 int l = 1; 685 size_t cidx = 0; 686 for( cidx = 0; cidx < line.length && l > 0; cidx++ ){ 687 if( line[cidx] == '(' ) l++; 688 else if( line[cidx] == ')' ) l--; 689 } 690 if( l > 0 ){ 691 logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]); 692 dst.put("("); 693 return; 694 } 695 if( cidx < 1 ){ 696 logDebug("Empty macro parens."); 697 return; 698 } 699 700 auto mnameidx = line[0 .. cidx-1].countUntilAny(", \t\r\n"); 701 if( mnameidx < 0 ) mnameidx = cidx-1; 702 if( mnameidx == 0 ){ 703 logDebug("Macro call in DDOC comment is missing macro name."); 704 return; 705 } 706 707 auto mname = line[0 .. mnameidx]; 708 string rawargtext = line[mnameidx .. cidx-1]; 709 710 string[] args; 711 if (rawargtext.length) { 712 auto rawargs = splitParams(rawargtext); 713 foreach( arg; rawargs ){ 714 auto argtext = appender!string(); 715 renderMacros(argtext, arg, context, macros, params, callstack); 716 auto newargs = splitParams(argtext.data); 717 if (newargs.length == 0) args ~= ""; // always add at least one argument per raw argument 718 else args ~= newargs; 719 } 720 } 721 if (args.length == 1 && args[0].specialStrip.length == 0) args = null; // remove a single empty argument 722 723 args = join(args, ",").specialStrip() ~ args.map!(a => a.specialStrip).array; 724 725 logTrace("PARAMS for %s: %s", mname, args); 726 line = line[cidx .. $]; 727 728 // check for recursion termination conditions 729 foreach_reverse (ref c; callstack) { 730 if (c.name == mname && (args.length <= 1 || args == c.params)) { 731 logTrace("Terminating recursive macro call of %s: %s", mname, params.length <= 1 ? "no argument text" : "same arguments as previous invocation"); 732 //line = line[cidx .. $]; 733 return; 734 } 735 } 736 callstack.assumeSafeAppend(); 737 callstack ~= MacroInvocation(mname, args); 738 739 740 const(string)* pm = mname in s_overrideMacros; 741 if( !pm ) pm = mname in macros; 742 if( !pm ) pm = mname in s_defaultMacros; 743 if( !pm ) pm = mname in s_standardMacros; 744 745 if (mname == "D") { 746 auto tmp = appender!string; 747 renderMacros(tmp, "$0", context, macros, args, callstack); 748 dst.put("<code class=\"lang-d\">"); 749 dst.renderCodeLine(tmp.data, context); 750 dst.put("</code>"); 751 } else if (pm) { 752 logTrace("MACRO %s: %s", mname, *pm); 753 renderMacros(dst, *pm, context, macros, args, callstack); 754 } else { 755 logTrace("Macro '%s' not found.", mname); 756 if( args.length ) dst.put(args[0]); 757 } 758 } else dst.put("$"); 759 } 760 761 private struct MacroInvocation { 762 string name; 763 string[] params; 764 } 765 766 private string[] splitParams(string ln) 767 { 768 string[] ret; 769 size_t i = 0, start = 0; 770 while(i < ln.length){ 771 if( ln[i] == ',' ){ 772 ret ~= ln[start .. i]; 773 start = ++i; 774 } else if( ln[i] == '(' ){ 775 i++; 776 int l = 1; 777 for( ; i < ln.length && l > 0; i++ ){ 778 if( ln[i] == '(' ) l++; 779 else if( ln[i] == ')' ) l--; 780 } 781 } else i++; 782 } 783 if( i > start ) ret ~= ln[start .. i]; 784 return ret; 785 } 786 787 private string skipHtmlTag(ref string ln) 788 { 789 assert(ln[0] == '<'); 790 791 // skip HTML comment 792 if (ln.startsWith("<!--")) { 793 auto idx = ln[4 .. $].indexOf("-->"); 794 if (idx < 0) { 795 ln.popFront(); 796 return "<"; 797 } 798 auto ret = ln[0 .. idx+7]; 799 ln = ln[ret.length .. $]; 800 return ret; 801 } 802 803 // too short for a tag 804 if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) { 805 // found no match, return escaped '<' 806 logTrace("Found stray '<' in DDOC string."); 807 ln.popFront(); 808 return "<"; 809 } 810 811 // skip over regular start/end tag 812 auto idx = ln.indexOf(">"); 813 if (idx < 0) { 814 ln.popFront(); 815 return "<"; 816 } 817 auto ret = ln[0 .. idx+1]; 818 ln = ln[ret.length .. $]; 819 return ret; 820 } 821 822 private string skipUrl(ref string ln) 823 { 824 if( !ln.startsWith("http://") && !ln.startsWith("http://") ) 825 return null; 826 827 bool saw_dot = false; 828 size_t i = 7; 829 830 for_loop: 831 while( i < ln.length ){ 832 switch( ln[i] ){ 833 default: 834 break for_loop; 835 case 'a': .. case 'z': 836 case 'A': .. case 'Z': 837 case '0': .. case '9': 838 case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~': 839 break; 840 case '.': 841 saw_dot = true; 842 break; 843 } 844 i++; 845 } 846 847 if( saw_dot ){ 848 auto ret = ln[0 .. i]; 849 ln = ln[i .. $]; 850 return ret; 851 } else return null; 852 } 853 854 private string skipIdent(ref string str) 855 { 856 string strcopy = str; 857 858 if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_')) 859 str.popFront(); 860 861 bool last_was_ident = false; 862 while( !str.empty ){ 863 auto ch = str.front; 864 865 if( last_was_ident ){ 866 // dots are allowed if surrounded by identifiers 867 if( ch == '.' ) last_was_ident = false; 868 else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break; 869 } else { 870 if( ch != '_' && !std.uni.isAlpha(ch) ) break; 871 last_was_ident = true; 872 } 873 str.popFront(); 874 } 875 876 // if the identifier ended in a '.', remove it again 877 if( str.length != strcopy.length && !last_was_ident ) 878 str = strcopy[strcopy.length-str.length-1 .. $]; 879 880 return strcopy[0 .. strcopy.length-str.length]; 881 } 882 883 private bool isIdent(string str) 884 { 885 skipIdent(str); 886 return str.length == 0; 887 } 888 889 private void parseMacros(ref string[string] macros, in string[] lines) 890 { 891 string name; 892 foreach (string ln; lines) { 893 // macro definitions are of the form IDENT = ... 894 auto pidx = ln.indexOf('='); 895 if (pidx > 0) { 896 auto tmpnam = ln[0 .. pidx].strip(); 897 // got new macro definition? 898 if (isIdent(tmpnam)) { 899 900 // strip the previous macro 901 if (name.length) macros[name] = macros[name].stripDD(); 902 903 // start parsing the new macro 904 name = tmpnam; 905 macros[name] = stripLeftDD(ln[pidx+1 .. $]); 906 continue; 907 } 908 } 909 910 // append to previous macro definition, if any 911 macros[name] ~= "\n" ~ ln; 912 } 913 } 914 915 private int baseIndent(string[] lines) 916 { 917 if( lines.length == 0 ) return 0; 918 int ret = int.max; 919 foreach( ln; lines ){ 920 int i = 0; 921 while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') ) 922 i++; 923 if( i < ln.length ) ret = min(ret, i); 924 } 925 return ret; 926 } 927 928 private string unindent(string ln, int amount) 929 { 930 while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') ) 931 ln = ln[1 .. $], amount--; 932 return ln; 933 } 934 935 private string stripLeftDD(string s) 936 { 937 while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n')) 938 s.popFront(); 939 return s; 940 } 941 942 private string specialStrip(string s) 943 { 944 import std.algorithm : among; 945 946 // strip trailing whitespace for all lines but the last 947 size_t idx = 0; 948 while (true) { 949 auto nidx = s[idx .. $].indexOf('\n'); 950 if (nidx < 0) break; 951 nidx += idx; 952 auto strippedfront = s[0 .. nidx].stripRightDD(); 953 s = strippedfront ~ "\n" ~ s[nidx+1 .. $]; 954 idx = strippedfront.length + 1; 955 } 956 957 // strip the first character, if whitespace 958 if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront(); 959 960 return s; 961 } 962 963 private string stripRightDD(string s) 964 { 965 while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n')) 966 s.popBack(); 967 return s; 968 } 969 970 private string stripDD(string s) 971 { 972 return s.stripLeftDD.stripRightDD; 973 } 974 975 import std.stdio; 976 unittest { 977 auto src = "$(M a b)\n$(M a\nb)\nMacros:\n M = -$0-\n"; 978 auto dst = "-a b-\n-a\nb-\n"; 979 assert(formatDdocComment(src) == dst); 980 } 981 982 unittest { 983 auto src = "\n $(M a b)\n$(M a \nb)\nMacros:\n M = -$0- \n\nN=$0"; 984 auto dst = "-a b-\n-a\nb-\n"; 985 assert(formatDdocComment(src) == dst); 986 } 987 988 unittest { 989 auto src = "$(M a, b)\n$(M a,\n b)\nMacros:\n M = -$1-\n\n +$2+\n\n N=$0"; 990 auto dst = "-a-\n\n +b+\n-a-\n\n + b+\n"; 991 assert(formatDdocComment(src) == dst); 992 } 993 994 unittest { 995 auto src = "$(GLOSSARY a\nb)\nMacros:\n GLOSSARY = $(LINK2 glossary.html#$0, $0)"; 996 auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n"; 997 assert(formatDdocComment(src) == dst); 998 } 999 1000 unittest { 1001 auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a."; 1002 auto dst = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.\n"; 1003 assert(formatDdocComment(src) == dst); 1004 } 1005 1006 unittest { 1007 auto src = "& &a < &#lt; &- &03; &;"; 1008 auto dst = "& &a < &#lt; &- &03; &;\n"; 1009 assert(formatDdocComment(src) == dst); 1010 } 1011 1012 unittest { 1013 auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = <\nGT = >"; 1014 auto dst = "<a href=\"abc\">test <peter@parker.com></a>\n"; 1015 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n")); 1016 assert(formatDdocComment(src) == dst); 1017 } 1018 1019 unittest { 1020 auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)"; 1021 auto dst = "[a][b][c][d]\n"; 1022 assert(formatDdocComment(src) == dst); 1023 } 1024 1025 unittest { 1026 auto src = "Testing `inline <code>`."; 1027 auto dst = "Testing <code class=\"lang-d\"><span class=\"pln\">inline </span><span class=\"pun\"><</span><span class=\"pln\">code</span><span class=\"pun\">></span></code>.\n"; 1028 assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string); 1029 } 1030 1031 unittest { 1032 auto src = "Testing `inline $(CODE)`."; 1033 auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n"; 1034 assert(formatDdocComment(src)); 1035 } 1036 1037 unittest { 1038 auto src = "---\nthis is a `string`.\n---"; 1039 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"kwd\">this is </span><span class=\"pln\">a </span><span class=\"str\">`string`<wbr/></span><span class=\"pun\">.</span></code></pre>\n</section>\n"; 1040 assert(formatDdocComment(src) == dst); 1041 } 1042 1043 unittest { // test for properly removed indentation in code blocks 1044 auto src = " ---\n testing\n ---"; 1045 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n"; 1046 assert(formatDdocComment(src) == dst); 1047 } 1048 1049 unittest { // issue #99 - parse macros in parameter sections 1050 import std.algorithm : find; 1051 auto src = "Params:\n\tfoo = $(B bar)"; 1052 auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n"; 1053 assert(formatDdocComment(src).find("<td> ") == dst); 1054 } 1055 1056 unittest { // issue #89 (minimal test) - empty first parameter 1057 auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>"; 1058 auto dst = "<div >foo</div>\n"; 1059 assert(formatDdocComment(src) == dst); 1060 } 1061 1062 unittest { // issue #89 (complex test) 1063 auto src = 1064 `$(LIST 1065 $(DIV oops, 1066 foo 1067 ), 1068 $(DIV , 1069 bar 1070 )) 1071 Macros: 1072 LIST=$(UL $(LIX $1, $+)) 1073 LIX=$(LI $1)$(LIX $+) 1074 UL=$(T ul, $0) 1075 LI = $(T li, $0) 1076 DIV=<div $1>$+</div> 1077 T=<$1>$+</$1> 1078 `; 1079 auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n"; 1080 assert(formatDdocComment(src) == dst); 1081 } 1082 1083 unittest { // issue #95 - trailing newlines must be stripped in macro definitions 1084 auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar"; 1085 auto dst = "foo\n"; 1086 assert(formatDdocComment(src) == dst); 1087 } 1088 1089 unittest { // missing macro closing clamp (because it's in a different section) 1090 auto src = "$(B\n\n)"; 1091 auto dst = "(B\n<section><p>)\n</p>\n</section>\n"; 1092 assert(formatDdocComment(src) == dst); 1093 } 1094 1095 unittest { // closing clamp should be found in a different *paragraph* of the same section, though 1096 auto src = "foo\n\n$(B\n\n)"; 1097 auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n"; 1098 assert(formatDdocComment(src) == dst); 1099 } 1100 1101 unittest { // more whitespace testing 1102 auto src = "$(M a , b , c )\nMacros:\nM = A$0B$1C$2D$+E"; 1103 auto dst = "A a , b , c B a C b D b , c E\n"; 1104 assert(formatDdocComment(src) == dst); 1105 } 1106 1107 unittest { // more whitespace testing 1108 auto src = " $(M \n a \n , \n b \n , \n c \n ) \nMacros:\nM = A$0B$1C$2D$+E"; 1109 auto dst = "A a\n ,\n b\n ,\n c\n B a\n C b\n D b\n ,\n c\n E\n"; 1110 assert(formatDdocComment(src) == dst); 1111 } 1112 1113 unittest { // escape in backtick code 1114 auto src = "`<b>&`"; 1115 auto dst = "<code class=\"lang-d\"><span class=\"pun\"><</span><span class=\"pln\">b</span><span class=\"pun\">>&</span><span class=\"pln\">amp</span><span class=\"pun\">;</span></code>\n"; 1116 assert(formatDdocComment(src) == dst); 1117 } 1118 1119 unittest { // escape in code blocks 1120 auto src = "---\n<b>&\n---"; 1121 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pun\"><</span><span class=\"pln\">b</span><span class=\"pun\">>&</span><span class=\"pln\">amp</span><span class=\"pun\">;</span></code></pre>\n</section>\n"; 1122 assert(formatDdocComment(src) == dst); 1123 } 1124 1125 unittest { // #81 empty first macro arguments 1126 auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>"; 1127 auto dst = "<table >test</table>\n"; 1128 assert(formatDdocComment(src) == dst); 1129 } 1130 1131 unittest { // #117 underscore identifiers as macro param 1132 auto src = "$(M __foo) __foo `__foo` $(D_CODE __foo)\nMacros:\nM=http://$1.com"; 1133 auto dst = "http://_foo.com _foo <code class=\"lang-d\"><span class=\"pln\">__foo</span></code> <pre class=\"d_code\">_foo</pre>\n"; 1134 assert(formatDdocComment(src) == dst); 1135 } 1136 1137 unittest { // #109 dot followed by unicode character causes infinite loop 1138 auto src = ".”"; 1139 auto dst = ".”\n"; 1140 assert(formatDdocComment(src) == dst); 1141 } 1142 1143 unittest { // #119 dot followed by space causes assertion 1144 static class Ctx : BareContext { 1145 override string lookupScopeSymbolLink(string name) { 1146 writefln("IDENT: %s", name); 1147 assert(name.length > 0 && name != "."); 1148 return null; 1149 } 1150 } 1151 auto src = "---\n. writeln();\n---"; 1152 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><wbr/><span class=\"pun\">. </span><span class=\"pln\">writeln</span><span class=\"pun\">();</span></code></pre>\n</section>\n"; 1153 assert(formatDdocComment(src, new Ctx) == dst); 1154 } 1155 1156 unittest { // dot followed by non-identifier 1157 static class Ctx : BareContext { 1158 override string lookupScopeSymbolLink(string name) { 1159 writefln("IDENT: %s", name); 1160 assert(name.length > 0 && name != "."); 1161 return null; 1162 } 1163 } 1164 auto src = "---\n.()\n---"; 1165 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><wbr/><span class=\"pun\">.()</span></code></pre>\n</section>\n"; 1166 assert(formatDdocComment(src, new Ctx) == dst); 1167 } 1168 1169 1170 unittest { // X-REF 1171 static class Ctx : BareContext { 1172 override string lookupScopeSymbolLink(string name) { 1173 if (name == "foo") return "foo.html"; 1174 else return null; 1175 } 1176 } 1177 auto src = "`foo` `bar` $(D foo) $(D bar)\n\n---\nfoo bar\n---"; 1178 auto dst = "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1179 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code> " 1180 ~ "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1181 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code>\n" 1182 ~ "<section><pre class=\"code\"><code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a>" 1183 ~ "<span class=\"pln\"> bar</span></code></pre>\n</section>\n"; 1184 assert(formatDdocComment(src, new Ctx) == dst, [formatDdocComment(src, new Ctx)].to!string); 1185 } 1186 1187 unittest { // nested macro in $(D ...) 1188 auto src = "$(D $(NOP foo))\n\nMacros: NOP: $0"; 1189 auto dst = "<code class=\"lang-d\"><span class=\"pln\">foo</span></code>\n<section></section>\n"; 1190 assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string); 1191 }