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