1 /** Pretty Printing to AsciiDoc, HTML, LaTeX, JIRA Wikitext, etc. 2 * 3 * Copyright: Per Nordlöw 2018-. 4 * License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 5 * Authors: $(WEB Per Nordlöw) 6 * 7 * TODO: Convert `Viz` param to delegate like for 8 * `void toString(scope void delegate(scope const(char)[]) @safe sink)` 9 * for independency on arsd. 10 * 11 * Rename `pp1` to `ppL` and `pp` to `pp1` where relevant to reduce number of 12 * template instantiations. And replace multi-calls to viz.pp() to viz.pp1() 13 * 14 * TODO: Add tester that prints types from `nxt.geometry` 15 * 16 * TODO: Remove all restrictions on pp.*Raw.* and call them using ranges such as repeat 17 * 18 * TODO: Use "alias this" on wrapper structures and test! 19 * 20 * TODO: How should std.typecons.Tuple be pretty printed? 21 * 22 * TODO: Add visited member to keeps track of what objects that have been visited 23 * 24 * TODO: Add asGCCMessage pretty prints 25 * seq $PATH, ':', $ROW, ':', $COL, ':', message, '[', $TYPE, ']' 26 */ 27 module nxt.pretty; 28 29 // version = useTerm; 30 31 import core.time : Duration; 32 33 import std.range.primitives : isInputRange; 34 import std.traits : isInstanceOf, isSomeString, Unqual, isArray, isIterable; 35 import std.stdio : stdout; 36 37 /* TODO: Move logic (toHTML) to these deps and remove these imports */ 38 import nxt.color : Color; 39 import nxt.mathml; 40 import nxt.lingua; 41 import nxt.attributes; 42 import nxt.rational : Rational; 43 44 import arsd.terminal : Terminal, ConsoleOutputType, Bright; 45 46 /// See_Also: http://forum.dlang.org/thread/fihymjelvnwfevegwryt@forum.dlang.org#post-fihymjelvnwfevegwryt:40forum.dlang.org 47 template Concise(Tuple) 48 { 49 static if(isTuple!Tuple) 50 { 51 struct Concise(Tuple) 52 { 53 Tuple tup; 54 alias tup this; 55 // should use a better overload 56 string toString() const 57 { 58 auto app = appender!string(); // TODO: use array_ex.Array instead 59 app.put(`(`); 60 app.put(to!string(concise(tuple[0]))); 61 foreach (t; tuple.expand[1 .. $]) 62 { 63 app.put(`, `); 64 app.put(to!string(concise(t))); 65 } 66 app.put(`)`); 67 return app.data; 68 } 69 } 70 } 71 else 72 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 (isInstanceOf!(AsWords, Arg)) 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 (isInstanceOf!(AsCSL, Arg)) 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 (isInstanceOf!(AsBold, Arg)) 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 (isInstanceOf!(AsItalic, Arg)) 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 (isInstanceOf!(AsMonospaced, Arg)) 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 (isInstanceOf!(AsCode, Arg)) 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 (isInstanceOf!(AsEmphasized, Arg)) 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 (isInstanceOf!(AsStronglyEmphasized, Arg)) 532 { 533 if (form == VizForm.Markdown) 534 { 535 ppRaw(`__`); 536 ppN(arg.args); 537 ppRaw(`__`); 538 } 539 } 540 else static if (isInstanceOf!(AsStrong, Arg)) 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 (isInstanceOf!(AsCitation, Arg)) 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 (isInstanceOf!(AsDeleted, Arg)) 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 (isInstanceOf!(AsInserted, Arg)) 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 (isInstanceOf!(AsSuperscript, Arg)) 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 (isInstanceOf!(AsSubscript, Arg)) 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 (isInstanceOf!(AsPreformatted, Arg)) 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 (isInstanceOf!(AsHeader, Arg)) 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 (isInstanceOf!(AsParagraph, Arg)) 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 (isInstanceOf!(AsBlockquote, Arg)) 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 (isInstanceOf!(AsBlockquoteSP, Arg)) 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 (isInstanceOf!(MDash, Arg)) 703 { 704 if (form == VizForm.HTML) 705 ppRaw(`—`); 706 else if (form == VizForm.jiraWikiMarkup || 707 form == VizForm.Markdown || 708 form == VizForm.LaTeX) 709 { 710 pplnRaw(`---`); 711 } 712 } 713 else static if (isInstanceOf!(AsUList, Arg)) 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 (isInstanceOf!(AsOList, Arg)) 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 (isInstanceOf!(AsDescription, Arg)) // 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 (isInstanceOf!(AsTable, Arg)) 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 (isInstanceOf!(AsRows, Arg) && 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 (isInstanceOf!(AsCols, Arg)) 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 (isInstanceOf!(AsRow, Arg)) 877 { 878 string spanArg; 879 static if (arg.args.length == 1 && 880 isInstanceOf!(Span, typeof(arg.args[0]))) 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 (isInstanceOf!(AsCell, Arg)) 889 { 890 string spanArg; 891 static if (arg.args.length != 0 && 892 isInstanceOf!(Span, typeof(arg.args[0]))) 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 (isInstanceOf!(AsTHeading, Arg)) 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 (isInstanceOf!(AsItem, Arg)) 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 (isInstanceOf!(AsPath, Arg) || 942 isInstanceOf!(AsURL, Arg)) 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 (isInstanceOf!(AsPath, Arg)) 953 ppTagOpen(`a href="file://` ~ arg.arg ~ `"`); 954 else static if (isInstanceOf!(AsURL, Arg)) 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 (isInstanceOf!(AsName, Arg)) 965 { 966 auto vizArg = viz; 967 vizArg.treeFlag = true; 968 pp1(term, vizArg, depth + 1, arg.arg); 969 } 970 // else static if (isInstanceOf!(AsHit, Arg)) 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 (isInstanceOf!(AsCtx, Arg)) 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 (isInstanceOf!(Digest, Arg)) // instead of is(Unqual!(Arg) == SHA1Digest) 1223 // { 1224 // return digestFace; 1225 // } 1226 // else static if (isInstanceOf!(AsHit, Arg)) 1227 // { 1228 // return keyFaces.cycle[arg.ix]; 1229 // } 1230 // else static if (isInstanceOf!(AsCtx, Arg)) 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 }