1 /** 2 Markdown parser implementation 3 4 Copyright: © 2012-2014 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 vibe.textfilter.markdown; 9 10 import vibe.core.log; 11 import vibe.textfilter.html; 12 import vibe.utils.string; 13 14 import std.algorithm : canFind, countUntil, min; 15 import std.array; 16 import std.format; 17 import std.range; 18 import std.string; 19 20 /* 21 TODO: 22 detect inline HTML tags 23 */ 24 25 version(MarkdownTest) 26 { 27 int main() 28 { 29 import std.file; 30 setLogLevel(LogLevel.Trace); 31 auto text = readText("test.txt"); 32 auto result = appender!string(); 33 filterMarkdown(result, text); 34 foreach( ln; splitLines(result.data) ) 35 logInfo(ln); 36 return 0; 37 } 38 } 39 40 /** Returns a Markdown filtered HTML string. 41 */ 42 string filterMarkdown()(string str, MarkdownFlags flags) 43 @trusted { // scope class is not @safe for DMD 2.072 44 scope settings = new MarkdownSettings; 45 settings.flags = flags; 46 return filterMarkdown(str, settings); 47 } 48 /// ditto 49 string filterMarkdown()(string str, scope MarkdownSettings settings = null) 50 @trusted { // Appender not @safe as of 2.065 51 auto dst = appender!string(); 52 filterMarkdown(dst, str, settings); 53 return dst.data; 54 } 55 56 57 /** Markdown filters the given string and writes the corresponding HTML to an output range. 58 */ 59 void filterMarkdown(R)(ref R dst, string src, MarkdownFlags flags) 60 { 61 scope settings = new MarkdownSettings; 62 settings.flags = flags; 63 filterMarkdown(dst, src, settings); 64 } 65 /// ditto 66 void filterMarkdown(R)(ref R dst, string src, scope MarkdownSettings settings = null) 67 { 68 if (!settings) settings = new MarkdownSettings; 69 70 auto all_lines = splitLines(src); 71 auto links = scanForReferences(all_lines); 72 auto lines = parseLines(all_lines, settings); 73 Block root_block; 74 parseBlocks(root_block, lines, null, settings); 75 writeBlock(dst, root_block, links, settings); 76 } 77 78 /** 79 Returns the hierarchy of sections 80 */ 81 Section[] getMarkdownOutline(string markdown_source, scope MarkdownSettings settings = null) 82 { 83 import std.conv : to; 84 85 if (!settings) settings = new MarkdownSettings; 86 auto all_lines = splitLines(markdown_source); 87 auto lines = parseLines(all_lines, settings); 88 Block root_block; 89 parseBlocks(root_block, lines, null, settings); 90 Section root; 91 92 foreach (ref sb; root_block.blocks) { 93 if (sb.type == BlockType.Header) { 94 auto s = &root; 95 while (true) { 96 if (s.subSections.length == 0) break; 97 if (s.subSections[$-1].headingLevel >= sb.headerLevel) break; 98 s = &s.subSections[$-1]; 99 } 100 s.subSections ~= Section(sb.headerLevel, sb.text[0], sb.text[0].asSlug.to!string); 101 } 102 } 103 104 return root.subSections; 105 } 106 107 /// 108 unittest { 109 import std.conv : to; 110 assert(getMarkdownOutline("## first\n## second\n### third\n# fourth\n### fifth") == 111 [ 112 Section(2, " first", "first"), 113 Section(2, " second", "second", [ 114 Section(3, " third", "third") 115 ]), 116 Section(1, " fourth", "fourth", [ 117 Section(3, " fifth", "fifth") 118 ]) 119 ] 120 ); 121 } 122 123 final class MarkdownSettings { 124 /// Controls the capabilities of the parser. 125 MarkdownFlags flags = MarkdownFlags.vanillaMarkdown; 126 127 /// Heading tags will start at this level. 128 size_t headingBaseLevel = 1; 129 130 /// Called for every link/image URL to perform arbitrary transformations. 131 string delegate(string url_or_path, bool is_image) urlFilter; 132 133 /// White list of URI schemas that can occur in link/image targets 134 string[] allowedURISchemas = ["http", "https", "ftp", "mailto"]; 135 } 136 137 enum MarkdownFlags { 138 none = 0, 139 keepLineBreaks = 1<<0, 140 backtickCodeBlocks = 1<<1, 141 noInlineHtml = 1<<2, 142 //noLinks = 1<<3, 143 //allowUnsafeHtml = 1<<4, 144 tables = 1<<5, 145 vanillaMarkdown = none, 146 forumDefault = keepLineBreaks|backtickCodeBlocks|noInlineHtml|tables 147 } 148 149 struct Section { 150 size_t headingLevel; 151 string caption; 152 string anchor; 153 Section[] subSections; 154 } 155 156 private { 157 immutable s_blockTags = ["div", "ol", "p", "pre", "section", "table", "ul"]; 158 } 159 160 private enum IndentType { 161 White, 162 Quote 163 } 164 165 private enum LineType { 166 Undefined, 167 Blank, 168 Plain, 169 Hline, 170 AtxHeader, 171 SetextHeader, 172 TableSeparator, 173 UList, 174 OList, 175 HtmlBlock, 176 CodeBlockDelimiter 177 } 178 179 private struct Line { 180 LineType type; 181 IndentType[] indent; 182 string text; 183 string unindented; 184 185 string unindent(size_t n) 186 pure @safe { 187 assert(n <= indent.length); 188 string ln = text; 189 foreach( i; 0 .. n ){ 190 final switch(indent[i]){ 191 case IndentType.White: 192 if( ln[0] == ' ' ) ln = ln[4 .. $]; 193 else ln = ln[1 .. $]; 194 break; 195 case IndentType.Quote: 196 ln = ln.stripLeft()[1 .. $]; 197 if (ln.startsWith(' ')) 198 ln.popFront(); 199 break; 200 } 201 } 202 return ln; 203 } 204 } 205 206 private Line[] parseLines(string[] lines, scope MarkdownSettings settings) 207 pure @safe { 208 Line[] ret; 209 while( !lines.empty ){ 210 auto ln = lines.front; 211 lines.popFront(); 212 213 Line lninfo; 214 lninfo.text = ln; 215 216 while( ln.length > 0 ){ 217 if( ln[0] == '\t' ){ 218 lninfo.indent ~= IndentType.White; 219 ln.popFront(); 220 } else if( ln.startsWith(" ") ){ 221 lninfo.indent ~= IndentType.White; 222 ln.popFrontN(4); 223 } else { 224 if( ln.stripLeft().startsWith(">") ){ 225 lninfo.indent ~= IndentType.Quote; 226 ln = ln.stripLeft(); 227 ln.popFront(); 228 if (ln.startsWith(' ')) 229 ln.popFront(); 230 } else break; 231 } 232 } 233 lninfo.unindented = ln; 234 235 if( (settings.flags & MarkdownFlags.backtickCodeBlocks) && isCodeBlockDelimiter(ln) ) lninfo.type = LineType.CodeBlockDelimiter; 236 else if( isAtxHeaderLine(ln) ) lninfo.type = LineType.AtxHeader; 237 else if( isSetextHeaderLine(ln) ) lninfo.type = LineType.SetextHeader; 238 else if( (settings.flags & MarkdownFlags.tables) && isTableSeparatorLine(ln) ) lninfo.type = LineType.TableSeparator; 239 else if( isHlineLine(ln) ) lninfo.type = LineType.Hline; 240 else if( isOListLine(ln) ) lninfo.type = LineType.OList; 241 else if( isUListLine(ln) ) lninfo.type = LineType.UList; 242 else if( isLineBlank(ln) ) lninfo.type = LineType.Blank; 243 else if( !(settings.flags & MarkdownFlags.noInlineHtml) && isHtmlBlockLine(ln) ) lninfo.type = LineType.HtmlBlock; 244 else lninfo.type = LineType.Plain; 245 246 ret ~= lninfo; 247 } 248 return ret; 249 } 250 251 unittest { 252 import std.conv : to; 253 auto s = new MarkdownSettings; 254 s.flags = MarkdownFlags.forumDefault; 255 auto lns = [">```D"]; 256 assert(parseLines(lns, s) == [Line(LineType.CodeBlockDelimiter, [IndentType.Quote], lns[0], "```D")]); 257 lns = ["> ```D"]; 258 assert(parseLines(lns, s) == [Line(LineType.CodeBlockDelimiter, [IndentType.Quote], lns[0], "```D")]); 259 lns = ["> ```D"]; 260 assert(parseLines(lns, s) == [Line(LineType.CodeBlockDelimiter, [IndentType.Quote], lns[0], " ```D")]); 261 lns = ["> ```D"]; 262 assert(parseLines(lns, s) == [Line(LineType.CodeBlockDelimiter, [IndentType.Quote, IndentType.White], lns[0], "```D")]); 263 lns = [">test"]; 264 assert(parseLines(lns, s) == [Line(LineType.Plain, [IndentType.Quote], lns[0], "test")]); 265 lns = ["> test"]; 266 assert(parseLines(lns, s) == [Line(LineType.Plain, [IndentType.Quote], lns[0], "test")]); 267 lns = ["> test"]; 268 assert(parseLines(lns, s) == [Line(LineType.Plain, [IndentType.Quote], lns[0], " test")]); 269 lns = ["> test"]; 270 assert(parseLines(lns, s) == [Line(LineType.Plain, [IndentType.Quote, IndentType.White], lns[0], "test")]); 271 } 272 273 private enum BlockType { 274 Plain, 275 Text, 276 Paragraph, 277 Header, 278 Table, 279 OList, 280 UList, 281 ListItem, 282 Code, 283 Quote 284 } 285 286 private struct Block { 287 BlockType type; 288 string[] text; 289 Block[] blocks; 290 size_t headerLevel; 291 Alignment[] columns; 292 } 293 294 295 private enum Alignment { 296 none = 0, 297 left = 1<<0, 298 right = 1<<1, 299 center = left | right 300 } 301 302 private void parseBlocks(ref Block root, ref Line[] lines, IndentType[] base_indent, scope MarkdownSettings settings) 303 pure @safe { 304 if( base_indent.length == 0 ) root.type = BlockType.Text; 305 else if( base_indent[$-1] == IndentType.Quote ) root.type = BlockType.Quote; 306 307 while( !lines.empty ){ 308 auto ln = lines.front; 309 310 if( ln.type == LineType.Blank ){ 311 lines.popFront(); 312 continue; 313 } 314 315 if( ln.indent != base_indent ){ 316 if( ln.indent.length < base_indent.length || ln.indent[0 .. base_indent.length] != base_indent ) 317 return; 318 319 auto cindent = base_indent ~ IndentType.White; 320 if( ln.indent == cindent ){ 321 Block cblock; 322 cblock.type = BlockType.Code; 323 while( !lines.empty && (lines.front.unindented.strip.empty || 324 lines.front.indent.length >= cindent.length && lines.front.indent[0 .. cindent.length] == cindent)) 325 { 326 cblock.text ~= lines.front.indent.length >= cindent.length ? lines.front.unindent(cindent.length) : ""; 327 lines.popFront(); 328 } 329 root.blocks ~= cblock; 330 } else { 331 Block subblock; 332 parseBlocks(subblock, lines, ln.indent[0 .. base_indent.length+1], settings); 333 root.blocks ~= subblock; 334 } 335 } else { 336 Block b; 337 final switch(ln.type){ 338 case LineType.Undefined: assert(false); 339 case LineType.Blank: assert(false); 340 case LineType.Plain: 341 if( lines.length >= 2 && lines[1].type == LineType.SetextHeader ){ 342 auto setln = lines[1].unindented; 343 b.type = BlockType.Header; 344 b.text = [ln.unindented]; 345 b.headerLevel = setln.strip()[0] == '=' ? 1 : 2; 346 lines.popFrontN(2); 347 } else if( lines.length >= 2 && lines[1].type == LineType.TableSeparator 348 && ln.unindented.indexOf('|') >= 0 ) 349 { 350 auto setln = lines[1].unindented; 351 b.type = BlockType.Table; 352 b.text = [ln.unindented]; 353 foreach (c; getTableColumns(setln)) { 354 Alignment a = Alignment.none; 355 if (c.startsWith(':')) a |= Alignment.left; 356 if (c.endsWith(':')) a |= Alignment.right; 357 b.columns ~= a; 358 } 359 360 lines.popFrontN(2); 361 while (!lines.empty && lines[0].unindented.indexOf('|') >= 0) { 362 b.text ~= lines.front.unindented; 363 lines.popFront(); 364 } 365 } else { 366 b.type = BlockType.Paragraph; 367 b.text = skipText(lines, base_indent); 368 } 369 break; 370 case LineType.Hline: 371 b.type = BlockType.Plain; 372 b.text = ["<hr>"]; 373 lines.popFront(); 374 break; 375 case LineType.AtxHeader: 376 b.type = BlockType.Header; 377 string hl = ln.unindented; 378 b.headerLevel = 0; 379 while( hl.length > 0 && hl[0] == '#' ){ 380 b.headerLevel++; 381 hl = hl[1 .. $]; 382 } 383 while( hl.length > 0 && (hl[$-1] == '#' || hl[$-1] == ' ') ) 384 hl = hl[0 .. $-1]; 385 b.text = [hl]; 386 lines.popFront(); 387 break; 388 case LineType.SetextHeader: 389 lines.popFront(); 390 break; 391 case LineType.TableSeparator: 392 lines.popFront(); 393 break; 394 case LineType.UList: 395 case LineType.OList: 396 b.type = ln.type == LineType.UList ? BlockType.UList : BlockType.OList; 397 auto itemindent = base_indent ~ IndentType.White; 398 bool firstItem = true, paraMode = false; 399 while(!lines.empty && lines.front.type == ln.type && lines.front.indent == base_indent ){ 400 Block itm; 401 itm.text = skipText(lines, itemindent); 402 itm.text[0] = removeListPrefix(itm.text[0], ln.type); 403 404 // emit <p></p> if there are blank lines between the items 405 if( firstItem && !lines.empty && lines.front.type == LineType.Blank ) 406 paraMode = true; 407 firstItem = false; 408 if( paraMode ){ 409 Block para; 410 para.type = BlockType.Paragraph; 411 para.text = itm.text; 412 itm.blocks ~= para; 413 itm.text = null; 414 } 415 416 parseBlocks(itm, lines, itemindent, settings); 417 itm.type = BlockType.ListItem; 418 b.blocks ~= itm; 419 } 420 break; 421 case LineType.HtmlBlock: 422 int nestlevel = 0; 423 auto starttag = parseHtmlBlockLine(ln.unindented); 424 if( !starttag.isHtmlBlock || !starttag.open ) 425 break; 426 427 b.type = BlockType.Plain; 428 while(!lines.empty){ 429 if( lines.front.indent.length < base_indent.length ) break; 430 if( lines.front.indent[0 .. base_indent.length] != base_indent ) break; 431 432 auto str = lines.front.unindent(base_indent.length); 433 auto taginfo = parseHtmlBlockLine(str); 434 b.text ~= lines.front.unindent(base_indent.length); 435 lines.popFront(); 436 if( taginfo.isHtmlBlock && taginfo.tagName == starttag.tagName ) 437 nestlevel += taginfo.open ? 1 : -1; 438 if( nestlevel <= 0 ) break; 439 } 440 break; 441 case LineType.CodeBlockDelimiter: 442 lines.popFront(); // TODO: get language from line 443 b.type = BlockType.Code; 444 while(!lines.empty){ 445 if( lines.front.indent.length < base_indent.length ) break; 446 if( lines.front.indent[0 .. base_indent.length] != base_indent ) break; 447 if( lines.front.type == LineType.CodeBlockDelimiter ){ 448 lines.popFront(); 449 break; 450 } 451 b.text ~= lines.front.unindent(base_indent.length); 452 lines.popFront(); 453 } 454 break; 455 } 456 root.blocks ~= b; 457 } 458 } 459 } 460 461 private string[] skipText(ref Line[] lines, IndentType[] indent) 462 pure @safe { 463 static bool matchesIndent(IndentType[] indent, IndentType[] base_indent) 464 { 465 if( indent.length > base_indent.length ) return false; 466 if( indent != base_indent[0 .. indent.length] ) return false; 467 sizediff_t qidx = -1; 468 foreach_reverse (i, tp; base_indent) if (tp == IndentType.Quote) { qidx = i; break; } 469 if( qidx >= 0 ){ 470 qidx = base_indent.length-1 - qidx; 471 if( indent.length <= qidx ) return false; 472 } 473 return true; 474 } 475 476 string[] ret; 477 478 while(true){ 479 ret ~= lines.front.unindent(min(indent.length, lines.front.indent.length)); 480 lines.popFront(); 481 482 if( lines.empty || !matchesIndent(lines.front.indent, indent) || lines.front.type != LineType.Plain ) 483 return ret; 484 } 485 } 486 487 /// private 488 private void writeBlock(R)(ref R dst, ref const Block block, LinkRef[string] links, scope MarkdownSettings settings) 489 { 490 final switch(block.type){ 491 case BlockType.Plain: 492 foreach( ln; block.text ){ 493 dst.put(ln); 494 dst.put("\n"); 495 } 496 foreach(b; block.blocks) 497 writeBlock(dst, b, links, settings); 498 break; 499 case BlockType.Text: 500 writeMarkdownEscaped(dst, block, links, settings); 501 foreach(b; block.blocks) 502 writeBlock(dst, b, links, settings); 503 break; 504 case BlockType.Paragraph: 505 assert(block.blocks.length == 0); 506 dst.put("<p>"); 507 writeMarkdownEscaped(dst, block, links, settings); 508 dst.put("</p>\n"); 509 break; 510 case BlockType.Header: 511 assert(block.blocks.length == 0); 512 auto hlvl = block.headerLevel + (settings ? settings.headingBaseLevel-1 : 0); 513 dst.formattedWrite("<h%s id=\"%s\">", hlvl, block.text[0].asSlug); 514 assert(block.text.length == 1); 515 writeMarkdownEscaped(dst, block.text[0], links, settings); 516 dst.formattedWrite("</h%s>\n", hlvl); 517 break; 518 case BlockType.Table: 519 import std.algorithm.iteration : splitter; 520 521 static string[Alignment.max+1] alstr = ["", " align=\"left\"", " align=\"right\"", " align=\"center\""]; 522 523 dst.put("<table>\n"); 524 dst.put("<tr>"); 525 size_t i = 0; 526 foreach (col; block.text[0].getTableColumns()) { 527 dst.put("<th"); 528 dst.put(alstr[block.columns[i]]); 529 dst.put('>'); 530 dst.writeMarkdownEscaped(col, links, settings); 531 dst.put("</th>"); 532 i++; 533 } 534 dst.put("</tr>\n"); 535 foreach (ln; block.text[1 .. $]) { 536 dst.put("<tr>"); 537 i = 0; 538 foreach (col; ln.getTableColumns()) { 539 dst.put("<td"); 540 dst.put(alstr[block.columns[i]]); 541 dst.put('>'); 542 dst.writeMarkdownEscaped(col, links, settings); 543 dst.put("</td>"); 544 i++; 545 } 546 dst.put("</tr>\n"); 547 } 548 dst.put("</table>\n"); 549 break; 550 case BlockType.OList: 551 dst.put("<ol>\n"); 552 foreach(b; block.blocks) 553 writeBlock(dst, b, links, settings); 554 dst.put("</ol>\n"); 555 break; 556 case BlockType.UList: 557 dst.put("<ul>\n"); 558 foreach(b; block.blocks) 559 writeBlock(dst, b, links, settings); 560 dst.put("</ul>\n"); 561 break; 562 case BlockType.ListItem: 563 dst.put("<li>"); 564 writeMarkdownEscaped(dst, block, links, settings); 565 foreach(b; block.blocks) 566 writeBlock(dst, b, links, settings); 567 dst.put("</li>\n"); 568 break; 569 case BlockType.Code: 570 assert(block.blocks.length == 0); 571 dst.put("<pre class=\"prettyprint\"><code>"); 572 foreach(ln; block.text){ 573 filterHTMLEscape(dst, ln); 574 dst.put("\n"); 575 } 576 dst.put("</code></pre>\n"); 577 break; 578 case BlockType.Quote: 579 dst.put("<blockquote>"); 580 writeMarkdownEscaped(dst, block, links, settings); 581 foreach(b; block.blocks) 582 writeBlock(dst, b, links, settings); 583 dst.put("</blockquote>\n"); 584 break; 585 } 586 } 587 588 private void writeMarkdownEscaped(R)(ref R dst, ref const Block block, in LinkRef[string] links, scope MarkdownSettings settings) 589 { 590 auto lines = () @trusted { return cast(string[])block.text; } (); 591 auto text = settings.flags & MarkdownFlags.keepLineBreaks ? lines.join("<br>") : lines.join("\n"); 592 writeMarkdownEscaped(dst, text, links, settings); 593 if (lines.length) dst.put("\n"); 594 } 595 596 /// private 597 private void writeMarkdownEscaped(R)(ref R dst, string ln, in LinkRef[string] linkrefs, scope MarkdownSettings settings) 598 { 599 bool isAllowedURI(string lnk) { 600 auto idx = lnk.indexOf('/'); 601 auto cidx = lnk.indexOf(':'); 602 // always allow local URIs 603 if (cidx < 0 || idx >= 0 && cidx > idx) return true; 604 return settings.allowedURISchemas.canFind(lnk[0 .. cidx]); 605 } 606 607 string filterLink(string lnk, bool is_image) { 608 if (isAllowedURI(lnk)) 609 return settings.urlFilter ? settings.urlFilter(lnk, is_image) : lnk; 610 return "#"; // replace link with unknown schema with dummy URI 611 } 612 613 bool br = ln.endsWith(" "); 614 while( ln.length > 0 ){ 615 switch( ln[0] ){ 616 default: 617 dst.put(ln[0]); 618 ln = ln[1 .. $]; 619 break; 620 case '\\': 621 if( ln.length >= 2 ){ 622 switch(ln[1]){ 623 default: 624 dst.put(ln[0 .. 2]); 625 ln = ln[2 .. $]; 626 break; 627 case '\'', '`', '*', '_', '{', '}', '[', ']', 628 '(', ')', '#', '+', '-', '.', '!': 629 dst.put(ln[1]); 630 ln = ln[2 .. $]; 631 break; 632 } 633 } else { 634 dst.put(ln[0]); 635 ln = ln[1 .. $]; 636 } 637 break; 638 case '_': 639 case '*': 640 string text; 641 if( auto em = parseEmphasis(ln, text) ){ 642 dst.put(em == 1 ? "<em>" : em == 2 ? "<strong>" : "<strong><em>"); 643 filterHTMLEscape(dst, text, HTMLEscapeFlags.escapeMinimal); 644 dst.put(em == 1 ? "</em>" : em == 2 ? "</strong>": "</em></strong>"); 645 } else { 646 dst.put(ln[0]); 647 ln = ln[1 .. $]; 648 } 649 break; 650 case '`': 651 string code; 652 if( parseInlineCode(ln, code) ){ 653 dst.put("<code class=\"prettyprint\">"); 654 filterHTMLEscape(dst, code, HTMLEscapeFlags.escapeMinimal); 655 dst.put("</code>"); 656 } else { 657 dst.put(ln[0]); 658 ln = ln[1 .. $]; 659 } 660 break; 661 case '[': 662 Link link; 663 if( parseLink(ln, link, linkrefs) ){ 664 dst.put("<a href=\""); 665 filterHTMLAttribEscape(dst, filterLink(link.url, false)); 666 dst.put("\""); 667 if( link.title.length ){ 668 dst.put(" title=\""); 669 filterHTMLAttribEscape(dst, link.title); 670 dst.put("\""); 671 } 672 dst.put(">"); 673 writeMarkdownEscaped(dst, link.text, linkrefs, settings); 674 dst.put("</a>"); 675 } else { 676 dst.put(ln[0]); 677 ln = ln[1 .. $]; 678 } 679 break; 680 case '!': 681 Link link; 682 if( parseLink(ln, link, linkrefs) ){ 683 dst.put("<img src=\""); 684 filterHTMLAttribEscape(dst, filterLink(link.url, true)); 685 dst.put("\" alt=\""); 686 filterHTMLAttribEscape(dst, link.text); 687 dst.put("\""); 688 if( link.title.length ){ 689 dst.put(" title=\""); 690 filterHTMLAttribEscape(dst, link.title); 691 dst.put("\""); 692 } 693 dst.put(">"); 694 } else if( ln.length >= 2 ){ 695 dst.put(ln[0 .. 2]); 696 ln = ln[2 .. $]; 697 } else { 698 dst.put(ln[0]); 699 ln = ln[1 .. $]; 700 } 701 break; 702 case '>': 703 if( settings.flags & MarkdownFlags.noInlineHtml ) dst.put(">"); 704 else dst.put(ln[0]); 705 ln = ln[1 .. $]; 706 break; 707 case '<': 708 string url; 709 if( parseAutoLink(ln, url) ){ 710 bool is_email = url.startsWith("mailto:"); 711 dst.put("<a href=\""); 712 if (is_email) filterHTMLAllEscape(dst, url); 713 else filterHTMLAttribEscape(dst, filterLink(url, false)); 714 dst.put("\">"); 715 if (is_email) filterHTMLAllEscape(dst, url[7 .. $]); 716 else filterHTMLEscape(dst, url, HTMLEscapeFlags.escapeMinimal); 717 dst.put("</a>"); 718 } else { 719 if (ln.startsWith("<br>")) { 720 // always support line breaks, since we embed them here ourselves! 721 dst.put("<br/>"); 722 ln = ln[4 .. $]; 723 } else if(ln.startsWith("<br/>")) { 724 dst.put("<br/>"); 725 ln = ln[5 .. $]; 726 } else { 727 if( settings.flags & MarkdownFlags.noInlineHtml ) dst.put("<"); 728 else dst.put(ln[0]); 729 ln = ln[1 .. $]; 730 } 731 } 732 break; 733 } 734 } 735 if( br ) dst.put("<br/>"); 736 } 737 738 private bool isLineBlank(string ln) 739 pure @safe { 740 return allOf(ln, " \t"); 741 } 742 743 private bool isSetextHeaderLine(string ln) 744 pure @safe { 745 ln = stripLeft(ln); 746 if( ln.length < 1 ) return false; 747 if( ln[0] == '=' ){ 748 while(!ln.empty && ln.front == '=') ln.popFront(); 749 return allOf(ln, " \t"); 750 } 751 if( ln[0] == '-' ){ 752 while(!ln.empty && ln.front == '-') ln.popFront(); 753 return allOf(ln, " \t"); 754 } 755 return false; 756 } 757 758 private bool isAtxHeaderLine(string ln) 759 pure @safe { 760 ln = stripLeft(ln); 761 size_t i = 0; 762 while( i < ln.length && ln[i] == '#' ) i++; 763 if( i < 1 || i > 6 || i >= ln.length ) return false; 764 return ln[i] == ' '; 765 } 766 767 private bool isTableSeparatorLine(string ln) 768 pure @safe { 769 import std.algorithm.iteration : splitter; 770 771 ln = strip(ln); 772 if (ln.startsWith("|")) ln = ln[1 .. $]; 773 if (ln.endsWith("|")) ln = ln[0 .. $-1]; 774 775 auto cols = ln.splitter('|'); 776 size_t cnt = 0; 777 foreach (c; cols) { 778 if (c.startsWith(':')) c = c[1 .. $]; 779 if (c.endsWith(':')) c = c[0 .. $-1]; 780 if (c.length < 3 || !c.allOf("-")) 781 return false; 782 cnt++; 783 } 784 return cnt >= 2; 785 } 786 787 private auto getTableColumns(string line) 788 pure @safe nothrow { 789 import std.algorithm.iteration : map, splitter; 790 791 if (line.startsWith("|")) line = line[1 .. $]; 792 if (line.endsWith("|")) line = line[0 .. $-1]; 793 return line.splitter('|').map!(s => s.strip()); 794 } 795 796 private size_t countTableColumns(string line) 797 pure @safe { 798 return getTableColumns(line).count(); 799 } 800 801 private bool isHlineLine(string ln) 802 pure @safe { 803 if( allOf(ln, " -") && count(ln, '-') >= 3 ) return true; 804 if( allOf(ln, " *") && count(ln, '*') >= 3 ) return true; 805 if( allOf(ln, " _") && count(ln, '_') >= 3 ) return true; 806 return false; 807 } 808 809 private bool isQuoteLine(string ln) 810 pure @safe { 811 return ln.stripLeft().startsWith(">"); 812 } 813 814 private size_t getQuoteLevel(string ln) 815 pure @safe { 816 size_t level = 0; 817 ln = stripLeft(ln); 818 while( ln.length > 0 && ln[0] == '>' ){ 819 level++; 820 ln = stripLeft(ln[1 .. $]); 821 } 822 return level; 823 } 824 825 private bool isUListLine(string ln) 826 pure @safe { 827 ln = stripLeft(ln); 828 if (ln.length < 2) return false; 829 if (!canFind("*+-", ln[0])) return false; 830 if (ln[1] != ' ' && ln[1] != '\t') return false; 831 return true; 832 } 833 834 private bool isOListLine(string ln) 835 pure @safe { 836 ln = stripLeft(ln); 837 if( ln.length < 1 ) return false; 838 if( ln[0] < '0' || ln[0] > '9' ) return false; 839 ln = ln[1 .. $]; 840 while( ln.length > 0 && ln[0] >= '0' && ln[0] <= '9' ) 841 ln = ln[1 .. $]; 842 if( ln.length < 2 ) return false; 843 if( ln[0] != '.' ) return false; 844 if( ln[1] != ' ' && ln[1] != '\t' ) 845 return false; 846 return true; 847 } 848 849 private string removeListPrefix(string str, LineType tp) 850 pure @safe { 851 switch(tp){ 852 default: assert(false); 853 case LineType.OList: // skip bullets and output using normal escaping 854 auto idx = str.indexOfCT('.'); 855 assert(idx > 0); 856 return str[idx+1 .. $].stripLeft(); 857 case LineType.UList: 858 return stripLeft(str.stripLeft()[1 .. $]); 859 } 860 } 861 862 863 private auto parseHtmlBlockLine(string ln) 864 pure @safe { 865 struct HtmlBlockInfo { 866 bool isHtmlBlock; 867 string tagName; 868 bool open; 869 } 870 871 HtmlBlockInfo ret; 872 ret.isHtmlBlock = false; 873 ret.open = true; 874 875 ln = strip(ln); 876 if( ln.length < 3 ) return ret; 877 if( ln[0] != '<' ) return ret; 878 if( ln[1] == '/' ){ 879 ret.open = false; 880 ln = ln[1 .. $]; 881 } 882 import std.ascii : isAlpha; 883 if( !isAlpha(ln[1]) ) return ret; 884 ln = ln[1 .. $]; 885 size_t idx = 0; 886 while( idx < ln.length && ln[idx] != ' ' && ln[idx] != '>' ) 887 idx++; 888 ret.tagName = ln[0 .. idx]; 889 ln = ln[idx .. $]; 890 891 auto eidx = ln.indexOf('>'); 892 if( eidx < 0 ) return ret; 893 if( eidx != ln.length-1 ) return ret; 894 895 if (!s_blockTags.canFind(ret.tagName)) return ret; 896 897 ret.isHtmlBlock = true; 898 return ret; 899 } 900 901 private bool isHtmlBlockLine(string ln) 902 pure @safe { 903 auto bi = parseHtmlBlockLine(ln); 904 return bi.isHtmlBlock && bi.open; 905 } 906 907 private bool isHtmlBlockCloseLine(string ln) 908 pure @safe { 909 auto bi = parseHtmlBlockLine(ln); 910 return bi.isHtmlBlock && !bi.open; 911 } 912 913 private bool isCodeBlockDelimiter(string ln) 914 pure @safe { 915 return ln.stripLeft.startsWith("```"); 916 } 917 918 private string getHtmlTagName(string ln) 919 pure @safe { 920 return parseHtmlBlockLine(ln).tagName; 921 } 922 923 private bool isLineIndented(string ln) 924 pure @safe { 925 return ln.startsWith("\t") || ln.startsWith(" "); 926 } 927 928 private string unindentLine(string ln) 929 pure @safe { 930 if( ln.startsWith("\t") ) return ln[1 .. $]; 931 if( ln.startsWith(" ") ) return ln[4 .. $]; 932 assert(false); 933 } 934 935 private int parseEmphasis(ref string str, ref string text) 936 pure @safe { 937 string pstr = str; 938 if( pstr.length < 3 ) return false; 939 940 string ctag; 941 if( pstr.startsWith("***") ) ctag = "***"; 942 else if( pstr.startsWith("**") ) ctag = "**"; 943 else if( pstr.startsWith("*") ) ctag = "*"; 944 else if( pstr.startsWith("___") ) ctag = "___"; 945 else if( pstr.startsWith("__") ) ctag = "__"; 946 else if( pstr.startsWith("_") ) ctag = "_"; 947 else return false; 948 949 pstr = pstr[ctag.length .. $]; 950 951 auto cidx = () @trusted { return pstr.indexOf(ctag); }(); 952 if( cidx < 1 ) return false; 953 954 text = pstr[0 .. cidx]; 955 956 str = pstr[cidx+ctag.length .. $]; 957 return cast(int)ctag.length; 958 } 959 960 private bool parseInlineCode(ref string str, ref string code) 961 pure @safe { 962 string pstr = str; 963 if( pstr.length < 3 ) return false; 964 string ctag; 965 if( pstr.startsWith("``") ) ctag = "``"; 966 else if( pstr.startsWith("`") ) ctag = "`"; 967 else return false; 968 pstr = pstr[ctag.length .. $]; 969 970 auto cidx = () @trusted { return pstr.indexOf(ctag); }(); 971 if( cidx < 1 ) return false; 972 973 code = pstr[0 .. cidx]; 974 str = pstr[cidx+ctag.length .. $]; 975 return true; 976 } 977 978 private bool parseLink(ref string str, ref Link dst, in LinkRef[string] linkrefs) 979 pure @safe { 980 string pstr = str; 981 if( pstr.length < 3 ) return false; 982 // ignore img-link prefix 983 if( pstr[0] == '!' ) pstr = pstr[1 .. $]; 984 985 // parse the text part [text] 986 if( pstr[0] != '[' ) return false; 987 auto cidx = pstr.matchBracket(); 988 if( cidx < 1 ) return false; 989 string refid; 990 dst.text = pstr[1 .. cidx]; 991 pstr = pstr[cidx+1 .. $]; 992 993 // parse either (link '['"title"']') or '[' ']'[refid] 994 if( pstr.length < 2 ) return false; 995 if( pstr[0] == '('){ 996 cidx = pstr.matchBracket(); 997 if( cidx < 1 ) return false; 998 auto inner = pstr[1 .. cidx]; 999 immutable qidx = inner.indexOfCT('"'); 1000 import std.ascii : isWhite; 1001 if( qidx > 1 && inner[qidx - 1].isWhite()){ 1002 dst.url = inner[0 .. qidx].stripRight(); 1003 immutable len = inner[qidx .. $].lastIndexOf('"'); 1004 if( len == 0 ) return false; 1005 assert(len > 0); 1006 dst.title = inner[qidx + 1 .. qidx + len]; 1007 } else { 1008 dst.url = inner.stripRight(); 1009 dst.title = null; 1010 } 1011 if (dst.url.startsWith("<") && dst.url.endsWith(">")) 1012 dst.url = dst.url[1 .. $-1]; 1013 pstr = pstr[cidx+1 .. $]; 1014 } else { 1015 if( pstr[0] == ' ' ) pstr = pstr[1 .. $]; 1016 if( pstr[0] != '[' ) return false; 1017 pstr = pstr[1 .. $]; 1018 cidx = pstr.indexOfCT(']'); 1019 if( cidx < 0 ) return false; 1020 if( cidx == 0 ) refid = dst.text; 1021 else refid = pstr[0 .. cidx]; 1022 pstr = pstr[cidx+1 .. $]; 1023 } 1024 1025 1026 if( refid.length > 0 ){ 1027 auto pr = toLower(refid) in linkrefs; 1028 if( !pr ){ 1029 debug if (!__ctfe) logDebug("[LINK REF NOT FOUND: '%s'", refid); 1030 return false; 1031 } 1032 dst.url = pr.url; 1033 dst.title = pr.title; 1034 } 1035 1036 str = pstr; 1037 return true; 1038 } 1039 1040 @safe unittest 1041 { 1042 static void testLink(string s, Link exp, in LinkRef[string] refs) 1043 { 1044 Link link; 1045 assert(parseLink(s, link, refs), s); 1046 assert(link == exp); 1047 } 1048 LinkRef[string] refs; 1049 refs["ref"] = LinkRef("ref", "target", "title"); 1050 1051 testLink(`[link](target)`, Link("link", "target"), null); 1052 testLink(`[link](target "title")`, Link("link", "target", "title"), null); 1053 testLink(`[link](target "title")`, Link("link", "target", "title"), null); 1054 testLink(`[link](target "title" )`, Link("link", "target", "title"), null); 1055 1056 testLink(`[link](target)`, Link("link", "target"), null); 1057 testLink(`[link](target "title")`, Link("link", "target", "title"), null); 1058 1059 testLink(`[link][ref]`, Link("link", "target", "title"), refs); 1060 testLink(`[ref][]`, Link("ref", "target", "title"), refs); 1061 1062 testLink(`[link[with brackets]](target)`, Link("link[with brackets]", "target"), null); 1063 testLink(`[link[with brackets]][ref]`, Link("link[with brackets]", "target", "title"), refs); 1064 1065 testLink(`[link](/target with spaces )`, Link("link", "/target with spaces"), null); 1066 testLink(`[link](/target with spaces "title")`, Link("link", "/target with spaces", "title"), null); 1067 1068 testLink(`[link](white-space "around title" )`, Link("link", "white-space", "around title"), null); 1069 testLink(`[link](tabs "around title" )`, Link("link", "tabs", "around title"), null); 1070 1071 testLink(`[link](target "")`, Link("link", "target", ""), null); 1072 testLink(`[link](target-no-title"foo" )`, Link("link", "target-no-title\"foo\"", ""), null); 1073 1074 testLink(`[link](<target>)`, Link("link", "target"), null); 1075 1076 auto failing = [ 1077 `text`, `[link](target`, `[link]target)`, `[link]`, 1078 `[link(target)`, `link](target)`, `[link] (target)`, 1079 `[link][noref]`, `[noref][]` 1080 ]; 1081 Link link; 1082 foreach (s; failing) 1083 assert(!parseLink(s, link, refs), s); 1084 } 1085 1086 private bool parseAutoLink(ref string str, ref string url) 1087 pure @safe { 1088 string pstr = str; 1089 if( pstr.length < 3 ) return false; 1090 if( pstr[0] != '<' ) return false; 1091 pstr = pstr[1 .. $]; 1092 auto cidx = pstr.indexOf('>'); 1093 if( cidx < 0 ) return false; 1094 url = pstr[0 .. cidx]; 1095 if( anyOf(url, " \t") ) return false; 1096 if( !anyOf(url, ":@") ) return false; 1097 str = pstr[cidx+1 .. $]; 1098 if( url.indexOf('@') > 0 ) url = "mailto:"~url; 1099 return true; 1100 } 1101 1102 private LinkRef[string] scanForReferences(ref string[] lines) 1103 pure @safe { 1104 LinkRef[string] ret; 1105 bool[size_t] reflines; 1106 1107 // search for reference definitions: 1108 // [refid] link "opt text" 1109 // [refid] <link> "opt text" 1110 // "opt text", 'opt text', (opt text) 1111 // line must not be indented 1112 foreach( lnidx, ln; lines ){ 1113 if( isLineIndented(ln) ) continue; 1114 ln = strip(ln); 1115 if( !ln.startsWith("[") ) continue; 1116 ln = ln[1 .. $]; 1117 1118 auto idx = () @trusted { return ln.indexOf("]:"); }(); 1119 if( idx < 0 ) continue; 1120 string refid = ln[0 .. idx]; 1121 ln = stripLeft(ln[idx+2 .. $]); 1122 1123 string url; 1124 if( ln.startsWith("<") ){ 1125 idx = ln.indexOfCT('>'); 1126 if( idx < 0 ) continue; 1127 url = ln[1 .. idx]; 1128 ln = ln[idx+1 .. $]; 1129 } else { 1130 idx = ln.indexOfCT(' '); 1131 if( idx > 0 ){ 1132 url = ln[0 .. idx]; 1133 ln = ln[idx+1 .. $]; 1134 } else { 1135 idx = ln.indexOfCT('\t'); 1136 if( idx < 0 ){ 1137 url = ln; 1138 ln = ln[$ .. $]; 1139 } else { 1140 url = ln[0 .. idx]; 1141 ln = ln[idx+1 .. $]; 1142 } 1143 } 1144 } 1145 ln = stripLeft(ln); 1146 1147 string title; 1148 if( ln.length >= 3 ){ 1149 if( ln[0] == '(' && ln[$-1] == ')' || ln[0] == '\"' && ln[$-1] == '\"' || ln[0] == '\'' && ln[$-1] == '\'' ) 1150 title = ln[1 .. $-1]; 1151 } 1152 1153 ret[toLower(refid)] = LinkRef(refid, url, title); 1154 reflines[lnidx] = true; 1155 1156 debug if (!__ctfe) logTrace("[detected ref on line %d]", lnidx+1); 1157 } 1158 1159 // remove all lines containing references 1160 auto nonreflines = appender!(string[])(); 1161 nonreflines.reserve(lines.length); 1162 foreach( i, ln; lines ) 1163 if( i !in reflines ) 1164 nonreflines.put(ln); 1165 lines = nonreflines.data(); 1166 1167 return ret; 1168 } 1169 1170 1171 /** 1172 Generates an identifier suitable to use as within a URL. 1173 1174 The resulting string will contain only ASCII lower case alphabetic or 1175 numeric characters, as well as dashes (-). Every sequence of 1176 non-alphanumeric characters will be replaced by a single dash. No dashes 1177 will be at either the front or the back of the result string. 1178 */ 1179 auto asSlug(R)(R text) 1180 if (isInputRange!R && is(typeof(R.init.front) == dchar)) 1181 { 1182 static struct SlugRange { 1183 private { 1184 R _input; 1185 bool _dash; 1186 } 1187 1188 this(R input) 1189 { 1190 _input = input; 1191 skipNonAlphaNum(); 1192 } 1193 1194 @property bool empty() const { return _dash ? false : _input.empty; } 1195 @property char front() const { 1196 if (_dash) return '-'; 1197 1198 char r = cast(char)_input.front; 1199 if (r >= 'A' && r <= 'Z') return cast(char)(r + ('a' - 'A')); 1200 return r; 1201 } 1202 1203 void popFront() 1204 { 1205 if (_dash) { 1206 _dash = false; 1207 return; 1208 } 1209 1210 _input.popFront(); 1211 auto na = skipNonAlphaNum(); 1212 if (na && !_input.empty) 1213 _dash = true; 1214 } 1215 1216 private bool skipNonAlphaNum() 1217 { 1218 bool have_skipped = false; 1219 while (!_input.empty) { 1220 switch (_input.front) { 1221 default: 1222 _input.popFront(); 1223 have_skipped = true; 1224 break; 1225 case 'a': .. case 'z': 1226 case 'A': .. case 'Z': 1227 case '0': .. case '9': 1228 return have_skipped; 1229 } 1230 } 1231 return have_skipped; 1232 } 1233 } 1234 return SlugRange(text); 1235 } 1236 1237 unittest { 1238 import std.algorithm : equal; 1239 assert("".asSlug.equal("")); 1240 assert(".,-".asSlug.equal("")); 1241 assert("abc".asSlug.equal("abc")); 1242 assert("aBc123".asSlug.equal("abc123")); 1243 assert("....aBc...123...".asSlug.equal("abc-123")); 1244 } 1245 1246 private struct LinkRef { 1247 string id; 1248 string url; 1249 string title; 1250 } 1251 1252 private struct Link { 1253 string text; 1254 string url; 1255 string title; 1256 } 1257 1258 @safe unittest { // alt and title attributes 1259 assert(filterMarkdown("") 1260 == "<p><img src=\"http://example.org/image\" alt=\"alt\">\n</p>\n"); 1261 assert(filterMarkdown("") 1262 == "<p><img src=\"http://example.org/image\" alt=\"alt\" title=\"Title\">\n</p>\n"); 1263 } 1264 1265 @safe unittest { // complex links 1266 assert(filterMarkdown("their [install\ninstructions](<http://www.brew.sh>) and") 1267 == "<p>their <a href=\"http://www.brew.sh\">install\ninstructions</a> and\n</p>\n"); 1268 assert(filterMarkdown("[](https://travis-ci.org/rejectedsoftware/vibe.d)") 1269 == "<p><a href=\"https://travis-ci.org/rejectedsoftware/vibe.d\"><img src=\"https://travis-ci.org/rejectedsoftware/vibe.d.png\" alt=\"Build Status\"></a>\n</p>\n"); 1270 } 1271 1272 @safe unittest { // check CTFE-ability 1273 enum res = filterMarkdown("### some markdown\n[foo][]\n[foo]: /bar"); 1274 assert(res == "<h3 id=\"some-markdown\"> some markdown</h3>\n<p><a href=\"/bar\">foo</a>\n</p>\n", res); 1275 } 1276 1277 @safe unittest { // correct line breaks in restrictive mode 1278 auto res = filterMarkdown("hello\nworld", MarkdownFlags.forumDefault); 1279 assert(res == "<p>hello<br/>world\n</p>\n", res); 1280 } 1281 1282 /*@safe unittest { // code blocks and blockquotes 1283 assert(filterMarkdown("\tthis\n\tis\n\tcode") == 1284 "<pre><code>this\nis\ncode</code></pre>\n"); 1285 assert(filterMarkdown(" this\n is\n code") == 1286 "<pre><code>this\nis\ncode</code></pre>\n"); 1287 assert(filterMarkdown(" this\n is\n\tcode") == 1288 "<pre><code>this\nis</code></pre>\n<pre><code>code</code></pre>\n"); 1289 assert(filterMarkdown("\tthis\n\n\tcode") == 1290 "<pre><code>this\n\ncode</code></pre>\n"); 1291 assert(filterMarkdown("\t> this") == 1292 "<pre><code>> this</code></pre>\n"); 1293 assert(filterMarkdown("> this") == 1294 "<blockquote><pre><code>this</code></pre></blockquote>\n"); 1295 assert(filterMarkdown("> this\n is code") == 1296 "<blockquote><pre><code>this\nis code</code></pre></blockquote>\n"); 1297 }*/ 1298 1299 @safe unittest { 1300 assert(filterMarkdown("## Hello, World!") == "<h2 id=\"hello-world\"> Hello, World!</h2>\n", filterMarkdown("## Hello, World!")); 1301 } 1302 1303 @safe unittest { // tables 1304 assert(filterMarkdown("foo|bar\n---|---", MarkdownFlags.tables) 1305 == "<table>\n<tr><th>foo</th><th>bar</th></tr>\n</table>\n"); 1306 assert(filterMarkdown(" *foo* | bar \n---|---\n baz|bam", MarkdownFlags.tables) 1307 == "<table>\n<tr><th><em>foo</em></th><th>bar</th></tr>\n<tr><td>baz</td><td>bam</td></tr>\n</table>\n"); 1308 assert(filterMarkdown("|foo|bar|\n---|---\n baz|bam", MarkdownFlags.tables) 1309 == "<table>\n<tr><th>foo</th><th>bar</th></tr>\n<tr><td>baz</td><td>bam</td></tr>\n</table>\n"); 1310 assert(filterMarkdown("foo|bar\n|---|---|\nbaz|bam", MarkdownFlags.tables) 1311 == "<table>\n<tr><th>foo</th><th>bar</th></tr>\n<tr><td>baz</td><td>bam</td></tr>\n</table>\n"); 1312 assert(filterMarkdown("foo|bar\n---|---\n|baz|bam|", MarkdownFlags.tables) 1313 == "<table>\n<tr><th>foo</th><th>bar</th></tr>\n<tr><td>baz</td><td>bam</td></tr>\n</table>\n"); 1314 assert(filterMarkdown("foo|bar|baz\n:---|---:|:---:\n|baz|bam|bap|", MarkdownFlags.tables) 1315 == "<table>\n<tr><th align=\"left\">foo</th><th align=\"right\">bar</th><th align=\"center\">baz</th></tr>\n" 1316 ~ "<tr><td align=\"left\">baz</td><td align=\"right\">bam</td><td align=\"center\">bap</td></tr>\n</table>\n"); 1317 assert(filterMarkdown(" |bar\n---|---", MarkdownFlags.tables) 1318 == "<table>\n<tr><th></th><th>bar</th></tr>\n</table>\n"); 1319 assert(filterMarkdown("foo|bar\n---|---\nbaz|", MarkdownFlags.tables) 1320 == "<table>\n<tr><th>foo</th><th>bar</th></tr>\n<tr><td>baz</td></tr>\n</table>\n"); 1321 } 1322 1323 @safe unittest { // issue #1527 - blank lines in code blocks 1324 assert(filterMarkdown(" foo\n\n bar\n") == 1325 "<pre class=\"prettyprint\"><code>foo\n\nbar\n</code></pre>\n"); 1326 } 1327 1328 @safe unittest { 1329 assert(filterMarkdown("> ```\r\n> test\r\n> ```", MarkdownFlags.forumDefault) == 1330 "<blockquote><pre class=\"prettyprint\"><code>test\n</code></pre>\n</blockquote>\n"); 1331 } 1332 1333 @safe unittest { // issue #1845 - malicious URI targets 1334 assert(filterMarkdown("[foo](javascript:foo)  <javascript:baz>", MarkdownFlags.forumDefault) == 1335 "<p><a href=\"#\">foo</a> <img src=\"#\" alt=\"bar\"> <a href=\"#\">javascript:baz</a>\n</p>\n"); 1336 assert(filterMarkdown("[foo][foo] ![foo][foo]\n[foo]: javascript:foo", MarkdownFlags.forumDefault) == 1337 "<p><a href=\"#\">foo</a> <img src=\"#\" alt=\"foo\">\n</p>\n"); 1338 assert(filterMarkdown("[foo](javascript%3Abar)", MarkdownFlags.forumDefault) == 1339 "<p><a href=\"javascript%3Abar\">foo</a>\n</p>\n"); 1340 1341 // extra XSS regression tests 1342 assert(filterMarkdown("[<script></script>](bar)", MarkdownFlags.forumDefault) == 1343 "<p><a href=\"bar\"><script></script></a>\n</p>\n"); 1344 assert(filterMarkdown("[foo](\"><script></script><span foo=\")", MarkdownFlags.forumDefault) == 1345 "<p><a href=\""><script></script><span foo="\">foo</a>\n</p>\n"); 1346 assert(filterMarkdown("[foo](javascript:bar)", MarkdownFlags.forumDefault) == 1347 "<p><a href=\"javascript&#58;bar\">foo</a>\n</p>\n"); 1348 }