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.algorithm.iteration : map;
34 import std.range.primitives : isInputRange;
35 import std.traits : isInstanceOf, isSomeString, Unqual, isArray, isIterable;
36 import std.stdio : stdout;
37 
38 /* TODO Move logic (toHTML) to these deps and remove these imports */
39 import nxt.color : Color;
40 import nxt.mathml;
41 import nxt.lingua;
42 import nxt.attributes;
43 import nxt.rational : Rational;
44 
45 import arsd.terminal : Terminal, ConsoleOutputType, Bright;
46 
47 /// See_Also: http://forum.dlang.org/thread/fihymjelvnwfevegwryt@forum.dlang.org#post-fihymjelvnwfevegwryt:40forum.dlang.org
48 template Concise(Tuple)
49 {
50     static if(isTuple!Tuple)
51     {
52         struct Concise(Tuple)
53         {
54             Tuple tup;
55             alias tup this;
56             // should use a better overload
57             string toString() const
58             {
59                 auto app = appender!string(); // TODO use array_ex.Array instead
60                 app.put(`(`);
61                 app.put(to!string(concise(tuple[0])));
62                 foreach (t; tuple.expand[1 .. $])
63                 {
64                     app.put(`, `);
65                     app.put(to!string(concise(t)));
66                 }
67                 app.put(`)`);
68                 return app.data;
69             }
70         }
71     }
72     else
73     {
74         alias Concise = Tuple;
75     }
76 }
77 
78 auto concise(T)(T t) { return Concise!T(t); }
79 
80 /** Returns: Duration `dur` in a Level-Of-Detail (LOD) string
81     representation.
82 */
83 string shortDurationString(in Duration dur) @safe pure nothrow
84 {
85     import std.conv: to;
86     immutable weeks = dur.total!`weeks`;
87     if (weeks)
88     {
89         if (weeks < 52)
90         {
91             return to!string(weeks) ~ ` week` ~ (weeks >= 2 ? `s` : ``);
92         }
93         else
94         {
95             immutable years = weeks / 52;
96             immutable weeks_rest = weeks % 52;
97             return to!string(years) ~ ` year` ~ (years >= 2 ? `s` : ``) ~
98             ` and ` ~
99             to!string(weeks_rest) ~ ` week` ~ (weeks_rest >= 2 ? `s` : ``);
100         }
101     }
102     immutable days = dur.total!`days`;       if (days)    return to!string(days) ~ ` day` ~ (days >= 2 ? `s` : ``);
103     immutable hours = dur.total!`hours`;     if (hours)   return to!string(hours) ~ ` hour` ~ (hours >= 2 ? `s` : ``);
104     immutable minutes = dur.total!`minutes`; if (minutes) return to!string(minutes) ~ ` minute` ~ (minutes >= 2 ? `s` : ``);
105     immutable seconds = dur.total!`seconds`; if (seconds) return to!string(seconds) ~ ` second` ~ (seconds >= 2 ? `s` : ``);
106     immutable msecs = dur.total!`msecs`;     if (msecs) return to!string(msecs) ~ ` millisecond` ~ (msecs >= 2 ? `s` : ``);
107     immutable usecs = dur.total!`usecs`;     if (usecs) return to!string(usecs) ~ ` microsecond` ~ (msecs >= 2 ? `s` : ``);
108     immutable nsecs = dur.total!`nsecs`;     return to!string(nsecs) ~ ` nanosecond` ~ (msecs >= 2 ? `s` : ``);
109 }
110 
111 /** Visual Form(at). */
112 enum VizForm
113 {
114     textAsciiDoc,               // TODO.
115     textAsciiDocUTF8,           // TODO.
116     HTML,
117     D3js,                       // TODO. See_Also: http://d3js.org/
118     LaTeX,                      // TODO.
119     jiraWikiMarkup, // TODO. See_Also: https://jira.atlassiana.com/secure/WikiRendererHelpAction.jspa?section=all
120     Markdown,       // TODO.
121 }
122 
123 /** Solarized light color theme.
124  *
125  * Each enumerator is an RGB hex string.
126  *
127  * TODO use RGB-type as enumerator instead
128  *
129  * See_Also: http://ethanschoonover.com/solarized
130  */
131 enum SolarizedLightColorTheme_hexstring
132 {
133     base00  = `657b83`,
134     base01  = `586e75`,
135     base02  = `073642`,
136     base03  = `002b36`,
137 
138     base0   = `839496`,
139     base1   = `93a1a1`,
140     base2   = `eee8d5`,
141     base3   = `fdf6e3`,
142 
143     yellow  = `b58900`,
144     orange  = `cb4b16`,
145     red     = `dc322f`,
146     magenta = `d33682`,
147     viole   = `6c71c4`,
148     blue    = `268bd2`,
149     cya     = `2aa198`,
150     gree    = `859900`
151 }
152 
153 /** HTML tags with no side-effect when its arguments is empty.
154     See_Also: http://www.w3schools.com/html/html_formatting.asp
155 */
156 static immutable nonStateHTMLTags = [`b`, `i`, `strong`, `em`, `sub`, `sup`, `small`, `ins`, `del`, `mark`,
157                                      `code`, `kbd`, `samp`, `samp`, `var`, `pre`];
158 
159 static immutable htmlHeader = `<!DOCTYPE html>
160 <html>
161 <head>
162 <meta charset="UTF-8"/>
163 <style>
164 
165 body { font: 10px Verdana, sans-serif; }
166 hit0 { background-color:#F2B701; border: solid 0px grey; }
167 hit1 { background-color:#F18204; border: solid 0px grey; }
168 hit2 { background-color:#F50035; border: solid 0px grey; }
169 hit3 { background-color:#F5007A; border: solid 0px grey; }
170 hit4 { background-color:#A449B6; border: solid 0px grey; }
171 hit5 { background-color:#3A70BB; border: solid 0px grey; }
172 hit6 { background-color:#0DE7A6; border: solid 0px grey; }
173 hit7 { background-color:#70AD48; border: solid 0px grey; }
174 
175 hit_context { background-color:#c0c0c0; border: solid 0px grey; }
176 
177 code { background-color:#FFFFE0; }
178 
179 dlang-byte   { background-color:#FFFFE0; }
180 dlang-short  { background-color:#FFFFE0; }
181 dlang-int    { background-color:#FFFFE0; }
182 dlang-long   { background-color:#FFFFE0; }
183 
184 dlang-ubyte   { background-color:#FFFFE0; }
185 dlang-ushort  { background-color:#FFFFE0; }
186 dlang-uint    { background-color:#FFFFE0; }
187 dlang-ulong   { background-color:#FFFFE0; }
188 
189 dlang-bool   { background-color:#FFFFE0; }
190 
191 dlang-float  { background-color:#FFFFE0; }
192 dlang-double { background-color:#FFFFE0; }
193 dlang-real   { background-color:#FFFFE0; }
194 
195 dlang-char   { background-color:#FFFFE0; }
196 dlang-wchar  { background-color:#FFFFE0; }
197 dlang-dchar  { background-color:#FFFFE0; }
198 
199 dlang-string  { background-color:#FFFFE0; }
200 dlang-wstring { background-color:#FFFFE0; }
201 dlang-dstring { background-color:#FFFFE0; }
202 
203 td, th { border: 1px solid black; }
204 table { border-collapse: collapse; }
205 
206 tr:nth-child(even) { background-color: #EBEBEB; }
207 tr:nth-child(2n+0) { background: #` ~ SolarizedLightColorTheme_hexstring.base2 ~ `; }
208 tr:nth-child(2n+1) { background: #` ~ SolarizedLightColorTheme_hexstring.base3 ~ `; }
209 
210 </style>
211 </head>
212 <body>
213 `;
214 
215 /** Visual Backend.
216  */
217 class Viz
218 {
219     import std.stdio : ioFile = File;
220 
221     ioFile outFile;
222     Terminal* term;
223 
224     bool treeFlag;
225     VizForm form;
226 
227     bool colorFlag;
228     bool flushNewlines = true;
229 
230     /* If any (HTML) tags should be ended with a newline.
231        This increases the readability of generated HTML code.
232      */
233     bool newlinedTags = true;
234 
235     this(ioFile outFile,
236          Terminal* term,
237          VizForm form = VizForm.textAsciiDocUTF8,
238          bool treeFlag = true,
239          bool colorFlag = true,
240          bool flushNewlines = true,
241          bool newlinedTags = true,
242         )
243     {
244         this.outFile = outFile;
245         this.term = term;
246         this.treeFlag = treeFlag;
247         this.form = form;
248         this.colorFlag = colorFlag;
249         this.flushNewlines = flushNewlines;
250         this.newlinedTags = newlinedTags;
251         if (form == VizForm.HTML)
252         {
253             ppRaw(this, htmlHeader);
254         }
255     }
256 
257     ~this()
258     {
259         if (form == VizForm.HTML)
260         {
261             ppRaw("</body>\n</html>");
262         }
263     }
264 
265     /** Put `arg` to `this` without any conversion nor coloring. */
266     void ppRaw(T...)(T args)
267     {
268         foreach (const arg; args)
269         {
270             if (outFile == stdout)
271             {
272                 term.write(arg); // trick
273             }
274             else
275             {
276                 outFile.write(arg);
277             }
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                 {
290                     term.writeln(arg);
291                 }
292                 else
293                 {
294                     term.write(arg, '\n');
295                 }
296             }
297             else
298             {
299                 if (flushNewlines)
300                 {
301                     outFile.writeln(arg);
302                 }
303                 else
304                 {
305                     outFile.write(arg, '\n');
306                 }
307             }
308         }
309     }
310 
311     /// Print opening of tag `tag`.
312     void ppTagOpen(T, P...)(T tag, P params)
313     {
314         if (form == VizForm.HTML)
315         {
316             ppRaw(`<` ~ tag);
317             foreach (param; params)
318             {
319                 ppRaw(' ', param);
320             }
321             ppRaw(`>`);
322         }
323     }
324 
325     /// Print closing of tag `tag`.
326     void ppTagClose(T)(T tag)
327     {
328         immutable arg = (form == VizForm.HTML) ? `</` ~ tag ~ `>` : tag;
329         ppRaw(arg);
330     }
331 
332     /// Print opening of tag `tag` on a separate line.
333     void pplnTagOpen(T)(T tag)
334     {
335         immutable arg = (form == VizForm.HTML) ? `<` ~ tag ~ `>` : tag;
336         if (newlinedTags)
337         {
338             pplnRaw(arg);
339         }
340         else
341         {
342             ppRaw(arg);
343         }
344     }
345 
346     /// Print closing of tag `tag` on a separate line.
347     void pplnTagClose(T)(T tag)
348     {
349         immutable arg = (form == VizForm.HTML) ? `</` ~ tag ~ `>` : tag;
350         if (newlinedTags)
351         {
352             pplnRaw(arg);
353         }
354         else
355         {
356             ppRaw(arg);
357         }
358     }
359 
360     /** Put `arg` to `viz` possibly with conversion. */
361     void ppPut(T)(T arg,
362                   bool nbsp = true)
363     {
364         if (outFile == stdout)
365         {
366             term.write(arg);
367         }
368         else
369         {
370             if (form == VizForm.HTML)
371             {
372                 import nxt.w3c : encodeHTML;
373                 outFile.write(arg.encodeHTML(nbsp));
374             }
375             else
376             {
377                 outFile.write(arg);
378             }
379         }
380     }
381 
382     /** Put `arg` to `viz` possibly with conversion. */
383     void ppPut(T)(Face face,
384                   T arg,
385                   bool nbsp = true)
386     {
387         term.setFace(face, colorFlag);
388         ppPut(arg, nbsp);
389     }
390 
391     /** Print `args` tagged as `tag`. */
392     void ppTaggedN(Tag, Args...)(in Tag tag, Args args)
393     if (isSomeString!Tag)
394     {
395         import std.algorithm.searching : find;
396         static if (args.length == 1 &&
397                    isSomeString!(typeof(args[0])))
398         {
399             import std.string : empty;
400             if (form == VizForm.HTML &&
401                 args[0].empty &&
402                 !nonStateHTMLTags.find(tag).empty)
403             {
404                 return;         // skip HTML tags with no content
405             }
406         }
407         if (form == VizForm.HTML) { ppRaw(`<` ~ tag ~ `>`); }
408         ppN(args);
409         if (form == VizForm.HTML) { ppRaw(`</` ~ tag ~ `>`); }
410     }
411 
412     /** Print `args` tagged as `tag` on a separate line. */
413     void pplnTaggedN(Tag, Args...)(in Tag tag, Args args)
414     if (isSomeString!Tag)
415     {
416         ppTaggedN(tag, args);
417         if (newlinedTags)
418             pplnRaw(``);
419     }
420 
421     // TODO Check for MathML support on backend
422     @property void ppMathML(SomeIntegral)(Rational!SomeIntegral arg)
423     {
424         ppTagOpen(`math`);
425         ppTagOpen(`mfrac`);
426         ppTaggedN(`mi`, arg.numerator);
427         ppTaggedN(`mi`, arg.denominator);
428         ppTagClose(`mfrac`);
429         ppTagClose(`math`);
430     }
431 
432     /** Pretty-Print Single Argument `arg` to Terminal `term`. */
433     void pp1(Arg)(Arg arg)
434     {
435         pp1(0, arg);
436     }
437 
438     /** Pretty-Print Single Argument `arg` to Terminal `term`. */
439     void pp1(Arg)(int depth,
440                   Arg arg)
441     {
442         static if (is(typeof(ppMathML(arg))))
443         {
444             if (form == VizForm.HTML)
445             {
446                 return ppMathML(arg);
447             }
448         }
449         static if (is(typeof(arg.toMathML)))
450         {
451             if (form == VizForm.HTML)
452             {
453                 // TODO Check for MathML support on backend
454                 return ppRaw(arg.toMathML);
455             }
456         }
457         static if (is(typeof(arg.toHTML)))
458         {
459             if (form == VizForm.HTML)
460             {
461                 return ppRaw(arg.toHTML);
462             }
463         }
464         static if (is(typeof(arg.toLaTeX)))
465         {
466             if (form == VizForm.LaTeX)
467             {
468                 return ppRaw(arg.toLaTeX);
469             }
470         }
471 
472         /* TODO Check if any member has mmber toMathML if so call it otherwise call
473          * toString. */
474 
475         static if (isInstanceOf!(AsWords, Arg))
476         {
477             foreach (ix, subArg; arg.args)
478             {
479                 static if (ix >= 1)
480                     ppRaw(` `); // separator
481                 pp1(depth + 1, subArg);
482             }
483         }
484         else static if (isInstanceOf!(AsCSL, Arg))
485         {
486             foreach (ix, subArg; arg.args)
487             {
488                 static if (ix >= 1)
489                 {
490                     pp1(depth + 1, `,`); // separator
491                 }
492                 static if (isInputRange!(typeof(subArg)))
493                 {
494                     foreach (subsubArg; subArg)
495                     {
496                         ppN(subsubArg, `,`);
497                     }
498                 }
499             }
500         }
501         else static if (isInstanceOf!(AsBold, Arg))
502         {
503             if      (form == VizForm.HTML)
504             {
505                 ppTaggedN(`b`, arg.args);
506             }
507             else if (form == VizForm.Markdown)
508             {
509                 ppRaw(`**`);
510                 ppN(arg.args);
511                 ppRaw(`**`);
512             }
513         }
514         else static if (isInstanceOf!(AsItalic, Arg))
515         {
516             if      (form == VizForm.HTML)
517             {
518                 ppTaggedN(`i`, arg.args);
519             }
520             else if (form == VizForm.Markdown)
521             {
522                 ppRaw(`*`);
523                 ppN(arg.args);
524                 ppRaw(`*`);
525             }
526         }
527         else static if (isInstanceOf!(AsMonospaced, Arg))
528         {
529             if      (form == VizForm.HTML)
530             {
531                 ppTaggedN(`tt`, arg.args);
532             }
533             else if (form == VizForm.jiraWikiMarkup)
534             {
535                 ppRaw(`{{`);
536                 ppN(arg.args);
537                 ppRaw(`}}`);
538             }
539             else if (form == VizForm.Markdown)
540             {
541                 ppRaw('`');
542                 ppN(arg.args);
543                 ppRaw('`');
544             }
545         }
546         else static if (isInstanceOf!(AsCode, Arg))
547         {
548             if      (form == VizForm.HTML)
549             {
550                 /* TODO Use arg.language member to highlight using fs tokenizers
551                  * which must be moved out of fs. */
552                 ppTaggedN(`code`, arg.args);
553             }
554             else if (form == VizForm.jiraWikiMarkup)
555             {
556                 ppRaw(arg.language ? `{code:` ~ arg.language ~ `}` : `{code}`);
557                 ppN(arg.args);
558                 ppRaw(`{code}`);
559             }
560         }
561         else static if (isInstanceOf!(AsEmphasized, Arg))
562         {
563             if      (form == VizForm.HTML)
564             {
565                 ppTaggedN(`em`, arg.args);
566             }
567             else if (form == VizForm.jiraWikiMarkup)
568             {
569                 ppRaw(`_`);
570                 ppN(arg.args);
571                 ppRaw(`_`);
572             }
573             else if (form == VizForm.Markdown)
574             {
575                 ppRaw(`_`);
576                 ppN(arg.args);
577                 ppRaw(`_`);
578             }
579         }
580         else static if (isInstanceOf!(AsStronglyEmphasized, Arg))
581         {
582             if (form == VizForm.Markdown)
583             {
584                 ppRaw(`__`);
585                 ppN(arg.args);
586                 ppRaw(`__`);
587             }
588         }
589         else static if (isInstanceOf!(AsStrong, Arg))
590         {
591             if      (form == VizForm.HTML)
592             {
593                 ppTaggedN(`strong`, arg.args);
594             }
595             else if (form == VizForm.jiraWikiMarkup)
596             {
597                 ppRaw(`*`);
598                 ppN(arg.args);
599                 ppRaw(`*`);
600             }
601         }
602         else static if (isInstanceOf!(AsCitation, Arg))
603         {
604             if      (form == VizForm.HTML)
605             {
606                 ppTaggedN(`cite`, arg.args);
607             }
608             else if (form == VizForm.jiraWikiMarkup)
609             {
610                 ppRaw(`??`);
611                 ppN(arg.args);
612                 ppRaw(`??`);
613             }
614         }
615         else static if (isInstanceOf!(AsDeleted, Arg))
616         {
617             if      (form == VizForm.HTML)
618             {
619                 ppTaggedN(`deleted`, arg.args);
620             }
621             else if (form == VizForm.jiraWikiMarkup)
622             {
623                 ppRaw(`-`);
624                 ppN(arg.args);
625                 ppRaw(`-`);
626             }
627         }
628         else static if (isInstanceOf!(AsInserted, Arg))
629         {
630             if      (form == VizForm.HTML)
631             {
632                 ppTaggedN(`inserted`, arg.args);
633             }
634             else if (form == VizForm.jiraWikiMarkup)
635             {
636                 ppRaw(`+`);
637                 ppN(arg.args);
638                 ppRaw(`+`);
639             }
640         }
641         else static if (isInstanceOf!(AsSuperscript, Arg))
642         {
643             if      (form == VizForm.HTML)
644             {
645                 ppTaggedN(`sup`, arg.args);
646             }
647             else if (form == VizForm.jiraWikiMarkup)
648             {
649                 ppRaw(`^`);
650                 ppN(arg.args);
651                 ppRaw(`^`);
652             }
653         }
654         else static if (isInstanceOf!(AsSubscript, Arg))
655         {
656             if      (form == VizForm.HTML)
657             {
658                 ppTaggedN(`sub`, arg.args);
659             }
660             else if (form == VizForm.jiraWikiMarkup)
661             {
662                 ppRaw(`~`);
663                 ppN(arg.args);
664                 ppRaw(`~`);
665             }
666         }
667         else static if (isInstanceOf!(AsPreformatted, Arg))
668         {
669             if      (form == VizForm.HTML)
670             {
671                 pplnTagOpen(`pre`);
672                 ppN(arg.args);
673                 pplnTagClose(`pre`);
674             }
675             else if (form == VizForm.jiraWikiMarkup)
676             {
677                 pplnRaw(`{noformat}`);
678                 ppN(arg.args);
679                 pplnRaw(`{noformat}`);
680             }
681         }
682         else static if (isInstanceOf!(AsHeader, Arg))
683         {
684             if      (form == VizForm.HTML)
685             {
686                 import std.conv: to;
687                 pplnTaggedN(`h` ~ to!string(arg.level),
688                             arg.args);
689             }
690             else if (form == VizForm.jiraWikiMarkup)
691             {
692                 import std.conv: to;
693                 ppRaw(`h` ~ to!string(arg.level) ~ `. `);
694                 ppN(arg.args);
695                 pplnRaw(``);
696             }
697             else if (form == VizForm.Markdown)
698             {
699                 foreach (_; 0 .. arg.level) { pp1(0, `#`); }
700                 ppN(` `, arg.args);
701                 pplnRaw(``);
702             }
703             else if (form == VizForm.textAsciiDoc ||
704                      form == VizForm.textAsciiDocUTF8)
705             {
706                 ppRaw('\n');
707                 foreach (_; 0 .. arg.level) { pp1(0, `=`); }
708                 ppN(' ', arg.args, ' ');
709                 foreach (_; 0 .. arg.level) { pp1(0, `=`); }
710                 ppRaw('\n');
711             }
712         }
713         else static if (isInstanceOf!(AsParagraph, Arg))
714         {
715             if (form == VizForm.HTML)
716             {
717                 pplnTaggedN(`p`, arg.args);
718             }
719             else if (form == VizForm.LaTeX)
720             {
721                 ppRaw(`\par `);
722                 pplnTaggedN(arg.args);
723             }
724             else if (form == VizForm.textAsciiDoc ||
725                      form == VizForm.textAsciiDocUTF8)
726             {
727                 ppRaw('\n');
728                 foreach (_; 0 .. arg.level) { pp1(0, `=`); }
729                 ppN(` `, arg.args, ` `, tag, '\n');
730             }
731         }
732         else static if (isInstanceOf!(AsBlockquote, Arg))
733         {
734             if (form == VizForm.HTML)
735             {
736                 pplnTaggedN(`blockquote`, arg.args);
737             }
738             else if (form == VizForm.jiraWikiMarkup)
739             {
740                 pplnRaw(`{quote}`);
741                 pplnRaw(arg.args);
742                 pplnRaw(`{quote}`);
743             }
744             else if (form == VizForm.Markdown)
745             {
746                 foreach (subArg; arg.args)
747                 {
748                     pplnRaw(`> `, subArg); // TODO Iterate for each line in subArg
749                 }
750             }
751         }
752         else static if (isInstanceOf!(AsBlockquoteSP, Arg))
753         {
754             if (form == VizForm.jiraWikiMarkup)
755             {
756                 ppRaw(`bq. `);
757                 ppN(arg.args);
758                 pplnRaw(``);
759             }
760         }
761         else static if (is(HorizontalRuler == Arg))
762         {
763             if (form == VizForm.HTML)
764             {
765                 pplnTagOpen(`hr`);
766             }
767             if (form == VizForm.jiraWikiMarkup)
768             {
769                 pplnRaw(`----`);
770             }
771         }
772         else static if (isInstanceOf!(MDash, Arg))
773         {
774             if (form == VizForm.HTML)
775             {
776                 ppRaw(`&mdash;`);
777             }
778             if (form == VizForm.jiraWikiMarkup ||
779                 form == VizForm.Markdown ||
780                 form == VizForm.LaTeX)
781             {
782                 pplnRaw(`---`);
783             }
784         }
785         else static if (isInstanceOf!(AsUList, Arg))
786         {
787             if (form == VizForm.HTML) { pplnTagOpen(`ul`); }
788             else if (form == VizForm.LaTeX) { pplnRaw(`\begin{enumerate}`); }
789             ppN(arg.args);
790             if (form == VizForm.HTML) { pplnTagClose(`ul`); }
791             else if (form == VizForm.LaTeX) { pplnRaw(`\end{enumerate}`); }
792         }
793         else static if (isInstanceOf!(AsOList, Arg))
794         {
795             if (form == VizForm.HTML) { pplnTagOpen(`ol`); }
796             else if (form == VizForm.LaTeX) { pplnRaw(`\begin{itemize}`); }
797             ppN(arg.args);
798             if (form == VizForm.HTML) { pplnTagClose(`ol`); }
799             else if (form == VizForm.LaTeX) { pplnRaw(`\end{itemize}`); }
800         }
801         else static if (isInstanceOf!(AsDescription, Arg)) // if args .length == 1 && an InputRange of 2-tuples pairs
802         {
803             if (form == VizForm.HTML) { pplnTagOpen(`dl`); } // TODO TERM <dt>, DEFINITION <dd>
804             else if (form == VizForm.LaTeX) { pplnRaw(`\begin{description}`); } // TODO \item[TERM] DEFINITION
805             ppN(arg.args);
806             if (form == VizForm.HTML) { pplnTagClose(`dl`); }
807             else if (form == VizForm.LaTeX) { pplnRaw(`\end{description}`); }
808         }
809         else static if (isInstanceOf!(AsTable, Arg))
810         {
811             if (form == VizForm.HTML)
812             {
813                 const border = (arg.border ? ` border=` ~ arg.border : ``);
814                 pplnTagOpen(`table` ~ border);
815             }
816             else if (form == VizForm.LaTeX)
817             {
818                 pplnRaw(`\begin{tabular}`);
819             }
820 
821             static if (arg.args.length == 1 &&
822                        isIterable!(typeof(arg.args[0])))
823             {
824                 auto rows = arg.args[0].asRows();
825                 rows.recurseFlag = arg.recurseFlag; // propagate
826                 rows.rowNr = arg.rowNr;
827                 ppNFlushed(rows);
828             }
829             else
830             {
831                 ppN(arg.args);
832             }
833 
834             if (form == VizForm.HTML)
835             {
836                 pplnTagClose(`table`);
837             }
838             else if (form == VizForm.LaTeX)
839             {
840                 pplnRaw(`\end{tabular}`);
841             }
842         }
843         else static if (isInstanceOf!(AsRows, Arg) &&
844                         arg.args.length == 1 &&
845                         isIterable!(typeof(arg.args[0])))
846         {
847             bool capitalizeHeadings = true;
848 
849             /* See_Also: http://forum.dlang.org/thread/wjksldfpkpenoskvhsqa@forum.dlang.org#post-jwfildowqrbwtamywsmy:40forum.dlang.org */
850 
851             // use aggregate members as header
852             import std.range.primitives : ElementType;
853             alias Front = ElementType!(typeof(arg.args[0]));
854             static if (is(Front == struct) ||
855                        is(Front == union) ||
856                        is(Front == class) ||
857                        is(Front == interface)) // isAggregateType
858             {
859                 /* TODO When __traits(documentation,x)
860                    here https://github.com/D-Programming-Language/dmd/pull/3531
861                    get merged use it! */
862                 // pplnTaggedN(`tr`, subArg.asCols); // TODO asItalic
863                 // Use __traits(allMembers, T) instead
864                 // Can we lookup file and line of user defined types aswell?
865 
866                 // member names header.
867                 if (form == VizForm.HTML) { pplnTagOpen(`tr`); } // TODO Functionize
868 
869                 // index column
870                 if      (arg.rowNr == RowNr.offsetZero) pplnTaggedN(`td`, `0-Offset`);
871                 else if (arg.rowNr == RowNr.offsetOne)  pplnTaggedN(`td`, `1-Offset`);
872                 foreach (const ix, Member; typeof(Front.tupleof))
873                 {
874                     import std.ascii : isUpper; // TODO support ASCII in fast path and Unicode in slow path
875                     import std.string : capitalize;
876                     import std.algorithm.iteration : joiner;
877 
878                     static      if (is(Memb == struct))    immutable qual = `struct `;
879                     else static if (is(Memb == class))     immutable qual = `class `;
880                     else static if (is(Memb == enum))      immutable qual = `enum `;
881                     else static if (is(Memb == interface)) immutable qual = `interface `;
882                     else                                   immutable qual = ``; // TODO Are there more qualifiers
883 
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 /** Key (Hit) Faces. */
1285 enum keyFaces = ctxFaces.map!(a => Face(a.foregroundColor, a.backgroundColor, true));
1286 
1287 void setFace(Term, Face)(ref Term term,
1288                          Face face,
1289                          bool colorFlag)
1290 {
1291     if (colorFlag)
1292     {
1293         version(useTerm)
1294         {
1295             term.color(face.foregroundColor | (face.bright ? Bright : 0) ,
1296                        face.backgroundColor);
1297         }
1298     }
1299 }
1300 
1301 /** Fazed (Rich) Text. */
1302 struct Fazed(T)
1303 {
1304     T text;
1305     const Face face;
1306     string toString() const @property pure nothrow
1307     {
1308         import std.conv : to;
1309         return to!string(text);
1310     }
1311 }
1312 auto faze(T)(T text, in Face face = stdFace) @safe pure nothrow
1313 {
1314     return Fazed!T(text, face);
1315 }
1316 
1317 auto getFace(Arg)(in Arg arg) @safe pure nothrow
1318 {
1319     // pick face
1320     static if (__traits(hasMember, arg, `face`))
1321     {
1322         return arg.face;
1323     }
1324     // else static if (isInstanceOf!(Digest, Arg)) // instead of is(Unqual!(Arg) == SHA1Digest)
1325     // {
1326     //     return digestFace;
1327     // }
1328     // else static if (isInstanceOf!(AsHit, Arg))
1329     // {
1330     //     return keyFaces.cycle[arg.ix];
1331     // }
1332     // else static if (isInstanceOf!(AsCtx, Arg))
1333     // {
1334     //     return ctxFaces.cycle[arg.ix];
1335     // }
1336     else
1337     {
1338         return stdFace;
1339     }
1340 }
1341 
1342 /** Show `viz`.
1343  */
1344 void show(Viz viz)
1345 {
1346     viz.outFile.flush();
1347     import std.process : spawnProcess, wait;
1348     auto pid = spawnProcess([`xdg-open`, viz.outFile.name]);
1349     assert(wait(pid) == 0);
1350 }
1351 
1352 version(unittest)
1353 {
1354     import std.algorithm.iteration : map;
1355     // TODO hide these stuff in constructor for Viz
1356     import std.uuid : randomUUID;
1357     import std.stdio : File;
1358     import nxt.rational : rational;
1359 }
1360 
1361 unittest
1362 {
1363     string outPath = `/tmp/fs-` ~ randomUUID.toString() ~ `.` ~ `html`; // reuse `nxt.tempfs`
1364     File outFile = File(outPath, `w`);
1365 
1366     auto term = Terminal(ConsoleOutputType.linear);
1367 
1368     auto viz = new Viz(outFile, &term, VizForm.HTML);
1369 
1370     viz.pp1(`Pretty Printing`.asH!1);
1371     viz.pp1(horizontalRuler);
1372 
1373     viz.pp1(`First Heading`.asH!2);
1374     viz.ppln(`Something first.`);
1375 
1376     viz.pp1(`Second Heading`.asH!2);
1377     viz.ppln(`Something else.`);
1378 
1379     struct S
1380     {
1381         string theUnit;
1382         int theSuperValue;
1383     }
1384 
1385     S[] s = [S("meter", 42),
1386              S("second", 43)];
1387 
1388     viz.pp1("Struct Array".asH!2);
1389     viz.pp1(s.asTable);
1390 
1391     viz.pp1("Map Struct Array".asH!2);
1392     viz.pp1(s.map!(_ => S(_.theUnit,
1393                           _.theSuperValue^^2)).asTable);
1394 
1395     viz.pp1("Rational Number Array".asH!2);
1396     viz.pp1([rational(11, 13),
1397              rational(14, 15),
1398              rational(17, 32)]);
1399 
1400     struct NamedRational
1401     {
1402         string name;
1403         Rational!long value;
1404     }
1405 
1406     viz.pp1("Named Rational Number Array as Table".asH!2);
1407     viz.pp1([NamedRational("x", Rational!long(11, 13)),
1408              NamedRational("y", Rational!long(111, 133)),
1409              NamedRational("z", Rational!long(1111, 1333))].asTable);
1410 
1411     viz.show();
1412 }