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