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 		auto lines = splitLines(text);
212 		if( !lines.length ) return;
213 
214 		int getLineType(int i)
215 		{
216 			auto ln = strip(lines[i]);
217 			if( ln.length == 0 ) return BLANK;
218 			else if( ln.length >= 3 && ln.allOf("-") ) return CODE;
219 			else if( ln.indexOf(':') > 0 && isIdent(ln[0 .. ln.indexOf(':')]) ) return SECTION;
220 			return TEXT;
221 		}
222 
223 		int skipCodeBlock(int start)
224 		{
225 			do {
226 				start++;
227 			} while(start < lines.length && getLineType(start) != CODE);
228 			if (start >= lines.length) return start; // unterminated code section
229 			return start+1;
230 		}
231 
232 		int skipSection(int start)
233 		{
234 			while (start < lines.length) {
235 				if (getLineType(start) == SECTION) break;
236 				if (getLineType(start) == CODE)
237 					start = skipCodeBlock(start);
238 				else start++;
239 			}
240 			return start;
241 		}
242 
243 		int skipBlock(int start)
244 		{
245 			do {
246 				start++;
247 			} while(start < lines.length && getLineType(start) == TEXT);
248 			return start;
249 		}
250 
251 
252 		int i = 0;
253 
254 		// special case short description on the first line
255 		while( i < lines.length && getLineType(i) == BLANK ) i++;
256 		if( i < lines.length && getLineType(i) == TEXT ){
257 			auto j = skipBlock(i);
258 			m_sections ~= Section("$Short", lines[i .. j]);
259 			i = j;
260 		}
261 
262 		// first section is implicitly the long description
263 		{
264 			auto j = skipSection(i);
265 			if( j > i ){
266 				m_sections ~= Section("$Long", lines[i .. j]);
267 				i = j;
268 			}
269 		}
270 
271 		// parse all other sections
272 		while( i < lines.length ){
273 			assert(getLineType(i) == SECTION);
274 			auto j = skipSection(i+1);
275 			assert(j <= lines.length);
276 			auto pidx = lines[i].indexOf(':');
277 			auto sect = strip(lines[i][0 .. pidx]);
278 			lines[i] = stripLeftDD(lines[i][pidx+1 .. $]);
279 			if (lines[i].empty && i < lines.length) i++;
280 			if (sect == "Macros") parseMacros(m_macros, lines[i .. j]);
281 			else {
282 				m_sections ~= Section(sect, lines[i .. j]);
283 			}
284 			i = j;
285 		}
286 	}
287 
288 	@property bool isDitto() const { return m_isDitto; }
289 	@property bool isPrivate() const { return m_isPrivate; }
290 
291 	/// The macros contained in the "Macros" section (if any)
292 	@property const(string[string]) macros() const { return m_macros; }
293 
294 	bool hasSection(string name) const { return m_sections.canFind!(s => s.name == name); }
295 
296 	void renderSectionR(R)(ref R dst, DdocContext context, string name, int hlevel = 2)
297 	{
298 		renderSectionsR(dst, context, s => s == name, hlevel);
299 	}
300 
301 	void renderSectionsR(R)(ref R dst, DdocContext context, scope bool delegate(string) display_section, int hlevel)
302 	{
303 		string[string] allmacros;
304 		foreach (k, v; context.defaultMacroDefinitions) allmacros[k] = v;
305 		foreach (k, v; m_macros) allmacros[k] = v;
306 		foreach (k, v; context.overrideMacroDefinitions) allmacros[k] = v;
307 
308 		foreach (s; m_sections) {
309 			if (display_section && !display_section(s.name)) continue;
310 			parseSection(dst, s.name, s.lines, context, hlevel, allmacros);
311 		}
312 	}
313 
314 	string renderSection(DdocContext context, string name, int hlevel = 2)
315 	{
316 		auto dst = appender!string();
317 		renderSectionR(dst, context, name, hlevel);
318 		return dst.data;
319 	}
320 
321 	string renderSections(DdocContext context, bool delegate(string) display_section, int hlevel)
322 	{
323 		auto dst = appender!string();
324 		renderSectionsR(dst, context, display_section, hlevel);
325 		return dst.data;
326 	}
327 }
328 
329 
330 /**
331 	Provides context information about the documented element.
332 */
333 interface DdocContext {
334 	/// A line array with macro definitions
335 	@property string[string] defaultMacroDefinitions();
336 
337 	/// Line array with macro definitions that take precedence over local macros
338 	@property string[string] overrideMacroDefinitions();
339 
340 	/// Looks up a symbol in the scope of the documented element and returns a link to it.
341 	string lookupScopeSymbolLink(string name);
342 }
343 
344 
345 private class BareContext : DdocContext {
346 	@property string[string] defaultMacroDefinitions() { return null; }
347 	@property string[string] overrideMacroDefinitions() { return null; }
348 	string lookupScopeSymbolLink(string name) { return null; }
349 }
350 
351 private enum {
352 	BLANK,
353 	TEXT,
354 	CODE,
355 	SECTION
356 }
357 
358 private struct Section {
359 	string name;
360 	string[] lines;
361 
362 	this(string name, string[] lines...)
363 	{
364 		this.name = name;
365 		this.lines = lines;
366 	}
367 }
368 
369 private {
370 	immutable string[string] s_standardMacros;
371 	string[string] s_defaultMacros;
372 	string[string] s_overrideMacros;
373 	bool s_enableHyphenation;
374 	Hyphenator s_hyphenator;
375 }
376 
377 /// private
378 private void parseSection(R)(ref R dst, string sect, string[] lines, DdocContext context, int hlevel, string[string] macros)
379 {
380 	if( sect == "$Short" ) hlevel = -1;
381 
382 	void putHeader(string hdr){
383 		if( hlevel <= 0 ) return;
384 		dst.put("<section>");
385 		if( sect.length > 0 && sect[0] != '$' ){
386 			dst.put("<h"~to!string(hlevel)~">");
387 			foreach( ch; hdr ) dst.put(ch == '_' ? ' ' : ch);
388 			dst.put("</h"~to!string(hlevel)~">\n");
389 		}
390 	}
391 
392 	void putFooter(){
393 		if( hlevel <= 0 ) return;
394 		dst.put("</section>\n");
395 	}
396 
397 	int getLineType(int i)
398 	{
399 		auto ln = strip(lines[i]);
400 		if( ln.length == 0 ) return BLANK;
401 		else if( ln.length >= 3 &&ln.allOf("-") ) return CODE;
402 		else if( ln.indexOf(':') > 0 && !ln[0 .. ln.indexOf(':')].anyOf(" \t") ) return SECTION;
403 		return TEXT;
404 	}
405 
406 	int skipBlock(int start)
407 	{
408 		do {
409 			start++;
410 		} while(start < lines.length && getLineType(start) == TEXT);
411 		return start;
412 	}
413 
414 	int skipCodeBlock(int start)
415 	{
416 		do {
417 			start++;
418 		} while(start < lines.length && getLineType(start) != CODE);
419 		return start;
420 	}
421 
422 	// handle backtick inline-code
423 	for (int i = 0; i < lines.length; i++) {
424 		int lntype = getLineType(i);
425 		if (lntype == CODE) i = skipCodeBlock(i);
426 		else if (sect == "Params") {
427 			auto idx = lines[i].indexOf('=');
428 			if (idx > 0 && isIdent(lines[i][0 .. idx].strip)) {
429 				lines[i] = lines[i][0 .. idx+1] ~ lines[i][idx+1 .. $].highlightAndCrossLink(context);
430 			} else {
431 				lines[i] = lines[i].highlightAndCrossLink(context);
432 			}
433 		} else lines[i] = lines[i].highlightAndCrossLink(context);
434 	}
435 	lines = renderMacros(lines.join("\n").stripDD, context, macros).splitLines();
436 
437 	switch( sect ){
438 		default:
439 			putHeader(sect);
440 			int i = 0;
441 			while( i < lines.length ){
442 				int lntype = getLineType(i);
443 
444 				switch( lntype ){
445 					default: assert(false, "Unexpected line type "~to!string(lntype)~": "~lines[i]);
446 					case BLANK:
447 						dst.put('\n');
448 						i++;
449 						continue;
450 					case SECTION:
451 					case TEXT:
452 						if( hlevel >= 0 ) dst.put("<p>");
453 						auto j = skipBlock(i);
454 						bool first = true;
455 						renderTextLine(dst, lines[i .. j].join("\n")/*.stripDD*/, context);
456 						dst.put('\n');
457 						if( hlevel >= 0 ) dst.put("</p>\n");
458 						i = j;
459 						break;
460 					case CODE:
461 						dst.put("<pre class=\"code\"><code class=\"lang-d\">");
462 						auto j = skipCodeBlock(i);
463 						auto base_indent = baseIndent(lines[i+1 .. j]);
464 						renderCodeLine(dst, lines[i+1 .. j].map!(ln => ln.unindent(base_indent)).join("\n"), context);
465 						dst.put("</code></pre>\n");
466 						i = j+1;
467 						break;
468 				}
469 			}
470 			putFooter();
471 			break;
472 		case "Params":
473 			putHeader("Parameters");
474 			dst.put("<table><col class=\"caption\"><tr><th>Name</th><th>Description</th></tr>\n");
475 			bool in_parameter = false;
476 			string desc;
477 			foreach( string ln; lines ){
478 				// check if the line starts a parameter documentation
479 				string name;
480 				auto eidx = ln.indexOf("=");
481 				if( eidx > 0 ) name = ln[0 .. eidx].strip();
482 				if( !isIdent(name) ) name = null;
483 
484 				// if it does, start a new row
485 				if( name.length ){
486 					if( in_parameter ){
487 						renderTextLine(dst, desc, context);
488 						dst.put("</td></tr>\n");
489 					}
490 
491 					dst.put("<tr><td id=\"");
492 					dst.put(name);
493 					dst.put("\">");
494 					dst.put(name);
495 					dst.put("</td><td>");
496 
497 					desc = ln[eidx+1 .. $];
498 					in_parameter = true;
499 				} else if( in_parameter ) desc ~= "\n" ~ ln;
500 			}
501 
502 			if( in_parameter ){
503 				renderTextLine(dst, desc, context);
504 				dst.put("</td></tr>\n");
505 			}
506 
507 			dst.put("</table>\n");
508 			putFooter();
509 			break;
510 	}
511 }
512 
513 private string highlightAndCrossLink(string line, DdocContext context)
514 {
515 	auto dst = appender!string;
516 	highlightAndCrossLink(dst, line, context);
517 	return dst.data;
518 }
519 
520 private void highlightAndCrossLink(R)(ref R dst, string line, DdocContext context)
521 {
522 	int inCode;
523 	while( line.length > 0 ){
524 		switch( line[0] ){
525 			default:
526 				dst.put(line[0]);
527 				line = line[1 .. $];
528 				break;
529 			case '<':
530 				auto res = skipHtmlTag(line);
531 				if (res.startsWith("<code"))
532 					++inCode;
533 				else if (res == "</code>")
534 					--inCode;
535 				dst.put(res);
536 				break;
537 			case '_':
538 				line = line[1 .. $];
539 				auto ident = skipIdent(line);
540 				if( ident.length )
541 				{
542 					if (s_enableHyphenation && !inCode)
543 						hyphenate(ident, dst);
544 					else
545 						dst.put(ident);
546 				}
547 				else dst.put('_');
548 				break;
549 			case '`':
550 				line.popFront();
551 				auto idx = line.indexOf('`');
552 				if (idx < 0) break;
553 				dst.put("<code class=\"lang-d\">");
554 				dst.renderCodeLine(line[0 .. idx], context);
555 				dst.put("</code>");
556 				line = line[idx+1 .. $];
557 				break;
558 			case '.':
559 				if (line.length > 1 && (line[1 .. $].front.isAlpha || line[1] == '_')) goto case;
560 				else goto default;
561 			case 'a': .. case 'z':
562 			case 'A': .. case 'Z':
563 
564 				auto url = skipUrl(line);
565 				if( url.length ){
566 					/*dst.put("<a href=\"");
567 					dst.put(url);
568 					dst.put("\">");*/
569 					dst.put(url);
570 					//dst.put("</a>");
571 					break;
572 				}
573 
574 				auto ident = skipIdent(line);
575 				auto link = context.lookupScopeSymbolLink(ident);
576 				if (link.length && inCode) {
577 					import ddox.highlight : highlightDCode;
578 					if( link != "#" ){
579 						dst.put("<a href=\"");
580 						dst.put(link);
581 						dst.put("\">");
582 					}
583 					dst.highlightDCode(ident, null);
584 					if( link != "#" ) dst.put("</a>");
585 				} else {
586 					ident = ident.replace("._", ".");
587 					if (s_enableHyphenation && !inCode)
588 						hyphenate(ident, dst);
589 					else
590 						dst.put(ident);
591 				}
592 				break;
593 		}
594 	}
595 }
596 
597 /// private
598 private void renderTextLine(R)(ref R dst, string line, DdocContext context)
599 {
600 	while( line.length > 0 ){
601 		switch( line[0] ){
602 			default:
603 				dst.put(line[0]);
604 				line = line[1 .. $];
605 				break;
606 			case '<':
607 				dst.put(skipHtmlTag(line));
608 				break;
609 			case '>':
610 				dst.put("&gt;");
611 				line.popFront();
612 				break;
613 			case '&':
614 				if (line.length >= 2 && (line[1].isAlpha || line[1] == '#')) dst.put('&');
615 				else dst.put("&amp;");
616 				line.popFront();
617 				break;
618 		}
619 	}
620 }
621 
622 /// private
623 private void renderCodeLine(R)(ref R dst, string line, DdocContext context)
624 {
625 	import ddox.highlight : highlightDCode;
626 	dst.highlightDCode(line, (string ident, scope void delegate(bool) insert_ident) {
627 		auto link = context.lookupScopeSymbolLink(ident);
628 		if (link.length && link != "#") {
629 			dst.put("<a href=\"");
630 			dst.put(link);
631 			dst.put("\">");
632 			insert_ident(true);
633 			dst.put("</a>");
634 		} else insert_ident(false);
635 	});
636 }
637 
638 /// private
639 private void renderMacros(R)(ref R dst, string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null)
640 {
641 	while( !line.empty ){
642 		auto idx = line.indexOf('$');
643 		if( idx < 0 ){
644 			dst.put(line);
645 			return;
646 		}
647 		dst.put(line[0 .. idx]);
648 		line = line[idx .. $];
649 		renderMacro(dst, line, context, macros, params, callstack);
650 	}
651 }
652 
653 /// private
654 private string renderMacros(string line, DdocContext context, string[string] macros, string[] params = null, MacroInvocation[] callstack = null)
655 {
656 	auto app = appender!string;
657 	renderMacros(app, line, context, macros, params, callstack);
658 	return app.data;
659 }
660 
661 /// private
662 private void renderMacro(R)(ref R dst, ref string line, DdocContext context, string[string] macros, string[] params, MacroInvocation[] callstack)
663 {
664 	assert(line[0] == '$');
665 	line = line[1 .. $];
666 	if( line.length < 1) {
667 		dst.put("$");
668 		return;
669 	}
670 
671 	if( line[0] >= '0' && line[0] <= '9' ){
672 		int pidx = line[0]-'0';
673 		if( pidx < params.length )
674 			dst.put(params[pidx]);
675 		line = line[1 .. $];
676 	} else if( line[0] == '+' ){
677 		if( params.length ){
678 			auto idx = params[0].indexOf(',');
679 			if( idx >= 0 ) dst.put(params[0][idx+1 .. $].specialStrip());
680 		}
681 		line = line[1 .. $];
682 	} else if( line[0] == '(' ){
683 		line = line[1 .. $];
684 		int l = 1;
685 		size_t cidx = 0;
686 		for( cidx = 0; cidx < line.length && l > 0; cidx++ ){
687 			if( line[cidx] == '(' ) l++;
688 			else if( line[cidx] == ')' ) l--;
689 		}
690 		if( l > 0 ){
691 			logDebug("Unmatched parenthesis in DDOC comment: %s", line[0 .. cidx]);
692 			dst.put("(");
693 			return;
694 		}
695 		if( cidx < 1 ){
696 			logDebug("Empty macro parens.");
697 			return;
698 		}
699 
700 		auto mnameidx = line[0 .. cidx-1].countUntilAny(", \t\r\n");
701 		if( mnameidx < 0 ) mnameidx = cidx-1;
702 		if( mnameidx == 0 ){
703 			logDebug("Macro call in DDOC comment is missing macro name.");
704 			return;
705 		}
706 
707 		auto mname = line[0 .. mnameidx];
708 		string rawargtext = line[mnameidx .. cidx-1];
709 
710 		string[] args;
711 		if (rawargtext.length) {
712 			auto rawargs = splitParams(rawargtext);
713 			foreach( arg; rawargs ){
714 				auto argtext = appender!string();
715 				renderMacros(argtext, arg, context, macros, params, callstack);
716 				auto newargs = splitParams(argtext.data);
717 				if (newargs.length == 0) args ~= ""; // always add at least one argument per raw argument
718 				else args ~= newargs;
719 			}
720 		}
721 		if (args.length == 1 && args[0].specialStrip.length == 0) args = null; // remove a single empty argument
722 
723 		args = join(args, ",").specialStrip() ~ args.map!(a => a.specialStrip).array;
724 
725 		logTrace("PARAMS for %s: %s", mname, args);
726 		line = line[cidx .. $];
727 
728 		// check for recursion termination conditions
729 		foreach_reverse (ref c; callstack) {
730 			if (c.name == mname && (args.length <= 1 || args == c.params)) {
731 				logTrace("Terminating recursive macro call of %s: %s", mname, params.length <= 1 ? "no argument text" : "same arguments as previous invocation");
732 				//line = line[cidx .. $];
733 				return;
734 			}
735 		}
736 		callstack.assumeSafeAppend();
737 		callstack ~= MacroInvocation(mname, args);
738 
739 
740 		const(string)* pm = mname in s_overrideMacros;
741 		if( !pm ) pm = mname in macros;
742 		if( !pm ) pm = mname in s_defaultMacros;
743 		if( !pm ) pm = mname in s_standardMacros;
744 
745 		if (mname == "D") {
746 			auto tmp = appender!string;
747 			renderMacros(tmp, "$0", context, macros, args, callstack);
748 			dst.put("<code class=\"lang-d\">");
749 			dst.renderCodeLine(tmp.data, context);
750 			dst.put("</code>");
751 		} else if (pm) {
752 			logTrace("MACRO %s: %s", mname, *pm);
753 			renderMacros(dst, *pm, context, macros, args, callstack);
754 		} else {
755 			logTrace("Macro '%s' not found.", mname);
756 			if( args.length ) dst.put(args[0]);
757 		}
758 	} else dst.put("$");
759 }
760 
761 private struct MacroInvocation {
762 	string name;
763 	string[] params;
764 }
765 
766 private string[] splitParams(string ln)
767 {
768 	string[] ret;
769 	size_t i = 0, start = 0;
770 	while(i < ln.length){
771 		if( ln[i] == ',' ){
772 			ret ~= ln[start .. i];
773 			start = ++i;
774 		} else if( ln[i] == '(' ){
775 			i++;
776 			int l = 1;
777 			for( ; i < ln.length && l > 0; i++ ){
778 				if( ln[i] == '(' ) l++;
779 				else if( ln[i] == ')' ) l--;
780 			}
781 		} else i++;
782 	}
783 	if( i > start ) ret ~= ln[start .. i];
784 	return ret;
785 }
786 
787 private string skipHtmlTag(ref string ln)
788 {
789 	assert(ln[0] == '<');
790 
791 	// skip HTML comment
792 	if (ln.startsWith("<!--")) {
793 		auto idx = ln[4 .. $].indexOf("-->");
794 		if (idx < 0) {
795 			ln.popFront();
796 			return "&lt;";
797 		}
798 		auto ret = ln[0 .. idx+7];
799 		ln = ln[ret.length .. $];
800 		return ret;
801 	}
802 
803 	// too short for a tag
804 	if (ln.length < 2 || (!ln[1].isAlpha && ln[1] != '#' && ln[1] != '/')) {
805 		// found no match, return escaped '<'
806 		logTrace("Found stray '<' in DDOC string.");
807 		ln.popFront();
808 		return "&lt;";
809 	}
810 
811 	// skip over regular start/end tag
812 	auto idx = ln.indexOf(">");
813 	if (idx < 0) {
814 		ln.popFront();
815 		return "<";
816 	}
817 	auto ret = ln[0 .. idx+1];
818 	ln = ln[ret.length .. $];
819 	return ret;
820 }
821 
822 private string skipUrl(ref string ln)
823 {
824 	if( !ln.startsWith("http://") && !ln.startsWith("http://") )
825 		return null;
826 
827 	bool saw_dot = false;
828 	size_t i = 7;
829 
830 	for_loop:
831 	while( i < ln.length ){
832 		switch( ln[i] ){
833 			default:
834 				break for_loop;
835 			case 'a': .. case 'z':
836 			case 'A': .. case 'Z':
837 			case '0': .. case '9':
838 			case '_', '-', '?', '=', '%', '&', '/', '+', '#', '~':
839 				break;
840 			case '.':
841 				saw_dot = true;
842 				break;
843 		}
844 		i++;
845 	}
846 
847 	if( saw_dot ){
848 		auto ret = ln[0 .. i];
849 		ln = ln[i .. $];
850 		return ret;
851 	} else return null;
852 }
853 
854 private string skipIdent(ref string str)
855 {
856 	string strcopy = str;
857 
858 	if (str.length >= 2 && str[0] == '.' && (str[1].isAlpha || str[1] == '_'))
859 		str.popFront();
860 
861 	bool last_was_ident = false;
862 	while( !str.empty ){
863 		auto ch = str.front;
864 
865 		if( last_was_ident ){
866 			// dots are allowed if surrounded by identifiers
867 			if( ch == '.' ) last_was_ident = false;
868 			else if( ch != '_' && (ch < '0' || ch > '9') && !std.uni.isAlpha(ch) ) break;
869 		} else {
870 			if( ch != '_' && !std.uni.isAlpha(ch) ) break;
871 			last_was_ident = true;
872 		}
873 		str.popFront();
874 	}
875 
876 	// if the identifier ended in a '.', remove it again
877 	if( str.length != strcopy.length && !last_was_ident )
878 		str = strcopy[strcopy.length-str.length-1 .. $];
879 
880 	return strcopy[0 .. strcopy.length-str.length];
881 }
882 
883 private bool isIdent(string str)
884 {
885 	skipIdent(str);
886 	return str.length == 0;
887 }
888 
889 private void parseMacros(ref string[string] macros, in string[] lines)
890 {
891 	string name;
892 	foreach (string ln; lines) {
893 		// macro definitions are of the form IDENT = ...
894 		auto pidx = ln.indexOf('=');
895 		if (pidx > 0) {
896 			auto tmpnam = ln[0 .. pidx].strip();
897 			// got new macro definition?
898 			if (isIdent(tmpnam)) {
899 
900 				// strip the previous macro
901 				if (name.length) macros[name] = macros[name].stripDD();
902 
903 				// start parsing the new macro
904 				name = tmpnam;
905 				macros[name] = stripLeftDD(ln[pidx+1 .. $]);
906 				continue;
907 			}
908 		}
909 
910 		// append to previous macro definition, if any
911 		macros[name] ~= "\n" ~ ln;
912 	}
913 }
914 
915 private int baseIndent(string[] lines)
916 {
917 	if( lines.length == 0 ) return 0;
918 	int ret = int.max;
919 	foreach( ln; lines ){
920 		int i = 0;
921 		while( i < ln.length && (ln[i] == ' ' || ln[i] == '\t') )
922 			i++;
923 		if( i < ln.length ) ret = min(ret, i);
924 	}
925 	return ret;
926 }
927 
928 private string unindent(string ln, int amount)
929 {
930 	while( amount > 0 && ln.length > 0 && (ln[0] == ' ' || ln[0] == '\t') )
931 		ln = ln[1 .. $], amount--;
932 	return ln;
933 }
934 
935 private string stripLeftDD(string s)
936 {
937 	while (!s.empty && (s.front == ' ' || s.front == '\t' || s.front == '\r' || s.front == '\n'))
938 		s.popFront();
939 	return s;
940 }
941 
942 private string specialStrip(string s)
943 {
944 	import std.algorithm : among;
945 
946 	// strip trailing whitespace for all lines but the last
947 	size_t idx = 0;
948 	while (true) {
949 		auto nidx = s[idx .. $].indexOf('\n');
950 		if (nidx < 0) break;
951 		nidx += idx;
952 		auto strippedfront = s[0 .. nidx].stripRightDD();
953 		s = strippedfront ~ "\n" ~ s[nidx+1 .. $];
954 		idx = strippedfront.length + 1;
955 	}
956 
957 	// strip the first character, if whitespace
958 	if (!s.empty && s.front.among!(' ', '\t', '\n', '\r')) s.popFront();
959 
960 	return s;
961 }
962 
963 private string stripRightDD(string s)
964 {
965 	while (!s.empty && (s.back == ' ' || s.back == '\t' || s.back == '\r' || s.back == '\n'))
966 		s.popBack();
967 	return s;
968 }
969 
970 private string stripDD(string s)
971 {
972 	return s.stripLeftDD.stripRightDD;
973 }
974 
975 import std.stdio;
976 unittest {
977 	auto src = "$(M a b)\n$(M a\nb)\nMacros:\n	M =     -$0-\n";
978 	auto dst = "-a b-\n-a\nb-\n";
979 	assert(formatDdocComment(src) == dst);
980 }
981 
982 unittest {
983 	auto src = "\n  $(M a b)\n$(M a  \nb)\nMacros:\n	M =     -$0-  \n\nN=$0";
984 	auto dst = "-a b-\n-a\nb-\n";
985 	assert(formatDdocComment(src) == dst);
986 }
987 
988 unittest {
989 	auto src = "$(M a, b)\n$(M a,\n    b)\nMacros:\n	M = -$1-\n\n	+$2+\n\n	N=$0";
990 	auto dst = "-a-\n\n	+b+\n-a-\n\n	+    b+\n";
991 	assert(formatDdocComment(src) == dst);
992 }
993 
994 unittest {
995 	auto src = "$(GLOSSARY a\nb)\nMacros:\n	GLOSSARY = $(LINK2 glossary.html#$0, $0)";
996 	auto dst = "<a href=\"glossary.html#a\nb\">a\nb</a>\n";
997 	assert(formatDdocComment(src) == dst);
998 }
999 
1000 unittest {
1001 	auto src = "a > b < < c > <a <# </ <br> <abc> <.abc> <-abc> <+abc> <0abc> <abc-> <> <!-- c --> <!--> <! > <!-- > >a.";
1002 	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";
1003 	assert(formatDdocComment(src) == dst);
1004 }
1005 
1006 unittest {
1007 	auto src = "& &a &lt; &#lt; &- &03; &;";
1008 	auto dst = "&amp; &a &lt; &#lt; &amp;- &amp;03; &amp;;\n";
1009 	assert(formatDdocComment(src) == dst);
1010 }
1011 
1012 unittest {
1013 	auto src = "<a href=\"abc\">test $(LT)peter@parker.com$(GT)</a>\nMacros:\nLT = &lt;\nGT = &gt;";
1014 	auto dst = "<a href=\"abc\">test &lt;peter@parker.com&gt;</a>\n";
1015 //writeln(formatDdocComment(src).splitLines().map!(s => "|"~s~"|").join("\n"));
1016 	assert(formatDdocComment(src) == dst);
1017 }
1018 
1019 unittest {
1020 	auto src = "$(LIX a, b, c, d)\nMacros:\nLI = [$0]\nLIX = $(LI $1)$(LIX $+)";
1021 	auto dst = "[a][b][c][d]\n";
1022 	assert(formatDdocComment(src) == dst);
1023 }
1024 
1025 unittest {
1026 	auto src = "Testing `inline <code>`.";
1027 	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";
1028 	assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string);
1029 }
1030 
1031 unittest {
1032 	auto src = "Testing `inline $(CODE)`.";
1033 	auto dst = "Testing <code class=\"lang-d\">inline $(CODE)</code>.\n";
1034 	assert(formatDdocComment(src));
1035 }
1036 
1037 unittest {
1038 	auto src = "---\nthis is a `string`.\n---";
1039 	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";
1040 	assert(formatDdocComment(src) == dst);
1041 }
1042 
1043 unittest { // test for properly removed indentation in code blocks
1044 	auto src = "  ---\n  testing\n  ---";
1045 	auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><span class=\"pln\">testing</span></code></pre>\n</section>\n";
1046 	assert(formatDdocComment(src) == dst);
1047 }
1048 
1049 unittest { // issue #99 - parse macros in parameter sections
1050 	import std.algorithm : find;
1051 	auto src = "Params:\n\tfoo = $(B bar)";
1052 	auto dst = "<td> <b>bar</b></td></tr>\n</table>\n</section>\n";
1053 	assert(formatDdocComment(src).find("<td> ") == dst);
1054 }
1055 
1056 unittest { // issue #89 (minimal test) - empty first parameter
1057 	auto src = "$(DIV , foo)\nMacros:\nDIV=<div $1>$+</div>";
1058 	auto dst = "<div >foo</div>\n";
1059 	assert(formatDdocComment(src) == dst);
1060 }
1061 
1062 unittest { // issue #89 (complex test)
1063 	auto src =
1064 `$(LIST
1065 $(DIV oops,
1066 foo
1067 ),
1068 $(DIV ,
1069 bar
1070 ))
1071 Macros:
1072 LIST=$(UL $(LIX $1, $+))
1073 LIX=$(LI $1)$(LIX $+)
1074 UL=$(T ul, $0)
1075 LI = $(T li, $0)
1076 DIV=<div $1>$+</div>
1077 T=<$1>$+</$1>
1078 `;
1079 	auto dst = "<ul><li><div oops>foo\n</div></li><li><div >bar\n</div></li></ul>\n";
1080 	assert(formatDdocComment(src) == dst);
1081 }
1082 
1083 unittest { // issue #95 - trailing newlines must be stripped in macro definitions
1084 	auto src = "$(FOO)\nMacros:\nFOO=foo\n\nBAR=bar";
1085 	auto dst = "foo\n";
1086 	assert(formatDdocComment(src) == dst);
1087 }
1088 
1089 unittest { // missing macro closing clamp (because it's in a different section)
1090 	auto src = "$(B\n\n)";
1091 	auto dst = "(B\n<section><p>)\n</p>\n</section>\n";
1092 	assert(formatDdocComment(src) == dst);
1093 }
1094 
1095 unittest { // closing clamp should be found in a different *paragraph* of the same section, though
1096 	auto src = "foo\n\n$(B\n\n)";
1097 	auto dst = "foo\n<section><p><b></b>\n</p>\n</section>\n";
1098 	assert(formatDdocComment(src) == dst);
1099 }
1100 
1101 unittest { // more whitespace testing
1102 	auto src = "$(M    a   ,   b   ,   c   )\nMacros:\nM =    A$0B$1C$2D$+E";
1103     auto dst = "A   a   ,   b   ,   c   B   a   C  b   D  b   ,   c   E\n";
1104     assert(formatDdocComment(src) == dst);
1105 }
1106 
1107 unittest { // more whitespace testing
1108 	auto src = "  $(M  \n  a  \n  ,  \n  b \n  ,  \n  c  \n  )  \nMacros:\nM =    A$0B$1C$2D$+E";
1109     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";
1110     assert(formatDdocComment(src) == dst);
1111 }
1112 
1113 unittest { // escape in backtick code
1114 	auto src = "`<b>&amp;`";
1115 	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";
1116 	assert(formatDdocComment(src) == dst);
1117 }
1118 
1119 unittest { // escape in code blocks
1120 	auto src = "---\n<b>&amp;\n---";
1121 	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";
1122 	assert(formatDdocComment(src) == dst);
1123 }
1124 
1125 unittest { // #81 empty first macro arguments
1126 	auto src = "$(BOOKTABLE,\ntest)\nMacros:\nBOOKTABLE=<table $1>$+</table>";
1127 	auto dst = "<table >test</table>\n";
1128 	assert(formatDdocComment(src) == dst);
1129 }
1130 
1131 unittest { // #117 underscore identifiers as macro param
1132 	auto src = "$(M __foo) __foo `__foo` $(D_CODE __foo)\nMacros:\nM=http://$1.com";
1133 	auto dst = "http://_foo.com _foo <code class=\"lang-d\"><span class=\"pln\">__foo</span></code> <pre class=\"d_code\">_foo</pre>\n";
1134 	assert(formatDdocComment(src) == dst);
1135 }
1136 
1137 unittest { // #109 dot followed by unicode character causes infinite loop
1138 	auto src = ".”";
1139 	auto dst = ".”\n";
1140 	assert(formatDdocComment(src) == dst);
1141 }
1142 
1143 unittest { // #119 dot followed by space causes assertion
1144 	static class Ctx : BareContext {
1145 		override string lookupScopeSymbolLink(string name) {
1146 			writefln("IDENT: %s", name);
1147 			assert(name.length > 0 && name != ".");
1148 			return null;
1149 		}
1150 	}
1151 	auto src = "---\n. writeln();\n---";
1152 	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";
1153 	assert(formatDdocComment(src, new Ctx) == dst);
1154 }
1155 
1156 unittest { // dot followed by non-identifier
1157 	static class Ctx : BareContext {
1158 		override string lookupScopeSymbolLink(string name) {
1159 			writefln("IDENT: %s", name);
1160 			assert(name.length > 0 && name != ".");
1161 			return null;
1162 		}
1163 	}
1164 	auto src = "---\n.()\n---";
1165 	auto dst = "<section><pre class=\"code\"><code class=\"lang-d\"><wbr/><span class=\"pun\">.()</span></code></pre>\n</section>\n";
1166 	assert(formatDdocComment(src, new Ctx) == dst);
1167 }
1168 
1169 
1170 unittest { // X-REF
1171 	static class Ctx : BareContext {
1172 		override string lookupScopeSymbolLink(string name) {
1173 			if (name == "foo") return "foo.html";
1174 			else return null;
1175 		}
1176 	}
1177 	auto src = "`foo` `bar` $(D foo) $(D bar)\n\n---\nfoo bar\n---";
1178 	auto dst = "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> "
1179 		~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code> "
1180 		~ "<code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a></code> "
1181 		~ "<code class=\"lang-d\"><span class=\"pln\">bar</span></code>\n"
1182 		~ "<section><pre class=\"code\"><code class=\"lang-d\"><a href=\"foo.html\"><span class=\"pln\">foo</span></a>"
1183 		~ "<span class=\"pln\"> bar</span></code></pre>\n</section>\n";
1184 	assert(formatDdocComment(src, new Ctx) == dst, [formatDdocComment(src, new Ctx)].to!string);
1185 }
1186 
1187 unittest { // nested macro in $(D ...)
1188 	auto src = "$(D $(NOP foo))\n\nMacros: NOP: $0";
1189 	auto dst = "<code class=\"lang-d\"><span class=\"pln\">foo</span></code>\n<section></section>\n";
1190 	assert(formatDdocComment(src) == dst, [formatDdocComment(src)].to!string);
1191 }