/usr/local/CPAN/Quilt/Quilt/Writer/Ascii.pm


#
# Copyright (C) 1997 Ken MacLeod
# See the file COPYING for distribution terms.
#
# $Id: Ascii.pm,v 1.3 1997/10/25 21:47:32 ken Exp $
#

package Quilt::Writer::Ascii;
@Quilt::Writer::Ascii::ISA = qw{Quilt::Context};

use strict;
use vars qw{$entity_maps};

use Text::EntityMap;
use Quilt::Context;

my $entity_maps = undef;

sub new {
    my ($type, %init) = @_;

    if (!defined $init{file_handle}) {
	if (!defined %FileHandle::) {
	    require FileHandle;
	    import FileHandle;
	}

	# default to stdout
	$init{file_handle} = FileHandle->new ('>-');
    }

    # XXX this probably shouldn't be here
    # note the conversion of `sdata_dirs' list to an anonymous array to
    # make a single argument
    if (!defined $entity_maps) {
	$entity_maps = load_char_maps ('.2ab', [ Text::EntityMap::sdata_dirs() ]);
    }

    my ($self) = {
	current => [{}],
	file_handle => $init{file_handle},
	entity_map => $entity_maps,
    };

    bless ($self, $type);

    $self->push ({
	inline => 0,
	line_width => 72,
    });

    return ($self);
}

# XXX move me or get rid of me
# `load_char_maps' takes a `EntityMap' format suffix and loads all of
# the character entity replacement sets for that suffix into a
# EntityMapGroup.  `load_char_maps' searches every directory in
# `@{$path}'.

sub load_char_maps {
    my ($format, $paths) = @_;

    my (@char_maps) = ();
    my ($path, $file_name, $char_map);

    foreach $path (@{$paths}) {
	if (-d $path) {
	    opendir (SDATADIR, $path)
		or die "load_char_map: opening directory \`$path' for reading: $!\n";
	    foreach $file_name (readdir (SDATADIR)) {
		next if ($file_name !~ /$format$/);
		eval {$char_map = Text::EntityMap->load ("$path/$file_name")}
  		    or die "load_char_map: loading \`$path/$file_name'\n$@\n";
		push (@char_maps, $char_map);
	    }
	    closedir (SDATADIR);
	}
    }

    warn "load_char_maps: no entity maps found\n"
	if ($#char_maps == -1);

    return (Text::EntityMap->group (@char_maps));
}

sub push_display {
    my ($self, $obj) = @_;

    $self->collect_data;
    my ($key);
    foreach $key (qw{start_indent end_indent}) {
	# this object may not have an indent
	my $value;
	eval { $value = $obj->$key() };
	last if !defined $value;
	if (defined ($value)) {
	    if ($value =~ /^[+-]/) {
		$obj->$key (eval "\$self->\$key() $value");
	    } else {
		$obj->$key ($value);
	    }
	}
    }
    $self->SUPER::push ($obj);
    $self->push_break (new Quilt::Flow::DisplaySpace (space => $self->space_before));
}

sub pop_display {
    my ($self) = @_;

    $self->collect_data;
    $self->push_break (new Quilt::Flow::DisplaySpace (space => $self->space_after));
    $self->SUPER::pop;
}

sub push_inline {
    my ($self, $obj) = @_;

    $self->collect_break;
    $self->SUPER::push ($obj);
}

sub pop_inline {
    my ($self) = @_;

    $self->collect_break;
    $self->SUPER::pop;
}

sub push_break {
    my ($self, $obj) = @_;

    $self->collect_data;
    my $space = $obj->space;
    if (!defined $space) {
	$obj->space (0);
	$space = 0;
    }
    my $priority = $obj->priority;
    if (!defined $priority) {
	$obj->priority (0);
	$priority = 0;
    }
    # XXX doesn't fully implement DSSSL display space semantics (12.5.4.1)
    if (!defined $self->{break}) {
	$self->{break} = $space;
	$self->{break_priority} = $priority;
    } else {
	if ($priority > $self->{break_priority}) {
	    $self->{break_priority} = $priority;
	    $self->{break} = $space;
	} elsif	($priority == $self->{break_priority}
		 && $space > $self->{break}) {
	    $self->{break} = $space;
	}
    }
}

sub collect_break {
    my $self = shift;

    if (defined $self->{break}) {
	if (ref ($self->{file_handle})) {
	    $self->{file_handle}->print ("\n" x $self->{break});
	} else {
	    $self->{file_handle} .= "\n" x $self->{break};
	}
	undef $self->{break};
    }
}

sub push_data {
    my ($self, $data) = @_;

    $self->collect_break;
    push (@{$self->{data}}, $data);
}

# XXX `mark' hack, I believe the way this is intended to be handled in
# DSSSL is to use side-by-side display objects.
sub push_mark {
    my ($self, $mark) = @_;

    $self->{mark} .= $mark;
}

sub collect_data {
    my ($self) = @_;

    if (defined $self->{data}) {
	my ($str);
	$str = join ("", @{$self->{data}});
	if ($self->lines eq "asis") {
	    my ($indent) = " " x $self->start_indent;
	    $str =~ s/^/$indent/mg;
	} else {
	    $str = $self->fmt ($str,
			       $self->start_indent,
			       $self->end_indent,
			       $self->line_width,
			       $self->quadding);
	}
	if (defined $self->{mark}) {
	    my ($mark_length) = length ($self->{mark});
	    substr ($str, $self->start_indent - $mark_length, $mark_length) = $self->{mark};
	    undef $self->{mark};
	}
	$str .= "\n"
	    if (substr ($str, -1) ne "\n");
	if (ref ($self->{file_handle})) {
	    $self->{file_handle}->print ($str);
	} else {
	    $self->{file_handle} .= $str;
	}
	undef $self->{data};
    }
}

#
# fmt re-fills paragraphs (like fmt(1)) in `ascii' output
#

sub _fmt {
    my ($str, $indent, $line_width, $justify) = @_;

    if ($justify eq 'center') {
	$indent += ($line_width - length ($str)) / 2;
    }
    return (" " x $indent . $str . "\n");
}

sub fmt {
    my ($self, $str, $indent, $rindent, $line_width, $justify) = @_;

    $str =~ s/[\s\n\r]+/ /gs;	# strip multiple spaces/newlines
    $str =~ s/^\s//;		# remove leading space
    $str =~ s/\s$//;		# remove trailing space
    $line_width = $line_width - $indent - $rindent;
    $str =~ s/(.{1,$line_width})(\s|$)/&_fmt($1, $indent, $line_width, $justify)/ge;

    return ($str);
}

sub format_table {
    my $self = shift; my $table = shift; my $builder = shift;

    $self->collect_break;

    my ($part, $row, $entry);
    my (@avg_length, @min_width, @col_width);
    my ($ii, $jj);

    my ($num_parts) = $table->num_parts;
    my ($num_table_rows) = $table->num_rows;
    my ($num_columns) = $table->num_columns;
    for ($ii = 0; $ii < $num_columns; $ii ++) {
	$avg_length[$ii] = 0;
	$min_width[$ii] = 0;
    }

    #
    # Calculate column widths
    #  1) find minimum word widths for each column (max of any one
    #     word within a column)
    #  2) find average character-length of each column (avg of all
    #     entries in the column)
    #  3) set column width to max of proportion of average column
    #     length and  minimum width (from 1)
    #     a) if proportion is less than minimum, set to minimum and
    #        subtract difference from available space for other columns
    #
    foreach $part (@{$table->parts}) {
	foreach $row (@{$part->rows}) {
	    my ($col_num) = 0;
	    foreach $entry (@{$row->entries}) {
		my ($word);
		my ($test_data) = $entry->as_string($self);

		$entry->{'length'} = length ($test_data);
		$avg_length[$col_num] += $entry->{'length'};
		foreach $word (split (/[\s\n]/s, $test_data)) {
		    if (length ($word) > $min_width[$col_num]) {
			$min_width[$col_num] = length ($word);
		    }
		}
		$col_num ++;
	    }
	}
    }

    # make `avg_length' earn it's name :-)
    my ($total_avg_length) = 0;
    for ($jj = 0; $jj < $num_columns; $jj ++) {
	$total_avg_length += ($avg_length[$jj]
			      = ($avg_length[$jj] / $num_table_rows));
    }

    # set `$col_width' to the proportion of the `avg_length' to
    # `total_avg_length', or `$min_width' if that's greater.
    # If `$min_width' _is_ greater, remove that much space from the
    # other columns ($less_space)
    die "$::prog: assert: \$total_avg_length is 0, no table data"
	if ($total_avg_length == 0);
    my ($less_space) = 0;
    # `3' is our inter-column gap
    my ($line_width) = $self->line_width - $num_columns * 3;
    for ($jj = 0; $jj < $num_columns; $jj ++) {
	$col_width[$jj] = int ($line_width * ($avg_length[$jj]/$total_avg_length) + 0.5);
	if ($col_width[$jj] < $min_width[$jj]) {
	    $less_space += $min_width[$jj] - $col_width[$jj];
	    $col_width[$jj] = $min_width[$jj];
	}
    }

    # now that we now how much space is really available, reproportion
    # the space among the wider columns.  If we run into another min
    # greater than column width, then warn
    my ($already_warned) = 0;
    $line_width -= $less_space;
    for ($jj = 0; $jj < $num_columns; $jj ++) {
	if ($col_width[$jj] != $min_width[$jj]) {
	    $col_width[$jj] = int ($line_width * ($avg_length[$jj]/$total_avg_length) + 0.5);
	    if ($col_width[$jj] < $min_width[$jj]) {
		$line_width -= $min_width[$jj] - $col_width[$jj];
		$col_width[$jj] = $min_width[$jj];
		if (!$already_warned) {
		    $already_warned = 1;
		    warn "table too wide\n";
		}
	    }
	}
    }

    #
    # format every entry of every row of every part
    #

    # our context is pretty bare, `center' is default for ``head'' part
    $self->push (new Quilt::Flow ('space_before' => 1,
				  'space_after' => 1,
				  'start_indent' => 0,
				  'end_indent' => 0,
				  'first_line_start_indent' => 0,
				  'line_width' => 0,
				  'quadding' => 'center',
				  'lines' => 'wrap'));
    my ($data) = "";
    my $single_row_sep = row_sep (\@col_width, $table->frame, "-") . "\n";
    my $double_row_sep = row_sep (\@col_width, $table->frame, "=") . "\n";
    if ($num_parts == 1 || $table->frame !~ /none/i) {
	# only a ``body'' part or framing all, put divider above and set start justify
	$data .= $single_row_sep;
	# `start' is default for other parts
	$self->quadding ('start');
    }
    my $part_num = 1;
    foreach $part (@{$table->parts}) {
	my (@rows);
	foreach $row (@{$part->rows}) {
	    my (@entries);
	    my ($col_num) = 0;
	    my ($ascii);
	    foreach $entry (@{$row->entries}) {
		$self->line_width ($col_width[$col_num]);
		# XXX this could be designed better
		push (@{$self->{'file_handles'}}, $self->{'file_handle'});
		$self->{'file_handle'} = '';
		$entry->iter->children_accept ($builder, $self);
		$self->collect_data;
		# remove leading and trailing blank lines
		$self->{'file_handle'} =~ s/^[\s\n]*\n//s;
		$self->{'file_handle'} =~ s/[\s\n]*$/\n/s; # leave one newline
		push (@entries, $self->{'file_handle'});
		$self->{'file_handle'} = pop (@{$self->{'file_handles'}});
		$col_num ++;
	    }

	    push (@rows, merge_entries (\@col_width, $table->frame, @entries));
	}
	
	if ($table->frame =~ /none/i) {
	    $data .= join ("\n", @rows);
	    $data .= $single_row_sep;
	} else {
	    $data .= join ($single_row_sep, @rows);
	    if ($part_num == $num_parts) {
		$data .= $single_row_sep;
	    } else {
		$data .= $double_row_sep;
	    }
	}
	# `start' is default for other parts
	$self->quadding ('start');

	$part_num ++;
    }
    $self->pop;

    return $data;
}

sub row_sep {
    my ($col_widths, $frame, $csep) = @_;

    my @entries;
    my $ii;
    for ($ii = 0; $ii <= $#$col_widths; $ii ++) {
	push (@entries, $csep x $col_widths->[$ii]);
    }
    my $pre = ($frame =~ /none/i) ? "" : "+$csep";
    my $post = ($frame =~ /none/i) ? "" : "$csep+";
    my $sep = ($frame =~ /none/i) ? "   " : "$csep+$csep";
    return ($pre . join ($sep, @entries) . $post);
}

sub merge_entries {
    my ($col_widths, $frame, @entries) = @_;
    my (@splits, $ii);
    my ($data) = "";

    for ($ii = 0; $ii <= $#{$col_widths}; $ii ++) {
	my (@line_splits) = split (/\n/, $entries[$ii]);
	$splits[$ii] = \@line_splits;
    }

    my ($done) = 0;
    while (!$done) {
	my (@line);
	$done = 1;
	for ($ii = 0; $ii <= $#{$col_widths}; $ii ++) {
	    my ($col_width) = $col_widths->[$ii];
	    if ($frame =~ /none/i && $ii == $#{$col_widths}) {
		# Leave off extra space at end of line
		$col_width = length ($splits[$ii]->[0]);
	    }
	    if ($#{$splits[$ii]} > -1) {
		push (@line, sprintf ("%-${col_width}.${col_width}s",
				      shift (@{$splits[$ii]})));
	    } else {
		push (@line, " " x $col_width);
	    }
	    ($#{$splits[$ii]} != -1) && ($done = 0);
	}
	my $pre = ($frame =~ /none/i) ? "" : "| ";
	my $post = ($frame =~ /none/i) ? "" : " |";
	my $sep = ($frame =~ /none/i) ? "   " : " | ";
	$data .= $pre . join ($sep, @line) . "$post\n";
    }

    return $data;
}

sub space_before  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'space_before'} = shift
	: return $self->{'current'}[-1]{'space_before'};
}
sub space_after  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'space_after'} = shift
	: return $self->{'current'}[-1]{'space_after'};
}
sub first_line_start_indent  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'first_line_start_indent'} = shift
	: return $self->{'current'}[-1]{'first_line_start_indent'};
}
sub start_indent  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'start_indent'} = shift
	: return $self->{'current'}[-1]{'start_indent'};
}
sub end_indent  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'end_indent'} = shift
	: return $self->{'current'}[-1]{'end_indent'};
}
sub line_width  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'line_width'} = shift
	: return $self->{'current'}[-1]{'line_width'};
}
sub lines  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'lines'} = shift
	: return $self->{'current'}[-1]{'lines'};
}
sub quadding  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'quadding'} = shift
	: return $self->{'current'}[-1]{'quadding'};
}
sub inline  {
    my $self = shift;
    @_ ? $self->{'current'}[-1]{'inline'} = shift
	: return $self->{'current'}[-1]{'inline'};
}

1;