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