File Coverage

blib/lib/Statocles/App/Perldoc.pm
Criterion Covered Total %
statement 67 79 84.8
branch 20 32 62.5
condition 4 5 80.0
subroutine 9 9 100.0
pod n/a
total 100 125 80.0


line stmt bran cond sub pod time code
1             package Statocles::App::Perldoc;
2             our $VERSION = '0.086';
3             # ABSTRACT: Render documentation for Perl modules
4              
5 2     2   2230 use Statocles::Base 'Class';
  2         48  
  2         15  
6 2     2   14385 use Statocles::Page::Plain;
  2         11  
  2         119  
7 2     2   24 use Scalar::Util qw( blessed );
  2         5  
  2         176  
8 2     2   1081 use Pod::Simple::Search;
  2         11217  
  2         93  
9 2     2   657 use Pod::Simple::XHTML;
  2         17288  
  2         3631  
10             with 'Statocles::App';
11              
12             #pod =attr inc
13             #pod
14             #pod The directories to search for modules. Defaults to @INC.
15             #pod
16             #pod =cut
17              
18             has inc => (
19             is => 'ro',
20             isa => ArrayRef[Path],
21             # We can't check for existence, because @INC might contain nonexistent
22             # directories (I think)
23             default => sub { [ @INC ] },
24             coerce => sub {
25             my ( $args ) = @_;
26             return [ map { Path::Tiny->new( $_ ) } @$args ];
27             },
28             );
29              
30             #pod =attr modules
31             #pod
32             #pod The root modules to find. Required. All child modules will be included. Any module that does
33             #pod not start with one of these strings will not be included.
34             #pod
35             #pod =cut
36              
37             has modules => (
38             is => 'ro',
39             isa => ArrayRef[Str],
40             required => 1,
41             );
42              
43             #pod =attr index_module
44             #pod
45             #pod The module to use for the index page. Required.
46             #pod
47             #pod =cut
48              
49             has index_module => (
50             is => 'ro',
51             isa => Str,
52             required => 1,
53             );
54              
55             #pod =attr weave
56             #pod
57             #pod If true, run the POD through L<Pod::Weaver> before converting to HTML
58             #pod
59             #pod =cut
60              
61             has weave => (
62             is => 'ro',
63             isa => Bool,
64             default => sub { 0 },
65             );
66              
67             #pod =attr weave_config
68             #pod
69             #pod The path to the Pod::Weaver configuration file
70             #pod
71             #pod =cut
72              
73             has weave_config => (
74             is => 'ro',
75             isa => Path,
76             default => sub { './weaver.ini' },
77             coerce => Path->coercion,
78             );
79              
80             #pod =attr template_dir
81             #pod
82             #pod The directory (inside the theme directory) to use for this app's templates.
83             #pod Defaults to C<blog>.
84             #pod
85             #pod =cut
86              
87             has '+template_dir' => (
88             default => 'perldoc',
89             );
90              
91             #pod =method pages
92             #pod
93             #pod my @pages = $app->pages;
94             #pod
95             #pod Render the requested modules as HTML. Returns an array of L<Statocles::Page> objects.
96             #pod
97             #pod =cut
98              
99             sub pages {
100             my ( $self ) = @_;
101             my @dirs = map { "$_" } @{ $self->inc };
102             my $pod_base = 'https://metacpan.org/pod/';
103              
104             my %modules;
105             for my $glob ( @{ $self->modules } ) {
106             %modules = (
107             %modules,
108             %{ Pod::Simple::Search->new->inc(0)->limit_re( qr{^$glob} )->survey( @dirs ) },
109             );
110              
111             # Also check for exact matches, for strange extensions
112             for my $dir ( @dirs ) {
113             my @glob_parts = split /::/, $glob;
114             my $path = Path::Tiny->new( $dir, @glob_parts );
115             if ( $path->is_file ) {
116             $modules{ $glob } = "$path";
117             }
118             }
119             }
120              
121              
122             #; use Data::Dumper;
123             #; say Dumper \%modules;
124              
125             my @pages;
126             for my $module ( keys %modules ) {
127              
128             my $path = $modules{ $module };
129             #; use Data::Dumper;
130             #; say Dumper $path;
131              
132             # Weave the POD before trying to make HTML
133             my $pod = $self->weave
134             ? $self->_weave_module( $path )
135             : Path::Tiny->new( $path )->slurp
136             ;
137              
138             my $parser = Pod::Simple::XHTML->new;
139             $parser->perldoc_url_prefix( $pod_base );
140             $parser->$_('') for qw( html_header html_footer );
141             $parser->output_string( \(my $parser_output) );
142             $parser->parse_string_document( $pod );
143             #; say $parser_output;
144              
145             my $dom = Mojo::DOM->new( $parser_output );
146             for my $node ( $dom->find( 'a[href]' )->each ) {
147             my $href = $node->attr( 'href' );
148              
149             # Rewrite links for modules that we will be serving locally
150             if ( grep { $href =~ /^$pod_base$_/ } @{ $self->modules } ) {
151             my ( $module, $section ) = $href =~ /^$pod_base([^#]+)(?:\#(.*))?$/;
152             my $url = $self->url( $self->_module_href( $module ) );
153             $node->attr( href => $section ? join( "#", $url, $section ) : $url );
154             }
155             # Add rel="external" for remaining external links
156             elsif ( $href =~ m{(?:[^:]+:)?//} ) {
157             $node->attr( rel => 'external' );
158             }
159              
160             }
161              
162             my $source_path = "$module/source.html";
163             $source_path =~ s{::}{/}g;
164              
165             my ( @parts ) = split m{::}, $module;
166             my @crumbtrail;
167             for my $i ( 0..$#parts ) {
168             my $trail_module = join "::", @parts[0..$i];
169             if ( $modules{ $trail_module } ) {
170             push @crumbtrail, {
171             text => $parts[ $i ],
172             href => $self->url( $self->_module_href( $trail_module ) ),
173             };
174             }
175             else {
176             push @crumbtrail, {
177             text => $parts[ $i ],
178             };
179             }
180             }
181              
182             my %page_args = (
183             layout => $self->template( 'layout.html' ),
184             template => $self->template( 'pod.html' ),
185             title => $module,
186             content => "$dom",
187             app => $self,
188             path => $self->_module_href( $module ),
189             data => {
190             source_path => $self->url( $source_path ),
191             crumbtrail => \@crumbtrail,
192             },
193             );
194              
195             if ( $module eq $self->index_module ) {
196             unshift @pages, Statocles::Page::Plain->new( %page_args );
197             }
198             else {
199             push @pages, Statocles::Page::Plain->new( %page_args );
200             }
201              
202             # Add the source as a text file
203             push @pages, Statocles::Page::Plain->new(
204             path => $source_path,
205             layout => $self->template( 'layout.html' ),
206             template => $self->template( 'source.html' ),
207             title => "$module (source)",
208             content => Path::Tiny->new( $path )->slurp,
209             app => $self,
210             data => {
211             doc_path => $self->url( $page_args{path} ),
212             crumbtrail => \@crumbtrail,
213             },
214             );
215              
216             }
217              
218             return @pages;
219             }
220              
221             sub _module_href {
222 28     28   4098 my ( $self, $module ) = @_;
223 28 100       106 if ( $module eq $self->index_module ) {
224 8         38 return '/index.html';
225             }
226              
227 20         40 my $page_url = "$module/index.html";
228 20         45 $page_url =~ s{::}{/}g;
229 20         75 return $page_url;
230             }
231              
232             # Run Pod::Weaver on the POD in the given path
233             sub _weave_module {
234 5     5   22 my ( $self, $path ) = @_;
235              
236             # Oh... My... GOD...
237 5         10 my %errors;
238 5 50       14 if ( !eval { require Pod::Weaver; 1; } ) {
  5         53  
  5         22  
239 0         0 $errors{ 'Pod::Weaver' } = $@;
240             }
241 5 50       11 if ( !eval { require PPI; 1; } ) {
  5         508  
  5         74745  
242 0         0 $errors{ 'PPI' } = $@;
243             }
244 5 50       13 if ( !eval { require Pod::Elemental; 1; } ) {
  5         31  
  5         22  
245 0         0 $errors{ 'Pod::Elemental' } = $@;
246             }
247 5 50       15 if ( !eval { require Encode; 1; } ) {
  5         22  
  5         18  
248 0         0 $errors{ 'Encode' } = $@;
249             }
250              
251             # Pod::Weaver 4.014 shipped with a bug that causes problems unless
252             # we have a LEGAL section, which we do not presently allow users to
253             # set. So warn them to upgrade if they have this version
254 5 50       20 if ( $Pod::Weaver::VERSION == 4.014 ) {
255 0         0 $errors{ 'Pod::Weaver' } = q{Pod::Weaver version 4.014 has a bug that will cause a fatal error when a LEGAL section isn't available. Please upgrade to version 4.015 or later.};
256             }
257              
258 5 50       19 if ( keys %errors ) {
259             die "Cannot weave POD: Error loading modules "
260 0         0 . join( "\n", map { "$_: $errors{$_}" } keys %errors )
  0         0  
261             ;
262             }
263              
264             # Check for a config and give a friendly error message if missing.
265             # The default exception thrown by a missing config is very difficult
266             # to understand out of context
267 5 100       48 if ( !$self->weave_config->parent->child( 'weaver.ini' )->is_file ) {
268 1         208 die sprintf q{Cannot find Pod::Weaver config in "%s". Missing "weaver.ini" file?},
269             $self->weave_config->parent;
270             }
271              
272 4         433 my $perl_utf8 = Encode::encode( 'utf-8', Path::Tiny->new( $path )->slurp, Encode::FB_CROAK );
273 4 50       1106 my $ppi_document = PPI::Document->new( \$perl_utf8 ) or die PPI::Document->errstr;
274              
275             ### Copy/paste from Pod::Elemental::PerlMunger
276             my $code_elems = $ppi_document->find(
277             sub {
278             return
279 32 100   32   283 if grep { $_[ 1 ]->isa( "PPI::Token::$_" ) }
  192         522  
280             qw(Comment Pod Whitespace Separator Data End);
281 8         15 return 1;
282             }
283 4         6231 );
284              
285 4   100     57 $code_elems ||= [];
286 4         10 my @pod_tokens;
287              
288 4         17 my @queue = $ppi_document->children;
289 4         36 while ( my $element = shift @queue ) {
290 32 100       92 if ( $element->isa( 'PPI::Token::Pod' ) ) {
291             # save the text for use in building the Pod-only document
292 4         18 push @pod_tokens, "$element";
293             }
294              
295 32 100 66     187 if ( blessed $element && $element->isa( 'PPI::Node' ) ) {
296             # Depth-first keeps the queue size down
297 2         8 unshift @queue, $element->children;
298             }
299             }
300              
301             ## Check for any problems, like POD inside of heredoc or strings
302             my $finder = sub {
303 32     32   282 my $node = $_[ 1 ];
304             return 0
305 32 50       40 unless grep { $node->isa( $_ ) }
  96         243  
306             qw( PPI::Token::Quote PPI::Token::QuoteLike PPI::Token::HereDoc );
307 0 0       0 return 1 if $node->content =~ /^=[a-z]/m;
308 0         0 return 0;
309 4         16 };
310              
311 4 50       18 if ( $ppi_document->find_first( $finder ) ) {
312 0         0 warn "can't invoke Pod::Weaver on '$path': There is POD in string literals";
313 0         0 return '';
314             }
315              
316 4         51 my $pod_str = join "\n", @pod_tokens;
317 4         36 my $pod_document = Pod::Elemental->read_string( $pod_str );
318              
319             ### MUNGE THE POD HERE!
320              
321 4         14477 my $weaved_doc;
322 4         7 eval {
323 4         25 my $weaver = Pod::Weaver->new_from_config(
324             { root => $self->weave_config->parent->stringify },
325             );
326 4         857140 $weaved_doc = $weaver->weave_document({
327             pod_document => $pod_document,
328             ppi_document => $ppi_document,
329             });
330             };
331              
332 4 50       64032 if ( $@ ) {
333 0         0 die sprintf q{Error weaving POD for path "%s": %s}, $path, $@;
334             }
335              
336             ### END MUNGE THE POD
337              
338 4         25 my $pod_text = $weaved_doc->as_pod_string;
339              
340             #; say $pod_text;
341 4         2833 return $pod_text;
342             }
343              
344             1;
345              
346             __END__
347              
348             =pod
349              
350             =encoding UTF-8
351              
352             =head1 NAME
353              
354             Statocles::App::Perldoc - Render documentation for Perl modules
355              
356             =head1 VERSION
357              
358             version 0.086
359              
360             =head1 DESCRIPTION
361              
362             This application generates HTML from the POD in the requested modules.
363              
364             =head1 ATTRIBUTES
365              
366             =head2 inc
367              
368             The directories to search for modules. Defaults to @INC.
369              
370             =head2 modules
371              
372             The root modules to find. Required. All child modules will be included. Any module that does
373             not start with one of these strings will not be included.
374              
375             =head2 index_module
376              
377             The module to use for the index page. Required.
378              
379             =head2 weave
380              
381             If true, run the POD through L<Pod::Weaver> before converting to HTML
382              
383             =head2 weave_config
384              
385             The path to the Pod::Weaver configuration file
386              
387             =head2 template_dir
388              
389             The directory (inside the theme directory) to use for this app's templates.
390             Defaults to C<blog>.
391              
392             =head1 METHODS
393              
394             =head2 pages
395              
396             my @pages = $app->pages;
397              
398             Render the requested modules as HTML. Returns an array of L<Statocles::Page> objects.
399              
400             =head1 AUTHOR
401              
402             Doug Bell <preaction@cpan.org>
403              
404             =head1 COPYRIGHT AND LICENSE
405              
406             This software is copyright (c) 2016 by Doug Bell.
407              
408             This is free software; you can redistribute it and/or modify it under
409             the same terms as the Perl 5 programming language system itself.
410              
411             =cut