File Coverage

blib/lib/Pod/Simple/Pandoc.pm
Criterion Covered Total %
statement 38 224 16.9
branch 0 100 0.0
condition 0 36 0.0
subroutine 13 34 38.2
pod 6 9 66.6
total 57 403 14.1


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