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