File Coverage

blib/lib/Pod/Simple/Pandoc.pm
Criterion Covered Total %
statement 185 209 88.5
branch 62 86 72.0
condition 12 24 50.0
subroutine 32 34 94.1
pod 6 9 66.6
total 297 362 82.0


line stmt bran cond sub pod time code
1             package Pod::Simple::Pandoc;
2 7     7   186682 use strict;
  7         11  
  7         182  
3 7     7   24 use warnings;
  7         10  
  7         171  
4 7     7   131 use 5.010;
  7         15  
5              
6             our $VERSION = '0.346.0';
7              
8 7     7   3300 use Pod::Simple::SimpleTree;
  7         86394  
  7         205  
9 7     7   3011 use Pandoc::Elements;
  7         338806  
  7         1634  
10 7     7   3857 use Pandoc::Filter::HeaderIdentifiers;
  7         20680  
  7         505  
11 7     7   2587 use Pod::Pandoc::Modules;
  7         19  
  7         255  
12 7     7   52 use Pandoc;
  7         12  
  7         50  
13 7     7   555 use File::Find ();
  7         8  
  7         89  
14 7     7   22 use File::Spec;
  7         9  
  7         118  
15 7     7   21 use IPC::Run3;
  7         9  
  7         275  
16 7     7   27 use Carp;
  7         8  
  7         247  
17 7     7   25 use utf8;
  7         7  
  7         46  
18              
19             sub new {
20 2     2 0 28 my ( $class, %opt ) = @_;
21              
22 2   50     15 $opt{parse} ||= [];
23 2 50       10 if ( $opt{parse} eq '*' ) {
24 0         0 $opt{parse} = [ pandoc->require('1.12')->input_formats ];
25             }
26              
27 2   50     15 $opt{podurl} //= 'https://metacpan.org/pod/';
28              
29 2         8 bless \%opt, $class;
30             }
31              
32             sub _parser {
33 10     10   14 my $self = shift;
34              
35 10         97 my $parser = Pod::Simple::SimpleTree->new;
36 10         276 $parser->nix_X_codes(1); # ignore X<...> codes
37 10         126 $parser->nbsp_for_S(1); # map S<...> to U+00A0 (non-breaking space)
38 10         79 $parser->merge_text(1); # emit text nodes combined
39 10         65 $parser->no_errata_section(1); # omit errata section
40 10         66 $parser->complain_stderr(1); # TODO: configure
41 10         66 $parser->accept_target('*'); # include all data sections
42              
43             # remove shortest leading whitespace string from verbatim sections
44             $parser->strip_verbatim_indent(
45             sub {
46 7     7   103493 my $indent = length $_[0][1];
47 7         11 for ( @{ $_[0] } ) {
  7         21  
48 47         52 $_ =~ /^(\s*)/;
49 47 100       90 $indent = length($1) if length($1) < $indent;
50             }
51 7         25 ' ' x $indent;
52             }
53 10         232 );
54              
55 10         68 return $parser;
56             }
57              
58             sub parse_file {
59 10     10 1 151624 my ( $self, $file ) = @_;
60              
61             # Pod::Simple::parse_file does not detect this
62 10 50       177 croak "Can't use directory as a source for parse_file" if -d $file;
63              
64 10         32 my $doc = $self->parse_tree( $self->_parser->parse_file($file)->root );
65              
66 8 50 33     871 if ( !ref $file and $file ne '-' ) {
67 8         24 $doc->meta->{file} = MetaString($file);
68             }
69              
70 8         136 $doc;
71             }
72              
73             sub parse_module {
74 1     1 1 1118 my ( $self, $name ) = @_;
75              
76             # map module name to name
77 1         7 run3 [ 'perldoc', '-lm', $name ], undef, \$name;
78 1 50       268872 croak $? if $?;
79 0         0 chomp $name;
80              
81 0         0 $self->parse_file($name);
82             }
83              
84             sub parse_string {
85 0     0 1 0 my ( $self, $string ) = @_;
86 0         0 $self->parse_tree( $self->_parser->parse_string_document($string)->root );
87             }
88              
89             sub parse_tree {
90 8     8 0 19151 my $doc = Pandoc::Filter::HeaderIdentifiers->new->apply( _pod_element(@_) );
91              
92 8         727218 my $sections = $doc->outline(1)->{sections};
93 8 100       3307 if ( my ($name) = grep { $_->{header}->string eq 'NAME' } @$sections ) {
  27         5083  
94              
95             # TODO: support formatting
96 5         1978 my $text = $name->{blocks}->[0]->string;
97 5         6629 my ( $title, $subtitle ) = $text =~ m{^\s*([^ ]+)\s*[:-]*\s*(.+)};
98 5 50       34 $doc->meta->{title} = MetaString($title) if $title;
99 5 50       166 $doc->meta->{subtitle} = MetaString($subtitle) if $subtitle;
100             }
101              
102 8         81 $doc;
103             }
104              
105             sub parse_and_merge {
106 0     0 1 0 my ( $self, @input ) = @_;
107              
108 0         0 my $doc;
109              
110 0         0 foreach my $file (@input) {
111              
112 0 0 0     0 my $cur =
113             ( $file ne '-' and not -e $file )
114             ? $self->parse_module($file)
115             : $self->parse_file($file);
116              
117 0 0       0 if ($doc) {
118 0         0 push @{ $doc->content }, @{ $cur->content };
  0         0  
  0         0  
119             }
120             else {
121 0         0 $doc = $cur;
122             }
123             }
124              
125 0 0       0 return unless $doc;
126              
127 0 0       0 if ( @input > 1 ) {
128 0         0 $doc->meta->{file} = MetaList [ map { MetaString $_ } @input ];
  0         0  
129             }
130              
131 0         0 return $doc;
132             }
133              
134             sub is_perl_file {
135 6     6 0 8 my $file = shift;
136 6 100       22 return 1 if $file =~ /\.(pm|pod)$/;
137 4 100       44 if ( -x $file ) {
138 2 50       40 open( my $fh, '<', $file ) or return;
139 2 100 100     49 return 1 if $fh and ( <$fh> // '' ) =~ /^#!.*perl/;
      66        
140             }
141 3         89 0;
142             }
143              
144             sub parse_dir {
145 1     1 1 1 my ( $parser, $directory ) = @_;
146 1         2 my $files = {};
147              
148             File::Find::find(
149             {
150             no_chdir => 1,
151             wanted => sub {
152 6     6   8 my $file = $_;
153 6 100       8 return unless is_perl_file($file);
154 3         12 my $doc = $parser->parse_file($file);
155 3         212 my $base = File::Spec->abs2rel( $directory, $file );
156 3         13 $base =~ s/\.\.$//;
157 3         8 $doc->meta->{base} = MetaString $base;
158 3         52 $files->{$file} = $doc;
159             }
160             },
161 1         92 $directory
162             );
163              
164 1         6 $files;
165             }
166              
167             sub parse_modules {
168 1     1 1 892 my ( $parser, $dir, %opt ) = @_;
169              
170 1         9 my $modules = Pod::Pandoc::Modules->new;
171 1 50       12 return $modules unless -d $dir;
172              
173 1         4 my $files = $parser->parse_dir($dir);
174 1         6 foreach my $file ( sort keys %$files ) {
175 3         6 my $doc = $files->{$file};
176 3         99 my $module = File::Spec->abs2rel( $file, $dir );
177 3         10 $module =~ s{\.(pm|pod)$}{}g;
178 3         4 $module =~ s{/}{::}g;
179 3 100 66     9 if ( ( $doc->metavalue('title') // $module ) eq $module ) {
180 2         25 my $old = $modules->{$module};
181 2 50       9 my $skipped = $modules->add( $module => $doc ) ? $old : $doc;
182 2 100 66     9 if ( $skipped and not $opt{quiet} ) {
183             warn $skipped->metavalue('file')
184             . " skipped for "
185 1         3 . $modules->{$module}->metavalue('file') . "\n";
186             }
187             }
188             else {
189 1 50       70 warn "$file NAME does not match module\n" unless $opt{quiet};
190             }
191             }
192              
193 1         69 $modules;
194             }
195              
196             my %POD_ELEMENT_TYPES = (
197             Document => sub {
198             Document {}, [ _pod_content(@_) ];
199             },
200             Para => sub {
201             Para [ _pod_content(@_) ];
202             },
203             I => sub {
204             Emph [ _pod_content(@_) ];
205             },
206             B => sub {
207             Strong [ _pod_content(@_) ];
208             },
209             L => \&_pod_link,
210             C => sub {
211             Code attributes {}, _pod_flatten(@_);
212             },
213             F => sub {
214             Code attributes { classes => ['filename'] }, _pod_flatten(@_);
215             },
216             head1 => sub {
217             Header 1, attributes {}, [ _pod_content(@_) ];
218             },
219             head2 => sub {
220             Header 2, attributes {}, [ _pod_content(@_) ];
221             },
222             head3 => sub {
223             Header 3, attributes {}, [ _pod_content(@_) ];
224             },
225             head4 => sub {
226             Header 4, attributes {}, [ _pod_content(@_) ];
227             },
228             Verbatim => sub {
229             CodeBlock attributes {}, _pod_flatten(@_);
230             },
231             'over-bullet' => sub {
232             BulletList [ _pod_list(@_) ];
233             },
234             'over-number' => sub {
235             OrderedList [ 1, DefaultStyle, DefaultDelim ], [ _pod_list(@_) ];
236             },
237             'over-text' => sub {
238             DefinitionList [ _pod_list(@_) ];
239             },
240             'over-block' => sub {
241             BlockQuote [ _pod_content(@_) ];
242             },
243             'for' => \&_pod_data,
244             );
245              
246             # option --smart
247             sub _str {
248 2229     2229   9644 my $s = shift;
249 2229         1794 $s =~ s/\.\.\./…/g;
250 2229         2553 Str $s;
251             }
252              
253             # map a single element or text to a list of Pandoc elements
254             sub _pod_element {
255 1062     1062   1005 my ( $self, $element ) = @_;
256              
257 1062 100       1237 if ( ref $element ) {
258 508 50       1135 my $type = $POD_ELEMENT_TYPES{ $element->[0] } or return;
259 508         651 $type->( $self, $element );
260             }
261             else {
262 554         367 my $n = 0;
263 554 100       1836 map { $n++ ? ( Space, _str($_) ) : _str($_) }
  2229         11795  
264             split( /\s+/, $element, -1 );
265             }
266             }
267              
268             # map the content of a Pod element to a list of Pandoc elements
269             sub _pod_content {
270 350     350   26307 my ( $self, $element ) = @_;
271 350         281 my $length = scalar @$element;
272 350         563 map { _pod_element( $self, $_ ) } @$element[ 2 .. ( $length - 1 ) ];
  1029         8145  
273             }
274              
275             # stringify the content of an element
276             sub _pod_flatten {
277 167     167   23140 my $string = '';
278 167         114 my $walk;
279             $walk = sub {
280 173     173   152 my ($element) = @_;
281 173         133 my $n = scalar @$element;
282 173         336 for ( @$element[ 2 .. $n - 1 ] ) {
283 176 100       209 if ( ref $_ ) {
284 6         17 $walk->($_);
285             }
286             else {
287 170         307 $string .= $_;
288             }
289             }
290 167         477 };
291 167         229 $walk->( $_[1] );
292              
293 167         380 return $string;
294             }
295              
296             # map link
297             sub _pod_link {
298 115     115   101 my ( $self, $link ) = @_;
299 115         172 my $type = $link->[1]{type};
300 115         132 my $to = $link->[1]{to};
301 115         104 my $section = $link->[1]{section};
302 115         86 my $url = '';
303              
304 115 100       274 if ( $type eq 'url' ) {
    100          
    50          
305 10         33 $url = "$to";
306             }
307             elsif ( $type eq 'man' ) {
308 9 50       23 if ( $to =~ /^([^(]+)(?:[(](\d+)[)])?$/ ) {
309              
310             # TODO: configure MAN_URL, e.g.
311             # http://man7.org/linux/man-pages/man{section}/{name}.{section}.html
312 9         142 $url = "http://linux.die.net/man/$2/$1";
313              
314             # TODO: add section to URL if given
315             }
316             }
317             elsif ( $type eq 'pod' ) {
318 96 100       223 if ($to) {
319 87         1413 $url = $self->{podurl} . $to;
320             }
321 96 100       673 if ($section) {
322 15 100       229 $section = header_identifier("$section") unless $to; # internal link
323 15         4205 $url .= "#" . $section;
324             }
325             }
326              
327 115         337 return Link attributes {}, [ _pod_content( $self, $link ) ], [ $url, '' ];
328             }
329              
330             # map data section
331             sub _pod_data {
332 15     15   16 my ( $self, $element ) = @_;
333 15         33 my $target = lc( $element->[1]{target} );
334              
335 15         13 my $length = scalar @$element;
336 18         34 my $content = join "\n\n", map { $_->[2] }
337 15         28 grep { $_->[0] eq 'Data' } @$element[ 2 .. $length - 1 ];
  18         36  
338              
339             # cleanup HTML and Tex blocks
340 15 100       53 if ( $target eq 'html' ) {
    100          
341 6 100       37 $content = "
$content
" if $content !~ /^<.+>$/s;
342             }
343             elsif ( $target =~ /^(la)?tex$/ ) {
344              
345             # TODO: more intelligent check & grouping, especiall at the end
346 6 50       20 $content = "\\begingroup $content \\endgroup" if $content !~ /^[\\{]/;
347 6         40 $target = 'tex';
348             }
349              
350             # parse and insert known formats if requested
351 15 100       26 my $format = $target eq 'tex' ? 'latex' : $target;
352 15 50       15 if ( grep { $format eq $_ } @{ $self->{parse} } ) {
  0         0  
  15         37  
353 0         0 utf8::decode($content);
354 0         0 my $doc = pandoc->parse( $format => $content, '--smart' );
355 0         0 return @{ $doc->content };
  0         0  
356             }
357              
358 15         44 RawBlock( $target, "$content\n" );
359              
360             # TODO: add Null element to not merge with following content
361             }
362              
363             # map a list (any kind)
364             sub _pod_list {
365 13     13   106 my ( $self, $element ) = @_;
366 13         16 my $length = scalar @$element;
367              
368 13         29 my $deflist = $element->[2][0] eq 'item-text';
369 13         12 my @list;
370 13         18 my $item = [];
371              
372             my $push_item = sub {
373 50 100   50   80 return unless @$item;
374 37 100       49 if ($deflist) {
375 25         26 my $term = shift @$item;
376 25         413 push @list, [ $term->content, [$item] ];
377             }
378             else {
379 12         17 push @list, $item;
380             }
381 13         47 };
382              
383 13         40 foreach my $e ( @$element[ 2 .. $length - 1 ] ) {
384 62         745 my $type = $e->[0];
385 62 100       160 if ( $type =~ /^item-(number|bullet|text)$/ ) {
386 37         49 $push_item->();
387 37         145 $item = [ Plain [ _pod_content( $self, $e ) ] ];
388             }
389             else {
390 25 50 33     100 if ( @$item == 1 and $item->[0]->name eq 'Plain' ) {
391              
392             # first block element in item should better be Paragraph
393 25         489 $item->[0] = Para $item->[0]->content;
394             }
395 25         269 push @$item, _pod_element( $self, $e );
396             }
397             }
398 13         206 $push_item->();
399              
400             # BulletList/OrderedList: [ @blocks ], ...
401             # DefinitionList: [ [ @inlines ], [ @blocks ] ], ...
402 13         111 return @list;
403             }
404              
405             1;
406             __END__