xref: /openbsd-src/gnu/usr.bin/perl/ext/Pod-Html/lib/Pod/Html.pm (revision 68dd5bb1859285b71cb62a10bf107b8ad54064d9)
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_">&nbsp;$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