File Coverage

blib/lib/App/sdview/Output/Formatted.pm
Criterion Covered Total %
statement 144 150 96.0
branch 32 40 80.0
condition 21 32 65.6
subroutine 14 14 100.0
pod 0 5 0.0
total 211 241 87.5


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2021-2023 -- leonerd@leonerd.org.uk
5              
6 2     2   26 use v5.26;
  2         8  
7 2     2   12 use warnings;
  2         4  
  2         55  
8 2     2   11 use utf8;
  2         4  
  2         15  
9              
10 2     2   80 use Object::Pad 0.800;
  2         12  
  2         96  
11              
12             package App::sdview::Output::Formatted 0.13;
13             class App::sdview::Output::Formatted :strict(params);
14              
15             # This isn't itself an output module; but a base class to build them on
16             # So no `format` constant.
17              
18 2     2   1305 use App::sdview::Style;
  2         6  
  2         121  
19              
20 2     2   16 use List::Util qw( max );
  2         4  
  2         157  
21 2     2   15 use String::Tagged 0.15; # ->from_sprintf
  2         49  
  2         8215  
22              
23             =head1 NAME
24              
25             C - base class for generating formatted output from L
26              
27             =head1 DESCRIPTION
28              
29             This module is the base class used by both L and
30             L. It shouldn't be used directly.
31              
32             =cut
33              
34             field $_TERMWIDTH;
35             field $_nextblank;
36              
37 8         14 method output ( @paragraphs )
  8         14  
  8         11  
38 8     8 0 21 {
39 8         26 $self->setup_output();
40              
41 8         10211 $_TERMWIDTH = $self->width;
42              
43 8         19 foreach my $para ( @paragraphs ) {
44 15 50       160 my $code = $self->can( "output_" . ( $para->type =~ s/-/_/gr ) )
45             or die "TODO: Unhandled paragraph type " . $para->type;
46              
47 15         48 $self->$code( $para );
48             }
49             }
50              
51             # Most paragraphs are handled in a uniform way
52             *output_head1 = \&_output_para;
53             *output_head2 = \&_output_para;
54             *output_head3 = \&_output_para;
55             *output_head4 = \&_output_para;
56              
57             *output_plain = \&_output_para;
58              
59             *output_verbatim = \&_output_para;
60              
61             *output_item = \&_output_para;
62              
63 21         29 method _output_para ( $para, %opts )
  21         34  
  21         40  
  21         26  
64 21     21   47 {
65 21   100     64 my $margin = $opts{margin} // 0;
66 21         32 my $leader = $opts{leader};
67 21         32 my $indent = $opts{indent};
68              
69 21         51 my %typestyle = App::sdview::Style->para_style( $para->type )->%*;
70              
71 21 100       85 $self->say() if $_nextblank;
72              
73 21         52 my $text = App::sdview::Style->convert_str( $para->text );
74              
75             $typestyle{$_} and $text->apply_tag( 0, -1, $_ => $typestyle{$_} )
76 21   66     1461 for qw( fg bg bold under italic monospace );
77              
78 21         347 $_nextblank = !!$typestyle{blank_after};
79              
80 21         96 my @lines = $text->split( qr/\n/ );
81 21 100 33     2006 @lines or @lines = ( String::Tagged->new ) if defined $leader;
82              
83             # If there's a background set, then space-pad every line to the same width
84             # so it looks neater on the terminal
85             # https://rt.cpan.org/Ticket/Display.html?id=140536
86 21 100       48 if( defined $typestyle{bg} ) {
87 1         4 my $width = max map { length $_ } @lines;
  3         15  
88 1         62 $_ .= " " x ( $width - length $_ ) for @lines;
89             }
90              
91 21   100     150 $margin += ( $typestyle{margin} // 0 );
92 21   100     62 $indent //= 0;
93              
94 21         35 foreach my $line ( @lines ) {
95 23 50 33     92 length $line or defined $leader or
96             ( $self->say() ), next;
97              
98 23         141 my $width = $_TERMWIDTH - $margin - $indent;
99              
100 23   66     51 while( length $line or defined $leader ) {
101 23         118 my $part;
102 23 50       43 if( length($line) > $width ) {
103 0 0       0 if( substr($line, 0, $width) =~ m/(\s+)\S*$/ ) {
104 0         0 my $partlen = $-[1];
105 0         0 my $chopat = $+[1];
106              
107 0         0 $part = $line->substr( 0, $partlen );
108 0         0 $line->set_substr( 0, $chopat, "" );
109             }
110             else {
111 0         0 die "ARGH: notsure how to trim this one\n";
112             }
113             }
114             else {
115 23         108 $part = $line;
116 23         62 $line = "";
117             }
118              
119 23         58 my $prefix = " "x$margin;;
120              
121 23 100       45 if( defined $leader ) {
122 11         30 my %leaderstyle = App::sdview::Style->para_style( "leader" )->%*;
123             $leaderstyle{$_} and $leader->apply_tag( 0, -1, $_ => $leaderstyle{$_} )
124 11   66     71 for qw( fg bg bold under italic monospace );
125              
126 11 100       398 if( length $leader <= $indent ) {
127             # If the leader will fit on the same line
128 8         51 $prefix .= $leader . " "x($indent - length $leader);
129             }
130             else {
131             # Spill the leader onto its own line
132 3         25 $self->say( $prefix, $leader );
133              
134 3 50       28 $prefix .= " "x$indent if length $part;
135             }
136              
137 11         2622 undef $leader;
138             }
139             else {
140 12         22 $prefix .= " "x$indent;
141             }
142              
143 23         74 $self->say( $prefix, $part );
144             }
145             }
146             }
147              
148 3     3 0 7 method output_list_bullet ( $para, %opts ) { $self->_output_list( bullet => $para, %opts ); }
  3         4  
  3         6  
  3         5  
  3         6  
  3         15  
149 1     1 0 3 method output_list_number ( $para, %opts ) { $self->_output_list( number => $para, %opts ); }
  1         3  
  1         1  
  1         3  
  1         1  
  1         5  
150 1     1 0 3 method output_list_text ( $para, %opts ) { $self->_output_list( text => $para, %opts ); }
  1         2  
  1         2  
  1         3  
  1         2  
  1         4  
151              
152 5         7 method _output_list( $listtype, $para, %opts )
  5         9  
  5         7  
  5         8  
  5         7  
153 5     5   11 {
154 5         17 my $n = $para->initial;
155              
156 5   100     20 my $margin = $opts{margin} // 0;
157 5   50     22 $margin += App::sdview::Style->para_style( "list" )->{margin} // 0;
158              
159 5         20 foreach my $item ( $para->items ) {
160 13         159 my $leader;
161 13 100       56 if( $item->type eq "plain" ) {
    100          
    100          
    100          
    50          
162             # plain paragraphs in list are treated like items with no leader
163             $self->output_item( $item,
164             # make sure not to double-count the margin
165             margin => $margin - App::sdview::Style->para_style( "plain" )->{margin},
166 1         5 indent => $para->indent,
167             );
168 1         15 next;
169             }
170             elsif( $item->type ne "item" ) {
171             # non-items just stand as they are + indent
172             }
173             elsif( $listtype eq "bullet" ) {
174 5         18 $leader = String::Tagged->new( "•" );
175             }
176             elsif( $listtype eq "number" ) {
177 3         15 $leader = String::Tagged->from_sprintf( "%d.", $n++ );
178             }
179             elsif( $listtype eq "text" ) {
180 3         9 $leader = App::sdview::Style->convert_str( $item->term );
181             }
182              
183 12 50       607 my $code = $self->can( "output_" . ( $item->type =~ s/-/_/gr ) ) or
184             die "TODO: Unhandled item type " . $item->type;
185              
186 12         39 $self->$code( $item,
187             margin => $margin,
188             indent => $para->indent,
189             leader => $leader,
190             );
191             }
192             }
193              
194 2         3 method output_table ( $para, %opts )
  2         6  
  2         3  
  2         4  
195 2     2 0 5 {
196 2   50     10 my $margin = $opts{margin} // 0;
197              
198 2         9 my %typestyle = App::sdview::Style->para_style( "table" )->%*;
199 2   50     8 $margin += $typestyle{margin} // 0;
200              
201 2         5 my $marginspace = " "x$margin;
202              
203 2         7 my @rows = $para->rows;
204 2         4 my $ncols = scalar $rows[0]->@*;
205 2         5 my $maxcol = $ncols - 1;
206              
207             my @colwidths = map {
208 2         5 my $colidx = $_;
  5         23  
209 5         11 max map { length $rows[$_][$colidx]->text } 0 .. $#rows;
  10         58  
210             } 0 .. $maxcol;
211              
212 2         14 my @hrules = map { "─" x ($colwidths[$_] + 2) } 0 .. $maxcol;
  5         15  
213              
214 2         13 $self->say( $marginspace, "┌", join( "┬", @hrules ), "┐" );
215              
216             # TODO: Much splitting / reflowing of content
217 2         5 my $firstrow = 1;
218 2         5 foreach my $row ( @rows ) {
219 4 100       11 if( !$firstrow ) {
220 2         26 $self->say( $marginspace, "├", join( "┼", @hrules ), "┤" );
221             }
222              
223 4         15 my %rowstyle = %typestyle;
224 4 100       14 %rowstyle = ( App::sdview::Style->para_style( "table-heading" )->%*, %rowstyle ) if $firstrow;
225              
226 4         17 my $out = "│";
227              
228 4         13 foreach my $colidx ( 0 .. $maxcol ) {
229 10         133 my $cell = $row->[$colidx];
230              
231 10         27 my $text = App::sdview::Style->convert_str( $cell->text );
232              
233             $rowstyle{$_} and $text->apply_tag( 0, -1, $_ => $rowstyle{$_} )
234 10   66     582 for qw( fg bg bold under italic monospace );
235              
236 10         189 my $spare = $colwidths[$colidx] - length $text;
237 10 100       64 my $leftpad = ( $cell->align eq "right" ) ? " "x$spare :
    100          
238             ( $cell->align eq "centre" ) ? " "x($spare/2) :
239             "";
240 10         24 my $rightpad = " "x($spare - length $leftpad);
241              
242 10         31 $out .= " " . $leftpad . $text . $rightpad . " ";
243 10         2794 $out .= "│";
244             }
245 4         100 $self->say( $marginspace, $out );
246              
247 4         43 undef $firstrow;
248             }
249              
250 2         10 $self->say( $marginspace, "└", join( "┴", @hrules ), "┘" );
251             }
252              
253             =head1 AUTHOR
254              
255             Paul Evans
256              
257             =cut
258              
259             0x55AA;