1 /**
2 	DietDoc/DDOC support routines
3 
4 	Copyright: © 2012 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 std.algorithm : canFind, countUntil, map, min, remove;
14 import std.array;
15 import std.conv;
16 import std.string;
17 import std.uni : isAlpha;
18 
19 
20 static this()
21 {
22 	s_standardMacros = [
23 		"P" : "<p>$0</p>",
24 		"DL" : "<dl>$0</dl>",
25 		"DT" : "<dt>$0</dt>",
26 		"DD" : "<dd>$0</dd>",
27 		"TABLE" : "<table>$0</table>",
28 		"TR" : "<tr>$0</tr>",
29 		"TH" : "<th>$0</th>",
30 		"TD" : "<td>$0</td>",
31 		"OL" : "<ol>$0</ol>",
32 		"UL" : "<ul>$0</ul>",
33 		"LI" : "<li>$0</li>",
34 		"LINK" : "<a href=\"$0\">$0</a>",
35 		"LINK2" : "<a href=\"$1\">$+</a>",
36 		"LPAREN" : "(",
37 		"RPAREN" : ")"
38 	];
39 }
40 
41 
42 /**
43 	Takes a DDOC string and outputs formatted HTML.
44 
45 	The hlevel parameter specifies the header level used for section names (&lt;h2&gt by default).
46 	By specifying a display_section callback it is also possible to output only certain sections.
47 */
48 string formatDdocComment(string ddoc_, int hlevel = 2, bool delegate(string) display_section = null)
49 {
50 	return formatDdocComment(ddoc_, new BareContext, hlevel, display_section);
51 }
52 /// ditto
53 string formatDdocComment(string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null)
54 {
55 	auto dst = appender!string();
56 	filterDdocComment(dst, text, context, hlevel, display_section);
57 	return dst.data;
58 }
59 /// ditto
60 void filterDdocComment(R)(ref R dst, string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null)
61 {
62 	auto comment = new DdocComment(text);
63 	comment.renderSectionsR(dst, context, display_section, hlevel);
64 }
65 
66 
67 /**
68 	Sets a set of macros that will be available to all calls to formatDdocComment.
69 */
70 void setDefaultDdocMacroFiles(string[] filenames)
71 {
72 	import vibe.core.file;
73 	import vibe.stream.operations;
74 	s_defaultMacros = null;
75 	foreach (filename; filenames) {
76 		auto text = readAllUTF8(openFile(filename));
77 		parseMacros(s_defaultMacros, splitLines(text));
78 	}
79 }
80 
81 
82 /**
83 	Sets a set of macros that will be available to all calls to formatDdocComment and override local macro definitions.
84 */
85 void setOverrideDdocMacroFiles(string[] filenames)
86 {
87 	import vibe.core.file;
88 	import vibe.stream.operations;
89 	s_overrideMacros = null;
90 	foreach (filename; filenames) {
91 		auto text = readAllUTF8(openFile(filename));
92 		parseMacros(s_overrideMacros, splitLines(text));
93 	}
94 }
95 
96 
97 /**
98 	Holds a DDOC comment and formats it sectionwise as HTML.
99 */
100 class DdocComment {
101 	private {
102 		Section[] m_sections;
103 		string[string] m_macros;
104 		bool m_isDitto = false;
105 		bool m_isPrivate = false;
106 	}
107 
108 	this(string text)
109 	{
110 
111 		if (text.strip.icmp("ditto") == 0) { m_isDitto = true; return; }
112 		if (text.strip.icmp("private") == 0) { m_isPrivate = true; return; }
113 
114 
115 //		parseMacros(m_macros, context.defaultMacroDefinitions);
116 
117 		auto lines = splitLines(text);
118 		if( !lines.length ) return;
119 
120 		int getLineType(int i)
121 		{
122 			auto ln = strip(lines[i]);
123 			if( ln.length == 0 ) return BLANK;
124 			else if( ln.length >= 3 && ln.allOf("-") ) return CODE;
125 			else if( ln.indexOf(':') > 0 && isIdent(ln[0 .. ln.indexOf(':')]) ) return SECTION;
126 			return TEXT;
127 		}
128 
129 		int skipCodeBlock(int start)
130 		{
131 			do {
132 				start++;
133 			} while(start < lines.length && getLineType(start) != CODE);
134 			return start+1;
135 		}
136 
137 		int skipSection(int start)
138 		{
139 			while(start < lines.length ){
140 				if( getLineType(start) == SECTION ) break;
141 				if( getLineType(start) == CODE )
142 					start = skipCodeBlock(start);
143 				else start++;
144 			}
145 			return start;
146 		}
147 
148 		int skipBlock(int start)
149 		{
150 			do {
151 				start++;
152 			} while(start < lines.length && getLineType(start) == TEXT);
153 			return start;
154 		}
155 
156 
157 		int i = 0;
158 
159 		// special case short description on the first line
160 		while( i < lines.length && getLineType(i) == BLANK ) i++;
161 		if( i < lines.length && getLineType(i) == TEXT ){
162 			auto j = skipBlock(i);
163 			m_sections ~= Section("$Short", lines[i .. j]);
164 			i = j;
165 		}
166 
167 		// first section is implicitly the long description
168 		{
169 			auto j = skipSection(i);
170 			if( j > i ){
171 				m_sections ~= Section("$Long", lines[i .. j]);
172 				i = j;
173 			}
174 		}
175 
176 		// parse all other sections
177 		while( i < lines.length ){
178 			assert(getLineType(i) == SECTION);
179 			auto j = skipSection(i+1);
180 			auto pidx = lines[i].indexOf(':');
181 			auto sect = strip(lines[i][0 .. pidx]);
182 			lines[i] = stripLeftDD(lines[i][pidx+1 .. $]);
183 			if( lines[i].empty ) i++;
184 			if( sect == "Macros" ) parseMacros(m_macros, lines[i .. j]);
185 			else {
186 				m_sections ~= Section(sect, lines[i .. j]);
187 			}
188 			i = j;
189 		}
190 
191 //		parseMacros(m_macros, context.overrideMacroDefinitions);
192 	}
193 
194 	@property bool isDitto() const { return m_isDitto; }
195 	@property bool isPrivate() const { return m_isPrivate; }
196 
197 	bool hasSection(string name) const { return m_sections.canFind!(s => s.name == name); }
198 
199 	void renderSectionR(R)(ref R dst, DdocContext context, string name, int hlevel = 2)
200 	{
201 		foreach (s; m_sections)
202 			if (s.name == name)
203 				parseSection(dst, name, s.lines, context, hlevel, m_macros);
204 	}
205 
206 	void renderSectionsR(R)(ref R dst, DdocContext context, bool delegate(string) display_section, int hlevel)
207 	{
208 		foreach (s; m_sections) {
209 			if (display_section && !display_section(s.name)) continue;
210 			parseSection(dst, s.name, s.lines, context, hlevel, m_macros);
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 
230 /**
231 	Provides context information about the documented element.
232 */
233 interface DdocContext {
234 	/// A line array with macro definitions
235 	@property string[] defaultMacroDefinitions();
236 
237 	/// Line array with macro definitions that take precedence over local macros
238 	@property string[] overrideMacroDefinitions();
239 
240 	/// Looks up a symbol in the scope of the documented element and returns a link to it.
241 	string lookupScopeSymbolLink(string name);
242 }
243 
244 
245 private class BareContext : DdocContext {
246 	@property string[] defaultMacroDefinitions() { return null; }
247 	@property string[] overrideMacroDefinitions() { return null; }
248 	string lookupScopeSymbolLink(string name) { return null; }
249 }
250 
251 private enum {
252 	BLANK,
253 	TEXT,
254 	CODE,
255 	SECTION
256 }
257 
258 private struct Section {
259 	string name;
260 	string[] lines;
261 
262 	this(string name, string[] lines...)
263 	{
264 		this.name = name;
265 		this.lines = lines;
266 	}
267 }
268 
269 private {
270 	string[string] s_standardMacros;
271 	string[string] s_defaultMacros;
272 	string[string] s_overrideMacros;
273 }
274 
275 /// private
276 private void parseSection(R)(ref R dst, string sect, string[] lines, DdocContext context, int hlevel, string[string] macros)
277 {
278 	if( sect == "$Short" ) hlevel = -1;
279 
280 	void putHeader(string hdr){
281 		if( hlevel <= 0 ) return;
282 		dst.put("<section>");
283 		if( sect.length > 0 && sect[0] != '$' ){
284 			dst.put("<h"~to!string(hlevel)~">");
285 			foreach( ch; hdr ) dst.put(ch == '_' ? ' ' : ch);
286 			dst.put("</h"~to!string(hlevel)~">\n");
287 		}
288 	}
289 
290 	void putFooter(){
291 		if( hlevel <= 0 ) return;
292 		dst.put("</section>\n");
293 	}
294 
295 	int getLineType(int i)
296 	{
297 		auto ln = strip(lines[i]);
298 		if( ln.length == 0 ) return BLANK;
299 		else if( ln.length >= 3 &&ln.allOf("-") ) return CODE;
300 		else if( ln.indexOf(':') > 0 && !ln[0 .. ln.indexOf(':')].anyOf(" \t") ) return SECTION;
301 		return TEXT;
302 	}
303 
304 	int skipBlock(int start)
305 	{
306 		do {
307 			start++;
308 		} while(start < lines.length && getLineType(start) == TEXT);
309 		return start;
310 	}
311 
312 	// run all macros first
313 	{
314 		//logTrace("MACROS for section %s: %s", sect, macros.keys);
315 		auto tmpdst = appender!string();
316 		auto text = lines.join("\n");
317 		renderMacros(tmpdst, text, context, macros);
318 		lines = splitLines(tmpdst.data);
319 	}
320 
321 	int skipCodeBlock(int start)
322 	{
323 		do {
324 			start++;
325 		} while(start < lines.length && getLineType(start) != CODE);
326 		return start;
327 	}
328 
329 	switch( sect ){
330 		default:
331 			putHeader(sect);
332 			int i = 0;
333 			while( i < lines.length ){
334 				int lntype = getLineType(i);
335 
336 				switch( lntype ){
337 					default: assert(false, "Unexpected line type "~to!string(lntype)~": "~lines[i]);
338 					case BLANK:
339 						dst.put('\n');
340 						i++;
341 						continue;
342 					case SECTION:
343 					case TEXT:
344 						if( hlevel >= 0 ) dst.put("<p>");
345 						auto j = skipBlock(i);
346 						bool first = true;
347 						renderTextLine(dst, lines[i .. j].join("\n"), context);
348 						dst.put('\n');
349 						if( hlevel >= 0 ) dst.put("</p>\n");
350 						i = j;
351 						break;
352 					case CODE:
353 						dst.put("<pre class=\"code prettyprint lang-d\">");
354 						auto j = skipCodeBlock(i);
355 						auto base_indent = baseIndent(lines[i+1 .. j]);
356 						foreach( ln; lines[i+1 .. j] ){
357 							renderCodeLine(dst, ln.unindent(base_indent), context);
358 							dst.put('\n');
359 						}
360 						dst.put("</pre>\n");
361 						i = j+1;
362 						break;
363 				}
364 			}
365 			putFooter();
366 			break;
367 		case "Params":
368 			putHeader("Parameters");
369 			dst.put("<table><col class=\"caption\"><tr><th>Name</th><th>Description</th></tr>\n");
370 			bool in_parameter = false;
371 			string desc;
372 			foreach( string ln; lines ){
373 				// check if the line starts a parameter documentation
374 				string name;
375 				auto eidx = ln.indexOf("=");
376 				if( eidx > 0 ) name = ln[0 .. eidx].strip();
377 				if( !isIdent(name) ) name = null;
378 
379 				// if it does, start a new row
380 				if( name.length ){
381 					if( in_parameter ){
382 						renderTextLine(dst, desc, context);
383 						dst.put("</td></tr>\n");
384 					}
385 
386 					dst.put("<tr><td id=\"");
387 					dst.put(name);
388 					dst.put("\">");
389 					dst.put(name);
390 					dst.put("</td><td>");
391 
392 					desc = ln[eidx+1 .. $];
393 					in_parameter = true;
394 				} else if( in_parameter ) desc ~= "\n" ~ ln;
395 			}
396 
397 			if( in_parameter ){
398 				renderTextLine(dst, desc, context);
399 				dst.put("</td></tr>\n");
400 			}
401 
402 			dst.put("</table>\n");
403 			putFooter();
404 			break;
405 	}
406 
407 }
408 
409 /// private
410 private void renderTextLine(R)(ref R dst, string line, DdocContext context)
411 {
412 	while( line.length > 0 ){
413 		switch( line[0] ){
414 			default:
415 				dst.put(line[0]);
416 				line = line[1 .. $];
417 				break;
418 			case '<':
419 				dst.put(skipHtmlTag(line));
420 				break;
421 			case '>':
422 				dst.put("&gt;");
423 				line.popFront();
424 				break;
425 			case '&':
426 				if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&');
427 				else dst.put("&amp;");
428 				line.popFront();
429 				break;
430 			case '_':
431 				line = line[1 .. $];
432 				auto ident = skipIdent(line);
433 				if( ident.length ) dst.put(ident);
434 				else dst.put('_');
435 				break;
436 			case 'a': .. case 'z':
437 			case 'A': .. case 'Z':
438 				assert(line[0] >= 'a' && line[0] <= 'z' || line[0] >= 'A' && line[0] <= 'Z');
439 
440 				auto url = skipUrl(line);
441 				if( url.length ){
442 					/*dst.put("<a href=\"");
443 					dst.put(url);
444 					dst.put("\">");*/
445 					dst.put(url);
446 					//dst.put("</a>");
447 					break;
448 				}
449 
450 				auto ident = skipIdent(line);
451 				auto link = context.lookupScopeSymbolLink(ident);
452 				if( link.length ){
453 					if( link != "#" ){
454 						dst.put("<a href=\"");
455 						dst.put(link);
456 						dst.put("\">");
457 					}
458 					dst.put("<code class=\"prettyprint lang-d\">");
459 					dst.put(ident);
460 					dst.put("</code>");
461 					if( link != "#" ) dst.put("</a>");
462 				} else dst.put(ident.replace("._", "."));
463 				break;
464 		}
465 	}
466 }
467 
468 /// private
469 private void renderCodeLine(R)(ref R dst, string line, DdocContext context)
470 {
471 	while( line.length > 0 ){
472 		switch( line[0] ){
473 			default:
474 				dst.put(line[0]);
475 				line = line[1 .. $];
476 				break;
477 			case 'a': .. case 'z':
478 			case 'A': .. case 'Z':
479 				assert(line[0] >= 'a' && line[0] <= 'z' || line[0] >= 'A' && line[0] <= 'Z');
480 				auto ident = skipIdent(line);
481 				auto link = context.lookupScopeSymbolLink(ident);
482 				if( link.length && link != "#" ){
483 					dst.put("<a href=\"");
484 					dst.put(link);
485 					dst.put("\">");
486 					dst.put(ident);
487 					dst.put("</a>");
488 				} else dst.put(ident);
489 				break;
490 		}
491 	}
492 }
493 
494 /// private
495 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null)
496 {
497 	while( !line.empty ){
498 		auto idx = line.indexOf('$');
499 		if( idx < 0 ){
500 			dst.put(line);
501 			return;
502 		}
503 		dst.put(line[0 .. idx]);
504 		line = line[idx .. $];
505 		renderMacro(dst, line, context, macros, params);
506 	}
507 }
508 
509 /// private
510 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params = null)
511 {
512 	assert(line[0] == '$');
513 	line = line[1 .. $];
514 	if( line.length < 1) return;
515 
516 	if( line[0] >= '0' && line[0] <= '9' ){
517 		int pidx = line[0]-'0';
518 		if( pidx < params.length )
519 			dst.put(params[pidx]);
520 		line = line[1 .. $];
521 	} else if( line[0] == '+' ){
522 		if( params.length ){
523 			auto idx = params[0].indexOf(',');
524 			if( idx >= 0 ) dst.put(params[0][idx+1 .. $].stripLeftDD());
525 		}
526 		line = line[1 .. $];
527 	} else if( line[0] == '(' ){
528 		line = line[1 .. $];
529 		int l = 1;
530 		size_t cidx = 0;
531 		for( cidx = 0; cidx < line.length && l > 0; cidx++ ){
532 			if( line[cidx] == '(' ) l++;
533 			else if( line[cidx] == ')' ) l--;
534 		}
535 		if( l > 0 ){
536 			logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]);
537 			return;
538 		}
539 		if( cidx < 1 ){
540 			logDebug("Empty macro parens.");
541 			return;
542 		}
543 
544 		auto mnameidx = line[0 .. cidx-1].countUntilAny(" \t\r\n");
545 		if( mnameidx < 0 ) mnameidx = cidx-1;
546 		if( mnameidx == 0 ){
547 			logDebug("Macro call in DDOC comment is missing macro name.");
548 			return;
549 		}
550 		auto mname = line[0 .. mnameidx];
551 
552 		string[] args;
553 		if( mnameidx+1 < cidx ){
554 			auto rawargs = splitParams(line[mnameidx+1 .. cidx-1]);
555 			foreach( arg; rawargs ){
556 				auto argtext = appender!string();
557 				renderMacros(argtext, arg, context, macros, params);
558 				args ~= argtext.data();
559 			}
560 		}
561 		args = join(args, ",").stripLeftDD() ~ args.map!(s => s.stripLeftDD()).array;
562 
563 		logTrace("PARAMS for %s: %s", mname, args);
564 		line = line[cidx .. $];
565 
566 		auto pm = mname in s_overrideMacros;
567 		if( !pm ) pm = mname in macros;
568 		if( !pm ) pm = mname in s_defaultMacros;
569 		if( !pm ) pm = mname in s_standardMacros;
570 
571 		if( pm ){
572 			logTrace("MACRO %s: %s", mname, *pm);
573 			renderMacros(dst, *pm, context, macros, args);
574 		} else {
575 			logTrace("Macro '%s' not found.", mname);
576 			if( args.length ) dst.put(args[0]);
577 		}
578 	}
579 }
580 
581 private string[] splitParams(string ln)
582 {
583 	string[] ret;
584 	size_t i = 0, start = 0;
585 	while(i < ln.length){
586 		if( ln[i] == ',' ){
587 			ret ~= ln[start .. i];
588 			start = ++i;
589 		} else if( ln[i] == '(' ){
590 			i++;
591 			int l = 1;
592 			for( ; i < ln.length && l > 0; i++ ){
593 				if( ln[i] == '(' ) l++;
594 				else if( ln[i] == ')' ) l--;
595 			}
596 		} else i++;
597 	}
598 	if( i > start ) ret ~= ln[start .. i];
599 	return ret;
600 }
601 
602 private string skipHtmlTag(ref string ln)
603 {
604 	assert(ln[0] == '<');
605 
606 	// skip HTML comment
607 	if (ln.startsWith("<!--")) {
608 		auto idx = ln[4 .. $].indexOf("-->");
609 		if (idx < 0) {
610 			ln.popFront();
611 			return "&lt;";
612 		}
613 		auto ret = ln[0 .. idx+7];
614 		ln = ln[ret.length .. $];
615 		return ret;
616 	}
617 
618 	// too short for a tag
619 	if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) {
620 		// found no match, return escaped '<'
621 		logTrace("Found stray '<' in DDOC string.");
622 		ln.popFront();
623 		return "&lt;";
624 	}
625 
626 	// skip over regular start/end tag
627 	auto idx = ln.indexOf(">");
628 	if (idx < 0) {
629 		ln.popFront();
630 		return "<";
631 	}
632 	auto ret = ln[0 .. idx+1];
633 	ln = ln[ret.length .. $];
634 	return ret;
635 }
636 
637 private string skipUrl(ref string ln)
638 {
639 	if( !ln.startsWith("http://") && !ln.startsWith("http://") )
640 		return null;
641 
642 	bool saw_dot = false;
643 	size_t i = 7;
644 
645 	for_loop:
646 	while( i < ln.length ){
647 		switch( ln[i] ){
648 			default:
649 				break for_loop;
650 			case 'a': .. case 'z':
651 			case 'A': .. case 'Z':
652 			case '0': .. case '9':
653 			case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~':
654 				break;
655 			case '.':
656 				saw_dot = true;
657 				break;
658 		}
659 		i++;
660 	}
661 
662 	if( saw_dot ){
663 		auto ret = ln[0 .. i];
664 		ln = ln[i .. $];
665 		return ret;
666 	} else return null;
667 }
668 
669 private string skipIdent(ref string str)
670 {
671 	string strcopy = str;
672 
673 	bool last_was_ident = false;
674 	while( !str.empty ){
675 		auto ch = str.front;
676 
677 		if( last_was_ident ){
678 			// dots are allowed if surrounded by identifiers
679 			if( ch == '.' ) last_was_ident = false;
680 			else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break;
681 		} else {
682 			if( ch != '_' && !std.uni.isAlpha(ch) ) break;
683 			last_was_ident = true;
684 		}
685 		str.popFront();
686 	}
687 
688 	// if the identifier ended in a '.', remove it again
689 	if( str.length != strcopy.length && !last_was_ident )
690 		str = strcopy[strcopy.length-str.length-1 .. $];
691 	
692 	return strcopy[0 .. strcopy.length-str.length];
693 }
694 
695 private bool isIdent(string str)
696 {
697 	skipIdent(str);
698 	return str.length == 0;
699 }
700 
701 private void parseMacros(ref string[string] macros, in string[] lines)
702 {
703 	string name;
704 	foreach (string ln; lines) {
705 		// macro definitions are of the form IDENT = ...
706 		auto pidx = ln.indexOf('=');
707 		if( pidx > 0 ){
708 			auto tmpnam = ln[0 .. pidx].strip();
709 			if( isIdent(tmpnam) ){
710 				// got new macro definition
711 				name = tmpnam;
712 				macros[name] = stripLeftDD(ln[pidx+1 .. $]);
713 				continue;
714 			}
715 		}
716 
717 		// append to previous macro definition, if any
718 		if (name.length) macros[name] ~= "\n" ~ ln;
719 	}
720 }
721 
722 private int baseIndent(string[] lines)
723 {
724 	if( lines.length == 0 ) return 0;
725 	int ret = int.max;
726 	foreach( ln; lines ){
727 		int i = 0;
728 		while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') )
729 			i++;
730 		if( i < ln.length ) ret = min(ret, i); 
731 	}
732 	return ret;
733 }
734 
735 private string unindent(string ln, int amount)
736 {
737 	while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') )
738 		ln = ln[1 .. $], amount--;
739 	return ln;
740 }
741 
742 private string stripLeftDD(string s)
743 {
744 	while (!s.empty && (s.front == ' ' || s.front == '\t'))
745 		s.popFront();
746 	return s;
747 }
748 
749 
750 import std.stdio;
751 unittest {
752 	auto src = "$(M a b)\n$(M a\nb)\nMacros:\n	M =     -$0-\n";
753 	auto dst = "-a b-\n-a\nb-\n";
754 	assert(formatDdocComment(src) == dst);
755 }
756 
757 unittest {
758 	auto src = "\n  $(M a b)\n$(M a  \nb)\nMacros:\n	M =     -$0-  \n\nN=$0";
759 	auto dst = "  -a b-  \n\n-a  \nb-  \n";
760 	assert(formatDdocComment(src) == dst);
761 }
762 
763 unittest {
764 	auto src = "$(M a, b)\n$(M a,\n    b)\nMacros:\n	M = -$1-\n\n	+$2+\n\n	N=$0";
765 	auto dst = "-a-\n\n	+b+\n\n-a-\n\n	+\n    b+\n";
766 	assert(formatDdocComment(src) == dst);
767 }
768 
769 unittest {
770 	auto src = "$(GLOSSARY a\nb)\nMacros:\n	GLOSSARY = $(LINK2 glossary.html#$0, $0)";
771 	auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n";
772 	assert(formatDdocComment(src) == dst);
773 }
774 
775 unittest {
776 	auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.";
777 	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";
778 	assert(formatDdocComment(src) == dst);
779 }
780 
781 unittest {
782 	auto src = "& &a &lt; &#lt; &- &03; &;";
783 	auto dst = "&amp; &a &lt; &#lt; &amp;- &amp;03; &amp;;\n";
784 	assert(formatDdocComment(src) == dst);
785 }
786 
787 unittest {
788 	auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = &lt;\nGT = &gt;";
789 	auto dst = "<a href=\"abc\">test &lt;peter@parker.com&gt;</a>\n";
790 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n"));
791 	assert(formatDdocComment(src) == dst);
792 }