File Coverage

blib/lib/App/sdview/Parser/Pod.pm
Criterion Covered Total %
statement 211 227 92.9
branch 30 38 78.9
condition 20 30 66.6
subroutine 38 40 95.0
pod 1 14 7.1
total 300 349 85.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, 2021-2024 -- leonerd@leonerd.org.uk
5              
6 4     4   727997 use v5.26;
  4         18  
7 4     4   32 use warnings;
  4         7  
  4         311  
8              
9 4     4   2419 use Object::Pad 0.807;
  4         33652  
  4         365  
10              
11             package App::sdview::Parser::Pod 0.20;
12             class App::sdview::Parser::Pod :strict(params);
13              
14 4     4   4539 inherit Pod::Simple::Methody;
  4         304273  
  4         475  
15              
16 3     3   1793 apply App::sdview::Parser;
  3         13  
  3         211  
17              
18 4     4   6586 use List::Keywords qw( any all );
  4         12192  
  4         33  
19 4     4   421 use List::Util qw( min );
  4         9  
  4         383  
20              
21 4     4   29 use String::Tagged;
  4         9  
  4         162  
22              
23 4     4   26 use constant format => "Pod";
  4         8  
  4         492  
24 4     4   57 use constant sort_order => 10;
  4         11  
  4         21906  
25              
26             =head1 NAME
27              
28             C - parse Pod files for L
29              
30             =head1 SYNOPSIS
31              
32             $ sdview README.pod
33              
34             $ sdview -f Pod my-document
35              
36             =head1 DESCRIPTION
37              
38             This parser module adds to L the ability to parse input text in
39             Pod formatting.
40              
41             It uses L as its driving parser.
42              
43             The C...E> formatting code is handled by converting inner spaces to
44             non-breaking spaces (U+00A0) characters in the returned string.
45              
46             By default, verbatim blocks are scanned for likely patterns that indicate perl
47             code, and emitted with the C field set to C if it looks
48             plausible. This can be overridden by embedded C<=code> or C<=for highlighter>
49             directives; see below.
50              
51             =head2 Extensions
52              
53             Partly as an experiment into how to handle possible future features of the Pod
54             spec, the following extensions are recognised:
55              
56             =over 4
57              
58             =item *
59              
60             Inline formatting code C...E> to request underline formatting.
61              
62             =item *
63              
64             C<=code> directive to set the highlighter language name for the next verbatim
65             paragraph.
66              
67             =item *
68              
69             Also follows the C<=for highlighter ...> spec used by
70             L for
71             setting the language name for following verbatim paragraphs.
72              
73             =item *
74              
75             Tables are I supported according to the suggestion given in
76             L,
77             within a section marked C<=begin table> or C<=begin table md>.
78              
79             =item *
80              
81             Tables are also partially supported by a format similar to mediawiki notation,
82             within a section marked C<=begin table mediawiki>.
83              
84             =back
85              
86             =cut
87              
88 0         0 sub find_file ( $class, $name )
89 0     0 0 0 {
  0         0  
  0         0  
90             # We could use `perldoc -l` but it's slow and noisy when it fails
91 0         0 require Pod::Perldoc;
92 0         0 my ( $found ) = Pod::Perldoc->new->searchfor( 0, $name, @INC );
93 0         0 return $found;
94             }
95              
96 2         5 sub can_parse_file ( $class, $file )
97 2     2 0 12683 {
  2         5  
  2         4  
98 2         30 return $file =~ m/\.pm$|\.pl$|\.pod$/;
99             }
100              
101             ADJUST
102             {
103             $self->accept_target( 'highlighter' );
104             $self->accept_directive_as_data( 'code' );
105              
106             $self->accept_target( 'table' );
107             }
108              
109             field @_indentstack;
110             field @_parastack;
111              
112             field $_curpara;
113              
114             field %_verbatim_options = ( language => "__AUTO__" );
115             field %_next_verbatim_options;
116              
117             field $_conv_nbsp;
118              
119 0     0 1 0 method parse_file ( $fh )
  0         0  
  0         0  
  0         0  
120             {
121 0         0 push @_indentstack, 0;
122 0         0 push @_parastack, [];
123 0         0 $self->SUPER::parse_file( $fh );
124 0         0 return $_parastack[0]->@*;
125             }
126              
127 31     31 0 999 method parse_string ( $str )
  31         121  
  31         77  
  31         59  
128             {
129 31         89 push @_indentstack, 0;
130 31         82 push @_parastack, [];
131 31         209 $self->SUPER::parse_string_document ( $str );
132 31         3231 return $_parastack[0]->@*;
133             }
134              
135             my %PARA_TYPES = (
136             Para => "App::sdview::Para::Plain",
137             Verbatim => "App::sdview::Para::Verbatim",
138             );
139              
140             field $_redirect_text;
141              
142             method start_Document { $self->reset_tags; }
143              
144             method start_head1 { $self->_start_head( 1 ); }
145             method start_head2 { $self->_start_head( 2 ); }
146             method start_head3 { $self->_start_head( 3 ); }
147             method start_head4 { $self->_start_head( 4 ); }
148 11     11   29 method _start_head ( $level )
  11         39  
  11         86  
  11         24  
149             {
150 11         94 push $_parastack[-1]->@*, $_curpara = App::sdview::Para::Heading->new(
151             level => $level,
152             text => String::Tagged->new,
153             );
154 11         78 $self->reset_tags;
155             }
156              
157             method start_code { $self->_start_highlighter( \%_next_verbatim_options ); }
158             method end_code { undef $_redirect_text; }
159              
160 8     8 0 2081 method start_for ( $attrs )
  8         62  
  8         17  
  8         17  
161             {
162 8         27 my $target = $attrs->{target};
163 8 50       63 my $code = $self->can( "start_for_$target" ) or return;
164 8         37 return $self->$code( $attrs );
165             }
166             method end_for { undef $_redirect_text; }
167              
168             method start_for_highlighter { $self->_start_highlighter( \%_verbatim_options ) }
169              
170 6     6 0 12 method start_for_table ( $attrs )
  6         20  
  6         12  
  6         12  
171             {
172 6   50     36 my @spec = split m/\s+/, $attrs->{title} // "";
173 6 50 33     56 $spec[0] = "style=$spec[0]" if @spec and $spec[0] !~ m/=/;
174 6 50       17 my %spec = map { m/^(.*?)=(.*)$/ ? ( $1, $2 ) : () } @spec;
  6         79  
175              
176 6   50     31 my $style = $spec{style} // "md";
177              
178 6 100       50 $style eq "md" and
179             $_redirect_text = \&_handle_text_table_md, return;
180 2 50       19 $style eq "mediawiki" and
181             $_redirect_text = \&_handle_text_table_mediawiki, return;
182              
183 0         0 warn "TODO unrecognised table style $style\n";
184             }
185              
186 3     3   7 method _start_highlighter ( $options )
  3         8  
  3         6  
  3         6  
187             {
188 3     3   4 $_redirect_text = method ( $text ) {
  3         8  
  3         7  
  3         5  
189 3         11 my @args = split m/\s+/, $text;
190 3 100 66     22 $args[0] = "language=$args[0]" if @args and $args[0] !~ m/=/;
191              
192 3         9 %$options = ();
193              
194 3         11 foreach ( @args ) {
195 2 50       19 my ( $key, $val ) = m/^(.*?)=(.*)$/ or next;
196 2         13 $options->{$key} = $val;
197             }
198 3         27 };
199             }
200              
201             method start_S { $_conv_nbsp = 1; }
202             method end_S { undef $_conv_nbsp; }
203              
204             role App::sdview::Parser::Pod::_TagHandler {
205             ADJUST {
206             $self->nix_X_codes( 1 );
207             $self->accept_codes(qw( U ));
208             }
209              
210 161     161 0 433 field %_curtags :reader;
  161     161   903  
        161      
211             method reset_tags { %_curtags = (); }
212              
213             method start_B { $_curtags{bold}++ }
214             method end_B { delete $_curtags{bold} }
215             method start_I { $_curtags{italic}++ }
216             method end_I { delete $_curtags{italic} }
217             method start_U { $_curtags{underline}++ }
218             method end_U { delete $_curtags{underline} }
219             method start_C { $_curtags{monospace}++ }
220             method end_C { delete $_curtags{monospace} }
221             method start_F { $_curtags{file}++ }
222             method end_F { delete $_curtags{file} }
223              
224 6     6 0 374 method start_L ( $attrs )
  6     6   28  
  6     6   12  
  6         11  
225             {
226 6         17 my $uri = $attrs->{to};
227             # TODO: more customizable
228 6 100 66     48 if( defined $uri and $uri !~ m(^\w+://) ) {
229 3         78 $uri = "https://metacpan.org/pod/$uri";
230             }
231 6         205 $_curtags{link} = { uri => $uri };
232             }
233             method end_L { delete $_curtags{link} }
234             }
235              
236             apply App::sdview::Parser::Pod::_TagHandler;
237              
238 1     1 0 348 method start_over_block ( $attrs )
  1         7  
  1         3  
  1         3  
239             {
240 1         7 push @_indentstack, $_indentstack[-1] + $attrs->{indent};
241             }
242             method end_over_block
243             {
244             pop @_indentstack;
245             }
246              
247 3     3 0 1063 method start_over_number ( $attrs ) { $self->_start_over( number => $attrs ); }
  3         16  
  3         9  
  3         7  
  3         17  
248 5     5 0 2156 method start_over_bullet ( $attrs ) { $self->_start_over( bullet => $attrs ); }
  5         28  
  5         11  
  5         12  
  5         28  
249 3     3 0 1083 method start_over_text ( $attrs ) { $self->_start_over( text => $attrs ); }
  3         19  
  3         8  
  3         8  
  3         15  
250 11     11   23 method _start_over ( $type, $attrs )
  11         37  
  11         24  
  11         22  
  11         20  
251             {
252             push $_parastack[-1]->@*, App::sdview::Para::List->new(
253             listtype => $type,
254             indent => $_indentstack[-1] + $attrs->{indent},
255 11         219 );
256 11         32 push @_parastack, [];
257 11         44 undef $_curpara;
258             }
259             method end_over_number { $self->_end_list( number => ); }
260             method end_over_bullet { $self->_end_list( bullet => ); }
261             method end_over_text { $self->_end_list( text => ); }
262 11     11   32 method _end_list ( $type )
  11         35  
  11         26  
  11         20  
263             {
264 11         40 my @items = ( pop @_parastack )->@*;
265 11         91 $_parastack[-1][-1]->push_item( $_ ) for @items;
266             }
267              
268             method start_item_number { $self->_start_item( number => ); }
269             method start_item_bullet { $self->_start_item( bullet => ); }
270 20     20   45 method _start_item ( $type )
  20         61  
  20         38  
  20         34  
271             {
272 20         140 push $_parastack[-1]->@*, $_curpara = App::sdview::Para::ListItem->new(
273             listtype => $type,
274             text => String::Tagged->new,
275             );
276             }
277             method start_item_text
278             {
279             push $_parastack[-1]->@*, $_curpara = App::sdview::Para::ListItem->new(
280             listtype => "text",
281             term => String::Tagged->new,
282             text => String::Tagged->new,
283             );
284             }
285             method end_item_text { $_parastack[-1][-1]->term_done; }
286              
287 38     38 0 21174 method start_Para ( $attrs )
  38         227  
  38         89  
  38         58  
288             {
289 38 100 100     414 if( $_curpara and $_curpara->type eq "item" and $_curpara->listtype eq "text"
      66        
      100        
290             and !length $_curpara->text ) {
291             # just extend the existing para
292             }
293             else {
294 27         200 push $_parastack[-1]->@*, $_curpara = App::sdview::Para::Plain->new(
295             text => String::Tagged->new,
296             indent => $_indentstack[-1],
297             );
298             }
299              
300 38         338 $self->reset_tags;
301             }
302              
303             method start_Verbatim
304             {
305             push $_parastack[-1]->@*, $_curpara = App::sdview::Para::Verbatim->new(
306             text => String::Tagged->new,
307             indent => $_indentstack[-1],
308             ( %_verbatim_options, %_next_verbatim_options ),
309             );
310             $self->reset_tags;
311             %_next_verbatim_options = ();
312             }
313             method end_Verbatim
314             {
315             my $para = $_parastack[-1][-1];
316             my @lines = $para->text->split( qr/\n/ );
317              
318             my $trimlen = min map { m/^(\s*)/; $+[1] } grep { length } @lines;
319              
320             length and $_ = $_->substr( $trimlen, length $_ ) for @lines;
321              
322             my $text = shift @lines;
323             $text .= "\n" . $_ for @lines;
324              
325             my $language = $para->language;
326             if( ( $language // "" ) eq "__AUTO__" ) {
327             # Try to detect the language. It doesn't have to be perfect, just a good
328             # guess is enough.
329             undef $language;
330              
331             if( $text =~ m/^use [A-Za-z_]|^package [A-Za-z_]/ ) {
332             $language = "perl";
333             }
334             elsif( $text =~ m/^(my )?[\$\@%][A-Za-z_]/m ) {
335             $language = "perl";
336             }
337             elsif( $text =~ m/^#!.*\bperl\b/ ) {
338             $language = "perl";
339             }
340             }
341              
342             $_parastack[-1][-1] = (ref $para)->new(
343             text => $text,
344             language => $language,
345             );
346             }
347              
348 133     133 0 5881 method handle_text ( $text )
  133         464  
  133         274  
  133         219  
349             {
350 133 100       452 if( $_redirect_text ) {
351 9         36 return $self->$_redirect_text( $text );
352             }
353              
354 124 100       349 $text =~ s/ /\xA0/g if $_conv_nbsp;
355              
356 124         464 $_curpara->append_text( $text, $self->curtags );
357             }
358              
359 4     4   11 method _handle_text_table_md ( $text )
  4         12  
  4         10  
  4         7  
360             {
361 4 50       29 my @lines = split m/\n/, $text
362             or return;
363              
364 4         9 my @rows;
365 4         50 push @rows, _split_table_row( shift @lines );
366              
367 4         15 my $heading = !!0;
368 4         9 my @align;
369 4         16 my $alignspec = _split_table_row( shift @lines );
370 4 100       20 if( all { $_ =~ m/^(:?)-{3,}(:?)$/ } @$alignspec ) {
  9         84  
371 3         36 $heading = !!1;
372             @align = map {
373 3         11 m/^(:?)-{3,}(:?)$/;
  8         19  
374 8 100 100     107 ( $1 and $2 ) ? "centre" :
    100          
375             ( $2 ) ? "right" :
376             "left";
377             } @$alignspec;
378             }
379             else {
380 1         13 push @rows, $alignspec;
381 1         35 @align = ( "left" ) x scalar @$alignspec;
382             }
383              
384 4         15 push @rows, map { _split_table_row( $_ ) } @lines;
  2         8  
385              
386 4         15 foreach my $row ( @rows ) {
387             @$row = map {
388 7         26 my $colidx = $_;
  17         38  
389 17         196 App::sdview::Para::TableCell->new(
390             align => $align[$colidx],
391             heading => $heading,
392             text => $row->[$colidx],
393             )
394             } keys @$row;
395 7         24 $heading = !!0;
396             }
397              
398 4         64 push $_parastack[-1]->@*, App::sdview::Para::Table->new(
399             rows => \@rows
400             );
401             }
402              
403             sub _split_table_row ( $str )
404 10     10   17 {
  10         26  
  10         17  
405             # Leading/trailing pipes are optional
406 10         65 $str =~ s/^\s*\|//;
407 10         51 $str =~ s/\|\s*$//;
408              
409 10         40 my @cols = split m/\|/, $str;
410 10         113 s/^\s+//, s/\s+$// for @cols;
411              
412             # TODO: Find out why these parsers aren't reusable
413 10         121 $_ = App::sdview::Parser::Pod::_TableCellParser->new->parse_string( $_ ) for @cols;
414              
415 10         48 return \@cols;
416             }
417              
418 2     2   5 method _handle_text_table_mediawiki ( $text )
  2         8  
  2         3  
  2         3  
419             {
420 2 50       14 my @lines = split m/\n/, $text
421             or return;
422              
423 2         5 my @rows;
424 2         6 foreach my $line ( @lines ) {
425 14 100 66     82 $line =~ m/^\|-/ and
426             push @rows, [] and next;
427              
428 11 50 0     59 $line =~ s/^([!|])\s*// or
429             warn "Unsure what to do with line $line" and next;
430 11         27 my $chr = $1;
431 11         21 my $heading = ( $chr eq "!" );
432              
433 11         271 foreach my $cell ( split m/\s*\Q$chr$chr\E\s*/, $line ) {
434 12 100       163 @rows or push @rows, [];
435 12         80 push $rows[-1]->@*, App::sdview::Para::TableCell->new(
436             align => "left", # TODO
437             heading => $heading,
438             text => App::sdview::Parser::Pod::_TableCellParser->new->parse_string( $cell ),
439             );
440             }
441             }
442              
443 2         26 push $_parastack[-1]->@*, App::sdview::Para::Table->new(
444             rows => \@rows,
445             );
446             }
447              
448             class App::sdview::Parser::Pod::_TableCellParser
449             {
450 4     4   47763 inherit Pod::Simple::Methody;
  4         11  
  4         4066  
451             apply App::sdview::Parser::Pod::_TagHandler;
452              
453             field $body;
454              
455 37     37   843 method parse_string ( $str )
  37         119  
  37         76  
  37         50  
456             {
457 37         173 $body = String::Tagged->new;
458              
459             # Protect a leading equals sign
460 37         1798 $str =~ s/^=/E<61>/;
461              
462 37         217 $self->SUPER::parse_string_document( "=pod\n\n$str" );
463              
464 37         6082 return $body;
465             }
466              
467 37     37   23160 method handle_text ( $text )
  37         172  
  37         71  
  37         58  
468             {
469 37         115 $body->append_tagged( $text, $self->curtags );
470             }
471             }
472              
473             =head1 AUTHOR
474              
475             Paul Evans
476              
477             =cut
478              
479             0x55AA;