File Coverage

blib/lib/Parse/Man.pm
Criterion Covered Total %
statement 174 209 83.2
branch 27 34 79.4
condition 6 8 75.0
subroutine 51 66 77.2
pod 1 38 2.6
total 259 355 72.9


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, 2011-2022 -- leonerd@leonerd.org.uk
5              
6             package Parse::Man 0.03;
7              
8 14     14   416349 use v5.14;
  14         98  
9 14     14   62 use warnings;
  14         23  
  14         376  
10 14     14   6903 use utf8;
  14         164  
  14         68  
11              
12 14     14   440 use base qw( Parser::MGC );
  14         24  
  14         6920  
13              
14 14     14   298301 use constant pattern_ws => qr/[ \t]+/;
  14         41  
  14         19240  
15              
16             =head1 NAME
17              
18             C - parse nroff-formatted manpages
19              
20             =head1 DESCRIPTION
21              
22             This abstract subclass of L recognises F grammar from a
23             file or string value. It invokes methods when various F directives are
24             encountered. It is intended that this class be used as a base class, with
25             methods provided to handle the various directives and formatting options.
26             Typically a subclass will store intermediate results in a data structure,
27             building it as directed by these method invocations.
28              
29             =cut
30              
31             sub from_file
32             {
33 0     0 1 0 my $self = shift;
34 0         0 my ( $file ) = @_;
35              
36 0 0       0 if( $file =~ m/\.gz$/ ) {
37 0         0 return $self->SUPER::from_file( $file, binmode => ":gzip" );
38             }
39             else {
40 0         0 return $self->SUPER::from_file( $file );
41             }
42             }
43              
44             sub parse
45             {
46 45     45 0 15678 my $self = shift;
47              
48 45         162 $self->_change_para_options(
49             mode => "P",
50             filling => 1,
51             indent => 0,
52             );
53 45         94 $self->{para_flushed} = 0;
54              
55 45         158 $self->sequence_of( \&parse_line );
56             }
57              
58             sub token_nl
59             {
60 52     52 0 73 my $self = shift;
61 52         115 $self->expect( "\n" );
62             }
63              
64             sub token_chunk
65             {
66 72     72 0 2306 my $self = shift;
67              
68 72         160 $self->skip_ws;
69              
70             # A single chunk is either "quoted sequence" or whitespace-separated
71             return $self->any_of(
72 72     72   925 sub { $self->committed_scope_of( '"', "_escaped", qr/"|$/m ) },
73 66     66   5098 sub { $self->_escaped( 1 ) },
74 72         843 );
75             }
76              
77             sub _escaped
78             {
79 72     72   431 my $self = shift;
80 72         120 my ( $break_on_space ) = @_;
81 72         94 my $ret = "";
82              
83 72         80 my $consumed = 0;
84              
85             $consumed++ while $self->any_of(
86 120     120   1371 sub { my $esc = ( $self->expect( qr/\\(?|\((..)|(.))/ ) )[1];
87 1         56 $self->commit;
88 1         7 $ret .= $self->parse_escape( $esc );
89 1         13 1;
90             },
91 119 100   119   7492 sub { length( my $more = $self->substring_before( $break_on_space ? qr/[\\\n\s]/ : qr/[\\\n]/ ) ) or return 0;
    100          
92 47         1599 $ret .= $more;
93 47         475 1 },
94 0     0   0 sub { 0 },
95 72         411 );
96              
97 72 100       2103 $consumed or $self->fail( "Expected a chunk" );
98              
99 48         209 return $ret;
100             }
101              
102             sub parse_chunks
103             {
104 22     22 0 31 my $self = shift;
105 22         26 @{ $self->sequence_of( \&token_chunk ) };
  22         86  
106             }
107              
108             sub parse_chunks_flat
109             {
110 12     12 0 18 my $self = shift;
111 12         32 return join " ", $self->parse_chunks;
112             }
113              
114             sub parse_line
115             {
116 100     100 0 7485 my $self = shift;
117              
118 100         269 $self->commit;
119              
120             $self->any_of(
121             sub {
122 100     100   1384 my $directive = ( $self->expect( qr/\.([A-Z]+)/i ) )[1];
123 52         3181 $self->commit;
124 52         297 $self->${\"parse_directive_$directive"}( $self );
  52         311  
125 52         635 $self->token_nl;
126             },
127             sub {
128             # comments
129 48     48   4355 $self->expect( qr/\.\\".*?\n/ );
130             },
131             sub {
132 48     48   3058 $self->commit;
133             $self->scope_of( undef, sub {
134 48         884 $self->{fonts} = [ "R" ];
135 48         141 $self->parse_plaintext;
136 48         397 }, "\n" );
137             },
138 100         1009 );
139             }
140              
141             =head1 TEXT CHUNK FORMATTING METHOD
142              
143             The following method is used to handle formatted text. Each call is passed a
144             plain string value from the input content.
145              
146             =cut
147              
148             =head2 chunk
149              
150             $parser->chunk( $text, %opts )
151              
152             The C<%opts> hash contains the following options:
153              
154             =over 4
155              
156             =item font => STRING
157              
158             The name of the current font (C, C, etc..)
159              
160             =item size => INT
161              
162             The current text size, relative to a paragraph base of 0.
163              
164             =back
165              
166             =cut
167              
168             sub parse_directive_B
169             {
170 4     4 0 6 my $self = shift;
171 4         15 $self->_flush_para;
172 4         17 $self->chunk( $self->parse_chunks_flat, font => "B", size => 0 );
173             }
174              
175             sub parse_directive_I
176             {
177 2     2 0 5 my $self = shift;
178 2         9 $self->_flush_para;
179 2         9 $self->chunk( $self->parse_chunks_flat, font => "I", size => 0 );
180             }
181              
182             sub parse_directive_R
183             {
184 2     2 0 6 my $self = shift;
185 2         6 $self->_flush_para;
186 2         10 $self->chunk( $self->parse_chunks_flat, font => "R", size => 0 );
187             }
188              
189             sub parse_directive_SM
190             {
191 3     3 0 5 my $self = shift;
192 3         7 $self->_flush_para;
193              
194 3         6 my @chunks = $self->parse_chunks;
195              
196 3 100       157 $self->chunk( shift @chunks, font => "R", size => 0 ) if @chunks > 2;
197 3         19 $self->chunk( shift @chunks, font => "R", size => -1 );
198 3 100       40 $self->chunk( shift @chunks, font => "R", size => 0 ) if @chunks;
199             }
200              
201             sub _parse_directive_alternate
202             {
203 4     4   5 my $self = shift;
204 4         8 my ( $first, $second ) = @_;
205 4         9 $self->_flush_para;
206 4         8 my $i = 0;
207 4 100       8 map { $self->chunk( $_, font => ( ++$i % 2 ? $first : $second ), size => 0 ) } $self->parse_chunks;
  12         282  
208             }
209              
210             sub parse_directive_BI
211             {
212 0     0 0 0 my $self = shift;
213 0         0 $self->_parse_directive_alternate( "B", "I" );
214             }
215              
216             sub parse_directive_IB
217             {
218 0     0 0 0 my $self = shift;
219 0         0 $self->_parse_directive_alternate( "I", "B" );
220             }
221              
222             sub parse_directive_RB
223             {
224 4     4 0 6 my $self = shift;
225 4         15 $self->_parse_directive_alternate( "R", "B" );
226             }
227              
228             sub parse_directive_BR
229             {
230 0     0 0 0 my $self = shift;
231 0         0 $self->_parse_directive_alternate( "B", "R" );
232             }
233              
234             sub parse_directive_RI
235             {
236 0     0 0 0 my $self = shift;
237 0         0 $self->_parse_directive_alternate( "R", "I" );
238             }
239              
240             sub parse_directive_IR
241             {
242 0     0 0 0 my $self = shift;
243 0         0 $self->_parse_directive_alternate( "I", "R" );
244             }
245              
246             =pod
247              
248             Other font requests that are found in C<\fX> or C<\f(AB> requests are handled
249             by similarly-named methods.
250              
251             =cut
252              
253             =head1 PARAGRAPH HANDLING METHODS
254              
255             The following methods are used to form paragraphs out of formatted text
256             chunks. Their return values are ignored.
257              
258             =cut
259              
260             =head2 para_TH
261              
262             $parser->para_TH( $name, $section )
263              
264             Handles the C<.TH> paragraph which gives the page title and section number.
265              
266             =cut
267              
268             sub parse_directive_TH
269             {
270 3     3 0 5 my $self = shift;
271 3         13 $self->_change_para( "P" ),
272             $self->para_TH( $self->parse_chunks );
273             }
274              
275             =head2 para_SH
276              
277             $parser->para_SH( $title )
278              
279             Handles the C<.SH> paragraph, which gives a section header.
280              
281             =cut
282              
283             sub parse_directive_SH
284             {
285 4     4 0 6 my $self = shift;
286 4         8 $self->_change_para( "P" ),
287             $self->para_SH( $self->parse_chunks_flat );
288             }
289              
290             =head2 para_SS
291              
292             $parser->para_SS( $title )
293              
294             Handles the C<.SS> paragraph, which gives a sub-section header.
295              
296             =cut
297              
298             sub parse_directive_SS
299             {
300 0     0 0 0 my $self = shift;
301 0         0 $self->_change_para( "P" ),
302             $self->para_SS( $self->parse_chunks_flat );
303             }
304              
305             =head2 para_TP
306              
307             $parser->para_TP( $opts )
308              
309             Handles a C<.TP> paragraph, which gives a term definition.
310              
311             =cut
312              
313             sub parse_directive_TP
314             {
315 4     4 0 7 my $self = shift;
316 4         12 $self->_change_para( "TP" );
317             }
318              
319             =head2 para_IP
320              
321             $parser->para_IP( $opts )
322              
323             Handles a C<.IP> paragraph, which is indented like the definition part of a
324             C<.TP> paragraph.
325              
326             =cut
327              
328             sub parse_directive_IP
329             {
330 3     3 0 5 my $self = shift;
331 3         10 $self->_change_para( "IP" );
332              
333 3 100       14 if( defined( my $marker = $self->maybe( "token_chunk" ) ) ) {
334 2         20 $self->_change_para_options( marker => $marker );
335             }
336 3 100       68 if( defined( my $indent = $self->maybe( "token_chunk" ) ) ) {
337 2         5 $self->_change_para_options( indent => $indent );
338             }
339             }
340              
341             =head2 para_P
342              
343             $parser->para_P( $opts )
344              
345             Handles the C<.P>, C<.PP> or C<.LP> paragraphs, which are all synonyms for a
346             plain paragraph content.
347              
348             =cut
349              
350             sub parse_directive_P
351             {
352 11     11 0 16 my $self = shift;
353 11         23 $self->_change_para( "P" );
354             }
355              
356             {
357 14     14   118 no warnings 'once';
  14         31  
  14         19596  
358             *parse_directive_PP = *parse_directive_LP = \&parse_directive_P;
359             }
360              
361             =head2 para_EX
362              
363             $parser->para_EX( $opts )
364              
365             Handles the C<.EX> paragraph, which is example text; intended to be rendered
366             in a fixed-width font without filling.
367              
368             =cut
369              
370             sub parse_directive_EX
371             {
372 2     2 0 3 my $self = shift;
373 2         6 $self->_push_para( "EX" );
374             }
375              
376             sub parse_directive_EE
377             {
378 2     2 0 3 my $self = shift;
379 2         10 $self->_pop_para( "EX" );
380             }
381              
382             sub parse_directive_RS
383             {
384 0     0 0 0 my $self = shift;
385 0 0       0 if( defined( my $indent = $self->maybe( "token_chunk" ) ) ) {
386 0         0 $self->_change_para_options( indent => $indent );
387             }
388             else {
389 0         0 $self->_change_para_options( indent => "4n" );
390             }
391             }
392              
393             sub parse_directive_RE
394             {
395 0     0 0 0 my $self = shift;
396 0         0 $self->_change_para_options( indent => "0" );
397             }
398              
399             sub parse_directive_br
400             {
401 0     0 0 0 my $self = shift;
402 0         0 $self->entity_br;
403             }
404              
405             sub parse_directive_fi
406             {
407 2     2 0 4 my $self = shift;
408 2         7 $self->_change_para_options( filling => 1 );
409             }
410              
411             sub parse_directive_in
412             {
413 0     0 0 0 my $self = shift;
414              
415 0         0 my @ret;
416 0         0 my $indent = 0;
417              
418             $self->maybe( sub {
419 0     0   0 $indent = $self->expect( qr/[+-]?\d+[n]?/ );
420 0         0 } );
421              
422 0         0 $self->_change_para_options( indent => $indent );
423             }
424              
425             sub parse_directive_nf
426             {
427 5     5 0 8 my $self = shift;
428 5         11 $self->_change_para_options( filling => 0 );
429             }
430              
431             sub parse_directive_sp
432             {
433 1     1 0 2 my $self = shift;
434 1         5 $self->entity_sp;
435             }
436              
437             sub parse_plaintext
438             {
439 48     48 0 68 my $self = shift;
440              
441 48         145 $self->_flush_para;
442              
443             $self->sequence_of(
444             sub { $self->any_of(
445 69         888 sub { my $esc = ( $self->expect( qr/\\(?|\((..)|(.))/ ) )[1];
446 15         662 $self->commit;
447 15         100 my @chunks = $self->parse_escape( $esc );
448 15         70 $self->chunk( $_, font => $self->{fonts}[-1], size => 0 ) for @chunks;
449             },
450 54         3807 sub { $self->chunk( $self->substring_before( qr/[\\\n]/ ), font => $self->{fonts}[-1], size => 0 ) },
451 69     69   3070 ); }
452 48         223 );
453             }
454              
455             sub parse_escape
456             {
457 16     16 0 21 my $self = shift;
458 16         26 my ( $esc ) = @_;
459              
460 16         29 my $meth = "parse_escape_$esc";
461 16 100 100     77 $meth = sprintf "parse_escape_x%v02X", $esc if length($esc) == 1 and $esc =~ m/[^A-Za-z0-9]/;
462 16 100       34 $meth = "parse_escape_char" if length($esc) > 1;
463 16 50       69 $meth = $self->can( $meth ) or
464             $self->fail( "Unrecognised escape sequence \\$esc" );
465 16         35 return $self->$meth( $esc );
466             }
467              
468             sub parse_escape_x2D # \-
469             {
470 1     1 0 2 my $self = shift;
471              
472             # TODO: Unicode minus sign?
473 1         14 return "-";
474             }
475              
476             # Ignore the "italic corrections" for now
477             *parse_escape_x2C = # \,
478             *parse_escape_x2F = # \/
479 0     0   0 sub { return };
480              
481             # The "empty" character
482             sub parse_escape_x26 # \&
483             {
484 1     1 0 2 my $self = shift;
485              
486 1         2 return "";
487             }
488              
489             *parse_escape_e = *parse_escape_E = *parse_escape_x5C = sub {
490 0     0   0 my $self = shift;
491              
492 0         0 return "\\";
493             };
494              
495             sub parse_escape_f
496             {
497 11     11 0 13 my $self = shift;
498              
499             $self->any_of(
500 11     11   157 sub { $self->expect( qr/P/ );
501 2         70 $self->commit;
502 2 50       10 @{ $self->{fonts} } > 1 or $self->fail( "Cannot \\fP without a \\f font defined" );
  2         7  
503 2         3 pop @{ $self->{fonts} }; },
  2         8  
504 9     9   564 sub { push @{ $self->{fonts} }, ( $self->expect( qr/([A-Z])/ ) )[1]; },
  9         28  
505 1     1   62 sub { push @{ $self->{fonts} }, ( $self->expect( qr/\((..)/ ) )[1]; },
  1         5  
506 11         76 );
507              
508 11         522 return; # empty
509             }
510              
511             # TODO: Vastly expand this table
512             my %chars = (
513             'aq' => q('),
514             'bu' => '•',
515             'co' => '©',
516             );
517              
518             sub parse_escape_char
519             {
520 3     3 0 4 my $self = shift;
521 3         7 my ( $name ) = @_;
522              
523 3   33     18 my $char = $chars{$name} // "";
524              
525 3         10 return $char;
526             }
527              
528             sub _change_para
529             {
530 27     27   40 my $self = shift;
531 27         45 my ( $mode ) = @_;
532 27         59 $self->_change_para_options( mode => $mode );
533 27         76 $self->{para_flushed} = 0;
534             }
535              
536             sub _change_para_options
537             {
538 83     83   114 my $self = shift;
539 83         225 my %opts = @_;
540              
541 83 100 100     203 if( grep { ($self->{para_options}{$_}//"") ne $opts{$_} } keys %opts ) {
  173         655  
542 35         62 $self->{para_flushed} = 0;
543             }
544              
545 83         336 $self->{para_options}{$_} = $opts{$_} for keys %opts;
546             }
547              
548             sub _flush_para
549             {
550 65     65   89 my $self = shift;
551 65 100       140 if( !$self->{para_flushed} ) {
552 50         76 my $mode = $self->{para_options}{mode};
553 50         71 $self->${\"para_$mode"}( $self->{para_options} );
  50         199  
554 50         274 $self->{para_flushed}++;
555             }
556             else {
557 15         40 $self->join_para;
558             }
559             }
560              
561             sub _push_para
562             {
563 2     2   2 my $self = shift;
564 2         4 my ( $new_mode ) = @_;
565              
566             # Shallow clone
567 2         3 push @{ $self->{para_stack} }, { %{ $self->{para_options} } };
  2         4  
  2         8  
568 2         7 $self->_change_para( $new_mode );
569             }
570              
571             sub _pop_para
572             {
573 2     2   2 my $self = shift;
574 2         4 my ( $expect_mode ) = @_;
575              
576 2 50       86 $self->{para_options}{mode} eq $expect_mode or
577             $self->fail( "Expected current paragraph mode of $expect_mode" );
578              
579 2         4 $self->{para_options} = pop @{ $self->{para_stack} };
  2         8  
580              
581 2         5 $self->_flush_para;
582 2         7 $self->{para_flushed} = 0;
583             }
584              
585             =head1 AUTHOR
586              
587             Paul Evans
588              
589             =cut
590              
591             0x55AA;