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.14'; 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 "%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 "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 " 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 "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 "Modules found (name => path):\n"; 549 foreach my $m (sort {lc($a) cmp lc($b)} keys %$m2p) { 550 print " $m $$m2p{$m}\n"; 551 } 552 print "(total ", scalar(keys %$m2p), ")\n\n"; 553 } elsif( DEBUG ) { 554 print "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 "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 "Noting $$chunk[0] as a file I'll create.\n"; 612 } else { 613 DEBUG > 5 and print "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 "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 foreach my $variation ( 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 $outname = "$variation\_"; 730 my $this_css = join "\n", 731 "/* This file is autogenerated. Do not edit. $outname */\n", 732 "\@import url(\"./_$variation.css\");", 733 ".indexgroup { display: none; }", 734 "\n", 735 ; 736 my $name = $outname; 737 $name =~ tr/-_/ /; 738 $self->add_css( "$outname.css", 0, $name, 0, 0, \$this_css); 739 } 740 741 return; 742} 743 744sub _color_negate { 745 my $x = lc $_[0]; 746 $x =~ tr[0123456789abcdef] 747 [fedcba9876543210]; 748 return $x; 749} 750 751#=========================================================================== 752 753sub add_javascript { 754 my($self, $url, $content_type, $_code) = @_; 755 return unless $url; 756 push @{ $self->_javascript_wad }, [ 757 $url, $content_type || 'text/javascript', $_code 758 ]; 759 return; 760} 761 762sub _spray_javascript { 763 my($self, $outdir) = @_; 764 return unless $self->javascript_flurry(); 765 $self->_gen_javascript_wad(); 766 767 my $lol = $self->_javascript_wad; 768 foreach my $script (@$lol) { 769 my $url = $script->[0]; 770 my $outfile; 771 772 if( ref($script->[-1]) and $url =~ m{^(_[-a-z0-9_]+\.js$)} ) { 773 $outfile = $self->filespecsys->catfile( $outdir, "$1" ); 774 DEBUG > 5 and print "Noting $$script[0] as a file I'll create.\n"; 775 } else { 776 DEBUG > 5 and print "OK, noting $$script[0] as an external JavaScript.\n"; 777 next; 778 } 779 780 #$self->muse( "Writing JavaScript file $outfile" ); 781 my $Jsout = $self->_wopen($outfile); 782 783 print $Jsout ${$script->[-1]} 784 or warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all"; 785 close($Jsout); 786 DEBUG > 5 and print "Wrote $outfile\n"; 787 } 788 789 return; 790} 791 792sub _gen_javascript_wad { 793 my $self = $_[0]; 794 my $js_code = $self->_javascript || return; 795 $self->add_javascript( "_podly.js", 0, \$js_code); 796 return; 797} 798 799sub _javascript_wad_to_markup { 800 my($self, $depth) = @_; 801 802 my @scripts = @{ $self->_javascript_wad || return '' }; 803 return '' unless @scripts; 804 805 my $out = ''; 806 807 --$depth; 808 my $uplink = $depth ? ('../' x $depth) : ''; 809 810 foreach my $s (@scripts) { 811 next unless $s and @$s; 812 813 my( $url1, $url2, $type, $media) = ( 814 $self->_maybe_uplink( $s->[0], $uplink ), 815 esc(grep !ref($_), @$s) 816 ); 817 818 $out .= qq{<script type="$type" src="$url1$url2"></script>\n}; 819 } 820 return $out; 821} 822 823#=========================================================================== 824 825sub _css_template { return $CSS } 826sub _javascript { return $JAVASCRIPT } 827 828$CSS = <<'EOCSS'; 829/* For accessibility reasons, never specify text sizes in px/pt/pc/in/cm/mm */ 830 831@media all { .hide { display: none; } } 832 833@media print { 834 .noprint, div.indexgroup, .backlinktop, .backlinkbottom { display: none } 835 836 * { 837 border-color: black !important; 838 color: black !important; 839 background-color: transparent !important; 840 background-image: none !important; 841 } 842 843 dl.superindex > dd { 844 word-spacing: .6em; 845 } 846} 847 848@media aural, braille, embossed { 849 div.indexgroup { display: none; } /* Too noisy, don't you think? */ 850 dl.superindex > dt:before { content: "Group "; } 851 dl.superindex > dt:after { content: " contains:"; } 852 .backlinktop a:before { content: "Back to contents"; } 853 .backlinkbottom a:before { content: "Back to contents"; } 854} 855 856@media aural { 857 dl.superindex > dt { pause-before: 600ms; } 858} 859 860@media screen, tty, tv, projection { 861 .noscreen { display: none; } 862 863 a:link { color: #7070ff; text-decoration: underline; } 864 a:visited { color: #e030ff; text-decoration: underline; } 865 a:active { color: #800000; text-decoration: underline; } 866 body.contentspage a { text-decoration: none; } 867 a.u { color: #fff !important; text-decoration: none; } 868 869 body.pod { 870 margin: 0 5px; 871 color: #fff; 872 background-color: #000; 873 } 874 875 body.pod h1, body.pod h2, body.pod h3, body.pod h4 { 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 889 p.backlinktop + h1 { border-top: none; margin-top: 0em; } 890 p.backlinktop + h2 { border-top: none; margin-top: 0em; } 891 p.backlinktop + h3 { border-top: none; margin-top: 0em; } 892 p.backlinktop + h4 { border-top: none; margin-top: 0em; } 893 894 body.pod dt { 895 font-size: 105%; /* just a wee bit more than normal */ 896 } 897 898 .indexgroup { font-size: 80%; } 899 900 .backlinktop, .backlinkbottom { 901 margin-left: -5px; 902 margin-right: -5px; 903 background-color: #040; 904 border-top: thin solid #050; 905 border-bottom: thin solid #050; 906 } 907 908 .backlinktop a, .backlinkbottom a { 909 text-decoration: none; 910 color: #080; 911 background-color: #000; 912 border: thin solid #0d0; 913 } 914 .backlinkbottom { margin-bottom: 0; padding-bottom: 0; } 915 .backlinktop { margin-top: 0; padding-top: 0; } 916 917 body.contentspage { 918 color: #fff; 919 background-color: #000; 920 } 921 922 body.contentspage h1 { 923 color: #0d0; 924 margin-left: 1em; 925 margin-right: 1em; 926 text-indent: -.9em; 927 font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif; 928 font-weight: normal; 929 border-top: thin solid #fff; 930 border-bottom: thin solid #fff; 931 text-align: center; 932 } 933 934 dl.superindex > dt { 935 font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif; 936 font-weight: normal; 937 font-size: 90%; 938 margin-top: .45em; 939 /* margin-bottom: -.15em; */ 940 } 941 dl.superindex > dd { 942 word-spacing: .6em; /* most important rule here! */ 943 } 944 dl.superindex > a:link { 945 text-decoration: none; 946 color: #fff; 947 } 948 949 .contentsfooty { 950 border-top: thin solid #999; 951 font-size: 90%; 952 } 953 954} 955 956/* The End */ 957 958EOCSS 959 960#========================================================================== 961 962$JAVASCRIPT = <<'EOJAVASCRIPT'; 963 964// From http://www.alistapart.com/articles/alternate/ 965 966function setActiveStyleSheet(title) { 967 var i, a, main; 968 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 969 if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { 970 a.disabled = true; 971 if(a.getAttribute("title") == title) a.disabled = false; 972 } 973 } 974} 975 976function getActiveStyleSheet() { 977 var i, a; 978 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 979 if( a.getAttribute("rel").indexOf("style") != -1 980 && a.getAttribute("title") 981 && !a.disabled 982 ) return a.getAttribute("title"); 983 } 984 return null; 985} 986 987function getPreferredStyleSheet() { 988 var i, a; 989 for(i=0 ; (a = document.getElementsByTagName("link")[i]) ; i++) { 990 if( a.getAttribute("rel").indexOf("style") != -1 991 && a.getAttribute("rel").indexOf("alt") == -1 992 && a.getAttribute("title") 993 ) return a.getAttribute("title"); 994 } 995 return null; 996} 997 998function createCookie(name,value,days) { 999 if (days) { 1000 var date = new Date(); 1001 date.setTime(date.getTime()+(days*24*60*60*1000)); 1002 var expires = "; expires="+date.toGMTString(); 1003 } 1004 else expires = ""; 1005 document.cookie = name+"="+value+expires+"; path=/"; 1006} 1007 1008function readCookie(name) { 1009 var nameEQ = name + "="; 1010 var ca = document.cookie.split(';'); 1011 for(var i=0 ; i < ca.length ; i++) { 1012 var c = ca[i]; 1013 while (c.charAt(0)==' ') c = c.substring(1,c.length); 1014 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 1015 } 1016 return null; 1017} 1018 1019window.onload = function(e) { 1020 var cookie = readCookie("style"); 1021 var title = cookie ? cookie : getPreferredStyleSheet(); 1022 setActiveStyleSheet(title); 1023} 1024 1025window.onunload = function(e) { 1026 var title = getActiveStyleSheet(); 1027 createCookie("style", title, 365); 1028} 1029 1030var cookie = readCookie("style"); 1031var title = cookie ? cookie : getPreferredStyleSheet(); 1032setActiveStyleSheet(title); 1033 1034// The End 1035 1036EOJAVASCRIPT 1037 1038# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10391; 1040__END__ 1041 1042 1043=head1 NAME 1044 1045Pod::Simple::HTMLBatch - convert several Pod files to several HTML files 1046 1047=head1 SYNOPSIS 1048 1049 perl -MPod::Simple::HTMLBatch -e 'Pod::Simple::HTMLBatch::go' in out 1050 1051 1052=head1 DESCRIPTION 1053 1054This module is used for running batch-conversions of a lot of HTML 1055documents 1056 1057This class is NOT a subclass of Pod::Simple::HTML 1058(nor of bad old Pod::Html) -- although it uses 1059Pod::Simple::HTML for doing the conversion of each document. 1060 1061The normal use of this class is like so: 1062 1063 use Pod::Simple::HTMLBatch; 1064 my $batchconv = Pod::Simple::HTMLBatch->new; 1065 $batchconv->some_option( some_value ); 1066 $batchconv->some_other_option( some_other_value ); 1067 $batchconv->batch_convert( \@search_dirs, $output_dir ); 1068 1069=head2 FROM THE COMMAND LINE 1070 1071Note that this class also provides 1072(but does not export) the function Pod::Simple::HTMLBatch::go. 1073This is basically just a shortcut for C<< 1074Pod::Simple::HTMLBatch->batch_convert(@ARGV) >>. 1075It's meant to be handy for calling from the command line. 1076 1077However, the shortcut requires that you specify exactly two command-line 1078arguments, C<indirs> and C<outdir>. 1079 1080Example: 1081 1082 % mkdir out_html 1083 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go @INC out_html 1084 (to convert the pod from Perl's @INC 1085 files under the directory ../htmlversion) 1086 1087(Note that the command line there contains a literal atsign-I-N-C. This 1088is handled as a special case by batch_convert, in order to save you having 1089to enter the odd-looking "" as the first command-line parameter when you 1090mean "just use whatever's in @INC".) 1091 1092Example: 1093 1094 % mkdir ../seekrut 1095 % chmod og-rx ../seekrut 1096 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go . ../htmlversion 1097 (to convert the pod under the current dir into HTML 1098 files under the directory ../htmlversion) 1099 1100Example: 1101 1102 % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go happydocs . 1103 (to convert all pod from happydocs into the current directory) 1104 1105 1106 1107=head1 MAIN METHODS 1108 1109=over 1110 1111=item $batchconv = Pod::Simple::HTMLBatch->new; 1112 1113This TODO 1114 1115 1116=item $batchconv->batch_convert( I<indirs>, I<outdir> ); 1117 1118this TODO 1119 1120=item $batchconv->batch_convert( undef , ...); 1121 1122=item $batchconv->batch_convert( q{@INC}, ...); 1123 1124These two values for I<indirs> specify that the normal Perl @INC 1125 1126=item $batchconv->batch_convert( \@dirs , ...); 1127 1128This specifies that the input directories are the items in 1129the arrayref C<\@dirs>. 1130 1131=item $batchconv->batch_convert( "somedir" , ...); 1132 1133This specifies that the director "somedir" is the input. 1134(This can be an absolute or relative path, it doesn't matter.) 1135 1136A common value you might want would be just "." for the current 1137directory: 1138 1139 $batchconv->batch_convert( "." , ...); 1140 1141 1142=item $batchconv->batch_convert( 'somedir:someother:also' , ...); 1143 1144This specifies that you want the dirs "somedir", "somother", and "also" 1145scanned, just as if you'd passed the arrayref 1146C<[qw( somedir someother also)]>. Note that a ":"-separator is normal 1147under Unix, but Under MSWin, you'll need C<'somedir;someother;also'> 1148instead, since the pathsep on MSWin is ";" instead of ":". (And 1149I<that> is because ":" often comes up in paths, like 1150C<"c:/perl/lib">.) 1151 1152(Exactly what separator character should be used, is gotten from 1153C<$Config::Config{'path_sep'}>, via the L<Config> module.) 1154 1155=item $batchconv->batch_convert( ... , undef ); 1156 1157This specifies that you want the HTML output to go into the current 1158directory. 1159 1160(Note that a missing or undefined value means a different thing in 1161the first slot than in the second. That's so that C<batch_convert()> 1162with no arguments (or undef arguments) means "go from @INC, into 1163the current directory.) 1164 1165=item $batchconv->batch_convert( ... , 'somedir' ); 1166 1167This specifies that you want the HTML output to go into the 1168directory 'somedir'. 1169(This can be an absolute or relative path, it doesn't matter.) 1170 1171=back 1172 1173 1174Note that you can also call C<batch_convert> as a class method, 1175like so: 1176 1177 Pod::Simple::HTMLBatch->batch_convert( ... ); 1178 1179That is just short for this: 1180 1181 Pod::Simple::HTMLBatch-> new-> batch_convert(...); 1182 1183That is, it runs a conversion with default options, for 1184whatever inputdirs and output dir you specify. 1185 1186 1187=head2 ACCESSOR METHODS 1188 1189The following are all accessor methods -- that is, they don't do anything 1190on their own, but just alter the contents of the conversion object, 1191which comprises the options for this particular batch conversion. 1192 1193We show the "put" form of the accessors below (i.e., the syntax you use 1194for setting the accessor to a specific value). But you can also 1195call each method with no parameters to get its current value. For 1196example, C<< $self->contents_file() >> returns the current value of 1197the contents_file attribute. 1198 1199=over 1200 1201 1202=item $batchconv->verbose( I<nonnegative_integer> ); 1203 1204This controls how verbose to be during batch conversion, as far as 1205notes to STDOUT (or whatever is C<select>'d) about how the conversion 1206is going. If 0, no progress information is printed. 1207If 1 (the default value), some progress information is printed. 1208Higher values print more information. 1209 1210 1211=item $batchconv->index( I<true-or-false> ); 1212 1213This controls whether or not each HTML page is liable to have a little 1214table of contents at the top (which we call an "index" for historical 1215reasons). This is true by default. 1216 1217 1218=item $batchconv->contents_file( I<filename> ); 1219 1220If set, should be the name of a file (in the output directory) 1221to write the HTML index to. The default value is "index.html". 1222If you set this to a false value, no contents file will be written. 1223 1224=item $batchconv->contents_page_start( I<HTML_string> ); 1225 1226This specifies what string should be put at the beginning of 1227the contents page. 1228The default is a string more or less like this: 1229 1230 <html> 1231 <head><title>Perl Documentation</title></head> 1232 <body class='contentspage'> 1233 <h1>Perl Documentation</h1> 1234 1235=item $batchconv->contents_page_end( I<HTML_string> ); 1236 1237This specifies what string should be put at the end of the contents page. 1238The default is a string more or less like this: 1239 1240 <p class='contentsfooty'>Generated by 1241 Pod::Simple::HTMLBatch v3.01 under Perl v5.008 1242 <br >At Fri May 14 22:26:42 2004 GMT, 1243 which is Fri May 14 14:26:42 2004 local time.</p> 1244 1245 1246 1247=item $batchconv->add_css( $url ); 1248 1249TODO 1250 1251=item $batchconv->add_javascript( $url ); 1252 1253TODO 1254 1255=item $batchconv->css_flurry( I<true-or-false> ); 1256 1257If true (the default value), we autogenerate some CSS files in the 1258output directory, and set our HTML files to use those. 1259TODO: continue 1260 1261=item $batchconv->javascript_flurry( I<true-or-false> ); 1262 1263If true (the default value), we autogenerate a JavaScript in the 1264output directory, and set our HTML files to use it. Currently, 1265the JavaScript is used only to get the browser to remember what 1266stylesheet it prefers. 1267TODO: continue 1268 1269=item $batchconv->no_contents_links( I<true-or-false> ); 1270 1271TODO 1272 1273=item $batchconv->html_render_class( I<classname> ); 1274 1275This sets what class is used for rendering the files. 1276The default is "Pod::Simple::HTML". If you set it to something else, 1277it should probably be a subclass of Pod::Simple::HTML, and you should 1278C<require> or C<use> that class so that's it's loaded before 1279Pod::Simple::HTMLBatch tries loading it. 1280 1281=item $batchconv->search_class( I<classname> ); 1282 1283This sets what class is used for searching for the files. 1284The default is "Pod::Simple::Search". If you set it to something else, 1285it should probably be a subclass of Pod::Simple::Search, and you should 1286C<require> or C<use> that class so that's it's loaded before 1287Pod::Simple::HTMLBatch tries loading it. 1288 1289=back 1290 1291 1292 1293 1294=head1 NOTES ON CUSTOMIZATION 1295 1296TODO 1297 1298 call add_css($someurl) to add stylesheet as alternate 1299 call add_css($someurl,1) to add as primary stylesheet 1300 1301 call add_javascript 1302 1303 subclass Pod::Simple::HTML and set $batchconv->html_render_class to 1304 that classname 1305 and maybe override 1306 $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth) 1307 or maybe override 1308 $batchconv->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth) 1309 subclass Pod::Simple::Search and set $batchconv->search_class to 1310 that classname 1311 1312 1313 1314=head1 ASK ME! 1315 1316If you want to do some kind of big pod-to-HTML version with some 1317particular kind of option that you don't see how to achieve using this 1318module, email me (C<sburke@cpan.org>) and I'll probably have a good idea 1319how to do it. For reasons of concision and energetic laziness, some 1320methods and options in this module (and the dozen modules it depends on) 1321are undocumented; but one of those undocumented bits might be just what 1322you're looking for. 1323 1324 1325=head1 SEE ALSO 1326 1327L<Pod::Simple>, L<Pod::Simple::HTMLBatch>, L<perlpod>, L<perlpodspec> 1328 1329=head1 SUPPORT 1330 1331Questions or discussion about POD and Pod::Simple should be sent to the 1332pod-people@perl.org mail list. Send an empty email to 1333pod-people-subscribe@perl.org to subscribe. 1334 1335This module is managed in an open GitHub repository, 1336L<http://github.com/theory/pod-simple/>. Feel free to fork and contribute, or 1337to clone L<git://github.com/theory/pod-simple.git> and send patches! 1338 1339Patches against Pod::Simple are welcome. Please send bug reports to 1340<bug-pod-simple@rt.cpan.org>. 1341 1342=head1 COPYRIGHT AND DISCLAIMERS 1343 1344Copyright (c) 2002 Sean M. Burke. 1345 1346This library is free software; you can redistribute it and/or modify it 1347under the same terms as Perl itself. 1348 1349This program is distributed in the hope that it will be useful, but 1350without any warranty; without even the implied warranty of 1351merchantability or fitness for a particular purpose. 1352 1353=head1 AUTHOR 1354 1355Pod::Simple was created by Sean M. Burke <sburke@cpan.org>. 1356But don't bother him, he's retired. 1357 1358Pod::Simple is maintained by: 1359 1360=over 1361 1362=item * Allison Randal C<allison@perl.org> 1363 1364=item * Hans Dieter Pearcey C<hdp@cpan.org> 1365 1366=item * David E. Wheeler C<dwheeler@cpan.org> 1367 1368=back 1369 1370=cut 1371