File Coverage

blib/lib/App/podman.pm
Criterion Covered Total %
statement 23 98 23.4
branch 0 34 0.0
condition 0 12 0.0
subroutine 8 10 80.0
pod 0 1 0.0
total 31 155 20.0


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 -- leonerd@leonerd.org.uk
5              
6 1     1   1050 use v5.26;
  1         5  
7 1     1   16 use utf8;
  1         22  
  1         9  
8              
9 1     1   29 use Object::Pad;
  1         2  
  1         5  
10              
11             package App::podman 0.01;
12             class App::podman;
13              
14 1     1   275 use List::Keywords qw( first );
  1         3  
  1         5  
15              
16 1     1   633 use Term::Size;
  1         621  
  1         56  
17 1     1   595 use Convert::Color;
  1         23332  
  1         54  
18 1     1   534 use Convert::Color::XTerm 0.06;
  1         3261  
  1         39  
19 1     1   558 use String::Tagged::Terminal;
  1         9073  
  1         2395  
20              
21             =head1 NAME
22              
23             C - a terminal document viewer for POD and other syntaxes
24              
25             =head1 SYNOPSIS
26              
27             use App::podman;
28              
29             exit App::podman->new->run( "some-file.pod" );
30              
31             =head1 DESCRIPTION
32              
33             This module implements a terminal-based program for viewing structured
34             documents. It currently understands POD and some simple Markdown formatting,
35             though future versions are expected to handle nroff (for manpages) and other
36             styles.
37              
38             To actually use it, you likely wanted wanted to see the F script.
39              
40             =cut
41              
42             my %FORMATSTYLES = (
43             B => { bold => 1 },
44             I => { italic => 1 },
45             F => { italic => 1, under => 1 },
46             C => { monospace => 1, bg => Convert::Color->new( "xterm:235" ) },
47             L => { under => 1, fg => Convert::Color->new( "xterm:rgb(3,3,5)" ) }, # light blue
48             );
49              
50             my %PARASTYLES = (
51             head1 => { fg => Convert::Color->new( "vga:yellow" ), bold => 1 },
52             head2 => { fg => Convert::Color->new( "vga:cyan" ), bold => 1, indent => 2 },
53             head3 => { fg => Convert::Color->new( "vga:green" ), bold => 1, indent => 4 },
54             # TODO head4
55             plain => { indent => 6, blank_after => 1 },
56             verbatim => { indent => 8, blank_after => 1, $FORMATSTYLES{C}->%* },
57             );
58             $PARASTYLES{item} = $PARASTYLES{plain};
59              
60             my @PARSER_CLASSES = qw(
61             App::podman::Parser::Pod
62             App::podman::Parser::Markdown
63             );
64              
65             require ( "$_.pm" =~ s{::}{/}gr ) for @PARSER_CLASSES;
66              
67 0           method run ( $file, %opts )
  0            
  0            
  0            
68 0     0 0   {
69 0           my $parser_class;
70              
71 0 0         if( defined $opts{format} ) {
72 0 0         $parser_class = first { $_->format eq $opts{format} } @PARSER_CLASSES or
  0            
73             die "Unrecognised format name $opts{format}\n";
74             }
75              
76 0 0         if( ! -f $file ) {
77 0           open my $f, "-|", "perldoc", "-l", $file;
78 0 0         $file = <$f>; chomp $file if defined $file;
  0            
79 0           close $f;
80 0 0         $? and return $? >> 8;
81             }
82              
83 0   0       $parser_class //= do {
84 0 0         first { $_->can_parse_file( $file ) } @PARSER_CLASSES or
  0            
85             die "Unable to find a handler for $file\n";
86             };
87              
88 0           my @paragraphs = $parser_class->new->parse_file( $file );
89              
90             # Unless -n switch
91 0           open my $outh, "|-", "less", "+R";
92 0           $outh->binmode( ":encoding(UTF-8)" );
93 0           select $outh;
94              
95 0           my $TERMWIDTH = Term::Size::chars;
96              
97 0           my $nextblank;
98              
99             # To avoid recusion over a bunch of variables as state, we'll maintain a queue
100 0           while ( @paragraphs ) {
101 0           my $para = shift @paragraphs;
102              
103 0           my $margin;
104             my $leader;
105 0           my %typestyle;
106              
107 0 0         if( ref $para eq "HASH" ) {
108 0           $margin = $para->{margin};
109 0           $leader = sprintf "%-*s", $margin, $para->{leader};
110              
111 0           %typestyle = ( $para->%* );
112              
113 0           $para = $para->{para};
114             }
115              
116 0 0         if( $para->type =~ m/^list-(.*)$/ ) {
117 0           my $listtype = $1;
118              
119 0           my $n = 1;
120              
121             unshift @paragraphs, map {
122 0           my $item = $_;
  0            
123 0 0         if( $item->type ne "item" ) {
    0          
    0          
    0          
124             # non-items just stand as they are + indent
125 0           { para => $item, margin => $margin + $para->indent }
126             }
127             elsif( $listtype eq "bullet" ) {
128 0           { para => $item, margin => $margin + $para->indent, leader => "*" }
129             }
130             elsif( $listtype eq "number" ) {
131 0           { para => $item, margin => $margin + $para->indent, leader => sprintf "%d.", $n++ }
132             }
133             elsif( $listtype eq "text" ) {
134 0           { para => $item, margin => $margin, blank_after => 0 }
135             }
136             } $para->items;
137 0           next;
138             }
139              
140 0 0         say "" if $nextblank;
141              
142 0           %typestyle = ( $PARASTYLES{ $para->type }->%*, %typestyle );
143              
144             my $s = $para->text->clone(
145             convert_tags => {
146 0     0     ( map { $_ => do { my $k = $_; sub { $FORMATSTYLES{$k}->%* } } } keys %FORMATSTYLES ),
  0            
  0            
  0            
  0            
147             },
148             );
149              
150             $typestyle{$_} and $s->apply_tag( 0, -1, $_ => $typestyle{$_} )
151 0   0       for qw( fg bg bold under italic monospace );
152              
153 0           $nextblank = !!$typestyle{blank_after};
154              
155 0           my @lines = $s->split( qr/\n/ );
156              
157 0   0       my $indent = $typestyle{indent} // 0;
158 0           $indent += $margin;
159              
160 0           foreach my $line ( @lines ) {
161 0 0         length $line or
162             ( print "\n" ), next;
163              
164 0           $s = String::Tagged::Terminal->new_from_formatting( $line );
165              
166 0   0       my $width = $TERMWIDTH - $indent - length($leader // "");
167              
168 0           while( length $s ) {
169 0           my $part;
170 0 0         if( length($s) > $width ) {
171 0 0         if( substr($s, 0, $width) =~ m/(\s+)\S*$/ ) {
172 0           my $partlen = $-[1];
173 0           my $chopat = $+[1];
174              
175 0           $part = $s->substr( 0, $partlen );
176 0           $s->set_substr( 0, $chopat, "" );
177             }
178             else {
179 0           die "ARGH: notsure how to trim this one\n";
180             }
181             }
182             else {
183 0           $part = $s;
184 0           $s = "";
185             }
186              
187 0   0       print " "x($indent - length($leader // ""));
188 0 0         print $leader if defined $leader;
189 0           print $part->build_terminal . "\n";
190              
191 0           undef $leader;
192             }
193             }
194             }
195             }
196              
197             =head1 TODO
198              
199             =over 4
200              
201             =item *
202              
203             Add more formats. nroff and ReST at least. Perhaps others.
204              
205             =item *
206              
207             Improved Markdown parser. Currently the parser is very simple.
208              
209             =item *
210              
211             Other outputs. Consider a L-based frontend. Also some structured file
212             writers - allowing cross-conversion between POD, Markdown, ReST, nroff and
213             maybe also HTML output.
214              
215             =back
216              
217             =cut
218              
219             =head1 AUTHOR
220              
221             Paul Evans
222              
223             =cut
224              
225             0x55AA;