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