File Coverage

blib/lib/Text/Tree.pm
Criterion Covered Total %
statement 100 111 90.0
branch 25 32 78.1
condition 4 4 100.0
subroutine 12 13 92.3
pod 2 9 22.2
total 143 169 84.6


line stmt bran cond sub pod time code
1             # Text::Tree - format a simple tree of strings into a textual tree graph
2              
3             #----------------------------------------------------------------------------
4             #
5             # Copyright (C) 2003-2004 Ron Isaacson
6             # Portions Copyright (C) 2003 Mark Jason Dominus
7             # Portions Copyright (C) 2004 Ed Halley
8             #
9             #----------------------------------------------------------------------------
10              
11             package Text::Tree;
12 1     1   53020 use vars qw($VERSION);
  1         3  
  1         95  
13             $VERSION = 1.0;
14              
15             =head1 NAME
16              
17             Text::Tree - format a simple tree of strings into a textual tree graph
18              
19             =head1 SYNOPSIS
20              
21             use Text::Tree;
22              
23             my $tree = new Text::Tree( "root",
24             [ "left\nnode" ],
25             [ "right", [ "1" ], [ "2" ] ] );
26             print $tree->layout("boxed");
27              
28             __OUTPUT__
29              
30             +----+
31             |root|
32             +----+
33             .---^---.
34             +----+ +-----+
35             |left| |right|
36             |node| +-----+
37             +----+ .-^-.
38             +-+ +-+
39             |1| |2|
40             +-+ +-+
41              
42             =cut
43              
44             #----------------------------------------------------------------------------
45              
46 1     1   6 use strict;
  1         1  
  1         2004  
47 1     1   11 use warnings;
  1         8  
  1         2102  
48              
49             sub NBSP() { "\x01" }
50              
51             =head1 METHODS
52              
53             =head2 new()
54              
55             my $tree = new Text::Tree( "label",
56             [ "left child label", [ ... ] ],
57             [ "right child label", [ ... ] );
58              
59             Create a new tree object from a nested set of array references. The
60             first element of each array must be a string used as a node label. The
61             remaining elements must each be an array reference for a child of the
62             node. Labels may contain newlines to support multiple lines of text.
63              
64             =cut
65              
66             sub new
67             {
68 5     5 1 27 my ($pack, $label, @subnodes) = @_;
69 5         9 my @subobjects = map { $pack->new(@$_) } @subnodes;
  4         15  
70 5         20 bless [ $label, @subobjects ] => $pack;
71             }
72              
73             =head2 layout()
74              
75             my @lines = $tree->layout( "centered in boxes" );
76             print @lines;
77              
78             Lays out the tree into an array of newline-terminated strings, ready for
79             printing or displaying. The optional style argument may contain various
80             keywords such as 'center', 'box', 'line', 'oval' and/or 'space'. These
81             style keywords affect how the tree nodes are formatted.
82              
83             =cut
84              
85             sub layout
86             {
87 2     2 1 2375 my $tree = shift;
88 2   100     11 my $style = shift || undef;
89 2         9 my @lines = layout_tree($tree, $style);
90 2         5 return map { s/\Q@{[NBSP]}/ /g; s/\s+$//; "$_\n" } @lines;
  16         18  
  16         64  
  16         59  
  16         48  
91             }
92              
93             #----------------------------------------------------------------------------
94              
95             # Support routines.
96              
97             # Return the length of longest line in all arguments. Assumes arguments
98             # are chomped and contain a single line of text.
99              
100             sub longest
101             {
102 32     32 0 50 return (sort { $b <=> $a } map { length } @_)[0];
  37         68  
  58         118  
103             }
104              
105             # Ensure all lines match given width (or longest line if 0).
106             sub pad
107             {
108 53     53 0 58 my $want = shift;
109 53 100       120 $want = longest(@_) if not $want;
110 53         95 my @lines = @_;
111 53         71 for (@lines)
112             {
113 56 100       166 $_ .= ' ' x ($want-length($_))
114             if $want > length($_);
115             }
116 53 100       175 return @lines if wantarray;
117 24         68 return $lines[0];
118             }
119              
120             # Center and pad to the given width (or longest line if 0).
121             sub center
122             {
123 24     24 0 27 my $want = shift;
124 24 50       46 $want = longest(@_) if not $want;
125 24         64 my @lines = @_;
126 24         39 for (@lines)
127             {
128 24 100       75 $_ = ' ' x (($want-length($_))/2) . $_
129             if $want > length($_);
130 24         46 $_ = pad($want, $_);
131             }
132 24 50       56 return @lines if wantarray;
133 24         78 return $lines[0];
134             }
135              
136             # Add box-border characters according to an 8-char style string.
137             # The characters are the four corners and four edges of the border.
138             sub border
139             {
140 5     5 0 6 my $style = shift;
141 5         21 my @style = split //, $style;
142 5         11 my @lines = pad(0, @_);
143              
144 5         11 my $want = longest(@lines);
145 5         7 for (@lines)
146             {
147 6         15 $_ = $style[5] . $_ . $style[7];
148             }
149 5         15 unshift(@lines, $style[0] . $style[4]x$want . $style[1]);
150 5         10 push(@lines, $style[2] . $style[6]x$want . $style[3]);
151 5         50 return @lines;
152             }
153              
154             # Turn the single string label (which may have newlines) into a properly
155             # centered and/or padded array. The style argument may contain keywords
156             # to specify different aspects of the formatting. All spaces in the
157             # label are turned into special NBSP characters during layout processing.
158              
159             sub text
160             {
161 10     10 0 13 my $self = shift;
162 10         21 my $label = $self->[0];
163 10   100     35 my $style = shift || '';
164              
165             # pad with spaces to width 5
166 10         28 my @lines = split /\n/, $label;
167              
168 10 50       22 if ($style =~ /center/)
169 0         0 { @lines = center(0, @lines); }
170             else
171 10         25 { @lines = pad(0, @lines); }
172              
173 10 50       27 @lines = border(" ", @lines) if $style =~ /space/;
174 10 100       44 @lines = border('++++-|-|', @lines) if $style =~ /line|box/;
175 10 50       97 @lines = border("..`'-|-|", @lines) if $style =~ /oval|round/;
176              
177 10         38 s/ /@{[NBSP]}/g for @lines;
  0         0  
178 10         36 return \@lines;
179             }
180              
181             # Return list of children trees.
182             sub children
183             {
184 10     10 0 13 my $self = shift;
185 10         19 my @children = @$self;
186              
187             # throw away the label
188 10         12 shift @children;
189 10         22 return @children;
190             }
191              
192             # Lay out one subtree into a space-padded rectangle.
193             sub layout_tree
194             {
195 10     10 0 13 my $tree = shift;
196 10         11 my $style = shift;
197 10         13 my @text = @{text($tree, $style)};
  10         19  
198 10         24 my @children = children($tree);
199              
200             # recurse depth-first, left-right through $tree, returning a
201             # downward view of the tree at each stop
202              
203             # if we're at a leaf node, then just return it; this is where the
204             # recursion stops
205 10 100       37 return @text unless @children;
206              
207             # build a picture of this node's children
208 4         8 my @out = ();
209 4         5 my $shift_len = 0;
210 4         6 foreach my $child (@children)
211             {
212 8 100       14 if (@out)
213             {
214             # find the length of the longest line seen so far (in the
215             # picture of this node's children), and pad all the lines seen
216             # so far to that length
217 4         10 my $pad_len = longest(@out);
218 4         8 @out = map { pad($pad_len, $_) } @out;
  10         15  
219              
220             # get the downward picture from this child, and tack each line
221             # of that picture on to the right of the current picture
222 4         11 my @child = layout_tree($child, $style);
223 4         13 for (0 .. $#child)
224             {
225 14 100       34 $out[$_] = ' ' x $shift_len if not $out[$_];
226 14         34 $out[$_] .= ' ' . $child[$_];
227             }
228             }
229             else
230             {
231             # this is the first child seen
232 4         44 @out = layout_tree($child, $style);
233             }
234              
235 8         17 $shift_len += longest(@out);
236             }
237              
238             # now we have the picture of all of this node's children, so we need
239             # to add the text of the node itself to the top
240              
241             # we're going to want to center this node above the picture of its
242             # children, but there may be additional padding on the left side if
243             # any of those children have children of their own; so for the
244             # purposes of centering, find the space occupied only by this node's
245             # immediate children, and center the text over that
246              
247 4         23 my $blank = ($out[0] =~ /^( *)/)[0];
248 4         6 my $len0 = length $out[0];
249 4         8 my $center = $len0 - length($blank);
250              
251 4 50       9 if (@children == 1)
252             {
253             # if this node has only one child, then just center a "|" above it
254 0         0 unshift (@out, pad($len0, $blank . center($center, "|")));
255             }
256             else
257             {
258             # if this node has multiple children, then we're not so lucky...
259             # we're going to take the first line of the existing output,
260             # duplicate it, and transform all of the cell borders into
261             # connection points
262              
263             # start by stripping off any whitespace to the left, and holding
264             # it for later
265 4         21 my ($pad, $lines) = ($out[0] =~ /^( *)(.*)$/);
266              
267             # replace each block of non-whitespace (ie, a cell border) with a
268             # ".", centered in the space where the border was
269 4         20 $lines =~ s/(\S+)/center(length($1), ".")/ge;
  8         26  
270              
271             # this is going to make some additional whitespace on the left, so
272             # strip that off too (and add it to what we've saved). any
273             # remaining spaces are part of the connection lines, so turn them
274             # into "-"'s.
275 4 50       27 $pad .= $1 if ($lines =~ s/^( *)//);
276 4         23 $lines =~ s/ *$//;
277 4         15 $lines =~ s/ /-/g;
278              
279             # now we have a line that connects all of the children; figure out
280             # where to attach it to its parent
281 4         9 my $text0 = $blank . center($center, $text[-1]);
282 4         16 $text0 =~ s/(\S+)/center(length($1), "x")/e;
  4         14  
283 4         13 my $pos = index($text0, "x") - length($pad);
284              
285             # attach it with a reasonable character ("+" if directly over a
286             # child's connection point, "^" otherwise)
287 4 100       16 substr($lines, $pos, 1) =
288             (substr($lines, $pos, 1) eq '.' ? '+' : '^');
289              
290             # add this mess to the output
291 4         11 unshift @out, pad($len0, $pad . $lines);
292             }
293              
294             # now add this cell itself, properly positioned, to the output, and
295             # we're done
296              
297 4         13 unshift(@out, $blank . center($center, $_)) for reverse @text;
298 4         24 return @out;
299             }
300              
301             #----------------------------------------------------------------------------
302              
303             sub _test
304             {
305 0     0     my $s = new Text::Tree( "5",
306             [ "4 1",
307             [ "3 1\n1",
308             [ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
309             ] );
310 0           my $t = Text::Tree->new( "5",
311             [ "4 1",
312             [ "3 1\n1",
313             [ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
314             [ "2 2 1", [ "1" ], $s ],
315             ],
316             [ "3 2\n1" ] );
317              
318 0           my $u = Text::Tree->new( "5",
319             [ "4 1",
320             [ "2 2 1", [ "1" ] ],
321             [ "3 1\n1",
322             [ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
323             ],
324             [ "3 2\n1" ] );
325 0           my $v = Text::Tree->new( "0", $t, $u );
326 0           print $v->layout();
327 0           print $/;
328              
329 0           my $tree = new Text::Tree( "root node",
330             [ "left node\nfunny node\nnode" ],
331             [ "right node", [ "r 1" ], [ "r 2" ] ] );
332 0           print $tree->layout("spaced and centered in ovals");
333             }
334              
335             1;
336             __END__