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; |