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.40'; 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, which is %s local time.</p>\n\n</body></html>\n", 98 esc( 99 ref($new), 100 eval {$new->VERSION} || $VERSION, 101 $], scalar(gmtime), scalar(localtime), 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, body.pod h4 { 875 font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif; 876 font-weight: normal; 877 margin-top: 1.2em; 878 margin-bottom: .1em; 879 border-top: thin solid transparent; 880 /* margin-left: -5px; border-left: 2px #7070ff solid; padding-left: 3px; */ 881 } 882 883 body.pod h1 { border-top-color: #0a0; } 884 body.pod h2 { border-top-color: #080; } 885 body.pod h3 { border-top-color: #040; } 886 body.pod h4 { border-top-color: #010; } 887 888 p.backlinktop + h1 { border-top: none; margin-top: 0em; } 889 p.backlinktop + h2 { border-top: none; margin-top: 0em; } 890 p.backlinktop + h3 { border-top: none; margin-top: 0em; } 891 p.backlinktop + h4 { border-top: none; margin-top: 0em; } 892 893 body.pod dt { 894 font-size: 105%; /* just a wee bit more than normal */ 895 } 896 897 .indexgroup { font-size: 80%; } 898 899 .backlinktop, .backlinkbottom { 900 margin-left: -5px; 901 margin-right: -5px; 902 background-color: #040; 903 border-top: thin solid #050; 904 border-bottom: thin solid #050; 905 } 906 907 .backlinktop a, .backlinkbottom a { 908 text-decoration: none; 909 color: #080; 910 background-color: #000; 911 border: thin solid #0d0; 912 } 913 .backlinkbottom { margin-bottom: 0; padding-bottom: 0; } 914 .backlinktop { margin-top: 0; padding-top: 0; } 915 916 body.contentspage { 917 color: #fff; 918 background-color: #000; 919 } 920 921 body.contentspage h1 { 922 color: #0d0; 923 margin-left: 1em; 924 margin-right: 1em; 925 text-indent: -.9em; 926 font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif; 927 font-weight: normal; 928 border-top: thin solid #fff; 929 border-bottom: thin solid #fff; 930 text-align: center; 931 } 932 933 dl.superindex > dt { 934 font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif; 935 font-weight: normal; 936 font-size: 90%; 937 margin-top: .45em; 938 /* margin-bottom: -.15em; */ 939 } 940 dl.superindex > dd { 941 word-spacing: .6em; /* most important rule here! */ 942 } 943 dl.superindex > a:link { 944 text-decoration: none; 945 color: #fff; 946 } 947 948 .contentsfooty { 949 border-top: thin solid #999; 950 font-size: 90%; 951 } 952 953} 954 955/* The End */ 956 957EOCSS 958 959#========================================================================== 960 961$JAVASCRIPT = <<'EOJAVASCRIPT'; 962 963// From http://www.alistapart.com/articles/alternate/ 964 965function setActiveStyleSheet(title) { 966 var i, a, main; 967 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 968 if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { 969 a.disabled = true; 970 if(a.getAttribute("title") == title) a.disabled = false; 971 } 972 } 973} 974 975function getActiveStyleSheet() { 976 var i, a; 977 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 978 if( a.getAttribute("rel").indexOf("style") != -1 979 && a.getAttribute("title") 980 && !a.disabled 981 ) return a.getAttribute("title"); 982 } 983 return null; 984} 985 986function getPreferredStyleSheet() { 987 var i, a; 988 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 989 if( a.getAttribute("rel").indexOf("style") != -1 990 && a.getAttribute("rel").indexOf("alt") == -1 991 && a.getAttribute("title") 992 ) return a.getAttribute("title"); 993 } 994 return null; 995} 996 997function createCookie(name,value,days) { 998 if (days) { 999 var date = new Date(); 1000 date.setTime(date.getTime()+(days*24*60*60*1000)); 1001 var expires = "; expires="+date.toGMTString(); 1002 } 1003 else expires = ""; 1004 document.cookie = name+"="+value+expires+"; path=/"; 1005} 1006 1007function readCookie(name) { 1008 var nameEQ = name + "="; 1009 var ca = document.cookie.split(';'); 1010 for(var i=0 ; i < ca.length ; i++) { 1011 var c = ca[i]; 1012 while (c.charAt(0)==' ') c = c.substring(1,c.length); 1013 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 1014 } 1015 return null; 1016} 1017 1018window.onload = function(e) { 1019 var cookie = readCookie("style"); 1020 var title = cookie ? cookie : getPreferredStyleSheet(); 1021 setActiveStyleSheet(title); 1022} 1023 1024window.onunload = function(e) { 1025 var title = getActiveStyleSheet(); 1026 createCookie("style", title, 365); 1027} 1028 1029var cookie = readCookie("style"); 1030var title = cookie ? cookie : getPreferredStyleSheet(); 1031setActiveStyleSheet(title); 1032 1033// The End 1034 1035EOJAVASCRIPT 1036 1037# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10381; 1039__END__ 1040 1041 1042=head1 NAME 1043 1044Pod::Simple::HTMLBatch - convert several Pod files to several HTML files 1045 1046=head1 SYNOPSIS 1047 1048 perl -MPod::Simple::HTMLBatch -e 'Pod::Simple::HTMLBatch::go' in out 1049 1050 1051=head1 DESCRIPTION 1052 1053This module is used for running batch-conversions of a lot of HTML 1054documents 1055 1056This class is NOT a subclass of Pod::Simple::HTML 1057(nor of bad old Pod::Html) -- although it uses 1058Pod::Simple::HTML for doing the conversion of each document. 1059 1060The normal use of this class is like so: 1061 1062 use Pod::Simple::HTMLBatch; 1063 my $batchconv = Pod::Simple::HTMLBatch->new; 1064 $batchconv->some_option( some_value ); 1065 $batchconv->some_other_option( some_other_value ); 1066 $batchconv->batch_convert( \@search_dirs, $output_dir ); 1067 1068=head2 FROM THE COMMAND LINE 1069 1070Note that this class also provides 1071(but does not export) the function Pod::Simple::HTMLBatch::go. 1072This is basically just a shortcut for C<< 1073Pod::Simple::HTMLBatch->batch_convert(@ARGV) >>. 1074It's meant to be handy for calling from the command line. 1075 1076However, the shortcut requires that you specify exactly two command-line 1077arguments, C<indirs> and C<outdir>. 1078 1079Example: 1080 1081 % mkdir out_html 1082 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go @INC out_html 1083 (to convert the pod from Perl's @INC 1084 files under the directory ./out_html) 1085 1086(Note that the command line there contains a literal atsign-I-N-C. This 1087is handled as a special case by batch_convert, in order to save you having 1088to enter the odd-looking "" as the first command-line parameter when you 1089mean "just use whatever's in @INC".) 1090 1091Example: 1092 1093 % mkdir ../seekrut 1094 % chmod og-rx ../seekrut 1095 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go . ../seekrut 1096 (to convert the pod under the current dir into HTML 1097 files under the directory ./seekrut) 1098 1099Example: 1100 1101 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go happydocs . 1102 (to convert all pod from happydocs into the current directory) 1103 1104 1105 1106=head1 MAIN METHODS 1107 1108=over 1109 1110=item $batchconv = Pod::Simple::HTMLBatch->new; 1111 1112This creates a new batch converter. The method doesn't take parameters. 1113To change the converter's attributes, use the L<"/ACCESSOR METHODS"> 1114below. 1115 1116=item $batchconv->batch_convert( I<indirs>, I<outdir> ); 1117 1118This searches the directories given in I<indirs> and writes 1119HTML files for each of these to a corresponding directory 1120in I<outdir>. The directory I<outdir> must exist. 1121 1122=item $batchconv->batch_convert( undef , ...); 1123 1124=item $batchconv->batch_convert( q{@INC}, ...); 1125 1126These two values for I<indirs> specify that the normal Perl @INC 1127 1128=item $batchconv->batch_convert( \@dirs , ...); 1129 1130This specifies that the input directories are the items in 1131the arrayref C<\@dirs>. 1132 1133=item $batchconv->batch_convert( "somedir" , ...); 1134 1135This specifies that the director "somedir" is the input. 1136(This can be an absolute or relative path, it doesn't matter.) 1137 1138A common value you might want would be just "." for the current 1139directory: 1140 1141 $batchconv->batch_convert( "." , ...); 1142 1143 1144=item $batchconv->batch_convert( 'somedir:someother:also' , ...); 1145 1146This specifies that you want the dirs "somedir", "someother", and "also" 1147scanned, just as if you'd passed the arrayref 1148C<[qw( somedir someother also)]>. Note that a ":"-separator is normal 1149under Unix, but Under MSWin, you'll need C<'somedir;someother;also'> 1150instead, since the pathsep on MSWin is ";" instead of ":". (And 1151I<that> is because ":" often comes up in paths, like 1152C<"c:/perl/lib">.) 1153 1154(Exactly what separator character should be used, is gotten from 1155C<$Config::Config{'path_sep'}>, via the L<Config> module.) 1156 1157=item $batchconv->batch_convert( ... , undef ); 1158 1159This specifies that you want the HTML output to go into the current 1160directory. 1161 1162(Note that a missing or undefined value means a different thing in 1163the first slot than in the second. That's so that C<batch_convert()> 1164with no arguments (or undef arguments) means "go from @INC, into 1165the current directory.) 1166 1167=item $batchconv->batch_convert( ... , 'somedir' ); 1168 1169This specifies that you want the HTML output to go into the 1170directory 'somedir'. 1171(This can be an absolute or relative path, it doesn't matter.) 1172 1173=back 1174 1175 1176Note that you can also call C<batch_convert> as a class method, 1177like so: 1178 1179 Pod::Simple::HTMLBatch->batch_convert( ... ); 1180 1181That is just short for this: 1182 1183 Pod::Simple::HTMLBatch-> new-> batch_convert(...); 1184 1185That is, it runs a conversion with default options, for 1186whatever inputdirs and output dir you specify. 1187 1188 1189=head2 ACCESSOR METHODS 1190 1191The following are all accessor methods -- that is, they don't do anything 1192on their own, but just alter the contents of the conversion object, 1193which comprises the options for this particular batch conversion. 1194 1195We show the "put" form of the accessors below (i.e., the syntax you use 1196for setting the accessor to a specific value). But you can also 1197call each method with no parameters to get its current value. For 1198example, C<< $self->contents_file() >> returns the current value of 1199the contents_file attribute. 1200 1201=over 1202 1203 1204=item $batchconv->verbose( I<nonnegative_integer> ); 1205 1206This controls how verbose to be during batch conversion, as far as 1207notes to STDOUT (or whatever is C<select>'d) about how the conversion 1208is going. If 0, no progress information is printed. 1209If 1 (the default value), some progress information is printed. 1210Higher values print more information. 1211 1212 1213=item $batchconv->index( I<true-or-false> ); 1214 1215This controls whether or not each HTML page is liable to have a little 1216table of contents at the top (which we call an "index" for historical 1217reasons). This is true by default. 1218 1219 1220=item $batchconv->contents_file( I<filename> ); 1221 1222If set, should be the name of a file (in the output directory) 1223to write the HTML index to. The default value is "index.html". 1224If you set this to a false value, no contents file will be written. 1225 1226=item $batchconv->contents_page_start( I<HTML_string> ); 1227 1228This specifies what string should be put at the beginning of 1229the contents page. 1230The default is a string more or less like this: 1231 1232 <html> 1233 <head><title>Perl Documentation</title></head> 1234 <body class='contentspage'> 1235 <h1>Perl Documentation</h1> 1236 1237=item $batchconv->contents_page_end( I<HTML_string> ); 1238 1239This specifies what string should be put at the end of the contents page. 1240The default is a string more or less like this: 1241 1242 <p class='contentsfooty'>Generated by 1243 Pod::Simple::HTMLBatch v3.01 under Perl v5.008 1244 <br >At Fri May 14 22:26:42 2004 GMT, 1245 which is Fri May 14 14:26:42 2004 local time.</p> 1246 1247 1248 1249=item $batchconv->add_css( $url ); 1250 1251TODO 1252 1253=item $batchconv->add_javascript( $url ); 1254 1255TODO 1256 1257=item $batchconv->css_flurry( I<true-or-false> ); 1258 1259If true (the default value), we autogenerate some CSS files in the 1260output directory, and set our HTML files to use those. 1261TODO: continue 1262 1263=item $batchconv->javascript_flurry( I<true-or-false> ); 1264 1265If true (the default value), we autogenerate a JavaScript in the 1266output directory, and set our HTML files to use it. Currently, 1267the JavaScript is used only to get the browser to remember what 1268stylesheet it prefers. 1269TODO: continue 1270 1271=item $batchconv->no_contents_links( I<true-or-false> ); 1272 1273TODO 1274 1275=item $batchconv->html_render_class( I<classname> ); 1276 1277This sets what class is used for rendering the files. 1278The default is "Pod::Simple::HTML". If you set it to something else, 1279it should probably be a subclass of Pod::Simple::HTML, and you should 1280C<require> or C<use> that class so that's it's loaded before 1281Pod::Simple::HTMLBatch tries loading it. 1282 1283=item $batchconv->search_class( I<classname> ); 1284 1285This sets what class is used for searching for the files. 1286The default is "Pod::Simple::Search". If you set it to something else, 1287it should probably be a subclass of Pod::Simple::Search, and you should 1288C<require> or C<use> that class so that's it's loaded before 1289Pod::Simple::HTMLBatch tries loading it. 1290 1291=back 1292 1293 1294 1295 1296=head1 NOTES ON CUSTOMIZATION 1297 1298TODO 1299 1300 call add_css($someurl) to add stylesheet as alternate 1301 call add_css($someurl,1) to add as primary stylesheet 1302 1303 call add_javascript 1304 1305 subclass Pod::Simple::HTML and set $batchconv->html_render_class to 1306 that classname 1307 and maybe override 1308 $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth) 1309 or maybe override 1310 $batchconv->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth) 1311 subclass Pod::Simple::Search and set $batchconv->search_class to 1312 that classname 1313 1314 1315=head1 SEE ALSO 1316 1317L<Pod::Simple>, L<Pod::Simple::HTMLBatch>, L<perlpod>, L<perlpodspec> 1318 1319=head1 SUPPORT 1320 1321Questions or discussion about POD and Pod::Simple should be sent to the 1322pod-people@perl.org mail list. Send an empty email to 1323pod-people-subscribe@perl.org to subscribe. 1324 1325This module is managed in an open GitHub repository, 1326L<https://github.com/perl-pod/pod-simple/>. Feel free to fork and contribute, or 1327to clone L<git://github.com/perl-pod/pod-simple.git> and send patches! 1328 1329Patches against Pod::Simple are welcome. Please send bug reports to 1330<bug-pod-simple@rt.cpan.org>. 1331 1332=head1 COPYRIGHT AND DISCLAIMERS 1333 1334Copyright (c) 2002 Sean M. Burke. 1335 1336This library is free software; you can redistribute it and/or modify it 1337under the same terms as Perl itself. 1338 1339This program is distributed in the hope that it will be useful, but 1340without any warranty; without even the implied warranty of 1341merchantability or fitness for a particular purpose. 1342 1343=head1 AUTHOR 1344 1345Pod::Simple was created by Sean M. Burke <sburke@cpan.org>. 1346But don't bother him, he's retired. 1347 1348Pod::Simple is maintained by: 1349 1350=over 1351 1352=item * Allison Randal C<allison@perl.org> 1353 1354=item * Hans Dieter Pearcey C<hdp@cpan.org> 1355 1356=item * David E. Wheeler C<dwheeler@cpan.org> 1357 1358=back 1359 1360=cut 1361