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