1 module tabletool; 2 3 import std.algorithm : canFind, max, min, map; 4 import std.array : array, split; 5 import std.conv : to; 6 import std.format : format; 7 import std.range : join, repeat, zip; 8 import std.traits : getUDAs, hasUDA; 9 import std.utf; 10 11 import eastasianwidth : eastasianDisplayWidth = displayWidth; 12 13 /// Option to specify the table style. 14 enum Style 15 { 16 simple, 17 markdown, 18 grid, 19 } 20 21 /// Option to specify the position of element in the cell. 22 enum Align 23 { 24 center, 25 left, 26 right, 27 } 28 29 /// Configurations for tabulate. 30 struct Config 31 { 32 Style style = Style.simple; 33 Align align_ = Align.center; 34 bool showHeader = true; 35 } 36 37 /// UDA to set display name of the struct member. 38 struct DisplayName 39 { 40 string name; 41 } 42 43 /// Detailed configurations to set table-wide appearance. 44 struct TableConfig 45 { 46 Style style = Style.simple; 47 string leftPadding = " "; 48 string rightPadding = " "; 49 bool showHeader = true; 50 size_t maxHeight = 0; 51 } 52 53 /// Detailed configurations to set each column appearance. 54 struct ColumnConfig 55 { 56 size_t width; 57 string header = ""; 58 Align align_ = Align.center; 59 } 60 61 /** 62 * Tabulate array of array data. 63 * Params: 64 * data = An array of array of string compatible data 65 * headers = Headers for each columns 66 * config = A configuration to set appearance 67 * Returns: The table string 68 */ 69 string tabulate(T)(in T[][] data, in string[] headers, in Config config = Config()) 70 { 71 assert(data.length > 0); 72 assert(headers.length == 0 || data[0].length == headers.length); 73 74 auto actualHeaders = headers.length > 0 ? headers : "".repeat(data[0].length).array(); 75 76 auto tableConfig = TableConfig(); 77 tableConfig.style = config.style; 78 tableConfig.showHeader = config.showHeader && headers.length > 0; 79 80 auto widthes = calcWidthes(data, actualHeaders, chooseLineBreak(config.style)); 81 82 auto columnConfigs = zip(widthes, actualHeaders).map!(tup => ColumnConfig(tup[0], tup[1], config 83 .align_)).array(); 84 85 return tabulate(data, tableConfig, columnConfigs); 86 } 87 88 /// 89 unittest 90 { 91 const testdata = [ 92 ["D-man", "Programming Language"], 93 ["D言語くん", "プログラミング言語"], 94 ]; 95 const headers = ["マスコットキャラクタ", "about"]; 96 const reference = 97 " マスコットキャラクタ about \n" ~ 98 "---------------------- ----------------------\n" ~ 99 " D-man Programming Language \n" ~ 100 " D言語くん プログラミング言語 "; 101 assert(tabulate(testdata, headers, Config(Style.simple, Align.center, true)) == reference); 102 } 103 104 /// 105 unittest 106 { 107 const testdata = [ 108 ["D-man", "Programming\nLanguage"], 109 ["D言語\nくん", "プログラミング言語"], 110 ]; 111 const headers = ["マスコットキャラクタ", "about"]; 112 import std; 113 114 assert(tabulate(testdata, headers, Config(Style.simple, Align.center, true)) == 115 " マスコットキャラクタ about \n" ~ 116 "---------------------- --------------------\n" ~ 117 " D-man Programming \n" ~ 118 " Language \n" ~ 119 " D言語 プログラミング言語 \n" ~ 120 " くん " 121 ); 122 123 assert(tabulate(testdata, headers, Config(Style.markdown, Align.center, true)) == 124 "| マスコットキャラクタ | about |\n" ~ 125 "|----------------------|-------------------------|\n" ~ 126 "| D-man | Programming<br>Language |\n" ~ 127 "| D言語<br>くん | プログラミング言語 |" 128 ); 129 } 130 131 /** 132 * Tabulate array of array data (headerless version). 133 * 134 * In this version, config.showHeader will be ignored and header section of the 135 * table will be invisible. 136 * 137 * Params: 138 * data = An array of array of string compatible data 139 * config = A configuration to set appearance 140 * Returns: The table string 141 */ 142 string tabulate(T)(in T[][] data, in Config config = Config()) 143 { 144 return tabulate(data, [], config); 145 } 146 147 /// 148 unittest 149 { 150 const testdata = [ 151 ["D-man", "Programming Language"], 152 ["D言語くん", "プログラミング言語"], 153 ]; 154 const reference = 155 " D-man Programming Language \n" ~ 156 " D言語くん プログラミング言語 "; 157 assert(tabulate(testdata, Config(Style.simple, Align.center, true)) == reference); 158 } 159 160 /** 161 * Tabulate array of strut data. 162 * 163 * This version consume an array of struct. The headers will be extrated from 164 * members' name and each member should be able to convert to string. If some 165 * of members need to be re-named, an UDA DisplayName can be used. 166 * 167 * Params: 168 * data = An array of struct data 169 * config = A configuration to set appearance 170 * Returns: The table string 171 */ 172 string tabulate(T)(in T[] data, in Config config = Config()) if (is(T == struct)) 173 { 174 string[][] stringData; 175 string[] headers; 176 177 foreach (member; __traits(allMembers, T)) 178 { 179 static if (hasUDA!(__traits(getMember, T, member), DisplayName)) 180 { 181 enum displayName = getUDAs!(__traits(getMember, T, member), DisplayName)[0]; 182 headers ~= displayName.name; 183 } 184 else 185 { 186 headers ~= member; 187 } 188 } 189 foreach (d; data) 190 { 191 string[] line; 192 foreach (member; __traits(allMembers, T)) 193 { 194 line ~= __traits(getMember, d, member).to!string; 195 } 196 stringData ~= line; 197 } 198 return tabulate(stringData, headers, config); 199 } 200 201 /// 202 unittest 203 { 204 struct TestData 205 { 206 @DisplayName("マスコットキャラクタ") 207 string name; 208 string about; 209 } 210 211 const testdata = [ 212 TestData("D-man", "Programming Language"), 213 TestData("D言語くん", "プログラミング言語"), 214 ]; 215 const reference = 216 " マスコットキャラクタ about \n" ~ 217 "---------------------- ----------------------\n" ~ 218 " D-man Programming Language \n" ~ 219 " D言語くん プログラミング言語 "; 220 221 assert(tabulate(testdata, Config(Style.simple, Align.center, true)) == reference); 222 } 223 224 /** 225 * Tabulate an array of associative array. 226 * 227 * This version tabulates an array of associative array. The keys will be used 228 * as headers and there is no need to align each keys of each array elements. 229 * If some missing key exists in the one line, that cell will be empty. 230 * 231 * Params: 232 * data = An array of associative array data 233 * config = A configuration to set appearance 234 * Returns: The table string 235 */ 236 string tabulate(Key, Value)(in Value[Key][] data, in Config config = Config()) 237 { 238 string[][] stringData; 239 Key[] headers; 240 foreach (line; data) 241 { 242 foreach (key; line.byKey()) 243 { 244 if (!headers.canFind(key)) 245 { 246 headers ~= key; 247 } 248 } 249 } 250 foreach (line; data) 251 { 252 string toStr(Key h) 253 { 254 if (h in line) 255 { 256 return line[h].to!string; 257 } 258 else 259 { 260 return ""; 261 } 262 } 263 264 stringData ~= headers.map!(h => toStr(h)).array(); 265 } 266 string[] stringHeaders = headers.map!(h => h.to!string).array(); 267 return tabulate(stringData, stringHeaders, config); 268 } 269 270 /// 271 unittest 272 { 273 const testdata = [ 274 [ 275 "マスコットキャラクタ": "D-man", 276 "about": "Programming Language" 277 ], 278 [ 279 "マスコットキャラクタ": "D言語くん", 280 "about": "プログラミング言語" 281 ], 282 ]; 283 const reference = 284 " マスコットキャラクタ about \n" ~ 285 "---------------------- ----------------------\n" ~ 286 " D-man Programming Language \n" ~ 287 " D言語くん プログラミング言語 "; 288 assert(tabulate(testdata, Config(Style.simple, Align.center, true)) == reference); 289 } 290 291 /** 292 * Tabulate an array of array data with detailed configurations. 293 * 294 * This version uses TableConfig and an array of ColumnConfig instead of Config. 295 * TableConfig affects the whole table appearance and ColumnConfigs affect each 296 * columns' appearance. This can be used if you want to configure (e.g.) 297 * columns one-by-one. 298 * 299 * Params: 300 * data = An array of array data 301 * tableConfig = A table-wide configuration 302 * columnConfigs = Configurations for each columns (The length should match with data) 303 * Returns: The table string 304 */ 305 string tabulate(T)(in T[][] data, in TableConfig tableConfig, in ColumnConfig[] columnConfigs) 306 { 307 assert(data.length > 0); 308 assert(data[0].length == columnConfigs.length); 309 310 const ruler = Ruler(tableConfig.style); 311 const lineBreak = chooseLineBreak(tableConfig.style); 312 const widthes = columnConfigs.map!(c => c.width).array(); 313 const aligns = columnConfigs.map!(c => c.align_).array(); 314 const widthForRuler = widthes.map!(w => w + displayWidth( 315 tableConfig.leftPadding, "") + displayWidth(tableConfig.rightPadding, "")).array(); 316 317 string[] lines; 318 319 if (auto top = ruler.top(widthForRuler)) 320 { 321 lines ~= top; 322 } 323 324 if (tableConfig.showHeader) 325 { 326 const headers = columnConfigs.map!(c => c.header).array(); 327 lines ~= makeRow( 328 headers, 329 widthes, 330 aligns, 331 ruler, 332 tableConfig.leftPadding, 333 tableConfig.rightPadding, 334 lineBreak, 335 tableConfig.maxHeight, 336 ); 337 if (auto sep = ruler.headerItemSeperator(widthForRuler)) 338 { 339 lines ~= sep; 340 } 341 } 342 foreach (i, line; data) 343 { 344 lines ~= makeRow( 345 line, 346 widthes, 347 aligns, 348 ruler, 349 tableConfig.leftPadding, 350 tableConfig.rightPadding, 351 lineBreak, 352 tableConfig.maxHeight, 353 ); 354 if ((i + 1) != data.length) 355 { 356 if (auto sep = ruler.horizontalItemSeperator(widthForRuler)) 357 { 358 lines ~= sep; 359 } 360 } 361 } 362 if (auto bottom = ruler.bottom(widthForRuler)) 363 { 364 lines ~= bottom; 365 } 366 367 return lines.join("\n"); 368 } 369 370 /// 371 unittest 372 { 373 const testdata = [ 374 ["D-man", "Programming Language"], 375 ["D言語くん", "プログラミング言語"], 376 ]; 377 const tableConfig = TableConfig(Style.simple, " ", " ", true); 378 const columnConfigs = [ 379 ColumnConfig(20, "マスコットキャラクタ", Align.center), 380 ColumnConfig(10, "about", Align.center) 381 ]; 382 const reference = 383 " マスコットキャラクタ about \n" ~ 384 "---------------------- ------------\n" ~ 385 " D-man ..ming L.. \n" ~ 386 " D言語くん ..ラミン.. "; 387 assert(tabulate(testdata, tableConfig, columnConfigs) == reference); 388 } 389 390 /// 391 unittest 392 { 393 const testdata = [ 394 ["D-man", "Programming\nLanguage"], 395 ["D言語\nくん", "プログラミング言語"], 396 ]; 397 const columnConfigs = [ 398 ColumnConfig(20, "マスコットキャラクタ", Align.center), 399 ColumnConfig(30, "about", Align.center) 400 ]; 401 402 assert(tabulate(testdata, TableConfig(Style.simple, " ", " ", true, 0), columnConfigs) == 403 " マスコットキャラクタ about \n" ~ 404 "---------------------- --------------------------------\n" ~ 405 " D-man Programming \n" ~ 406 " Language \n" ~ 407 " D言語 プログラミング言語 \n" ~ 408 " くん " 409 ); 410 assert(tabulate(testdata, TableConfig(Style.simple, " ", " ", true, 1), columnConfigs) == 411 " マスコットキャラクタ about \n" ~ 412 "---------------------- --------------------------------\n" ~ 413 " D-man Programming.. \n" ~ 414 " D言語.. プログラミング言語 " 415 ); 416 assert(tabulate(testdata, TableConfig(Style.markdown, " ", " ", true, 0), columnConfigs) == 417 "| マスコットキャラクタ | about |\n" ~ 418 "|----------------------|--------------------------------|\n" ~ 419 "| D-man | Programming<br>Language |\n" ~ 420 "| D言語<br>くん | プログラミング言語 |" 421 ); 422 423 } 424 425 /// Unescape bash color sequence 426 private string unescape(string text) 427 { 428 import std.regex; 429 430 return replaceAll(text, regex(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|k]"), ""); 431 } 432 433 /// 434 unittest 435 { 436 foreach (text; ["hello", "こんにちは"]) 437 { 438 auto normal = text; 439 auto red = "\033[31m" ~ text; 440 auto greenBlueAndReset = "\033[32m\033[44m" ~ text ~ "\033[0m"; 441 auto combination = red ~ greenBlueAndReset ~ normal; 442 443 assert(normal == unescape(normal)); 444 assert(normal == unescape(red)); 445 assert(normal == unescape(greenBlueAndReset)); 446 assert(normal ~ normal ~ normal == unescape(combination)); 447 } 448 } 449 450 private size_t displayWidth(string text, string lineBreak) 451 { 452 const tmp = unescape(text); 453 if (lineBreak == "\n") 454 { 455 size_t width = 0; 456 foreach (line; tmp.split("\n")) 457 { 458 width = max(width, eastasianDisplayWidth(line)); 459 } 460 return width; 461 } 462 else 463 { 464 size_t width = 0; 465 const lines = tmp.split("\n"); 466 foreach (line; lines) 467 { 468 width += eastasianDisplayWidth(line); 469 } 470 width += lineBreak.length * (lines.length.to!int - 1); 471 return width; 472 } 473 } 474 475 /// 476 unittest 477 { 478 assert(displayWidth("hello", "") == 5); 479 assert(displayWidth("こんにちは", "") == 10); 480 assert(displayWidth("helloこんにちは", "") == 15); 481 assert(displayWidth("\033[31m" ~ "helloこんにちは" ~ "\033[0m", "") == 15); 482 assert(displayWidth("he\nllo", " ") == 7); 483 assert(displayWidth("he\nllo", "\n") == 3); 484 } 485 486 private string alignment(string text, Align align_, size_t width) 487 { 488 static immutable dotTable = ["", ".", ".."]; 489 if (width == 0) 490 { 491 return ""; 492 } 493 // Assume one line 494 const textWidth = displayWidth(text, ""); 495 if (textWidth > width) 496 { 497 with (Align) final switch (align_) 498 { 499 case left: 500 return cutRight(text, width.to!int - 2) ~ dotTable[min(width, 2)]; 501 case right: 502 return dotTable[min(width, 2)] ~ cutLeft(text, width.to!int - 2); 503 case center: 504 const c = cutBoth(text, width.to!int - 4); 505 const l = min(width / 2, 2); 506 const r = min(width / 2 + width % 2, 2); 507 return dotTable[l] ~ c ~ dotTable[r]; 508 } 509 } 510 else 511 { 512 with (Align) final switch (align_) 513 { 514 case left: 515 return format!"%-s%-s"(text, ' '.repeat(width - textWidth)); 516 case right: 517 return format!"%-s%-s"(' '.repeat(width - textWidth), text); 518 case center: 519 const l = (width - textWidth) / 2; 520 const r = (width - textWidth) / 2 + (width - textWidth) % 2; 521 return format!"%-s%-s%-s"(' '.repeat(l), text, ' '.repeat(r)); 522 } 523 } 524 } 525 526 private string cutRight(string text, int width) 527 { 528 if (width <= 0) 529 { 530 return ""; 531 } 532 // Assume one line 533 for (int c = count(text).to!int - 1; displayWidth(text, "") > width; c--) 534 { 535 text = text[0 .. toUTFindex(text, c)]; 536 } 537 return width > displayWidth(text, "") ? text ~ "." : text; 538 } 539 540 private string cutLeft(string text, int width) 541 { 542 if (width <= 0) 543 { 544 return ""; 545 } 546 // Assume one line 547 while (displayWidth(text, "") > width) 548 { 549 text = text[toUTFindex(text, 1) .. $]; 550 } 551 return width > displayWidth(text, "") ? "." ~ text : text; 552 } 553 554 private string cutBoth(string text, int width) 555 { 556 if (width <= 0) 557 { 558 return ""; 559 } 560 bool cutLeftSide = false; 561 // Assume one line 562 while (displayWidth(text, "") > width) 563 { 564 if (cutLeftSide) 565 { 566 text = text[toUTFindex(text, 1) .. $]; 567 } 568 else 569 { 570 text = text[0 .. toUTFindex(text, count(text).to!int - 1)]; 571 } 572 cutLeftSide = !cutLeftSide; 573 } 574 return width > displayWidth(text, "") ? text ~ "." : text; 575 } 576 577 unittest 578 { 579 string a = "こんにちは"; 580 581 assert(alignment(a, Align.left, 12) == "こんにちは "); 582 assert(alignment(a, Align.left, 11) == "こんにちは "); 583 assert(alignment(a, Align.left, 10) == "こんにちは"); 584 assert(alignment(a, Align.left, 9) == "こんに..."); 585 assert(alignment(a, Align.left, 8) == "こんに.."); 586 assert(alignment(a, Align.left, 3) == "..."); 587 assert(alignment(a, Align.left, 2) == ".."); 588 assert(alignment(a, Align.left, 1) == "."); 589 assert(alignment(a, Align.left, 0) == ""); 590 591 assert(alignment(a, Align.center, 12) == " こんにちは "); 592 assert(alignment(a, Align.center, 11) == "こんにちは "); 593 assert(alignment(a, Align.center, 10) == "こんにちは"); 594 assert(alignment(a, Align.center, 9) == "..んに..."); 595 assert(alignment(a, Align.center, 8) == "..んに.."); 596 assert(alignment(a, Align.center, 5) == "....."); 597 assert(alignment(a, Align.center, 4) == "...."); 598 assert(alignment(a, Align.center, 3) == "..."); 599 assert(alignment(a, Align.center, 2) == ".."); 600 assert(alignment(a, Align.center, 1) == "."); 601 assert(alignment(a, Align.center, 0) == ""); 602 603 assert(alignment(a, Align.right, 12) == " こんにちは"); 604 assert(alignment(a, Align.right, 11) == " こんにちは"); 605 assert(alignment(a, Align.right, 10) == "こんにちは"); 606 assert(alignment(a, Align.right, 9) == "...にちは"); 607 assert(alignment(a, Align.right, 8) == "..にちは"); 608 assert(alignment(a, Align.right, 3) == "..."); 609 assert(alignment(a, Align.right, 2) == ".."); 610 assert(alignment(a, Align.right, 1) == "."); 611 assert(alignment(a, Align.right, 0) == ""); 612 } 613 614 private string makeRow(T)( 615 in T[] row, 616 in size_t[] widthes, 617 in Align[] aligns, 618 in Ruler ruler, 619 in string leftPadding, 620 in string rightPadding, 621 in string lineBreak, 622 in size_t maxHeight, 623 ) 624 { 625 if (lineBreak == "\n") 626 { 627 // Need to make multiline field 628 string[][] lines; 629 lines ~= new string[row.length]; 630 foreach (i, elem; row) 631 { 632 foreach (j, l; elem.to!string.split("\n")) 633 { 634 if (maxHeight != 0 && maxHeight == j) 635 { 636 lines[j - 1][i] ~= ".."; 637 break; 638 } 639 if (lines.length <= j) 640 { 641 lines ~= new string[row.length]; 642 } 643 lines[j][i] = l; 644 } 645 } 646 string[] ret; 647 foreach (line; lines) 648 { 649 ret ~= makeItemLine(line, widthes, aligns, ruler, leftPadding, rightPadding); 650 } 651 652 return ret.join("\n"); 653 } 654 else 655 { 656 // 1 line per row 657 string[] line = row.map!(elem => elem.to!string.split("\n").join(lineBreak)).array(); 658 return makeItemLine(line, widthes, aligns, ruler, leftPadding, rightPadding); 659 } 660 661 } 662 663 unittest 664 { 665 string[] line = ["a", "ab", "a\nbc", "abcd", "ab\ncde"]; 666 size_t[] widthes = [10, 10, 10, 10, 10]; 667 Ruler ruler = Ruler(Style.markdown); 668 string leftPadding = "*"; 669 string rightPadding = "^^"; 670 Align[] aligns = [ 671 Align.left, Align.right, Align.center, Align.left, Align.right 672 ]; 673 674 assert(makeRow(line, widthes, aligns, ruler, leftPadding, rightPadding, "\n", 0) == 675 "|*a ^^|* ab^^|* a ^^|*abcd ^^|* ab^^|\n" ~ 676 "|* ^^|* ^^|* bc ^^|* ^^|* cde^^|" 677 ); 678 assert(makeRow(line, widthes, aligns, ruler, leftPadding, rightPadding, "\n", 1) == 679 "|*a ^^|* ab^^|* a.. ^^|*abcd ^^|* ab..^^|" 680 ); 681 assert(makeRow(line, widthes, aligns, ruler, leftPadding, rightPadding, "<br>", 0) == 682 "|*a ^^|* ab^^|* a<br>bc ^^|*abcd ^^|* ab<br>cde^^|" 683 ); 684 685 } 686 687 private string makeItemLine(T)( 688 in T[] line, 689 in size_t[] widthes, 690 in Align[] aligns, 691 in Ruler ruler, 692 in string leftPadding, 693 in string rightPadding, 694 ) 695 { 696 return ruler.left() 697 ~ zip(line, aligns, widthes) 698 .map!(tup => leftPadding ~ alignment(tup[0].to!string, tup[1], tup[2]) ~ rightPadding) 699 .join(ruler.vertical()) 700 ~ ruler.right(); 701 } 702 703 unittest 704 { 705 string[] line = ["a", "ab", "abc", "abcd", "abcde"]; 706 size_t[] widthes = [6, 5, 4, 3, 2]; 707 Ruler ruler = Ruler(Style.markdown); 708 string leftPadding = "*"; 709 string rightPadding = "^^"; 710 Align[] aligns = [ 711 Align.left, Align.right, Align.center, Align.left, Align.right 712 ]; 713 assert(makeItemLine(line, widthes, aligns, ruler, leftPadding, rightPadding) 714 == "|*a ^^|* ab^^|*abc ^^|*a..^^|*..^^|"); 715 } 716 717 private size_t[] calcWidthes(T)(in T[][] data, in string[] headers, string lineBreak) 718 { 719 assert(data.length > 0); 720 assert(data[0].length == headers.length); 721 722 auto widthes = headers.map!(h => displayWidth(h, lineBreak)).array(); 723 724 foreach (line; data) 725 { 726 assert(line.length == widthes.length); 727 foreach (i; 0 .. widthes.length) 728 { 729 widthes[i] = max(widthes[i], displayWidth(line[i], lineBreak)); 730 } 731 } 732 return widthes; 733 } 734 735 /// Nethack(vi) style function naming 736 private struct Ruler 737 { 738 Style style; 739 740 enum Index 741 { 742 HL, // ─ 743 JK, // │ 744 JL, // ┌ 745 HJ, // ┐ 746 HK, // ┘ 747 KL, // └ 748 JKL, // ├ 749 HJL, // ┬ 750 HJK, // ┤ 751 HKL, // ┴ 752 HJKL, // ┼ 753 } 754 755 private static immutable string[] simpleLiterals = [ 756 "-", " ", "", "", "", "", "", "", "", "", " " 757 ]; 758 private static immutable string[] markdownLiterals = [ 759 "-", "|", "", "", "", "", "|", "", "|", "", "|" 760 ]; 761 private static immutable string[] gridLiterals = [ 762 "─", "│", "┌", "┐", "┘", "└", "├", "┬", "┤", "┴", 763 "┼" 764 ]; 765 766 private immutable(string)[] select() const @nogc nothrow pure 767 { 768 with (Style) final switch (style) 769 { 770 case simple: 771 return simpleLiterals; 772 case markdown: 773 return markdownLiterals; 774 case grid: 775 return gridLiterals; 776 } 777 } 778 779 string get(Index index) const 780 { 781 const target = select(); 782 return target[index.to!int]; 783 } 784 785 string horizontalItemSeperator(const size_t[] widthes) const 786 { 787 with (Style) final switch (style) 788 { 789 case simple, markdown: 790 return null; 791 case grid: 792 return makeHorizontal(widthes, get(Index.HL), get(Index.HJKL), get(Index.JKL), get( 793 Index.HJK)); 794 } 795 } 796 797 string headerItemSeperator(const size_t[] widthes) const 798 { 799 return makeHorizontal(widthes, get(Index.HL), get(Index.HJKL), get(Index.JKL), get( 800 Index.HJK)); 801 } 802 803 string left() const 804 { 805 with (Style) final switch (style) 806 { 807 case simple: 808 return ""; 809 case markdown, grid: 810 return get(Index.JK); 811 } 812 } 813 814 string right() const 815 { 816 with (Style) final switch (style) 817 { 818 case simple: 819 return ""; 820 case markdown, grid: 821 return get(Index.JK); 822 } 823 } 824 825 string vertical() const 826 { 827 return get(Index.JK); 828 } 829 830 string top(const size_t[] widthes) const 831 { 832 with (Style) final switch (style) 833 { 834 case simple, markdown: 835 return null; 836 case grid: 837 return makeHorizontal(widthes, get(Index.HL), get(Index.HJL), get(Index.JL), get( 838 Index.HJ)); 839 } 840 } 841 842 string bottom(const size_t[] widthes) const 843 { 844 with (Style) final switch (style) 845 { 846 case simple, markdown: 847 return null; 848 case grid: 849 return makeHorizontal(widthes, get(Index.HL), get(Index.HKL), get(Index.KL), get( 850 Index.HK)); 851 } 852 } 853 854 private static string makeHorizontal(const size_t[] widthes, string h, string p, string l, string r) 855 { 856 return format!"%-s%-s%-s"(l, widthes.map!(w => h.repeat(w).join()).join(p), r); 857 } 858 } 859 860 unittest 861 { 862 const ruler = Ruler(Style.simple); 863 size_t[] widthes = [1, 2, 3]; 864 865 assert(ruler.horizontalItemSeperator(widthes) is null); 866 assert(ruler.headerItemSeperator(widthes) == "- -- ---"); 867 assert(ruler.top(widthes) is null); 868 assert(ruler.bottom(widthes) is null); 869 assert(ruler.left() == ""); 870 assert(ruler.right() == ""); 871 assert(ruler.vertical() == " "); 872 } 873 874 unittest 875 { 876 const ruler = Ruler(Style.markdown); 877 size_t[] widthes = [1, 2, 3]; 878 879 assert(ruler.horizontalItemSeperator(widthes) is null); 880 assert(ruler.headerItemSeperator(widthes) == "|-|--|---|"); 881 assert(ruler.top(widthes) is null); 882 assert(ruler.bottom(widthes) is null); 883 assert(ruler.left() == "|"); 884 assert(ruler.right() == "|"); 885 assert(ruler.vertical() == "|"); 886 } 887 888 unittest 889 { 890 const ruler = Ruler(Style.grid); 891 size_t[] widthes = [1, 2, 3]; 892 893 assert(ruler.horizontalItemSeperator(widthes) == "├─┼──┼───┤"); 894 assert(ruler.headerItemSeperator(widthes) == "├─┼──┼───┤"); 895 assert(ruler.top(widthes) == "┌─┬──┬───┐"); 896 assert(ruler.bottom(widthes) == "└─┴──┴───┘"); 897 assert(ruler.left() == "│"); 898 assert(ruler.right() == "│"); 899 assert(ruler.vertical() == "│"); 900 } 901 902 private string chooseLineBreak(in Style style) 903 { 904 with (Style) final switch (style) 905 { 906 case markdown: 907 return "<br>"; 908 case simple, grid: 909 return "\n"; 910 } 911 }