1 /** Fanzy Pretty Printing to AsciiDoc, HTML, LaTeX, JIRA Wikitext, etc.
2  *
3  * Copyright: Per Nordlöw 2022-.
4  * License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
5  * Authors: $(WEB Per Nordlöw)
6  *
7  * TODO: Convert `Viz` param to delegate like for
8  * `void toString(Sink)(ref scope Sink sink)`
9  * for independency on arsd.
10  *
11  * Rename `pp1` to `ppL` and `pp` to `pp1` where relevant to reduce number of
12  * template instantiations. And replace multi-calls to viz.pp() to viz.pp1()
13  *
14  * TODO: Add tester that prints types from `nxt.geometry`
15  *
16  * TODO: Remove all restrictions on pp.*Raw.* and call them using ranges such as repeat
17  *
18  * TODO: Use "alias this" on wrapper structures and test!
19  *
20  * TODO: How should std.typecons.Tuple be pretty printed?
21  *
22  * TODO: Add visited member to keeps track of what objects that have been visited
23  *
24  * TODO: Add asGCCMessage pretty prints
25  * seq $PATH, ':', $ROW, ':', $COL, ':', message, '[', $TYPE, ']'
26  */
27 module nxt.fancyio;
28 
29 version (none) : // TODO: activate
30 
31 // version = useTerm;
32 // version = show;
33 
34 import core.time : Duration;
35 
36 import std.range.primitives : isInputRange;
37 import std.traits : isSomeString, Unqual, isArray, isIterable;
38 import std.stdio : stdout;
39 
40 /* TODO: Move logic (toHTML) to these deps and remove these imports */
41 import nxt.color : Color;
42 import nxt.mathml;
43 import nxt.lingua;
44 import nxt.attributes;
45 import nxt.rational : Rational;
46 
47 import arsd.terminal : Terminal, ConsoleOutputType, Bright;
48 
49 /// See_Also: http://forum.dlang.org/thread/fihymjelvnwfevegwryt@forum.dlang.org#post-fihymjelvnwfevegwryt:40forum.dlang.org
50 template Concise(Tuple)
51 {
52 	static if(isTuple!Tuple)
53 	{
54 		struct Concise(Tuple)
55 		{
56 			Tuple tup;
57 			alias tup this;
58 			// should use a better overload
59 			string toString() const
60 			{
61 				auto app = appender!string(); /+ TODO: use array_ex.Array instead +/
62 				app.put(`(`);
63 				app.put(to!string(concise(tuple[0])));
64 				foreach (t; tuple.expand[1 .. $])
65 				{
66 					app.put(`, `);
67 					app.put(to!string(concise(t)));
68 				}
69 				app.put(`)`);
70 				return app.data;
71 			}
72 		}
73 	}
74 	else
75 		alias Concise = Tuple;
76 }
77 
78 auto concise(T)(T t)
79 {
80 	return Concise!T(t);
81 }
82 
83 /** Returns: Duration `dur` in a Level-Of-Detail (LOD) string
84 	representation.
85 */
86 string shortDurationString(in Duration dur) @safe pure nothrow
87 {
88 	import std.conv: to;
89 	immutable weeks = dur.total!`weeks`;
90 	if (weeks)
91 	{
92 		if (weeks < 52)
93 			return to!string(weeks) ~ ` week` ~ (weeks >= 2 ? `s` : ``);
94 		else
95 		{
96 			immutable years = weeks / 52;
97 			immutable weeks_rest = weeks % 52;
98 			return to!string(years) ~ ` year` ~ (years >= 2 ? `s` : ``) ~
99 			` and ` ~
100 			to!string(weeks_rest) ~ ` week` ~ (weeks_rest >= 2 ? `s` : ``);
101 		}
102 	}
103 	immutable days = dur.total!`days`;	   if (days)	return to!string(days) ~ ` day` ~ (days >= 2 ? `s` : ``);
104 	immutable hours = dur.total!`hours`;	 if (hours)   return to!string(hours) ~ ` hour` ~ (hours >= 2 ? `s` : ``);
105 	immutable minutes = dur.total!`minutes`; if (minutes) return to!string(minutes) ~ ` minute` ~ (minutes >= 2 ? `s` : ``);
106 	immutable seconds = dur.total!`seconds`; if (seconds) return to!string(seconds) ~ ` second` ~ (seconds >= 2 ? `s` : ``);
107 	immutable msecs = dur.total!`msecs`;	 if (msecs) return to!string(msecs) ~ ` millisecond` ~ (msecs >= 2 ? `s` : ``);
108 	immutable usecs = dur.total!`usecs`;	 if (usecs) return to!string(usecs) ~ ` microsecond` ~ (msecs >= 2 ? `s` : ``);
109 	immutable nsecs = dur.total!`nsecs`;	 return to!string(nsecs) ~ ` nanosecond` ~ (msecs >= 2 ? `s` : ``);
110 }
111 
112 /** Visual Form(at). */
113 enum VizForm
114 {
115 	textAsciiDoc,			   // TODO.
116 	textAsciiDocUTF8,		   // TODO.
117 	HTML,
118 	D3js,					   // TODO. See_Also: http://d3js.org/
119 	LaTeX,					  // TODO.
120 	jiraWikiMarkup, // TODO. See_Also: https://jira.atlassiana.com/secure/WikiRendererHelpAction.jspa?section=all
121 	Markdown,	   // TODO.
122 }
123 
124 /** Solarized light color theme.
125  *
126  * Each enumerator is an RGB hex string.
127  *
128  * TODO: use RGB-type as enumerator instead
129  *
130  * See_Also: http://ethanschoonover.com/solarized
131  */
132 enum SolarizedLightColorTheme_hexstring
133 {
134 	base00  = `657b83`,
135 	base01  = `586e75`,
136 	base02  = `073642`,
137 	base03  = `002b36`,
138 
139 	base0   = `839496`,
140 	base1   = `93a1a1`,
141 	base2   = `eee8d5`,
142 	base3   = `fdf6e3`,
143 
144 	yellow  = `b58900`,
145 	orange  = `cb4b16`,
146 	red	 = `dc322f`,
147 	magenta = `d33682`,
148 	viole   = `6c71c4`,
149 	blue	= `268bd2`,
150 	cya	 = `2aa198`,
151 	gree	= `859900`
152 }
153 
154 /** HTML tags with no side-effect when its arguments is empty.
155 	See_Also: http://www.w3schools.com/html/html_formatting.asp
156 */
157 static immutable nonStateHTMLTags = [`b`, `i`, `strong`, `em`, `sub`, `sup`, `small`, `ins`, `del`, `mark`,
158 									 `code`, `kbd`, `samp`, `samp`, `var`, `pre`];
159 
160 static immutable htmlHeader = `<!DOCTYPE html>
161 <html>
162 <head>
163 <meta charset="UTF-8"/>
164 <style>
165 
166 body { font: 10px Verdana, sans-serif; }
167 hit0 { background-color:#F2B701; border: solid 0px grey; }
168 hit1 { background-color:#F18204; border: solid 0px grey; }
169 hit2 { background-color:#F50035; border: solid 0px grey; }
170 hit3 { background-color:#F5007A; border: solid 0px grey; }
171 hit4 { background-color:#A449B6; border: solid 0px grey; }
172 hit5 { background-color:#3A70BB; border: solid 0px grey; }
173 hit6 { background-color:#0DE7A6; border: solid 0px grey; }
174 hit7 { background-color:#70AD48; border: solid 0px grey; }
175 
176 hit_context { background-color:#c0c0c0; border: solid 0px grey; }
177 
178 code { background-color:#FFFFE0; }
179 
180 dlang-byte   { background-color:#FFFFE0; }
181 dlang-short  { background-color:#FFFFE0; }
182 dlang-int	{ background-color:#FFFFE0; }
183 dlang-long   { background-color:#FFFFE0; }
184 
185 dlang-ubyte   { background-color:#FFFFE0; }
186 dlang-ushort  { background-color:#FFFFE0; }
187 dlang-uint	{ background-color:#FFFFE0; }
188 dlang-ulong   { background-color:#FFFFE0; }
189 
190 dlang-bool   { background-color:#FFFFE0; }
191 
192 dlang-float  { background-color:#FFFFE0; }
193 dlang-double { background-color:#FFFFE0; }
194 dlang-real   { background-color:#FFFFE0; }
195 
196 dlang-char   { background-color:#FFFFE0; }
197 dlang-wchar  { background-color:#FFFFE0; }
198 dlang-dchar  { background-color:#FFFFE0; }
199 
200 dlang-string  { background-color:#FFFFE0; }
201 dlang-wstring { background-color:#FFFFE0; }
202 dlang-dstring { background-color:#FFFFE0; }
203 
204 td, th { border: 1px solid black; }
205 table { border-collapse: collapse; }
206 
207 tr:nth-child(even) { background-color: #EBEBEB; }
208 tr:nth-child(2n+0) { background: #` ~ SolarizedLightColorTheme_hexstring.base2 ~ `; }
209 tr:nth-child(2n+1) { background: #` ~ SolarizedLightColorTheme_hexstring.base3 ~ `; }
210 
211 </style>
212 </head>
213 <body>
214 `;
215 
216 /** Visual Backend.
217  */
218 class Viz
219 {
220 	import std.stdio : IOFile = File;
221 
222 	IOFile outFile;
223 	Terminal* term;
224 
225 	bool treeFlag;
226 	VizForm form;
227 
228 	bool colorFlag;
229 	bool flushNewlines = true;
230 
231 	/* If any (HTML) tags should be ended with a newline.
232 	   This increases the readability of generated HTML code.
233 	 */
234 	bool newlinedTags = true;
235 
236 	bool _completed = false;
237 
238 	this(IOFile outFile,
239 		 Terminal* term,
240 		 VizForm form = VizForm.textAsciiDocUTF8,
241 		 bool treeFlag = true,
242 		 bool colorFlag = true,
243 		 bool flushNewlines = true,
244 		 bool newlinedTags = true,
245 		)
246 	{
247 		this.outFile = outFile;
248 		this.term = term;
249 		this.treeFlag = treeFlag;
250 		this.form = form;
251 		this.colorFlag = colorFlag;
252 		this.flushNewlines = flushNewlines;
253 		this.newlinedTags = newlinedTags;
254 		if (form == VizForm.HTML)
255 			ppRaw(this, htmlHeader);
256 	}
257 
258 	private void completeIfNeeded()
259 	{
260 		if (!_completed)
261 		{
262 			if (form == VizForm.HTML)
263 				ppRaw("</body>\n</html>");
264 			_completed = true;
265 			this.outFile.flush();
266 		}
267 	}
268 
269 	/** Put `arg` to `this` without any conversion nor coloring. */
270 	void ppRaw(T...)(T args)
271 	{
272 		foreach (const arg; args)
273 		{
274 			if (outFile == stdout)
275 				term.write(arg); // trick
276 			else
277 				outFile.write(arg);
278 		}
279 	}
280 
281 	/** Put `arg` to `this` without any conversion nor coloring. */
282 	void pplnRaw(T...)(T args)
283 	{
284 		foreach (arg; args)
285 		{
286 			if (outFile == stdout)
287 			{
288 				if (flushNewlines)
289 					term.writeln(arg);
290 				else
291 					term.write(arg, '\n');
292 			}
293 			else
294 			{
295 				if (flushNewlines)
296 					outFile.writeln(arg);
297 				else
298 					outFile.write(arg, '\n');
299 			}
300 		}
301 	}
302 
303 	/// Print opening of tag `tag`.
304 	void ppTagOpen(T, P...)(T tag, P params)
305 	{
306 		if (form == VizForm.HTML)
307 		{
308 			ppRaw(`<` ~ tag);
309 			foreach (param; params)
310 				ppRaw(' ', param);
311 			ppRaw(`>`);
312 		}
313 	}
314 
315 	/// Print closing of tag `tag`.
316 	void ppTagClose(T)(T tag)
317 	{
318 		immutable arg = (form == VizForm.HTML) ? `</` ~ tag ~ `>` : tag;
319 		ppRaw(arg);
320 	}
321 
322 	/// Print opening of tag `tag` on a separate line.
323 	void pplnTagOpen(T)(T tag)
324 	{
325 		immutable arg = (form == VizForm.HTML) ? `<` ~ tag ~ `>` : tag;
326 		if (newlinedTags)
327 			pplnRaw(arg);
328 		else
329 			ppRaw(arg);
330 	}
331 
332 	/// Print closing of tag `tag` on a separate line.
333 	void pplnTagClose(T)(T tag)
334 	{
335 		immutable arg = (form == VizForm.HTML) ? `</` ~ tag ~ `>` : tag;
336 		if (newlinedTags)
337 			pplnRaw(arg);
338 		else
339 			ppRaw(arg);
340 	}
341 
342 	/** Put `arg` to `viz` possibly with conversion. */
343 	void ppPut(T)(T arg,
344 				  bool nbsp = true)
345 	{
346 		if (outFile == stdout)
347 			term.write(arg);
348 		else
349 		{
350 			import nxt.w3c : encodeHTML;
351 			if (form == VizForm.HTML)
352 				outFile.write(arg.encodeHTML(nbsp));
353 			else
354 				outFile.write(arg);
355 		}
356 	}
357 
358 	/** Put `arg` to `viz` possibly with conversion. */
359 	void ppPut(T)(Face face,
360 				  T arg,
361 				  bool nbsp = true)
362 	{
363 		term.setFace(face, colorFlag);
364 		ppPut(arg, nbsp);
365 	}
366 
367 	/** Print `args` tagged as `tag`. */
368 	void ppTaggedN(Tag, Args...)(in Tag tag, Args args)
369 	if (isSomeString!Tag)
370 	{
371 		import std.algorithm.searching : find;
372 		static if (args.length == 1 &&
373 				   isSomeString!(typeof(args[0])))
374 		{
375 			import std.string : empty;
376 			if (form == VizForm.HTML &&
377 				args[0].empty &&
378 				!nonStateHTMLTags.find(tag).empty)
379 				return;		 // skip HTML tags with no content
380 		}
381 		if (form == VizForm.HTML)
382 			ppRaw(`<` ~ tag ~ `>`);
383 		ppN(args);
384 		if (form == VizForm.HTML)
385 			ppRaw(`</` ~ tag ~ `>`);
386 	}
387 
388 	/** Print `args` tagged as `tag` on a separate line. */
389 	void pplnTaggedN(Tag, Args...)(in Tag tag, Args args)
390 	if (isSomeString!Tag)
391 	{
392 		ppTaggedN(tag, args);
393 		if (newlinedTags)
394 			pplnRaw(``);
395 	}
396 
397 	/+ TODO: Check for MathML support on backend +/
398 	@property void ppMathML(SomeIntegral)(Rational!SomeIntegral arg)
399 	{
400 		ppTagOpen(`math`);
401 		ppTagOpen(`mfrac`);
402 		ppTaggedN(`mi`, arg.numerator);
403 		ppTaggedN(`mi`, arg.denominator);
404 		ppTagClose(`mfrac`);
405 		ppTagClose(`math`);
406 	}
407 
408 	/** Pretty-Print Single Argument `arg` to Terminal `term`. */
409 	void pp1(Arg)(Arg arg)
410 	{
411 		pp1(0, arg);
412 	}
413 
414 	/** Pretty-Print Single Argument `arg` to Terminal `term`. */
415 	void pp1(Arg)(int depth,
416 				  Arg arg)
417 	{
418 		static if (is(typeof(ppMathML(arg))))
419 		{
420 			if (form == VizForm.HTML)
421 				return ppMathML(arg);
422 		}
423 		static if (is(typeof(arg.toMathML)))
424 		{
425 			if (form == VizForm.HTML)
426 				/+ TODO: Check for MathML support on backend +/
427 				return ppRaw(arg.toMathML);
428 		}
429 		static if (is(typeof(arg.toHTML)))
430 		{
431 			if (form == VizForm.HTML)
432 				return ppRaw(arg.toHTML);
433 		}
434 		static if (is(typeof(arg.toLaTeX)))
435 		{
436 			if (form == VizForm.LaTeX)
437 				return ppRaw(arg.toLaTeX);
438 		}
439 
440 		/* TODO: Check if any member has mmber toMathML if so call it otherwise call
441 		 * toString. */
442 
443 		static if (is(Arg == AsWords!(_), _))
444 		{
445 			foreach (ix, subArg; arg.args)
446 			{
447 				static if (ix >= 1)
448 					ppRaw(` `); // separator
449 				pp1(depth + 1, subArg);
450 			}
451 		}
452 		else static if (is(Arg == AsCSL!(_), _))
453 		{
454 			foreach (ix, subArg; arg.args)
455 			{
456 				static if (ix >= 1)
457 					pp1(depth + 1, `,`); // separator
458 				static if (isInputRange!(typeof(subArg)))
459 					foreach (subsubArg; subArg)
460 						ppN(subsubArg, `,`);
461 			}
462 		}
463 		else static if (is(Arg == AsBold!(_), _))
464 		{
465 			if	  (form == VizForm.HTML)
466 				ppTaggedN(`b`, arg.args);
467 			else if (form == VizForm.Markdown)
468 			{
469 				ppRaw(`**`);
470 				ppN(arg.args);
471 				ppRaw(`**`);
472 			}
473 		}
474 		else static if (is(Arg == AsItalic!(_), _))
475 		{
476 			if	  (form == VizForm.HTML)
477 				ppTaggedN(`i`, arg.args);
478 			else if (form == VizForm.Markdown)
479 			{
480 				ppRaw(`*`);
481 				ppN(arg.args);
482 				ppRaw(`*`);
483 			}
484 		}
485 		else static if (is(Arg == AsMonospaced!(_), _))
486 		{
487 			if	  (form == VizForm.HTML)
488 				ppTaggedN(`tt`, arg.args);
489 			else if (form == VizForm.jiraWikiMarkup)
490 			{
491 				ppRaw(`{{`);
492 				ppN(arg.args);
493 				ppRaw(`}}`);
494 			}
495 			else if (form == VizForm.Markdown)
496 			{
497 				ppRaw('`');
498 				ppN(arg.args);
499 				ppRaw('`');
500 			}
501 		}
502 		else static if (is(Arg == AsCode!(_), _))
503 		{
504 			if	  (form == VizForm.HTML)
505 			{
506 				/* TODO: Use arg.language member to highlight using fs tokenizers
507 				 * which must be moved out of fs. */
508 				ppTaggedN(`code`, arg.args);
509 			}
510 			else if (form == VizForm.jiraWikiMarkup)
511 			{
512 				ppRaw(arg.language ? `{code:` ~ arg.language ~ `}` : `{code}`);
513 				ppN(arg.args);
514 				ppRaw(`{code}`);
515 			}
516 		}
517 		else static if (is(Arg == AsEmphasized!(_), _))
518 		{
519 			if	  (form == VizForm.HTML)
520 				ppTaggedN(`em`, arg.args);
521 			else if (form == VizForm.jiraWikiMarkup)
522 			{
523 				ppRaw(`_`);
524 				ppN(arg.args);
525 				ppRaw(`_`);
526 			}
527 			else if (form == VizForm.Markdown)
528 			{
529 				ppRaw(`_`);
530 				ppN(arg.args);
531 				ppRaw(`_`);
532 			}
533 		}
534 		else static if (is(Arg == AsStronglyEmphasized!(_), _))
535 		{
536 			if (form == VizForm.Markdown)
537 			{
538 				ppRaw(`__`);
539 				ppN(arg.args);
540 				ppRaw(`__`);
541 			}
542 		}
543 		else static if (is(Arg == AsStrong!(_), _))
544 		{
545 			if	  (form == VizForm.HTML)
546 				ppTaggedN(`strong`, arg.args);
547 			else if (form == VizForm.jiraWikiMarkup)
548 			{
549 				ppRaw(`*`);
550 				ppN(arg.args);
551 				ppRaw(`*`);
552 			}
553 		}
554 		else static if (is(Arg == AsCitation!(_), _))
555 		{
556 			if	  (form == VizForm.HTML)
557 				ppTaggedN(`cite`, arg.args);
558 			else if (form == VizForm.jiraWikiMarkup)
559 			{
560 				ppRaw(`??`);
561 				ppN(arg.args);
562 				ppRaw(`??`);
563 			}
564 		}
565 		else static if (is(Arg == AsDeleted!(_), _))
566 		{
567 			if	  (form == VizForm.HTML)
568 				ppTaggedN(`deleted`, arg.args);
569 			else if (form == VizForm.jiraWikiMarkup)
570 			{
571 				ppRaw(`-`);
572 				ppN(arg.args);
573 				ppRaw(`-`);
574 			}
575 		}
576 		else static if (is(Arg == AsInserted!(_), _))
577 		{
578 			if	  (form == VizForm.HTML)
579 				ppTaggedN(`inserted`, arg.args);
580 			else if (form == VizForm.jiraWikiMarkup)
581 			{
582 				ppRaw(`+`);
583 				ppN(arg.args);
584 				ppRaw(`+`);
585 			}
586 		}
587 		else static if (is(Arg == AsSuperscript!(_), _))
588 		{
589 			if	  (form == VizForm.HTML)
590 				ppTaggedN(`sup`, arg.args);
591 			else if (form == VizForm.jiraWikiMarkup)
592 			{
593 				ppRaw(`^`);
594 				ppN(arg.args);
595 				ppRaw(`^`);
596 			}
597 		}
598 		else static if (is(Arg == AsSubscript!(_), _))
599 		{
600 			if	  (form == VizForm.HTML)
601 				ppTaggedN(`sub`, arg.args);
602 			else if (form == VizForm.jiraWikiMarkup)
603 			{
604 				ppRaw(`~`);
605 				ppN(arg.args);
606 				ppRaw(`~`);
607 			}
608 		}
609 		else static if (is(Arg == AsPreformatted!(_), _))
610 		{
611 			if	  (form == VizForm.HTML)
612 			{
613 				pplnTagOpen(`pre`);
614 				ppN(arg.args);
615 				pplnTagClose(`pre`);
616 			}
617 			else if (form == VizForm.jiraWikiMarkup)
618 			{
619 				pplnRaw(`{noformat}`);
620 				ppN(arg.args);
621 				pplnRaw(`{noformat}`);
622 			}
623 		}
624 		else static if (is(Arg == AsHeader!(_), _))
625 		{
626 			import std.conv: to;
627 			if	  (form == VizForm.HTML)
628 				pplnTaggedN(`h` ~ to!string(arg.level),
629 							arg.args);
630 			else if (form == VizForm.jiraWikiMarkup)
631 			{
632 				ppRaw(`h` ~ to!string(arg.level) ~ `. `);
633 				ppN(arg.args);
634 				pplnRaw(``);
635 			}
636 			else if (form == VizForm.Markdown)
637 			{
638 				foreach (_; 0 .. arg.level)
639 					pp1(0, `#`);
640 				ppN(` `, arg.args);
641 				pplnRaw(``);
642 			}
643 			else if (form == VizForm.textAsciiDoc ||
644 					 form == VizForm.textAsciiDocUTF8)
645 			{
646 				ppRaw('\n');
647 				foreach (_; 0 .. arg.level)
648 					pp1(0, `=`);
649 				ppN(' ', arg.args, ' ');
650 				foreach (_; 0 .. arg.level)
651 					pp1(0, `=`);
652 				ppRaw('\n');
653 			}
654 		}
655 		else static if (is(Arg == AsParagraph!(_), _))
656 		{
657 			if (form == VizForm.HTML)
658 				pplnTaggedN(`p`, arg.args);
659 			else if (form == VizForm.LaTeX)
660 			{
661 				ppRaw(`\par `);
662 				pplnTaggedN(arg.args);
663 			}
664 			else if (form == VizForm.textAsciiDoc ||
665 					 form == VizForm.textAsciiDocUTF8)
666 			{
667 				ppRaw('\n');
668 				foreach (_; 0 .. arg.level)
669 					pp1(0, `=`);
670 				ppN(` `, arg.args, ` `, tag, '\n');
671 			}
672 		}
673 		else static if (is(Arg == AsBlockquote!(_), _))
674 		{
675 			if (form == VizForm.HTML)
676 				pplnTaggedN(`blockquote`, arg.args);
677 			else if (form == VizForm.jiraWikiMarkup)
678 			{
679 				pplnRaw(`{quote}`);
680 				pplnRaw(arg.args);
681 				pplnRaw(`{quote}`);
682 			}
683 			else if (form == VizForm.Markdown)
684 			{
685 				foreach (subArg; arg.args)
686 					pplnRaw(`> `, subArg); /+ TODO: Iterate for each line in subArg +/
687 			}
688 		}
689 		else static if (is(Arg == AsBlockquoteSP!(_), _))
690 		{
691 			if (form == VizForm.jiraWikiMarkup)
692 			{
693 				ppRaw(`bq. `);
694 				ppN(arg.args);
695 				pplnRaw(``);
696 			}
697 		}
698 		else static if (is(HorizontalRuler == Arg))
699 		{
700 			if (form == VizForm.HTML)
701 				pplnTagOpen(`hr`);
702 			else if (form == VizForm.jiraWikiMarkup)
703 				pplnRaw(`----`);
704 		}
705 		else static if (is(Arg == MDash!(_), _))
706 		{
707 			if (form == VizForm.HTML)
708 				ppRaw(`&mdash;`);
709 			else if (form == VizForm.jiraWikiMarkup ||
710 					 form == VizForm.Markdown ||
711 					 form == VizForm.LaTeX)
712 			{
713 				pplnRaw(`---`);
714 			}
715 		}
716 		else static if (is(Arg == AsUList!(_), _))
717 		{
718 			if (form == VizForm.HTML)
719 				pplnTagOpen(`ul`);
720 			else if (form == VizForm.LaTeX)
721 				pplnRaw(`\begin{enumerate}`);
722 			ppN(arg.args);
723 			if (form == VizForm.HTML)
724 				pplnTagClose(`ul`);
725 			else if (form == VizForm.LaTeX)
726 				pplnRaw(`\end{enumerate}`);
727 		}
728 		else static if (is(Arg == AsOList!(_), _))
729 		{
730 			if (form == VizForm.HTML)
731 				pplnTagOpen(`ol`);
732 			else if (form == VizForm.LaTeX)
733 				pplnRaw(`\begin{itemize}`);
734 			ppN(arg.args);
735 			if (form == VizForm.HTML)
736 				pplnTagClose(`ol`);
737 			else if (form == VizForm.LaTeX)
738 				pplnRaw(`\end{itemize}`);
739 		}
740 		else static if (is(Arg == AsDescription!(_), _)) // if args .length == 1 && an InputRange of 2-tuples pairs
741 		{
742 			if (form == VizForm.HTML)
743 				pplnTagOpen(`dl`); /+ TODO: TERM <dt>, DEFINITION <dd> +/
744 			else if (form == VizForm.LaTeX)
745 				pplnRaw(`\begin{description}`); /+ TODO: \item[TERM] DEFINITION +/
746 			ppN(arg.args);
747 			if (form == VizForm.HTML)
748 				pplnTagClose(`dl`);
749 			else if (form == VizForm.LaTeX)
750 				pplnRaw(`\end{description}`);
751 		}
752 		else static if (is(Arg == AsTable!(_), _))
753 		{
754 			if (form == VizForm.HTML)
755 			{
756 				const border = (arg.border ? ` border=` ~ arg.border : ``);
757 				pplnTagOpen(`table` ~ border);
758 			}
759 			else if (form == VizForm.LaTeX)
760 				pplnRaw(`\begin{tabular}`);
761 
762 			static if (arg.args.length == 1 &&
763 					   isIterable!(typeof(arg.args[0])))
764 			{
765 				auto rows = arg.args[0].asRows();
766 				rows.recurseFlag = arg.recurseFlag; // propagate
767 				rows.rowNr = arg.rowNr;
768 				ppNFlushed(rows);
769 			}
770 			else
771 				ppN(arg.args);
772 
773 			if (form == VizForm.HTML)
774 				pplnTagClose(`table`);
775 			else if (form == VizForm.LaTeX)
776 				pplnRaw(`\end{tabular}`);
777 		}
778 		else static if (is(Arg == AsRows!(_), _) &&
779 						arg.args.length == 1 &&
780 						isIterable!(typeof(arg.args[0])))
781 		{
782 			bool capitalizeHeadings = true;
783 
784 			/* See_Also: http://forum.dlang.org/thread/wjksldfpkpenoskvhsqa@forum.dlang.org#post-jwfildowqrbwtamywsmy:40forum.dlang.org */
785 
786 			// use aggregate members as header
787 			import std.range.primitives : ElementType;
788 			alias Front = ElementType!(typeof(arg.args[0]));
789 			static if (is(Front == struct) ||
790 					   is(Front == union) ||
791 					   is(Front == class) ||
792 					   is(Front == interface)) // isAggregateType
793 			{
794 				/* TODO: When __traits(documentation,x)
795 				   here https://github.com/D-Programming-Language/dmd/pull/3531
796 				   get merged use it! */
797 				// pplnTaggedN(`tr`, subArg.asCols); /+ TODO: asItalic +/
798 				// Use __traits(allMembers, T) instead
799 				// Can we lookup file and line of user defined types aswell?
800 
801 				// member names header.
802 				if (form == VizForm.HTML)
803 					pplnTagOpen(`tr`); /+ TODO: Functionize +/
804 
805 				// index column
806 				if	  (arg.rowNr == RowNr.offsetZero)
807 					pplnTaggedN(`td`, `0-Offset`);
808 				else if (arg.rowNr == RowNr.offsetOne)
809 					pplnTaggedN(`td`, `1-Offset`);
810 				foreach (const ix, Member; typeof(Front.tupleof))
811 				{
812 					import std.ascii : isUpper; /+ TODO: support ASCII in fast path and Unicode in slow path +/
813 					import std.string : capitalize;
814 					import std.algorithm.iteration : joiner;
815 
816 					static	  if (is(Memb == struct))	immutable qual = `struct `;
817 					else static if (is(Memb == class))	 immutable qual = `class `;
818 					else static if (is(Memb == enum))	  immutable qual = `enum `;
819 					else static if (is(Memb == interface)) immutable qual = `interface `;
820 					else								   immutable qual = ``; /+ TODO: Are there more qualifiers +/
821 
822 					import std.algorithm.iteration : map;
823 					import nxt.slicing : preSlicer;
824 					immutable idName = __traits(identifier, Front.tupleof[ix]).preSlicer!isUpper.map!capitalize.joiner(` `); /+ TODO: reuse `nxt.casing.camelCasedToLowerSpaced` +/
825 					immutable typeName = Unqual!(Member).stringof; // constness of no interest here
826 
827 					pplnTaggedN(`td`,
828 								idName.asItalic.asBold,
829 								`<br>`,
830 								qual.asKeyword,
831 								typeName.asType);
832 				}
833 				if (form == VizForm.HTML)
834 					pplnTagClose(`tr`);
835 			}
836 
837 			size_t ix = 0;
838 			foreach (subArg; arg.args[0]) // for each table row
839 			{
840 				auto cols = subArg.asCols();
841 				cols.recurseFlag = arg.recurseFlag; // propagate
842 				cols.rowNr = arg.rowNr;
843 				cols.rowIx = ix;
844 				pplnTaggedN(`tr`, cols); // print columns
845 				ix++;
846 			}
847 		}
848 		else static if (is(Arg == AsCols!(_), _))
849 		{
850 			alias T_ = typeof(arg.args[0]);
851 			if (arg.args.length == 1 &&
852 				(is(T_ == struct) ||
853 				 is(T_ == class) ||
854 				 is(T_ == union) ||
855 				 is(T_ == interface))) // isAggregateType
856 			{
857 				auto args0 = arg.args[0];
858 				if (form == VizForm.jiraWikiMarkup)
859 				{
860 					/* if (args0.length >= 1) { ppRaw(`|`); } */
861 				}
862 				if	  (arg.rowNr == RowNr.offsetZero)
863 					pplnTaggedN(`td`, arg.rowIx + 0);
864 				else if (arg.rowNr == RowNr.offsetOne)
865 					pplnTaggedN(`td`, arg.rowIx + 1);
866 				foreach (subArg; args0.tupleof) // for each table column
867 				{
868 					if (form == VizForm.HTML)
869 						pplnTaggedN(`td`, subArg); // each element in aggregate as a column
870 					else if (form == VizForm.jiraWikiMarkup)
871 					{
872 						/* pp1(subArg); ppRaw(`|`); */
873 					}
874 				}
875 			}
876 			else
877 				pplnTaggedN(`tr`, arg.args);
878 		}
879 		else static if (is(Arg == AsRow!(_), _))
880 		{
881 			string spanArg;
882 			static if (arg.args.length == 1 &&
883 					   is(typeof(arg.args[0] == Span!(_), _)))
884 				spanArg ~= ` rowspan="` ~ to!string(arg._span) ~ `"`;
885 			if (form == VizForm.HTML)
886 				pplnTagOpen(`tr` ~ spanArg);
887 			ppN(arg.args);
888 			if (form == VizForm.HTML)
889 				pplnTagClose(`tr`);
890 		}
891 		else static if (is(Arg == AsCell!(_), _))
892 		{
893 			string spanArg;
894 			static if (arg.args.length != 0 &&
895 					   is(typeof(arg.args[0] == Span!(_), _)))
896 				spanArg ~= ` colspan="` ~ to!string(arg._span) ~ `"`;
897 			if (form == VizForm.HTML)
898 				ppTagOpen(`td` ~ spanArg);
899 			ppN(arg.args);
900 			if (form == VizForm.HTML)
901 				pplnTagClose(`td`);
902 		}
903 		else static if (is(Arg == AsTHeading!(_), _))
904 		{
905 			if (form == VizForm.HTML)
906 			{
907 				pplnTagOpen(`th`);
908 				ppN(arg.args);
909 				pplnTagClose(`th`);
910 			}
911 			else if (form == VizForm.jiraWikiMarkup)
912 			{
913 				if (args.length != 0)
914 					ppRaw(`||`);
915 				foreach (subArg; args)
916 				{
917 					pp1(subArg);
918 					ppRaw(`||`);
919 				}
920 			}
921 		}
922 		else static if (is(Arg == AsItem!(_), _))
923 		{
924 			if (form == VizForm.HTML)
925 				ppTagOpen(`li`);
926 			else if (form == VizForm.textAsciiDoc)
927 				ppRaw(` - `);   // if inside ordered list use . instead of -
928 			else if (form == VizForm.LaTeX)
929 				ppRaw(`\item `);
930 			else if (form == VizForm.textAsciiDocUTF8)
931 				ppRaw(` • `);
932 			else if (form == VizForm.Markdown)
933 				ppRaw(`* `); /+ TODO: Alternatively +,-,*, or 1. TODO: Need counter for ordered lists +/
934 			ppN(arg.args);
935 			if (form == VizForm.HTML)
936 				pplnTagClose(`li`);
937 			else if (form == VizForm.LaTeX)
938 				pplnRaw(``);
939 			else if (form == VizForm.textAsciiDoc ||
940 					 form == VizForm.textAsciiDocUTF8 ||
941 					 form == VizForm.Markdown)
942 				pplnRaw(``);
943 		}
944 		else static if (is(Arg == AsPath!(_), _) ||
945 						is(Arg == AsURL!(_), _))
946 		{
947 			auto vizArg = this;
948 			vizArg.treeFlag = false;
949 
950 			enum isString = isSomeString!(typeof(arg.arg)); // only create hyperlink if arg is a string
951 
952 			static if (isString)
953 				if (form == VizForm.HTML)
954 				{
955 					static if (is(Arg == AsPath!(_), _))
956 						ppTagOpen(`a href="file://` ~ arg.arg ~ `"`);
957 					else static if (is(Arg == AsURL!(_), _))
958 						ppTagOpen(`a href="` ~ arg.arg ~ `"`);
959 				}
960 
961 			vizArg.pp1(depth + 1, arg.arg);
962 
963 			static if (isString)
964 				if (form == VizForm.HTML)
965 					ppTagClose(`a`);
966 		}
967 		else static if (is(Arg == AsName!(_), _))
968 		{
969 			auto vizArg = viz;
970 			vizArg.treeFlag = true;
971 			pp1(term, vizArg, depth + 1, arg.arg);
972 		}
973 		// else static if (is(Arg == AsHit!(_), _))
974 		// {
975 		//	 const ixs = to!string(arg.ix);
976 		//	 if (form == VizForm.HTML) { ppTagOpen(`hit` ~ ixs); }
977 		//	 pp1(depth + 1, arg.args);
978 		//	 if (form == VizForm.HTML) { ppTagClose(`hit` ~ ixs); }
979 		// }
980 		// else static if (is(Arg == AsCtx!(_), _))
981 		// {
982 		//	 if (form == VizForm.HTML) { ppTagOpen(`hit_context`); }
983 		//	 pp1(depth + 1, arg.args);
984 		//	 if (form == VizForm.HTML) { ppTagClose(`hit_context`); }
985 		// }
986 		else static if (isArray!Arg &&
987 						!isSomeString!Arg)
988 		{
989 			ppRaw(`[`);
990 			foreach (ix, subArg; arg)
991 			{
992 				if (ix >= 1)
993 					ppRaw(`,`); // separator
994 				pp1(depth + 1, subArg);
995 			}
996 			ppRaw(`]`);
997 		}
998 		else static if (isInputRange!Arg)
999 		{
1000 			foreach (subArg; arg)
1001 				pp1(depth + 1, subArg);
1002 		}
1003 		else static if (__traits(hasMember, arg, `parent`)) /+ TODO: Use isFile = File or NonNull!File +/
1004 		{
1005 			import std.path: dirSeparator;
1006 			if (form == VizForm.HTML)
1007 			{
1008 				ppRaw(`<a href="file://`);
1009 				ppPut(arg.path);
1010 				ppRaw(`">`);
1011 			}
1012 
1013 			if (!treeFlag)
1014 			{
1015 				// write parent path
1016 				foreach (parent; arg.parents)
1017 				{
1018 					ppPut(dirSeparator);
1019 					if (form == VizForm.HTML)
1020 						ppTagOpen(`b`);
1021 					ppPut(dirFace, parent.name);
1022 					if (form == VizForm.HTML)
1023 						ppTagClose(`b`);
1024 				}
1025 				ppPut(dirSeparator);
1026 			}
1027 
1028 			// write name
1029 			static if (__traits(hasMember, arg, `isRoot`)) /+ TODO: Use isDir = Dir or NonNull!Dir +/
1030 				immutable name = arg.isRoot ? dirSeparator : arg.name ~ dirSeparator;
1031 			else
1032 				immutable name = arg.name;
1033 
1034 			if (form == VizForm.HTML)
1035 			{
1036 				// static	  if (isSymlink!Arg) { ppTagOpen(`i`); }
1037 				// static if (isDir!Arg) { ppTagOpen(`b`); }
1038 			}
1039 
1040 			ppPut(arg.getFace(), name);
1041 
1042 			if (form == VizForm.HTML)
1043 			{
1044 				// static	  if (isSymlink!Arg) { ppTagClose(`i`); }
1045 				// static if (isDir!Arg) { ppTagClose(`b`); }
1046 			}
1047 
1048 			if (form == VizForm.HTML)
1049 				ppTagClose(`a`);
1050 		}
1051 		else
1052 		{
1053 			static if (__traits(hasMember, arg, `path`))
1054 				const arg_string = arg.path;
1055 			else
1056 			{
1057 				import std.conv: to;
1058 				const arg_string = to!string(arg);
1059 			}
1060 
1061 			static if (__traits(hasMember, arg, `face`) &&
1062 					   __traits(hasMember, arg.face, `tagsHTML`))
1063 			{
1064 				if (form == VizForm.HTML)
1065 					foreach (tag; arg.face.tagsHTML)
1066 						outFile.write(`<`, tag, `>`);
1067 			}
1068 
1069 			// write
1070 			term.setFace(arg.getFace(), colorFlag);
1071 			if (outFile == stdout)
1072 				term.write(arg_string);
1073 			else
1074 				ppPut(arg.getFace(), arg_string);
1075 
1076 			static if (__traits(hasMember, arg, `face`) &&
1077 					   __traits(hasMember, arg.face, `tagsHTML`))
1078 			{
1079 				if (form == VizForm.HTML)
1080 					foreach (tag; arg.face.tagsHTML)
1081 						outFile.write(`</`, tag, `>`);
1082 			}
1083 		}
1084 	}
1085 
1086 	/** Pretty-Print Multiple Arguments `args` to Terminal `term`. */
1087 	void ppN(Args...)(Args args)
1088 	{
1089 		foreach (arg; args)
1090 			pp1(0, arg);
1091 	}
1092 
1093 	/** Pretty-Print Arguments `args` to Terminal `term` without Line Termination. */
1094 	void ppNFlushed(Args...)(Args args)
1095 	{
1096 		ppN(args);
1097 		if (outFile == stdout)
1098 			term.flush();
1099 	}
1100 
1101 	/** Pretty-Print Arguments `args` including final line termination. */
1102 	void ppln(Args...)(Args args)
1103 	{
1104 		ppN(args);
1105 		if (outFile == stdout)
1106 		{
1107 			term.writeln(lbr(form == VizForm.HTML));
1108 			term.flush();
1109 		}
1110 		else
1111 			outFile.writeln(lbr(form == VizForm.HTML));
1112 	}
1113 
1114 	/** Pretty-Print Arguments `args` each including a final line termination. */
1115 	void pplns(Args...)(Args args)
1116 	{
1117 		foreach (arg; args)
1118 			ppln(args);
1119 	}
1120 
1121 	/** Print End of Line to Terminal `term`. */
1122 	void ppendl()
1123 	{
1124 		ppln(``);
1125 	}
1126 
1127 }
1128 
1129 /// Face with color of type `SomeColor`.
1130 struct Face
1131 {
1132 	this(Color foregroundColor,
1133 		 Color backgroundColor,
1134 		 bool bright = false,
1135 		 bool italic = false,
1136 		 string[] tagsHTML = [])
1137 	{
1138 		this.foregroundColor = foregroundColor;
1139 		this.backgroundColor = backgroundColor;
1140 		this.bright = bright;
1141 		this.tagsHTML = tagsHTML;
1142 	}
1143 	string[] tagsHTML;
1144 	Color foregroundColor;
1145 	Color backgroundColor;
1146 	bool bright;
1147 	bool italic;
1148 }
1149 
1150 // Faces (Font/Color)
1151 enum stdFace = Face(Color.white, Color.black);
1152 enum pathFace = Face(Color.green, Color.black, true);
1153 
1154 enum dirFace = Face(Color.blue, Color.black, true);
1155 enum fileFace = Face(Color.magenta, Color.black, true);
1156 enum baseNameFace = fileFace;
1157 enum specialFileFace = Face(Color.red, Color.black, true);
1158 enum regFileFace = Face(Color.white, Color.black, true, false, [`b`]);
1159 enum symlinkFace = Face(Color.cyan, Color.black, true, true, [`i`]);
1160 enum symlinkBrokenFace = Face(Color.red, Color.black, true, true, [`i`]);
1161 enum missingSymlinkTargetFace = Face(Color.red, Color.black, false, true, [`i`]);
1162 
1163 enum contextFace = Face(Color.green, Color.black);
1164 
1165 enum timeFace = Face(Color.magenta, Color.black);
1166 enum digestFace = Face(Color.yellow, Color.black);
1167 
1168 enum infoFace = Face(Color.white, Color.black, true);
1169 enum warnFace = Face(Color.yellow, Color.black);
1170 enum kindFace = warnFace;
1171 enum errorFace = Face(Color.red, Color.black);
1172 
1173 enum titleFace = Face(Color.white, Color.black, false, false, [`title`]);
1174 enum h1Face = Face(Color.white, Color.black, false, false, [`h1`]);
1175 
1176 // Support these as immutable
1177 
1178 /** Key (Hit) Face Palette. */
1179 enum ctxFaces = [Face(Color.red, Color.black),
1180 				 Face(Color.green, Color.black),
1181 				 Face(Color.blue, Color.black),
1182 				 Face(Color.cyan, Color.black),
1183 				 Face(Color.magenta, Color.black),
1184 				 Face(Color.yellow, Color.black),
1185 	];
1186 
1187 import std.algorithm.iteration : map;
1188 
1189 /** Key (Hit) Faces. */
1190 enum keyFaces = ctxFaces.map!(a => Face(a.foregroundColor, a.backgroundColor, true)); /+ TODO: avoid map +/
1191 
1192 void setFace(Term, Face)(ref Term term,
1193 						 Face face,
1194 						 bool colorFlag)
1195 {
1196 	if (colorFlag)
1197 	{
1198 		version (useTerm)
1199 			term.color(face.foregroundColor | (face.bright ? Bright : 0) ,
1200 					   face.backgroundColor);
1201 	}
1202 }
1203 
1204 /** Fazed (Rich) Text. */
1205 struct Fazed(T)
1206 {
1207 	T text;
1208 	const Face face;
1209 	string toString() const return scope @property pure nothrow
1210 	{
1211 		import std.conv : to;
1212 		return to!string(text);
1213 	}
1214 }
1215 auto faze(T)(T text, in Face face = stdFace) @safe pure nothrow
1216 {
1217 	return Fazed!T(text, face);
1218 }
1219 
1220 auto getFace(Arg)(in Arg arg) @safe pure nothrow
1221 {
1222 	// pick face
1223 	static if (__traits(hasMember, arg, `face`))
1224 		return arg.face;
1225 	// else static if (is(Arg == Digest!(_), _)) // instead of is(Unqual!(Arg) == SHA1Digest)
1226 	// {
1227 	//	 return digestFace;
1228 	// }
1229 	// else static if (is(Arg == AsHit!(_), _))
1230 	// {
1231 	//	 return keyFaces.cycle[arg.ix];
1232 	// }
1233 	// else static if (is(Arg == AsCtx!(_), _))
1234 	// {
1235 	//	 return ctxFaces.cycle[arg.ix];
1236 	// }
1237 	else
1238 		return stdFace;
1239 }
1240 
1241 /** Show `viz`.
1242  */
1243 void show(Viz viz)
1244 {
1245 	viz.completeIfNeeded();
1246 	import std.process : spawnProcess, wait;
1247 	auto pid = spawnProcess([`xdg-open`, viz.outFile.name]);
1248 	assert(wait(pid) == 0);
1249 }
1250 
1251 version (show)
1252 unittest {
1253 	import std.algorithm.iteration : map;
1254 
1255 	const outPath = `/tmp/fs-` ~ randomUUID.toString() ~ `.` ~ `html`; // reuse `nxt.tempfs`
1256 	File outFile = File(outPath, `w`);
1257 
1258 	auto term = Terminal(ConsoleOutputType.linear);
1259 
1260 	auto viz = new Viz(outFile, &term, VizForm.HTML);
1261 
1262 	viz.pp1(`Pretty Printing`.asH!1);
1263 	viz.pp1(horizontalRuler);
1264 
1265 	viz.pp1(`First Heading`.asH!2);
1266 	viz.ppln(`Something first.`);
1267 
1268 	viz.pp1(`Second Heading`.asH!2);
1269 	viz.ppln(`Something else.`);
1270 
1271 	struct S
1272 	{
1273 		string theUnit;
1274 		int theSuperValue;
1275 	}
1276 
1277 	S[] s = [S("meter", 42),
1278 			 S("second", 43)];
1279 
1280 	viz.pp1("Struct Array".asH!2);
1281 	viz.pp1(s.asTable);
1282 
1283 	viz.pp1("Map Struct Array".asH!2);
1284 	viz.pp1(s.map!(_ => S(_.theUnit,
1285 						  _.theSuperValue^^2)).asTable);
1286 
1287 	viz.pp1("Rational Number Array".asH!2);
1288 	viz.pp1([rational(11, 13),
1289 			 rational(14, 15),
1290 			 rational(17, 32)]);
1291 
1292 	struct NamedRational
1293 	{
1294 		string name;
1295 		Rational!long value;
1296 	}
1297 
1298 	viz.pp1("Named Rational Number Array as Table".asH!2);
1299 	viz.pp1([NamedRational("x", Rational!long(11, 13)),
1300 			 NamedRational("y", Rational!long(111, 133)),
1301 			 NamedRational("z", Rational!long(1111, 1333))].asTable);
1302 	viz.show();
1303 }
1304 
1305 version (unittest)
1306 {
1307 	/+ TODO: hide these stuff in constructor for Viz +/
1308 	import std.uuid : randomUUID;
1309 	import std.stdio : File;
1310 	import nxt.rational : rational;
1311 }