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 }