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   24 use v5.26;
  2         14  
7 2     2   10 use warnings;
  2         4  
  2         54  
8 2     2   31 use utf8;
  2         6  
  2         14  
9              
10 2     2   64 use Object::Pad 0.800;
  2         16  
  2         80  
11              
12             package App::sdview::Output::Formatted 0.12;
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   1289 use App::sdview::Style;
  2         11  
  2         96  
19              
20 2     2   13 use List::Util qw( max );
  2         12  
  2         150  
21 2     2   15 use String::Tagged 0.15; # ->from_sprintf
  2         38  
  2         8066  
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         12 method output ( @paragraphs )
  8         16  
  8         13  
38 8     8 0 24 {
39 8         39 $self->setup_output();
40              
41 8         10011 $_TERMWIDTH = $self->width;
42              
43 8         21 foreach my $para ( @paragraphs ) {
44 15 50       172 my $code = $self->can( "output_" . ( $para->type =~ s/-/_/gr ) )
45             or die "TODO: Unhandled paragraph type " . $para->type;
46              
47 15         46 $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         31 method _output_para ( $para, %opts )
  21         30  
  21         75  
  21         31  
64 21     21   47 {
65 21   100     60 my $margin = $opts{margin} // 0;
66 21         34 my $leader = $opts{leader};
67 21         29 my $indent = $opts{indent};
68              
69 21         45 my %typestyle = App::sdview::Style->para_style( $para->type )->%*;
70              
71 21 100       74 $self->say() if $_nextblank;
72              
73 21         51 my $text = App::sdview::Style->convert_str( $para->text );
74              
75             $typestyle{$_} and $text->apply_tag( 0, -1, $_ => $typestyle{$_} )
76 21   66     1418 for qw( fg bg bold under italic monospace );
77              
78 21         357 $_nextblank = !!$typestyle{blank_after};
79              
80 21         110 my @lines = $text->split( qr/\n/ );
81 21 100 33     2040 @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       55 if( defined $typestyle{bg} ) {
87 1         5 my $width = max map { length $_ } @lines;
  3         15  
88 1         9 $_ .= " " x ( $width - length $_ ) for @lines;
89             }
90              
91 21   100     141 $margin += ( $typestyle{margin} // 0 );
92 21   100     63 $indent //= 0;
93              
94 21         35 foreach my $line ( @lines ) {
95 23 50 33     87 length $line or defined $leader or
96             ( $self->say() ), next;
97              
98 23         158 my $width = $_TERMWIDTH - $margin - $indent;
99              
100 23   66     51 while( length $line or defined $leader ) {
101 23         121 my $part;
102 23 50       76 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         115 $part = $line;
116 23         38 $line = "";
117             }
118              
119 23         54 my $prefix = " "x$margin;;
120              
121 23 100       50 if( defined $leader ) {
122 11         32 my %leaderstyle = App::sdview::Style->para_style( "leader" )->%*;
123             $leaderstyle{$_} and $leader->apply_tag( 0, -1, $_ => $leaderstyle{$_} )
124 11   66     69 for qw( fg bg bold under italic monospace );
125              
126 11 100       389 if( length $leader <= $indent ) {
127             # If the leader will fit on the same line
128 8         61 $prefix .= $leader . " "x($indent - length $leader);
129             }
130             else {
131             # Spill the leader onto its own line
132 3         21 $self->say( $prefix, $leader );
133              
134 3 50       28 $prefix .= " "x$indent if length $part;
135             }
136              
137 11         2568 undef $leader;
138             }
139             else {
140 12         23 $prefix .= " "x$indent;
141             }
142              
143 23         69 $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         5  
  3         6  
  3         9  
  3         4  
  3         19  
149 1     1 0 3 method output_list_number ( $para, %opts ) { $self->_output_list( number => $para, %opts ); }
  1         2  
  1         3  
  1         3  
  1         2  
  1         4  
150 1     1 0 4 method output_list_text ( $para, %opts ) { $self->_output_list( text => $para, %opts ); }
  1         2  
  1         2  
  1         2  
  1         3  
  1         5  
151              
152 5         8 method _output_list( $listtype, $para, %opts )
  5         9  
  5         8  
  5         10  
  5         7  
153 5     5   12 {
154 5         13 my $n = $para->initial;
155              
156 5   100     24 my $margin = $opts{margin} // 0;
157 5   50     26 $margin += App::sdview::Style->para_style( "list" )->{margin} // 0;
158              
159 5         45 foreach my $item ( $para->items ) {
160 13         172 my $leader;
161 13 100       39 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         16 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         35 $leader = String::Tagged->new( "•" );
175             }
176             elsif( $listtype eq "number" ) {
177 3         14 $leader = String::Tagged->from_sprintf( "%d.", $n++ );
178             }
179             elsif( $listtype eq "text" ) {
180 3         24 $leader = App::sdview::Style->convert_str( $item->term );
181             }
182              
183 12 50       593 my $code = $self->can( "output_" . ( $item->type =~ s/-/_/gr ) ) or
184             die "TODO: Unhandled item type " . $item->type;
185              
186 12         40 $self->$code( $item,
187             margin => $margin,
188             indent => $para->indent,
189             leader => $leader,
190             );
191             }
192             }
193              
194 2         4 method output_table ( $para, %opts )
  2         4  
  2         4  
  2         3  
195 2     2 0 5 {
196 2   50     10 my $margin = $opts{margin} // 0;
197              
198 2         10 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         8 my @rows = $para->rows;
204 2         6 my $ncols = scalar $rows[0]->@*;
205 2         3 my $maxcol = $ncols - 1;
206              
207             my @colwidths = map {
208 2         6 my $colidx = $_;
  5         26  
209 5         9 max map { length $rows[$_][$colidx]->text } 0 .. $#rows;
  10         49  
210             } 0 .. $maxcol;
211              
212 2         19 my @hrules = map { "─" x ($colwidths[$_] + 2) } 0 .. $maxcol;
  5         30  
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       12 if( !$firstrow ) {
220 2         11 $self->say( $marginspace, "├", join( "┼", @hrules ), "┤" );
221             }
222              
223 4         14 my %rowstyle = %typestyle;
224 4 100       14 %rowstyle = ( App::sdview::Style->para_style( "table-heading" )->%*, %rowstyle ) if $firstrow;
225              
226 4         9 my $out = "│";
227              
228 4         10 foreach my $colidx ( 0 .. $maxcol ) {
229 10         140 my $cell = $row->[$colidx];
230              
231 10         30 my $text = App::sdview::Style->convert_str( $cell->text );
232              
233             $rowstyle{$_} and $text->apply_tag( 0, -1, $_ => $rowstyle{$_} )
234 10   66     549 for qw( fg bg bold under italic monospace );
235              
236 10         200 my $spare = $colwidths[$colidx] - length $text;
237 10 100       66 my $leftpad = ( $cell->align eq "right" ) ? " "x$spare :
    100          
238             ( $cell->align eq "centre" ) ? " "x($spare/2) :
239             "";
240 10         23 my $rightpad = " "x($spare - length $leftpad);
241              
242 10         36 $out .= " " . $leftpad . $text . $rightpad . " ";
243 10         2775 $out .= "│";
244             }
245 4         95 $self->say( $marginspace, $out );
246              
247 4         42 undef $firstrow;
248             }
249              
250 2         11 $self->say( $marginspace, "└", join( "┴", @hrules ), "┘" );
251             }
252              
253             =head1 AUTHOR
254              
255             Paul Evans
256              
257             =cut
258              
259             0x55AA;