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> \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"><<</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"><<</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