File Coverage

blib/lib/Text/SpanningTable.pm
Criterion Covered Total %
statement 136 136 100.0
branch 74 88 84.0
condition 11 16 68.7
subroutine 11 11 100.0
pod 9 9 100.0
total 241 260 92.6


line stmt bran cond sub pod time code
1             package Text::SpanningTable;
2              
3             our $VERSION = "1.000000";
4             $VERSION = eval $VERSION;
5              
6 3     3   40857 use warnings;
  3         6  
  3         87  
7 3     3   11 use strict;
  3         3  
  3         3812  
8              
9             # ABSTRACT: ASCII tables with support for column spanning.
10              
11             # this hash-ref holds the characters used to print the table decorations.
12             our $C = {
13             top => { # the top border, i.e. hr('top')
14             left => '.-',
15             border => '-',
16             sep => '-+-',
17             right => '-.',
18             },
19             middle => { # simple horizontal rule, i.e. hr('middle') or hr()
20             left => '+-',
21             border => '-',
22             sep => '-+-',
23             right => '-+',
24             },
25             dhr => { # double horizontal rule, i.e. hr('dhr') or dhr()
26             left => '+=',
27             border => '=',
28             sep => '=+=',
29             right => '=+',
30             },
31             bottom => { # bottom border, i.e. hr('bottom')
32             left => "'-",
33             border => '-',
34             sep => '-+-',
35             right => "-'",
36             },
37             row => { # row decoration
38             left => '| ',
39             sep => ' | ',
40             right => ' |',
41             },
42             };
43              
44             =head1 NAME
45              
46             Text::SpanningTable - ASCII tables with support for column spanning.
47              
48             =head1 SYNOPSIS
49              
50             use Text::SpanningTable;
51              
52             # create a table object with four columns of varying widths
53             my $t = Text::SpanningTable->new(10, 20, 15, 25);
54              
55             # enable automatic trailing newlines
56             $t->newlines(1);
57              
58             # print a top border
59             print $t->hr('top');
60              
61             # print a row (with header information)
62             print $t->row('Column 1', 'Column 2', 'Column 3', 'Column 4');
63              
64             # print a double horizontal rule
65             print $t->dhr; # also $t->hr('dhr');
66              
67             # print a row of data
68             print $t->row('one', 'two', 'three', 'four');
69              
70             # print a horizontal rule
71             print $t->hr;
72              
73             # print another row, with one column that spans all four columns
74             print $t->row([4, 'Creedence Clearwater Revival']);
75              
76             # print a horizontal rule
77             print $t->hr;
78              
79             # print a row with the first column as normal and another column
80             # spanning the remaining three
81             print $t->row(
82             'normal',
83             [3, 'this column spans three columns and also wraps to the next line.']
84             );
85              
86             # finally, print the bottom border
87             print $t->hr('bottom');
88              
89             # the output from all these commands is:
90             .----------+------------------+-------------+-----------------------.
91             | Column 1 | Column 2 | Column 3 | Column 4 |
92             +==========+==================+=============+=======================+
93             | one | two | three | four |
94             +----------+------------------+-------------+-----------------------+
95             | Creedence Clearwater Revival |
96             +----------+------------------+-------------+-----------------------+
97             | normal | this column spans three columns and also wraps to the |
98             | | next line. |
99             '----------+------------------+-------------+-----------------------'
100              
101             =head1 DESCRIPTION
102              
103             C provides a mechanism for creating simple ASCII tables,
104             with support for column spanning. It is meant to be used with monospace
105             fonts such as common in terminals, and thus is useful for logging purposes.
106              
107             This module is inspired by L and can generally produce
108             the same output (except that C doesn't support column
109             spanning), but with a few key differences:
110              
111             =over
112              
113             =item * In C, you build your table in the object and
114             C it when you're done. In C, you can print
115             your table (or do whatever you want with the output) as it is being built.
116             If you don't need to have your tables in "real-time", you can just save the
117             output in a variable, but for convenience and compatibility with
118             C, this module provides a C method (which is
119             actually an alias for the C method) that returns the table's
120             output.
121              
122             =item * C takes care of the top and bottom borders of
123             the table by itself. Due to C's "real-time" nature,
124             this functionality is not provided, and you have to take care of that yourself.
125              
126             =item * C allows you to pass titles for a header column
127             when creating the table object. This module doesn't have that functionality,
128             you have to create header rows (or footer rows) yourself and how you see
129             fit.
130              
131             =item * C provides a second type of horizontal rules
132             (called 'dhr' for 'double horizontal rule') that can be used for header
133             and footer rows (or whatever you see fit).
134              
135             =item * C provides an option to define a callback
136             function that can be automatically invoked on the module's output when
137             calling C, C or C.
138              
139             =item * In C, the widths you define for the columns
140             are the widths of the data they can accommodate, i.e. without the borders
141             and padding. In C, the widths you define are WITH
142             the borders and padding. If you are familiar with the CSS and the box model,
143             then columns in C have C set to C,
144             while in C they have C set to C.
145             So take into account that the width of the column's data will be four
146             characters less than defined.
147              
148             =back
149              
150             Like C, the columns of the table will always be exactly
151             the same width as defined, i.e. they will not stretch to accommodate the
152             data passed to the cells. If a cell's data is too big, it will be wrapped
153             (with possible word-breaking using the '-' character), thus resulting in
154             more lines of text.
155              
156             =head1 METHODS
157              
158             =head2 new( [@column_widths] )
159              
160             Creates a new instance of C with columns of the
161             provided widths. If you don't provide any column widths, the table will
162             have one column with a width of 100 characters.
163              
164             Note that currently, a column cannot be less than 6 characters in width.
165              
166             =cut
167              
168             sub new {
169 5     5 1 144034 my ($class, @cols) = @_;
170              
171 5         5 my $width; # total width of the table
172              
173             # default widths
174 5 100 100     28 @cols = (100) unless @cols and scalar @cols;
175              
176 5         11 foreach (@cols) {
177 14 50       27 die "Minimum column size is 6 characters"
178             if $_ < 6;
179 14         15 $width += $_;
180             }
181              
182 5         26 return bless {
183             cols => \@cols,
184             width => $width,
185             newlines => 0,
186             decorate => 1,
187             output => [],
188             }, $class;
189             }
190              
191             =head2 newlines( [$boolean] )
192              
193             By default, trailing newlines will NOT be added automatically to the output generated
194             by this module (for example, when printing a horizontal rule, a newline
195             character will not be appended). Pass a boolean value to this method to
196             enable/disable automatic newline creation. Returns the current value of
197             this attribute (after changing it if a boolean value had been passed).
198              
199             =cut
200              
201             sub newlines {
202 111 100   111 1 321 $_[0]->{newlines} = $_[1]
203             if defined $_[1];
204              
205 111         365 return $_[0]->{newlines};
206             }
207              
208             =head2 decoration( [$boolean] )
209              
210             By default, the table will be printed with border decoration. If you want a table
211             with no decoration at all, pass this a false value. Returns the current value of this
212             attribute (after changing it if a boolean value had been passed).
213              
214             Note that in undecorated tables, the C method will behave differently, as
215             documented under L'middle'E'bottom'E'dhr'] )">.
216              
217             =cut
218              
219             sub decoration {
220 2 50   2 1 9 $_[0]->{decorate} = $_[1]
221             if defined $_[1];
222              
223 2         3 $_[0]->{decorate};
224             }
225              
226             =head2 exec( \&sub, [@args] )
227              
228             Define a callback function to be invoked whenever calling C, C
229             or C. Pass this method an anonymous subroutine (C<\&sub> above)
230             or a reference to a subroutine, and a list of parameters/arguments you
231             wish this subroutine to have (C<@args> above). When called, the subroutine
232             will receive, as arguments, the generated output, and C<@args>.
233              
234             So, for example, you can do:
235              
236             $t->exec(sub { my ($output, $log) = @_; $log->info($output); }, $log);
237              
238             This would result in C<< $log->info($output) >> being invoken whenever
239             calling C, C or C, with C<$output> being the output
240             these methods generated. See more info at the C's method documentation
241             below.
242              
243             =cut
244              
245             sub exec {
246 3     3 1 635 my $self = shift;
247              
248 3         6 $self->{exec} = shift;
249 3 50       14 $self->{args} = \@_ if scalar @_;
250             }
251              
252             =head2 hr( ['top'|'middle'|'bottom'|'dhr'] )
253              
254             Generates a horizontal rule of a certain type. Unless a specific type is
255             provided, 'middle' we be used. 'top' generates a top border for the table,
256             'bottom' generates a bottom border, and 'dhr' is the same as 'middle', but
257             generates a 'double horizontal rule' that is more pronounced and thus can
258             be used for headers and footers.
259              
260             This method will always result in one line of text.
261              
262             If table decoration is off (see L), this method
263             will return an empty string, unless 'dhr' is passed, in which case a horizontal
264             rule made out of dashes will be returned.
265              
266             =cut
267              
268             sub hr {
269 16     16 1 511 my ($self, $type) = @_;
270              
271             # generate a simple horizontal rule by default
272 16   100     37 $type ||= 'middle';
273              
274 16         13 my $output = '';
275              
276 16 100       29 if ($self->{decorate}) {
    100          
277             # start with the left decoration
278 13         20 $output .= $C->{$type}->{left};
279              
280             # print a border for every column in the table, with separator
281             # decorations between them
282 13         12 for (my $i = 0; $i < scalar @{$self->{cols}}; $i++) {
  56         82  
283 43         35 my $width = $self->{cols}->[$i] - 4;
284 43         43 $output .= $C->{$type}->{border} x$width;
285              
286             # print a separator unless this is the last column
287 43 100       36 $output .= $C->{$type}->{sep} unless $i == (scalar @{$self->{cols}} - 1);
  43         77  
288             }
289              
290             # right decoration
291 13         14 $output .= $C->{$type}->{right};
292             } elsif ($type eq 'dhr') {
293 2         5 $output .= '-'x$self->{width};
294             } else {
295 1         5 return $output;
296             }
297              
298             # push this to the output buffer
299 15         12 push(@{$self->{output}}, $output);
  15         31  
300              
301             # are we adding newlines?
302 15 100       20 $output .= "\n" if $self->newlines;
303              
304             # if a callback function is defined, invoke it
305 15 100       25 if ($self->{exec}) {
306 10         13 my @args = ($output);
307 10 50       16 unshift(@args, @{$self->{args}}) if $self->{args};
  10         13  
308 10         19 $self->{exec}->(@args);
309             }
310              
311 15         52 return $output;
312             }
313              
314             =head2 dhr()
315              
316             Convenience method that simply calls C.
317              
318             =cut
319              
320             sub dhr {
321 3     3 1 10 shift->hr('dhr');
322             }
323              
324             =head2 row( @column_data )
325              
326             Generates a new row from an array holding the data for the row's columns.
327             At a maximum, the number of items in the C<@column_data> array will be
328             the number of columns defined when creating the object. At a minimum, it
329             will have one item. If the passed data doesn't fill the entire row, the
330             rest of the columns will be printed blank (so it is not structurally
331             incorrect to pass insufficient data).
332              
333             When a column doesn't span, simply push a scalar to the array. When it
334             does span, push an array-ref with two items, the first being the number
335             of columns to span, the second being the scalar data to print. Passing an
336             array-ref with 1 for the first item is the same as just passing the scalar
337             data (as the column will simply span itself).
338              
339             So, for example, if the table has nine columns, the following is a valid
340             value for C<@column_data>:
341              
342             ( 'one', [2, 'two and three'], 'four', [5, 'five through nine'] )
343              
344             The following is also valid:
345              
346             ( 'one', [5, 'two through six'] )
347              
348             Columns seven through nine in the above example will be blank, so it's the
349             same as passing:
350              
351             ( 'one', [5, 'two through six'], ' ', ' ', ' ' )
352              
353             If a column's data is longer than its width, the data will be wrapped
354             and broken, which will result in the row being constructed from more than one
355             lines of text. Thus, as opposed to the C method, this method has
356             two options for a return value: in list context, it will return all the
357             lines constructing the row (with or without newlines at the end of each
358             string as per what was defined with the C); in scalar
359             context, however, it will return the row as a string containing newline
360             characters that separate the lines of text (once again, a trailing newline
361             will be added to this string only if a true value was passed to C).
362              
363             If a callback function has been defined, it will not be invoked with the
364             complete output of this row (i.e. with all the lines of text that has
365             resulted), but instead will be called once per each line of text. This is
366             what makes the callback function so useful, as it helps you cope with
367             problems resulting from all the newline characters separating these lines.
368             When the callback function is called on each line of text, the line will
369             only contain the newline character at its end if C has been
370             set to true.
371              
372             =cut
373              
374             sub row {
375 17     17 1 340 my ($self, @data) = @_;
376              
377 17         12 my @rows; # will hold a matrix of the table
378              
379 17         23 my $done = 0; # how many columns have we generated yet?
380              
381             # go over all columns provided
382 17         35 for (my $i = 0; $i < scalar @data; $i++) {
383             # is this a spanning column? what is the width of it?
384 40         29 my $width = 0;
385              
386 40         29 my $text = ''; # will hold column's text
387              
388 40 100       58 if (ref $data[$i] eq 'ARRAY') {
389             # this is a spanning column
390 8 50       18 $text .= $data[$i]->[1] if defined $data[$i]->[1];
391              
392 8         17 foreach (0 .. $data[$i]->[0] - 1) {
393             # $data[$i]->[0] is the number of columns this column spans
394 21         28 $width += $self->{cols}->[$done + $_];
395             }
396              
397             # subtract the number of columns this column spans
398             # minus 1, because two adjacent columns share the
399             # same separating border
400             $width -= $data[$i]->[0] - 1
401 8 100       14 if $self->{decorate};
402              
403             # increase $done with the number of columns we have
404             # just parsed
405 8         8 $done += $data[$i]->[0];
406             } else {
407             # no spanning
408 32 50       56 $text .= $data[$i] if defined $data[$i];
409 32         30 $width = $self->{cols}->[$done];
410 32         23 $done++;
411             }
412              
413 40 100       48 if ($self->{decorate}) {
414             # make sure the column's data is at least 4 characters long
415             # (because we're subtracting four from every column to make
416             # room for the borders and separators)
417 24 100       43 $text .= ' 'x(4 - length($text))
418             if length($text) < 4;
419              
420             # subtract four from the width, for the column's decorations
421 24         18 $width -= 4;
422             } else {
423 16 50       24 $text = ' '
424             if length($text) == 0;
425 16         11 $width -= 1;
426             }
427              
428             # if the column's text is longer than the available width,
429             # we need to wrap it.
430 40         29 my $new_string = ''; # will hold parsed text
431 40 100       41 if (length($text) > $width) {
432 13   66     46 while (length($text) && length($text) > $width) {
433             # if the $width'th character of the string
434             # is a whitespace, just break it with a
435             # new line.
436              
437             # else if the $width'th - 1 character of the string
438             # is a whitespace, this is probably the start
439             # of a word, so add a whitespace and a newline.
440              
441             # else if the $width'th + 1 character is a whitespace,
442             # it is probably the end of a word, so just
443             # break it with a newline.
444              
445             # else we're in the middle of a word, so
446             # we need to break it with '-'.
447              
448              
449 46 100       143 if (substr($text, $width - 1, 1) =~ m/^\s$/) {
    100          
    100          
450 2         20 $new_string .= substr($text, 0, $width, '') . "\n";
451             } elsif (substr($text, $width - 2, 1) =~ m/^\s$/) {
452 8         26 $new_string .= substr($text, 0, $width - 1, '') . " \n";
453             } elsif (substr($text, $width, 1) =~ m/^\s$/) {
454 7         21 $new_string .= substr($text, 0, $width, '') . "\n";
455             } else {
456 29         95 $new_string .= substr($text, 0, $width - 1, '') . "-\n";
457             }
458             }
459 13 50       20 $new_string .= $text if length($text);
460             } else {
461 27         25 $new_string = $text;
462             }
463              
464             # if this row's data was split into more than one lines,
465             # we need to store these lines appropriately in our table's
466             # matrix (@rows).
467 40         62 my @fake_rows = split(/\n/, $new_string);
468 40         60 for (my $j = 0; $j < scalar @fake_rows; $j++) {
469 86 100       269 $rows[$j]->[$i] = ref $data[$i] eq 'ARRAY' ? [$data[$i]->[0], $fake_rows[$j]] : $fake_rows[$j];
470             }
471             }
472              
473             # suppose one column's data was wrapped into more than one lines
474             # of text. this means the matrix won't have data for all these
475             # lines in other columns that did not wrap (or wrapped less), so
476             # let's go over the matrix and fill missing cells with whitespace.
477 17         27 for (my $i = 1; $i < scalar @rows; $i++) {
478 37         34 for (my $j = 0; $j < scalar @{$self->{cols}}; $j++) {
  169         253  
479 132 100       162 next if $rows[$i]->[$j];
480              
481 86 100       123 if (ref $rows[$i - 1]->[$j] eq 'ARRAY') {
482 17         13 my $width = length($rows[$i - 1]->[$j]->[1]);
483 17         29 $rows[$i]->[$j] = [$rows[$i - 1]->[$j]->[0], ' 'x$width];
484             }
485             }
486             }
487              
488             # okay, now we go over the matrix and actually generate the
489             # decorated output
490 17         16 my @output;
491 17         27 for (my $i = 0; $i < scalar @rows; $i++) {
492 54 100       76 my $output = $self->{decorate} ? $C->{row}->{left} : '';
493              
494 54         36 my $push = 0; # how many columns have we generated already?
495              
496             # print the columns
497 54         46 for (my $j = 0; $j < scalar @{$rows[$i]}; $j++) {
  179         249  
498 125         72 my $width = 0;
499 125         87 my $text;
500              
501 125 100       139 if (ref $rows[$i]->[$j] eq 'ARRAY') {
502             # a spanning column, calculate width and
503             # get the text
504 31         25 $text = $rows[$i]->[$j]->[1];
505 31         42 foreach (0 .. $rows[$i]->[$j]->[0] - 1) {
506 79         80 $width += $self->{cols}->[$push + $_];
507             }
508 31         29 $width -= $rows[$i]->[$j]->[0] - 1;
509             } else {
510             # normal column
511 94         55 $text = $rows[$i]->[$j];
512 94         95 $width = $self->{cols}->[$push];
513             }
514              
515 125 100       134 $width -= $self->{decorate} ? 4 : 1;
516              
517             # is there any text for this column? if not just
518             # generate whitespace
519 125 100 66     507 $output .= $text && length($text) ? $text . ' 'x($width - length($text)) : ' 'x$width;
520              
521             # increase the number of columns we just processed
522 125 100       161 $push += ref $rows[$i]->[$j] eq 'ARRAY' ? $rows[$i]->[$j]->[0] : 1;
523              
524             # print a separator, unless this is the last column
525 125 100       61 if ($push != scalar @{$self->{cols}}) {
  125         212  
526 82 100       126 $output .= $self->{decorate} ? $C->{row}->{sep} : ' ';
527             }
528             }
529              
530             # have we processed all columns? (i.e. has the user provided
531             # data for all the columns?) if not, generate empty columns
532 54         58 my $left = scalar @{$self->{cols}} - $push;
  54         52  
533              
534 54 100       69 if ($left) {
535 11         20 for (my $k = 1; $k <= $left; $k++) {
536 15         13 my $width = $self->{cols}->[$push++];
537             $width -= 4
538 15 100       22 if $self->{decorate};
539 15         14 $output .= ' 'x$width;
540 15 100       33 if ($k != $left) {
541 4 50       9 $output .= $self->{decorate} ? $C->{row}->{sep} : ' ';
542             }
543             }
544             }
545              
546             $output .= $C->{row}->{right}
547 54 100       81 if $self->{decorate};
548              
549 54         101 push(@output, $output);
550             }
551              
552             # save output in the object
553 17         15 push(@{$self->{output}}, @output);
  17         25  
554              
555             # invoke callback function, if any
556 17 100       29 if ($self->{exec}) {
557 13         8 my @args;
558 13 50       22 push(@args, @{$self->{args}}) if $self->{args};
  13         13  
559 13         19 foreach (@output) {
560 37 50 33     40 $_ .= "\n" if $self->newlines && !m/\n$/;
561 37         39 push(@args, $_);
562 37         50 $self->{exec}->(@args);
563 37         117 pop @args;
564             }
565             }
566              
567             # is the user expecting an array?
568 17         20 foreach (@output) {
569 54 50 66     53 $_ .= "\n" if $self->newlines && !m/\n$/;
570             }
571 17 50       76 return wantarray ? @output : join("\n", @output);
572             }
573              
574             =head2 output()
575              
576             =head2 draw()
577              
578             Returns the entire output generated for the table up to the point of calling
579             this method. It should be stressed that this method does not "finalize"
580             the table by adding top and bottom borders or anything at all. Decoration
581             is done "real-time" and if you don't add top and bottom borders yourself
582             (with C and C, respectively), this method will
583             not do that for you. Returned output will or will not contain newlines as
584             per the value defined with C.
585              
586             Both the above methods do the same, C is provided as an alias for
587             compatibility with L.
588              
589             =cut
590              
591             sub output {
592 2     2 1 5 my $self = shift;
593              
594 2         2 my $output = join("\n", @{$self->{output}});
  2         6  
595 2 50       3 $output .= "\n" if $self->newlines;
596              
597 2         5 return $output;
598             }
599              
600             sub draw {
601 1     1 1 4 shift->output;
602             }
603              
604             =head1 AUTHOR
605              
606             Ido Perlmuter, C<< >>
607              
608             =head1 BUGS
609              
610             Please report any bugs or feature requests to C, or through
611             the web interface at L. I will be notified, and then you'll
612             automatically be notified of progress on your bug as I make changes.
613              
614             =head1 SUPPORT
615              
616             You can find documentation for this module with the perldoc command.
617              
618             perldoc Text::SpanningTable
619              
620             You can also look for information at:
621              
622             =over 4
623              
624             =item * RT: CPAN's request tracker
625              
626             L
627              
628             =item * AnnoCPAN: Annotated CPAN documentation
629              
630             L
631              
632             =item * CPAN Ratings
633              
634             L
635              
636             =item * Search CPAN
637              
638             L
639              
640             =back
641              
642             =head1 ACKNOWLEDGEMENTS
643              
644             Sebastian Riedel and Marcus Ramberg, authors of L, which
645             provided the inspiration of this module.
646              
647             =head1 LICENSE AND COPYRIGHT
648              
649             Copyright 2017 Ido Perlmuter
650              
651             Licensed under the Apache License, Version 2.0 (the "License");
652             you may not use this file except in compliance with the License.
653             You may obtain a copy of the License at
654              
655             http://www.apache.org/licenses/LICENSE-2.0
656              
657             Unless required by applicable law or agreed to in writing, software
658             distributed under the License is distributed on an "AS IS" BASIS,
659             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
660             See the License for the specific language governing permissions and
661             limitations under the License.
662              
663             =cut
664              
665             1;