File Coverage

blib/lib/App/podweaver.pm
Criterion Covered Total %
statement 156 216 72.2
branch 37 102 36.2
condition 12 30 40.0
subroutine 26 32 81.2
pod 6 6 100.0
total 237 386 61.4


line stmt bran cond sub pod time code
1             package App::podweaver;
2              
3             # ABSTRACT: Run Pod::Weaver on the files within a distribution.
4              
5 5     5   1017467 use warnings;
  5         8  
  5         134  
6 5     5   17 use strict;
  5         12  
  5         77  
7              
8 5     5   15 use Carp;
  5         8  
  5         244  
9 5     5   1916 use Config::Tiny;
  5         3788  
  5         114  
10 5     5   2362 use CPAN::Meta;
  5         109611  
  5         138  
11 5     5   2235 use IO::File;
  5         26176  
  5         522  
12 5     5   1713 use File::Copy;
  5         11463  
  5         254  
13 5     5   2146 use File::HomeDir;
  5         20016  
  5         210  
14 5     5   2090 use File::Find::Rule;
  5         28031  
  5         32  
15 5     5   2351 use File::Find::Rule::Perl;
  5         14939  
  5         41  
16 5     5   2299 use File::Find::Rule::VCS;
  5         3773  
  5         27  
17 5     5   2312 use File::Slurp ();
  5         49483  
  5         136  
18 5     5   34 use File::Spec;
  5         10  
  5         125  
19 5     5   2182 use Log::Any qw/$log/;
  5         31013  
  5         24  
20 5     5   11338 use Module::Metadata;
  5         21603  
  5         163  
21 5     5   2037 use Pod::Elemental;
  5         3099712  
  5         40  
22 5     5   1565 use Pod::Elemental::Transformer::Pod5;
  5         7  
  5         97  
23 5     5   2555 use Pod::Weaver;
  5         2591546  
  5         184  
24 5     5   2648 use PPI::Document;
  5         424591  
  5         177  
25 5     5   40 use Try::Tiny;
  5         30  
  5         7517  
26              
27             our $VERSION = '0.99_04';
28              
29             sub FAIL() { 0; }
30             sub SUCCESS_UNCHANGED() { 1; }
31             sub SUCCESS_CHANGED() { 2; }
32              
33             sub weave_file
34             {
35 10     10 1 48178 my ( $self, %input ) = @_;
36 10         22 my ( $file, $no_backup, $write_to_dot_new, $weaver );
37 0         0 my ( $perl, $ppi_document, $pod_after_end, @pod_tokens, $pod_str,
38             $pod_document, %weave_args, $new_pod, $end, $new_perl,
39             $output_file, $backup_file, $fh, $module_info );
40              
41 10 50       54 unless( $file = delete $input{ filename } )
42             {
43 0 0       0 $log->errorf( 'Missing file parameter in args %s', \%input )
44             if $log->is_error();
45 0         0 return( FAIL );
46             }
47 10 50       46 unless( $weaver = delete $input{ weaver } )
48             {
49 0 0       0 $log->errorf( 'Missing weaver parameter in args %s', \%input )
50             if $log->is_error();
51 0         0 return( FAIL );
52             }
53 10         24 $no_backup = delete $input{ no_backup };
54 10         14 $write_to_dot_new = delete $input{ new };
55              
56             # From here and below is mostly hacked out from
57             # Dist::Zilla::Plugin::PodWeaver
58              
59 10         67 $perl = File::Slurp::read_file( $file );
60              
61 10 50       655 unless( $ppi_document = PPI::Document->new( \$perl ) )
62             {
63 0 0       0 $log->errorf( "PPI error in '%s': %s", $file, PPI::Document->errstr() )
64             if $log->is_error();
65 0         0 return( FAIL );
66             }
67              
68             # If they have some pod after __END__ then assume it's safe to put
69             # it all there.
70             $pod_after_end =
71             ( $ppi_document->find( 'PPI::Statement::End' ) and
72             grep { $_->find_first( 'PPI::Token::Pod' ) }
73 10 100 100     36751 @{$ppi_document->find( 'PPI::Statement::End' )} ) ?
74             1 : 0;
75              
76             @pod_tokens =
77 10 100       6923 map { "$_" } @{ $ppi_document->find( 'PPI::Token::Pod' ) || [] };
  14         6918  
  10         29  
78 10         2792 $ppi_document->prune( 'PPI::Token::Pod' );
79              
80 10 50       10175 if( $ppi_document->serialize =~ /^=[a-z]/m )
81             {
82             # TODO: no idea what the problem is here, but DZP::PodWeaver had it...
83 0 0       0 $log->errorf( "Can't do podweave on '%s': " .
84             "there is POD inside string literals", $file )
85             if $log->is_error();
86 0         0 return( FAIL );
87             }
88              
89 10         3084 $pod_str = join "\n", @pod_tokens;
90 10         106 $pod_document = Pod::Elemental->read_string( $pod_str );
91              
92             # TODO: This _really_ doesn't like being run twice on a document with
93             # TODO: regions for some reason. Comment out for now and trust they
94             # TODO: have [@CorePrep] enabled.
95             # Pod::Elemental::Transformer::Pod5->new->transform_node( $pod_document );
96              
97 10         37555 %weave_args = (
98             %input,
99             pod_document => $pod_document,
100             ppi_document => $ppi_document,
101             filename => $file,
102             );
103              
104 10         86 $module_info = Module::Metadata->new_from_file( $file );
105 10 50 33     7561 if( $module_info and defined( $module_info->version() ) )
    50          
106             {
107 0         0 $weave_args{ version } = $module_info->version();
108             }
109             elsif( defined( $input{ dist_version } ) )
110             {
111             $log->warningf( "Unable to parse version in '%s', " .
112             "using dist_version '%s'", $file, $input{ dist_version } )
113 10 50       210 if $log->is_warning();
114 10         154 $weave_args{ version } = $input{ dist_version };
115             }
116             else
117             {
118 0 0       0 $log->warningf( "Unable to parse version in '%s' and " .
119             "no dist_version supplied", $file )
120             if $log->is_warning();
121             }
122              
123             # Try::Tiny this, it can croak.
124             try
125             {
126 10     10   449 $pod_document = $weaver->weave_document( \%weave_args );
127              
128 10 50 33     71009 $log->errorf( "weave_document() failed on '%s': No Pod generated",
129             $file )
130             if $log->is_error() and not $pod_document;
131             }
132             catch
133             {
134 0 0   0   0 $log->errorf( "weave_document() failed on '%s': %s",
135             $file, $_ )
136             if $log->is_error();
137 0         0 $pod_document = undef;
138 10         110 };
139 10 50       289 return( FAIL ) unless $pod_document;
140              
141 10         44 $new_pod = $pod_document->as_pod_string;
142              
143 10         5683 $end = do {
144 10   100     70 my $end_elem = $ppi_document->find( 'PPI::Statement::Data' )
145             || $ppi_document->find( 'PPI::Statement::End' );
146 10 100       17311 join q{}, @{ $end_elem || [] };
  10         66  
147             };
148              
149 10         150 $ppi_document->prune( 'PPI::Statement::End' );
150 10         9711 $ppi_document->prune( 'PPI::Statement::Data' );
151              
152 10         9255 $new_perl = $ppi_document->serialize;
153              
154 10         2966 $new_perl =~ s/\n+$//;
155 10         18 $new_perl .= "\n";
156              
157 10         74 $new_pod =~ s/\n+$//;
158 10         35 $new_pod =~ s/^\n+//;
159 10         15 $new_pod .= "\n";
160              
161 10 100       31 if( not $end )
162             {
163 3         6 $end = "__END__\n\n";
164 3         4 $pod_after_end = 1;
165             }
166              
167 10 100       26 if( $pod_after_end )
168             {
169 5         19 $new_perl = "$new_perl\n$end$new_pod";
170             }
171             else
172             {
173 5         21 $new_perl = "$new_perl\n$new_pod\n$end";
174             }
175              
176 10 100       42 if( $perl eq $new_perl )
177             {
178 3 50       21 $log->infof( "Contents of '%s' unchanged", $file )
179             if $log->is_info();
180 3         58 return( SUCCESS_UNCHANGED );
181             }
182              
183 7 50       28 $output_file = $write_to_dot_new ? ( $file . '.new' ) : $file;
184 7         15 $backup_file = $file . '.bak';
185              
186 7 50 33     23 unless( $write_to_dot_new or $no_backup )
187             {
188 0         0 unlink( $backup_file );
189 0         0 copy( $file, $backup_file );
190             }
191              
192 7 50       39 $log->debugf( "Writing new '%s' for '%s'", $output_file, $file )
193             if $log->is_debug();
194             # We want to preserve permissions and other stuff, so we open
195             # it for read/write.
196 7 50       111 $fh = IO::File->new( $output_file, $write_to_dot_new ? '>' : '+<' );
197 7 50       1095 unless( $fh )
198             {
199 0 0       0 $log->errorf( "Unable to write to '%s' for '%s': %s",
200             $output_file, $file, $! )
201             if $log->is_error();
202 0         0 return( FAIL );
203             }
204 7         36 $fh->truncate( 0 );
205 7         183 $fh->print( $new_perl );
206 7         105 $fh->close();
207 7         348 return( SUCCESS_CHANGED );
208             }
209              
210             sub get_dist_info
211             {
212 2     2 1 2726 my ( $self, %options ) = @_;
213 2         4 my ( $dist_info, $dist_root, $meta_file );
214              
215 2   50     14 $dist_root = $options{ dist_root } || '.';
216              
217 2         4 $dist_info = {};
218              
219 2 50 33     87 if( -r ( $meta_file = File::Spec->catfile( $dist_root, 'META.json' ) ) or
220             -r ( $meta_file = File::Spec->catfile( $dist_root, 'META.yml' ) ) )
221             {
222 2 50       17 $log->debugf( "Reading '%s'", $meta_file )
223             if $log->is_debug();
224 2         68 $dist_info->{ meta } = CPAN::Meta->load_file( $meta_file );
225             }
226             else
227             {
228 0 0       0 $log->warningf( "No META.json or META.yml file found, " .
229             "is '%s' a distribution directory?", $dist_root )
230             if $log->is_warning();
231             }
232              
233 2 50       27985 if( $dist_info->{ meta } )
234             {
235 2         9 $dist_info->{ authors } = [ $dist_info->{ meta }->authors() ];
236              
237             $dist_info->{ authors } =
238 1         7 [ map { s/\@/ $options{ antispam } /; $_; }
  1         4  
239 1         2 @{$dist_info->{ authors }} ]
240 2 100       833 if $options{ antispam };
241              
242 2 50       20 $log->debug( "Creating license object" )
243             if $log->is_debug();
244 2         23 my @licenses = $dist_info->{ meta }->licenses();
245 2 50       404 if( @licenses != 1 )
246             {
247 0 0       0 $log->error( "Pod::Weaver requires one, and only one, " .
248             "license at a time." )
249             if $log->is_error();
250 0         0 return;
251             }
252              
253 2         4 my $license = $licenses[ 0 ];
254              
255             # Cribbed from Module::Build, really should be in Software::License.
256 2         26 my %licenses = (
257             perl => 'Perl_5',
258             perl_5 => 'Perl_5',
259             apache => 'Apache_2_0',
260             apache_1_1 => 'Apache_1_1',
261             artistic => 'Artistic_1_0',
262             artistic_2 => 'Artistic_2_0',
263             lgpl => 'LGPL_2_1',
264             lgpl2 => 'LGPL_2_1',
265             lgpl3 => 'LGPL_3_0',
266             bsd => 'BSD',
267             gpl => 'GPL_1',
268             gpl2 => 'GPL_2',
269             gpl3 => 'GPL_3',
270             mit => 'MIT',
271             mozilla => 'Mozilla_1_1',
272             open_source => undef,
273             unrestricted => undef,
274             restrictive => undef,
275             unknown => undef,
276             );
277              
278 2 50       5 unless( $licenses{ $license } )
279             {
280 0 0       0 $log->errorf( "Unknown license: '%s'", $license )
281             if $log->is_error();
282 0         0 return;
283             }
284              
285 2         3 $license = $licenses{ $license };
286              
287 2         5 my $class = "Software::License::$license";
288 2 50   1   153 unless( eval "use $class; 1" )
  1     1   456  
  1         7315  
  1         13  
  1         7  
  1         1  
  1         16  
289             {
290 0 0       0 $log->errorf( "Can't load Software::License::$license: %s", $@ )
291             if $log->is_error();
292 0         0 return;
293             }
294              
295             $dist_info->{ license } = $class->new( {
296 2         4 holder => join( ' & ', @{$dist_info->{ authors }} ),
  2         16  
297             } );
298              
299 2 50       19 $log->debugf( "Using license: '%s'", $dist_info->{ license }->name() )
300             if $log->is_debug();
301              
302 2         20 $dist_info->{ dist_version } = $dist_info->{ meta }->version();
303             }
304              
305 2         18 return( $dist_info );
306             }
307              
308             sub get_weaver
309             {
310 0     0 1 0 my ( $self, %options ) = @_;
311 0         0 my ( $dist_root, $config_file );
312              
313 0   0     0 $dist_root = $options{ dist_root } || '.';
314 0 0       0 if( -r ( $config_file = File::Spec->catfile( $dist_root, 'weaver.ini' ) ) )
315             {
316 0 0       0 $log->debug( "Initializing weaver from ./weaver.ini" )
317             if $log->is_debug();
318 0         0 return( Pod::Weaver->new_from_config( {
319             root => $dist_root,
320             } ) );
321             }
322 0 0       0 $log->warningf( "No '%s' found, using Pod::Weaver defaults, " .
323             "this will most likely insert duplicate sections",
324             $config_file )
325             if $log->is_warning();
326 0         0 return( Pod::Weaver->new_with_default_config() );
327             }
328              
329             sub find_files_to_weave
330             {
331 1     1 1 682 my ( $self, %options ) = @_;
332 1         1 my ( $dist_root );
333              
334 1   50     8 $dist_root = $options{ dist_root } || '.';
335              
336             return(
337             File::Find::Rule->ignore_vcs
338             ->not_name( qr/~$/ )
339             ->perl_file
340             ->in(
341 3         50 grep { -d $_ }
342 1         12 map { File::Spec->catfile( $dist_root, $_ ) }
  3         1083  
343             qw/lib bin script/
344             )
345             );
346             }
347              
348             sub weave_distribution
349             {
350 0     0 1 0 my ( $self, %options ) = @_;
351 0         0 my ( $weaver, $dist_info );
352              
353 0         0 $dist_info = $self->get_dist_info( %options );
354 0         0 $weaver = $self->get_weaver( %options );
355              
356 0         0 foreach my $file ( $self->find_files_to_weave() )
357             {
358 0 0       0 $log->noticef( "Weaving file '%s'", $file )
359             if $log->is_notice();
360              
361             $self->weave_file(
362             %options,
363 0         0 %{$dist_info},
  0         0  
364             filename => $file,
365             weaver => $weaver,
366             );
367             }
368             }
369              
370             sub _config_dir
371             {
372 0     0   0 my ( $self ) = @_;
373 0         0 my ( $leaf_dir, $config_dir );
374              
375             # Following lifted from File::UserDir.
376             # I'd use that directly but it forces creation and population of the dir.
377              
378             # Derive from the caller based on HomeDir naming scheme
379 0 0       0 my $scheme = $File::HomeDir::IMPLEMENTED_BY or
380             die "Failed to find File::HomeDir naming scheme";
381 0 0 0     0 if( $scheme->isa( 'File::HomeDir::Darwin' ) or
    0          
382             $scheme->isa( 'File::HomeDir::Windows' ) )
383             {
384 0         0 $leaf_dir = 'App-podweaver';
385             }
386             elsif( $scheme->isa('File::HomeDir::Unix') )
387             {
388 0         0 $leaf_dir = '.app-podweaver';
389             }
390             else
391             {
392 0         0 die "Unsupported HomeDir naming scheme $scheme";
393             }
394              
395 0         0 $config_dir = File::Spec->catdir(
396             File::HomeDir->my_data(),
397             $leaf_dir
398             );
399              
400 0         0 return( $config_dir );
401             }
402              
403             sub _config_file
404             {
405 0     0   0 my ( $self ) = @_;
406 0         0 my ( $config_dir, $config_file );
407              
408 0 0       0 return( undef ) unless $config_dir = $self->_config_dir();
409              
410 0         0 $config_file = File::Spec->catfile( $config_dir, 'podweaver.ini' );
411 0         0 return( $config_file );
412             }
413              
414             sub config
415             {
416 0     0 1 0 my ( $self ) = @_;
417 0         0 my ( $config_file, $config );
418              
419 0         0 $config_file = $self->_config_file();
420 0 0 0     0 return( {} ) unless $config_file and -e $config_file;
421 0 0       0 $config = Config::Tiny->read( $config_file ) or
422             die "Error reading '$config_file': " . Config::Tiny->errstr();
423              
424 0         0 return( $config );
425             }
426              
427             1;
428              
429             __END__
430              
431             =pod
432              
433             =head1 NAME
434              
435             App::podweaver - Run Pod::Weaver on the files within a distribution.
436              
437             =head1 VERSION
438              
439             version 0.99_04
440              
441             =head1 SYNOPSIS
442              
443             L<App::podweaver> provides a mechanism to run L<Pod::Weaver> over the files
444             within a distribution, without needing to use L<Dist::Zilla>.
445              
446             Where L<Dist::Zilla> works on a copy of your source code, L<App::podweaver>
447             is intended to modify your source code directly, and as such it is highly
448             recommended that you use the L<Pod::Weaver::PluginBundle::ReplaceBoilerplate>
449             plugin bundle so that you over-write existing POD sections, instead of the
450             default L<Pod::Weaver> behaviour of repeatedly appending.
451              
452             You can configure the L<Pod::Weaver> invocation by providinng a
453             C<weaver.ini> file in the root directory of your distribution.
454              
455             =head1 BOOTSTRAPPING WITH META.json/META.yml
456              
457             Since the META.json/yml file is often generated with an abstract extracted
458             from the POD, and L<App::podweaver> expects a valid META file for
459             some of the information to insert into the POD, there's a chicken-and-egg
460             situation on the first invocation of either.
461              
462             Running L<App::podweaver> first should produce a POD with an abstract
463             line populated from your C<< # ABSTRACT: >> header, but without additional
464             sections like version and authors.
465             You can then generate your META file as per usual, and then run
466             L<App::podweaver> again to produce the missing sections:
467              
468             $ ./Build distmeta
469             Creating META.yml
470             ERROR: Missing required field 'dist_abstract' for metafile
471             $ podweaver -v
472             No META.json or META.yml file found, are you running in a distribution directory?
473             Processing lib/App/podweaver.pm
474             $ ./Build distmeta
475             Creating META.yml
476             $ podweaver -v
477             Processing lib/App/podweaver.pm
478              
479             This should only be neccessary on newly created distributions as
480             both the META and the neccessary POD abstract should be present
481             subsequently.
482              
483             =head1 METHODS
484              
485             =begin :private
486              
487             =head2 B<FAIL>
488              
489             Indicates the file failed to be woven.
490              
491             =head2 B<SUCCESS_UNCHANGED>
492              
493             Indicates the file was successfully woven but resulted in no changes.
494              
495             =head2 B<SUCCESS_CHANGED>
496              
497             Indicates the file was successfully woven and contained changes.
498              
499             =end :private
500              
501             =head2 I<$success> = B<< App::podweaver->weave_file( >> I<%options> B<)>
502              
503             Runs L<Pod::Weaver> on the given file, merges the generated Pod back
504             into the appropriate place and writes the new file out.
505              
506             C<< App::podweaver->weave_file() >> returns
507             C<< App::podweaver::FAIL >> on failure,
508             and either C<< App::podweaver::SUCCESS_UNCHANGED >> or
509             C<< App::podweaver::SUCCESS_CHANGED >> on success,
510             depending on whether changes needed to be made as a result of
511             the weaving.
512              
513             Currently these constants are not exportable.
514              
515             The following options configure C<< App::podweaver->weave_file() >>:
516              
517             =over
518              
519             =item B<< filename => >> I<$filename> (required)
520              
521             The filename of the file to weave.
522              
523             =item B<< weaver => >> I<$weaver> (required)
524              
525             The L<Pod::Weaver> instance to use for the weaving.
526              
527             =item B<< no_backup => >> I<0> | I<1> (default: 0)
528              
529             If set to a true value, no backup will be made of the original file.
530              
531             =item B<< new => >> I<0> | I<1> (default: 0)
532              
533             If set to a true value, the modified file will be written to the
534             original filename with C<.new> appended, rather than overwriting
535             the original.
536              
537             =item B<< dist_version => >> I<$version>
538              
539             If no C<$VERSION> can be parsed from the file by
540             L<Module::Metadata>, the version supplied in
541             C<dist_version> will be used as a fallback.
542              
543             =back
544              
545             Any additional options are passed untouched to L<Pod::Weaver>.
546              
547             =head2 I<$dist_info> = B<< App::podweaver->get_dist_info( >> I<%options> B<)>
548              
549             Attempts to extract the information needed by L<Pod::Weaver>
550             about the distribution.
551              
552             It does this by examining any C<META.json> or C<META.yml> file
553             it finds, and by expanding various fields found within.
554              
555             Valid options are:
556              
557             =over
558              
559             =item B<< dist_root => >> I<$directory> (default: current working directory)
560              
561             Treats I<$directory> as the root directory of the distribution,
562             where the C<META.json> or C<META.yml> file should be found.
563              
564             If not supplied, this will default to the current working directory.
565              
566             =item B<< antispam => >> I<$string>
567              
568             If set, any @ sign in author emails will be replaced by a space,
569             the given string, and a further space, in an attempt to confuse
570             spammers.
571              
572             For example C<< antispam => 'NOSPAM' >> will transform an email
573             of C<< nobody@127.0.0.1 >> into C<< nobody NOSPAM 127.0.0.1 >>.
574              
575             =back
576              
577             =head2 I<$weaver> = B<< App::podweaver->get_weaver( >> I<%options> B<)>
578              
579             Builds a L<Pod::Weaver> instance, attemping to find a C<weaver.ini>
580             in the distribution root directory.
581              
582             Valid options are:
583              
584             =over
585              
586             =item B<< dist_root => >> I<$directory> (default: current working directory)
587              
588             Treats I<$directory> as the root directory of the distribution,
589             where the C<weaver.ini> file should be found.
590              
591             If not supplied, this will default to the current working directory.
592              
593             =back
594              
595             =head2 I<@files> = B<< App::podweaver->find_files_to_weave( >> I<%options> B<)>
596              
597             Invokes L<File::Find::Rule>, L<File::Find::Rule::VCS> and
598             L<File::Find::Rule::Perl> to return a list of perl files that are
599             candidates to run L<Pod::Weaver> on in the C<lib>, C<bin> and C<script>
600             dirs of the distribution directory.
601              
602             Valid options are:
603              
604             =over
605              
606             =item B<< dist_root => >> I<$directory> (default: current working directory)
607              
608             Treats I<$directory> as the root directory of the distribution.
609              
610             If not supplied, this will default to the current working directory.
611              
612             =back
613              
614             =head2 B<< App::podweaver->weave_distribution( >> I<%options> B<)>
615              
616             Rolls all the other methods together to run L<Pod::Weaver> on the
617             appropriate files within the distribution found in the current
618             working directory.
619              
620             =head2 I<$config> = B<< App::podweaver->config() >>
621              
622             Retrieves the L<Config::Tiny> contents of the user's config file for
623             the application, as found in the C<podweaver.ini> file in the usual
624             place for user configuration files for your OS.
625              
626             (C<~/.app_podweaver/podweaver.ini> for UNIX, C<~/Local Settings/Application
627             Data/App-podweaver/podweaver.ini> under Windows.)
628              
629             =head1 KNOWN ISSUES AND BUGS
630              
631             =over
632              
633             =item META.json/yml bootstrap is a mess
634              
635             The whole bootstrap issue with META.json/yml is ugly.
636              
637             =back
638              
639             =head1 REPORTING BUGS
640              
641             Please report any bugs or feature requests to C<bug-app-podweaver at rt.cpan.org>, or through
642             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-podweaver>. I will be notified, and then you'll
643             automatically be notified of progress on your bug as I make changes.
644              
645             =head1 SUPPORT
646              
647             You can find documentation for this module with the perldoc command.
648              
649             perldoc App::podweaver
650              
651             You can also look for information at:
652              
653             =over 4
654              
655             =item * RT: CPAN's request tracker
656              
657             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-podweaver>
658              
659             =item * AnnoCPAN: Annotated CPAN documentation
660              
661             L<http://annocpan.org/dist/App-podweaver>
662              
663             =item * CPAN Ratings
664              
665             L<http://cpanratings.perl.org/d/App-podweaver>
666              
667             =item * Search CPAN
668              
669             L<http://search.cpan.org/dist/App-podweaver/>
670              
671             =back
672              
673             =head1 AUTHOR
674              
675             Sam Graham <libapp-podweaver-perl BLAHBLAH illusori.co.uk>
676              
677             =head1 COPYRIGHT AND LICENSE
678              
679             This software is copyright (c) 2010 by Sam Graham <libapp-podweaver-perl BLAHBLAH illusori.co.uk>.
680              
681             This is free software; you can redistribute it and/or modify it under
682             the same terms as the Perl 5 programming language system itself.
683              
684             =cut