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("&gt;");
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("&lt;");
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("![alt](http://example.org/image)")
1260 		== "<p><img src=\"http://example.org/image\" alt=\"alt\">\n</p>\n");
1261 	assert(filterMarkdown("![alt](http://example.org/image \"Title\")")
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("[![Build Status](https://travis-ci.org/rejectedsoftware/vibe.d.png)](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>&gt; 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) ![bar](javascript:bar) <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\">&lt;script&gt;&lt;/script&gt;</a>\n</p>\n");
1344 	assert(filterMarkdown("[foo](\"><script></script><span foo=\")", MarkdownFlags.forumDefault) ==
1345 		"<p><a href=\"&quot;&gt;&lt;script&gt;&lt;/script&gt;&lt;span foo=&quot;\">foo</a>\n</p>\n");
1346 	assert(filterMarkdown("[foo](javascript&#58;bar)", MarkdownFlags.forumDefault) ==
1347 		"<p><a href=\"javascript&amp;#58;bar\">foo</a>\n</p>\n");
1348 }