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(`—`); 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 }