1 /**
2 	DietDoc/DDOC support routines
3 
4 	Copyright: © 2012-2015 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;
18 import std.string;
19 import std.uni : isAlpha;
20 
21 // TODO: support escapes section
22 
23 
24 shared static this()
25 {
26 	s_standardMacros =
27 		[
28 		 `B`: `<b>$0</b>`,
29 		 `I`: `<i>$0</i>`,
30 		 `U`: `<u>$0</u>`,
31 		 `P` : `<p>$0</p>`,
32 		 `DL` : `<dl>$0</dl>`,
33 		 `DT` : `<dt>$0</dt>`,
34 		 `DD` : `<dd>$0</dd>`,
35 		 `TABLE` : `<table>$0</table>`,
36 		 `TR` : `<tr>$0</tr>`,
37 		 `TH` : `<th>$0</th>`,
38 		 `TD` : `<td>$0</td>`,
39 		 `OL` : `<ol>$0</ol>`,
40 		 `UL` : `<ul>$0</ul>`,
41 		 `LI` : `<li>$0</li>`,
42 		 `LINK` : `<a href="$0">$0</a>`,
43 		 `LINK2` : `<a href="$1">$+</a>`,
44 		 `LPAREN` : `(`,
45 		 `RPAREN` : `)`,
46 
47 		 `RED` :   `<font color=red>$0</font>`,
48 		 `BLUE` :  `<font color=blue>$0</font>`,
49 		 `GREEN` : `<font color=green>$0</font>`,
50 		 `YELLOW` : `<font color=yellow>$0</font>`,
51 		 `BLACK` : `<font color=black>$0</font>`,
52 		 `WHITE` : `<font color=white>$0</font>`,
53 
54 		 `D_CODE` : `<pre class="d_code">$0</pre>`,
55 		 `D_COMMENT` : `$(GREEN $0)`,
56 		 `D_STRING`  : `$(RED $0)`,
57 		 `D_KEYWORD` : `$(BLUE $0)`,
58 		 `D_PSYMBOL` : `$(U $0)`,
59 		 `D_PARAM` : `$(I $0)`,
60 		 `BACKTICK`: "`",
61 		 `DDOC_BACKQUOTED`: `$(D_INLINECODE $0)`,
62 		 //`D_INLINECODE`: `<pre style="display:inline;" class="d_inline_code">$0</pre>`,
63 		 `D_INLINECODE`: `<code class="lang-d">$0</code>`,
64 
65 		 `DDOC` : `<html>
66   <head>
67     <META http-equiv="content-type" content="text/html; charset=utf-8">
68     <title>$(TITLE)</title>
69   </head>
70   <body>
71   <h1>$(TITLE)</h1>
72   $(BODY)
73   </body>
74 </html>`,
75 
76 		 `DDOC_COMMENT` : `<!-- $0 -->`,
77 		 `DDOC_DECL` : `$(DT $(BIG $0))`,
78 		 `DDOC_DECL_DD` : `$(DD $0)`,
79 		 `DDOC_DITTO` : `$(BR)$0`,
80 		 `DDOC_SECTIONS` : `$0`,
81 		 `DDOC_SUMMARY` : `$0$(BR)$(BR)`,
82 		 `DDOC_DESCRIPTION` : `$0$(BR)$(BR)`,
83 		 `DDOC_AUTHORS` : "$(B Authors:)$(BR)\n$0$(BR)$(BR)",
84 		 `DDOC_BUGS` : "$(RED BUGS:)$(BR)\n$0$(BR)$(BR)",
85 		 `DDOC_COPYRIGHT` : "$(B Copyright:)$(BR)\n$0$(BR)$(BR)",
86 		 `DDOC_DATE` : "$(B Date:)$(BR)\n$0$(BR)$(BR)",
87 		 `DDOC_DEPRECATED` : "$(RED Deprecated:)$(BR)\n$0$(BR)$(BR)",
88 		 `DDOC_EXAMPLES` : "$(B Examples:)$(BR)\n$0$(BR)$(BR)",
89 		 `DDOC_HISTORY` : "$(B History:)$(BR)\n$0$(BR)$(BR)",
90 		 `DDOC_LICENSE` : "$(B License:)$(BR)\n$0$(BR)$(BR)",
91 		 `DDOC_RETURNS` : "$(B Returns:)$(BR)\n$0$(BR)$(BR)",
92 		 `DDOC_SEE_ALSO` : "$(B See Also:)$(BR)\n$0$(BR)$(BR)",
93 		 `DDOC_STANDARDS` : "$(B Standards:)$(BR)\n$0$(BR)$(BR)",
94 		 `DDOC_THROWS` : "$(B Throws:)$(BR)\n$0$(BR)$(BR)",
95 		 `DDOC_VERSION` : "$(B Version:)$(BR)\n$0$(BR)$(BR)",
96 		 `DDOC_SECTION_H` : `$(B $0)$(BR)$(BR)`,
97 		 `DDOC_SECTION` : `$0$(BR)$(BR)`,
98 		 `DDOC_MEMBERS` : `$(DL $0)`,
99 		 `DDOC_MODULE_MEMBERS` : `$(DDOC_MEMBERS $0)`,
100 		 `DDOC_CLASS_MEMBERS` : `$(DDOC_MEMBERS $0)`,
101 		 `DDOC_STRUCT_MEMBERS` : `$(DDOC_MEMBERS $0)`,
102 		 `DDOC_ENUM_MEMBERS` : `$(DDOC_MEMBERS $0)`,
103 		 `DDOC_TEMPLATE_MEMBERS` : `$(DDOC_MEMBERS $0)`,
104 		 `DDOC_PARAMS` : "$(B Params:)$(BR)\n$(TABLE $0)$(BR)",
105 		 `DDOC_PARAM_ROW` : `$(TR $0)`,
106 		 `DDOC_PARAM_ID` : `$(TD $0)`,
107 		 `DDOC_PARAM_DESC` : `$(TD $0)`,
108 		 `DDOC_BLANKLINE` : `$(BR)$(BR)`,
109 
110 		 `DDOC_ANCHOR` : `<a name="$1"></a>`,
111 		 `DDOC_PSYMBOL` : `$(U $0)`,
112 		 `DDOC_KEYWORD` : `$(B $0)`,
113 		 `DDOC_PARAM` : `$(I $0)`,
114 		 ];
115 	import std.datetime : Clock;
116 	auto now = Clock.currTime();
117 	s_standardMacros["DATETIME"] = "%s %s %s %s:%s:%s %s".format(
118 		now.dayOfWeek.to!string.capitalize, now.month.to!string.capitalize,
119 		now.day, now.hour, now.minute, now.second, now.year);
120 	s_standardMacros["YEAR"] = now.year.to!string;
121 }
122 
123 
124 /**
125 	Takes a DDOC string and outputs formatted HTML.
126 
127 	The hlevel parameter specifies the header level used for section names (&lt;h2&gt by default).
128 	By specifying a display_section callback it is also possible to output only certain sections.
129 */
130 string formatDdocComment(string ddoc_, int hlevel = 2, bool delegate(string) display_section = null)
131 {
132 	return formatDdocComment(ddoc_, new BareContext, hlevel, display_section);
133 }
134 /// ditto
135 string formatDdocComment(string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null)
136 {
137 	auto dst = appender!string();
138 	filterDdocComment(dst, text, context, hlevel, display_section);
139 	return dst.data;
140 }
141 /// ditto
142 void filterDdocComment(R)(ref R dst, string text, DdocContext context, int hlevel = 2, bool delegate(string) display_section = null)
143 {
144 	auto comment = new DdocComment(text);
145 	comment.renderSectionsR(dst, context, display_section, hlevel);
146 }
147 
148 
149 /**
150 	Sets a set of macros that will be available to all calls to formatDdocComment.
151 */
152 void setDefaultDdocMacroFiles(string[] filenames)
153 {
154 	import vibe.core.file;
155 	import vibe.stream.operations;
156 	s_defaultMacros = null;
157 	foreach (filename; filenames) {
158 		auto text = readAllUTF8(openFile(filename));
159 		parseMacros(s_defaultMacros, splitLines(text));
160 	}
161 }
162 
163 
164 /**
165 	Sets a set of macros that will be available to all calls to formatDdocComment and override local macro definitions.
166 */
167 void setOverrideDdocMacroFiles(string[] filenames)
168 {
169 	import vibe.core.file;
170 	import vibe.stream.operations;
171 	s_overrideMacros = null;
172 	foreach (filename; filenames) {
173 		auto text = readAllUTF8(openFile(filename));
174 		parseMacros(s_overrideMacros, splitLines(text));
175 	}
176 }
177 
178 
179 /**
180    Enable hyphenation of doc text.
181 */
182 void enableHyphenation()
183 {
184 	s_hyphenator = Hyphenator(import("hyphen.tex")); // en-US
185 	s_enableHyphenation = true;
186 }
187 
188 
189 void hyphenate(R)(in char[] word, R orng)
190 {
191 	s_hyphenator.hyphenate(word, "\&shy;", s => orng.put(s));
192 }
193 
194 /**
195 	Holds a DDOC comment and formats it sectionwise as HTML.
196 */
197 class DdocComment {
198 	private {
199 		Section[] m_sections;
200 		string[string] m_macros;
201 		bool m_isDitto = false;
202 		bool m_isPrivate = false;
203 	}
204 
205 	this(string text)
206 	{
207 
208 		if (text.strip.icmp("ditto") == 0) { m_isDitto = true; return; }
209 		if (text.strip.icmp("private") == 0) { m_isPrivate = true; return; }
210 
211 
212 //		parseMacros(m_macros, context.defaultMacroDefinitions);
213 
214 		auto lines = splitLines(text);
215 		if( !lines.length ) return;
216 
217 		int getLineType(int i)
218 		{
219 			auto ln = strip(lines[i]);
220 			if( ln.length == 0 ) return BLANK;
221 			else if( ln.length >= 3 && ln.allOf("-") ) return CODE;
222 			else if( ln.indexOf(':') > 0 && isIdent(ln[0 .. ln.indexOf(':')]) ) return SECTION;
223 			return TEXT;
224 		}
225 
226 		int skipCodeBlock(int start)
227 		{
228 			do {
229 				start++;
230 			} while(start < lines.length && getLineType(start) != CODE);
231 			if (start >= lines.length) return start; // unterminated code section
232 			return start+1;
233 		}
234 
235 		int skipSection(int start)
236 		{
237 			while (start < lines.length) {
238 				if (getLineType(start) == SECTION) break;
239 				if (getLineType(start) == CODE)
240 					start = skipCodeBlock(start);
241 				else start++;
242 			}
243 			return start;
244 		}
245 
246 		int skipBlock(int start)
247 		{
248 			do {
249 				start++;
250 			} while(start < lines.length && getLineType(start) == TEXT);
251 			return start;
252 		}
253 
254 
255 		int i = 0;
256 
257 		// special case short description on the first line
258 		while( i < lines.length && getLineType(i) == BLANK ) i++;
259 		if( i < lines.length && getLineType(i) == TEXT ){
260 			auto j = skipBlock(i);
261 			m_sections ~= Section("$Short", lines[i .. j]);
262 			i = j;
263 		}
264 
265 		// first section is implicitly the long description
266 		{
267 			auto j = skipSection(i);
268 			if( j > i ){
269 				m_sections ~= Section("$Long", lines[i .. j]);
270 				i = j;
271 			}
272 		}
273 
274 		// parse all other sections
275 		while( i < lines.length ){
276 			assert(getLineType(i) == SECTION);
277 			auto j = skipSection(i+1);
278 			assert(j <= lines.length);
279 			auto pidx = lines[i].indexOf(':');
280 			auto sect = strip(lines[i][0 .. pidx]);
281 			lines[i] = stripLeftDD(lines[i][pidx+1 .. $]);
282 			if (lines[i].empty && i < lines.length) i++;
283 			if (sect == "Macros") parseMacros(m_macros, lines[i .. j]);
284 			else {
285 				m_sections ~= Section(sect, lines[i .. j]);
286 			}
287 			i = j;
288 		}
289 
290 //		parseMacros(m_macros, context.overrideMacroDefinitions);
291 	}
292 
293 	@property bool isDitto() const { return m_isDitto; }
294 	@property bool isPrivate() const { return m_isPrivate; }
295 
296 	bool hasSection(string name) const { return m_sections.canFind!(s => s.name == name); }
297 
298 	void renderSectionR(R)(ref R dst, DdocContext context, string name, int hlevel = 2)
299 	{
300 		foreach (s; m_sections)
301 			if (s.name == name)
302 				parseSection(dst, name, s.lines, context, hlevel, m_macros);
303 	}
304 
305 	void renderSectionsR(R)(ref R dst, DdocContext context, bool delegate(string) display_section, int hlevel)
306 	{
307 		foreach (s; m_sections) {
308 			if (display_section && !display_section(s.name)) continue;
309 			parseSection(dst, s.name, s.lines, context, hlevel, m_macros);
310 		}
311 	}
312 
313 	string renderSection(DdocContext context, string name, int hlevel = 2)
314 	{
315 		auto dst = appender!string();
316 		renderSectionR(dst, context, name, hlevel);
317 		return dst.data;
318 	}
319 
320 	string renderSections(DdocContext context, bool delegate(string) display_section, int hlevel)
321 	{
322 		auto dst = appender!string();
323 		renderSectionsR(dst, context, display_section, hlevel);
324 		return dst.data;
325 	}
326 }
327 
328 
329 /**
330 	Provides context information about the documented element.
331 */
332 interface DdocContext {
333 	/// A line array with macro definitions
334 	@property string[] defaultMacroDefinitions();
335 
336 	/// Line array with macro definitions that take precedence over local macros
337 	@property string[] overrideMacroDefinitions();
338 
339 	/// Looks up a symbol in the scope of the documented element and returns a link to it.
340 	string lookupScopeSymbolLink(string name);
341 }
342 
343 
344 private class BareContext : DdocContext {
345 	@property string[] defaultMacroDefinitions() { return null; }
346 	@property string[] overrideMacroDefinitions() { return null; }
347 	string lookupScopeSymbolLink(string name) { return null; }
348 }
349 
350 private enum {
351 	BLANK,
352 	TEXT,
353 	CODE,
354 	SECTION
355 }
356 
357 private struct Section {
358 	string name;
359 	string[] lines;
360 
361 	this(string name, string[] lines...)
362 	{
363 		this.name = name;
364 		this.lines = lines;
365 	}
366 }
367 
368 private {
369 	immutable string[string] s_standardMacros;
370 	string[string] s_defaultMacros;
371 	string[string] s_overrideMacros;
372 	bool s_enableHyphenation;
373 	Hyphenator s_hyphenator;
374 }
375 
376 /// private
377 private void parseSection(R)(ref R dst, string sect, string[] lines, DdocContext context, int hlevel, string[string] macros)
378 {
379 	if( sect == "$Short" ) hlevel = -1;
380 
381 	void putHeader(string hdr){
382 		if( hlevel <= 0 ) return;
383 		dst.put("<section>");
384 		if( sect.length > 0 && sect[0] != '$' ){
385 			dst.put("<h"~to!string(hlevel)~">");
386 			foreach( ch; hdr ) dst.put(ch == '_' ? ' ' : ch);
387 			dst.put("</h"~to!string(hlevel)~">\n");
388 		}
389 	}
390 
391 	void putFooter(){
392 		if( hlevel <= 0 ) return;
393 		dst.put("</section>\n");
394 	}
395 
396 	int getLineType(int i)
397 	{
398 		auto ln = strip(lines[i]);
399 		if( ln.length == 0 ) return BLANK;
400 		else if( ln.length >= 3 &&ln.allOf("-") ) return CODE;
401 		else if( ln.indexOf(':') > 0 && !ln[0 .. ln.indexOf(':')].anyOf(" \t") ) return SECTION;
402 		return TEXT;
403 	}
404 
405 	int skipBlock(int start)
406 	{
407 		do {
408 			start++;
409 		} while(start < lines.length && getLineType(start) == TEXT);
410 		return start;
411 	}
412 
413 	int skipCodeBlock(int start)
414 	{
415 		do {
416 			start++;
417 		} while(start < lines.length && getLineType(start) != CODE);
418 		return start;
419 	}
420 
421 	// handle backtick inline-code
422 	for (int i = 0; i < lines.length; i++) {
423 		int lntype = getLineType(i);
424 		if (lntype == CODE) i = skipCodeBlock(i);
425 		else lines[i] = replaceBacktickCode(lines[i]);
426 	}
427 	lines = renderMacros(lines.join("\n").stripDD, context, macros).splitLines();
428 
429 	switch( sect ){
430 		default:
431 			putHeader(sect);
432 			int i = 0;
433 			while( i < lines.length ){
434 				int lntype = getLineType(i);
435 
436 				switch( lntype ){
437 					default: assert(false, "Unexpected line type "~to!string(lntype)~": "~lines[i]);
438 					case BLANK:
439 						dst.put('\n');
440 						i++;
441 						continue;
442 					case SECTION:
443 					case TEXT:
444 						if( hlevel >= 0 ) dst.put("<p>");
445 						auto j = skipBlock(i);
446 						bool first = true;
447 						renderTextLine(dst, lines[i .. j].join("\n")/*.stripDD*/, context);
448 						dst.put('\n');
449 						if( hlevel >= 0 ) dst.put("</p>\n");
450 						i = j;
451 						break;
452 					case CODE:
453 						dst.put("<pre class=\"code\"><code class=\"lang-d\">");
454 						auto j = skipCodeBlock(i);
455 						auto base_indent = baseIndent(lines[i+1 .. j]);
456 						renderCodeLine(dst, lines[i+1 .. j].map!(ln => ln.unindent(base_indent)).join("\n"), context);
457 						dst.put("</code></pre>\n");
458 						i = j+1;
459 						break;
460 				}
461 			}
462 			putFooter();
463 			break;
464 		case "Params":
465 			putHeader("Parameters");
466 			dst.put("<table><col class=\"caption\"><tr><th>Name</th><th>Description</th></tr>\n");
467 			bool in_parameter = false;
468 			string desc;
469 			foreach( string ln; lines ){
470 				// check if the line starts a parameter documentation
471 				string name;
472 				auto eidx = ln.indexOf("=");
473 				if( eidx > 0 ) name = ln[0 .. eidx].strip();
474 				if( !isIdent(name) ) name = null;
475 
476 				// if it does, start a new row
477 				if( name.length ){
478 					if( in_parameter ){
479 						renderTextLine(dst, desc, context);
480 						dst.put("</td></tr>\n");
481 					}
482 
483 					dst.put("<tr><td id=\"");
484 					dst.put(name);
485 					dst.put("\">");
486 					dst.put(name);
487 					dst.put("</td><td>");
488 
489 					desc = ln[eidx+1 .. $];
490 					in_parameter = true;
491 				} else if( in_parameter ) desc ~= "\n" ~ ln;
492 			}
493 
494 			if( in_parameter ){
495 				renderTextLine(dst, desc, context);
496 				dst.put("</td></tr>\n");
497 			}
498 
499 			dst.put("</table>\n");
500 			putFooter();
501 			break;
502 	}
503 
504 }
505 
506 /// private
507 private void renderTextLine(R)(ref R dst, string line, DdocContext context)
508 {
509 	size_t inCode;
510 	while( line.length > 0 ){
511 		switch( line[0] ){
512 			default:
513 				dst.put(line[0]);
514 				line = line[1 .. $];
515 				break;
516 			case '<':
517 				auto res = skipHtmlTag(line);
518 				if (res.startsWith("<code"))
519 					++inCode;
520 				else if (res == "</code>")
521 					--inCode;
522 				dst.put(res);
523 				break;
524 			case '>':
525 				dst.put("&gt;");
526 				line.popFront();
527 				break;
528 			case '&':
529 				if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&');
530 				else dst.put("&amp;");
531 				line.popFront();
532 				break;
533 			case '_':
534 				line = line[1 .. $];
535 				auto ident = skipIdent(line);
536 				if( ident.length )
537 				{
538 					if (s_enableHyphenation && !inCode)
539 						hyphenate(ident, dst);
540 					else
541 						dst.put(ident);
542 				}
543 				else dst.put('_');
544 				break;
545 			case '.':
546 				if (line.length > 1 && (line[1].isAlpha || line[1] == '_')) goto case;
547 				else goto default;
548 			case 'a': .. case 'z':
549 			case 'A': .. case 'Z':
550 
551 				auto url = skipUrl(line);
552 				if( url.length ){
553 					/*dst.put("<a href=\"");
554 					dst.put(url);
555 					dst.put("\">");*/
556 					dst.put(url);
557 					//dst.put("</a>");
558 					break;
559 				}
560 
561 				auto ident = skipIdent(line);
562 				auto link = context.lookupScopeSymbolLink(ident);
563 				if (link.length) {
564 					import ddox.highlight : highlightDCode;
565 					if( link != "#" ){
566 						dst.put("<a href=\"");
567 						dst.put(link);
568 						dst.put("\">");
569 					}
570 					if (!inCode) dst.put("<code class=\"lang-d\">");
571 					dst.highlightDCode(ident, null);
572 					if (!inCode) dst.put("</code>");
573 					if( link != "#" ) dst.put("</a>");
574 				} else {
575 					ident = ident.replace("._", ".");
576 					if (s_enableHyphenation && !inCode)
577 						hyphenate(ident, dst);
578 					else
579 						dst.put(ident);
580 				}
581 				break;
582 		}
583 	}
584 }
585 
586 /// private
587 private void renderCodeLine(R)(ref R dst, string line, DdocContext context)
588 {
589 	import ddox.highlight : highlightDCode;
590 	dst.highlightDCode(line, (string ident, scope void delegate() insert_ident) {
591 		auto link = context.lookupScopeSymbolLink(ident);
592 		if (link.length && link != "#") {
593 			dst.put("<a href=\"");
594 			dst.put(link);
595 			dst.put("\">");
596 			insert_ident();
597 			dst.put("</a>");
598 		} else insert_ident();
599 	});
600 }
601 
602 /// private
603 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null)
604 {
605 	while( !line.empty ){
606 		auto idx = line.indexOf('$');
607 		if( idx < 0 ){
608 			dst.put(line);
609 			return;
610 		}
611 		dst.put(line[0 .. idx]);
612 		line = line[idx .. $];
613 		renderMacro(dst, line, context, macros, params, callstack);
614 	}
615 }
616 
617 /// private
618 private string renderMacros(string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null)
619 {
620 	auto app = appender!string;
621 	renderMacros(app, line, context, macros, params, callstack);
622 	return app.data;
623 }
624 
625 /// private
626 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params, MacroInvocation[] callstack)
627 {
628 	assert(line[0] == '$');
629 	line = line[1 .. $];
630 	if( line.length < 1) {
631 		dst.put("$");
632 		return;
633 	}
634 
635 	if( line[0] >= '0' && line[0] <= '9' ){
636 		int pidx = line[0]-'0';
637 		if( pidx < params.length )
638 			dst.put(params[pidx]);
639 		line = line[1 .. $];
640 	} else if( line[0] == '+' ){
641 		if( params.length ){
642 			auto idx = params[0].indexOf(',');
643 			if( idx >= 0 ) dst.put(params[0][idx+1 .. $].specialStrip());
644 		}
645 		line = line[1 .. $];
646 	} else if( line[0] == '(' ){
647 		line = line[1 .. $];
648 		int l = 1;
649 		size_t cidx = 0;
650 		for( cidx = 0; cidx < line.length && l > 0; cidx++ ){
651 			if( line[cidx] == '(' ) l++;
652 			else if( line[cidx] == ')' ) l--;
653 		}
654 		if( l > 0 ){
655 			logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]);
656 			dst.put("(");
657 			return;
658 		}
659 		if( cidx < 1 ){
660 			logDebug("Empty macro parens.");
661 			return;
662 		}
663 
664 		auto mnameidx = line[0 .. cidx-1].countUntilAny(", \t\r\n");
665 		if( mnameidx < 0 ) mnameidx = cidx-1;
666 		if( mnameidx == 0 ){
667 			logDebug("Macro call in DDOC comment is missing macro name.");
668 			return;
669 		}
670 
671 		auto mname = line[0 .. mnameidx];
672 		string rawargtext = line[mnameidx .. cidx-1];
673 
674 		string[] args;
675 		if (rawargtext.length) {
676 			auto rawargs = splitParams(rawargtext);
677 			foreach( arg; rawargs ){
678 				auto argtext = appender!string();
679 				renderMacros(argtext, arg, context, macros, params, callstack);
680 				auto newargs = splitParams(argtext.data);
681 				if (newargs.length == 0) args ~= ""; // always add at least one argument per raw argument
682 				else args ~= newargs;
683 			}
684 		}
685 		if (args.length == 1 && args[0].specialStrip.length == 0) args = null; // remove a single empty argument
686 
687 		args = join(args, ",").specialStrip() ~ args.map!(a => a.specialStrip).array;
688 
689 		logTrace("PARAMS for %s: %s", mname, args);
690 		line = line[cidx .. $];
691 
692 		// check for recursion termination conditions
693 		foreach_reverse (ref c; callstack) {
694 			if (c.name == mname && (args.length <= 1 || args == c.params)) {
695 				logTrace("Terminating recursive macro call of %s: %s", mname, params.length <= 1 ? "no argument text" : "same arguments as previous invocation");
696 				//line = line[cidx .. $];
697 				return;
698 			}
699 		}
700 		callstack.assumeSafeAppend();
701 		callstack ~= MacroInvocation(mname, args);
702 
703 
704 		const(string)* pm = mname in s_overrideMacros;
705 		if( !pm ) pm = mname in macros;
706 		if( !pm ) pm = mname in s_defaultMacros;
707 		if( !pm ) pm = mname in s_standardMacros;
708 
709 		if( pm ){
710 			logTrace("MACRO %s: %s", mname, *pm);
711 			renderMacros(dst, *pm, context, macros, args, callstack);
712 		} else {
713 			logTrace("Macro '%s' not found.", mname);
714 			if( args.length ) dst.put(args[0]);
715 		}
716 	} else dst.put("$");
717 }
718 
719 private struct MacroInvocation {
720 	string name;
721 	string[] params;
722 }
723 
724 private string[] splitParams(string ln)
725 {
726 	string[] ret;
727 	size_t i = 0, start = 0;
728 	while(i < ln.length){
729 		if( ln[i] == ',' ){
730 			ret ~= ln[start .. i];
731 			start = ++i;
732 		} else if( ln[i] == '(' ){
733 			i++;
734 			int l = 1;
735 			for( ; i < ln.length && l > 0; i++ ){
736 				if( ln[i] == '(' ) l++;
737 				else if( ln[i] == ')' ) l--;
738 			}
739 		} else i++;
740 	}
741 	if( i > start ) ret ~= ln[start .. i];
742 	return ret;
743 }
744 
745 private string replaceBacktickCode(string line)
746 {
747 	auto ret = appender!string;
748 
749 	while (line.length > 0) {
750 		auto idx = line.indexOf('`');
751 		if (idx < 0) break;
752 
753 		auto eidx = line[idx+1 .. $].indexOf('`');
754 		if (eidx < 0) break;
755 		eidx += idx+1;
756 
757 		ret.put(line[0 .. idx]);
758 		ret.put("$(DDOC_BACKQUOTED ");
759 		foreach (i; idx+1 .. eidx) {
760 			switch (line[i]) {
761 				default: ret.put(line[i]); break;
762 				case '&': ret.put("&amp;"); break;
763 				case '<': ret.put("&lt;"); break;
764 				case '>': ret.put("&gt;"); break;
765 				case '(': ret.put("$(LPAREN)"); break;
766 				case ')': ret.put("$(RPAREN)"); break;
767 			}
768 		}
769 		ret.put(")");
770 		line = line[eidx+1 .. $];
771 	}
772 
773 	if (ret.data.length == 0) return line;
774 	ret.put(line);
775 	return ret.data;
776 }
777 
778 private string skipHtmlTag(ref string ln)
779 {
780 	assert(ln[0] == '<');
781 
782 	// skip HTML comment
783 	if (ln.startsWith("<!--")) {
784 		auto idx = ln[4 .. $].indexOf("-->");
785 		if (idx < 0) {
786 			ln.popFront();
787 			return "&lt;";
788 		}
789 		auto ret = ln[0 .. idx+7];
790 		ln = ln[ret.length .. $];
791 		return ret;
792 	}
793 
794 	// too short for a tag
795 	if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) {
796 		// found no match, return escaped '<'
797 		logTrace("Found stray '<' in DDOC string.");
798 		ln.popFront();
799 		return "&lt;";
800 	}
801 
802 	// skip over regular start/end tag
803 	auto idx = ln.indexOf(">");
804 	if (idx < 0) {
805 		ln.popFront();
806 		return "<";
807 	}
808 	auto ret = ln[0 .. idx+1];
809 	ln = ln[ret.length .. $];
810 	return ret;
811 }
812 
813 private string skipUrl(ref string ln)
814 {
815 	if( !ln.startsWith("http://") && !ln.startsWith("http://") )
816 		return null;
817 
818 	bool saw_dot = false;
819 	size_t i = 7;
820 
821 	for_loop:
822 	while( i < ln.length ){
823 		switch( ln[i] ){
824 			default:
825 				break for_loop;
826 			case 'a': .. case 'z':
827 			case 'A': .. case 'Z':
828 			case '0': .. case '9':
829 			case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~':
830 				break;
831 			case '.':
832 				saw_dot = true;
833 				break;
834 		}
835 		i++;
836 	}
837 
838 	if( saw_dot ){
839 		auto ret = ln[0 .. i];
840 		ln = ln[i .. $];
841 		return ret;
842 	} else return null;
843 }
844 
845 private string skipIdent(ref string str)
846 {
847 	string strcopy = str;
848 
849 	if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_'))
850 		str.popFront();
851 
852 	bool last_was_ident = false;
853 	while( !str.empty ){
854 		auto ch = str.front;
855 
856 		if( last_was_ident ){
857 			// dots are allowed if surrounded by identifiers
858 			if( ch == '.' ) last_was_ident = false;
859 			else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break;
860 		} else {
861 			if( ch != '_' && !std.uni.isAlpha(ch) ) break;
862 			last_was_ident = true;
863 		}
864 		str.popFront();
865 	}
866 
867 	// if the identifier ended in a '.', remove it again
868 	if( str.length != strcopy.length && !last_was_ident )
869 		str = strcopy[strcopy.length-str.length-1 .. $];
870 
871 	return strcopy[0 .. strcopy.length-str.length];
872 }
873 
874 private bool isIdent(string str)
875 {
876 	skipIdent(str);
877 	return str.length == 0;
878 }
879 
880 private void parseMacros(ref string[string] macros, in string[] lines)
881 {
882 	string name;
883 	foreach (string ln; lines) {
884 		// macro definitions are of the form IDENT = ...
885 		auto pidx = ln.indexOf('=');
886 		if (pidx > 0) {
887 			auto tmpnam = ln[0 .. pidx].strip();
888 			// got new macro definition?
889 			if (isIdent(tmpnam)) {
890 
891 				// strip the previous macro
892 				if (name.length) macros[name] = macros[name].stripDD();
893 
894 				// start parsing the new macro
895 				name = tmpnam;
896 				macros[name] = stripLeftDD(ln[pidx+1 .. $]);
897 				continue;
898 			}
899 		}
900 
901 		// append to previous macro definition, if any
902 		macros[name] ~= "\n" ~ ln;
903 	}
904 }
905 
906 private int baseIndent(string[] lines)
907 {
908 	if( lines.length == 0 ) return 0;
909 	int ret = int.max;
910 	foreach( ln; lines ){
911 		int i = 0;
912 		while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') )
913 			i++;
914 		if( i < ln.length ) ret = min(ret, i);
915 	}
916 	return ret;
917 }
918 
919 private string unindent(string ln, int amount)
920 {
921 	while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') )
922 		ln = ln[1 .. $], amount--;
923 	return ln;
924 }
925 
926 private string stripLeftDD(string s)
927 {
928 	while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n'))
929 		s.popFront();
930 	return s;
931 }
932 
933 private string specialStrip(string s)
934 {
935 	import std.algorithm : among;
936 
937 	// strip trailing whitespace for all lines but the last
938 	size_t idx = 0;
939 	while (true) {
940 		auto nidx = s[idx .. $].indexOf('\n');
941 		if (nidx < 0) break;
942 		nidx += idx;
943 		auto strippedfront = s[0 .. nidx].stripRightDD();
944 		s = strippedfront ~ "\n" ~ s[nidx+1 .. $];
945 		idx = strippedfront.length + 1;
946 	}
947 
948 	// strip the first character, if whitespace
949 	if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront();
950 
951 	return s;
952 }
953 
954 private string stripRightDD(string s)
955 {
956 	while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n'))
957 		s.popBack();
958 	return s;
959 }
960 
961 private string stripDD(string s)
962 {
963 	return s.stripLeftDD.stripRightDD;
964 }
965 
966 import std.stdio;
967 unittest {
968 	auto src = "$(M a b)\n$(M a\nb)\nMacros:\n	M =     -$0-\n";
969 	auto dst = "-a b-\n-a\nb-\n";
970 	assert(formatDdocComment(src) == dst);
971 }
972 
973 unittest {
974 	auto src = "\n  $(M a b)\n$(M a  \nb)\nMacros:\n	M =     -$0-  \n\nN=$0";
975 	auto dst = "-a b-\n-a\nb-\n";
976 	assert(formatDdocComment(src) == dst);
977 }
978 
979 unittest {
980 	auto src = "$(M a, b)\n$(M a,\n    b)\nMacros:\n	M = -$1-\n\n	+$2+\n\n	N=$0";
981 	auto dst = "-a-\n\n	+b+\n-a-\n\n	+    b+\n";
982 	assert(formatDdocComment(src) == dst);
983 }
984 
985 unittest {
986 	auto src = "$(GLOSSARY a\nb)\nMacros:\n	GLOSSARY = $(LINK2 glossary.html#$0, $0)";
987 	auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n";
988 	assert(formatDdocComment(src) == dst);
989 }
990 
991 unittest {
992 	auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.";
993 	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";
994 	assert(formatDdocComment(src) == dst);
995 }
996 
997 unittest {
998 	auto src = "& &a &lt; &#lt; &- &03; &;";
999 	auto dst = "&amp; &a &lt; &#lt; &amp;- &amp;03; &amp;;\n";
1000 	assert(formatDdocComment(src) == dst);
1001 }
1002 
1003 unittest {
1004 	auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = &lt;\nGT = &gt;";
1005 	auto dst = "<a href=\"abc\">test &lt;peter@parker.com&gt;</a>\n";
1006 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n"));
1007 	assert(formatDdocComment(src) == dst);
1008 }
1009 
1010 unittest {
1011 	auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)";
1012 	auto dst = "[a][b][c][d]\n";
1013 	assert(formatDdocComment(src) == dst);
1014 }
1015 
1016 unittest {
1017 	auto src = "Testing `inline <code>`.";
1018 	auto dst = "Testing <code class=\"lang-d\">inline &lt;code&gt;</code>.\n";
1019 	assert(formatDdocComment(src) == dst);
1020 }
1021 
1022 unittest {
1023 	auto src = "Testing `inline $(CODE)`.";
1024 	auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n";
1025 	assert(formatDdocComment(src));
1026 }
1027 
1028 unittest {
1029 	auto src = "---\nthis is a `string`.\n---";
1030 	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";
1031 	assert(formatDdocComment(src) == dst);
1032 }
1033 
1034 unittest { // test for properly removed indentation in code blocks
1035 	auto src = "  ---\n  testing\n  ---";
1036 	auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n";
1037 	assert(formatDdocComment(src) == dst);
1038 }
1039 
1040 unittest { // issue #99 - parse macros in parameter sections
1041 	import std.algorithm : find;
1042 	auto src = "Params:\n\tfoo = $(B bar)";
1043 	auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n";
1044 	assert(formatDdocComment(src).find("<td> ") == dst);
1045 }
1046 
1047 unittest { // issue #89 (minimal test) - empty first parameter
1048 	auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>";
1049 	auto dst = "<div >foo</div>\n";
1050 	assert(formatDdocComment(src) == dst);
1051 }
1052 
1053 unittest { // issue #89 (complex test)
1054 	auto src =
1055 `$(LIST
1056 $(DIV oops,
1057 foo
1058 ),
1059 $(DIV ,
1060 bar
1061 ))
1062 Macros:
1063 LIST=$(UL $(LIX $1, $+))
1064 LIX=$(LI $1)$(LIX $+)
1065 UL=$(T ul, $0)
1066 LI = $(T li, $0)
1067 DIV=<div $1>$+</div>
1068 T=<$1>$+</$1>
1069 `;
1070 	auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n";
1071 	assert(formatDdocComment(src) == dst);
1072 }
1073 
1074 unittest { // issue #95 - trailing newlines must be stripped in macro definitions
1075 	auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar";
1076 	auto dst = "foo\n";
1077 	assert(formatDdocComment(src) == dst);
1078 }
1079 
1080 unittest { // missing macro closing clamp (because it's in a different section)
1081 	auto src = "$(B\n\n)";
1082 	auto dst = "(B\n<section><p>)\n</p>\n</section>\n";
1083 	assert(formatDdocComment(src) == dst);
1084 }
1085 
1086 unittest { // closing clamp should be found in a different *paragraph* of the same section, though
1087 	auto src = "foo\n\n$(B\n\n)";
1088 	auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n";
1089 	assert(formatDdocComment(src) == dst);
1090 }
1091 
1092 unittest { // more whitespace testing
1093 	auto src = "$(M    a   ,   b   ,   c   )\nMacros:\nM =    A$0B$1C$2D$+E";
1094     auto dst = "A   a   ,   b   ,   c   B   a   C  b   D  b   ,   c   E\n";
1095     assert(formatDdocComment(src) == dst);
1096 }
1097 
1098 unittest { // more whitespace testing
1099 	auto src = "  $(M  \n  a  \n  ,  \n  b \n  ,  \n  c  \n  )  \nMacros:\nM =    A$0B$1C$2D$+E";
1100     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";
1101     assert(formatDdocComment(src) == dst);
1102 }
1103 
1104 unittest { // escape in backtick code
1105 	auto src = "`<b>&amp;`";
1106 	auto dst = "<code class=\"lang-d\">&lt;b&gt;&amp;amp;</code>\n";
1107 	assert(formatDdocComment(src) == dst,formatDdocComment(src) );
1108 }
1109 
1110 unittest { // escape in code blocks
1111 	auto src = "---\n<b>&amp;\n---";
1112 	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";
1113 	assert(formatDdocComment(src) == dst);
1114 }
1115 
1116 unittest { // #81 empty first macro arguments
1117 	auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>";
1118 	auto dst = "<table >test</table>\n";
1119 	assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string);
1120 }