Text::Tree - format a simple tree of strings into a textual tree graph


Text-Tree documentation Contained in the Text-Tree distribution.

Index


Code Index:

NAME

Top

Text::Tree - format a simple tree of strings into a textual tree graph

SYNOPSIS

Top

    use Text::Tree;

    my $tree = new Text::Tree( "root",
                               [ "left\nnode" ],
			       [ "right", [ "1" ], [ "2" ] ] );
    print $tree->layout("boxed");

    __OUTPUT__

        +----+
        |root|
        +----+
      .---^---.
    +----+ +-----+
    |left| |right|
    |node| +-----+
    +----+  .-^-.
           +-+ +-+
           |1| |2|
           +-+ +-+

METHODS

Top

new()

    my $tree = new Text::Tree( "label",
                               [ "left child label", [ ... ] ],
			       [ "right child label", [ ... ] );

Create a new tree object from a nested set of array references. The first element of each array must be a string used as a node label. The remaining elements must each be an array reference for a child of the node. Labels may contain newlines to support multiple lines of text.

layout()

    my @lines = $tree->layout( "centered in boxes" );
    print @lines;

Lays out the tree into an array of newline-terminated strings, ready for printing or displaying. The optional style argument may contain various keywords such as 'center', 'box', 'line', 'oval' and/or 'space'. These style keywords affect how the tree nodes are formatted.

DESCRIPTION

Top

Allows the caller to develop a tree structure, using nested arrays of strings and references. Once developed, the whole tree can be printed as a diagram, with the root of the tree at the top, and child nodes formatted horizontally below them.

The string labels are printed as-is, or optionally surrounded with a simple outlining style using printable ASCII characters.

This module may be used with object-oriented or simple function calls.

HISTORY

Top

Mark Jason Dominus (aka MJD) asked for this functionality on his Expert-level "Perl Quiz of the Week" Number 5. You can find out more about the QOTW discussion forum at http://perl.plover.com/qotw/

The central formatting routine was submitted by Ron Isaacson to the Quiz forum as one possible solution to the general problem.

Ed Halley adapted the Ron Isaacson entry (with permission), to correct some tree structures not originally handled, and to allow more formatting options for the box styles.

COPYRIGHT AND LICENSE

Top


Text-Tree documentation Contained in the Text-Tree distribution.
# Text::Tree - format a simple tree of strings into a textual tree graph

#----------------------------------------------------------------------------
#
# Copyright (C) 2003-2004 Ron Isaacson
# Portions Copyright (C) 2003 Mark Jason Dominus
# Portions Copyright (C) 2004 Ed Halley
#
#----------------------------------------------------------------------------

package Text::Tree;
use vars qw($VERSION);
$VERSION = 1.0;

#----------------------------------------------------------------------------

use strict;
use warnings;

sub NBSP() { "\x01" }

sub new
{
    my ($pack, $label, @subnodes) = @_;
    my @subobjects = map { $pack->new(@$_) } @subnodes;
    bless [ $label, @subobjects ] => $pack;
}

sub layout
{
    my $tree = shift;
    my $style = shift || undef;
    my @lines = layout_tree($tree, $style);
    return map { s/\Q@{[NBSP]}/ /g; s/\s+$//; "$_\n" } @lines;
}

#----------------------------------------------------------------------------

# Support routines.

# Return the length of longest line in all arguments.  Assumes arguments
# are chomped and contain a single line of text.

sub longest
{
    return (sort { $b <=> $a } map { length } @_)[0];
}

# Ensure all lines match given width (or longest line if 0).
sub pad
{
    my $want = shift;
    $want = longest(@_) if not $want;
    my @lines = @_;
    for (@lines)
    {
	$_ .= ' ' x ($want-length($_))
	    if $want > length($_);
    }
    return @lines if wantarray;
    return $lines[0];
}

# Center and pad to the given width (or longest line if 0).
sub center
{
    my $want = shift;
    $want = longest(@_) if not $want;
    my @lines = @_;
    for (@lines)
    {
	$_ = ' ' x (($want-length($_))/2) . $_
	    if $want > length($_);
	$_ = pad($want, $_);
    }
    return @lines if wantarray;
    return $lines[0];
}

# Add box-border characters according to an 8-char style string.
# The characters are the four corners and four edges of the border.
sub border
{
    my $style = shift;
    my @style = split //, $style;
    my @lines = pad(0, @_);

    my $want = longest(@lines);
    for (@lines)
    {
	$_ = $style[5] . $_ . $style[7];
    }
    unshift(@lines, $style[0] . $style[4]x$want . $style[1]);
    push(@lines, $style[2] . $style[6]x$want . $style[3]);
    return @lines;
}

# Turn the single string label (which may have newlines) into a properly
# centered and/or padded array. The style argument may contain keywords
# to specify different aspects of the formatting.  All spaces in the
# label are turned into special NBSP characters during layout processing.

sub text
{
    my $self = shift;
    my $label = $self->[0];
    my $style = shift || '';

    # pad with spaces to width 5
    my @lines = split /\n/, $label;

    if ($style =~ /center/)
        { @lines = center(0, @lines); }
    else
        { @lines = pad(0, @lines); }

    @lines = border("        ", @lines) if $style =~ /space/;
    @lines = border('++++-|-|', @lines) if $style =~ /line|box/;
    @lines = border("..`'-|-|", @lines) if $style =~ /oval|round/;

    s/ /@{[NBSP]}/g for @lines;
    return \@lines;
}

# Return list of children trees.
sub children
{
    my $self = shift;
    my @children = @$self;

    # throw away the label
    shift @children;
    return @children;
}

# Lay out one subtree into a space-padded rectangle.
sub layout_tree
{
    my $tree = shift;
    my $style = shift;
    my @text = @{text($tree, $style)};
    my @children = children($tree);

    # recurse depth-first, left-right through $tree, returning a
    # downward view of the tree at each stop

    # if we're at a leaf node, then just return it; this is where the
    # recursion stops
    return @text unless @children;

    # build a picture of this node's children
    my @out = ();
    my $shift_len = 0;
    foreach my $child (@children)
    {
	if (@out)
	{
	    # find the length of the longest line seen so far (in the
	    # picture of this node's children), and pad all the lines seen
	    # so far to that length
	    my $pad_len = longest(@out);
	    @out = map { pad($pad_len, $_) } @out;

	    # get the downward picture from this child, and tack each line
	    # of that picture on to the right of the current picture
	    my @child = layout_tree($child, $style);
	    for (0 .. $#child)
	    {
		$out[$_] = ' ' x $shift_len if not $out[$_];
		$out[$_] .= ' ' . $child[$_];
	    }
	}
	else
	{
	    # this is the first child seen
	    @out = layout_tree($child, $style);
	}

	$shift_len += longest(@out);
    }

    # now we have the picture of all of this node's children, so we need
    # to add the text of the node itself to the top

    # we're going to want to center this node above the picture of its
    # children, but there may be additional padding on the left side if
    # any of those children have children of their own; so for the
    # purposes of centering, find the space occupied only by this node's
    # immediate children, and center the text over that

    my $blank = ($out[0] =~ /^( *)/)[0];
    my $len0 = length $out[0];
    my $center = $len0 - length($blank);

    if (@children == 1)
    {
	# if this node has only one child, then just center a "|" above it
	unshift (@out, pad($len0, $blank . center($center, "|")));
    }
    else
    {
	# if this node has multiple children, then we're not so lucky...
	# we're going to take the first line of the existing output,
	# duplicate it, and transform all of the cell borders into
	# connection points

	# start by stripping off any whitespace to the left, and holding
	# it for later
	my ($pad, $lines) = ($out[0] =~ /^( *)(.*)$/);

	# replace each block of non-whitespace (ie, a cell border) with a
	# ".", centered in the space where the border was
	$lines =~ s/(\S+)/center(length($1), ".")/ge;

	# this is going to make some additional whitespace on the left, so
	# strip that off too (and add it to what we've saved). any
	# remaining spaces are part of the connection lines, so turn them
	# into "-"'s.
	$pad .= $1 if ($lines =~ s/^( *)//);
	$lines =~ s/ *$//;
	$lines =~ s/ /-/g;

	# now we have a line that connects all of the children; figure out
	# where to attach it to its parent
	my $text0 = $blank . center($center, $text[-1]);
	$text0 =~ s/(\S+)/center(length($1), "x")/e;
	my $pos = index($text0, "x") - length($pad);

	# attach it with a reasonable character ("+" if directly over a
	# child's connection point, "^" otherwise)
	substr($lines, $pos, 1) =
	    (substr($lines, $pos, 1) eq '.' ? '+' : '^');

	# add this mess to the output
	unshift @out, pad($len0, $pad . $lines);
    }

    # now add this cell itself, properly positioned, to the output, and
    # we're done

    unshift(@out, $blank . center($center, $_)) for reverse @text;
    return @out;
}

#----------------------------------------------------------------------------

sub _test
{
    my $s = new Text::Tree( "5",
			    [ "4 1", 
			      [ "3 1\n1", 
				[ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
			      ] );
    my $t = Text::Tree->new( "5",
			     [ "4 1", 
			       [ "3 1\n1", 
				 [ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
			       [ "2 2 1", [ "1" ], $s ],
			       ], 
			     [ "3 2\n1" ] );

    my $u = Text::Tree->new( "5",
			     [ "4 1", 
			       [ "2 2 1", [ "1" ] ],
			       [ "3 1\n1", 
				 [ "2 1\n1 1 1", [ "1 1 1 1 1" ], ], ],
			       ],
			     [ "3 2\n1" ] );
    my $v = Text::Tree->new( "0", $t, $u );
    print $v->layout();
    print $/;

    my $tree = new Text::Tree( "root node",
			       [ "left node\nfunny node\nnode" ],
			       [ "right node", [ "r 1" ], [ "r 2" ] ] );
    print $tree->layout("spaced and centered in ovals");
}

1;
__END__
#----------------------------------------------------------------------------