1package Text::Table::More;
2
3use 5.010001;
4use strict;
5use warnings;
6#use utf8;
7
8our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
9our $DATE = '2021-08-28'; # DATE
10our $DIST = 'Text-Table-More'; # DIST
11our $VERSION = '0.020'; # VERSION
12
13# see Module::Features for more details on this
14our %FEATURES = (
15    set_v => {
16        TextTable => 1,
17    },
18
19    features => {
20        PerlTrove => {
21            "Development Status" => "4 - Beta",
22            "Environment" => "Console",
23            # Framework
24            "Intended Audience" => ["Developers"],
25            "License" => "OSI Approved :: Artistic License",
26            # Natural Language
27            # Operating System
28            "Programming Language" => "Perl",
29            "Topic" => ["Software Development :: Libraries :: Perl Modules", "Utilities"],
30            # Typing
31        },
32
33        TextTable => {
34            can_align_cell_containing_wide_character => 1,
35            can_align_cell_containing_color_code     => 1,
36            can_align_cell_containing_newline        => 1,
37            can_use_box_character                    => 1,
38            can_customize_border                     => 1,
39            can_halign                               => 1,
40            can_halign_individual_row                => 1,
41            can_halign_individual_column             => 1,
42            can_halign_individual_cell               => 1,
43            can_valign                               => 1,
44            can_valign_individual_row                => 1,
45            can_valign_individual_column             => 1,
46            can_valign_individual_cell               => 1,
47            can_rowspan                              => 1,
48            can_colspan                              => 1,
49            can_color                                => 0,
50            can_color_theme                          => 0,
51            can_set_cell_height                      => 0,
52            can_set_cell_height_of_individual_row    => 0,
53            can_set_cell_width                       => 0,
54            can_set_cell_width_of_individual_column  => 0,
55            speed                                    => 'slow',
56            can_hpad                                 => 0,
57            can_hpad_individual_row                  => 0,
58            can_hpad_individual_column               => 0,
59            can_hpad_individual_cell                 => 0,
60            can_vpad                                 => 0,
61            can_vpad_individual_row                  => 0,
62            can_vpad_individual_column               => 0,
63            can_vpad_individual_cell                 => 0,
64        },
65    },
66);
67
68use List::AllUtils qw(first firstidx max);
69
70use Exporter qw(import);
71our @EXPORT_OK = qw/ generate_table /;
72
73our $_split_lines_func;
74our $_pad_func;
75our $_length_height_func;
76
77# consts
78sub IDX_EXPTABLE_CELL_ROWSPAN()         {0} # number of rowspan, only defined for the rowspan head
79sub IDX_EXPTABLE_CELL_COLSPAN()         {1} # number of colspan, only defined for the colspan head
80sub IDX_EXPTABLE_CELL_WIDTH()           {2} # visual width. this does not include the cell padding.
81sub IDX_EXPTABLE_CELL_HEIGHT()          {3} # visual height. this does not include row separator.
82sub IDX_EXPTABLE_CELL_ORIG()            {4} # str/hash
83sub IDX_EXPTABLE_CELL_IS_ROWSPAN_TAIL() {5} # whether this cell is tail of a rowspan
84sub IDX_EXPTABLE_CELL_IS_COLSPAN_TAIL() {6} # whether this cell is tail of a colspan
85
86# whether an exptable cell is the head (1st cell) or tail (the rest) of a
87# rowspan/colspan. these should be macros if possible, for speed.
88sub _exptable_cell_is_rowspan_tail { defined($_[0]) &&  $_[0][IDX_EXPTABLE_CELL_IS_ROWSPAN_TAIL] }
89sub _exptable_cell_is_colspan_tail { defined($_[0]) &&  $_[0][IDX_EXPTABLE_CELL_IS_COLSPAN_TAIL] }
90sub _exptable_cell_is_tail         { defined($_[0]) && ($_[0][IDX_EXPTABLE_CELL_IS_ROWSPAN_TAIL] || $_[0][IDX_EXPTABLE_CELL_IS_COLSPAN_TAIL]) }
91sub _exptable_cell_is_rowspan_head { defined($_[0]) && !$_[0][IDX_EXPTABLE_CELL_IS_ROWSPAN_TAIL] }
92sub _exptable_cell_is_colspan_head { defined($_[0]) && !$_[0][IDX_EXPTABLE_CELL_IS_COLSPAN_TAIL] }
93sub _exptable_cell_is_head         { defined($_[0]) && defined $_[0][IDX_EXPTABLE_CELL_ORIG] }
94
95sub _divide_int_to_n_ints {
96    my ($int, $n) = @_;
97    my $subtot = 0;
98    my $int_subtot = 0;
99    my $prev_int_subtot = 0;
100    my @ints;
101    for (1..$n) {
102        $subtot += $int/$n;
103        $int_subtot = sprintf "%.0f", $subtot;
104        push @ints, $int_subtot - $prev_int_subtot;
105        $prev_int_subtot = $int_subtot;
106    }
107    @ints;
108}
109
110sub _vpad {
111    my ($lines, $num_lines, $width, $which) = @_;
112    return $lines if @$lines >= $num_lines; # we don't do truncate
113    my @vpadded_lines;
114    my $pad_line = " " x $width;
115    if ($which =~ /^b/) { # bottom padding
116        push @vpadded_lines, @$lines;
117        push @vpadded_lines, $pad_line for @$lines+1 .. $num_lines;
118    } elsif ($which =~ /^t/) { # top padding
119        push @vpadded_lines, $pad_line for @$lines+1 .. $num_lines;
120        push @vpadded_lines, @$lines;
121    } else { # center padding
122        my $p  = $num_lines - @$lines;
123        my $p1 = int($p/2);
124        my $p2 = $p - $p1;
125        push @vpadded_lines, $pad_line for 1..$p1;
126        push @vpadded_lines, @$lines;
127        push @vpadded_lines, $pad_line for 1..$p2;
128    }
129    \@vpadded_lines;
130}
131
132sub _get_attr {
133    my ($attr_name, $y, $x, $cell_value, $table_args) = @_;
134
135  CELL_ATTRS_FROM_CELL_VALUE: {
136        last unless ref $cell_value eq 'HASH';
137        my $attr_val = $cell_value->{$attr_name};
138        return $attr_val if defined $attr_val;
139    }
140
141  CELL_ATTRS_FROM_CELL_ATTRS_ARG:
142    {
143        last unless defined $x && defined $y;
144        my $cell_attrs = $table_args->{cell_attrs};
145        last unless $cell_attrs;
146        for my $entry (@$cell_attrs) {
147            next unless $entry->[0] == $y && $entry->[1] == $x;
148            my $attr_val = $entry->[2]{$attr_name};
149            return $attr_val if defined $attr_val;
150        }
151    }
152
153  COL_ATTRS:
154    {
155        last unless defined $x;
156        my $col_attrs = $table_args->{col_attrs};
157        last unless $col_attrs;
158        for my $entry (@$col_attrs) {
159            next unless $entry->[0] == $x;
160            my $attr_val = $entry->[1]{$attr_name};
161            return $attr_val if defined $attr_val;
162        }
163    }
164
165  ROW_ATTRS:
166    {
167        last unless defined $y;
168        my $row_attrs = $table_args->{row_attrs};
169        last unless $row_attrs;
170        for my $entry (@$row_attrs) {
171            next unless $entry->[0] == $y;
172            my $attr_val = $entry->[1]{$attr_name};
173            return $attr_val if defined $attr_val;
174        }
175    }
176
177  TABLE_ARGS:
178    {
179        my $attr_val = $table_args->{$attr_name};
180        return $attr_val if defined $attr_val;
181    }
182
183    undef;
184}
185
186sub _get_exptable_cell_lines {
187    my ($table_args, $exptable, $row_heights, $column_widths,
188        $bottom_borders, $intercol_width, $y, $x) = @_;
189
190    my $exptable_cell = $exptable->[$y][$x];
191    my $cell   = $exptable_cell->[IDX_EXPTABLE_CELL_ORIG];
192    my $text   = ref $cell eq 'HASH' ? $cell->{text} : $cell;
193    my $align  = _get_attr('align', $y, $x, $cell, $table_args) // 'left';
194    my $valign = _get_attr('valign', $y, $x, $cell, $table_args) // 'top';
195    my $pad    = $align eq 'left' ? 'r' : $align eq 'right' ? 'l' : 'c';
196    my $vpad   = $valign eq 'top' ? 'b' : $valign eq 'bottom' ? 't' : 'c';
197    my $height = 0;
198    my $width  = 0;
199    for my $ic (1..$exptable_cell->[IDX_EXPTABLE_CELL_COLSPAN]) {
200        $width += $column_widths->[$x+$ic-1];
201        $width += $intercol_width if $ic > 1;
202    }
203    for my $ir (1..$exptable_cell->[IDX_EXPTABLE_CELL_ROWSPAN]) {
204        $height += $row_heights->[$y+$ir-1];
205        $height++ if $bottom_borders->[$y+$ir-2] && $ir > 1;
206    }
207
208    my @datalines = map { $_pad_func->($_, $width, $pad, ' ', 'truncate') }
209        ($_split_lines_func->($text));
210    _vpad(\@datalines, $height, $width, $vpad);
211}
212
213sub generate_table {
214    require Module::Load::Util;
215    require Text::NonWideChar::Util;
216
217    my %args = @_;
218    my $rows = $args{rows} or die "Please specify rows";
219    my $bs_name = $args{border_style} // 'ASCII::SingleLineDoubleAfterHeader';
220    my $cell_attrs = $args{cell_attrs} // [];
221
222    my $bs_obj = Module::Load::Util::instantiate_class_with_optional_args({ns_prefix=>"BorderStyle"}, $bs_name);
223
224  DETERMINE_CODES: {
225        my $color = $args{color};
226        my $wide_char = $args{wide_char};
227
228        # split_lines
229        if ($color) {
230            require Text::ANSI::Util;
231            $_split_lines_func = sub { Text::ANSI::Util::ta_add_color_resets(split /\R/, $_[0]) };
232        } else {
233            $_split_lines_func = sub { split /\R/, $_[0] };
234        }
235
236        # pad & length_height
237        if ($color) {
238            if ($wide_char) {
239                require Text::ANSI::WideUtil;
240                $_pad_func           = \&Text::ANSI::WideUtil::ta_mbpad;
241                $_length_height_func = \&Text::ANSI::WideUtil::ta_mbswidth_height;
242            } else {
243                require Text::ANSI::Util;
244                $_pad_func           = \&Text::ANSI::Util::ta_pad;
245                $_length_height_func = \&Text::ANSI::Util::ta_length_height;
246            }
247        } else {
248            if ($wide_char) {
249                require Text::WideChar::Util;
250                $_pad_func           = \&Text::WideChar::Util::mbpad;
251                $_length_height_func = \&Text::WideChar::Util::mbswidth_height;
252            } else {
253                require String::Pad;
254                require Text::NonWideChar::Util;
255                $_pad_func           = \&String::Pad::pad;
256                $_length_height_func = \&Text::NonWideChar::Util::length_height;
257            }
258        }
259    }
260
261    # XXX when we allow cell attrs right_border and left_border, this will
262    # become array too like $exptable_bottom_borders.
263    my $intercol_width = length(" " . $bs_obj->get_border_char(3, 1) . " ");
264
265    my $exptable = []; # [ [[$orig_rowidx,$orig_colidx,$rowspan,$colspan,...], ...], [[...], ...], ... ]
266    my $exptable_bottom_borders = []; # idx=exptable rownum, val=bool
267    my $M = 0; # number of rows in the exptable
268    my $N = 0; # number of columns in the exptable
269  CONSTRUCT_EXPTABLE: {
270        # 1. the first step is to construct a 2D array we call "exptable" (short
271        # for expanded table), which is like the original table but with all the
272        # spanning rows/columns split into the smaller boxes so it's easier to
273        # draw later. for example, a table cell with colspan=2 will become 2
274        # exptable cells. an m-row x n-column table will become M-row x N-column
275        # exptable, where M>=m, N>=n.
276
277        my $rownum;
278
279        # 1a. first substep: construct exptable and calculate everything except
280        # each exptable cell's width and height, because this will require
281        # information from the previous substeps.
282
283        $rownum = -1;
284        for my $row (@$rows) {
285            $rownum++;
286            my $colnum = -1;
287            $exptable->[$rownum] //= [];
288            push @{ $exptable->[$rownum] }, undef
289                if (@{ $exptable->[$rownum] } == 0 ||
290                defined($exptable->[$rownum][-1]));
291            #use DDC; say "D:exptable->[$rownum] = ", DDC::dump($exptable->[$rownum]);
292            my $exptable_colnum = firstidx {!defined} @{ $exptable->[$rownum] };
293            #say "D:rownum=$rownum, exptable_colnum=$exptable_colnum";
294            if ($exptable_colnum == -1) { $exptable_colnum = 0 }
295            $exptable_bottom_borders->[$rownum] //= $args{separate_rows} ? 1:0;
296
297            for my $cell (@$row) {
298                $colnum++;
299                my $text;
300
301                my $rowspan = 1;
302                my $colspan = 1;
303                if (ref $cell eq 'HASH') {
304                    $text = $cell->{text};
305                    $rowspan = $cell->{rowspan} if $cell->{rowspan};
306                    $colspan = $cell->{colspan} if $cell->{colspan};
307                } else {
308                    $text = $cell;
309                    my $el;
310                    $el = first {$_->[0] == $rownum && $_->[1] == $colnum && $_->[2]{rowspan}} @$cell_attrs;
311                    $rowspan = $el->[2]{rowspan} if $el;
312                    $el = first {$_->[0] == $rownum && $_->[1] == $colnum && $_->[2]{colspan}} @$cell_attrs;
313                    $colspan = $el->[2]{colspan} if $el;
314                }
315
316                my @widths;
317                my @heights;
318              ROW:
319                for my $ir (1..$rowspan) {
320                    for my $ic (1..$colspan) {
321                        my $exptable_cell;
322                        $exptable->[$rownum+$ir-1][$exptable_colnum+$ic-1] = $exptable_cell = [];
323
324                        if ($ir == 1 && $ic == 1) {
325                            $exptable_cell->[IDX_EXPTABLE_CELL_ROWSPAN]     = $rowspan;
326                            $exptable_cell->[IDX_EXPTABLE_CELL_COLSPAN]     = $colspan;
327                            $exptable_cell->[IDX_EXPTABLE_CELL_ORIG]        = $cell;
328                        } else {
329                            $exptable_cell->[IDX_EXPTABLE_CELL_IS_ROWSPAN_TAIL] = 1 if $ir > 1;
330                            $exptable_cell->[IDX_EXPTABLE_CELL_IS_COLSPAN_TAIL] = 1 if $ic > 1;
331                        }
332                        #use DDC; dd $exptable; say ''; # debug
333                    }
334
335                    # determine whether we should draw bottom border of each row
336                    if ($rownum+$ir-1 == 0 && $args{header_row}) {
337                        $exptable_bottom_borders->[0] = 1
338                    } else {
339                        my $val;
340                        $val = _get_attr('bottom_border', $rownum+$ir-1, 0, $cell, \%args);     $exptable_bottom_borders->[$rownum+$ir-1] = $val if $val;
341                        $val = _get_attr('top_border'   , $rownum+$ir-1, 0, $cell, \%args);     $exptable_bottom_borders->[$rownum+$ir-2] = $val if $val;
342                        $val = _get_attr('bottom_border', $rownum+$ir-1, undef, undef, \%args); $exptable_bottom_borders->[$rownum+$ir-1] = $val if $val;
343                        $val = _get_attr('top_border'   , $rownum+$ir-1, undef, undef, \%args); $exptable_bottom_borders->[$rownum+$ir-2] = $val if $val;
344                    }
345
346                    $M = $rownum+$ir if $M < $rownum+$ir;
347                }
348
349                $exptable_colnum += $colspan;
350                $exptable_colnum++ while defined $exptable->[$rownum][$exptable_colnum];
351
352            } # for a row
353            $N = $exptable_colnum if $N < $exptable_colnum;
354        } # for rows
355
356        # 1b. calculate the heigth and width of each exptable cell (as required
357        # by the text, or specified width/height when we allow cell attrs width,
358        # height)
359
360        for my $exptable_rownum (0..$M-1) {
361            for my $exptable_colnum (0..$N-1) {
362                my $exptable_cell = $exptable->[$exptable_rownum][$exptable_colnum];
363                next if _exptable_cell_is_tail($exptable_cell);
364                my $rowspan = $exptable_cell->[IDX_EXPTABLE_CELL_ROWSPAN];
365                my $colspan = $exptable_cell->[IDX_EXPTABLE_CELL_COLSPAN];
366                my $cell = $exptable_cell->[IDX_EXPTABLE_CELL_ORIG];
367                my $text = ref $cell eq 'HASH' ? $cell->{text} : $cell;
368                my $lh = $_length_height_func->($text);
369                #use DDC; say "D:length_height[$exptable_rownum,$exptable_colnum] = (".DDC::dump($text)."): ".DDC::dump($lh);
370                my $tot_intercol_widths = ($colspan-1) * $intercol_width;
371                my $tot_interrow_heights = 0; for (1..$rowspan-1) { $tot_interrow_heights++ if $exptable_bottom_borders->[$exptable_rownum+$_-1] }
372                #say "D:interrow_heights=$tot_interrow_heights";
373                my @heights = _divide_int_to_n_ints(max(0, $lh->[1] - $tot_interrow_heights), $rowspan);
374                my @widths  = _divide_int_to_n_ints(max(0, $lh->[0] - $tot_intercol_widths ), $colspan);
375                for my $ir (1..$rowspan) {
376                    for my $ic (1..$colspan) {
377                        $exptable->[$exptable_rownum+$ir-1][$exptable_colnum+$ic-1][IDX_EXPTABLE_CELL_HEIGHT]  = $heights[$ir-1];
378                        $exptable->[$exptable_rownum+$ir-1][$exptable_colnum+$ic-1][IDX_EXPTABLE_CELL_WIDTH]   = $widths [$ic-1];
379                    }
380                }
381            }
382        } # for rows
383
384    } # CONSTRUCT_EXPTABLE
385    #use DDC; dd $exptable; # debug
386    #print "D: exptable size: $M x $N (HxW)\n"; # debug
387    #use DDC; print "bottom borders: "; dd $exptable_bottom_borders; # debug
388
389  OPTIMIZE_EXPTABLE: {
390        # TODO
391
392        # 2. we reduce extraneous columns and rows if there are colspan that are
393        # too many. for example, if all exptable cells in column 1 has colspan=2
394        # (or one row has colspan=2 and another row has colspan=3), we might as
395        # remove 1 column because the extra column span doesn't have any
396        # content. same case for extraneous row spans.
397
398        # 2a. remove extra undefs. skip this. doesn't make a difference.
399        #for my $exptable_row (@{ $exptable }) {
400        #    splice @$exptable_row, $N if @$exptable_row > $N;
401        #}
402
403        1;
404    } # OPTIMIZE_EXPTABLE
405    #use DDC; dd $exptable; # debug
406
407    my $exptable_column_widths  = []; # idx=exptable colnum
408    my $exptable_row_heights    = []; # idx=exptable rownum
409  DETERMINE_SIZE_OF_EACH_EXPTABLE_COLUMN_AND_ROW: {
410        # 3. before we draw the exptable, we need to determine the width and
411        # height of each exptable column and row.
412        #use DDC;
413        for my $ir (0..$M-1) {
414            my $exptable_row = $exptable->[$ir];
415            $exptable_row_heights->[$ir] = max(
416                1, map {$_->[IDX_EXPTABLE_CELL_HEIGHT] // 0} @$exptable_row);
417        }
418
419        for my $ic (0..$N-1) {
420            $exptable_column_widths->[$ic] = max(
421                1, map {$exptable->[$_][$ic] ? $exptable->[$_][$ic][IDX_EXPTABLE_CELL_WIDTH] : 0} 0..$M-1);
422        }
423    } # DETERMINE_SIZE_OF_EACH_EXPTABLE_COLUMN_AND_ROW
424    #use DDC; print "column widths: "; dd $exptable_column_widths; # debug
425    #use DDC; print "row heights: "; dd $exptable_row_heights; # debug
426
427    # each elem is an arrayref containing characters to render a line of the
428    # table, e.g. for element [0] the row is all borders. for element [1]:
429    # [$left_border_str, $exptable_cell_content1, $border_between_col,
430    # $exptable_cell_content2, ...]. all will be joined together with "\n" to
431    # form the final rendered table.
432    my @buf;
433
434  DRAW_EXPTABLE: {
435        # 4. finally we draw the (exp)table.
436
437        my $y = 0;
438
439        for my $ir (0..$M-1) {
440
441          DRAW_TOP_BORDER:
442            {
443                last unless $ir == 0;
444                my $b_y = $args{header_row} ? 0 : 6;
445                my $b_topleft    = $bs_obj->get_border_char($b_y, 0);
446                my $b_topline    = $bs_obj->get_border_char($b_y, 1);
447                my $b_topbetwcol = $bs_obj->get_border_char($b_y, 2);
448                my $b_topright   = $bs_obj->get_border_char($b_y, 3);
449                last unless length $b_topleft || length $b_topline || length $b_topbetwcol || length $b_topright;
450                $buf[$y][0] = $b_topleft;
451                for my $ic (0..$N-1) {
452                    my $cell_right = $ic < $N-1 ? $exptable->[$ir][$ic+1] : undef;
453                    my $cell_right_has_content = defined $cell_right && _exptable_cell_is_head($cell_right);
454                    $buf[$y][$ic*4+2] = $bs_obj->get_border_char($b_y, 1, $exptable_column_widths->[$ic]+2); # +1, +2, +3
455                    $buf[$y][$ic*4+4] = $ic == $N-1 ? $b_topright : ($cell_right_has_content ? $b_topbetwcol : $b_topline);
456                }
457                $y++;
458            } # DRAW_TOP_BORDER
459
460            # DRAW_DATA_OR_HEADER_ROW
461            {
462                # draw leftmost border, which we always do.
463                my $b_y = $ir == 0 && $args{header_row} ? 1 : 3;
464                for my $i (1 .. $exptable_row_heights->[$ir]) {
465                    $buf[$y+$i-1][0] = $bs_obj->get_border_char($b_y, 0);
466                }
467
468                my $lines;
469                for my $ic (0..$N-1) {
470                    my $cell = $exptable->[$ir][$ic];
471
472                    # draw cell content. also possibly draw border between
473                    # cells. we don't draw border inside a row/colspan.
474                    if (_exptable_cell_is_head($cell)) {
475                        $lines = _get_exptable_cell_lines(
476                            \%args, $exptable, $exptable_row_heights, $exptable_column_widths,
477                            $exptable_bottom_borders, $intercol_width, $ir, $ic);
478                        for my $i (0..$#{$lines}) {
479                            $buf[$y+$i][$ic*4+0] = $bs_obj->get_border_char($b_y, 1);
480                            $buf[$y+$i][$ic*4+1] = " ";
481                            $buf[$y+$i][$ic*4+2] = $lines->[$i];
482                            $buf[$y+$i][$ic*4+3] = " ";
483                        }
484                        #use DDC; say "D: Drawing exptable_cell($ir,$ic): ", DDC::dump($lines);
485                    }
486
487                    # draw rightmost border, which we always do.
488                    if ($ic == $N-1) {
489                        my $b_y = $ir == 0 && $args{header_row} ? 1 : 3;
490                        for my $i (1 .. $exptable_row_heights->[$ir]) {
491                            $buf[$y+$i-1][$ic*4+4] = $bs_obj->get_border_char($b_y, 2);
492                        }
493                    }
494
495                }
496            } # DRAW_DATA_OR_HEADER_ROW
497            $y += $exptable_row_heights->[$ir];
498
499          DRAW_ROW_SEPARATOR:
500            {
501                last unless $ir < $M-1;
502                last unless $exptable_bottom_borders->[$ir];
503                my $b_y = $ir == 0 && $args{header_row} ? 2 : 4;
504                my $b_betwrowleft    = $bs_obj->get_border_char($b_y, 0);
505                my $b_betwrowline    = $bs_obj->get_border_char($b_y, 1);
506                my $b_betwrowbetwcol = $bs_obj->get_border_char($b_y, 2);
507                my $b_betwrowright   = $bs_obj->get_border_char($b_y, 3);
508                last unless length $b_betwrowleft || length $b_betwrowline || length $b_betwrowbetwcol || length $b_betwrowright;
509                my $b_betwrowbetwcol_notop = $bs_obj->get_border_char($b_y, 4);
510                my $b_betwrowbetwcol_nobot = $bs_obj->get_border_char($b_y, 5);
511                my $b_betwrowbetwcol_noleft  = $bs_obj->get_border_char($b_y, 6);
512                my $b_betwrowbetwcol_noright = $bs_obj->get_border_char($b_y, 7);
513                my $b_yd = $ir == 0 && $args{header_row} ? 2 : 3;
514                my $b_datarowleft    = $bs_obj->get_border_char($b_yd, 0);
515                my $b_datarowbetwcol = $bs_obj->get_border_char($b_yd, 1);
516                my $b_datarowright   = $bs_obj->get_border_char($b_yd, 2);
517                for my $ic (0..$N-1) {
518                    my $cell             = $exptable->[$ir][$ic];
519                    my $cell_right       = $ic < $N-1 ? $exptable->[$ir][$ic+1] : undef;
520                    my $cell_bottom      = $ir < $M-1 ? $exptable->[$ir+1][$ic] : undef;
521                    my $cell_rightbottom = $ir < $M-1 && $ic < $N-1 ? $exptable->[$ir+1][$ic+1] : undef;
522
523                    # leftmost border
524                    if ($ic == 0) {
525                        $buf[$y][0] = _exptable_cell_is_rowspan_tail($cell_bottom) ? $b_datarowleft : $b_betwrowleft;
526                    }
527
528                    # along the width of cell content
529                    if (_exptable_cell_is_rowspan_head($cell_bottom)) {
530                        $buf[$y][$ic*4+2] = $bs_obj->get_border_char($b_y, 1, $exptable_column_widths->[$ic]+2);
531                    }
532
533                    my $char;
534                    if ($ic == $N-1) {
535                        # rightmost
536                        if (_exptable_cell_is_rowspan_tail($cell_bottom)) {
537                            $char = $b_datarowright;
538                        } else {
539                            $char = $b_betwrowright;
540                        }
541                    } else {
542                        # between cells
543                        if (_exptable_cell_is_colspan_tail($cell_right)) {
544                            if (_exptable_cell_is_colspan_tail($cell_rightbottom)) {
545                                if (_exptable_cell_is_rowspan_tail($cell_bottom)) {
546                                    $char = "";
547                                } else {
548                                    $char = $b_betwrowline;
549                                }
550                            } else {
551                                $char = $b_betwrowbetwcol_notop;
552                            }
553                        } else {
554                            if (_exptable_cell_is_colspan_tail($cell_rightbottom)) {
555                                $char = $b_betwrowbetwcol_nobot;
556                            } else {
557                                if (_exptable_cell_is_rowspan_tail($cell_bottom)) {
558                                    if (_exptable_cell_is_rowspan_tail($cell_rightbottom)) {
559                                        $char = $b_datarowbetwcol;
560                                    } else {
561                                        $char = $b_betwrowbetwcol_noleft;
562                                    }
563                                } elsif (_exptable_cell_is_rowspan_tail($cell_rightbottom)) {
564                                    $char = $b_betwrowbetwcol_noright;
565                                } else {
566                                    $char = $b_betwrowbetwcol;
567                                }
568                            }
569                        }
570                    }
571                    $buf[$y][$ic*4+4] = $char;
572
573                }
574                $y++;
575            } # DRAW_ROW_SEPARATOR
576
577          DRAW_BOTTOM_BORDER:
578            {
579                last unless $ir == $M-1;
580                my $b_y = $ir == 0 && $args{header_row} ? 7 : 5;
581                my $b_botleft    = $bs_obj->get_border_char($b_y, 0);
582                my $b_botline    = $bs_obj->get_border_char($b_y, 1);
583                my $b_botbetwcol = $bs_obj->get_border_char($b_y, 2);
584                my $b_botright   = $bs_obj->get_border_char($b_y, 3);
585                last unless length $b_botleft || length $b_botline || length $b_botbetwcol || length $b_botright;
586                $buf[$y][0] = $b_botleft;
587                for my $ic (0..$N-1) {
588                    my $cell_right = $ic < $N-1 ? $exptable->[$ir][$ic+1] : undef;
589                    $buf[$y][$ic*4+2] = $bs_obj->get_border_char($b_y, 1, $exptable_column_widths->[$ic]+2);
590                    $buf[$y][$ic*4+4] = $ic == $N-1 ? $b_botright : (_exptable_cell_is_colspan_tail($cell_right) ? $b_botline : $b_botbetwcol);
591                }
592                $y++;
593            } # DRAW_BOTTOM_BORDER
594
595        }
596    } # DRAW_EXPTABLE
597
598    for my $row (@buf) { for (@$row) { $_ = "" if !defined($_) } } # debug. remove undef to "" to save dump width
599    #use DDC; dd \@buf;
600    join "", (map { my $linebuf = $_; join("", grep {defined} @$linebuf)."\n" } @buf);
601}
602
603# Back-compat: 'table' is an alias for 'generate_table', but isn't exported
604{
605    no warnings 'once';
606    *table = \&generate_table;
607}
608
6091;
610# ABSTRACT: Generate text table with simple interface and many options
611
612__END__
613
614=pod
615
616=encoding UTF-8
617
618=head1 NAME
619
620Text::Table::More - Generate text table with simple interface and many options
621
622=head1 VERSION
623
624This document describes version 0.020 of Text::Table::More (from Perl distribution Text-Table-More), released on 2021-08-28.
625
626=head1 SYNOPSIS
627
628 #!perl
629
630 use 5.010001;
631 use strict;
632 use warnings;
633
634 use Text::Table::More qw/generate_table/;
635
636 my $rows = [
637     # header row
638     ["Year",
639      "Comedy",
640      "Drama",
641      "Variety",
642      "Lead Comedy Actor",
643      "Lead Drama Actor",
644      "Lead Comedy Actress",
645      "Lead Drama Actress"],
646
647     # first data row
648     [1962,
649      "The Bob Newhart Show (NBC)",
650      {text=>"The Defenders (CBS)", rowspan=>3}, # each cell can be hashref to specify text (content) as well as attributes
651      "The Garry Moore Show (CBS)",
652      {text=>"E. G. Marshall, The Defenders (CBS)", rowspan=>2, colspan=>2},
653      {text=>"Shirley Booth, Hazel (NBC)", rowspan=>2, colspan=>2}],
654
655     # second data row
656     [1963,
657      {text=>"The Dick Van Dyke Show (CBS)", rowspan=>2},
658      "The Andy Williams Show (NBC)"],
659
660     # third data row
661     [1964,
662      "The Danny Kaye Show (CBS)",
663      {text=>"Dick Van Dyke, The Dick Van Dyke Show (CBS)", colspan=>2},
664      {text=>"Mary Tyler Moore, The Dick Van Dyke Show (CBS)", colspan=>2}],
665
666     # fourth data row
667     [1965,
668      {text=>"four winners (Outstanding Program Achievements in Entertainment)", colspan=>3},
669      {text=>"five winners (Outstanding Program Achievements in Entertainment)", colspan=>4}],
670
671     # fifth data row
672     [1966,
673      "The Dick Van Dyke Show (CBS)",
674      "The Fugitive (ABC)",
675      "The Andy Williams Show (NBC)",
676      "Dick Van Dyke, The Dick Van Dyke Show (CBS)",
677      "Bill Cosby, I Spy (CBS)",
678      "Mary Tyler Moore, The Dick Van Dyke Show (CBS)",
679      "Barbara Stanwyck, The Big Valley (CBS)"],
680 ];
681
682 binmode STDOUT, "utf8";
683 print generate_table(
684     rows => $rows,      # required
685     header_row => 1,    # optional, default 0
686     separate_rows => 1, # optional, default 0
687     border_style => $ARGV[0] // 'ASCII::SingleLineDoubleAfterHeader', # optional, this is module name in BorderStyle::* namespace, without the prefix
688     #align => 'left',   # optional, default 'left'. can be left/middle/right.
689     #valign => 'top',   # optional, default 'top'. can be top/middle/bottom.
690     #color => 1,        # optional, default 0. turn on support for cell content that contain ANSI color codes.
691     #wide_char => 1,    # optional, default 0. turn on support for wide Unicode characters.
692
693     row_attrs => [      # optional, specify per-row attributes
694         # rownum (0-based int), attributes (hashref)
695         [0, {align=>'middle', bottom_border=>1}],
696     ],
697
698     col_attrs => [      # optional, per-column attributes
699         # colnum (0-based int), attributes (hashref)
700         [2, {valign=>'middle'}],
701     ],
702
703     #cell_attrs => [    # optional, per-cell attributes
704     #    # rownum (0-based int), colnum (0-based int), attributes (hashref)
705     #    [1, 2, {rowspan=>3}],
706     #    [1, 4, {rowspan=>2, colspan=>2}],
707     #    [1, 5, {rowspan=>2, colspan=>2}],
708     #    [2, 1, {rowspan=>2}],
709     #    [3, 2, {colspan=>2}],
710     #    [3, 3, {colspan=>2}],
711     #    [4, 1, {colspan=>3}],
712     #    [4, 2, {colspan=>4}],
713     #],
714
715 );
716
717will output something like:
718
719 .------+------------------------------+----------------------+------------------------------+---------------------------------------------+-------------------------+------------------------------------------------+----------------------------------------.
720 | Year |            Comedy            |        Drama         |           Variety            |              Lead Comedy Actor              |    Lead Drama Actor     |              Lead Comedy Actress               |           Lead Drama Actress           |
721 +======+==============================+======================+==============================+=============================================+=========================+================================================+========================================+
722 | 1962 | The Bob Newhart Show (NBC)   |                      | The Garry Moore Show (CBS)   | E. G. Marshall, The Defenders (CBS)                                   | Shirley Booth, Hazel (NBC)                                                              |
723 +------+------------------------------+                      +------------------------------+                                                                       |                                                                                         |
724 | 1963 | The Dick Van Dyke Show (CBS) | The Defenders (CBS)  | The Andy Williams Show (NBC) |                                                                       |                                                                                         |
725 +------+                              |                      +------------------------------+-----------------------------------------------------------------------+-----------------------------------------------------------------------------------------+
726 | 1964 |                              |                      | The Danny Kaye Show (CBS)    | Dick Van Dyke, The Dick Van Dyke Show (CBS)                           | Mary Tyler Moore, The Dick Van Dyke Show (CBS)                                          |
727 +------+------------------------------+----------------------+------------------------------+-----------------------------------------------------------------------+-----------------------------------------------------------------------------------------+
728 | 1965 | four winners (Outstanding Program Achievements in Entertainment)                   | five winners (Outstanding Program Achievements in Entertainment)                                                                                                |
729 +------+------------------------------+----------------------+------------------------------+---------------------------------------------+-------------------------+------------------------------------------------+----------------------------------------+
730 | 1966 | The Dick Van Dyke Show (CBS) | The Fugitive (ABC)   | The Andy Williams Show (NBC) | Dick Van Dyke, The Dick Van Dyke Show (CBS) | Bill Cosby, I Spy (CBS) | Mary Tyler Moore, The Dick Van Dyke Show (CBS) | Barbara Stanwyck, The Big Valley (CBS) |
731 `------+------------------------------+----------------------+------------------------------+---------------------------------------------+-------------------------+------------------------------------------------+----------------------------------------'
732
733If you set the C<border_style> argument to C<"UTF8::SingleLineBoldHeader">:
734
735 print generate_table(
736     rows => $rows,
737     border_style => "UTF8::SingleLineBoldHeader",
738     ...
739 );
740
741then the output will be something like:
742
743 ┏━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
744 ┃ Year ┃            Comedy            ┃        Drama         ┃           Variety            ┃              Lead Comedy Actor              ┃    Lead Drama Actor     ┃              Lead Comedy Actress               ┃           Lead Drama Actress           ┃
745 ┡━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
746 │ 1962 │ The Bob Newhart Show (NBC)   │                      │ The Garry Moore Show (CBS)   │ E. G. Marshall, The Defenders (CBS)                                   │ Shirley Booth, Hazel (NBC)                                                              │
747 ├──────┼──────────────────────────────┤                      ├──────────────────────────────┤                                                                       │                                                                                         │
748 │ 1963 │ The Dick Van Dyke Show (CBS) │ The Defenders (CBS)  │ The Andy Williams Show (NBC) │                                                                       │                                                                                         │
749 ├──────┤                              │                      ├──────────────────────────────┼───────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
750 │ 1964 │                              │                      │ The Danny Kaye Show (CBS)    │ Dick Van Dyke, The Dick Van Dyke Show (CBS)                           │ Mary Tyler Moore, The Dick Van Dyke Show (CBS)                                          │
751 ├──────┼──────────────────────────────┴──────────────────────┴──────────────────────────────┼───────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┤
752 │ 1965 │ four winners (Outstanding Program Achievements in Entertainment)                   │ five winners (Outstanding Program Achievements in Entertainment)                                                                                                │
753 ├──────┼──────────────────────────────┬──────────────────────┬──────────────────────────────┼─────────────────────────────────────────────┬─────────────────────────┬────────────────────────────────────────────────┬────────────────────────────────────────┤
754 │ 1966 │ The Dick Van Dyke Show (CBS) │ The Fugitive (ABC)   │ The Andy Williams Show (NBC) │ Dick Van Dyke, The Dick Van Dyke Show (CBS) │ Bill Cosby, I Spy (CBS) │ Mary Tyler Moore, The Dick Van Dyke Show (CBS) │ Barbara Stanwyck, The Big Valley (CBS) │
755 └──────┴──────────────────────────────┴──────────────────────┴──────────────────────────────┴─────────────────────────────────────────────┴─────────────────────────┴────────────────────────────────────────────────┴────────────────────────────────────────┘
756
757=head1 DESCRIPTION
758
759This module uses the simple interface of L<Text::Table::Tiny> (0.04) with
760support for more formatting options like column/row spans, border style,
761per-row/column/cell align/valign.
762
763Keywords: rowspan, colspan.
764
765=for Pod::Coverage ^(.+)$
766
767=head1 DECLARED FEATURES
768
769Features declared by this module:
770
771=head2 From feature set PerlTrove
772
773Features from feature set L<PerlTrove|Module::Features::PerlTrove> declared by this module:
774
775=over
776
777=item * Development Status
778
779Value: "4 - Beta".
780
781=item * Environment
782
783Value: "Console".
784
785=item * Intended Audience
786
787Value: ["Developers"].
788
789=item * License
790
791Value: "OSI Approved :: Artistic License".
792
793=item * Programming Language
794
795Value: "Perl".
796
797=item * Topic
798
799Value: ["Software Development :: Libraries :: Perl Modules","Utilities"].
800
801=back
802
803=head2 From feature set TextTable
804
805Features from feature set L<TextTable|Module::Features::TextTable> declared by this module:
806
807=over
808
809=item * can_align_cell_containing_color_code
810
811Value: yes.
812
813=item * can_align_cell_containing_newline
814
815Value: yes.
816
817=item * can_align_cell_containing_wide_character
818
819Value: yes.
820
821=item * can_color
822
823Can produce colored table.
824
825Value: no.
826
827=item * can_color_theme
828
829Allow choosing colors from a named set of palettes.
830
831Value: no.
832
833=item * can_colspan
834
835Value: yes.
836
837=item * can_customize_border
838
839Let user customize border character in some way, e.g. selecting from several available borders, disable border.
840
841Value: yes.
842
843=item * can_halign
844
845Provide a way for user to specify horizontal alignment (leftE<sol>middleE<sol>right) of cells.
846
847Value: yes.
848
849=item * can_halign_individual_cell
850
851Provide a way for user to specify different horizontal alignment (leftE<sol>middleE<sol>right) for individual cells.
852
853Value: yes.
854
855=item * can_halign_individual_column
856
857Provide a way for user to specify different horizontal alignment (leftE<sol>middleE<sol>right) for individual columns.
858
859Value: yes.
860
861=item * can_halign_individual_row
862
863Provide a way for user to specify different horizontal alignment (leftE<sol>middleE<sol>right) for individual rows.
864
865Value: yes.
866
867=item * can_hpad
868
869Provide a way for user to specify horizontal padding of cells.
870
871Value: no.
872
873=item * can_hpad_individual_cell
874
875Provide a way for user to specify different horizontal padding of individual cells.
876
877Value: no.
878
879=item * can_hpad_individual_column
880
881Provide a way for user to specify different horizontal padding of individual columns.
882
883Value: no.
884
885=item * can_hpad_individual_row
886
887Provide a way for user to specify different horizontal padding of individual rows.
888
889Value: no.
890
891=item * can_rowspan
892
893Value: yes.
894
895=item * can_set_cell_height
896
897Allow setting height of rows.
898
899Value: no.
900
901=item * can_set_cell_height_of_individual_row
902
903Allow setting height of individual rows.
904
905Value: no.
906
907=item * can_set_cell_width
908
909Allow setting height of rows.
910
911Value: no.
912
913=item * can_set_cell_width_of_individual_column
914
915Allow setting height of individual rows.
916
917Value: no.
918
919=item * can_use_box_character
920
921Can use terminal box-drawing character when drawing border.
922
923Value: yes.
924
925=item * can_valign
926
927Provide a way for user to specify vertical alignment (topE<sol>middleE<sol>bottom) of cells.
928
929Value: yes.
930
931=item * can_valign_individual_cell
932
933Provide a way for user to specify different vertical alignment (topE<sol>middleE<sol>bottom) for individual cells.
934
935Value: yes.
936
937=item * can_valign_individual_column
938
939Provide a way for user to specify different vertical alignment (topE<sol>middleE<sol>bottom) for individual columns.
940
941Value: yes.
942
943=item * can_valign_individual_row
944
945Provide a way for user to specify different vertical alignment (topE<sol>middleE<sol>bottom) for individual rows.
946
947Value: yes.
948
949=item * can_vpad
950
951Provide a way for user to specify vertical padding of cells.
952
953Value: no.
954
955=item * can_vpad_individual_cell
956
957Provide a way for user to specify different vertical padding of individual cells.
958
959Value: no.
960
961=item * can_vpad_individual_column
962
963Provide a way for user to specify different vertical padding of individual columns.
964
965Value: no.
966
967=item * can_vpad_individual_row
968
969Provide a way for user to specify different vertical padding of individual rows.
970
971Value: no.
972
973=item * speed
974
975Subjective speed rating, relative to other text table modules.
976
977Value: "slow".
978
979=back
980
981For more details on module features, see L<Module::Features>.
982
983=head1 PER-ROW ATTRIBUTES
984
985=head2 align
986
987String. Value is either C<"left">, C<"middle">, C<"right">. Specify text
988alignment of cells. Override table argument, but is overridden by per-column or
989per-cell attribute of the same name.
990
991=head2 valign
992
993String. Value is either C<"top">, C<"middle">, C<"bottom">. Specify vertical
994text alignment of cells. Override table argument, but is overridden by
995per-column or per-cell attribute of the same name.
996
997=head2 bottom_border
998
999Boolean.
1000
1001=head2 top_border
1002
1003Boolean.
1004
1005=head1 PER-COLUMN ATTRIBUTES
1006
1007=head2 align
1008
1009String. Value is either C<"left">, C<"middle">, C<"right">. Specify text
1010alignment of cells. Override table argument and per-row attribute of the same
1011name, but is overridden by per-cell attribute of the same name.
1012
1013=head2 valign
1014
1015String. Value is either C<"top">, C<"middle">, C<"bottom">. Specify vertical
1016text alignment of cells. Override table argument and per-row attribute of the
1017same name, but is overridden by per-cell attribute of the same name.
1018
1019=head1 PER-CELL ATTRIBUTES
1020
1021=head2 align
1022
1023String. Value is either C<"left">, C<"middle">, C<"right">. Override table
1024argument, per-row attribute, and per-column attribute of the same name.
1025
1026=head2 valign
1027
1028String. Value is either C<"top">, C<"middle">, C<"bottom">. Specify vertical
1029text alignment of cells. Override table argument, per-row attribute, and
1030per-column attribute of the same name.
1031
1032=head2 colspan
1033
1034Positive integer. Default 1.
1035
1036=head2 rowspan
1037
1038Positive integer. Default 1.
1039
1040=head2 bottom_border.
1041
1042Boolean. Currently the attribute of he leftmost cell is used.
1043
1044=head2 top_border.
1045
1046Boolean. Currently the attribute of he leftmost cell is used.
1047
1048=head1 FUNCTIONS
1049
1050=head2 generate_table
1051
1052Usage:
1053
1054 my $table_str = generate_table(%args);
1055
1056Arguments:
1057
1058=over
1059
1060=item * rows
1061
1062Array of arrayrefs (of strings or hashrefs). Required. Each array element is a
1063row of cells. A cell can be a string like C<"foo"> specifying only the text
1064(equivalent to C<< {text=>"foo"} >>) or a hashref which allows you to specify a
1065cell's text (C<text>) as well as attributes like C<rowspan> (int, >= 1),
1066C<colspan> (int, >= 1), etc. See L</PER-CELL ATTRIBUTES> for the list of known
1067per-cell attributes.
1068
1069Currently, C<top_border> and C<bottom_border> needs to be specified for the
1070first column of a row and will take effect for the whole row.
1071
1072Alternatively, you can also specify cell attributes using L</cell_attrs>
1073argument.
1074
1075=item * header_row
1076
1077Boolean. Optional. Default 0. Whether to treat the first row as the header row,
1078which means draw a separator line between it and the rest.
1079
1080=item * border_style
1081
1082Str. Optional. Default to C<ASCII::SingleLineDoubleAfterHeader>. This is Perl
1083module under the L<BorderStyle> namespace, without the namespace prefix. To see
1084how a border style looks like, you can use the CLI L<show-border-style> from
1085L<App::BorderStyleUtils>.
1086
1087=item * align
1088
1089String. Value is either C<"left">, C<"middle">, C<"right">. Specify horizontal
1090text alignment of cells. Overriden by overridden by per-row, per-column, or
1091per-cell attribute of the same name.
1092
1093=item * valign
1094
1095String. Value is either C<"top">, C<"middle">, C<"bottom">. Specify vertical
1096text alignment of cells. Overriden by overridden by per-row, per-column, or
1097per-cell attribute of the same name.
1098
1099=item * row_attrs
1100
1101Array of records. Optional. Specify per-row attributes. Each record is a
11022-element arrayref: C<< [$row_idx, \%attrs] >>. C<$row_idx> is zero-based. See
1103L</PER-ROW ATTRIBUTES> for the list of known attributes.
1104
1105=item * col_attrs
1106
1107Array of records. Optional. Specify per-column attributes. Each record is a
11082-element arrayref: C<< [$col_idx, \%attrs] >>. C<$col_idx> is zero-based. See
1109L</PER-COLUMN ATTRIBUTES> for the list of known attributes.
1110
1111=item * cell_attrs
1112
1113Array of records. Optional. Specify per-cell attributes. Each record is a
11143-element arrayref: C<< [$row_idx, $col_idx, \%attrs] >>. C<$row_idx> and
1115C<$col_idx> are zero-based. See L</PER-CELL ATTRIBUTES> for the list of known
1116attributes.
1117
1118Alternatively, you can specify a cell's attribute in the L</rows> argument
1119directly, by specifying a cell as hashref.
1120
1121=item * separate_rows
1122
1123Boolean. Optional. Default 0. If set to true, will add a separator between data
1124rows. Equivalent to setting C<bottom_border> or C<top_border> attribute to true
1125for each row.
1126
1127=item * wide_char
1128
1129Boolean. Optional. Default false. Turn on wide character support. Cells that
1130contain wide Unicode characters will still be properly aligned. Note that this
1131requires optional prereq L<Text::WideChar::Util> or L<Text::ANSI::WideUtil>.
1132
1133=item * color
1134
1135Boolean. Optional. Default false. Turn on color support. Cells that contain ANSI
1136color codes will still be properly aligned. Note that this requires optional
1137prereq L<Text::ANSI::Util> or L<Text::ANSI::WideUtil>.
1138
1139=back
1140
1141=head1 HOMEPAGE
1142
1143Please visit the project's homepage at L<https://metacpan.org/release/Text-Table-More>.
1144
1145=head1 SOURCE
1146
1147Source repository is at L<https://github.com/perlancar/perl-Text-Table-More>.
1148
1149=head1 SEE ALSO
1150
1151L<Text::ANSITable> also offers lots of formatting options, but currently lacks
1152support for rowspan/colspan. It also uses an OO interface and has features I
1153never use: hiding rows and selecting display columns different from declared
1154columns. I currently plan to actively develop Text::Table::More instead of
1155Text::ANSITable, but we'll see.
1156
1157L<Acme::CPANModules::TextTable> contains a comparison and benchmark for modules
1158that generate text table.
1159
1160HTML E<lt>TABLEE<gt> element,
1161L<https://www.w3.org/TR/2014/REC-html5-20141028/tabular-data.html>,
1162L<https://www.w3.org/html/wiki/Elements/table>
1163
1164=head1 AUTHOR
1165
1166perlancar <perlancar@cpan.org>
1167
1168=head1 CONTRIBUTING
1169
1170
1171To contribute, you can send patches by email/via RT, or send pull requests on
1172GitHub.
1173
1174Most of the time, you don't need to build the distribution yourself. You can
1175simply modify the code, then test via:
1176
1177 % prove -l
1178
1179If you want to build the distribution (e.g. to try to install it locally on your
1180system), you can install L<Dist::Zilla>,
1181L<Dist::Zilla::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
1182Dist::Zilla plugin and/or Pod::Weaver::Plugin. Any additional steps required
1183beyond that are considered a bug and can be reported to me.
1184
1185=head1 COPYRIGHT AND LICENSE
1186
1187This software is copyright (c) 2021 by perlancar <perlancar@cpan.org>.
1188
1189This is free software; you can redistribute it and/or modify it under
1190the same terms as the Perl 5 programming language system itself.
1191
1192=head1 BUGS
1193
1194Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Text-Table-More>
1195
1196When submitting a bug or request, please include a test-file or a
1197patch to an existing test-file that illustrates the bug or desired
1198feature.
1199
1200=cut
1201