xref: /openbsd-src/gnu/usr.bin/perl/cpan/Pod-Simple/lib/Pod/Simple/HTMLBatch.pm (revision 3d61058aa5c692477b6d18acfbbdb653a9930ff9)
1package Pod::Simple::HTMLBatch;
2use strict;
3our $VERSION = '3.45';
4our @ISA = ();  # Yup, we're NOT a subclass of Pod::Simple::HTML!
5
6# TODO: nocontents stylesheets. Strike some of the color variations?
7
8use Pod::Simple::HTML ();
9BEGIN {*esc = \&Pod::Simple::HTML::esc }
10use File::Spec ();
11
12use Pod::Simple::Search;
13our $SEARCH_CLASS ||= 'Pod::Simple::Search';
14
15BEGIN {
16  if(defined &DEBUG) { } # no-op
17  elsif( defined &Pod::Simple::DEBUG ) { *DEBUG = \&Pod::Simple::DEBUG }
18  else { *DEBUG = sub () {0}; }
19}
20
21our $SLEEPY;
22$SLEEPY = 1 if !defined $SLEEPY and $^O =~ /mswin|mac/i;
23# flag to occasionally sleep for $SLEEPY - 1 seconds.
24
25our $HTML_RENDER_CLASS ||= "Pod::Simple::HTML";
26our $HTML_EXTENSION;
27
28#
29# Methods beginning with "_" are particularly internal and possibly ugly.
30#
31
32Pod::Simple::_accessorize( __PACKAGE__,
33 'verbose', # how verbose to be during batch conversion
34 'html_render_class', # what class to use to render
35 'search_class', # what to use to search for POD documents
36 'contents_file', # If set, should be the name of a file (in current directory)
37                  # to write the list of all modules to
38 'index', # will set $htmlpage->index(...) to this (true or false)
39 'progress', # progress object
40 'contents_page_start',  'contents_page_end',
41
42 'css_flurry', '_css_wad', 'javascript_flurry', '_javascript_wad',
43 'no_contents_links', # set to true to suppress automatic adding of << links.
44 '_contents',
45);
46
47# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
48# Just so we can run from the command line more easily
49sub go {
50  @ARGV == 2 or die sprintf(
51    "Usage: perl -M%s -e %s:go indirs outdir\n  (or use \"\@INC\" for indirs)\n",
52    __PACKAGE__, __PACKAGE__,
53  );
54
55  if(defined($ARGV[1]) and length($ARGV[1])) {
56    my $d = $ARGV[1];
57    -e $d or die "I see no output directory named \"$d\"\nAborting";
58    -d $d or die "But \"$d\" isn't a directory!\nAborting";
59    -w $d or die "Directory \"$d\" isn't writeable!\nAborting";
60  }
61
62  __PACKAGE__->batch_convert(@ARGV);
63}
64# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
65
66
67sub new {
68  my $new = bless {}, ref($_[0]) || $_[0];
69  $new->html_render_class($HTML_RENDER_CLASS);
70  $new->search_class($SEARCH_CLASS);
71  $new->verbose(1 + DEBUG);
72  $new->_contents([]);
73
74  $new->index(1);
75
76  $new->       _css_wad([]);         $new->css_flurry(1);
77  $new->_javascript_wad([]);  $new->javascript_flurry(1);
78
79  $new->contents_file(
80    'index' . ($HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION)
81  );
82
83  $new->contents_page_start( join "\n", grep $_,
84    $Pod::Simple::HTML::Doctype_decl,
85    "<html><head>",
86    "<title>Perl Documentation</title>",
87    $Pod::Simple::HTML::Content_decl,
88    "</head>",
89    "\n<body class='contentspage'>\n<h1>Perl Documentation</h1>\n"
90  ); # override if you need a different title
91
92
93  $new->contents_page_end( sprintf(
94    "\n\n<p class='contentsfooty'>Generated by %s v%s under Perl v%s\n<br >At %s GMT.</p>\n\n</body></html>\n",
95    esc(
96      ref($new),
97      eval {$new->VERSION} || $VERSION,
98      $], scalar(gmtime($ENV{SOURCE_DATE_EPOCH} || time)),
99  )));
100
101  return $new;
102}
103
104# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
105
106sub muse {
107  my $self = shift;
108  if($self->verbose) {
109    print 'T+', int(time() - $self->{'_batch_start_time'}), "s: ", @_, "\n";
110  }
111  return 1;
112}
113
114# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
115
116sub batch_convert {
117  my($self, $dirs, $outdir) = @_;
118  $self ||= __PACKAGE__; # tolerate being called as an optionless function
119  $self = $self->new unless ref $self; # tolerate being used as a class method
120
121  if(!defined($dirs)  or  $dirs eq ''  or  $dirs eq '@INC' ) {
122    $dirs = '';
123  } elsif(ref $dirs) {
124    # OK, it's an explicit set of dirs to scan, specified as an arrayref.
125  } else {
126    # OK, it's an explicit set of dirs to scan, specified as a
127    #  string like "/thing:/also:/whatever/perl" (":"-delim, as usual)
128    #  or, under MSWin, like "c:/thing;d:/also;c:/whatever/perl" (";"-delim!)
129    require Config;
130    my $ps = quotemeta( $Config::Config{'path_sep'} || ":" );
131    $dirs = [ grep length($_), split qr/$ps/, $dirs ];
132  }
133
134  $outdir = $self->filespecsys->curdir
135   unless defined $outdir and length $outdir;
136
137  $self->_batch_convert_main($dirs, $outdir);
138}
139
140# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
141
142sub _batch_convert_main {
143  my($self, $dirs, $outdir) = @_;
144  # $dirs is either false, or an arrayref.
145  # $outdir is a pathspec.
146
147  $self->{'_batch_start_time'} ||= time();
148
149  $self->muse( "= ", scalar(localtime) );
150  $self->muse( "Starting batch conversion to \"$outdir\"" );
151
152  my $progress = $self->progress;
153  if(!$progress and $self->verbose > 0 and $self->verbose() <= 5) {
154    require Pod::Simple::Progress;
155    $progress = Pod::Simple::Progress->new(
156        ($self->verbose  < 2) ? () # Default omission-delay
157      : ($self->verbose == 2) ? 1  # Reduce the omission-delay
158                              : 0  # Eliminate the omission-delay
159    );
160    $self->progress($progress);
161  }
162
163  if($dirs) {
164    $self->muse(scalar(@$dirs), " dirs to scan: @$dirs");
165  } else {
166    $self->muse("Scanning \@INC.  This could take a minute or two.");
167  }
168  my $mod2path = $self->find_all_pods($dirs ? $dirs : ());
169  $self->muse("Done scanning.");
170
171  my $total = keys %$mod2path;
172  unless($total) {
173    $self->muse("No pod found.  Aborting batch conversion.\n");
174    return $self;
175  }
176
177  $progress and $progress->goal($total);
178  $self->muse("Now converting pod files to HTML.",
179    ($total > 25) ? "  This will take a while more." : ()
180  );
181
182  $self->_spray_css(        $outdir );
183  $self->_spray_javascript( $outdir );
184
185  $self->_do_all_batch_conversions($mod2path, $outdir);
186
187  $progress and $progress->done(sprintf (
188    "Done converting %d files.",  $self->{"__batch_conv_page_count"}
189  ));
190  return $self->_batch_convert_finish($outdir);
191  return $self;
192}
193
194
195sub _do_all_batch_conversions {
196  my($self, $mod2path, $outdir) = @_;
197  $self->{"__batch_conv_page_count"} = 0;
198
199  foreach my $module (sort {lc($a) cmp lc($b)} keys %$mod2path) {
200    $self->_do_one_batch_conversion($module, $mod2path, $outdir);
201    sleep($SLEEPY - 1) if $SLEEPY;
202  }
203
204  return;
205}
206
207sub _batch_convert_finish {
208  my($self, $outdir) = @_;
209  $self->write_contents_file($outdir);
210  $self->muse("Done with batch conversion.  $$self{'__batch_conv_page_count'} files done.");
211  $self->muse( "= ", scalar(localtime) );
212  $self->progress and $self->progress->done("All done!");
213  return;
214}
215
216# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
217
218sub _do_one_batch_conversion {
219  my($self, $module, $mod2path, $outdir, $outfile) = @_;
220
221  my $retval;
222  my $total    = scalar keys %$mod2path;
223  my $infile   = $mod2path->{$module};
224  my @namelets = grep m/\S/, split "::", $module;
225        # this can stick around in the contents LoL
226  my $depth    = scalar @namelets;
227  die "Contentless thingie?! $module $infile" unless @namelets; #sanity
228
229  $outfile  ||= do {
230    my @n = @namelets;
231    $n[-1] .= $HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION;
232    $self->filespecsys->catfile( $outdir, @n );
233  };
234
235  my $progress = $self->progress;
236
237  my $page = $self->html_render_class->new;
238  if(DEBUG > 5) {
239    $self->muse($self->{"__batch_conv_page_count"} + 1, "/$total: ",
240      ref($page), " render ($depth) $module => $outfile");
241  } elsif(DEBUG > 2) {
242    $self->muse($self->{"__batch_conv_page_count"} + 1, "/$total: $module => $outfile")
243  }
244
245  # Give each class a chance to init the converter:
246  $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth)
247   if $page->can('batch_mode_page_object_init');
248  # Init for the index (TOC), too.
249  $self->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth)
250   if $self->can('batch_mode_page_object_init');
251
252  # Now get busy...
253  $self->makepath($outdir => \@namelets);
254
255  $progress and $progress->reach($self->{"__batch_conv_page_count"}, "Rendering $module");
256
257  if( $retval = $page->parse_from_file($infile, $outfile) ) {
258    ++ $self->{"__batch_conv_page_count"} ;
259    $self->note_for_contents_file( \@namelets, $infile, $outfile );
260  } else {
261    $self->muse("Odd, parse_from_file(\"$infile\", \"$outfile\") returned false.");
262  }
263
264  $page->batch_mode_page_object_kill($self, $module, $infile, $outfile, $depth)
265   if $page->can('batch_mode_page_object_kill');
266  # The following isn't a typo.  Note that it switches $self and $page.
267  $self->batch_mode_page_object_kill($page, $module, $infile, $outfile, $depth)
268   if $self->can('batch_mode_page_object_kill');
269
270  DEBUG > 4 and printf STDERR "%s %sb < $infile %s %sb\n",
271     $outfile, -s $outfile, $infile, -s $infile
272  ;
273
274  undef($page);
275  return $retval;
276}
277
278# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
279sub filespecsys { $_[0]{'_filespecsys'} || 'File::Spec' }
280
281# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
282
283sub note_for_contents_file {
284  my($self, $namelets, $infile, $outfile) = @_;
285
286  # I think the infile and outfile parts are never used. -- SMB
287  # But it's handy to have them around for debugging.
288
289  if( $self->contents_file ) {
290    my $c = $self->_contents();
291    push @$c,
292     [ join("::", @$namelets), $infile, $outfile, $namelets ]
293     #            0               1         2         3
294    ;
295    DEBUG > 3 and print STDERR "Noting @$c[-1]\n";
296  }
297  return;
298}
299
300#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
301
302sub write_contents_file {
303  my($self, $outdir) = @_;
304  my $outfile  = $self->_contents_filespec($outdir) || return;
305
306  $self->muse("Preparing list of modules for ToC");
307
308  my($toplevel,           # maps  toplevelbit => [all submodules]
309     $toplevel_form_freq, # ends up being  'foo' => 'Foo'
310    ) = $self->_prep_contents_breakdown;
311
312  my $Contents = eval { $self->_wopen($outfile) };
313  if( $Contents ) {
314    $self->muse( "Writing contents file $outfile" );
315  } else {
316    warn "Couldn't write-open contents file $outfile: $!\nAbort writing to $outfile at all";
317    return;
318  }
319
320  $self->_write_contents_start(  $Contents, $outfile, );
321  $self->_write_contents_middle( $Contents, $outfile, $toplevel, $toplevel_form_freq );
322  $self->_write_contents_end(    $Contents, $outfile, );
323  return $outfile;
324}
325
326# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
327
328sub _write_contents_start {
329  my($self, $Contents, $outfile) = @_;
330  my $starter = $self->contents_page_start || '';
331
332  {
333    my $css_wad = $self->_css_wad_to_markup(1);
334    if( $css_wad ) {
335      $starter =~ s{(</head>)}{\n$css_wad\n$1}i;  # otherwise nevermind
336    }
337
338    my $javascript_wad = $self->_javascript_wad_to_markup(1);
339    if( $javascript_wad ) {
340      $starter =~ s{(</head>)}{\n$javascript_wad\n$1}i;   # otherwise nevermind
341    }
342  }
343
344  unless(print $Contents $starter, "<dl class='superindex'>\n" ) {
345    warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
346    close($Contents);
347    return 0;
348  }
349  return 1;
350}
351
352# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
353
354sub _write_contents_middle {
355  my($self, $Contents, $outfile, $toplevel2submodules, $toplevel_form_freq) = @_;
356
357  foreach my $t (sort keys %$toplevel2submodules) {
358    my @downlines = sort {$a->[-1] cmp $b->[-1]}
359                          @{ $toplevel2submodules->{$t} };
360
361    printf $Contents qq[<dt><a name="%s">%s</a></dt>\n<dd>\n],
362      esc( $t, $toplevel_form_freq->{$t} )
363    ;
364
365    my($path, $name);
366    foreach my $e (@downlines) {
367      $name = $e->[0];
368      $path = join( "/", '.', esc( @{$e->[3]} ) )
369        . ($HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION);
370      print $Contents qq{  <a href="$path">}, esc($name), "</a>&nbsp;&nbsp;\n";
371    }
372    print $Contents "</dd>\n\n";
373  }
374  return 1;
375}
376
377# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
378
379sub _write_contents_end {
380  my($self, $Contents, $outfile) = @_;
381  unless(
382    print $Contents "</dl>\n",
383      $self->contents_page_end || '',
384  ) {
385    warn "Couldn't write to $outfile: $!";
386  }
387  close($Contents) or warn "Couldn't close $outfile: $!";
388  return 1;
389}
390
391# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
392
393sub _prep_contents_breakdown {
394  my($self) = @_;
395  my $contents = $self->_contents;
396  my %toplevel; # maps  lctoplevelbit => [all submodules]
397  my %toplevel_form_freq; # ends up being  'foo' => 'Foo'
398                               # (mapping anycase forms to most freq form)
399
400  foreach my $entry (@$contents) {
401    my $toplevel =
402      $entry->[0] =~ m/^perl\w*$/ ? 'perl_core_docs'
403          # group all the perlwhatever docs together
404      : $entry->[3][0] # normal case
405    ;
406    ++$toplevel_form_freq{ lc $toplevel }{ $toplevel };
407    push @{ $toplevel{ lc $toplevel } }, $entry;
408    push @$entry, lc($entry->[0]); # add a sort-order key to the end
409  }
410
411  foreach my $toplevel (sort keys %toplevel) {
412    my $fgroup = $toplevel_form_freq{$toplevel};
413    $toplevel_form_freq{$toplevel} =
414    (
415      sort { $fgroup->{$b} <=> $fgroup->{$a}  or  $a cmp $b }
416        keys %$fgroup
417      # This hash is extremely unlikely to have more than 4 members, so this
418      # sort isn't so very wasteful
419    )[0];
420  }
421
422  return(\%toplevel, \%toplevel_form_freq) if wantarray;
423  return \%toplevel;
424}
425
426# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
427
428sub _contents_filespec {
429  my($self, $outdir) = @_;
430  my $outfile = $self->contents_file;
431  return unless $outfile;
432  return $self->filespecsys->catfile( $outdir, $outfile );
433}
434
435#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
436
437sub makepath {
438  my($self, $outdir, $namelets) = @_;
439  return unless @$namelets > 1;
440  for my $i (0 .. ($#$namelets - 1)) {
441    my $dir = $self->filespecsys->catdir( $outdir, @$namelets[0 .. $i] );
442    if(-e $dir) {
443      die "$dir exists but not as a directory!?" unless -d $dir;
444      next;
445    }
446    DEBUG > 3 and print STDERR "  Making $dir\n";
447    mkdir $dir, 0777
448     or die "Can't mkdir $dir: $!\nAborting"
449    ;
450  }
451  return;
452}
453
454#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
455
456sub batch_mode_page_object_init {
457  my $self = shift;
458  my($page, $module, $infile, $outfile, $depth) = @_;
459
460  # TODO: any further options to percolate onto this new object here?
461
462  $page->default_title($module);
463  $page->index( $self->index );
464
465  $page->html_css(        $self->       _css_wad_to_markup($depth) );
466  $page->html_javascript( $self->_javascript_wad_to_markup($depth) );
467
468  $self->add_header_backlink($page, $module, $infile, $outfile, $depth);
469  $self->add_footer_backlink($page, $module, $infile, $outfile, $depth);
470
471
472  return $self;
473}
474
475# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
476
477sub add_header_backlink {
478  my $self = shift;
479  return if $self->no_contents_links;
480  my($page, $module, $infile, $outfile, $depth) = @_;
481  $page->html_header_after_title( join '',
482    $page->html_header_after_title || '',
483
484    qq[<p class="backlinktop"><b><a name="___top" href="],
485    $self->url_up_to_contents($depth),
486    qq[" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>\n],
487  )
488   if $self->contents_file
489  ;
490  return;
491}
492
493sub add_footer_backlink {
494  my $self = shift;
495  return if $self->no_contents_links;
496  my($page, $module, $infile, $outfile, $depth) = @_;
497  $page->html_footer( join '',
498    qq[<p class="backlinkbottom"><b><a name="___bottom" href="],
499    $self->url_up_to_contents($depth),
500    qq[" title="All Documents">&lt;&lt;</a></b></p>\n],
501
502    $page->html_footer || '',
503  )
504   if $self->contents_file
505  ;
506  return;
507}
508
509sub url_up_to_contents {
510  my($self, $depth) = @_;
511  --$depth;
512  return join '/', ('..') x $depth, esc($self->contents_file);
513}
514
515#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
516
517sub find_all_pods {
518  my($self, $dirs) = @_;
519  # You can override find_all_pods in a subclass if you want to
520  #  do extra filtering or whatnot.  But for the moment, we just
521  #  pass to modnames2paths:
522  return $self->modnames2paths($dirs);
523}
524
525#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
526
527sub modnames2paths { # return a hashref mapping modulenames => paths
528  my($self, $dirs) = @_;
529
530  my $m2p;
531  {
532    my $search = $self->search_class->new;
533    DEBUG and print STDERR "Searching via $search\n";
534    $search->verbose(1) if DEBUG > 10;
535    $search->progress( $self->progress->copy->goal(0) ) if $self->progress;
536    $search->shadows(0);  # don't bother noting shadowed files
537    $search->inc(     $dirs ? 0      :  1 );
538    $search->survey(  $dirs ? @$dirs : () );
539    $m2p = $search->name2path;
540    die "What, no name2path?!" unless $m2p;
541  }
542
543  $self->muse("That's odd... no modules found!") unless keys %$m2p;
544  if( DEBUG > 4 ) {
545    print STDERR "Modules found (name => path):\n";
546    foreach my $m (sort {lc($a) cmp lc($b)} keys %$m2p) {
547      print STDERR "  $m  $$m2p{$m}\n";
548    }
549    print STDERR "(total ",     scalar(keys %$m2p), ")\n\n";
550  } elsif( DEBUG ) {
551    print STDERR      "Found ", scalar(keys %$m2p), " modules.\n";
552  }
553  $self->muse( "Found ", scalar(keys %$m2p), " modules." );
554
555  # return the Foo::Bar => /whatever/Foo/Bar.pod|pm hashref
556  return $m2p;
557}
558
559#===========================================================================
560
561sub _wopen {
562  # this is abstracted out so that the daemon class can override it
563  my($self, $outpath) = @_;
564  require Symbol;
565  my $out_fh = Symbol::gensym();
566  DEBUG > 5 and print STDERR "Write-opening to $outpath\n";
567  return $out_fh if open($out_fh, "> $outpath");
568  require Carp;
569  Carp::croak("Can't write-open $outpath: $!");
570}
571
572#==========================================================================
573
574sub add_css {
575  my($self, $url, $is_default, $name, $content_type, $media, $_code) = @_;
576  return unless $url;
577  unless($name) {
578    # cook up a reasonable name based on the URL
579    $name = $url;
580    if( $name !~ m/\?/ and $name =~ m{([^/]+)$}s ) {
581      $name = $1;
582      $name =~ s/\.css//i;
583    }
584  }
585  $media        ||= 'all';
586  $content_type ||= 'text/css';
587
588  my $bunch = [$url, $name, $content_type, $media, $_code];
589  if($is_default) { unshift @{ $self->_css_wad }, $bunch }
590  else            { push    @{ $self->_css_wad }, $bunch }
591  return;
592}
593
594# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
595
596sub _spray_css {
597  my($self, $outdir) = @_;
598
599  return unless $self->css_flurry();
600  $self->_gen_css_wad();
601
602  my $lol = $self->_css_wad;
603  foreach my $chunk (@$lol) {
604    my $url = $chunk->[0];
605    my $outfile;
606    if( ref($chunk->[-1]) and $url =~ m{^(_[-a-z0-9_]+\.css$)} ) {
607      $outfile = $self->filespecsys->catfile( $outdir, "$1" );
608      DEBUG > 5 and print STDERR "Noting $$chunk[0] as a file I'll create.\n";
609    } else {
610      DEBUG > 5 and print STDERR "OK, noting $$chunk[0] as an external CSS.\n";
611      # Requires no further attention.
612      next;
613    }
614
615    #$self->muse( "Writing autogenerated CSS file $outfile" );
616    my $Cssout = $self->_wopen($outfile);
617    print $Cssout ${$chunk->[-1]}
618     or warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
619    close($Cssout);
620    DEBUG > 5 and print STDERR "Wrote $outfile\n";
621  }
622
623  return;
624}
625
626# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
627
628sub _css_wad_to_markup {
629  my($self, $depth) = @_;
630
631  my @css  = @{ $self->_css_wad || return '' };
632  return '' unless @css;
633
634  my $rel = 'stylesheet';
635  my $out = '';
636
637  --$depth;
638  my $uplink = $depth ? ('../' x $depth) : '';
639
640  foreach my $chunk (@css) {
641    next unless $chunk and @$chunk;
642
643    my( $url1, $url2, $title, $type, $media) = (
644      $self->_maybe_uplink( $chunk->[0], $uplink ),
645      esc(grep !ref($_), @$chunk)
646    );
647
648    $out .= qq{<link rel="$rel" title="$title" type="$type" href="$url1$url2" media="$media" >\n};
649
650    $rel = 'alternate stylesheet'; # alternates = all non-first iterations
651  }
652  return $out;
653}
654
655# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
656sub _maybe_uplink {
657  # if the given URL looks relative, return the given uplink string --
658  # otherwise return emptystring
659  my($self, $url, $uplink) = @_;
660  ($url =~ m{^\./} or $url !~ m{[/\:]} )
661    ? $uplink
662    : ''
663    # qualify it, if/as needed
664}
665
666# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
667sub _gen_css_wad {
668  my $self = $_[0];
669  my $css_template = $self->_css_template;
670  foreach my $variation (
671
672   # Commented out for sake of concision:
673   #
674   #  011n=black_with_red_on_white
675   #  001n=black_with_yellow_on_white
676   #  101n=black_with_green_on_white
677   #  110=white_with_yellow_on_black
678   #  010=white_with_green_on_black
679   #  011=white_with_blue_on_black
680   #  100=white_with_red_on_black
681    '110n=blkbluw',  # black_with_blue_on_white
682    '010n=blkmagw',  # black_with_magenta_on_white
683    '100n=blkcynw',  # black_with_cyan_on_white
684    '101=whtprpk',   # white_with_purple_on_black
685    '001=whtnavk',   # white_with_navy_blue_on_black
686    '010a=grygrnk',  # grey_with_green_on_black
687    '010b=whtgrng',  # white_with_green_on_grey
688    '101an=blkgrng', # black_with_green_on_grey
689    '101bn=grygrnw', # grey_with_green_on_white
690  ) {
691
692    my $outname = $variation;
693    my($flipmode, @swap) = ( ($4 || ''), $1,$2,$3)
694      if $outname =~ s/^([012])([012])([[012])([a-z]*)=?//s;
695    @swap = () if '010' eq join '', @swap; # 010 is a swop-no-op!
696
697    my $this_css =
698      "/* This file is autogenerated.  Do not edit.  $variation */\n\n"
699      . $css_template;
700
701    # Only look at three-digitty colors, for now at least.
702    if( $flipmode =~ m/n/ ) {
703      $this_css =~ s/(#[0-9a-fA-F]{3})\b/_color_negate($1)/eg;
704      $this_css =~ s/\bthin\b/medium/g;
705    }
706    $this_css =~ s<#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\b>
707                  < join '', '#', ($1,$2,$3)[@swap] >eg   if @swap;
708
709    if(   $flipmode =~ m/a/)
710       { $this_css =~ s/#fff\b/#999/gi } # black -> dark grey
711    elsif($flipmode =~ m/b/)
712       { $this_css =~ s/#000\b/#666/gi } # white -> light grey
713
714    my $name = $outname;
715    $name =~ tr/-_/  /;
716    $self->add_css( "_$outname.css", 0, $name, 0, 0, \$this_css);
717  }
718
719  # Now a few indexless variations:
720  for (my ($outfile, $variation) = each %{{
721      blkbluw => 'black_with_blue_on_white',
722      whtpurk => 'white_with_purple_on_black',
723      whtgrng => 'white_with_green_on_grey',
724      grygrnw => 'grey_with_green_on_white',
725  }}) {
726    my $this_css = join "\n",
727      "/* This file is autogenerated.  Do not edit.  $outfile */\n",
728      "\@import url(\"./_$variation.css\");",
729      ".indexgroup { display: none; }",
730      "\n",
731    ;
732    my $name = $outfile;
733    $name =~ tr/-_/  /;
734    $self->add_css( "_$outfile.css", 0, $name, 0, 0, \$this_css);
735  }
736
737  return;
738}
739
740sub _color_negate {
741  my $x = lc $_[0];
742  $x =~ tr[0123456789abcdef]
743          [fedcba9876543210];
744  return $x;
745}
746
747#===========================================================================
748
749sub add_javascript {
750  my($self, $url, $content_type, $_code) = @_;
751  return unless $url;
752  push  @{ $self->_javascript_wad }, [
753    $url, $content_type || 'text/javascript', $_code
754  ];
755  return;
756}
757
758sub _spray_javascript {
759  my($self, $outdir) = @_;
760  return unless $self->javascript_flurry();
761  $self->_gen_javascript_wad();
762
763  my $lol = $self->_javascript_wad;
764  foreach my $script (@$lol) {
765    my $url = $script->[0];
766    my $outfile;
767
768    if( ref($script->[-1]) and $url =~ m{^(_[-a-z0-9_]+\.js$)} ) {
769      $outfile = $self->filespecsys->catfile( $outdir, "$1" );
770      DEBUG > 5 and print STDERR "Noting $$script[0] as a file I'll create.\n";
771    } else {
772      DEBUG > 5 and print STDERR "OK, noting $$script[0] as an external JavaScript.\n";
773      next;
774    }
775
776    #$self->muse( "Writing JavaScript file $outfile" );
777    my $Jsout = $self->_wopen($outfile);
778
779    print $Jsout ${$script->[-1]}
780     or warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
781    close($Jsout);
782    DEBUG > 5 and print STDERR "Wrote $outfile\n";
783  }
784
785  return;
786}
787
788sub _gen_javascript_wad {
789  my $self = $_[0];
790  my $js_code = $self->_javascript || return;
791  $self->add_javascript( "_podly.js", 0, \$js_code);
792  return;
793}
794
795sub _javascript_wad_to_markup {
796  my($self, $depth) = @_;
797
798  my @scripts  = @{ $self->_javascript_wad || return '' };
799  return '' unless @scripts;
800
801  my $out = '';
802
803  --$depth;
804  my $uplink = $depth ? ('../' x $depth) : '';
805
806  foreach my $s (@scripts) {
807    next unless $s and @$s;
808
809    my( $url1, $url2, $type, $media) = (
810      $self->_maybe_uplink( $s->[0], $uplink ),
811      esc(grep !ref($_), @$s)
812    );
813
814    $out .= qq{<script type="$type" src="$url1$url2"></script>\n};
815  }
816  return $out;
817}
818
819#===========================================================================
820
821our $CSS = <<'EOCSS';
822/* For accessibility reasons, never specify text sizes in px/pt/pc/in/cm/mm */
823
824@media all { .hide { display: none; } }
825
826@media print {
827  .noprint, div.indexgroup, .backlinktop, .backlinkbottom { display: none }
828
829  * {
830    border-color: black !important;
831    color: black !important;
832    background-color: transparent !important;
833    background-image: none !important;
834  }
835
836  dl.superindex > dd  {
837    word-spacing: .6em;
838  }
839}
840
841@media aural, braille, embossed {
842  div.indexgroup  { display: none; }  /* Too noisy, don't you think? */
843  dl.superindex > dt:before { content: "Group ";  }
844  dl.superindex > dt:after  { content: " contains:"; }
845  .backlinktop    a:before  { content: "Back to contents"; }
846  .backlinkbottom a:before  { content: "Back to contents"; }
847}
848
849@media aural {
850  dl.superindex > dt  { pause-before: 600ms; }
851}
852
853@media screen, tty, tv, projection {
854  .noscreen { display: none; }
855
856  a:link    { color: #7070ff; text-decoration: underline; }
857  a:visited { color: #e030ff; text-decoration: underline; }
858  a:active  { color: #800000; text-decoration: underline; }
859  body.contentspage a            { text-decoration: none; }
860  a.u { color: #fff !important; text-decoration: none; }
861
862  body.pod {
863    margin: 0 5px;
864    color:            #fff;
865    background-color: #000;
866  }
867
868  body.pod h1, body.pod h2, body.pod h3,
869  body.pod h4, body.pod h5, body.pod h6  {
870    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
871    font-weight: normal;
872    margin-top: 1.2em;
873    margin-bottom: .1em;
874    border-top: thin solid transparent;
875    /* margin-left: -5px;  border-left: 2px #7070ff solid;  padding-left: 3px; */
876  }
877
878  body.pod h1  { border-top-color: #0a0; }
879  body.pod h2  { border-top-color: #080; }
880  body.pod h3  { border-top-color: #040; }
881  body.pod h4  { border-top-color: #010; }
882  body.pod h5  { border-top-color: #010; }
883  body.pod h6  { border-top-color: #010; }
884
885  p.backlinktop + h1 { border-top: none; margin-top: 0em;  }
886  p.backlinktop + h2 { border-top: none; margin-top: 0em;  }
887  p.backlinktop + h3 { border-top: none; margin-top: 0em;  }
888  p.backlinktop + h4 { border-top: none; margin-top: 0em;  }
889  p.backlinktop + h5 { border-top: none; margin-top: 0em;  }
890  p.backlinktop + h6 { border-top: none; margin-top: 0em;  }
891
892  body.pod dt {
893    font-size: 105%; /* just a wee bit more than normal */
894  }
895
896  .indexgroup { font-size: 80%; }
897
898  .backlinktop,   .backlinkbottom    {
899    margin-left:  -5px;
900    margin-right: -5px;
901    background-color:         #040;
902    border-top:    thin solid #050;
903    border-bottom: thin solid #050;
904  }
905
906  .backlinktop a, .backlinkbottom a  {
907    text-decoration: none;
908    color: #080;
909    background-color:  #000;
910    border: thin solid #0d0;
911  }
912  .backlinkbottom { margin-bottom: 0; padding-bottom: 0; }
913  .backlinktop    { margin-top:    0; padding-top:    0; }
914
915  body.contentspage {
916    color:            #fff;
917    background-color: #000;
918  }
919
920  body.contentspage h1  {
921    color:            #0d0;
922    margin-left: 1em;
923    margin-right: 1em;
924    text-indent: -.9em;
925    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
926    font-weight: normal;
927    border-top:    thin solid #fff;
928    border-bottom: thin solid #fff;
929    text-align: center;
930  }
931
932  dl.superindex > dt  {
933    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
934    font-weight: normal;
935    font-size: 90%;
936    margin-top: .45em;
937    /* margin-bottom: -.15em; */
938  }
939  dl.superindex > dd  {
940    word-spacing: .6em;    /* most important rule here! */
941  }
942  dl.superindex > a:link  {
943    text-decoration: none;
944    color: #fff;
945  }
946
947  .contentsfooty {
948    border-top: thin solid #999;
949    font-size: 90%;
950  }
951
952}
953
954/* The End */
955
956EOCSS
957
958#==========================================================================
959
960our $JAVASCRIPT = <<'EOJAVASCRIPT';
961
962// From http://www.alistapart.com/articles/alternate/
963
964function setActiveStyleSheet(title) {
965  var i, a, main;
966  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
967    if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) {
968      a.disabled = true;
969      if(a.getAttribute("title") == title) a.disabled = false;
970    }
971  }
972}
973
974function getActiveStyleSheet() {
975  var i, a;
976  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
977    if(   a.getAttribute("rel").indexOf("style") != -1
978       && a.getAttribute("title")
979       && !a.disabled
980       ) return a.getAttribute("title");
981  }
982  return null;
983}
984
985function getPreferredStyleSheet() {
986  var i, a;
987  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
988    if(   a.getAttribute("rel").indexOf("style") != -1
989       && a.getAttribute("rel").indexOf("alt") == -1
990       && a.getAttribute("title")
991       ) return a.getAttribute("title");
992  }
993  return null;
994}
995
996function createCookie(name,value,days) {
997  if (days) {
998    var date = new Date();
999    date.setTime(date.getTime()+(days*24*60*60*1000));
1000    var expires = "; expires="+date.toGMTString();
1001  }
1002  else expires = "";
1003  document.cookie = name+"="+value+expires+"; path=/";
1004}
1005
1006function readCookie(name) {
1007  var nameEQ = name + "=";
1008  var ca = document.cookie.split(';');
1009  for(var i=0  ;  i < ca.length  ;  i++) {
1010    var c = ca[i];
1011    while (c.charAt(0)==' ') c = c.substring(1,c.length);
1012    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
1013  }
1014  return null;
1015}
1016
1017window.onload = function(e) {
1018  var cookie = readCookie("style");
1019  var title = cookie ? cookie : getPreferredStyleSheet();
1020  setActiveStyleSheet(title);
1021}
1022
1023window.onunload = function(e) {
1024  var title = getActiveStyleSheet();
1025  createCookie("style", title, 365);
1026}
1027
1028var cookie = readCookie("style");
1029var title = cookie ? cookie : getPreferredStyleSheet();
1030setActiveStyleSheet(title);
1031
1032// The End
1033
1034EOJAVASCRIPT
1035
1036sub _css_template { return $CSS }
1037sub _javascript   { return $JAVASCRIPT }
1038
1039# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
10401;
1041__END__
1042
1043
1044=head1 NAME
1045
1046Pod::Simple::HTMLBatch - convert several Pod files to several HTML files
1047
1048=head1 SYNOPSIS
1049
1050  perl -MPod::Simple::HTMLBatch -e 'Pod::Simple::HTMLBatch::go' in out
1051
1052
1053=head1 DESCRIPTION
1054
1055This module is used for running batch-conversions of a lot of HTML
1056documents
1057
1058This class is NOT a subclass of Pod::Simple::HTML
1059(nor of bad old Pod::Html) -- although it uses
1060Pod::Simple::HTML for doing the conversion of each document.
1061
1062The normal use of this class is like so:
1063
1064  use Pod::Simple::HTMLBatch;
1065  my $batchconv = Pod::Simple::HTMLBatch->new;
1066  $batchconv->some_option( some_value );
1067  $batchconv->some_other_option( some_other_value );
1068  $batchconv->batch_convert( \@search_dirs, $output_dir );
1069
1070=head2 FROM THE COMMAND LINE
1071
1072Note that this class also provides
1073(but does not export) the function Pod::Simple::HTMLBatch::go.
1074This is basically just a shortcut for C<<
1075Pod::Simple::HTMLBatch->batch_convert(@ARGV) >>.
1076It's meant to be handy for calling from the command line.
1077
1078However, the shortcut requires that you specify exactly two command-line
1079arguments, C<indirs> and C<outdir>.
1080
1081Example:
1082
1083  % mkdir out_html
1084  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go @INC out_html
1085      (to convert the pod from Perl's @INC
1086       files under the directory ./out_html)
1087
1088(Note that the command line there contains a literal atsign-I-N-C.  This
1089is handled as a special case by batch_convert, in order to save you having
1090to enter the odd-looking "" as the first command-line parameter when you
1091mean "just use whatever's in @INC".)
1092
1093Example:
1094
1095  % mkdir ../seekrut
1096  % chmod og-rx ../seekrut
1097  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go . ../seekrut
1098      (to convert the pod under the current dir into HTML
1099       files under the directory ./seekrut)
1100
1101Example:
1102
1103  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go happydocs .
1104      (to convert all pod from happydocs into the current directory)
1105
1106
1107
1108=head1 MAIN METHODS
1109
1110=over
1111
1112=item $batchconv = Pod::Simple::HTMLBatch->new;
1113
1114This creates a new batch converter.  The method doesn't take parameters.
1115To change the converter's attributes, use the L<"/ACCESSOR METHODS">
1116below.
1117
1118=item $batchconv->batch_convert( I<indirs>, I<outdir> );
1119
1120This searches the directories given in I<indirs> and writes
1121HTML files for each of these to a corresponding directory
1122in I<outdir>.  The directory I<outdir> must exist.
1123
1124=item $batchconv->batch_convert( undef    , ...);
1125
1126=item $batchconv->batch_convert( q{@INC}, ...);
1127
1128These two values for I<indirs> specify that the normal Perl @INC
1129
1130=item $batchconv->batch_convert( \@dirs , ...);
1131
1132This specifies that the input directories are the items in
1133the arrayref C<\@dirs>.
1134
1135=item $batchconv->batch_convert( "somedir" , ...);
1136
1137This specifies that the director "somedir" is the input.
1138(This can be an absolute or relative path, it doesn't matter.)
1139
1140A common value you might want would be just "." for the current
1141directory:
1142
1143     $batchconv->batch_convert( "." , ...);
1144
1145
1146=item $batchconv->batch_convert( 'somedir:someother:also' , ...);
1147
1148This specifies that you want the dirs "somedir", "someother", and "also"
1149scanned, just as if you'd passed the arrayref
1150C<[qw( somedir someother also)]>.  Note that a ":"-separator is normal
1151under Unix, but Under MSWin, you'll need C<'somedir;someother;also'>
1152instead, since the pathsep on MSWin is ";" instead of ":".  (And
1153I<that> is because ":" often comes up in paths, like
1154C<"c:/perl/lib">.)
1155
1156(Exactly what separator character should be used, is gotten from
1157C<$Config::Config{'path_sep'}>, via the L<Config> module.)
1158
1159=item $batchconv->batch_convert( ... , undef );
1160
1161This specifies that you want the HTML output to go into the current
1162directory.
1163
1164(Note that a missing or undefined value means a different thing in
1165the first slot than in the second.  That's so that C<batch_convert()>
1166with no arguments (or undef arguments) means "go from @INC, into
1167the current directory.)
1168
1169=item $batchconv->batch_convert( ... , 'somedir' );
1170
1171This specifies that you want the HTML output to go into the
1172directory 'somedir'.
1173(This can be an absolute or relative path, it doesn't matter.)
1174
1175=back
1176
1177
1178Note that you can also call C<batch_convert> as a class method,
1179like so:
1180
1181  Pod::Simple::HTMLBatch->batch_convert( ... );
1182
1183That is just short for this:
1184
1185  Pod::Simple::HTMLBatch-> new-> batch_convert(...);
1186
1187That is, it runs a conversion with default options, for
1188whatever inputdirs and output dir you specify.
1189
1190
1191=head2 ACCESSOR METHODS
1192
1193The following are all accessor methods -- that is, they don't do anything
1194on their own, but just alter the contents of the conversion object,
1195which comprises the options for this particular batch conversion.
1196
1197We show the "put" form of the accessors below (i.e., the syntax you use
1198for setting the accessor to a specific value).  But you can also
1199call each method with no parameters to get its current value.  For
1200example, C<< $self->contents_file() >> returns the current value of
1201the contents_file attribute.
1202
1203=over
1204
1205
1206=item $batchconv->verbose( I<nonnegative_integer> );
1207
1208This controls how verbose to be during batch conversion, as far as
1209notes to STDOUT (or whatever is C<select>'d) about how the conversion
1210is going.  If 0, no progress information is printed.
1211If 1 (the default value), some progress information is printed.
1212Higher values print more information.
1213
1214
1215=item $batchconv->index( I<true-or-false> );
1216
1217This controls whether or not each HTML page is liable to have a little
1218table of contents at the top (which we call an "index" for historical
1219reasons).  This is true by default.
1220
1221
1222=item $batchconv->contents_file( I<filename> );
1223
1224If set, should be the name of a file (in the output directory)
1225to write the HTML index to.  The default value is "index.html".
1226If you set this to a false value, no contents file will be written.
1227
1228=item $batchconv->contents_page_start( I<HTML_string> );
1229
1230This specifies what string should be put at the beginning of
1231the contents page.
1232The default is a string more or less like this:
1233
1234  <html>
1235  <head><title>Perl Documentation</title></head>
1236  <body class='contentspage'>
1237  <h1>Perl Documentation</h1>
1238
1239=item $batchconv->contents_page_end( I<HTML_string> );
1240
1241This specifies what string should be put at the end of the contents page.
1242The default is a string more or less like this:
1243
1244  <p class='contentsfooty'>Generated by
1245  Pod::Simple::HTMLBatch v3.01 under Perl v5.008
1246  <br >At Fri May 14 22:26:42 2004 GMT,
1247  which is Fri May 14 14:26:42 2004 local time.</p>
1248
1249
1250
1251=item $batchconv->add_css( $url );
1252
1253TODO
1254
1255=item $batchconv->add_javascript( $url );
1256
1257TODO
1258
1259=item $batchconv->css_flurry( I<true-or-false> );
1260
1261If true (the default value), we autogenerate some CSS files in the
1262output directory, and set our HTML files to use those.
1263TODO: continue
1264
1265=item $batchconv->javascript_flurry( I<true-or-false> );
1266
1267If true (the default value), we autogenerate a JavaScript in the
1268output directory, and set our HTML files to use it.  Currently,
1269the JavaScript is used only to get the browser to remember what
1270stylesheet it prefers.
1271TODO: continue
1272
1273=item $batchconv->no_contents_links( I<true-or-false> );
1274
1275TODO
1276
1277=item $batchconv->html_render_class( I<classname> );
1278
1279This sets what class is used for rendering the files.
1280The default is "Pod::Simple::HTML".  If you set it to something else,
1281it should probably be a subclass of Pod::Simple::HTML, and you should
1282C<require> or C<use> that class so that's it's loaded before
1283Pod::Simple::HTMLBatch tries loading it.
1284
1285=item $batchconv->search_class( I<classname> );
1286
1287This sets what class is used for searching for the files.
1288The default is "Pod::Simple::Search".  If you set it to something else,
1289it should probably be a subclass of Pod::Simple::Search, and you should
1290C<require> or C<use> that class so that's it's loaded before
1291Pod::Simple::HTMLBatch tries loading it.
1292
1293=back
1294
1295
1296
1297
1298=head1 NOTES ON CUSTOMIZATION
1299
1300TODO
1301
1302  call add_css($someurl) to add stylesheet as alternate
1303  call add_css($someurl,1) to add as primary stylesheet
1304
1305  call add_javascript
1306
1307  subclass Pod::Simple::HTML and set $batchconv->html_render_class to
1308    that classname
1309  and maybe override
1310    $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth)
1311  or maybe override
1312    $batchconv->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth)
1313  subclass Pod::Simple::Search and set $batchconv->search_class to
1314    that classname
1315
1316
1317=head1 SEE ALSO
1318
1319L<Pod::Simple>, L<Pod::Simple::HTMLBatch>, L<perlpod>, L<perlpodspec>
1320
1321=head1 SUPPORT
1322
1323Questions or discussion about POD and Pod::Simple should be sent to the
1324pod-people@perl.org mail list. Send an empty email to
1325pod-people-subscribe@perl.org to subscribe.
1326
1327This module is managed in an open GitHub repository,
1328L<https://github.com/perl-pod/pod-simple/>. Feel free to fork and contribute, or
1329to clone L<https://github.com/perl-pod/pod-simple.git> and send patches!
1330
1331Patches against Pod::Simple are welcome. Please send bug reports to
1332<bug-pod-simple@rt.cpan.org>.
1333
1334=head1 COPYRIGHT AND DISCLAIMERS
1335
1336Copyright (c) 2002 Sean M. Burke.
1337
1338This library is free software; you can redistribute it and/or modify it
1339under the same terms as Perl itself.
1340
1341This program is distributed in the hope that it will be useful, but
1342without any warranty; without even the implied warranty of
1343merchantability or fitness for a particular purpose.
1344
1345=head1 AUTHOR
1346
1347Pod::Simple was created by Sean M. Burke <sburke@cpan.org>.
1348But don't bother him, he's retired.
1349
1350Pod::Simple is maintained by:
1351
1352=over
1353
1354=item * Allison Randal C<allison@perl.org>
1355
1356=item * Hans Dieter Pearcey C<hdp@cpan.org>
1357
1358=item * David E. Wheeler C<dwheeler@cpan.org>
1359
1360=back
1361
1362=cut
1363use warnings;
1364