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 if (line.startsWith("$(D ")) { 551 line = line[4 .. $]; 552 int l = 1; 553 size_t cidx; 554 for (cidx = 0; cidx < line.length && l > 0; cidx++) { 555 if (line[cidx] == '(') l++; 556 else if (line[cidx] == ')') l--; 557 } 558 dst.put("<code class=\"lang-d\">"); 559 dst.renderCodeLine(line[0 .. cidx-1], context); 560 dst.put("</code>"); 561 line = line[cidx .. $]; 562 } else { 563 dst.put(line[0]); 564 line = line[1 .. $]; 565 } 566 break; 567 case '`': 568 line.popFront(); 569 auto idx = line.indexOf('`'); 570 if (idx < 0) break; 571 dst.put("<code class=\"lang-d\">"); 572 dst.renderCodeLine(line[0 .. idx], context); 573 dst.put("</code>"); 574 line = line[idx+1 .. $]; 575 break; 576 case '.': 577 if (line.length > 1 && (line[1 .. $].front.isAlpha || line[1] == '_')) goto case; 578 else goto default; 579 case 'a': .. case 'z': 580 case 'A': .. case 'Z': 581 582 auto url = skipUrl(line); 583 if( url.length ){ 584 /*dst.put("<a href=\""); 585 dst.put(url); 586 dst.put("\">");*/ 587 dst.put(url); 588 //dst.put("</a>"); 589 break; 590 } 591 592 auto ident = skipIdent(line); 593 auto link = context.lookupScopeSymbolLink(ident); 594 if (link.length) { 595 import ddox.highlight : highlightDCode; 596 if( link != "#" ){ 597 dst.put("<a href=\""); 598 dst.put(link); 599 dst.put("\">"); 600 } 601 if (!inCode) dst.put("<code class=\"lang-d\">"); 602 dst.highlightDCode(ident, null); 603 if (!inCode) dst.put("</code>"); 604 if( link != "#" ) dst.put("</a>"); 605 } else { 606 ident = ident.replace("._", "."); 607 if (s_enableHyphenation && !inCode) 608 hyphenate(ident, dst); 609 else 610 dst.put(ident); 611 } 612 break; 613 } 614 } 615 } 616 617 /// private 618 private void renderTextLine(R)(ref R dst, string line, DdocContext context) 619 { 620 while( line.length > 0 ){ 621 switch( line[0] ){ 622 default: 623 dst.put(line[0]); 624 line = line[1 .. $]; 625 break; 626 case '<': 627 dst.put(skipHtmlTag(line)); 628 break; 629 case '>': 630 dst.put(">"); 631 line.popFront(); 632 break; 633 case '&': 634 if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&'); 635 else dst.put("&"); 636 line.popFront(); 637 break; 638 } 639 } 640 } 641 642 /// private 643 private void renderCodeLine(R)(ref R dst, string line, DdocContext context) 644 { 645 import ddox.highlight : highlightDCode; 646 dst.highlightDCode(line, (string ident, scope void delegate(bool) insert_ident) { 647 auto link = context.lookupScopeSymbolLink(ident); 648 if (link.length && link != "#") { 649 dst.put("<a href=\""); 650 dst.put(link); 651 dst.put("\">"); 652 insert_ident(true); 653 dst.put("</a>"); 654 } else insert_ident(false); 655 }); 656 } 657 658 /// private 659 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 660 { 661 while( !line.empty ){ 662 auto idx = line.indexOf('$'); 663 if( idx < 0 ){ 664 dst.put(line); 665 return; 666 } 667 dst.put(line[0 .. idx]); 668 line = line[idx .. $]; 669 renderMacro(dst, line, context, macros, params, callstack); 670 } 671 } 672 673 /// private 674 private string renderMacros(string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 675 { 676 auto app = appender!string; 677 renderMacros(app, line, context, macros, params, callstack); 678 return app.data; 679 } 680 681 /// private 682 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params, MacroInvocation[] callstack) 683 { 684 assert(line[0] == '$'); 685 line = line[1 .. $]; 686 if( line.length < 1) { 687 dst.put("$"); 688 return; 689 } 690 691 if( line[0] >= '0' && line[0] <= '9' ){ 692 int pidx = line[0]-'0'; 693 if( pidx < params.length ) 694 dst.put(params[pidx]); 695 line = line[1 .. $]; 696 } else if( line[0] == '+' ){ 697 if( params.length ){ 698 auto idx = params[0].indexOf(','); 699 if( idx >= 0 ) dst.put(params[0][idx+1 .. $].specialStrip()); 700 } 701 line = line[1 .. $]; 702 } else if( line[0] == '(' ){ 703 line = line[1 .. $]; 704 int l = 1; 705 size_t cidx = 0; 706 for( cidx = 0; cidx < line.length && l > 0; cidx++ ){ 707 if( line[cidx] == '(' ) l++; 708 else if( line[cidx] == ')' ) l--; 709 } 710 if( l > 0 ){ 711 logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]); 712 dst.put("("); 713 return; 714 } 715 if( cidx < 1 ){ 716 logDebug("Empty macro parens."); 717 return; 718 } 719 720 auto mnameidx = line[0 .. cidx-1].countUntilAny(", \t\r\n"); 721 if( mnameidx < 0 ) mnameidx = cidx-1; 722 if( mnameidx == 0 ){ 723 logDebug("Macro call in DDOC comment is missing macro name."); 724 return; 725 } 726 727 auto mname = line[0 .. mnameidx]; 728 string rawargtext = line[mnameidx .. cidx-1]; 729 730 string[] args; 731 if (rawargtext.length) { 732 auto rawargs = splitParams(rawargtext); 733 foreach( arg; rawargs ){ 734 auto argtext = appender!string(); 735 renderMacros(argtext, arg, context, macros, params, callstack); 736 auto newargs = splitParams(argtext.data); 737 if (newargs.length == 0) args ~= ""; // always add at least one argument per raw argument 738 else args ~= newargs; 739 } 740 } 741 if (args.length == 1 && args[0].specialStrip.length == 0) args = null; // remove a single empty argument 742 743 args = join(args, ",").specialStrip() ~ args.map!(a => a.specialStrip).array; 744 745 logTrace("PARAMS for %s: %s", mname, args); 746 line = line[cidx .. $]; 747 748 // check for recursion termination conditions 749 foreach_reverse (ref c; callstack) { 750 if (c.name == mname && (args.length <= 1 || args == c.params)) { 751 logTrace("Terminating recursive macro call of %s: %s", mname, params.length <= 1 ? "no argument text" : "same arguments as previous invocation"); 752 //line = line[cidx .. $]; 753 return; 754 } 755 } 756 callstack.assumeSafeAppend(); 757 callstack ~= MacroInvocation(mname, args); 758 759 760 const(string)* pm = mname in s_overrideMacros; 761 if( !pm ) pm = mname in macros; 762 if( !pm ) pm = mname in s_defaultMacros; 763 if( !pm ) pm = mname in s_standardMacros; 764 765 if( pm ){ 766 logTrace("MACRO %s: %s", mname, *pm); 767 renderMacros(dst, *pm, context, macros, args, callstack); 768 } else { 769 logTrace("Macro '%s' not found.", mname); 770 if( args.length ) dst.put(args[0]); 771 } 772 } else dst.put("$"); 773 } 774 775 private struct MacroInvocation { 776 string name; 777 string[] params; 778 } 779 780 private string[] splitParams(string ln) 781 { 782 string[] ret; 783 size_t i = 0, start = 0; 784 while(i < ln.length){ 785 if( ln[i] == ',' ){ 786 ret ~= ln[start .. i]; 787 start = ++i; 788 } else if( ln[i] == '(' ){ 789 i++; 790 int l = 1; 791 for( ; i < ln.length && l > 0; i++ ){ 792 if( ln[i] == '(' ) l++; 793 else if( ln[i] == ')' ) l--; 794 } 795 } else i++; 796 } 797 if( i > start ) ret ~= ln[start .. i]; 798 return ret; 799 } 800 801 private string skipHtmlTag(ref string ln) 802 { 803 assert(ln[0] == '<'); 804 805 // skip HTML comment 806 if (ln.startsWith("<!--")) { 807 auto idx = ln[4 .. $].indexOf("-->"); 808 if (idx < 0) { 809 ln.popFront(); 810 return "<"; 811 } 812 auto ret = ln[0 .. idx+7]; 813 ln = ln[ret.length .. $]; 814 return ret; 815 } 816 817 // too short for a tag 818 if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) { 819 // found no match, return escaped '<' 820 logTrace("Found stray '<' in DDOC string."); 821 ln.popFront(); 822 return "<"; 823 } 824 825 // skip over regular start/end tag 826 auto idx = ln.indexOf(">"); 827 if (idx < 0) { 828 ln.popFront(); 829 return "<"; 830 } 831 auto ret = ln[0 .. idx+1]; 832 ln = ln[ret.length .. $]; 833 return ret; 834 } 835 836 private string skipUrl(ref string ln) 837 { 838 if( !ln.startsWith("http://") && !ln.startsWith("http://") ) 839 return null; 840 841 bool saw_dot = false; 842 size_t i = 7; 843 844 for_loop: 845 while( i < ln.length ){ 846 switch( ln[i] ){ 847 default: 848 break for_loop; 849 case 'a': .. case 'z': 850 case 'A': .. case 'Z': 851 case '0': .. case '9': 852 case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~': 853 break; 854 case '.': 855 saw_dot = true; 856 break; 857 } 858 i++; 859 } 860 861 if( saw_dot ){ 862 auto ret = ln[0 .. i]; 863 ln = ln[i .. $]; 864 return ret; 865 } else return null; 866 } 867 868 private string skipIdent(ref string str) 869 { 870 string strcopy = str; 871 872 if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_')) 873 str.popFront(); 874 875 bool last_was_ident = false; 876 while( !str.empty ){ 877 auto ch = str.front; 878 879 if( last_was_ident ){ 880 // dots are allowed if surrounded by identifiers 881 if( ch == '.' ) last_was_ident = false; 882 else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break; 883 } else { 884 if( ch != '_' && !std.uni.isAlpha(ch) ) break; 885 last_was_ident = true; 886 } 887 str.popFront(); 888 } 889 890 // if the identifier ended in a '.', remove it again 891 if( str.length != strcopy.length && !last_was_ident ) 892 str = strcopy[strcopy.length-str.length-1 .. $]; 893 894 return strcopy[0 .. strcopy.length-str.length]; 895 } 896 897 private bool isIdent(string str) 898 { 899 skipIdent(str); 900 return str.length == 0; 901 } 902 903 private void parseMacros(ref string[string] macros, in string[] lines) 904 { 905 string name; 906 foreach (string ln; lines) { 907 // macro definitions are of the form IDENT = ... 908 auto pidx = ln.indexOf('='); 909 if (pidx > 0) { 910 auto tmpnam = ln[0 .. pidx].strip(); 911 // got new macro definition? 912 if (isIdent(tmpnam)) { 913 914 // strip the previous macro 915 if (name.length) macros[name] = macros[name].stripDD(); 916 917 // start parsing the new macro 918 name = tmpnam; 919 macros[name] = stripLeftDD(ln[pidx+1 .. $]); 920 continue; 921 } 922 } 923 924 // append to previous macro definition, if any 925 macros[name] ~= "\n" ~ ln; 926 } 927 } 928 929 private int baseIndent(string[] lines) 930 { 931 if( lines.length == 0 ) return 0; 932 int ret = int.max; 933 foreach( ln; lines ){ 934 int i = 0; 935 while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') ) 936 i++; 937 if( i < ln.length ) ret = min(ret, i); 938 } 939 return ret; 940 } 941 942 private string unindent(string ln, int amount) 943 { 944 while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') ) 945 ln = ln[1 .. $], amount--; 946 return ln; 947 } 948 949 private string stripLeftDD(string s) 950 { 951 while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n')) 952 s.popFront(); 953 return s; 954 } 955 956 private string specialStrip(string s) 957 { 958 import std.algorithm : among; 959 960 // strip trailing whitespace for all lines but the last 961 size_t idx = 0; 962 while (true) { 963 auto nidx = s[idx .. $].indexOf('\n'); 964 if (nidx < 0) break; 965 nidx += idx; 966 auto strippedfront = s[0 .. nidx].stripRightDD(); 967 s = strippedfront ~ "\n" ~ s[nidx+1 .. $]; 968 idx = strippedfront.length + 1; 969 } 970 971 // strip the first character, if whitespace 972 if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront(); 973 974 return s; 975 } 976 977 private string stripRightDD(string s) 978 { 979 while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n')) 980 s.popBack(); 981 return s; 982 } 983 984 private string stripDD(string s) 985 { 986 return s.stripLeftDD.stripRightDD; 987 } 988 989 import std.stdio; 990 unittest { 991 auto src = "$(M a b)\n$(M a\nb)\nMacros:\n M = -$0-\n"; 992 auto dst = "-a b-\n-a\nb-\n"; 993 assert(formatDdocComment(src) == dst); 994 } 995 996 unittest { 997 auto src = "\n $(M a b)\n$(M a \nb)\nMacros:\n M = -$0- \n\nN=$0"; 998 auto dst = "-a b-\n-a\nb-\n"; 999 assert(formatDdocComment(src) == dst); 1000 } 1001 1002 unittest { 1003 auto src = "$(M a, b)\n$(M a,\n b)\nMacros:\n M = -$1-\n\n +$2+\n\n N=$0"; 1004 auto dst = "-a-\n\n +b+\n-a-\n\n + b+\n"; 1005 assert(formatDdocComment(src) == dst); 1006 } 1007 1008 unittest { 1009 auto src = "$(GLOSSARY a\nb)\nMacros:\n GLOSSARY = $(LINK2 glossary.html#$0, $0)"; 1010 auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n"; 1011 assert(formatDdocComment(src) == dst); 1012 } 1013 1014 unittest { 1015 auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a."; 1016 auto dst = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.\n"; 1017 assert(formatDdocComment(src) == dst); 1018 } 1019 1020 unittest { 1021 auto src = "& &a < &#lt; &- &03; &;"; 1022 auto dst = "& &a < &#lt; &- &03; &;\n"; 1023 assert(formatDdocComment(src) == dst); 1024 } 1025 1026 unittest { 1027 auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = <\nGT = >"; 1028 auto dst = "<a href=\"abc\">test <peter@parker.com></a>\n"; 1029 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n")); 1030 assert(formatDdocComment(src) == dst); 1031 } 1032 1033 unittest { 1034 auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)"; 1035 auto dst = "[a][b][c][d]\n"; 1036 assert(formatDdocComment(src) == dst); 1037 } 1038 1039 unittest { 1040 auto src = "Testing `inline <code>`."; 1041 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"; 1042 assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string); 1043 } 1044 1045 unittest { 1046 auto src = "Testing `inline $(CODE)`."; 1047 auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n"; 1048 assert(formatDdocComment(src)); 1049 } 1050 1051 unittest { 1052 auto src = "---\nthis is a `string`.\n---"; 1053 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"; 1054 assert(formatDdocComment(src) == dst); 1055 } 1056 1057 unittest { // test for properly removed indentation in code blocks 1058 auto src = " ---\n testing\n ---"; 1059 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n"; 1060 assert(formatDdocComment(src) == dst); 1061 } 1062 1063 unittest { // issue #99 - parse macros in parameter sections 1064 import std.algorithm : find; 1065 auto src = "Params:\n\tfoo = $(B bar)"; 1066 auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n"; 1067 assert(formatDdocComment(src).find("<td> ") == dst); 1068 } 1069 1070 unittest { // issue #89 (minimal test) - empty first parameter 1071 auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>"; 1072 auto dst = "<div >foo</div>\n"; 1073 assert(formatDdocComment(src) == dst); 1074 } 1075 1076 unittest { // issue #89 (complex test) 1077 auto src = 1078 `$(LIST 1079 $(DIV oops, 1080 foo 1081 ), 1082 $(DIV , 1083 bar 1084 )) 1085 Macros: 1086 LIST=$(UL $(LIX $1, $+)) 1087 LIX=$(LI $1)$(LIX $+) 1088 UL=$(T ul, $0) 1089 LI = $(T li, $0) 1090 DIV=<div $1>$+</div> 1091 T=<$1>$+</$1> 1092 `; 1093 auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n"; 1094 assert(formatDdocComment(src) == dst); 1095 } 1096 1097 unittest { // issue #95 - trailing newlines must be stripped in macro definitions 1098 auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar"; 1099 auto dst = "foo\n"; 1100 assert(formatDdocComment(src) == dst); 1101 } 1102 1103 unittest { // missing macro closing clamp (because it's in a different section) 1104 auto src = "$(B\n\n)"; 1105 auto dst = "(B\n<section><p>)\n</p>\n</section>\n"; 1106 assert(formatDdocComment(src) == dst); 1107 } 1108 1109 unittest { // closing clamp should be found in a different *paragraph* of the same section, though 1110 auto src = "foo\n\n$(B\n\n)"; 1111 auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n"; 1112 assert(formatDdocComment(src) == dst); 1113 } 1114 1115 unittest { // more whitespace testing 1116 auto src = "$(M a , b , c )\nMacros:\nM = A$0B$1C$2D$+E"; 1117 auto dst = "A a , b , c B a C b D b , c E\n"; 1118 assert(formatDdocComment(src) == dst); 1119 } 1120 1121 unittest { // more whitespace testing 1122 auto src = " $(M \n a \n , \n b \n , \n c \n ) \nMacros:\nM = A$0B$1C$2D$+E"; 1123 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"; 1124 assert(formatDdocComment(src) == dst); 1125 } 1126 1127 unittest { // escape in backtick code 1128 auto src = "`<b>&`"; 1129 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"; 1130 assert(formatDdocComment(src) == dst); 1131 } 1132 1133 unittest { // escape in code blocks 1134 auto src = "---\n<b>&\n---"; 1135 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"; 1136 assert(formatDdocComment(src) == dst); 1137 } 1138 1139 unittest { // #81 empty first macro arguments 1140 auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>"; 1141 auto dst = "<table >test</table>\n"; 1142 assert(formatDdocComment(src) == dst); 1143 } 1144 1145 unittest { // #117 underscore identifiers as macro param 1146 auto src = "$(M __foo) __foo `__foo` $(D_CODE __foo)\nMacros:\nM=http://$1.com"; 1147 auto dst = "http://_foo.com _foo <code class=\"lang-d\"><span class=\"pln\">__foo</span></code> <pre class=\"d_code\">_foo</pre>\n"; 1148 assert(formatDdocComment(src) == dst); 1149 } 1150 1151 unittest { // #109 dot followed by unicode character causes infinite loop 1152 auto src = ".”"; 1153 auto dst = ".”\n"; 1154 assert(formatDdocComment(src) == dst); 1155 } 1156 1157 unittest { // #119 dot followed by space causes assertion 1158 static class Ctx : BareContext { 1159 override string lookupScopeSymbolLink(string name) { 1160 writefln("IDENT: %s", name); 1161 assert(name.length > 0 && name != "."); 1162 return null; 1163 } 1164 } 1165 auto src = "---\n. writeln();\n---"; 1166 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"; 1167 assert(formatDdocComment(src, new Ctx) == dst); 1168 } 1169 1170 unittest { // dot followed by non-identifier 1171 static class Ctx : BareContext { 1172 override string lookupScopeSymbolLink(string name) { 1173 writefln("IDENT: %s", name); 1174 assert(name.length > 0 && name != "."); 1175 return null; 1176 } 1177 } 1178 auto src = "---\n.()\n---"; 1179 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><wbr/><span class=\"pun\">.()</span></code></pre>\n</section>\n"; 1180 assert(formatDdocComment(src, new Ctx) == dst); 1181 } 1182 1183 1184 unittest { // X-REF 1185 static class Ctx : BareContext { 1186 override string lookupScopeSymbolLink(string name) { 1187 if (name == "foo") return "foo.html"; 1188 else return null; 1189 } 1190 } 1191 auto src = "`foo` `bar` $(D foo) $(D bar)\n\n---\nfoo bar\n---"; 1192 auto dst = "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1193 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code> " 1194 ~ "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> " 1195 ~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code>\n" 1196 ~ "<section><pre class=\"code\"><code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a>" 1197 ~ "<span class=\"pln\"> bar</span></code></pre>\n</section>\n"; 1198 assert(formatDdocComment(src, new Ctx) == dst); 1199 }