File Coverage

blib/lib/Data/Formatter/Text.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Data::Formatter::Text;
2 1     1   39687 use strict;
  1         2  
  1         131  
3 1     1   6 use warnings;
  1         1  
  1         33  
4              
5 1     1   5 use List::Util qw(max);
  1         6  
  1         261  
6 1     1   785 use Roman;
  0            
  0            
7              
8             our $VERSION = 1.0;
9             use base qw(Data::Formatter);
10              
11             ######################################
12             # Constants #
13             ######################################
14             our $HEADING_WIDTH = 50; # 50 chars is just an arbitrary default
15             our @BULLETS = ('*', '-', '~');
16             our $COLUMN_SEPARATOR = '|';
17             our $ROW_SEPARATOR = '-';
18              
19             ######################################
20             # Overriden Public Methods #
21             ######################################
22             sub heading
23             {
24             my ($self, $text) = @_;
25            
26             # Headers are centered, all caps, and enclosed in a wide thick box
27             return _box('#', '=', _centreAlign(uc($text), $HEADING_WIDTH));
28             }
29              
30             sub emphasized
31             {
32             my ($self, $text) = @_;
33            
34             # Emphasized text is just all caps enclosed in a narrow thin box
35             return _box(' !! ', '~', uc($text));
36             }
37              
38             ######################################
39             # Overriden Protected Methods #
40             ######################################
41             sub _write
42             {
43             my ($self, $text) = @_;
44             my $handle = $self->{__OUTPUT_HANDLE} or return;
45            
46             print $handle ($text);
47             }
48              
49             sub _text
50             {
51             my ($self, $text) = @_;
52             return split(/\n/, $text);
53             }
54              
55             sub _table
56             {
57             my ($self, $rows, %options) = @_;
58            
59             # Determine the dimensions of the table
60             my @rowHeights;
61             my @colWidths;
62             foreach my $row (@{$rows})
63             {
64             my $rowHeight = -1;
65             foreach my $colNum (0 .. $#{$row})
66             {
67             my @cellContents = $self->_formatCell($row->[$colNum]);
68             if (@cellContents > $rowHeight)
69             {
70             $rowHeight = @cellContents;
71             }
72            
73             # Get the width of the cell in characters
74             my $cellWidth = max(map {length} @cellContents) || 0;
75             if (!defined $colWidths[$colNum] || $cellWidth > $colWidths[$colNum])
76             {
77             $colWidths[$colNum] = $cellWidth;
78             }
79             }
80             push(@rowHeights, $rowHeight);
81             }
82            
83             # Generate a row separation line
84             my $rowSepLine = join($COLUMN_SEPARATOR, map { $ROW_SEPARATOR x $_ } @colWidths);
85            
86             # Output the table
87             my @buffer;
88             foreach my $rowIdx (0 .. $#{$rows})
89             {
90             my $row = $rows->[$rowIdx];
91            
92             # Get an array of all the cells in this row
93             my @columns = map { [$self->_formatCell($_)] } @{$row};
94            
95             # Create an array of lines that constitute this row
96             my @rowBuffer;
97             foreach my $lineIdx (0 .. $rowHeights[$rowIdx] - 1)
98             {
99             my @parallelLines;
100             foreach my $colNum (0 .. $#{$row})
101             {
102             my @cell = @{$columns[$colNum]};
103            
104             if (defined $cell[$lineIdx])
105             {
106             push(@parallelLines, _leftAlign($cell[$lineIdx], $colWidths[$colNum]));
107             }
108             else
109             {
110             push(@parallelLines, _leftAlign('', $colWidths[$colNum]));
111             }
112             }
113             push(@rowBuffer, join($COLUMN_SEPARATOR, @parallelLines));
114             }
115             push(@buffer, @rowBuffer);
116            
117             if ($rowIdx != $#{$rows})
118             {
119             push(@buffer, $rowSepLine);
120             }
121             }
122            
123             # Draw a border around the table
124             @buffer = _box($COLUMN_SEPARATOR, $ROW_SEPARATOR, @buffer);
125            
126             return @buffer;
127             }
128              
129             sub _list
130             {
131             my ($self, $list, %options) = @_;
132             my $listType = $options{listType} || 'UNORDERED';
133             my $bulletTypeIdx = $options{bulletType} || 0;
134             my $numTypeIdx = $options{numberType} || 0;
135              
136             # Determine the type of bullet or number we will use
137             my $point;
138             if ($listType eq 'ORDERED')
139             {
140             $options{numberType} = $numTypeIdx + 1;
141             }
142             else
143             {
144             $point = @BULLETS[$bulletTypeIdx % @BULLETS];
145             $options{bulletType} = $bulletTypeIdx + 1;
146             }
147              
148             my @buffer = ();
149             foreach my $elementIdx (0 .. $#{$list})
150             {
151             my $element = $list->[$elementIdx];
152              
153             # Alternate between latin and roman numbering for ordered lists
154             if ($listType eq 'ORDERED')
155             {
156             $point = $numTypeIdx % 2
157             ? roman($elementIdx + 1) . '.'
158             : ($elementIdx + 1) . '.';
159             }
160            
161             my $prefix = "$point ";
162             my @elementLines = $self->_format($element, %options);
163              
164             # Nested lists are not printed "inside" a list element, or
165             # it would look weird. Hence, a nested list is not prefixed
166             # by a bullet or a number.
167             if ($self->_getStructType($element) =~ /\w+_LIST/)
168             {
169             push(@buffer, map { ' ' x length($prefix) . $_ } @elementLines);
170             }
171             else
172             {
173             push(@buffer, "$prefix$elementLines[0]",
174             map { ' ' x length($prefix) . $_ } @elementLines[1 .. $#elementLines]);
175             }
176             }
177             return @buffer;
178             }
179              
180              
181             sub _unorderedList
182             {
183             my ($self, $list, %options) = @_;
184            
185             return $self->_list($list, %options, listType => 'UNORDERED');
186             }
187              
188             sub _orderedList
189             {
190             my ($self, $list, %options) = @_;
191            
192             return $self->_list($list, %options, listType => 'ORDERED');
193             }
194              
195             sub _definitionList
196             {
197             my ($self, $pairs) = @_;
198             my @buffer = ();
199            
200             # Output the pairs in alphabetical order with respect to the key
201             my @keys = sort (keys %{$pairs});
202            
203             # Determine the max length of a key to perform some nice indenting.
204             my $maxKeyLength = max(map {length} @keys);
205            
206             foreach my $key (@keys)
207             {
208             my $value = $pairs->{$key};
209             my @valueLines = $self->_format($value);
210            
211             my $structType = $self->_getStructType($value);
212             # Tables go below and are indented a constant 4 spaces
213             if ($structType eq 'TABLE')
214             {
215             push(@buffer,
216             "$key:",
217             map {" $_"} @valueLines);
218             }
219             # The first line of text goes on the same line as the definition and subsequent
220             # lines are indented by the maximum key length
221             elsif ($structType eq 'TEXT')
222             {
223             push(@buffer,
224             "$key:" . ' ' x ($maxKeyLength - length($key) + 1) . $valueLines[0],
225             map {' ' x ($maxKeyLength + 2) . " $_"} @valueLines[1..$#valueLines] );
226             }
227             # Everything else but text and tables goes on the following line and is indented
228             # to line up with the end of the key
229             else
230             {
231             push(@buffer,
232             "$key:",
233             map { ' ' x ($maxKeyLength + 2) . " $_" } @valueLines);
234             }
235             }
236              
237             return @buffer;
238             }
239              
240             ######################################
241             # Private Methods #
242             ######################################
243             sub _formatCell
244             {
245             my ($self, $cell) = @_;
246            
247             if (ref($cell) && ref($cell) =~ /SCALAR/)
248             {
249             return (uc ${$cell});
250             }
251            
252             return $self->_format($cell);
253             }
254              
255             sub _leftAlign
256             {
257             my ($text, $width) = @_;
258             if ($width <= length $text)
259             {
260             return $text;
261             }
262              
263             return $text . (' ' x ($width - length($text)));
264             }
265              
266             sub _centreAlign
267             {
268             my ($text, $width) = @_;
269            
270             my $lengthDiff = $width - length($text);
271             if ($lengthDiff <= 0)
272             {
273             return $text;
274             }
275              
276             my $leftMargin = ' ' x ($lengthDiff / 2);
277             my $rightMargin = ' ' x ($lengthDiff - int($lengthDiff / 2));
278             return $leftMargin . $text . $rightMargin;
279             }
280              
281             sub _underline
282             {
283             my ($text) = @_;
284            
285             return ($text, '-' x length($text));
286             }
287              
288             sub _rightAlign
289             {
290             my ($text, $width) = @_;
291             if ($width <= length $text)
292             {
293             return $text;
294             }
295              
296             return (' ' x ($width - length($text))) . $text;
297             }
298              
299             sub _box
300             {
301             my ($vertChar, $horizChar, @lines) = @_;
302            
303             # Determine the width of the whole block of text (the length of its longest line)
304             my $width = max(map {length} @lines);
305            
306             # Insert the left and right side lines and, if necesary, append spaces to
307             # any lines that aren't as long as the longest line
308             @lines = map {$vertChar . _leftAlign($_, $width) . $vertChar} @lines;
309            
310             # Add two to the width to account for the side lines
311             $width += 2 * length($vertChar);
312            
313             # Insert the top border line
314             unshift(@lines, $horizChar x $width);
315            
316             # Insert the bottom border line
317             push(@lines, $horizChar x $width);
318            
319             return @lines;
320             }
321              
322             1;
323              
324             =head1 NAME
325              
326             Data::Formatter::Text - Perl extension for formatting data stored in scalars, hashes, and arrays into strings, definition lists, and bulletted lists, etc. using plain ASCII text.
327              
328             =head1 SYNOPSIS
329              
330             use Data::Formatter::Text;
331              
332             # The only argument to the constructor is a file handle.
333             # If no file handle is specified, output is sent to STDOUT
334             my $text = new Data::Formatter::Text(\*STDOUT);
335              
336             $text->out('The following foods are tasty:',
337             ['Pizza', 'Pumpkin pie', 'Sweet-n-sour Pork']);
338              
339             # Outputs,
340             #
341             # The following foods are tasty:
342             # * Pizza
343             # * Pumpkin pie
344             # * Sweet-n-sour Pork
345             #
346              
347             $text->out('Do these things to eat an orange:',
348             \['Peal it', 'Split it', 'Eat it']);
349              
350             # Outputs,
351             #
352             # Do these things to eat an orange:
353             # 1. Peal it
354             # 2. Split it
355             # 3. Eat it
356             #
357              
358             # If you don't need to output to a file, you can also use the format() class method
359             # instead of the out() instance method.
360             my $nums = Data::Formatter::Text->format(
361             'Phone numbers' =>
362             {
363             Pat => '123-4567',
364             Joe => '999-9999',
365             Xenu => '000-0000',
366             });
367            
368             # Stores in $nums:
369             #
370             # Phone numbers
371             # Joe: 999-9999
372             # Pat: 123-4567
373             # Xenu: 000-0000
374             #
375              
376             =head1 DESCRIPTION
377              
378             A module that converts Perl data structures into formatted text,
379             much like Data::Dumper, except for that it formats the data nicely
380             for presentation to a human. For instance, refs to arrays are
381             converted into bulleted lists, refs to arrays that contain only refs
382             to arrays are converted into tables, and refs to hashes are
383             converted to definition lists.
384              
385             All in all, data structures are mapped to display elements as follows:
386              
387             SCALAR => Text,
388             REF to an ARRAY of ARRAYs => Table
389             REF to an ARRAY => Unordered (bulleted) list
390             REF to a REF to an ARRAY => Ordered (numbered) list
391             REF to a HASH => Definition list
392              
393             Elements can be nested, so, for instance, you may have an array that
394             contains one or more references to arrays, and it will be translated
395             into a nested bulletted list.
396              
397             =head2 Methods
398              
399             =over 4
400              
401             =item I->new()
402              
403             Returns a newly created C object.
404              
405             =item I->format(I)
406              
407             Returns the string representation of the arguments, formatted nicely.
408              
409             =item I<$OBJ>->out(I)
410              
411             Outputs the string representation of the arguments to the file stream specified in the constructor.
412              
413             =item I<$OBJ>->heading(I)
414              
415             Returns a new data-structure containing the same data as I, but which will be displayed as a heading if passed to out().
416             Headings are center aligned, made all uppercase, and surrounded by a thick border.
417              
418             For example,
419              
420             $text->out($text->heading("Test Results"), "All is well!");
421            
422             =item I<$OBJ>->emphasized(I)
423              
424             Returns a new data-structure containing the same data as I, but which will be displayed as emphasized text if passed to out().
425             Emphasized text is made all uppercase and surrounded by a thin border.
426              
427             For example,
428            
429             $text->out($text->emphasized("Cannot find file!"));
430              
431             =back
432              
433             =head2 Configuration
434              
435             =over 4
436              
437             =item I<$PACKAGE>::HEADING_WIDTH
438              
439             The minimum width of a heading, as created by the I method, excluding its surrounding box and measured in characters. By default, this is 50.
440              
441             =item I<@PACKAGE>::BULLETS
442              
443             An array of all the styles of bullet used for bulleted lists, in the order they are used as you move deeper into a nested list. This array must contain
444             at least one element, and by default is equal to ('*', '-', '~').
445              
446             =item I<$PACKAGE>::COLUMN_SEPARATOR
447              
448             The string used to separate columns in a table and draw the vertical portions of its border.
449              
450             =item I<$PACKAGE>::ROW_SEPARATOR
451              
452             The string used to separate rows in a table and draw the horizontal portions of its border.
453              
454             =back
455              
456             =head2 Example
457              
458             $formatter->out('Recipes',
459             {
460             "Peanutbutter and Jam Sandwich" =>
461             [
462             ['Ingredient', 'Amount', 'Preparation'],
463             ['Bread', '2 slices', ''],
464             ['Jam', 'Enough to cover inner face of slice 1', ''],
465             ['Peanutbutter', 'Enough to cover inner face of slice 2', '']
466             ]
467             }
468             );
469              
470             The code above will output the text:
471              
472             Recipes
473             Peanutbutter and Jam Sandwich:
474             ----------------------------------------------------------------
475             |Ingredient |Amount |Preparation|
476             |------------|-------------------------------------|-----------|
477             |Bread |2 slices | |
478             |------------|-------------------------------------|-----------|
479             |Jam |Enough to cover inner face of slice 1| |
480             |------------|-------------------------------------|-----------|
481             |Peanutbutter|Enough to cover inner face of slice 2| |
482             ----------------------------------------------------------------
483              
484             Note that the order of elements in a hash is not necessarily the same as the order the elements are printed in; instead, hash elements are sorted alphabetically by their keys before being output.
485              
486             =head1 AUTHOR
487              
488             Zachary Blair, Ezblair@cpan.orgE
489              
490             =head1 COPYRIGHT AND LICENSE
491              
492             Copyright (C) 2008 by Zachary Blair
493              
494             This library is free software; you can redistribute it and/or modify
495             it under the same terms as Perl itself, either Perl version 5.8.8 or,
496             at your option, any later version of Perl 5 you may have available.
497              
498              
499             =cut