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 212 // parseMacros(m_macros, context.defaultMacroDefinitions); 213 214 auto lines = splitLines(text); 215 if( !lines.length ) return; 216 217 int getLineType(int i) 218 { 219 auto ln = strip(lines[i]); 220 if( ln.length == 0 ) return BLANK; 221 else if( ln.length >= 3 && ln.allOf("-") ) return CODE; 222 else if( ln.indexOf(':') > 0 && isIdent(ln[0 .. ln.indexOf(':')]) ) return SECTION; 223 return TEXT; 224 } 225 226 int skipCodeBlock(int start) 227 { 228 do { 229 start++; 230 } while(start < lines.length && getLineType(start) != CODE); 231 if (start >= lines.length) return start; // unterminated code section 232 return start+1; 233 } 234 235 int skipSection(int start) 236 { 237 while (start < lines.length) { 238 if (getLineType(start) == SECTION) break; 239 if (getLineType(start) == CODE) 240 start = skipCodeBlock(start); 241 else start++; 242 } 243 return start; 244 } 245 246 int skipBlock(int start) 247 { 248 do { 249 start++; 250 } while(start < lines.length && getLineType(start) == TEXT); 251 return start; 252 } 253 254 255 int i = 0; 256 257 // special case short description on the first line 258 while( i < lines.length && getLineType(i) == BLANK ) i++; 259 if( i < lines.length && getLineType(i) == TEXT ){ 260 auto j = skipBlock(i); 261 m_sections ~= Section("$Short", lines[i .. j]); 262 i = j; 263 } 264 265 // first section is implicitly the long description 266 { 267 auto j = skipSection(i); 268 if( j > i ){ 269 m_sections ~= Section("$Long", lines[i .. j]); 270 i = j; 271 } 272 } 273 274 // parse all other sections 275 while( i < lines.length ){ 276 assert(getLineType(i) == SECTION); 277 auto j = skipSection(i+1); 278 assert(j <= lines.length); 279 auto pidx = lines[i].indexOf(':'); 280 auto sect = strip(lines[i][0 .. pidx]); 281 lines[i] = stripLeftDD(lines[i][pidx+1 .. $]); 282 if (lines[i].empty && i < lines.length) i++; 283 if (sect == "Macros") parseMacros(m_macros, lines[i .. j]); 284 else { 285 m_sections ~= Section(sect, lines[i .. j]); 286 } 287 i = j; 288 } 289 290 // parseMacros(m_macros, context.overrideMacroDefinitions); 291 } 292 293 @property bool isDitto() const { return m_isDitto; } 294 @property bool isPrivate() const { return m_isPrivate; } 295 296 bool hasSection(string name) const { return m_sections.canFind!(s => s.name == name); } 297 298 void renderSectionR(R)(ref R dst, DdocContext context, string name, int hlevel = 2) 299 { 300 foreach (s; m_sections) 301 if (s.name == name) 302 parseSection(dst, name, s.lines, context, hlevel, m_macros); 303 } 304 305 void renderSectionsR(R)(ref R dst, DdocContext context, bool delegate(string) display_section, int hlevel) 306 { 307 foreach (s; m_sections) { 308 if (display_section && !display_section(s.name)) continue; 309 parseSection(dst, s.name, s.lines, context, hlevel, m_macros); 310 } 311 } 312 313 string renderSection(DdocContext context, string name, int hlevel = 2) 314 { 315 auto dst = appender!string(); 316 renderSectionR(dst, context, name, hlevel); 317 return dst.data; 318 } 319 320 string renderSections(DdocContext context, bool delegate(string) display_section, int hlevel) 321 { 322 auto dst = appender!string(); 323 renderSectionsR(dst, context, display_section, hlevel); 324 return dst.data; 325 } 326 } 327 328 329 /** 330 Provides context information about the documented element. 331 */ 332 interface DdocContext { 333 /// A line array with macro definitions 334 @property string[] defaultMacroDefinitions(); 335 336 /// Line array with macro definitions that take precedence over local macros 337 @property string[] overrideMacroDefinitions(); 338 339 /// Looks up a symbol in the scope of the documented element and returns a link to it. 340 string lookupScopeSymbolLink(string name); 341 } 342 343 344 private class BareContext : DdocContext { 345 @property string[] defaultMacroDefinitions() { return null; } 346 @property string[] overrideMacroDefinitions() { return null; } 347 string lookupScopeSymbolLink(string name) { return null; } 348 } 349 350 private enum { 351 BLANK, 352 TEXT, 353 CODE, 354 SECTION 355 } 356 357 private struct Section { 358 string name; 359 string[] lines; 360 361 this(string name, string[] lines...) 362 { 363 this.name = name; 364 this.lines = lines; 365 } 366 } 367 368 private { 369 immutable string[string] s_standardMacros; 370 string[string] s_defaultMacros; 371 string[string] s_overrideMacros; 372 bool s_enableHyphenation; 373 Hyphenator s_hyphenator; 374 } 375 376 /// private 377 private void parseSection(R)(ref R dst, string sect, string[] lines, DdocContext context, int hlevel, string[string] macros) 378 { 379 if( sect == "$Short" ) hlevel = -1; 380 381 void putHeader(string hdr){ 382 if( hlevel <= 0 ) return; 383 dst.put("<section>"); 384 if( sect.length > 0 && sect[0] != '$' ){ 385 dst.put("<h"~to!string(hlevel)~">"); 386 foreach( ch; hdr ) dst.put(ch == '_' ? ' ' : ch); 387 dst.put("</h"~to!string(hlevel)~">\n"); 388 } 389 } 390 391 void putFooter(){ 392 if( hlevel <= 0 ) return; 393 dst.put("</section>\n"); 394 } 395 396 int getLineType(int i) 397 { 398 auto ln = strip(lines[i]); 399 if( ln.length == 0 ) return BLANK; 400 else if( ln.length >= 3 &&ln.allOf("-") ) return CODE; 401 else if( ln.indexOf(':') > 0 && !ln[0 .. ln.indexOf(':')].anyOf(" \t") ) return SECTION; 402 return TEXT; 403 } 404 405 int skipBlock(int start) 406 { 407 do { 408 start++; 409 } while(start < lines.length && getLineType(start) == TEXT); 410 return start; 411 } 412 413 int skipCodeBlock(int start) 414 { 415 do { 416 start++; 417 } while(start < lines.length && getLineType(start) != CODE); 418 return start; 419 } 420 421 // handle backtick inline-code 422 for (int i = 0; i < lines.length; i++) { 423 int lntype = getLineType(i); 424 if (lntype == CODE) i = skipCodeBlock(i); 425 else lines[i] = replaceBacktickCode(lines[i]); 426 } 427 lines = renderMacros(lines.join("\n").stripDD, context, macros).splitLines(); 428 429 switch( sect ){ 430 default: 431 putHeader(sect); 432 int i = 0; 433 while( i < lines.length ){ 434 int lntype = getLineType(i); 435 436 switch( lntype ){ 437 default: assert(false, "Unexpected line type "~to!string(lntype)~": "~lines[i]); 438 case BLANK: 439 dst.put('\n'); 440 i++; 441 continue; 442 case SECTION: 443 case TEXT: 444 if( hlevel >= 0 ) dst.put("<p>"); 445 auto j = skipBlock(i); 446 bool first = true; 447 renderTextLine(dst, lines[i .. j].join("\n")/*.stripDD*/, context); 448 dst.put('\n'); 449 if( hlevel >= 0 ) dst.put("</p>\n"); 450 i = j; 451 break; 452 case CODE: 453 dst.put("<pre class=\"code\"><code class=\"lang-d\">"); 454 auto j = skipCodeBlock(i); 455 auto base_indent = baseIndent(lines[i+1 .. j]); 456 renderCodeLine(dst, lines[i+1 .. j].map!(ln => ln.unindent(base_indent)).join("\n"), context); 457 dst.put("</code></pre>\n"); 458 i = j+1; 459 break; 460 } 461 } 462 putFooter(); 463 break; 464 case "Params": 465 putHeader("Parameters"); 466 dst.put("<table><col class=\"caption\"><tr><th>Name</th><th>Description</th></tr>\n"); 467 bool in_parameter = false; 468 string desc; 469 foreach( string ln; lines ){ 470 // check if the line starts a parameter documentation 471 string name; 472 auto eidx = ln.indexOf("="); 473 if( eidx > 0 ) name = ln[0 .. eidx].strip(); 474 if( !isIdent(name) ) name = null; 475 476 // if it does, start a new row 477 if( name.length ){ 478 if( in_parameter ){ 479 renderTextLine(dst, desc, context); 480 dst.put("</td></tr>\n"); 481 } 482 483 dst.put("<tr><td id=\""); 484 dst.put(name); 485 dst.put("\">"); 486 dst.put(name); 487 dst.put("</td><td>"); 488 489 desc = ln[eidx+1 .. $]; 490 in_parameter = true; 491 } else if( in_parameter ) desc ~= "\n" ~ ln; 492 } 493 494 if( in_parameter ){ 495 renderTextLine(dst, desc, context); 496 dst.put("</td></tr>\n"); 497 } 498 499 dst.put("</table>\n"); 500 putFooter(); 501 break; 502 } 503 504 } 505 506 /// private 507 private void renderTextLine(R)(ref R dst, string line, DdocContext context) 508 { 509 size_t inCode; 510 while( line.length > 0 ){ 511 switch( line[0] ){ 512 default: 513 dst.put(line[0]); 514 line = line[1 .. $]; 515 break; 516 case '<': 517 auto res = skipHtmlTag(line); 518 if (res.startsWith("<code")) 519 ++inCode; 520 else if (res == "</code>") 521 --inCode; 522 dst.put(res); 523 break; 524 case '>': 525 dst.put(">"); 526 line.popFront(); 527 break; 528 case '&': 529 if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&'); 530 else dst.put("&"); 531 line.popFront(); 532 break; 533 case '_': 534 line = line[1 .. $]; 535 auto ident = skipIdent(line); 536 if( ident.length ) 537 { 538 if (s_enableHyphenation && !inCode) 539 hyphenate(ident, dst); 540 else 541 dst.put(ident); 542 } 543 else dst.put('_'); 544 break; 545 case '.': 546 if (line.length > 1 && (line[1].isAlpha || line[1] == '_')) goto case; 547 else goto default; 548 case 'a': .. case 'z': 549 case 'A': .. case 'Z': 550 551 auto url = skipUrl(line); 552 if( url.length ){ 553 /*dst.put("<a href=\""); 554 dst.put(url); 555 dst.put("\">");*/ 556 dst.put(url); 557 //dst.put("</a>"); 558 break; 559 } 560 561 auto ident = skipIdent(line); 562 auto link = context.lookupScopeSymbolLink(ident); 563 if (link.length) { 564 import ddox.highlight : highlightDCode; 565 if( link != "#" ){ 566 dst.put("<a href=\""); 567 dst.put(link); 568 dst.put("\">"); 569 } 570 if (!inCode) dst.put("<code class=\"lang-d\">"); 571 dst.highlightDCode(ident, null); 572 if (!inCode) dst.put("</code>"); 573 if( link != "#" ) dst.put("</a>"); 574 } else { 575 ident = ident.replace("._", "."); 576 if (s_enableHyphenation && !inCode) 577 hyphenate(ident, dst); 578 else 579 dst.put(ident); 580 } 581 break; 582 } 583 } 584 } 585 586 /// private 587 private void renderCodeLine(R)(ref R dst, string line, DdocContext context) 588 { 589 import ddox.highlight : highlightDCode; 590 dst.highlightDCode(line, (string ident, scope void delegate() insert_ident) { 591 auto link = context.lookupScopeSymbolLink(ident); 592 if (link.length && link != "#") { 593 dst.put("<a href=\""); 594 dst.put(link); 595 dst.put("\">"); 596 insert_ident(); 597 dst.put("</a>"); 598 } else insert_ident(); 599 }); 600 } 601 602 /// private 603 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 604 { 605 while( !line.empty ){ 606 auto idx = line.indexOf('$'); 607 if( idx < 0 ){ 608 dst.put(line); 609 return; 610 } 611 dst.put(line[0 .. idx]); 612 line = line[idx .. $]; 613 renderMacro(dst, line, context, macros, params, callstack); 614 } 615 } 616 617 /// private 618 private string renderMacros(string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null) 619 { 620 auto app = appender!string; 621 renderMacros(app, line, context, macros, params, callstack); 622 return app.data; 623 } 624 625 /// private 626 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params, MacroInvocation[] callstack) 627 { 628 assert(line[0] == '$'); 629 line = line[1 .. $]; 630 if( line.length < 1) { 631 dst.put("$"); 632 return; 633 } 634 635 if( line[0] >= '0' && line[0] <= '9' ){ 636 int pidx = line[0]-'0'; 637 if( pidx < params.length ) 638 dst.put(params[pidx]); 639 line = line[1 .. $]; 640 } else if( line[0] == '+' ){ 641 if( params.length ){ 642 auto idx = params[0].indexOf(','); 643 if( idx >= 0 ) dst.put(params[0][idx+1 .. $].specialStrip()); 644 } 645 line = line[1 .. $]; 646 } else if( line[0] == '(' ){ 647 line = line[1 .. $]; 648 int l = 1; 649 size_t cidx = 0; 650 for( cidx = 0; cidx < line.length && l > 0; cidx++ ){ 651 if( line[cidx] == '(' ) l++; 652 else if( line[cidx] == ')' ) l--; 653 } 654 if( l > 0 ){ 655 logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]); 656 dst.put("("); 657 return; 658 } 659 if( cidx < 1 ){ 660 logDebug("Empty macro parens."); 661 return; 662 } 663 664 auto mnameidx = line[0 .. cidx-1].countUntilAny(", \t\r\n"); 665 if( mnameidx < 0 ) mnameidx = cidx-1; 666 if( mnameidx == 0 ){ 667 logDebug("Macro call in DDOC comment is missing macro name."); 668 return; 669 } 670 671 auto mname = line[0 .. mnameidx]; 672 string rawargtext = line[mnameidx .. cidx-1]; 673 674 string[] args; 675 if (rawargtext.length) { 676 auto rawargs = splitParams(rawargtext); 677 foreach( arg; rawargs ){ 678 auto argtext = appender!string(); 679 renderMacros(argtext, arg, context, macros, params, callstack); 680 auto newargs = splitParams(argtext.data); 681 if (newargs.length == 0) args ~= ""; // always add at least one argument per raw argument 682 else args ~= newargs; 683 } 684 } 685 if (args.length == 1 && args[0].specialStrip.length == 0) args = null; // remove a single empty argument 686 687 args = join(args, ",").specialStrip() ~ args.map!(a => a.specialStrip).array; 688 689 logTrace("PARAMS for %s: %s", mname, args); 690 line = line[cidx .. $]; 691 692 // check for recursion termination conditions 693 foreach_reverse (ref c; callstack) { 694 if (c.name == mname && (args.length <= 1 || args == c.params)) { 695 logTrace("Terminating recursive macro call of %s: %s", mname, params.length <= 1 ? "no argument text" : "same arguments as previous invocation"); 696 //line = line[cidx .. $]; 697 return; 698 } 699 } 700 callstack.assumeSafeAppend(); 701 callstack ~= MacroInvocation(mname, args); 702 703 704 const(string)* pm = mname in s_overrideMacros; 705 if( !pm ) pm = mname in macros; 706 if( !pm ) pm = mname in s_defaultMacros; 707 if( !pm ) pm = mname in s_standardMacros; 708 709 if( pm ){ 710 logTrace("MACRO %s: %s", mname, *pm); 711 renderMacros(dst, *pm, context, macros, args, callstack); 712 } else { 713 logTrace("Macro '%s' not found.", mname); 714 if( args.length ) dst.put(args[0]); 715 } 716 } else dst.put("$"); 717 } 718 719 private struct MacroInvocation { 720 string name; 721 string[] params; 722 } 723 724 private string[] splitParams(string ln) 725 { 726 string[] ret; 727 size_t i = 0, start = 0; 728 while(i < ln.length){ 729 if( ln[i] == ',' ){ 730 ret ~= ln[start .. i]; 731 start = ++i; 732 } else if( ln[i] == '(' ){ 733 i++; 734 int l = 1; 735 for( ; i < ln.length && l > 0; i++ ){ 736 if( ln[i] == '(' ) l++; 737 else if( ln[i] == ')' ) l--; 738 } 739 } else i++; 740 } 741 if( i > start ) ret ~= ln[start .. i]; 742 return ret; 743 } 744 745 private string replaceBacktickCode(string line) 746 { 747 auto ret = appender!string; 748 749 while (line.length > 0) { 750 auto idx = line.indexOf('`'); 751 if (idx < 0) break; 752 753 auto eidx = line[idx+1 .. $].indexOf('`'); 754 if (eidx < 0) break; 755 eidx += idx+1; 756 757 ret.put(line[0 .. idx]); 758 ret.put("$(DDOC_BACKQUOTED "); 759 foreach (i; idx+1 .. eidx) { 760 switch (line[i]) { 761 default: ret.put(line[i]); break; 762 case '&': ret.put("&"); break; 763 case '<': ret.put("<"); break; 764 case '>': ret.put(">"); break; 765 case '(': ret.put("$(LPAREN)"); break; 766 case ')': ret.put("$(RPAREN)"); break; 767 } 768 } 769 ret.put(")"); 770 line = line[eidx+1 .. $]; 771 } 772 773 if (ret.data.length == 0) return line; 774 ret.put(line); 775 return ret.data; 776 } 777 778 private string skipHtmlTag(ref string ln) 779 { 780 assert(ln[0] == '<'); 781 782 // skip HTML comment 783 if (ln.startsWith("<!--")) { 784 auto idx = ln[4 .. $].indexOf("-->"); 785 if (idx < 0) { 786 ln.popFront(); 787 return "<"; 788 } 789 auto ret = ln[0 .. idx+7]; 790 ln = ln[ret.length .. $]; 791 return ret; 792 } 793 794 // too short for a tag 795 if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) { 796 // found no match, return escaped '<' 797 logTrace("Found stray '<' in DDOC string."); 798 ln.popFront(); 799 return "<"; 800 } 801 802 // skip over regular start/end tag 803 auto idx = ln.indexOf(">"); 804 if (idx < 0) { 805 ln.popFront(); 806 return "<"; 807 } 808 auto ret = ln[0 .. idx+1]; 809 ln = ln[ret.length .. $]; 810 return ret; 811 } 812 813 private string skipUrl(ref string ln) 814 { 815 if( !ln.startsWith("http://") && !ln.startsWith("http://") ) 816 return null; 817 818 bool saw_dot = false; 819 size_t i = 7; 820 821 for_loop: 822 while( i < ln.length ){ 823 switch( ln[i] ){ 824 default: 825 break for_loop; 826 case 'a': .. case 'z': 827 case 'A': .. case 'Z': 828 case '0': .. case '9': 829 case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~': 830 break; 831 case '.': 832 saw_dot = true; 833 break; 834 } 835 i++; 836 } 837 838 if( saw_dot ){ 839 auto ret = ln[0 .. i]; 840 ln = ln[i .. $]; 841 return ret; 842 } else return null; 843 } 844 845 private string skipIdent(ref string str) 846 { 847 string strcopy = str; 848 849 if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_')) 850 str.popFront(); 851 852 bool last_was_ident = false; 853 while( !str.empty ){ 854 auto ch = str.front; 855 856 if( last_was_ident ){ 857 // dots are allowed if surrounded by identifiers 858 if( ch == '.' ) last_was_ident = false; 859 else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break; 860 } else { 861 if( ch != '_' && !std.uni.isAlpha(ch) ) break; 862 last_was_ident = true; 863 } 864 str.popFront(); 865 } 866 867 // if the identifier ended in a '.', remove it again 868 if( str.length != strcopy.length && !last_was_ident ) 869 str = strcopy[strcopy.length-str.length-1 .. $]; 870 871 return strcopy[0 .. strcopy.length-str.length]; 872 } 873 874 private bool isIdent(string str) 875 { 876 skipIdent(str); 877 return str.length == 0; 878 } 879 880 private void parseMacros(ref string[string] macros, in string[] lines) 881 { 882 string name; 883 foreach (string ln; lines) { 884 // macro definitions are of the form IDENT = ... 885 auto pidx = ln.indexOf('='); 886 if (pidx > 0) { 887 auto tmpnam = ln[0 .. pidx].strip(); 888 // got new macro definition? 889 if (isIdent(tmpnam)) { 890 891 // strip the previous macro 892 if (name.length) macros[name] = macros[name].stripDD(); 893 894 // start parsing the new macro 895 name = tmpnam; 896 macros[name] = stripLeftDD(ln[pidx+1 .. $]); 897 continue; 898 } 899 } 900 901 // append to previous macro definition, if any 902 macros[name] ~= "\n" ~ ln; 903 } 904 } 905 906 private int baseIndent(string[] lines) 907 { 908 if( lines.length == 0 ) return 0; 909 int ret = int.max; 910 foreach( ln; lines ){ 911 int i = 0; 912 while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') ) 913 i++; 914 if( i < ln.length ) ret = min(ret, i); 915 } 916 return ret; 917 } 918 919 private string unindent(string ln, int amount) 920 { 921 while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') ) 922 ln = ln[1 .. $], amount--; 923 return ln; 924 } 925 926 private string stripLeftDD(string s) 927 { 928 while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n')) 929 s.popFront(); 930 return s; 931 } 932 933 private string specialStrip(string s) 934 { 935 import std.algorithm : among; 936 937 // strip trailing whitespace for all lines but the last 938 size_t idx = 0; 939 while (true) { 940 auto nidx = s[idx .. $].indexOf('\n'); 941 if (nidx < 0) break; 942 nidx += idx; 943 auto strippedfront = s[0 .. nidx].stripRightDD(); 944 s = strippedfront ~ "\n" ~ s[nidx+1 .. $]; 945 idx = strippedfront.length + 1; 946 } 947 948 // strip the first character, if whitespace 949 if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront(); 950 951 return s; 952 } 953 954 private string stripRightDD(string s) 955 { 956 while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n')) 957 s.popBack(); 958 return s; 959 } 960 961 private string stripDD(string s) 962 { 963 return s.stripLeftDD.stripRightDD; 964 } 965 966 import std.stdio; 967 unittest { 968 auto src = "$(M a b)\n$(M a\nb)\nMacros:\n M = -$0-\n"; 969 auto dst = "-a b-\n-a\nb-\n"; 970 assert(formatDdocComment(src) == dst); 971 } 972 973 unittest { 974 auto src = "\n $(M a b)\n$(M a \nb)\nMacros:\n M = -$0- \n\nN=$0"; 975 auto dst = "-a b-\n-a\nb-\n"; 976 assert(formatDdocComment(src) == dst); 977 } 978 979 unittest { 980 auto src = "$(M a, b)\n$(M a,\n b)\nMacros:\n M = -$1-\n\n +$2+\n\n N=$0"; 981 auto dst = "-a-\n\n +b+\n-a-\n\n + b+\n"; 982 assert(formatDdocComment(src) == dst); 983 } 984 985 unittest { 986 auto src = "$(GLOSSARY a\nb)\nMacros:\n GLOSSARY = $(LINK2 glossary.html#$0, $0)"; 987 auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n"; 988 assert(formatDdocComment(src) == dst); 989 } 990 991 unittest { 992 auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a."; 993 auto dst = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.\n"; 994 assert(formatDdocComment(src) == dst); 995 } 996 997 unittest { 998 auto src = "& &a < &#lt; &- &03; &;"; 999 auto dst = "& &a < &#lt; &- &03; &;\n"; 1000 assert(formatDdocComment(src) == dst); 1001 } 1002 1003 unittest { 1004 auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = <\nGT = >"; 1005 auto dst = "<a href=\"abc\">test <peter@parker.com></a>\n"; 1006 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n")); 1007 assert(formatDdocComment(src) == dst); 1008 } 1009 1010 unittest { 1011 auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)"; 1012 auto dst = "[a][b][c][d]\n"; 1013 assert(formatDdocComment(src) == dst); 1014 } 1015 1016 unittest { 1017 auto src = "Testing `inline <code>`."; 1018 auto dst = "Testing <code class=\"lang-d\">inline <code></code>.\n"; 1019 assert(formatDdocComment(src) == dst); 1020 } 1021 1022 unittest { 1023 auto src = "Testing `inline $(CODE)`."; 1024 auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n"; 1025 assert(formatDdocComment(src)); 1026 } 1027 1028 unittest { 1029 auto src = "---\nthis is a `string`.\n---"; 1030 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"; 1031 assert(formatDdocComment(src) == dst); 1032 } 1033 1034 unittest { // test for properly removed indentation in code blocks 1035 auto src = " ---\n testing\n ---"; 1036 auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n"; 1037 assert(formatDdocComment(src) == dst); 1038 } 1039 1040 unittest { // issue #99 - parse macros in parameter sections 1041 import std.algorithm : find; 1042 auto src = "Params:\n\tfoo = $(B bar)"; 1043 auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n"; 1044 assert(formatDdocComment(src).find("<td> ") == dst); 1045 } 1046 1047 unittest { // issue #89 (minimal test) - empty first parameter 1048 auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>"; 1049 auto dst = "<div >foo</div>\n"; 1050 assert(formatDdocComment(src) == dst); 1051 } 1052 1053 unittest { // issue #89 (complex test) 1054 auto src = 1055 `$(LIST 1056 $(DIV oops, 1057 foo 1058 ), 1059 $(DIV , 1060 bar 1061 )) 1062 Macros: 1063 LIST=$(UL $(LIX $1, $+)) 1064 LIX=$(LI $1)$(LIX $+) 1065 UL=$(T ul, $0) 1066 LI = $(T li, $0) 1067 DIV=<div $1>$+</div> 1068 T=<$1>$+</$1> 1069 `; 1070 auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n"; 1071 assert(formatDdocComment(src) == dst); 1072 } 1073 1074 unittest { // issue #95 - trailing newlines must be stripped in macro definitions 1075 auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar"; 1076 auto dst = "foo\n"; 1077 assert(formatDdocComment(src) == dst); 1078 } 1079 1080 unittest { // missing macro closing clamp (because it's in a different section) 1081 auto src = "$(B\n\n)"; 1082 auto dst = "(B\n<section><p>)\n</p>\n</section>\n"; 1083 assert(formatDdocComment(src) == dst); 1084 } 1085 1086 unittest { // closing clamp should be found in a different *paragraph* of the same section, though 1087 auto src = "foo\n\n$(B\n\n)"; 1088 auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n"; 1089 assert(formatDdocComment(src) == dst); 1090 } 1091 1092 unittest { // more whitespace testing 1093 auto src = "$(M a , b , c )\nMacros:\nM = A$0B$1C$2D$+E"; 1094 auto dst = "A a , b , c B a C b D b , c E\n"; 1095 assert(formatDdocComment(src) == dst); 1096 } 1097 1098 unittest { // more whitespace testing 1099 auto src = " $(M \n a \n , \n b \n , \n c \n ) \nMacros:\nM = A$0B$1C$2D$+E"; 1100 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"; 1101 assert(formatDdocComment(src) == dst); 1102 } 1103 1104 unittest { // escape in backtick code 1105 auto src = "`<b>&`"; 1106 auto dst = "<code class=\"lang-d\"><b>&amp;</code>\n"; 1107 assert(formatDdocComment(src) == dst,formatDdocComment(src) ); 1108 } 1109 1110 unittest { // escape in code blocks 1111 auto src = "---\n<b>&\n---"; 1112 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"; 1113 assert(formatDdocComment(src) == dst); 1114 } 1115 1116 unittest { // #81 empty first macro arguments 1117 auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>"; 1118 auto dst = "<table >test</table>\n"; 1119 assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string); 1120 }