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 string c = tmp.data; 750 while (c.length) { 751 auto idx = c.indexOf('<'); 752 if (idx > 0) dst.renderCodeLine(c[0 .. idx], context); 753 if (idx < 0) break; 754 c = c[idx .. $]; 755 dst.put(skipHtmlTag(c)); 756 } 757 if (c.length > 0) dst.renderCodeLine(c, context); 758 dst.put("</code>"); 759 } else if (mname == "DDOX_NAMED_REF") { 760 auto sym = appender!string; 761 renderMacros(sym, "$1", context, macros, args, callstack); 762 763 auto link = sym.data.length > 0 && !sym.data.endsWith('.') ? context.lookupScopeSymbolLink(sym.data) : null; 764 if (link.length) { 765 dst.put(`<a href="`); 766 dst.put(link); 767 dst.put(`" title="`); 768 dst.put(sym.data); 769 dst.put(`">`); 770 } 771 dst.renderMacros("$+", context, macros, args, callstack); 772 if (link.length) dst.put("</a>"); 773 } else if (pm) { 774 logTrace("MACRO %s: %s", mname, *pm); 775 renderMacros(dst, *pm, context, macros, args, callstack); 776 } else { 777 logTrace("Macro '%s' not found.", mname); 778 if( args.length ) dst.put(args[0]); 779 } 780 } else dst.put("$"); 781 } 782 783 private struct MacroInvocation { 784 string name; 785 string[] params; 786 } 787 788 private string[] splitParams(string ln) 789 { 790 string[] ret; 791 size_t i = 0, start = 0; 792 while(i < ln.length){ 793 if( ln[i] == ',' ){ 794 ret ~= ln[start .. i]; 795 start = ++i; 796 } else if( ln[i] == '(' ){ 797 i++; 798 int l = 1; 799 for( ; i < ln.length && l > 0; i++ ){ 800 if( ln[i] == '(' ) l++; 801 else if( ln[i] == ')' ) l--; 802 } 803 } else i++; 804 } 805 if( i > start ) ret ~= ln[start .. i]; 806 return ret; 807 } 808 809 private string skipHtmlTag(ref string ln) 810 { 811 assert(ln[0] == '<'); 812 813 // skip HTML comment 814 if (ln.startsWith("<!--")) { 815 auto idx = ln[4 .. $].indexOf("-->"); 816 if (idx < 0) { 817 ln.popFront(); 818 return "<"; 819 } 820 auto ret = ln[0 .. idx+7]; 821 ln = ln[ret.length .. $]; 822 return ret; 823 } 824 825 // too short for a tag 826 if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) { 827 // found no match, return escaped '<' 828 logTrace("Found stray '<' in DDOC string."); 829 ln.popFront(); 830 return "<"; 831 } 832 833 // skip over regular start/end tag 834 auto idx = ln.indexOf(">"); 835 if (idx < 0) { 836 ln.popFront(); 837 return "<"; 838 } 839 auto ret = ln[0 .. idx+1]; 840 ln = ln[ret.length .. $]; 841 return ret; 842 } 843 844 private string skipUrl(ref string ln) 845 { 846 if( !ln.startsWith("http://") && !ln.startsWith("http://") ) 847 return null; 848 849 bool saw_dot = false; 850 size_t i = 7; 851 852 for_loop: 853 while( i < ln.length ){ 854 switch( ln[i] ){ 855 default: 856 break for_loop; 857 case 'a': .. case 'z': 858 case 'A': .. case 'Z': 859 case '0': .. case '9': 860 case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~': 861 break; 862 case '.': 863 saw_dot = true; 864 break; 865 } 866 i++; 867 } 868 869 if( saw_dot ){ 870 auto ret = ln[0 .. i]; 871 ln = ln[i .. $]; 872 return ret; 873 } else return null; 874 } 875 876 private string skipIdent(ref string str) 877 { 878 static import std.uni; 879 880 string strcopy = str; 881 882 if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_')) 883 str.popFront(); 884 885 bool last_was_ident = false; 886 while( !str.empty ){ 887 auto ch = str.front; 888 889 if( last_was_ident ){ 890 // dots are allowed if surrounded by identifiers 891 if( ch == '.' ) last_was_ident = false; 892 else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break; 893 } else { 894 if( ch != '_' && !std.uni.isAlpha(ch) ) break; 895 last_was_ident = true; 896 } 897 str.popFront(); 898 } 899 900 // if the identifier ended in a '.', remove it again 901 if( str.length != strcopy.length && !last_was_ident ) 902 str = strcopy[strcopy.length-str.length-1 .. $]; 903 904 return strcopy[0 .. strcopy.length-str.length]; 905 } 906 907 private bool isIdent(string str) 908 { 909 skipIdent(str); 910 return str.length == 0; 911 } 912 913 private void parseMacros(ref string[string] macros, in string[] lines) 914 { 915 string name; 916 foreach (string ln; lines) { 917 // macro definitions are of the form IDENT = ... 918 auto pidx = ln.indexOf('='); 919 if (pidx > 0) { 920 auto tmpnam = ln[0 .. pidx].strip(); 921 // got new macro definition? 922 if (isIdent(tmpnam)) { 923 924 // strip the previous macro 925 if (name.length) macros[name] = macros[name].stripDD(); 926 927 // start parsing the new macro 928 name = tmpnam; 929 macros[name] = stripLeftDD(ln[pidx+1 .. $]); 930 continue; 931 } 932 } 933 934 // append to previous macro definition, if any 935 macros[name] ~= "\n" ~ ln; 936 } 937 } 938 939 private int baseIndent(string[] lines) 940 { 941 if( lines.length == 0 ) return 0; 942 int ret = int.max; 943 foreach( ln; lines ){ 944 int i = 0; 945 while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') ) 946 i++; 947 if( i < ln.length ) ret = min(ret, i); 948 } 949 return ret; 950 } 951 952 private string unindent(string ln, int amount) 953 { 954 while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') ) 955 ln = ln[1 .. $], amount--; 956 return ln; 957 } 958 959 private string stripLeftDD(string s) 960 { 961 while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n')) 962 s.popFront(); 963 return s; 964 } 965 966 private string specialStrip(string s) 967 { 968 import std.algorithm : among; 969 970 // strip trailing whitespace for all lines but the last 971 size_t idx = 0; 972 while (true) { 973 auto nidx = s[idx .. $].indexOf('\n'); 974 if (nidx < 0) break; 975 nidx += idx; 976 auto strippedfront = s[0 .. nidx].stripRightDD(); 977 s = strippedfront ~ "\n" ~ s[nidx+1 .. $]; 978 idx = strippedfront.length + 1; 979 } 980 981 // strip the first character, if whitespace 982 if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront(); 983 984 return s; 985 } 986 987 private string stripRightDD(string s) 988 { 989 while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n')) 990 s.popBack(); 991 return s; 992 } 993 994 private string stripDD(string s) 995 { 996 return s.stripLeftDD.stripRightDD; 997 } 998 999 import std.stdio; 1000 unittest { 1001 auto src = "$(M a b)\n$(M a\nb)\nMacros:\n M = -$0-\n"; 1002 auto dst = "-a b-\n-a\nb-\n"; 1003 assert(formatDdocComment(src) == dst); 1004 } 1005 1006 unittest { 1007 auto src = "\n $(M a b)\n$(M a \nb)\nMacros:\n M = -$0- \n\nN=$0"; 1008 auto dst = "-a b-\n-a\nb-\n"; 1009 assert(formatDdocComment(src) == dst); 1010 } 1011 1012 unittest { 1013 auto src = "$(M a, b)\n$(M a,\n b)\nMacros:\n M = -$1-\n\n +$2+\n\n N=$0"; 1014 auto dst = "-a-\n\n +b+\n-a-\n\n + b+\n"; 1015 assert(formatDdocComment(src) == dst); 1016 } 1017 1018 unittest { 1019 auto src = "$(GLOSSARY a\nb)\nMacros:\n GLOSSARY = $(LINK2 glossary.html#$0, $0)"; 1020 auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n"; 1021 assert(formatDdocComment(src) == dst); 1022 } 1023 1024 unittest { 1025 auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a."; 1026 auto dst = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.\n"; 1027 assert(formatDdocComment(src) == dst); 1028 } 1029 1030 unittest { 1031 auto src = "& &a < &#lt; &- &03; &;"; 1032 auto dst = "& &a < &#lt; &- &03; &;\n"; 1033 assert(formatDdocComment(src) == dst); 1034 } 1035 1036 unittest { 1037 auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = <\nGT = >"; 1038 auto dst = "<a href=\"abc\">test <peter@parker.com></a>\n"; 1039 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n")); 1040 assert(formatDdocComment(src) == dst); 1041 } 1042 1043 unittest { 1044 auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)"; 1045 auto dst = "[a][b][c][d]\n"; 1046 assert(formatDdocComment(src) == dst); 1047 } 1048 1049 unittest { 1050 auto src = "Testing `inline <code>`."; 1051 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"; 1052 assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string); 1053 } 1054 1055 unittest { 1056 auto src = "Testing `inline $(CODE)`."; 1057 auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n"; 1058 assert(formatDdocComment(src)); 1059 } 1060 1061 unittest { 1062 auto src = "---\nthis is a `string`.\n---"; 1063 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"; 1064 assert(formatDdocComment(src) == dst); 1065 } 1066 1067 unittest { // test for properly removed indentation in code blocks 1068 auto src = " ---\n testing\n ---"; 1069 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n"; 1070 assert(formatDdocComment(src) == dst); 1071 } 1072 1073 unittest { // issue #99 - parse macros in parameter sections 1074 import std.algorithm : find; 1075 auto src = "Params:\n\tfoo = $(B bar)"; 1076 auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n"; 1077 assert(formatDdocComment(src).find("<td> ") == dst); 1078 } 1079 1080 unittest { // issue #89 (minimal test) - empty first parameter 1081 auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>"; 1082 auto dst = "<div >foo</div>\n"; 1083 assert(formatDdocComment(src) == dst); 1084 } 1085 1086 unittest { // issue #89 (complex test) 1087 auto src = 1088 `$(LIST 1089 $(DIV oops, 1090 foo 1091 ), 1092 $(DIV , 1093 bar 1094 )) 1095 Macros: 1096 LIST=$(UL $(LIX $1, $+)) 1097 LIX=$(LI $1)$(LIX $+) 1098 UL=$(T ul, $0) 1099 LI = $(T li, $0) 1100 DIV=<div $1>$+</div> 1101 T=<$1>$+</$1> 1102 `; 1103 auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n"; 1104 assert(formatDdocComment(src) == dst); 1105 } 1106 1107 unittest { // issue #95 - trailing newlines must be stripped in macro definitions 1108 auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar"; 1109 auto dst = "foo\n"; 1110 assert(formatDdocComment(src) == dst); 1111 } 1112 1113 unittest { // missing macro closing clamp (because it's in a different section) 1114 auto src = "$(B\n\n)"; 1115 auto dst = "(B\n<section><p>)\n</p>\n</section>\n"; 1116 assert(formatDdocComment(src) == dst); 1117 } 1118 1119 unittest { // closing clamp should be found in a different *paragraph* of the same section, though 1120 auto src = "foo\n\n$(B\n\n)"; 1121 auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n"; 1122 assert(formatDdocComment(src) == dst); 1123 } 1124 1125 unittest { // more whitespace testing 1126 auto src = "$(M a , b , c )\nMacros:\nM = A$0B$1C$2D$+E"; 1127 auto dst = "A a , b , c B a C b D b , c E\n"; 1128 assert(formatDdocComment(src) == dst); 1129 } 1130 1131 unittest { // more whitespace testing 1132 auto src = " $(M \n a \n , \n b \n , \n c \n ) \nMacros:\nM = A$0B$1C$2D$+E"; 1133 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"; 1134 assert(formatDdocComment(src) == dst); 1135 } 1136 1137 unittest { // escape in backtick code 1138 auto src = "`<b>&`"; 1139 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"; 1140 assert(formatDdocComment(src) == dst); 1141 } 1142 1143 unittest { // escape in code blocks 1144 auto src = "---\n<b>&\n---"; 1145 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"; 1146 assert(formatDdocComment(src) == dst); 1147 } 1148 1149 unittest { // #81 empty first macro arguments 1150 auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>"; 1151 auto dst = "<table >test</table>\n"; 1152 assert(formatDdocComment(src) == dst); 1153 } 1154 1155 unittest { // #117 underscore identifiers as macro param 1156 auto src = "$(M __foo) __foo `__foo` $(D_CODE __foo)\nMacros:\nM=http://$1.com"; 1157 auto dst = "http://_foo.com _foo <code class=\"lang-d\"><span class=\"pln\">__foo</span></code> <pre class=\"d_code\">_foo</pre>\n"; 1158 assert(formatDdocComment(src) == dst); 1159 } 1160 1161 unittest { // #109 dot followed by unicode character causes infinite loop 1162 auto src = ".”"; 1163 auto dst = ".”\n"; 1164 assert(formatDdocComment(src) == dst); 1165 } 1166 1167 unittest { // #119 dot followed by space causes assertion 1168 static class Ctx : BareContext { 1169 override string lookupScopeSymbolLink(string name) { 1170 assert(name.length > 0 && name != "."); 1171 return null; 1172 } 1173 } 1174 auto src = "---\n. writeln();\n---"; 1175 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"; 1176 assert(formatDdocComment(src, new Ctx) == dst); 1177 } 1178 1179 unittest { // dot followed by non-identifier 1180 static class Ctx : BareContext { 1181 override string lookupScopeSymbolLink(string name) { 1182 assert(name.length > 0 && name != "."); 1183 return null; 1184 } 1185 } 1186 auto src = "---\n.()\n---"; 1187 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><wbr/><span class=\"pun\">.()</span></code></pre>\n</section>\n"; 1188 assert(formatDdocComment(src, new Ctx) == dst); 1189 } 1190 1191 1192 unittest { // X-REF 1193 static class Ctx : BareContext { 1194 override string lookupScopeSymbolLink(string name) { 1195 if (name == "foo") return "foo.html"; 1196 else return null; 1197 } 1198 } 1199 auto src = "`foo` `bar` $(D foo) $(D bar)\n\n---\nfoo bar\n---"; 1200 auto dst = "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1201 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code> " 1202 ~ "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1203 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code>\n" 1204 ~ "<section><pre class=\"code\"><code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a>" 1205 ~ "<span class=\"pln\"> bar</span></code></pre>\n</section>\n"; 1206 assert(formatDdocComment(src, new Ctx) == dst); 1207 } 1208 1209 unittest { // nested macro in $(D ...) 1210 auto src = "$(D $(NOP foo))\n\nMacros: NOP: $0"; 1211 auto dst = "<code class=\"lang-d\"><span class=\"pln\">foo</span></code>\n<section></section>\n"; 1212 assert(formatDdocComment(src) == dst); 1213 } 1214 1215 unittest { // nested $(D $(D case)) (do not escape HTML tags) 1216 auto src = "$(D $(D foo))"; 1217 auto dst = "<code class=\"lang-d\"><code class=\"lang-d\"><span class=\"pln\"><span class=\"pln\">foo</span></span></code></code>\n"; 1218 assert(formatDdocComment(src) == dst); 1219 } 1220 1221 unittest { // DDOX_NAMED_REF special macro 1222 static class Ctx : BareContext { 1223 override string lookupScopeSymbolLink(string symbol) { 1224 if (symbol == "bar.baz") 1225 return "bar/baz.html"; 1226 else 1227 return null; 1228 } 1229 } 1230 1231 auto src = "$(DDOX_NAMED_REF bar.baz, $(D foo))"; 1232 auto dst = "<code class=\"lang-d\"><span class=\"pln\">foo</span></code>\n"; 1233 auto dst_ctx = "<a href=\"bar/baz.html\" title=\"bar.baz\"><code class=\"lang-d\"><span class=\"pln\">foo</span></code></a>\n"; 1234 assert(formatDdocComment(src) == dst); 1235 assert(formatDdocComment(src, new Ctx) == dst_ctx); 1236 } 1237 1238 unittest { // DDOX_NAMED_REF special macro - handle invalid identifiers gracefully 1239 static class Ctx : BareContext { 1240 override string lookupScopeSymbolLink(string symbol) { 1241 assert(symbol.length > 0); 1242 assert(!symbol.endsWith(".")); 1243 return null; 1244 } 1245 } 1246 1247 auto src1 = "$(DDOX_NAMED_REF bar., $(D foo))"; 1248 auto src2 = "$(DDOX_NAMED_REF , $(D foo))"; 1249 auto dst = "<code class=\"lang-d\"><span class=\"pln\">foo</span></code>\n"; 1250 assert(formatDdocComment(src1, new Ctx) == dst); 1251 assert(formatDdocComment(src2, new Ctx) == dst); 1252 }