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