App-sdview

 view release on metacpan or  search on metacpan

lib/App/sdview/Output/Formatted.pm  view on Meta::CPAN

# This isn't itself an output module; but a base class to build them on
# So no `format` constant.

use App::sdview::Style;

use List::Util qw( max );
use String::Tagged 0.15;  # ->from_sprintf

=head1 NAME

C<App::sdview::Output::Formatted> - base class for generating formatted output from L<App::sdview>

=head1 DESCRIPTION

This module is the base class used by  both L<App::sdview::Output::Plain> and
L<App::sdview::Output::Terminal>. It shouldn't be used directly.

=cut

field $_TERMWIDTH;
field $_nextblank;

method output ( @paragraphs )
{
   $self->setup_output();

   $_TERMWIDTH = $self->width;

   foreach my $para ( @paragraphs ) {
      my $code = $self->can( "output_" . ( $para->type =~ s/-/_/gr ) )
         or die "TODO: Unhandled paragraph type " . $para->type;

      $self->$code( $para );
   }
}

# Most paragraphs are handled in a uniform way
*output_head1 = \&_output_para;
*output_head2 = \&_output_para;
*output_head3 = \&_output_para;
*output_head4 = \&_output_para;

*output_plain = \&_output_para;

*output_verbatim = \&_output_para;

*output_item = \&_output_para;

method _output_para ( $para,
   :$margin //= 0,
   :$leader = undef,
   :$indent //= 0,
) {
   my %typestyle = App::sdview::Style->para_style( $para->type )->%*;

   $self->say() if $_nextblank;

   my $text = App::sdview::Style->convert_str( $para->text );

   $typestyle{$_} and $text->apply_tag( 0, -1, $_ => $typestyle{$_} )
      for qw( fg bg bold under italic monospace );

   $_nextblank = !!$typestyle{blank_after};

   my @lines = $text->split( qr/\n/ );
   @lines or @lines = ( String::Tagged->new ) if defined $leader;

   # If there's a background set, then space-pad every line to the same width
   # so it looks neater on the terminal
   #   https://rt.cpan.org/Ticket/Display.html?id=140536
   if( defined $typestyle{bg} ) {
      my $width = max map { length $_ } @lines;
      $_ .= " " x ( $width - length $_ ) for @lines;
   }

   $margin += ( $typestyle{margin} // 0 );

   foreach my $line ( @lines ) {
      length $line or defined $leader or
         ( $self->say() ), next;

      my $width = $_TERMWIDTH - $margin - $indent;

      while( length $line or defined $leader ) {
         my $part;
         if( length($line) > $width ) {
            if( substr($line, 0, $width) =~ m/(\s+)\S*$/ ) {
               my $partlen = $-[1];
               my $chopat = $+[1];

               $part = $line->substr( 0, $partlen );
               $line->set_substr( 0, $chopat, "" );
            }
            else {
               die "ARGH: notsure how to trim this one\n";
            }
         }
         else {
            $part = $line;
            $line = "";
         }

         my $prefix = " "x$margin;;

         if( defined $leader ) {
            my %leaderstyle = App::sdview::Style->para_style( "leader" )->%*;
            $leaderstyle{$_} and $leader->apply_tag( 0, -1, $_ => $leaderstyle{$_} )
               for qw( fg bg bold under italic monospace );

            if( length($leader) + 1 <= $indent ) {
               # If the leader will fit on the same line with at least one space
               $prefix .= $leader . " "x($indent - length $leader);
            }
            else {
               # Spill the leader onto its own line
               $self->say( $prefix, $leader );

               $prefix .= " "x$indent if length $part;
            }

            undef $leader;
         }
         else {
            $prefix .= " "x$indent;
         }

         $self->say( $prefix, $part );
      }
   }
}

method output_list_bullet ( $para, %opts ) { $self->_output_list( bullet => $para, %opts ); }
method output_list_number ( $para, %opts ) { $self->_output_list( number => $para, %opts ); }
method output_list_text   ( $para, %opts ) { $self->_output_list( text   => $para, %opts ); }

method _output_list( $listtype, $para,
   :$margin //= 0,
   %  # ignore other named opts
) {
   my $n = $para->initial;

   $margin += App::sdview::Style->para_style( "list" )->{margin} // 0;

   foreach my $item ( $para->items ) {
      my $leader;
      if( $item->type eq "plain" ) {
         # plain paragraphs in list are treated like items with no leader
         $self->output_item( $item,
            # make sure not to double-count the margin
            margin => $margin - App::sdview::Style->para_style( "plain" )->{margin},
            indent => $para->indent,
         );
         next;
      }
      elsif( $item->type ne "item" ) {
         # non-items just stand as they are + indent
      }
      elsif( $listtype eq "bullet" ) {
         $leader = String::Tagged->new( "•" );
      }
      elsif( $listtype eq "number" ) {
         $leader = String::Tagged->from_sprintf( "%d.", $n++ );
      }
      elsif( $listtype eq "text" ) {
         $leader = App::sdview::Style->convert_str( $item->term );
      }

      my $code = $self->can( "output_" . ( $item->type =~ s/-/_/gr ) ) or
         die "TODO: Unhandled item type " . $item->type;

      $self->$code( $item,
         margin => $margin,
         indent => $para->indent,
         leader => $leader,
      );
   }
}

method output_table ( $para, :$margin //= 0 )
{
   my %typestyle = App::sdview::Style->para_style( "table" )->%*;
   $margin += $typestyle{margin} // 0;

   my $marginspace = " "x$margin;

   my @rows = $para->rows;
   my $ncols = scalar $rows[0]->@*;
   my $maxcol = $ncols - 1;

   my @colwidths = map {
      my $colidx = $_;
      max map { length $rows[$_][$colidx]->text } 0 .. $#rows;
   } 0 .. $maxcol;

   my @hrules = map { "─" x ($colwidths[$_] + 2) } 0 .. $maxcol;

   $self->say( $marginspace, "┌", join( "┬", @hrules ), "┐" );

   # TODO: Much splitting / reflowing of content
   my $firstrow = 1;
   foreach my $row ( @rows ) {
      if( !$firstrow ) {
         $self->say( $marginspace, "├", join( "┼", @hrules ), "┤" );
      }

      my $out = "│";

      foreach my $colidx ( 0 .. $maxcol ) {
         my $cell = $row->[$colidx];

         my $text = App::sdview::Style->convert_str( $cell->text );

         my %cellstyle = %typestyle;
         %cellstyle = ( App::sdview::Style->para_style( "table-heading" )->%*, %cellstyle ) if $cell->heading;

         $cellstyle{$_} and $text->apply_tag( 0, -1, $_ => $cellstyle{$_} )
            for qw( fg bg bold under italic monospace );

         my $spare = $colwidths[$colidx] - length $text;
         my $leftpad = ( $cell->align eq "right"  ) ? " "x$spare :
                       ( $cell->align eq "centre" ) ? " "x($spare/2) :
                                                      "";
         my $rightpad = " "x($spare - length $leftpad);

         $out .= " " . $leftpad . $text . $rightpad . " ";
         $out .= "│";
      }
      $self->say( $marginspace, $out );

      undef $firstrow;
   }

   $self->say( $marginspace, "└", join( "┴", @hrules ), "┘" );
}

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;



( run in 0.863 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )