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