1package Pod::Html; 2use strict; 3use Exporter 'import'; 4 5our $VERSION = 1.33; 6$VERSION = eval $VERSION; 7our @EXPORT = qw(pod2html htmlify); 8our @EXPORT_OK = qw(anchorify relativize_url); 9 10use Config; 11use Cwd; 12use File::Basename; 13use File::Spec; 14use Pod::Simple::Search; 15use Pod::Simple::SimpleTree (); 16use Pod::Html::Util qw( 17 html_escape 18 process_command_line 19 trim_leading_whitespace 20 unixify 21 usage 22 htmlify 23 anchorify 24 relativize_url 25); 26use locale; # make \w work right in non-ASCII lands 27 28=head1 NAME 29 30Pod::Html - module to convert pod files to HTML 31 32=head1 SYNOPSIS 33 34 use Pod::Html; 35 pod2html([options]); 36 37=head1 DESCRIPTION 38 39Converts files from pod format (see L<perlpod>) to HTML format. It 40can automatically generate indexes and cross-references, and it keeps 41a cache of things it knows how to cross-reference. 42 43=head1 FUNCTIONS 44 45=head2 pod2html 46 47 pod2html("pod2html", 48 "--podpath=lib:ext:pod:vms", 49 "--podroot=/usr/src/perl", 50 "--htmlroot=/perl/nmanual", 51 "--recurse", 52 "--infile=foo.pod", 53 "--outfile=/perl/nmanual/foo.html"); 54 55pod2html takes the following arguments: 56 57=over 4 58 59=item backlink 60 61 --backlink 62 63Turns every C<head1> heading into a link back to the top of the page. 64By default, no backlinks are generated. 65 66=item cachedir 67 68 --cachedir=name 69 70Creates the directory cache in the given directory. 71 72=item css 73 74 --css=stylesheet 75 76Specify the URL of a cascading style sheet. Also disables all HTML/CSS 77C<style> attributes that are output by default (to avoid conflicts). 78 79=item flush 80 81 --flush 82 83Flushes the directory cache. 84 85=item header 86 87 --header 88 --noheader 89 90Creates header and footer blocks containing the text of the C<NAME> 91section. By default, no headers are generated. 92 93=item help 94 95 --help 96 97Displays the usage message. 98 99=item htmldir 100 101 --htmldir=name 102 103Sets the directory to which all cross references in the resulting 104html file will be relative. Not passing this causes all links to be 105absolute since this is the value that tells Pod::Html the root of the 106documentation tree. 107 108Do not use this and --htmlroot in the same call to pod2html; they are 109mutually exclusive. 110 111=item htmlroot 112 113 --htmlroot=name 114 115Sets the base URL for the HTML files. When cross-references are made, 116the HTML root is prepended to the URL. 117 118Do not use this if relative links are desired: use --htmldir instead. 119 120Do not pass both this and --htmldir to pod2html; they are mutually 121exclusive. 122 123=item index 124 125 --index 126 --noindex 127 128Generate an index at the top of the HTML file. This is the default 129behaviour. 130 131=item infile 132 133 --infile=name 134 135Specify the pod file to convert. Input is taken from STDIN if no 136infile is specified. 137 138=item outfile 139 140 --outfile=name 141 142Specify the HTML file to create. Output goes to STDOUT if no outfile 143is specified. 144 145=item poderrors 146 147 --poderrors 148 --nopoderrors 149 150Include a "POD ERRORS" section in the outfile if there were any POD 151errors in the infile. This section is included by default. 152 153=item podpath 154 155 --podpath=name:...:name 156 157Specify which subdirectories of the podroot contain pod files whose 158HTML converted forms can be linked to in cross references. 159 160=item podroot 161 162 --podroot=name 163 164Specify the base directory for finding library pods. Default is the 165current working directory. 166 167=item quiet 168 169 --quiet 170 --noquiet 171 172Don't display I<mostly harmless> warning messages. These messages 173will be displayed by default. But this is not the same as C<verbose> 174mode. 175 176=item recurse 177 178 --recurse 179 --norecurse 180 181Recurse into subdirectories specified in podpath (default behaviour). 182 183=item title 184 185 --title=title 186 187Specify the title of the resulting HTML file. 188 189=item verbose 190 191 --verbose 192 --noverbose 193 194Display progress messages. By default, they won't be displayed. 195 196=back 197 198=head2 Auxiliary Functions 199 200Prior to perl-5.36, the following three functions were exported by 201F<Pod::Html>, either by default or on request: 202 203=over 4 204 205=item * C<htmlify()> (by default) 206 207=item * C<anchorify()> (upon request) 208 209=item * C<relativize_url()> (upon request) 210 211=back 212 213The definition and documentation of these functions have been moved to 214F<Pod::Html::Util>, viewable via C<perldoc Pod::Html::Util>. 215 216In perl-5.36, these functions will be importable from either F<Pod::Html> or 217F<Pod::Html::Util>. However, beginning with perl-5.38 they will only be 218importable, upon request, from F<Pod::Html::Util>. Please modify your code as 219needed. 220 221=head1 ENVIRONMENT 222 223Uses C<$Config{pod2html}> to setup default options. 224 225=head1 AUTHOR 226 227Marc Green, E<lt>marcgreen@cpan.orgE<gt>. 228 229Original version by Tom Christiansen, E<lt>tchrist@perl.comE<gt>. 230 231=head1 SEE ALSO 232 233L<perlpod> 234 235=head1 COPYRIGHT 236 237This program is distributed under the Artistic License. 238 239=cut 240 241sub new { 242 my $class = shift; 243 return bless {}, $class; 244} 245 246sub pod2html { 247 local(@ARGV) = @_; 248 local $_; 249 250 my $self = Pod::Html->new(); 251 $self->init_globals(); 252 253 my $opts = process_command_line; 254 $self->process_options($opts); 255 256 $self->refine_globals(); 257 258 # load or generate/cache %Pages 259 unless ($self->get_cache()) { 260 # generate %Pages 261 #%Pages = $self->generate_cache(\%Pages); 262 $self->generate_cache($self->{Pages}); 263 } 264 my $input = $self->identify_input(); 265 my $podtree = $self->parse_input_for_podtree($input); 266 $self->set_Title_from_podtree($podtree); 267 268 # set options for the HTML generator 269 my $parser = Pod::Simple::XHTML::LocalPodLinks->new(); 270 $parser->codes_in_verbatim(0); 271 $parser->anchor_items(1); # the old Pod::Html always did 272 $parser->backlink($self->{Backlink}); # linkify =head1 directives 273 $parser->force_title($self->{Title}); 274 $parser->htmldir($self->{Htmldir}); 275 $parser->htmlfileurl($self->{Htmlfileurl}); 276 $parser->htmlroot($self->{Htmlroot}); 277 $parser->index($self->{Doindex}); 278 $parser->output_string(\$self->{output}); # written to file later 279 #$parser->pages(\%Pages); 280 $parser->pages($self->{Pages}); 281 $parser->quiet($self->{Quiet}); 282 $parser->verbose($self->{Verbose}); 283 284 $parser = $self->refine_parser($parser); 285 $self->feed_tree_to_parser($parser, $podtree); 286 $self->write_file(); 287} 288 289sub init_globals { 290 my $self = shift; 291 $self->{Cachedir} = "."; # The directory to which directory caches 292 # will be written. 293 294 $self->{Dircache} = "pod2htmd.tmp"; 295 296 $self->{Htmlroot} = "/"; # http-server base directory from which all 297 # relative paths in $podpath stem. 298 $self->{Htmldir} = ""; # The directory to which the html pages 299 # will (eventually) be written. 300 $self->{Htmlfile} = ""; # write to stdout by default 301 $self->{Htmlfileurl} = ""; # The url that other files would use to 302 # refer to this file. This is only used 303 # to make relative urls that point to 304 # other files. 305 306 $self->{Poderrors} = 1; 307 $self->{Podfile} = ""; # read from stdin by default 308 $self->{Podpath} = []; # list of directories containing library pods. 309 $self->{Podroot} = $self->{Curdir} = File::Spec->curdir; 310 # filesystem base directory from which all 311 # relative paths in $podpath stem. 312 $self->{Css} = ''; # Cascading style sheet 313 $self->{Recurse} = 1; # recurse on subdirectories in $podpath. 314 $self->{Quiet} = 0; # not quiet by default 315 $self->{Verbose} = 0; # not verbose by default 316 $self->{Doindex} = 1; # non-zero if we should generate an index 317 $self->{Backlink} = 0; # no backlinks added by default 318 $self->{Header} = 0; # produce block header/footer 319 $self->{Title} = undef; # title to give the pod(s) 320 $self->{Saved_Cache_Key} = ''; 321 $self->{Pages} = {}; 322 return $self; 323} 324 325sub process_options { 326 my ($self, $opts) = @_; 327 328 $self->{Podpath} = (defined $opts->{podpath}) 329 ? [ split(":", $opts->{podpath}) ] 330 : []; 331 332 $self->{Backlink} = $opts->{backlink} if defined $opts->{backlink}; 333 $self->{Cachedir} = unixify($opts->{cachedir}) if defined $opts->{cachedir}; 334 $self->{Css} = $opts->{css} if defined $opts->{css}; 335 $self->{Header} = $opts->{header} if defined $opts->{header}; 336 $self->{Htmldir} = unixify($opts->{htmldir}) if defined $opts->{htmldir}; 337 $self->{Htmlroot} = unixify($opts->{htmlroot}) if defined $opts->{htmlroot}; 338 $self->{Doindex} = $opts->{index} if defined $opts->{index}; 339 $self->{Podfile} = unixify($opts->{infile}) if defined $opts->{infile}; 340 $self->{Htmlfile} = unixify($opts->{outfile}) if defined $opts->{outfile}; 341 $self->{Poderrors} = $opts->{poderrors} if defined $opts->{poderrors}; 342 $self->{Podroot} = unixify($opts->{podroot}) if defined $opts->{podroot}; 343 $self->{Quiet} = $opts->{quiet} if defined $opts->{quiet}; 344 $self->{Recurse} = $opts->{recurse} if defined $opts->{recurse}; 345 $self->{Title} = $opts->{title} if defined $opts->{title}; 346 $self->{Verbose} = $opts->{verbose} if defined $opts->{verbose}; 347 348 warn "Flushing directory caches\n" 349 if $opts->{verbose} && defined $opts->{flush}; 350 $self->{Dircache} = "$self->{Cachedir}/pod2htmd.tmp"; 351 if (defined $opts->{flush}) { 352 1 while unlink($self->{Dircache}); 353 } 354 return $self; 355} 356 357sub refine_globals { 358 my $self = shift; 359 360 # prevent '//' in urls 361 $self->{Htmlroot} = "" if $self->{Htmlroot} eq "/"; 362 $self->{Htmldir} =~ s#/\z##; 363 364 if ( $self->{Htmlroot} eq '' 365 && defined( $self->{Htmldir} ) 366 && $self->{Htmldir} ne '' 367 && substr( $self->{Htmlfile}, 0, length( $self->{Htmldir} ) ) eq $self->{Htmldir} 368 ) { 369 # Set the 'base' url for this file, so that we can use it 370 # as the location from which to calculate relative links 371 # to other files. If this is '', then absolute links will 372 # be used throughout. 373 #$self->{Htmlfileurl} = "$self->{Htmldir}/" . substr( $self->{Htmlfile}, length( $self->{Htmldir} ) + 1); 374 # Is the above not just "$self->{Htmlfileurl} = $self->{Htmlfile}"? 375 $self->{Htmlfileurl} = unixify($self->{Htmlfile}); 376 } 377 return $self; 378} 379 380sub generate_cache { 381 my $self = shift; 382 my $pwd = getcwd(); 383 chdir($self->{Podroot}) || 384 die "$0: error changing to directory $self->{Podroot}: $!\n"; 385 386 # find all pod modules/pages in podpath, store in %Pages 387 # - inc(0): do not prepend directories in @INC to search list; 388 # limit search to those in @{$self->{Podpath}} 389 # - verbose: report (via 'warn') what search is doing 390 # - laborious: to allow '.' in dirnames (e.g., /usr/share/perl/5.14.1) 391 # - recurse: go into subdirectories 392 # - survey: search for POD files in PodPath 393 my ($name2path, $path2name) = 394 Pod::Simple::Search->new->inc(0)->verbose($self->{Verbose})->laborious(1) 395 ->recurse($self->{Recurse})->survey(@{$self->{Podpath}}); 396 # remove Podroot and extension from each file 397 for my $k (keys %{$name2path}) { 398 $self->{Pages}{$k} = _transform($self, $name2path->{$k}); 399 } 400 401 chdir($pwd) || die "$0: error changing to directory $pwd: $!\n"; 402 403 # cache the directory list for later use 404 warn "caching directories for later use\n" if $self->{Verbose}; 405 open my $cache, '>', $self->{Dircache} 406 or die "$0: error open $self->{Dircache} for writing: $!\n"; 407 408 print $cache join(":", @{$self->{Podpath}}) . "\n$self->{Podroot}\n"; 409 my $_updirs_only = ($self->{Podroot} =~ /\.\./) && !($self->{Podroot} =~ /[^\.\\\/]/); 410 foreach my $key (keys %{$self->{Pages}}) { 411 if($_updirs_only) { 412 my $_dirlevel = $self->{Podroot}; 413 while($_dirlevel =~ /\.\./) { 414 $_dirlevel =~ s/\.\.//; 415 # Assume $Pagesref->{$key} has '/' separators (html dir separators). 416 $self->{Pages}->{$key} =~ s/^[\w\s\-\.]+\///; 417 } 418 } 419 print $cache "$key $self->{Pages}->{$key}\n"; 420 } 421 close $cache or die "error closing $self->{Dircache}: $!"; 422} 423 424sub _transform { 425 my ($self, $v) = @_; 426 $v = $self->{Podroot} eq File::Spec->curdir 427 ? File::Spec->abs2rel($v) 428 : File::Spec->abs2rel($v, 429 File::Spec->canonpath($self->{Podroot})); 430 431 # Convert path to unix style path 432 $v = unixify($v); 433 434 my ($file, $dir) = fileparse($v, qr/\.[^.]*/); # strip .ext 435 return $dir.$file; 436} 437 438sub get_cache { 439 my $self = shift; 440 441 # A first-level cache: 442 # Don't bother reading the cache files if they still apply 443 # and haven't changed since we last read them. 444 445 my $this_cache_key = $self->cache_key(); 446 return 1 if $self->{Saved_Cache_Key} and $this_cache_key eq $self->{Saved_Cache_Key}; 447 $self->{Saved_Cache_Key} = $this_cache_key; 448 449 # load the cache of %Pages if possible. $tests will be 450 # non-zero if successful. 451 my $tests = 0; 452 if (-f $self->{Dircache}) { 453 warn "scanning for directory cache\n" if $self->{Verbose}; 454 $tests = $self->load_cache(); 455 } 456 457 return $tests; 458} 459 460sub cache_key { 461 my $self = shift; 462 return join('!', 463 $self->{Dircache}, 464 $self->{Recurse}, 465 @{$self->{Podpath}}, 466 $self->{Podroot}, 467 stat($self->{Dircache}), 468 ); 469} 470 471# 472# load_cache - tries to find if the cache stored in $dircache is a valid 473# cache of %Pages. if so, it loads them and returns a non-zero value. 474# 475sub load_cache { 476 my $self = shift; 477 my $tests = 0; 478 local $_; 479 480 warn "scanning for directory cache\n" if $self->{Verbose}; 481 open(my $cachefh, '<', $self->{Dircache}) || 482 die "$0: error opening $self->{Dircache} for reading: $!\n"; 483 $/ = "\n"; 484 485 # is it the same podpath? 486 $_ = <$cachefh>; 487 chomp($_); 488 $tests++ if (join(":", @{$self->{Podpath}}) eq $_); 489 490 # is it the same podroot? 491 $_ = <$cachefh>; 492 chomp($_); 493 $tests++ if ($self->{Podroot} eq $_); 494 495 # load the cache if its good 496 if ($tests != 2) { 497 close($cachefh); 498 return 0; 499 } 500 501 warn "loading directory cache\n" if $self->{Verbose}; 502 while (<$cachefh>) { 503 /(.*?) (.*)$/; 504 $self->{Pages}->{$1} = $2; 505 } 506 507 close($cachefh); 508 return 1; 509} 510 511sub identify_input { 512 my $self = shift; 513 my $input; 514 unless (@ARGV && $ARGV[0]) { 515 if ($self->{Podfile} and $self->{Podfile} ne '-') { 516 $input = $self->{Podfile}; 517 } else { 518 $input = '-'; # XXX: make a test case for this 519 } 520 } else { 521 $self->{Podfile} = $ARGV[0]; 522 $input = *ARGV; 523 } 524 return $input; 525} 526 527sub parse_input_for_podtree { 528 my ($self, $input) = @_; 529 # set options for input parser 530 my $input_parser = Pod::Simple::SimpleTree->new; 531 # Normalize whitespace indenting 532 $input_parser->strip_verbatim_indent(\&trim_leading_whitespace); 533 534 $input_parser->codes_in_verbatim(0); 535 $input_parser->accept_targets(qw(html HTML)); 536 $input_parser->no_errata_section(!$self->{Poderrors}); # note the inverse 537 538 warn "Converting input file $self->{Podfile}\n" if $self->{Verbose}; 539 my $podtree = $input_parser->parse_file($input)->root; 540 return $podtree; 541} 542 543sub set_Title_from_podtree { 544 my ($self, $podtree) = @_; 545 unless(defined $self->{Title}) { 546 if($podtree->[0] eq "Document" && ref($podtree->[2]) eq "ARRAY" && 547 $podtree->[2]->[0] eq "head1" && @{$podtree->[2]} == 3 && 548 ref($podtree->[2]->[2]) eq "" && $podtree->[2]->[2] eq "NAME" && 549 ref($podtree->[3]) eq "ARRAY" && $podtree->[3]->[0] eq "Para" && 550 @{$podtree->[3]} >= 3 && 551 !(grep { ref($_) ne "" } 552 @{$podtree->[3]}[2..$#{$podtree->[3]}]) && 553 (@$podtree == 4 || 554 (ref($podtree->[4]) eq "ARRAY" && 555 $podtree->[4]->[0] eq "head1"))) { 556 $self->{Title} = join("", @{$podtree->[3]}[2..$#{$podtree->[3]}]); 557 } 558 } 559 560 $self->{Title} //= ""; 561 $self->{Title} = html_escape($self->{Title}); 562 return $self; 563} 564 565sub refine_parser { 566 my ($self, $parser) = @_; 567 # We need to add this ourselves because we use our own header, not 568 # ::XHTML's header. We need to set $parser->backlink to linkify 569 # the =head1 directives 570 my $bodyid = $self->{Backlink} ? ' id="_podtop_"' : ''; 571 572 my $csslink = ''; 573 my $tdstyle = ' style="background-color: #cccccc; color: #000"'; 574 575 if ($self->{Css}) { 576 $csslink = qq(\n<link rel="stylesheet" href="$self->{Css}" type="text/css" />); 577 $csslink =~ s,\\,/,g; 578 $csslink =~ s,(/.):,$1|,; 579 $tdstyle= ''; 580 } 581 582 # header/footer block 583 my $block = $self->{Header} ? <<END_OF_BLOCK : ''; 584<table border="0" width="100%" cellspacing="0" cellpadding="3"> 585<tr><td class="_podblock_"$tdstyle valign="middle"> 586<big><strong><span class="_podblock_"> $self->{Title}</span></strong></big> 587</td></tr> 588</table> 589END_OF_BLOCK 590 591 # create own header/footer because of --header 592 $parser->html_header(<<"HTMLHEAD"); 593<?xml version="1.0" ?> 594<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 595<html xmlns="http://www.w3.org/1999/xhtml"> 596<head> 597<title>$self->{Title}</title>$csslink 598<meta http-equiv="content-type" content="text/html; charset=utf-8" /> 599<link rev="made" href="mailto:$Config{perladmin}" /> 600</head> 601 602<body$bodyid> 603$block 604HTMLHEAD 605 606 $parser->html_footer(<<"HTMLFOOT"); 607$block 608</body> 609 610</html> 611HTMLFOOT 612 return $parser; 613} 614 615# This sub duplicates the guts of Pod::Simple::FromTree. We could have 616# used that module, except that it would have been a non-core dependency. 617sub feed_tree_to_parser { 618 my($self, $parser, $tree) = @_; 619 if(ref($tree) eq "") { 620 $parser->_handle_text($tree); 621 } elsif(!($tree->[0] eq "X" && $parser->nix_X_codes)) { 622 $parser->_handle_element_start($tree->[0], $tree->[1]); 623 $self->feed_tree_to_parser($parser, $_) foreach @{$tree}[2..$#$tree]; 624 $parser->_handle_element_end($tree->[0]); 625 } 626} 627 628sub write_file { 629 my $self = shift; 630 $self->{Htmlfile} = "-" unless $self->{Htmlfile}; # stdout 631 my $fhout; 632 if($self->{Htmlfile} and $self->{Htmlfile} ne '-') { 633 open $fhout, ">", $self->{Htmlfile} 634 or die "$0: cannot open $self->{Htmlfile} file for output: $!\n"; 635 } else { 636 open $fhout, ">-"; 637 } 638 binmode $fhout, ":utf8"; 639 print $fhout $self->{output}; 640 close $fhout or die "Failed to close $self->{Htmlfile}: $!"; 641 chmod 0644, $self->{Htmlfile} unless $self->{Htmlfile} eq '-'; 642} 643 644package Pod::Simple::XHTML::LocalPodLinks; 645use strict; 646use warnings; 647use parent 'Pod::Simple::XHTML'; 648 649use File::Spec; 650use File::Spec::Unix; 651 652__PACKAGE__->_accessorize( 653 'htmldir', 654 'htmlfileurl', 655 'htmlroot', 656 'pages', # Page name => relative/path/to/page from root POD dir 657 'quiet', 658 'verbose', 659); 660 661sub resolve_pod_page_link { 662 my ($self, $to, $section) = @_; 663 664 return undef unless defined $to || defined $section; 665 if (defined $section) { 666 $section = '#' . $self->idify($section, 1); 667 return $section unless defined $to; 668 } else { 669 $section = ''; 670 } 671 672 my $path; # path to $to according to %Pages 673 unless (exists $self->pages->{$to}) { 674 # Try to find a POD that ends with $to and use that. 675 # e.g., given L<XHTML>, if there is no $Podpath/XHTML in %Pages, 676 # look for $Podpath/*/XHTML in %Pages, with * being any path, 677 # as a substitute (e.g., $Podpath/Pod/Simple/XHTML) 678 my @matches; 679 foreach my $modname (keys %{$self->pages}) { 680 push @matches, $modname if $modname =~ /::\Q$to\E\z/; 681 } 682 683 # make it look like a path instead of a namespace 684 my $modloc = File::Spec->catfile(split(/::/, $to)); 685 686 if ($#matches == -1) { 687 warn "Cannot find file \"$modloc.*\" directly under podpath, " . 688 "cannot find suitable replacement: link remains unresolved.\n" 689 if $self->verbose; 690 return ''; 691 } elsif ($#matches == 0) { 692 $path = $self->pages->{$matches[0]}; 693 my $matchloc = File::Spec->catfile(split(/::/, $path)); 694 warn "Cannot find file \"$modloc.*\" directly under podpath, but ". 695 "I did find \"$matchloc.*\", so I'll assume that is what you ". 696 "meant to link to.\n" 697 if $self->verbose; 698 } else { 699 # Use [-1] so newer (higher numbered) perl PODs are used 700 # XXX currently, @matches isn't sorted so this is not true 701 $path = $self->pages->{$matches[-1]}; 702 my $matchloc = File::Spec->catfile(split(/::/, $path)); 703 warn "Cannot find file \"$modloc.*\" directly under podpath, but ". 704 "I did find \"$matchloc.*\" (among others), so I'll use that " . 705 "to resolve the link.\n" if $self->verbose; 706 } 707 } else { 708 $path = $self->pages->{$to}; 709 } 710 711 my $url = File::Spec::Unix->catfile(Pod::Html::Util::unixify($self->htmlroot), 712 $path); 713 714 if ($self->htmlfileurl ne '') { 715 # then $self->htmlroot eq '' (by definition of htmlfileurl) so 716 # $self->htmldir needs to be prepended to link to get the absolute path 717 # that will be relativized 718 $url = Pod::Html::Util::relativize_url( 719 File::Spec::Unix->catdir(Pod::Html::Util::unixify($self->htmldir), $url), 720 $self->htmlfileurl # already unixified 721 ); 722 } 723 724 return $url . ".html$section"; 725} 726 7271; 728