File Coverage

blib/lib/App/sdview/Output/Formatted.pm
Criterion Covered Total %
statement 141 147 95.9
branch 32 40 80.0
condition 14 24 58.3
subroutine 15 15 100.0
pod 0 5 0.0
total 202 231 87.4


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