Ticket #51: manServer_107.pl

File manServer_107.pl, 61.6 KB (added by Yuri Dario, 16 years ago)

perl script for man page conversion to html

Line 
1#!/usr/bin/perl
2
3# manServer - Unix man page to HTML converter
4# Rolf Howarth, rolf@squarebox.co.uk
5# Version 1.07  16 July 2001
6
7$version = "1.07";
8$manServerUrl = "<A HREF=\"http://www.squarebox.co.uk/download/manServer.shtml\">manServer $version</A>";
9
10use Socket;
11
12$ENV{'PATH'} = "/bin:/usr/bin";
13
14initialise();
15$request = shift @ARGV;
16# Usage: manServer [-dn] filename | manServer [-s port]
17
18$root = "";
19$cgiMode = 0;
20$bodyTag = "BODY bgcolor=#F0F0F0 text=#000000 link=#0000ff vlink=#C000C0 alink=#ff0000";
21
22if ($ENV{'GATEWAY_INTERFACE'} ne "")
23{
24        *OUT = *STDOUT;
25        open(LOG, ">>/tmp/manServer.log");
26        chmod(0666, '/tmp/manServer.log');
27        $root = $ENV{'SCRIPT_NAME'};
28        $url = $ENV{'PATH_INFO'};
29        if ($ENV{'REQUEST_METHOD'} eq "POST")
30                { $args = <STDIN>; chop $args; }
31        else
32                { $args = $ENV{'QUERY_STRING'}; }
33        $url .= "?".$args if ($args);
34        $cgiMode = 1;
35        $date = &fmtTime(time);
36        $remoteHost = $ENV{'REMOTE_HOST'} || $ENV{'REMOTE_ADDR'};
37        $referer = $ENV{'HTTP_REFERER'};
38        $userAgent = $ENV{'HTTP_USER_AGENT'};
39        print LOG "$date\t$remoteHost\t$url\t$referer\t$userAgent\n";
40        processRequest($url);
41}
42elsif ($request eq "-s" || $request eq "")
43{
44        *LOG = *STDERR;
45        startServer();
46}
47else
48{
49        $cmdLineMode = 1;
50        if ($request =~ m/^-d(\d)/)
51        {
52                $debug = $1;
53                $request = shift @ARGV;
54        }
55        *OUT = *STDOUT;
56        *LOG = *STDERR;
57        $file = findPage($request);
58        man2html($file);
59}
60
61exit(0);
62
63
64##### Mini HTTP Server ####
65
66sub startServer
67{
68        ($port) = @ARGV;
69        $port = 8888 unless $port;
70
71        $sockaddr = 'S n a4 x8';
72
73        ($name, $aliases, $proto) = getprotobyname('tcp');
74        ($name, $aliases, $port) = getservbyname($port, 'tcp')
75                        unless $port =~ /^\d+$/;
76
77        while(1)
78        {
79                $this = pack($sockaddr, AF_INET, $port, "\0\0\0\0");
80
81                select(NS); $| = 1; select(stdout);
82
83                socket(S, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
84                if (bind(S, $this))
85                {
86                        last;
87                }
88                else
89                {
90                        print STDERR "Failed to bind to port $port: $!\n";
91                        ++$port;
92                }
93        }
94
95        listen(S, 5) || die "connect: $!";
96
97        select(S); $| = 1; select(stdout);
98
99        while(1)
100        {
101                print LOG "Waiting for connection on port $port\n";
102                ($addr = accept(NS,S)) || die $!;
103                #print "accept ok\n";
104
105                ($af,$rport,$inetaddr) = unpack($sockaddr,$addr);
106                @inetaddr = unpack('C4',$inetaddr);
107                print LOG "Got connection from ", join(".",@inetaddr), "\n";
108
109                while (<NS>)
110                {
111                        if (m/^GET (\S+)/) { $url = $1; }
112                        last if (m/^\s*$/);
113                }
114                *OUT = *NS;
115                processRequest($url);
116                close NS ;
117        }
118}
119
120
121sub processRequest
122{
123        $url = $_[0];
124        print LOG "Request = $url, root = $root\n";
125
126        if ( ($url =~ m/^([^?]*)\?(.*)$/) || ($url =~ m/^([^&]*)&(.*)$/) )
127        {
128                $request = $1;
129                $args = $2;
130        }
131        else
132        {
133                $request = $url;
134                $args = "";
135        }
136
137        @params = split(/[=&]/, $args);
138        for ($i=0; $i<=$#params; ++$i)
139        {
140                $params[$i] =~ tr/+/ /;
141                $params[$i] =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack("C",hex($1))/eg;
142        }
143        %params = @params;
144
145        $request = $params{'q'} if ($params{'q'});
146        $searchType = $params{'t'};
147        $debug = $params{'d'};
148
149        $processed = 0;
150        $file = "";
151
152        if ($searchType)
153        {
154                print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode);
155                print OUT "Content-type: text/html\n\n";
156                print OUT "<H1>Searching not yet implemented</H1>\n";
157                print LOG "Searching not implemented\n";
158                $processed = 1;
159        }
160        elsif ($request eq "/" || $request eq "")
161        {
162                print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode);
163                print OUT "Content-type: text/html\n\n";
164                print LOG "Home page\n";
165                homePage();
166                $processed = 1;
167        }
168        elsif ($request =~ m,^/.*/$,)
169        {
170                print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode);
171                print OUT "Content-type: text/html\n\n";
172                print LOG "List directory\n";
173                listDir($request);
174                $processed = 1;
175        }
176        elsif (-f $request || -f "$request.gz" || -f "$request.bz2")
177        {
178                # Only allow fully specified files if they're in our manpath
179                foreach $md (@manpath)
180                {
181                        $dir = $md;
182                        if (substr($request,0,length($dir)) eq $dir)
183                        {
184                                print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode);
185                                print OUT "Content-type: text/html\n\n";
186                                man2html($request);
187                                $processed = 1;
188                                last;
189                        }
190                }
191        }
192        else
193        {
194                $file = findPage($request);
195                if (@multipleMatches)
196                {
197                        print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode);
198                        print OUT "Content-type: text/html\n\n";
199                        print LOG "Multiple matches\n";
200                        printMatches();
201                        $processed = 1;
202                }
203                elsif ($file)
204                {
205                        print OUT "HTTP/1.0 301 Redirected\n" unless ($cgiMode);
206                        $file .= "&d=$debug" if ($debug);
207                        print OUT "Location: $root$file\n\n";
208                        print LOG "Redirect to $root$file\n";
209                        $processed = 1;
210                }
211        }
212
213        unless ($processed)
214        {
215                print OUT "HTTP/1.0 404 Not Found\n" unless ($cgiMode);
216                print OUT "Content-type: text/html\n\n";
217                print OUT "<HTML><HEAD>\n<TITLE>Not Found</TITLE>\n<$bodyTag>\n";
218                print OUT "<CENTER><H1><HR>Not Found<HR></H1></CENTER>\nFailed to find man page /$request\n";
219                print OUT "<P><HR><P><A HREF=\"$root/\">Main Index</A>\n</HTML>\n";
220                print STDERR "Failed to find /$request\n" unless ($cgiMode);
221        }
222}
223
224sub homePage
225{
226        print OUT "<HTML><HEAD><TITLE>Manual Pages - Main Index</TITLE>
227</HEAD><$bodyTag><CENTER><H1><HR><I>Manual Reference Pages</I> - Main Index<HR></H1></CENTER>
228<FORM ACTION=\"$root/\" METHOD=get>\n";
229        $uname = `uname -s -r`;
230        if (! $?)
231        {
232                $hostname = `hostname`;
233                print OUT "<B>$uname pages on $hostname</B><P>\n";
234        }
235        # print OUT "<SELECT name=t> <OPTION selected value=0>Command name
236        # <OPTION value=1>Keyword search <OPTION value=2>Full text search</SELECT>\n";
237        print OUT "Command name: <INPUT name=q size=20> <INPUT type=submit value=\"Show Page\"> </FORM><P>\n";
238        loadManDirs();
239        foreach $dir (@mandirs)
240        {
241                ($section) = ($dir =~ m/man([0-9A-Za-z]+)$/);
242                print OUT "<A HREF=\"$root$dir/\">$dir" ;
243                print OUT "- <I>$sectionName{$section}</I>" if ($sectionName{$section});
244                print OUT "</A><BR>\n";
245        }
246        print OUT "<P><HR><P><FONT SIZE=-1>Generated by $manServerUrl from local unix man pages.</FONT>\n</BODY></HTML>\n";
247}
248
249sub listDir
250{
251        foreach $md (@manpath)
252        {
253                $dir = $md;
254                if (substr($request,0,length($dir)) eq $dir)
255                {
256                        $request =~ s,/$,,;
257                        ($section) = ($request =~ m/man([0-9A-Za-z]+)$/);
258                        $sectionName = $sectionName{$section};
259                        $sectionName = "Manual Reference Pages" unless ($sectionName);
260                        print OUT "<HTML><HEAD><TITLE>Contents of $request</TITLE></HEAD>\n<$bodyTag>\n";
261                        print OUT "<CENTER><H1><HR><NOBR><I>$sectionName</I></NOBR> - <NOBR>Index of $request</NOBR><HR></H1></CENTER>\n";
262                        print OUT "<FORM ACTION=\"$root/\" METHOD=get>\n";
263                        print OUT "Command name: <INPUT name=q size=20> <INPUT type=submit value=\"Show Page\"> </FORM><P>\n";
264
265                        if (opendir(DIR, $request))
266                        {
267                                @files = sort readdir DIR;
268                                foreach $f (@files)
269                                {
270                                        next if ($f eq "." || $f eq ".." || $f !~ m/\./);
271                                        $f =~ s/\.(gz|bz2)$//;
272                                        # ($name) = ($f =~ m,/([^/]*)$,);
273                                        print OUT "<A HREF=\"$root$request/$f\">$f</A>&nbsp;\n";
274                                }
275                                closedir DIR;
276                        }
277                        print OUT "<P><A HREF=\"$root/\">Main Index</A>\n</HTML>\n";
278                        print OUT "<P><HR><P><FONT SIZE=-1>Generated by $manServerUrl from local unix man pages.</FONT>\n</BODY></HTML>\n";
279                        return;
280                }
281        }
282        print OUT "<H1>Directory $request not known</H1>\n";
283}
284
285sub printMatches
286{
287        print OUT "<HTML><HEAD><TITLE>Ambiguous Request '$request'</TITLE></HEAD>\n<$bodyTag>\n";
288        print OUT "<CENTER><H1><HR>Ambiguous Request '$request'<HR></H1></CENTER>\nPlease select one of the following pages:<P><BLOCKQUOTE>";
289        foreach $f (@multipleMatches)
290        {
291                print OUT "<A HREF=\"$root$f\">$f</A><BR>\n";
292        }
293        print OUT "</BLOCKQUOTE><HR><P><A HREF=\"$root/\">Main Index</A>\n</HTML>\n";
294}
295
296
297##### Process troff input using man macros into HTML #####
298
299sub man2html
300{
301        $file = $_[0];
302        $srcfile = $file;
303        $zfile = $file;
304        if (! -f $file)
305        {
306                if (-f "$file.gz")
307                {
308                        $zfile = "$file.gz";
309                        $zcat = "/usr/bin/zcat";
310                        $zcat = "/bin/zcat" unless (-x $zcat);
311                        $srcfile = "$zcat $zfile |";
312                        $srcfile =~ m/^(.*)$/;
313                        $srcfile = $1;  # untaint
314                }
315                elsif (-f "$file.bz2")
316                {
317                        $zfile = "$file.bz2";
318                        $srcfile = "/usr/bin/bzcat $zfile |";
319                        $srcfile =~ m/^(.*)$/;
320                        $srcfile = $1;  # untaint
321                }
322        }
323        print LOG "man2html $file\n";
324        $foundNroffTag = 0;
325        loadContents($file);
326        unless (open(SRC, $srcfile))
327        {
328                print OUT "<H1>Failed to open $file</H1>\n";
329                print STDERR "Failed to open $srcfile\n";
330                return;
331        }
332        ($dir,$page,$sect) = ($file =~ m,^(.*)/([^/]+)\.([^.]+)$,);
333        $troffTable = 0;
334        %macro = ();
335        %renamedMacro = ();
336        %deletedMacro = ();
337        @indent = ();
338        @tabstops = ();
339        $indentLevel = 0;
340        $prevailingIndent = 6;
341        $trapLine = 0;
342        $blockquote = 0;
343        $noSpace = 0;
344        $firstSection = 0;
345        $eqnStart = "";
346        $eqnEnd = "";
347        $eqnMode = 0;
348        %eqndefs = ();
349        $defaultNm = "";
350        $title = $file;
351        $title = "Manual Page - $page($sect)" if ($page && $sect);
352
353        $_ = getLine();
354        if (m/^.so (man.*)$/)
355        {
356                # An .so include on the first line only is replaced by the referenced page.
357                # (See elsewhere for processing of included sections that occur later in document.)
358                man2html("$dir/../$1");
359                return;
360        }
361
362        $perlPattern = "";
363        if ($file =~ m/perl/)
364        {
365                &loadPerlPages();
366                $perlPattern = join('|', grep($_ ne $page, keys %perlPages));
367        }
368
369        print OUT "<HTML><HEAD>\n<TITLE>$title</TITLE>\n<$bodyTag><A NAME=top></A>\n";
370
371        if ($foundNroffTag)
372        {
373                do
374                {
375                        preProcessLine();
376                        processLine();
377                }
378                while(getLine());
379                endNoFill();
380                endParagraph();
381        }
382        else
383        {
384                # Special case where input is not nroff at all but is preformatted text
385                $sectionName = "Manual Reference Pages";
386                $sectionNumber = $sect;
387                $left = "Manual Page";
388                $right = "Manual Page";
389                $macroPackage = "(preformatted text)";
390                $pageName = "$page($sect)";
391                $saveCurrentLine = $_;
392                outputPageHead();
393                $_ = $saveCurrentLine;
394                print OUT "<PRE>\n";
395                do
396                {
397                        print OUT $_;
398                }
399                while(getLine());
400                print OUT "</PRE>\n";
401        }
402        outputPageFooter();
403}
404
405sub outputPageHead
406{
407        plainOutput( "<CENTER>\n" );
408        outputLine( "<H1><HR><I>$sectionName &nbsp;-&nbsp;</I><NOBR>$pageName</NOBR><HR></H1>\n" );
409        plainOutput( "</CENTER>\n" );
410}
411
412sub outputPageFooter
413{
414        if ($pageName)
415        {
416                unless ($cmdLineMode)
417                {
418                        plainOutput( "<FORM ACTION=\"$root/\" METHOD=get>\n" );
419                        plainOutput( "Jump to page &nbsp;<INPUT name=q size=12>&nbsp; or go to <A HREF=#top>Top of page</A>&nbsp;|&nbsp;\n" );
420                        plainOutput( "<A HREF=\"$root$dir/\">Section $sectionNumber</A>&nbsp;|&nbsp;\n" );
421                        plainOutput( "<A HREF=\"$root/\">Main Index</A>.\n" );
422                        plainOutput( "<FORM>\n" );
423                }
424                endBlockquote();
425                outputLine("<P><HR>\n<TABLE width=100%><TR> <TD width=33%><I>$left</I></TD> <TD width=33% align=center>$pageName</TD> <TD align=right width=33%><I>$right</I></TD> </TR></TABLE>");
426        }
427        plainOutput("<FONT SIZE=-1>Generated by $manServerUrl from $zfile $macroPackage.</FONT>\n</BODY></HTML>\n");
428}
429
430sub outputContents
431{
432        print OUT "<A name=contents></A><H3>CONTENTS</H3></A>\n";
433        blockquote();
434        for ($id=1; $id<=$#contents; ++$id)
435        {
436                $name = $contents[$id];
437                $pre = "";
438                $pre = "&nbsp; &nbsp; &nbsp;" if ($name =~ m/^ /);
439                $pre .= "&nbsp; &nbsp; &nbsp;" if ($name =~ m/^  /);
440                $name =~ s,^\s+,,;
441                next if ($name eq "" || $name =~ m,^/,);
442                unless ($name =~ m/[a-z]/)
443                {
444                        $name = "\u\L$name";
445                        $name =~ s/ (.)/ \u\1/g;
446                }
447                outputLine("$pre<A HREF=#$id>$name</A><BR>\n");
448        }
449        endBlockquote();
450}
451
452# First pass to extract table of contents
453sub loadContents
454{
455        @contents = ();
456        %contents = ();
457        # print STDERR "SRCFILE = $srcfile\n";
458        open(SRC, $srcfile) || return;
459        while (<SRC>)
460        {
461                preProcessLine();
462                $foundNroffTag = $foundNroffTag || (m/^\.(\\\"|TH|so) /);
463                if (m/^\.(S[HShs]) ([A-Z].*)\s*$/)
464                {
465                        $foundNroffTag = 1;
466                        $c = $1;
467                        $t = $2;
468                        $t =~ s/"//g;
469                        $id = @contents;
470                        if ($c eq "SH" || $c eq "Sh")
471                        {
472                                push(@contents, $t);
473                        }
474                        elsif ($t =~ m/\\f/)
475                        {
476                                $t =~ s/\\f.//g;
477                                push(@contents, "  $t");
478                        }
479                        else
480                        {
481                                push(@contents, " $t");
482                        }
483                        $contents{"\U$t"} = $id;
484                }
485        }
486        close SRC;
487}
488
489# Preprocess $_
490sub preProcessLine
491{
492        # Remove spurious white space to canonicise the input
493        chop;
494        $origLine = $_;
495        s, $,,g;
496        s,^',.,;        # treat non breaking requests as if there was a dot
497        s,^\.\s*,\.,;
498
499        if ($eqnMode == 1)
500        {
501                if (m/$eqnEnd/)
502                {
503                        s,^(.*?)$eqnEnd,&processEqnd($1),e;
504                        $eqnMode = 0;
505                }
506                else
507                {
508                        &processEqns($_);
509                }
510        }
511        if ($eqnStart && $eqnMode==0)
512        {
513                s,$eqnStart(.*?)$eqnEnd,&processEqnd($1),ge;
514                if (m/$eqnStart/)
515                {
516                        s,$eqnStart(.*)$,&processEqns($1),e;
517                        $eqnMode = 1;
518                }
519        }
520
521        # XXX Note: multiple levels of escaping aren't handled properly, eg. \\*.. as a macro argument
522        # should get interpolated as string but ends up with a literal '\' being copied through to output.
523        s,\\\\\*q,&#34;,g; # treat mdoc \\*q as special case
524       
525        s,\\\\,_DBLSLASH_,g;
526        s,\\ ,_SPACE_,g;
527        s,\s*\\".*$,,;
528        s,\\$,,;
529
530        # Then apply any variable substitutions and escape < and >
531        # (which has to be done before we start inserting tags...)
532        s,\\\*\((..),$vars{$1},ge;
533        s/\\\*([*'`,^,:~].)/$vars{$1}||"\\*$1"/ge;
534        s,\\\*(.),$vars{$1},ge;
535        # Expand special characters for the first time (eg. \(<-
536        s,\\\((..),$special{$1}||"\\($1",ge;
537        s,<,&lt;,g;
538        s,>,&gt;,g;
539
540        # Interpolate width and number registers
541        s,\\w(.)(.*?)\1,&width($2),ge;
542        s,\\n\((..),&numreg($1),ge;
543        s,\\n(.),&numreg($1),ge;
544}
545
546# Undo slash escaping, normally done at output stage, also in macro defn
547sub postProcessLine
548{
549        s,_DBLSLASH_,\\,g;
550        s,_SPACE_, ,g;
551}
552
553# Rewrite the line, expanding escapes such as font styles, and output it.
554# The line may be a plain text troff line, or it might be the expanded output of a
555# macro in which case some HTML tags may already have been inserted into the text.
556sub outputLine
557{
558        $_ = $_[0];
559
560        print OUT "<!-- Output: \"$_\" -->\n" if ($debug>1);
561
562        if ($needBreak)
563        {
564                plainOutput("<!-- Need break --><BR>\n");
565                lineBreak();
566        }
567        if ($textSinceBreak && !$noFill && $_ =~ m/^\s/)
568        {
569                plainOutput("<BR>\n");
570                lineBreak();
571        }
572
573        s,\\&\.,&#46;,g;    # \&. often used to escape dot at start of line
574        s,\\\.,&#46;,g;
575        s,\\\^,,g;
576        s,\\\|,,g;
577        s,\\c,,g;
578        s,\\0,&nbsp;,g;
579        s,\\t,\t,g;
580
581        s,\\%,&nbsp;,g;
582        s,\\{,,g;
583        s,\\},,g;
584        s,\\$,,g;
585
586        s,\\e,&#92;,g;
587        s,\\([-+_~#[]),\1,g;
588
589        # Can't implement local motion tags
590        s,\\[hv](.).*?\1,,g;
591        s,\\z,,g;
592
593        # Font changes, super/sub-scripts and font size changes
594        s,\\(f[^(]|f\(..|u|d|s[-+]?\d),&inlineStyle($1),ge;
595
596        # Overstrike
597        if (m/\\o/)
598        {
599                # handle a few special accent cases we know how to deal with
600                s,\\o(.)([aouAOU])"\1,\\o\1\2:\1,g;
601                s,\\o(.)(.)\\(.)\1,\\o\1\2\3\1,g;
602                s;\\o(.)([A-Za-z])(['`:,^~])\1;\\o\1\3\2\1;g;
603                #s,\\o(.)(.*?)\1,"<BLINK>".($vars{$2}||$2)."</BLINK>",ge;
604                s,\\o(.)(.*?)\1,$vars{$2}||$2,ge;
605        }
606        # Bracket building (ignore)
607        s,\\b(.)(.*?)\1,\2,g;
608
609        s,\\`,&#96;,g;
610        s,\\',&#39;,g;
611        s,',&#146;,g;
612        s,`,&#145;,g;
613
614        # Expand special characters introduced by eqn
615        s,\\\((..),$special{$1}||"\\($1",ge;
616        s,\\\((..),<BLINK>\\($1</BLINK>,g unless (m,^\.,);
617
618        # Don't know how to handle other escapes
619        s,(\\[^&]),<BLINK>\1</BLINK>,g unless (m,^\.,);
620
621        postProcessLine();
622
623        # Insert links for http, ftp and mailto URLs
624        # Recognised URLs are sequence of alphanumerics and special chars like / and ~
625        # but must finish with an alphanumeric rather than punctuation like "."
626        s,\b(http://[-\w/~:@.%#+$?=]+\w),<A HREF=\"\1\">\1</A>,g;
627        s,\b(ftp://[-\w/~:@.%#+$?=]+),<A HREF=\"\1\">\1</A>,g;
628        s,([-_A-Za-z0-9.]+@[A-Za-z][-_A-Za-z0-9]*\.[-_A-Za-z0-9.]+),<A HREF=\"mailto:\1\">\1</A>,g;
629
630        # special case for things like 'perlre' as it's so useful but the
631        # pod-generated pages aren't very parser friendly...
632        if ($perlPattern && ! m/<A HREF/i)
633        {
634                s,\b($perlPattern)\b,<A HREF=\"$root$perlPages{$1}\">\1</A>,g;
635        }
636
637        # Do this late so \& can be used to suppress conversion of URLs etc.
638        s,\\&,,g;
639
640        # replace tabs with spaces to next multiple of 8
641        if (m/\t/)
642        {
643                $tmp = $_;
644                $tmp =~ s/<[^>]*>//g;
645                $tmp =~ s/&[^;]*;/@/g;
646                @tmp = split(/\t/, $tmp);
647                $pos = 0;
648                for ($i=0; $i<=$#tmp; ++$i)
649                {
650                        $pos += length($tmp[$i]);
651                        $tab[$i] = 0;
652                        $tab[$i] = 8 - $pos%8 unless (@tabstops);
653                        foreach $ts (@tabstops)
654                        {
655                                if ($pos < $ts)
656                                {
657                                        $tab[$i] = $ts-$pos;
658                                        last;
659                                }
660                        }
661                        $pos += $tab[$i];
662                }
663                while (m/\t/)
664                {
665                        s,\t,"&nbsp;" x (shift @tab),e;
666                }
667        }
668
669        $textSinceBreak = $_ unless ($textSinceBreak);
670        print OUT $_;
671}
672
673# Output a line consisting purely of HTML tags which shouldn't be regarded as
674# a troff output line.
675sub plainOutput
676{
677        print OUT $_[0];
678}
679
680
681# Output the original line for debugging
682sub outputOrigLine
683{
684        # print OUT "<!-- $origLine -->\n";
685}
686
687# Use this to read the next input line (buffered to implement lookahead)
688sub getLine
689{
690        $lookaheadPtr = 0;
691        if (@lookahead)
692        {
693                $_ =  shift @lookahead;
694                return $_;
695        }
696        $_ = <SRC>;
697}
698
699# Look ahead to peek at the next input line
700sub _lookahead
701{
702        # set lookaheadPtr to 0 to re-read the lines we've looked ahead at
703        if ($lookaheadPtr>=0 && $lookaheadPtr <= $#lookahead)
704        {
705                return $lookahead[$lookaheadPtr++];
706        }
707        $lookaheadPtr = -1;
708        $ll = <SRC>;
709        push(@lookahead, $ll);
710        return $ll;
711}
712
713# Consume the last line that was returned by lookahead
714sub consume
715{
716        --$lookaheadPtr;
717        if ($lookaheadPtr>=0 && $lookaheadPtr <= $#lookahead)
718        {
719                $removed = $lookahead[$lookaheadPtr];
720                @lookahead = (@lookahead[0..$lookaheadPtr-1],@lookahead[$lookaheadPtr+1..$#lookahead]);
721        }
722        else
723        {
724                $removed = pop @lookahead;
725        }
726        chop $removed;
727        plainOutput("<!-- Consumed $removed -->\n");
728}
729
730# Look ahead skipping comments and other common non-text tags
731sub lookahead
732{
733        $ll = _lookahead();
734        while ($ll =~ m/^\.(\\"|PD|IX|ns)/)
735        {
736                $ll = _lookahead();
737        }
738        return $ll;
739}
740
741# Process $_, expaning any macros into HTML and calling outputLine().
742# If necessary, this method can read more lines of input from <SRC> (.ig & .de)
743# The following state variables are used:
744# ...
745sub processLine
746{
747        $doneLine = 1;  # By default, this counts as a line for trap purposes
748
749        s,^\.if t ,,;
750        s,^\.el ,,;             # conditions assumed to evaluate false, so else must be true...
751
752        if ($troffTable)
753        {
754                processTable();
755        }
756        elsif ($eqnMode == 2)   
757        {
758                # plainOutput("<!-- $_ -->\n");
759                processEqns($_);
760        }
761        elsif (m/^\./)
762        {
763                processMacro();
764        }
765        else
766        {
767                processPlainText();
768        }
769        if ($doneLine)
770        {
771                # Called after processing (most) input lines to decrement trapLine. This is needed
772                # to implement the .it 1 trap after one line for .TP, where the first line is outdented
773                if ($trapLine > 0)
774                {
775                        --$trapLine;
776                        if ($trapLine == 0)
777                        {
778                                &$trapAction;
779                        }
780                }
781        }
782}
783
784
785# Process plain text lines
786sub processPlainText
787{
788        if ($_ eq "")
789        {
790                lineBreak();
791                plainOutput("<P>\n");
792                return;
793        }
794
795        s,(\\f[23BI])([A-Z].*?)(\\f.),$1.($contents{"\U$2"}?"<A HREF=#".$contents{"\U$2"}.">$2</A>":$2).$3,ge;
796
797        if ($currentSection eq "SEE ALSO" && ! $cmdLineMode)
798        {
799                # Some people don't use BR or IR for see also refs
800                s,(^|\s)([-.A-Za-z_0-9]+)\s?\(([0-9lL][0-9a-zA-Z]*)\),\1<A HREF=\"$root/$2.$3\">$2($3)</A>,g;
801        }
802        outputLine("$_\n");
803}
804
805
806# Process macros and built-in directives
807sub processMacro
808{
809        outputOrigLine();
810
811        # Place macro arguments (space delimited unless within ") into @p
812        # Remove " from $_, place command in $c, remainder in $joined
813
814        @p = grep($_ !~ m/^\s*$/, split(/("[^"]*"|\s+)/) );
815        grep(s/"//g, @p);
816        $_ = join(" ", @p);
817        $p[0] =~ s/^\.//;
818        $c = $p[0];
819        $joined = join(" ", @p[1..$#p]);
820        $joined2 = join(" ", @p[2..$#p]);
821        $joined3 = join(" ", @p[3..$#p]);
822
823        if ($macro{$c})                         # Expand macro
824        {
825                # Get full macro text
826                $macro = $macro{$c};
827                # Interpolate arguments
828                $macro =~ s,\\\$(\d),$p[$1],ge;
829                #print OUT "<!-- Expanding $c to\n$macro-->\n";
830                foreach $_ (split(/\n/, $macro))
831                {
832                        $_ .= "\n";
833                        preProcessLine();
834                        processLine();
835                }
836                $doneLine = 0;
837                return;
838        }
839        elsif ($renamedMacro{$c})
840        {
841                $c = $renamedMacro{$c};
842        }
843
844        if ($c eq "ds")                 # Define string
845        {
846                $vars{$p[1]} = $joined2;
847                $doneLine = 0;
848        }
849        elsif ($c eq "nr")                      # Define number register
850        {
851                $number{$p[1]} = evalnum($joined2);
852                $doneLine = 0;
853        }
854        elsif ($c eq "ti")                      # Temporary indent
855        {
856                plainOutput("&nbsp; &nbsp;");
857        }
858        elsif ($c eq "rm")
859        {
860                $macroName = $p[1];
861                if ($macro{$macroName})
862                {
863                        delete $macro{$macroName};
864                }
865                else
866                {
867                        $deletedMacro{$macroName} = 1;
868                }
869        }
870        elsif ($c eq "rn")
871        {
872                $oldName = $p[1];
873                $newName = $p[2];
874                $macro = $macro{$oldName};
875                if ($macro)
876                {
877                        if ($newName =~ $reservedMacros && ! $deletedMacro{$newName})
878                        {
879                                plainOutput("<!-- Not overwriting reserved macro '$newName' -->\n");
880                        }
881                        else
882                        {
883                                $macro{$newName} = $macro;
884                                delete $deletedMacro{$newName};
885                        }
886                        delete $macro{$oldName};
887                }
888                else
889                {
890                        # Support renaming of reserved macros by mapping occurrences of new name
891                        # to old name after macro expansion so that built in definition is still
892                        # available, also mark the name as deleted to override reservedMacro checks.
893                        plainOutput("<!-- Fake renaming reserved macro '$oldName' -->\n");
894                        $renamedMacro{$newName} = $oldName;
895                        $deletedMacro{$oldName} = 1;
896                }
897        }
898        elsif ($c eq "de" || $c eq "ig")        # Define macro or ignore
899        {
900                $macroName = $p[1];
901                if ($c eq "ig")
902                        { $delim = ".$p[1]"; }
903                else
904                        { $delim = ".$p[2]"; }
905                $delim = ".." if ($delim eq ".");
906                # plainOutput("<!-- Scanning for delimiter $delim -->\n");
907
908                $macro = "";
909                $_ = getLine();
910                preProcessLine();
911                while ($_ ne $delim)
912                {
913                        postProcessLine();
914                        outputOrigLine();
915                        $macro .= "$_\n";
916                        $_ = getLine();
917                        last if ($_ eq "");
918                        preProcessLine();
919                }
920                outputOrigLine();
921                # plainOutput("<!-- Found delimiter -->\n");
922                if ($c eq "de")
923                {
924                        if ($macroName =~ $reservedMacros && ! $deletedMacro{$macroName})
925                        {
926                                plainOutput("<!-- Not defining reserved macro '$macroName' ! -->\n");
927                        }
928                        else
929                        {
930                                $macro{$macroName} = $macro;
931                                delete $deletedMacro{$macroName};
932                        }
933                }
934        }
935        elsif ($c eq "so")                      # Source
936        {
937                plainOutput("<P>[<A HREF=\"$root$dir/../$p[1]\">Include document $p[1]</A>]<P>\n");
938        }
939        elsif ($c eq "TH" || $c eq "Dt")                        # Man page title
940        {
941                endParagraph();
942                $sectionNumber = $p[2];
943                $sectionName = $sectionName{"\L$sectionNumber"};
944                $sectionName = "Manual Reference Pages" unless ($sectionName);
945                $pageName = "$p[1] ($sectionNumber)";
946                outputPageHead();
947                if ($c eq "TH")
948                {
949                        $right = $p[3];
950                        $left = $p[4];
951                        $left = $osver unless ($left);
952                        $macroPackage = "using man macros";
953                }
954                else
955                {
956                        $macroPackage = "using doc macros";
957                }
958        }
959        elsif ($c eq "Nd")
960        {
961                outputLine("- $joined\n");
962        }
963        elsif ($c eq "SH" || $c eq "SS" || $c eq "Sh" || $c eq "Ss")            # Section/subsection
964        {
965                lineBreak();
966                endNoFill();
967                endParagraph();
968                $id = $contents{"\U$joined"};
969                $currentSection = $joined;
970
971                if ($c eq "SH" || $c eq "Sh")
972                {
973                        endBlockquote();
974                        if ($firstSection++==1) # after first 'Name' section
975                        {
976                                outputContents();
977                        }
978                        outputLine( "<A name=$id>\n\n     <H3>$joined</H3>\n\n</A>\n" );
979                        blockquote();
980                }
981                elsif ($joined =~ m/\\f/)
982                {
983                        $joined =~ s/\\f.//g;
984                        $id = $contents{"\U$joined"};
985                        outputLine( "<A name=$id>\n<H4><I>$joined</I></H4></A>\n" );
986                }
987                else
988                {
989                        endBlockquote();
990                        outputLine( "<A name=$id>\n\n    <H4>&nbsp; &nbsp; $joined</H4>\n</A>\n" );
991                        blockquote();
992                }
993                lineBreak();
994        }
995        elsif ($c eq "TX" || $c eq "TZ")        # Document reference
996        {
997                $title = $title{$p[1]};
998                $title = "Document [$p[1]]" unless ($title);
999                outputLine( "\\fI$title\\fP$joined2\n" );
1000        }
1001        elsif ($c eq "PD")                      # Line spacing
1002        {
1003                $noSpace = ($p[1] eq "0");
1004                $doneLine = 0;
1005        }
1006        elsif ($c eq "TS")                      # Table start
1007        {
1008                unless ($macroPackage =~ /tbl/)
1009                {
1010                        if ($macroPackage =~ /eqn/)
1011                                { $macroPackage =~ s/eqn/eqn & tbl/; }
1012                        else
1013                                { $macroPackage .= " with tbl support"; }
1014                }
1015                resetStyles();
1016                endNoFill();
1017                $troffTable = 1;
1018                $troffSeparator = "\t";
1019                plainOutput( "<P><BLOCKQUOTE><TABLE bgcolor=#E0E0E0 border=1 cellspacing=0 cellpadding=3>\n" );
1020        }
1021        elsif ($c eq "EQ")                      # Eqn start
1022        {
1023                unless ($macroPackage =~ /eqn/)
1024                {
1025                        if ($macroPackage =~ /tbl/)
1026                                { $macroPackage =~ s/tbl/tbl & eqn/; }
1027                        else
1028                                { $macroPackage .= " with eqn support"; }
1029                }
1030                $eqnMode = 2;
1031        }
1032        elsif ($c eq "ps")                      # Point size
1033        {
1034                plainOutput(&sizeChange($p[1]));
1035        }
1036        elsif ($c eq "ft")                      # Font change
1037        {
1038                plainOutput(&fontChange($p[1]));
1039        }
1040        elsif ($c eq "I" || $c eq "B")  # Single word font change
1041        {
1042                $id = $contents{"\U$joined"};
1043                if ($id && $joined =~ m/^[A-Z]/)
1044                        { $joined = "<A HREF=#$id>$joined</A>"; }
1045                outputLine( "\\f$c$joined\\fP " );
1046                plainOutput("\n") if ($noFill);
1047        }
1048        elsif ($c eq "SM")                      # Single word smaller
1049        {
1050                outputLine("\\s-1$joined\\s0 ");
1051                $doneLine = 0 unless ($joined);
1052        }
1053        elsif ($c eq "SB")                      # Single word bold and small
1054        {
1055                outputLine("\\fB\\s-1$joined\\s0\\fP ");
1056        }
1057        elsif (m/^\.[BI]R (\S+)\s?\(\s?([0-9lL][0-9a-zA-Z]*)\s?\)(.*)$/)
1058        {
1059                # Special form, .BR is generally used for references to other pages
1060                # Annoyingly, some people have more than one per line...
1061                # Also, some people use .IR ...
1062                for ($i=1; $i<=$#p; $i+=2)
1063                {
1064                        $pair = $p[$i]." ".$p[$i+1];
1065                        if ($p[$i+1] eq "(")
1066                        {
1067                                $pair .= $p[$i+2].$p[$i+3];
1068                                $i += 2;
1069                        }
1070                        if ($pair =~ m/^(\S+)\s?\(\s?([0-9lL][0-9a-zA-Z]*)\s?\)(.*)$/)
1071                        {
1072                                if ($cmdLineMode)
1073                                        { outputLine( "\\fB$1\\fR($2)$3\n" ); }
1074                                else
1075                                        { outputLine( "<A HREF=\"$root/$1.$2\">$1($2)</A>$3\n" ); }
1076                        }
1077                        else
1078                                { outputLine( "$pair\n" ); }
1079                }
1080        }
1081        elsif ($c eq "BR" || $c eq "BI" || $c eq "IB" ||
1082                   $c eq "IR" || $c eq "RI" || $c eq "RB")
1083        {
1084                $f1 = (substr($c ,0,1));
1085                $f2 = (substr($c,1,1));
1086
1087                # Check if first param happens to be a section name
1088                $id = $contents{"\U$p[1]"};
1089                if ($id && $p[1] =~ m/^[A-Z]/)
1090                {
1091                        $p[1] = "<A HREF=#$id>$p[1]</A>";
1092                }
1093
1094                for ($i=1; $i<=$#p; ++$i)
1095                {
1096                        $f = ($i%2 == 1) ? $f1 : $f2;
1097                        outputLine("\\f$f$p[$i]");
1098                }
1099                outputLine("\\fP ");
1100                plainOutput("\n") if ($noFill);
1101        }
1102        elsif ($c eq "nf" || $c eq "Bd")                        # No fill
1103        {
1104                startNoFill();
1105        }
1106        elsif ($c eq "fi" || $c eq "Ed")                        # Fill
1107        {
1108                endNoFill();
1109        }
1110        elsif ($c eq "HP")
1111        {
1112                $indent = evalnum($p[1]);
1113                if ($trapOnBreak)
1114                {
1115                        plainOutput("<BR>\n");
1116                }
1117                else
1118                {
1119                        # Outdent first line, ie. until next break
1120                        $trapOnBreak = 1;
1121                        $trapAction = *trapHP;
1122                        newParagraph($indent);
1123                        plainOutput( "<TD colspan=2>\n" );
1124                        $colState = 2;
1125                }
1126        }
1127        elsif ($c eq "IP")
1128        {
1129                $trapOnBreak = 0;
1130                $tag = $p[1];
1131                $indent = evalnum($p[2]);
1132                newParagraph($indent);
1133                outputLine("<TD$width>\n$tag\n</TD><TD>\n");
1134                $colState = 1;
1135                lineBreak();
1136        }
1137        elsif ($c eq "TP")
1138        {
1139                $trapOnBreak = 0;
1140                $trapLine = 1;  # Next line is tag, then next column
1141                $doneLine = 0;  # (But don't count this line)
1142                $trapAction = *trapTP;
1143                $indent = evalnum($p[1]);
1144                $tag = lookahead();
1145                chop $tag;
1146                $i = ($indent ? $indent : $prevailingIndent) ;
1147                $w = width($tag);
1148                if ($w > $i)
1149                {
1150                        plainOutput("<!-- Length of tag '$tag' ($w) > indent ($i) -->\n") if ($debug);
1151                        newParagraph($indent);
1152                        $trapAction = *trapHP;
1153                        plainOutput( "<TD colspan=2>\n" );
1154                        $colState = 2;
1155                }
1156                else
1157                {
1158                        newParagraph($indent);
1159                        plainOutput( "<TD$width nowrap>\n" );
1160                        $colState = 0;
1161                }
1162                $body = lookahead();
1163                $lookaheadPtr = 0;
1164                if ($body =~ m/^\.[HILP]?P/)
1165                {
1166                        chop $body;
1167                        plainOutput("<!-- Suppressing TP body due to $body -->\n");
1168                        $trapLine = 0;
1169                }
1170        }
1171        elsif ($c eq "LP" || $c eq "PP" || $c eq "P" || $c eq "Pp")     # Paragraph
1172        {
1173                $trapOnBreak = 0;
1174                $prevailingIndent = 6;
1175                if ($indent[$indentLevel] > 0 && $docListStyle eq "")
1176                {
1177                        $line = lookahead();
1178                        if ($line =~ m/^\.(TP|IP|HP)/)
1179                        {
1180                                plainOutput("<!-- suppressed $c before $1 -->\n");
1181                        }
1182                        elsif ($line =~ m/^\.RS/)
1183                        {
1184                                plainOutput("<P>\n");
1185                        }
1186                        else
1187                        {
1188                                endRow();
1189                                $foundTag = "";
1190                                $lookaheadPtr = 0;
1191                                do
1192                                {
1193                                        $line = lookahead();
1194                                        if ($line =~ m/^\.(TP|HP|IP|RS)( \d+)?/)
1195                                        {
1196                                                $indent = $2;
1197                                                $indent = $prevailingIndent unless ($2);
1198                                                if ($indent == $indent[$indentLevel])
1199                                                        { $foundTag = $1; }
1200                                                $line = "";
1201                                        }
1202                                }
1203                                while ($line ne "" && $line !~ m/^\.(RE|SH|SS|PD)/);
1204                                $lookaheadPtr = 0;
1205                                if ($foundTag)
1206                                {
1207                                        plainOutput("<!-- Found tag $foundTag -->\n");
1208                                        plainOutput("<TR><TD colspan=2>\n");
1209                                        $colState = 2;
1210                                }
1211                                else
1212                                {
1213                                        plainOutput("<!-- $c ends table -->\n");
1214                                        setIndent(0);
1215                                }
1216                        }
1217                }
1218                else
1219                {
1220                        plainOutput("<P>\n");
1221                }
1222                lineBreak();
1223        }
1224        elsif ($c eq "br")                      # Break
1225        {
1226                if ($trapOnBreak)
1227                {
1228                        # Should this apply to all macros that cause a break?
1229                        $trapOnBreak = 0;
1230                        &$trapAction();
1231                }
1232                $needBreak = 1 if ($textSinceBreak);
1233        }
1234        elsif ($c eq "sp")                      # Space
1235        {
1236                lineBreak();
1237                plainOutput("<P>\n");
1238        }
1239        elsif ($c eq "RS")                      # Block indent start
1240        {
1241                if ($indentLevel==0 && $indent[0]==0)
1242                {
1243                        blockquote();
1244                }
1245                else
1246                {
1247                        $indent = $p[1];
1248                        $indent = $prevailingIndent unless ($indent);
1249                        if ($indent > $indent[$indentLevel] && !$extraIndent)
1250                        {
1251                                $extraIndent = 1;
1252                                ++$indentLevel;
1253                                $indent[$indentLevel] = 0;
1254                                setIndent($indent-$indent[$indentLevel-1]);
1255                                plainOutput("<TR><TD$width>&nbsp;</TD><TD>\n");
1256                                $colState = 1;
1257                        }
1258                        elsif ($indent < $indent[$indentLevel] || $colState==2)
1259                        {
1260                                endRow();
1261                                setIndent($indent);
1262                                plainOutput("<TR><TD$width>&nbsp;</TD><TD>\n");
1263                                $colState = 1;
1264                        }
1265                        ++$indentLevel;
1266                        $indent[$indentLevel] = 0;
1267                }
1268                $prevailingIndent = 6;
1269        }
1270        elsif ($c eq "RE")                      # Block indent end
1271        {
1272                if ($extraIndent)
1273                {
1274                        endRow();
1275                        setIndent(0);
1276                        --$indentLevel;
1277                        $extraIndent = 0;
1278                }
1279                if ($indentLevel==0)
1280                {
1281                        endParagraph();
1282                        if ($blockquote>0)
1283                        {
1284                                plainOutput("</BLOCKQUOTE>\n");
1285                                --$blockquote;
1286                        }
1287                }
1288                else
1289                {
1290                        endRow();
1291                        setIndent(0);
1292                        --$indentLevel;
1293                }
1294                $prevailingIndent = $indent[$indentLevel];
1295                $prevailingIndent = 6 unless($prevailingIndent);
1296        }
1297        elsif ($c eq "DT")                      # default tabs
1298        {
1299                @tabstops = ();
1300        }
1301        elsif ($c eq "ta")                      # Tab stops
1302        {
1303                @tabstops = ();
1304                for ($i=0; $i<$#p; ++$i)
1305                {
1306                        $ts = $p[$i+1];
1307                        $tb = 0;
1308                        if ($ts =~ m/^\+/)
1309                        {
1310                                $tb = $tabstops[$i-1];
1311                                $ts =~ s/^\+//;
1312                        }
1313                        $ts = evalnum($ts);
1314                        $tabstops[$i] = $tb + $ts;
1315                }
1316                plainOutput("<!-- Tabstops set at ".join(",", @tabstops)." -->\n") if ($debug);
1317        }
1318        elsif ($c eq "It")                      # List item (mdoc)
1319        {
1320                lineBreak();
1321                if ($docListStyle eq "-tag")
1322                {
1323                        endRow() unless($multilineIt);
1324                        if ($tagWidth)
1325                        {
1326                                setIndent($tagWidth);
1327                        }
1328                        else
1329                        {
1330                                setIndent(6);
1331                                $width = "";    # let table take care of own width
1332                        }
1333                        if ($p[1] eq "Xo")
1334                        {
1335                                plainOutput("<TR valign=top><TD colspan=2>");
1336                        }
1337                        else
1338                        {
1339                                $tag = &mdocStyle(@p[1..$#p]);
1340                                $body = lookahead();
1341                                if ($body =~ m/^\.It/)
1342                                        { $multilineItNext = 1; }
1343                                else
1344                                        { $multilineItNext = 0; }
1345                                if ($multilineIt)
1346                                {
1347                                        outputLine("<BR>\n$tag\n");
1348                                }
1349                                elsif ($multilineItNext || $tagWidth>0 && width($tag)>$tagWidth)
1350                                {
1351                                        outputLine("<TR valign=top><TD colspan=2>$tag\n");
1352                                        $colState = 2;
1353                                }
1354                                else
1355                                {
1356                                        outputLine("<TR valign=top><TD>$tag\n");
1357                                        $colState = 1;
1358                                }
1359                                if ($multilineItNext)
1360                                {
1361                                        $multilineIt = 1;
1362                                }
1363                                else
1364                                {
1365                                        $multilineIt = 0;
1366                                        if ($colState==2)
1367                                                { plainOutput("</TD></TR><TR><TD>&nbsp;</TD><TD>\n"); }
1368                                        else
1369                                                { plainOutput("</TD><TD>\n"); }
1370                                }
1371                        }
1372                }
1373                else
1374                {
1375                        plainOutput("<LI>");
1376                }
1377                lineBreak();
1378        }
1379        elsif ($c eq "Xc")
1380        {
1381                if ($docListStyle eq "-tag")
1382                {
1383                        plainOutput("</TD></TR><TR><TD>&nbsp;</TD><TD>\n");
1384                }
1385        }
1386        elsif ($c eq "Bl")              # Begin list (mdoc)
1387        {
1388                push @docListStyles, $docListStyle;
1389                if ($p[1] eq "-enum")
1390                {
1391                        plainOutput("<OL>\n");
1392                        $docListStyle = $p[1];
1393                }
1394                elsif($p[1] eq "-bullet")
1395                {
1396                        plainOutput("<UL>\n");
1397                        $docListStyle = $p[1];
1398                }
1399                else
1400                {
1401                        $docListStyle = "-tag";
1402                        if ($p[2] eq "-width")
1403                        {
1404                                $tagWidth = width($p[3]);
1405                                if ($tagWidth < 6) { $tagWidth = 6; }
1406                        }
1407                        else
1408                        {
1409                                $tagWidth = 0;
1410                        }
1411                        $multilineIt = 0;
1412                }
1413        }
1414        elsif ($c eq "El")              # End list
1415        {
1416                if ($docListStyle eq "-tag")
1417                {
1418                        endRow();
1419                        setIndent(0);
1420                }
1421                elsif ($docListStyle eq "-bullet")
1422                {
1423                        plainOutput("</UL>\n");
1424                }
1425                else
1426                {
1427                        plainOutput("</OL>\n");
1428                }
1429                $docListStyle = pop @docListStyles;
1430        }
1431        elsif ($c eq "Os")
1432        {
1433                $right = $joined;
1434        }
1435        elsif ($c eq "Dd")
1436        {
1437                $left = $joined;
1438        }
1439        elsif ($c eq "Sx")              # See section
1440        {
1441                $id = $contents{"\U$joined"};
1442                if ($id && $joined =~ m/^[A-Z]/)
1443                {
1444                        outputLine("<A HREF=#$id>".&mdocStyle(@p[1..$#p])."</A>\n");
1445                }
1446                else
1447                {
1448                        my $x = &mdocStyle(@p[1..$#p]);
1449                        $x =~ s/^ //;
1450                        outputLine($x."\n");
1451                }
1452        }
1453        elsif (&mdocCallable($c))
1454        {
1455                my $x = &mdocStyle(@p);
1456                $x =~ s/^ //;
1457                outputLine($x."\n");
1458        }
1459        elsif ($c eq "Bx")
1460        {
1461                outputLine("<I>BSD $joined</I>\n");
1462        }
1463        elsif ($c eq "Ux")
1464        {
1465                outputLine("<I>Unix $joined</I>\n");
1466        }
1467        elsif ($c eq "At")
1468        {
1469                outputLine("<I>AT&T $joined</I>\n");
1470        }
1471        elsif ($c =~ m/[A-Z][a-z]/)             # Unsupported doc directive
1472        {
1473                outputLine("<BR>.$c $joined\n");
1474        }
1475        elsif ($c eq "")                                # Empty line (eg. troff comment)
1476        {
1477                $doneLine = 0;
1478        }
1479        else                                            # Unsupported directive
1480        {
1481                # Unknown macros are ignored, and don't count as a line as far as trapLine goes
1482                $doneLine = 0;
1483                plainOutput("<!-- ignored unsupported tag .$c -->\n");
1484        }
1485}
1486
1487sub trapTP
1488{
1489        $lookaheadPtr = 0;
1490        $body = lookahead();
1491        if ($body =~ m/^\.TP/)
1492        {
1493                consume();
1494                $trapLine = 1;  # restore TP trap
1495                $doneLine = 0;  # don't count this line
1496                plainOutput("<BR>\n");
1497        }
1498        else
1499        {
1500                plainOutput("</TD><TD valign=bottom>\n");
1501                $colState = 1;
1502        }
1503        lineBreak();
1504}
1505
1506sub trapHP
1507{
1508        $lookaheadPtr = 0;
1509        $body = lookahead();
1510        if ($body =~ m/^\.([TH]P)/)
1511        {
1512                consume();
1513                # Restore appropriate type of trap
1514                if ($1 eq "TP")
1515                {
1516                        $trapLine = 1;
1517                        $doneLine = 0;  # don't count this line
1518                }
1519                else
1520                {
1521                        $trapOnBreak = 1;
1522                }
1523                plainOutput("<BR>\n");
1524        }
1525        else
1526        {
1527                plainOutput("</TD></TR><TR valign=top><TD$width>&nbsp;</TD><TD>\n");
1528                $colState = 1;
1529        }
1530        lineBreak();
1531}
1532
1533sub newParagraph
1534{
1535        $indent = $_[0];
1536        endRow();
1537        startRow($indent);
1538}
1539
1540sub startRow
1541{
1542        $indent = $_[0];
1543        $indent = $prevailingIndent unless ($indent);
1544        $prevailingIndent = $indent;
1545        setIndent($indent);
1546        plainOutput( "<TR valign=top>" );
1547}
1548
1549# End an existing HP/TP/IP/RS row
1550sub endRow
1551{
1552        if ($indent[$indentLevel] > 0)
1553        {
1554                lineBreak();
1555                plainOutput( "</TD></TR>\n" );
1556        }
1557}
1558
1559# Called when we output a line break tag. Only needs to be called once if
1560# calling plainOutput, but should call before and after if using outputLine.
1561sub lineBreak
1562{
1563        $needBreak = 0;
1564        $textSinceBreak = 0;
1565}
1566
1567# Called to reset all indents and pending paragraphs (eg. at the start of
1568# a new top level section).
1569sub endParagraph
1570{
1571        ++$indentLevel;
1572        while ($indentLevel > 0)
1573        {
1574                --$indentLevel;
1575                if ($indent[$indentLevel] > 0)
1576                {
1577                        endRow();
1578                        setIndent(0);
1579                }
1580        }
1581}
1582
1583# Interpolate a number register (possibly autoincrementing)
1584sub numreg
1585{
1586        return 0 + $number{$_[0]};
1587}
1588
1589# Evaluate a numeric expression
1590sub evalnum
1591{
1592        $n = $_[0];
1593        return "" if ($n eq "");
1594        if ($n =~ m/i$/)        # inches
1595        {
1596                $n =~ s/i//;
1597                $n *= 10;
1598        }
1599        return 0+$n;
1600}
1601
1602sub setIndent
1603{
1604        $tsb = $textSinceBreak;
1605        $indent = evalnum($_[0]);
1606        if ($indent==0 && $_[0] !~ m/^0/)
1607        {
1608                $indent = 6;
1609        }
1610        plainOutput("<!-- setIndent $indent, indent[$indentLevel] = $indent[$indentLevel] -->\n") if ($debug);
1611        if ($indent[$indentLevel] != $indent)
1612        {
1613                lineBreak();
1614                if ($indent[$indentLevel] > 0)
1615                {
1616                        plainOutput("<TR></TR>") unless ($noSpace);
1617                        plainOutput("</TABLE>");
1618                }
1619                if ($indent > 0)
1620                {
1621                        endNoFill();
1622                        $border = "";
1623                        $border = " border=1" if ($debug>2);
1624                        #plainOutput("<P>") unless ($indent[$indentLevel] > 0);
1625                        plainOutput("<TABLE$border");
1626                        # Netscape bug, makes 2 cols same width? : plainOutput("<TABLE$border COLS=2");
1627                        # Overcome some of the vagaries of Netscape tables
1628                        plainOutput(" width=100%") if ($indentLevel>0);
1629                        if ($noSpace)
1630                        {
1631                                plainOutput(" cellpadding=0 cellspacing=0>\n");
1632                        }
1633                        else
1634                        {
1635                                plainOutput(" cellpadding=3>".($tsb ? "<!-- tsb: $tsb -->\n<TR></TR><TR></TR>\n" : "\n") );
1636                        }
1637                        #$width = " width=".($indent*5);        # causes text to be chopped if too big
1638                        $percent = $indent;
1639                        if ($indentLevel > 0)
1640                                { $percent = $indent * 100 / (100-$indentLevel[0]); }
1641                        $width = " width=$percent%";
1642                }
1643                $indent[$indentLevel] = $indent;
1644        }
1645}
1646
1647# Process mdoc style macros recursively, as one of the macro arguments
1648# may itself be the name of another macro to invoke.
1649sub mdocStyle
1650{
1651        return "" unless @_;
1652        my ($tag, @param) = @_;
1653        my ($rest, $term);
1654
1655        # Don't format trailing punctuation
1656        if ($param[$#param] =~ m/^[.,;:]$/)
1657        {
1658                $term = pop @param;
1659        }
1660        if ($param[$#param] =~ m/^[)\]]$/)
1661        {
1662                $term = (pop @param).$term;
1663        }
1664
1665        if ($param[0] =~ m,\\\\,)
1666        {
1667                print STDERR "$tag: ",join(",", @param),"\n";
1668        }
1669        $rest = &mdocStyle(@param);
1670       
1671        if ($tag eq "Op")
1672        {
1673                $rest =~ s/ //; # remove first space
1674                return " \\fP[$rest]$term";
1675        }
1676        elsif ($tag eq "Xr")    # cross reference
1677        {
1678                my $p = shift @param;
1679                my $url = $p;
1680                if (@param==1)
1681                {
1682                        $url .= ".".$param[0];
1683                        $rest = "(".$param[0].")";
1684                }
1685                else
1686                {
1687                        $rest = &mdocStyle(@param);
1688                }
1689                if ($cmdLineMode)
1690                {
1691                        return " <B>".$p."</B>".$rest.$term;
1692                }
1693                else
1694                {
1695                        return " <A HREF=\"".$root."/".$url."\">".$p."</A>".$rest.$term;
1696                }
1697        }
1698        elsif ($tag eq "Fl")
1699        {
1700                my ($sofar);
1701                while (@param)
1702                {
1703                        $f = shift @param;
1704                        if ($f eq "Ns") # no space
1705                        {
1706                                chop $sofar;
1707                        }
1708                        elsif (&mdocCallable($f))
1709                        {
1710                                unshift @param, $f;
1711                                return $sofar.&mdocStyle(@param).$term;
1712                        }
1713                        else
1714                        {
1715                                $sofar .= "-<B>$f</B> "
1716                        }
1717                }
1718                return $sofar.$term;
1719        }
1720        elsif ($tag eq "Pa" || $tag eq "Er" || $tag eq "Fn" || $tag eq "Dv")
1721        {
1722                return "\\fC$rest\\fP$term";
1723        }
1724        elsif ($tag eq "Ad" || $tag eq "Ar" || $tag eq "Em" || $tag eq "Fa" || $tag eq "St" ||
1725                $tag eq "Ft" || $tag eq "Va" || $tag eq "Ev" || $tag eq "Tn" || $tag eq "%T")
1726        {
1727                return "\\fI$rest\\fP$term";
1728        }
1729        elsif ($tag eq "Nm")
1730        {
1731                $defaultNm = $param[0] unless ($defaultNm);
1732                $rest = $defaultNm unless ($param[0]);
1733                return "\\fB$rest\\fP$term";
1734        }
1735        elsif ($tag eq "Ic" || $tag eq "Cm" || $tag eq "Sy")
1736        {
1737                return "\\fB$rest\\fP$term";
1738        }
1739        elsif ($tag eq "Ta")            # Tab
1740        {
1741                # Tabs are used inconsistently so this is the best we can do. Columns won't line up. Tough.
1742                return "&nbsp; &nbsp; &nbsp; $rest$term";
1743        }
1744        elsif ($tag eq "Ql")
1745        {
1746                $rest =~ s/ //;
1747                return "`<TT>$rest</TT>'$term";
1748        }
1749        elsif ($tag eq "Dl")
1750        {
1751                return "<P>&nbsp; &nbsp; <TT>$rest</TT>$term<P>\n";
1752        }
1753        elsif ($tag =~ m/^[ABDEOPQS][qoc]$/)
1754        {
1755                $lq = "";
1756                $rq = "";
1757                if ($tag =~ m/^A/)
1758                        { $lq = "&lt;"; $rq = "&gt;"; }
1759                elsif ($tag =~ m/^B/)
1760                        { $lq = "["; $rq = "]"; }
1761                elsif ($tag =~ m/^D/)
1762                        { $lq = "\""; $rq = "\""; }
1763                elsif ($tag =~ m/^P/)
1764                        { $lq = "("; $rq = ")"; }
1765                elsif ($tag =~ m/^Q/)
1766                        { $lq = "\""; $rq = "\""; }
1767                elsif ($tag =~ m/^S/)
1768                        { $lq = "\\'"; $rq = "\\'"; }
1769                elsif ($tag =~ m/^O/)
1770                        { $lq = "["; $rq = "]"; }
1771                if ($tag =~ m/^.o/)
1772                        { $rq = ""; }
1773                if ($tag =~ m/^.c/)
1774                        { $lq = ""; }
1775                $rest =~ s/ //;
1776                return $lq.$rest.$rq.$term ;
1777        }
1778        elsif (&mdocCallable($tag))     # but not in list above...
1779        {
1780                return $rest.$term;
1781        }
1782        elsif ($tag =~ m/^[.,;:()\[\]]$/)       # punctuation
1783        {
1784                return $tag.$rest.$term;
1785        }
1786        elsif ($tag eq "Ns")
1787        {
1788                return $rest.$term;
1789        }
1790        else
1791        {
1792                return " ".$tag.$rest.$term;
1793        }
1794}
1795
1796# Determine if a macro is mdoc parseable/callable
1797sub mdocCallable
1798{
1799        return ($_[0] =~ m/^(Op|Fl|Pa|Er|Fn|Ns|No|Ad|Ar|Xr|Em|Fa|Ft|St|Ic|Cm|Va|Sy|Nm|Li|Dv|Ev|Tn|Pf|Dl|%T|Ta|Ql|[ABDEOPQS][qoc])$/);
1800}
1801
1802
1803# Estimate the output width of a string
1804sub width
1805{
1806        local($word) = $_[0];
1807        $word =~ s,<[/A-Z][^>]*>,,g;            # remove any html tags
1808        $word =~ s/^\.\S+\s//;
1809        $word =~ s/\\..//g;
1810        $x = length($word);
1811        $word =~ s/[ ()|.,!;:"']//g;    # width of punctuation is about half a character
1812        return ($x + length($word)) / 2;
1813}
1814
1815# Process a tbl table (between TS/TE tags)
1816sub processTable
1817{
1818        if ($troffTable == "1")
1819        {
1820                @troffRowDefs = ();
1821                @tableRows = ();
1822                $hadUnderscore = 0;
1823                while(1)
1824                {
1825                        outputOrigLine();
1826                        if (m/;\s*$/)
1827                        {
1828                                $troffSeparator = quotemeta($1) if (m/tab\s*\((.)\)/);
1829                        }
1830                        else
1831                        {
1832                                s/\.\s*$//;
1833                                s/\t/ /g;
1834                                s/^[^lrcan^t]*//;       # remove any 'modifiers' coming before tag
1835                                # delimit on tags excluding s (viewed as modifier of previous column)
1836                                s/([lrcan^t])/\t$1/g;
1837                                s/^\t//;
1838                                push @troffRowDefs, $_;
1839                                last if ($origLine =~ m/\.\s*$/);
1840                        }
1841                        $_ = getLine();
1842                        preProcessLine();
1843                }
1844                $troffTable = 2;
1845                return;
1846        }
1847
1848        s/$troffSeparator/\t/g;
1849        if ($_ eq ".TE")
1850        {
1851                endTblRow();
1852                flushTable();
1853                $troffTable = 0;
1854                plainOutput("</TABLE></BLOCKQUOTE>\n");
1855        }
1856        elsif ($_ eq ".T&")
1857        {
1858                endTblRow();
1859                flushTable();
1860                $troffTable = 1;
1861        }
1862        elsif (m/[_=]/ && m/^[_=\t]*$/ && $troffCol==0)
1863        {
1864                if (m/^[_=]$/)
1865                {
1866                        flushTable();
1867                        plainOutput("<TR></TR><TR></TR>\n");
1868                        $hadUnderscore = 1;
1869                }
1870                elsif ($troffCol==0 && @troffRowDefs)
1871                {
1872                        # Don't output a row, but this counts as a row as far as row defs go
1873                        $rowDef = shift @troffRowDefs;
1874                        @troffColDefs = split(/\t/, $rowDef);
1875                }
1876        }
1877        elsif (m/^\.sp/ && $troffCol==0 && !$hadUnderscore)
1878        {
1879                flushTable();
1880                plainOutput("<TR></TR><TR></TR>\n");
1881        }
1882        elsif ($_ eq ".br" && $troffMultiline)
1883        {
1884                $rowref->[$troffCol] .= "<BR>\n";
1885        }
1886        elsif ($_ !~ m/^\./)
1887        {
1888                $rowref = $tableRows[$#tableRows];      # reference to current row (last row in array)
1889                if ($troffCol==0 && @troffRowDefs)
1890                {
1891                        $rowDef = shift @troffRowDefs;
1892                        if ($rowDef =~ m/^[_=]/)
1893                        {
1894                                $xxx = $_;
1895                                flushTable();
1896                                plainOutput("<TR></TR><TR></TR>\n");
1897                                $hadUnderscore = 1;
1898                                $_ = $xxx;
1899                                $rowDef = shift @troffRowDefs;
1900                        }
1901                        @troffColDefs = split(/\t/, $rowDef);
1902                }
1903
1904                if ($troffCol == 0 && !$troffMultiline)
1905                {
1906                        $rowref = [];
1907                        push(@tableRows, $rowref);
1908                        #plainOutput("<TR valign=top>");
1909                }
1910
1911                #{
1912                if (m/T}/)
1913                {
1914                        $troffMultiline = 0;
1915                }
1916                if ($troffMultiline)
1917                {
1918                        $rowref->[$troffCol] .= "$_\n";
1919                        return;
1920                }
1921
1922                @columns = split(/\t/, $_);
1923                plainOutput("<!-- Adding (".join(",", @columns)."), type (".join(",", @troffColDefs).") -->\n") if ($debug);
1924                while ($troffCol <= $#troffColDefs && @columns > 0)
1925                {
1926                        $def = $troffColDefs[$troffCol];
1927                        $col = shift @columns;
1928                        $col =~ s/\s*$//;
1929                        $align = "";
1930                        $col = "\\^" if ($col eq "" && $def =~ m/\^/);
1931                        $col = "&nbsp;" if ($col eq "");
1932                        $style1 = "";
1933                        $style2 = "";
1934                        if ($col ne "\\^")
1935                        {
1936                                if ($def =~ m/[bB]/ || $def =~ m/f3/)
1937                                        { $style1 = "\\fB"; $style2 = "\\fP"; }
1938                                if ($def =~ m/I/ || $def =~ m/f2/)
1939                                        { $style1 = "\\fI"; $style2 = "\\fP"; }
1940                        }
1941                        if ($def =~ m/c/) { $align = " align=center"; }
1942                        if ($def =~ m/[rn]/) { $align = " align=right"; }
1943                        $span = $def;
1944                        $span =~ s/[^s]//g;
1945                        if ($span) { $align.= " colspan=".(length($span)+1); }
1946
1947                        #{
1948                        if ($col =~ m/T}/)
1949                        {
1950                                $rowref->[$troffCol] .= "$style2</TD>";
1951                                ++$troffCol;
1952                        }
1953                        elsif ($col =~ m/T{/) #}
1954                        {
1955                                $col =~ s/T{//; #}
1956                                $rowref->[$troffCol] = "<TD$align>$style1$col";
1957                                $troffMultiline = 1;
1958                        }
1959                        else
1960                        {
1961                                $rowref->[$troffCol] = "<TD$align>$style1$col$style2</TD>";
1962                                ++$troffCol;
1963                        }
1964                }
1965
1966                endTblRow() unless ($troffMultiline);
1967        }
1968}
1969
1970sub endTblRow
1971{
1972        return if ($troffCol == 0);
1973        while ($troffCol <= $#troffColDefs)
1974        {
1975                $rowref->[$troffCol] = "<TD>&nbsp;</TD>";
1976                #print OUT "<TD>&nbsp;</TD>";
1977                ++$troffCol;
1978        }
1979        $troffCol = 0;
1980        #print OUT "</TR>\n"
1981}
1982
1983sub flushTable
1984{
1985        plainOutput("<!-- flushTable $#tableRows rows -->\n") if ($debug);
1986
1987        # Treat rows with first cell blank or with more than one vertically
1988        # spanned row as a continuation of the previous line.
1989        # Note this is frequently a useful heuristic but isn't foolproof.
1990        for($r=0; $r<$#tableRows; ++$r)
1991        {
1992                $vspans = 0;
1993                for ($c=0; $c<=$#{$tableRows[$r+1]}; ++$c)
1994                        {++$vspans if ($tableRows[$r+1][$c] =~ m,<TD.*?>\\\^</TD>,);}
1995                if ((($vspans>1) || ($tableRows[$r+1][0] =~ m,<TD.*?>&nbsp;</TD>,)) &&
1996                        $#{$tableRows[$r]} == $#{$tableRows[$r+1]}  && 0)
1997                {
1998                        if ($debug)
1999                        {
2000                                plainOutput("<!-- merging row $r+1 into previous -->\n");
2001                                plainOutput("<!-- row $r: (".join(",", @{$tableRows[$r]}).") -->\n");
2002                                plainOutput("<!-- row $r+1: (".join(",", @{$tableRows[$r+1]}).") -->\n");
2003                        }
2004                        for ($c=0; $c<=$#{$tableRows[$r]}; ++$c)
2005                        {
2006                                $tableRows[$r][$c] .= $tableRows[$r+1][$c];
2007                                $tableRows[$r][$c] =~ s,\\\^,,g;        # merging is stronger than spanning!
2008                                $tableRows[$r][$c] =~ s,</TD><TD.*?>,<BR>,;
2009                        }
2010                        @tableRows = (@tableRows[0..$r], @tableRows[$r+2 .. $#tableRows]);
2011                        --$r;   # process again
2012                }
2013        }
2014
2015        # Turn \^ vertical span requests into rowspan tags
2016        for($r=0; $r<$#tableRows; ++$r)
2017        {
2018                for ($c=0; $c<=$#{$tableRows[$r]}; ++$c)
2019                {
2020                        $r2 = $r+1;
2021                        while ( $r2<=$#tableRows && ($tableRows[$r2][$c] =~ m,<TD.*?>\\\^</TD>,) )
2022                        {
2023                                ++$r2;
2024                        }
2025                        $rs = $r2-$r;
2026                        if ($rs > 1)
2027                        {
2028                                plainOutput("<!-- spanning from $r,$c -->\n") if ($debug);
2029                                $tableRows[$r][$c] =~ s/<TD/<TD rowspan=$rs/;
2030                        }
2031                }
2032        }
2033
2034        # As tbl and html differ in whether they expect spanned cells to be
2035        # supplied, remove any cells that are 'rowspanned into'.
2036        for($r=0; $r<=$#tableRows; ++$r)
2037        {
2038                for ($c=$#{$tableRows[$r]}; $c>=0; --$c)
2039                {
2040                        if ($tableRows[$r][$c] =~ m/<TD rowspan=(\d+)/)
2041                        {
2042                                for ($r2=$r+1; $r2<$r+$1; ++$r2)
2043                                {
2044                                        $rowref = $tableRows[$r2];
2045                                        plainOutput("<!-- removing $r2,$c: ".$rowref->[$c]." -->\n") if ($debug);
2046                                        @$rowref = (@{$rowref}[0..$c-1], @{$rowref}[$c+1..$#$rowref]);
2047                                }
2048                        }
2049                }
2050        }
2051
2052        # Finally, output the cells that are left
2053        for($r=0; $r<=$#tableRows; ++$r)
2054        {
2055                plainOutput("<TR valign=top>\n");
2056                for ($c=0; $c <= $#{$tableRows[$r]}; ++$c)
2057                {
2058                        outputLine($tableRows[$r][$c]);
2059                }
2060                plainOutput("</TR>\n");
2061        }
2062        @tableRows = ();
2063        $troffCol = 0;
2064        plainOutput("<!-- flushTable done -->\n") if ($debug);
2065}
2066
2067
2068# Use these for all font changes, including .ft, .ps, .B, .BI, .SM etc.
2069# Need to add a mechanism to stack up these changes so tags match: <X> <Y> ... </Y> </X> etc.
2070
2071sub pushStyle
2072{
2073        $result = "";
2074        $type = $_[0];
2075        $tag = $_[1];
2076        print OUT "<!-- pushStyle $type($tag) [".join(",", @styleStack)."] " if ($debug>1);
2077        @oldItems = ();
2078        if (grep(m/^$type/, @styleStack))
2079        {
2080                print OUT "undoing up to old $type " if ($debug>1);
2081                while (@styleStack)
2082                {
2083                        # search back, undoing intervening tags in reverse order
2084                        $oldItem = pop @styleStack;
2085                        ($oldTag) = ($oldItem =~ m/^.(\S+)/);
2086                        $result .= "</$oldTag>";
2087                        if (substr($oldItem,0,1) eq $type)
2088                        {
2089                                print OUT "found $oldItem " if ($debug>1);
2090                                while (@oldItems)
2091                                {
2092                                        # restore the intermediates again
2093                                        $oldItem = shift @oldItems;
2094                                        push(@styleStack, $oldItem);
2095                                        $result .= "<".substr($oldItem,1).">";
2096                                }
2097                                last;
2098                        }
2099                        else
2100                        {
2101                                unshift(@oldItems, $oldItem);
2102                        }
2103                }
2104        }
2105        print OUT "oldItems=(@oldItems) " if ($debug>1);
2106        push(@styleStack, @oldItems);   # if we didn't find anything of type
2107        if ($tag)
2108        {
2109                $result .= "<$tag>";
2110                push(@styleStack, $type.$tag);
2111        }
2112        print OUT "-> '$result' -->\n" if ($debug>1);
2113        return $result;
2114}
2115
2116sub resetStyles
2117{
2118        if (@styleStack)
2119        {
2120                print OUT "<!-- resetStyles [".join(",", @styleStack)."] -->\n";
2121                print OUT "<HR> resetStyles [".join(",", @styleStack)."] <HR>\n" if ($debug);
2122        }
2123        while (@styleStack)
2124        {
2125                $oldItem = pop @styleStack;
2126                ($oldTag) = ($oldItem =~ m/^.(\S+)/);
2127                print OUT "</$oldTag>";
2128        }
2129        $currentSize = 0;
2130        $currentShift = 0;
2131}
2132
2133sub blockquote
2134{
2135        print OUT "<BLOCKQUOTE>\n";
2136        ++$blockquote;
2137}
2138
2139sub endBlockquote
2140{
2141        resetStyles();
2142        while ($blockquote > 0)
2143        {
2144                print OUT "</BLOCKQUOTE>\n";
2145                --$blockquote;
2146        }
2147}
2148
2149sub indent
2150{
2151        plainOutput(pushStyle("I", "TABLE"));
2152        $width = $_[0];
2153        $width = " width=$width%" if ($width);
2154        plainOutput("<TR><TD$width>&nbsp;</TD><TD>\n");
2155}
2156
2157sub outdent
2158{
2159        plainOutput("</TD></TR>\n");
2160        plainOutput(pushStyle("I"));
2161}
2162
2163sub inlineStyle
2164{
2165        $_[0] =~ m/^(.)(.*)$/;
2166        if ($1 eq "f")
2167                { fontChange($2); }
2168        elsif ($1 eq "s" && ! $noFill)
2169                { sizeChange($2); }
2170        else
2171                { superSub($1); }
2172}
2173
2174sub fontChange
2175{
2176        $fnt = $_[0];
2177        $fnt =~ s/^\(//;
2178
2179        if ($fnt eq "P" || $fnt eq "R" || $fnt eq "1" || $fnt eq "")
2180                { $font = ""; }
2181        elsif ($fnt eq "B" || $fnt eq "3")
2182                { $font = "B"; }
2183        elsif ($fnt eq "I" || $fnt eq "2")
2184                { $font = "I"; }
2185        else
2186                { $font = "TT"; }
2187        return pushStyle("F", $font);
2188}
2189
2190sub sizeChange
2191{
2192        $size= $_[0];
2193        if ($size =~ m/^[+-]/)
2194                { $currentSize += $size; }
2195        else
2196                { $currentSize = $size-10; }
2197        $currentSize = 0 if (! $size);
2198
2199        $sz = $currentSize;
2200        $sz = -2 if ($sz < -2);
2201        $sz = 2 if ($sz > 2);
2202
2203        if ($currentSize eq "0")
2204                { $size = ""; }
2205        else
2206                { $size = "FONT size=$sz"; }
2207        return pushStyle("S", $size);
2208}
2209
2210sub superSub
2211{
2212        $sub = $_[0];
2213        ++$currentShift if ($sub eq "u");
2214        --$currentShift if ($sub eq "d");
2215        $tag = "";
2216        $tag = "SUP" if ($currentShift > 0);
2217        $tag = "SUB" if ($currentShift < 0);
2218        return pushStyle("D", $tag);
2219}
2220
2221sub startNoFill
2222{
2223        print OUT "<PRE>\n" unless($noFill);
2224        $noFill = 1;
2225}
2226
2227sub endNoFill
2228{
2229        print OUT "</PRE>\n" if ($noFill);
2230        $noFill = 0;
2231}
2232
2233
2234sub processEqns
2235{
2236        if ($eqnMode==2 && $_[0] =~ m/^\.EN/)
2237        {
2238                $eqnMode = 0;
2239                outputLine(flushEqn());
2240                plainOutput("\n");
2241                return;
2242        }
2243        $eqnBuffer .= $_[0]." ";
2244}
2245
2246sub processEqnd
2247{
2248        processEqns(@_);
2249        return flushEqn();
2250}
2251
2252sub flushEqn
2253{
2254        @p = grep($_ !~ m/^ *$/, split(/("[^"]*"|\s+|[{}~^])/, $eqnBuffer) );
2255        $eqnBuffer = "";
2256        #return "[".join(',', @p)." -> ".&doEqn(@p)."]\n";
2257        $res = &doEqn(@p);
2258        #$res =~ s,\\\((..),$special{$1}||"\\($1",ge;
2259        #$res =~ s,<,&lt;,g;
2260        #$res =~ s,>,&gt;,g;
2261        return $res;
2262}
2263
2264sub doEqn
2265{
2266        my @p = @_;
2267        my $result = "";
2268        my $res;
2269        my $c;
2270        while (@p)
2271        {
2272                ($res, @p) = doEqn1(@p);
2273                $result .= $res;
2274        }
2275        return $result;
2276}
2277
2278sub doEqn1
2279{
2280        my @p = @_;
2281        my $res = "";
2282        my $c;
2283
2284        $c = shift @p;
2285        if ($eqndefs{$c})
2286        {
2287                @x = split(/\0/, $eqndefs{$c});
2288                unshift @p, @x;
2289                $c = shift @p;
2290        }
2291        if ($c =~ m/^"(.*)"$/)
2292        {
2293                $res = $1;
2294        }
2295        elsif ($c eq "delim")
2296        {
2297                $c = shift @p;
2298                if ($c eq "off")
2299                {
2300                        $eqnStart = "";
2301                        $eqnEnd = "";
2302                }
2303                else
2304                {
2305                        $c =~ m/^(.)(.)/;
2306                        $eqnStart = quotemeta($1);
2307                        $eqnEnd = quotemeta($2);
2308                }
2309        }
2310        elsif ($c eq "define" || $c eq "tdefine" || $c eq "ndefine")
2311        {
2312                $t = shift @p;
2313                $d = shift @p;
2314                $def = "";
2315                if (length($d) != 1)
2316                {
2317                        $def = $d;
2318                        $def =~ s/^.(.*)./\1/;
2319                }
2320                else
2321                {
2322                        while (@p && $p[0] ne $d)
2323                        {
2324                                $def .= shift @p;
2325                                $def .= "\0";
2326                        }
2327                        chop $def;
2328                        shift @p;
2329                }
2330                $eqndefs{$t} = $def unless ($c eq "ndefine");
2331        }
2332        elsif ($c eq "{")
2333        {
2334                my $level = 1;
2335                my $i;
2336                for ($i=0; $i<=$#p; ++$i)
2337                {
2338                        ++$level if ($p[$i] eq "{");
2339                        --$level if ($p[$i] eq "}");
2340                        last if ($level==0);
2341                }
2342                $res = doEqn(@p[0..$i-1]);
2343                @p = @p[$i+1..$#p];
2344        }
2345        elsif ($c eq "sup")
2346        {
2347                ($c,@p) = &doEqn1(@p);
2348                $res = "\\u$c\\d";
2349        }
2350        elsif ($c eq "to")
2351        {
2352                ($c,@p) = &doEqn1(@p);
2353                $res = "\\u$c\\d ";
2354        }
2355        elsif ($c eq "sub" || $c eq "from")
2356        {
2357                ($c,@p) = &doEqn1(@p);
2358                $res = "\\d$c\\u";
2359        }
2360        elsif ($c eq "matrix")
2361        {
2362                ($c,@p) = &doEqn1(@p);
2363                $res = "matrix ( $c )";
2364        }
2365        elsif ($c eq "bold")
2366        {
2367                ($c,@p) = &doEqn1(@p);
2368                $res = "\\fB$c\\fP";
2369        }
2370        elsif ($c eq "italic")
2371        {
2372                ($c,@p) = &doEqn1(@p);
2373                $res = "\\fI$c\\fP";
2374        }
2375        elsif ($c eq "roman")
2376        {
2377        }
2378        elsif ($c eq "font" || $c eq "gfont" || $c eq "size" || $c eq "gsize")
2379        {
2380                shift @p;
2381        }
2382        elsif ($c eq "mark" || $c eq "lineup")
2383        {
2384        }
2385        elsif ($c eq "~" || $c eq "^")
2386        {
2387                $res = " ";
2388        }
2389        elsif ($c eq "over")
2390        {
2391                $res = " / ";
2392        }
2393        elsif ($c eq "half")
2394        {
2395                $res = "\\(12";
2396        }
2397        elsif ($c eq "prime")
2398        {
2399                $res = "\\' ";
2400        }
2401        elsif ($c eq "dot")
2402        {
2403                $res = "\\u.\\d ";
2404        }
2405        elsif ($c eq "dotdot")
2406        {
2407                $res = "\\u..\\d ";
2408        }
2409        elsif ($c eq "tilde")
2410        {
2411                $res = "\\u~\\d ";
2412        }
2413        elsif ($c eq "hat")
2414        {
2415                $res = "\\u^\\d ";
2416        }
2417        elsif ($c eq "bar" || $c eq "vec")
2418        {
2419                $res = "\\(rn ";
2420        }
2421        elsif ($c eq "under")
2422        {
2423                $res = "_ ";
2424        }
2425        elsif ( $c eq "sqrt" || $c eq "lim" || $c eq "sum" || $c eq "pile" || $c eq "lpile" ||
2426                        $c eq "rpile" || $c eq "cpile" || $c eq "int" || $c eq "prod" )
2427        {
2428                $res = " $c ";
2429        }
2430        elsif ($c eq "cdot")
2431        {
2432                $res = " . ";
2433        }
2434        elsif ($c eq "inf")
2435        {
2436                $res = "\\(if";
2437        }
2438        elsif ($c eq "above" || $c eq "lcol" || $c eq "ccol")
2439        {
2440                $res = " ";
2441        }
2442        elsif ($c eq "sin" || $c eq "cos" || $c eq "tan" || $c eq "log" || $c eq "ln" )
2443        {
2444                $res = " $c ";
2445        }
2446        elsif ($c eq "left" || $c eq "right" || $c eq "nothing")
2447        {
2448        }
2449        elsif ($c =~ m/^[A-Za-z]/)
2450        {
2451                $res = "\\fI$c\\fP";
2452        }
2453        else
2454        {
2455                $res = $c;
2456        }
2457
2458        return ($res, @p);
2459}
2460
2461##### Search manpath and initialise special char array #####
2462
2463sub initialise
2464{
2465        # Parse the macro definition file for section names
2466        if (open(MACRO, "/usr/lib/tmac/tmac.an") ||
2467                open(MACRO, "/usr/lib/tmac/an") ||
2468                open(MACRO, "/usr/lib/groff/tmac/tmac.an") ||
2469                open(MACRO, "/usr/share/tmac/tmac.an") ||
2470                open(MACRO, "/usr/share/groff/tmac/tmac.an") )
2471        {
2472                while (<MACRO>)
2473                {
2474                        chop;
2475                        if (m/\$2'([0-9a-zA-Z]+)' .ds ]D (.*)$/)
2476                        {
2477                                $sn = $2;
2478                                unless ($sn =~ m/[a-z]/)
2479                                {
2480                                        $sn = "\u\L$sn";
2481                                        $sn =~ s/ (.)/ \u\1/g;
2482                                }
2483                                $sectionName{"\L$1"} = $sn;
2484                        }
2485                        if (m/\$1'([^']+)' .ds Tx "?(.*)$/)
2486                        {
2487                                $title{"$1"} = $2;
2488                        }
2489                        if (m/^.ds ]W (.*)$/)
2490                        {
2491                                $osver = $1;
2492                        }
2493                }
2494        }
2495        else
2496        {
2497                print STDERR "Failed to read tmac.an definitions\n" unless ($cgiMode);
2498        }
2499        if (open(MACRO, "/usr/lib/tmac/tz.map"))
2500        {
2501                while (<MACRO>)
2502                {
2503                        chop;
2504                        if (m/\$1'([^']+)' .ds Tz "?(.*)$/)
2505                        {
2506                                $title{"$1"} = $2;
2507                        }
2508                }
2509        }
2510
2511        # Prevent redefinition of macros that have special meaning to us
2512        $reservedMacros = '^(SH|SS|Sh|Ss)$';
2513
2514        # Predefine special number registers
2515        $number{'.l'} = 75;
2516
2517        # String variables defined by man package
2518        $vars{'lq'} = '&#147;';
2519        $vars{'rq'} = '&#148;';
2520        $vars{'R'} = '\\(rg';
2521        $vars{'S'} = '\\s0';
2522
2523        # String variables defined by mdoc package
2524        $vars{'Le'} = '\\(<=';
2525        $vars{'<='} = '\\(<=';
2526        $vars{'Ge'} = '\\(>=';
2527        $vars{'Lt'} = '<';
2528        $vars{'Gt'} = '>';
2529        $vars{'Ne'} = '\\(!=';
2530        $vars{'>='} = '\\(>=';
2531        $vars{'q'} = '&#34;';   # see also special case in preProcessLine
2532        $vars{'Lq'} = '&#147;';
2533        $vars{'Rq'} = '&#148;';
2534        $vars{'ua'} = '\\(ua';
2535        $vars{'ga'} = '\\(ga';
2536        $vars{'Pi'} = '\\(*p';
2537        $vars{'Pm'} = '\\(+-';
2538        $vars{'Na'} = 'NaN';
2539        $vars{'If'} = '\\(if';
2540        $vars{'Ba'} = '|';
2541
2542        # String variables defined by ms package (access to accented characters)
2543        $vars{'bu'} = '&#187;';
2544        $vars{'66'} = '&#147;';
2545        $vars{'99'} = '&#148;';
2546        $vars{'*!'} = '&#161;';
2547        $vars{'ct'} = '&#162;';
2548        $vars{'po'} = '&#163;';
2549        $vars{'gc'} = '&#164;';
2550        $vars{'ye'} = '&#165;';
2551        #$vars{'??'} = '&#166;';
2552        $vars{'sc'} = '&#167;';
2553        $vars{'*:'} = '&#168;';
2554        $vars{'co'} = '&#169;';
2555        $vars{'_a'} = '&#170;';
2556        $vars{'<<'} = '&#171;';
2557        $vars{'no'} = '&#172;';
2558        $vars{'hy'} = '&#173;';
2559        $vars{'rg'} = '&#174;';
2560        $vars{'ba'} = '&#175;';
2561        $vars{'de'} = '&#176;';
2562        $vars{'pm'} = '&#177;';
2563        #$vars{'??'} = '&#178;';
2564        #$vars{'??'} = '&#179;';
2565        $vars{'aa'} = '&#180;';
2566        $vars{'mu'} = '&#181;';
2567        $vars{'pg'} = '&#182;';
2568        $vars{'c.'} = '&#183;';
2569        $vars{'cd'} = '&#184;';
2570        #$vars{'??'} = '&#185;';
2571        $vars{'_o'} = '&#186;';
2572        $vars{'>>'} = '&#187;';
2573        $vars{'14'} = '&#188;';
2574        $vars{'12'} = '&#189;';
2575        #$vars{'??'} = '&#190;';
2576        $vars{'*?'} = '&#191;';
2577        $vars{'`A'} = '&#192;';
2578        $vars{"'A"} = '&#193;';
2579        $vars{'^A'} = '&#194;';
2580        $vars{'~A'} = '&#195;';
2581        $vars{':A'} = '&#196;';
2582        $vars{'oA'} = '&#197;';
2583        $vars{'AE'} = '&#198;';
2584        $vars{',C'} = '&#199;';
2585        $vars{'`E'} = '&#200;';
2586        $vars{"'E"} = '&#201;';
2587        $vars{'^E'} = '&#202;';
2588        $vars{':E'} = '&#203;';
2589        $vars{'`I'} = '&#204;';
2590        $vars{"'I"} = '&#205;';
2591        $vars{'^I'} = '&#206;';
2592        $vars{':I'} = '&#207;';
2593        $vars{'-D'} = '&#208;';
2594        $vars{'~N'} = '&#209;';
2595        $vars{'`O'} = '&#210;';
2596        $vars{"'O"} = '&#211;';
2597        $vars{'^O'} = '&#212;';
2598        $vars{'~O'} = '&#213;';
2599        $vars{':O'} = '&#214;';
2600        #$vars{'mu'} = '&#215;';
2601        $vars{'NU'} = '&#216;';
2602        $vars{'`U'} = '&#217;';
2603        $vars{"'U"} = '&#218;';
2604        $vars{'^U'} = '&#219;';
2605        $vars{':U'} = '&#220;';
2606        #$vars{'??'} = '&#221;';
2607        $vars{'Th'} = '&#222;';
2608        $vars{'*b'} = '&#223;';
2609        $vars{'`a'} = '&#224;';
2610        $vars{"'a"} = '&#225;';
2611        $vars{'^a'} = '&#226;';
2612        $vars{'~a'} = '&#227;';
2613        $vars{':a'} = '&#228;';
2614        $vars{'oa'} = '&#229;';
2615        $vars{'ae'} = '&#230;';
2616        $vars{',c'} = '&#231;';
2617        $vars{'`e'} = '&#232;';
2618        $vars{"'e"} = '&#233;';
2619        $vars{'^e'} = '&#234;';
2620        $vars{':e'} = '&#235;';
2621        $vars{'`i'} = '&#236;';
2622        $vars{"'i"} = '&#237;';
2623        $vars{'^i'} = '&#238;';
2624        $vars{':i'} = '&#239;';
2625        #$vars{'??'} = '&#240;';
2626        $vars{'~n'} = '&#241;';
2627        $vars{'`o'} = '&#242;';
2628        $vars{"'o"} = '&#243;';
2629        $vars{'^o'} = '&#244;';
2630        $vars{'~o'} = '&#245;';
2631        $vars{':o'} = '&#246;';
2632        $vars{'di'} = '&#247;';
2633        $vars{'nu'} = '&#248;';
2634        $vars{'`u'} = '&#249;';
2635        $vars{"'u"} = '&#250;';
2636        $vars{'^u'} = '&#251;';
2637        $vars{':u'} = '&#252;';
2638        #$vars{'??'} = '&#253;';
2639        $vars{'th'} = '&#254;';
2640        $vars{':y'} = '&#255;';
2641
2642        # troff special characters and their closest equivalent
2643
2644        $special{'em'} = '&#151;';
2645        $special{'hy'} = '-';
2646        $special{'\-'} = '&#150;';      # was -
2647        $special{'bu'} = 'o';
2648        $special{'sq'} = '[]';
2649        $special{'ru'} = '_';
2650        $special{'14'} = '&#188;';
2651        $special{'12'} = '&#189;';
2652        $special{'34'} = '&#190;';
2653        $special{'fi'} = 'fi';
2654        $special{'fl'} = 'fl';
2655        $special{'ff'} = 'ff';
2656        $special{'Fi'} = 'ffi';
2657        $special{'Fl'} = 'ffl';
2658        $special{'de'} = '&#176;';
2659        $special{'dg'} = '&#134;';      # was 182, para symbol
2660        $special{'fm'} = "\\'";
2661        $special{'ct'} = '&#162;';
2662        $special{'rg'} = '&#174;';
2663        $special{'co'} = '&#169;';
2664        $special{'pl'} = '+';
2665        $special{'mi'} = '-';
2666        $special{'eq'} = '=';
2667        $special{'**'} = '*';
2668        $special{'sc'} = '&#167;';
2669        $special{'aa'} = '&#180;';      # was '
2670        $special{'ga'} = '&#96;';       # was `
2671        $special{'ul'} = '_';
2672        $special{'sl'} = '/';
2673        $special{'*a'} = 'a';
2674        $special{'*b'} = '&#223;';
2675        $special{'*g'} = 'y';
2676        $special{'*d'} = 'd';
2677        $special{'*e'} = 'e';
2678        $special{'*z'} = 'z';
2679        $special{'*y'} = 'n';
2680        $special{'*h'} = 'th';
2681        $special{'*i'} = 'i';
2682        $special{'*k'} = 'k';
2683        $special{'*l'} = 'l';
2684        $special{'*m'} = '&#181;';
2685        $special{'*n'} = 'v';
2686        $special{'*c'} = '3';
2687        $special{'*o'} = 'o';
2688        $special{'*p'} = 'pi';
2689        $special{'*r'} = 'p';
2690        $special{'*s'} = 's';
2691        $special{'*t'} = 't';
2692        $special{'*u'} = 'u';
2693        $special{'*f'} = 'ph';
2694        $special{'*x'} = 'x';
2695        $special{'*q'} = 'ps';
2696        $special{'*w'} = 'o';
2697        $special{'*A'} = 'A';
2698        $special{'*B'} = 'B';
2699        $special{'*G'} = '|\\u_\\d';
2700        $special{'*D'} = '/&#92;';
2701        $special{'*E'} = 'E';
2702        $special{'*Z'} = 'Z';
2703        $special{'*Y'} = 'H';
2704        $special{'*H'} = 'TH';
2705        $special{'*I'} = 'I';
2706        $special{'*K'} = 'K';
2707        $special{'*L'} = 'L';
2708        $special{'*M'} = 'M';
2709        $special{'*N'} = 'N';
2710        $special{'*C'} = 'Z';
2711        $special{'*O'} = 'O';
2712        $special{'*P'} = '||';
2713        $special{'*R'} = 'P';
2714        $special{'*S'} = 'S';
2715        $special{'*T'} = 'T';
2716        $special{'*U'} = 'Y';
2717        $special{'*F'} = 'PH';
2718        $special{'*X'} = 'X';
2719        $special{'*Q'} = 'PS';
2720        $special{'*W'} = 'O';
2721        $special{'ts'} = 's';
2722        $special{'sr'} = 'v/';
2723        $special{'rn'} = '\\u&#150;\\d';        # was 175
2724        $special{'>='} = '&gt;=';
2725        $special{'<='} = '&lt;=';
2726        $special{'=='} = '==';
2727        $special{'~='} = '~=';
2728        $special{'ap'} = '&#126;';      # was ~
2729        $special{'!='} = '!=';
2730        $special{'->'} = '-&gt;';
2731        $special{'<-'} = '&lt;-';
2732        $special{'ua'} = '^';
2733        $special{'da'} = 'v';
2734        $special{'mu'} = '&#215;';
2735        $special{'di'} = '&#247;';
2736        $special{'+-'} = '&#177;';
2737        $special{'cu'} = 'U';
2738        $special{'ca'} = '^';
2739        $special{'sb'} = '(';
2740        $special{'sp'} = ')';
2741        $special{'ib'} = '(=';
2742        $special{'ip'} = '=)';
2743        $special{'if'} = 'oo';
2744        $special{'pd'} = '6';
2745        $special{'gr'} = 'V';
2746        $special{'no'} = '&#172;';
2747        $special{'is'} = 'I';
2748        $special{'pt'} = '~';
2749        $special{'es'} = '&#216;';
2750        $special{'mo'} = 'e';
2751        $special{'br'} = '|';
2752        $special{'dd'} = '&#135;';      # was 165, yen
2753        $special{'rh'} = '=&gt;';
2754        $special{'lh'} = '&lt;=';
2755        $special{'or'} = '|';
2756        $special{'ci'} = 'O';
2757        $special{'lt'} = '(';
2758        $special{'lb'} = '(';
2759        $special{'rt'} = ')';
2760        $special{'rb'} = ')';
2761        $special{'lk'} = '|';
2762        $special{'rk'} = '|';
2763        $special{'bv'} = '|';
2764        $special{'lf'} = '|';
2765        $special{'rf'} = '|';
2766        $special{'lc'} = '|';
2767        $special{'rc'} = '|';
2768
2769        # Not true troff characters but very common typos
2770        $special{'cp'} = '&#169;';
2771        $special{'tm'} = '&#174;';
2772        $special{'en'} = '-';
2773
2774        # Build a list of directories containing man pages
2775        @manpath = ();
2776        if (open(MPC, "/etc/manpath.config") || open(MPC, "/etc/man.config"))
2777        {
2778                while (<MPC>)
2779                {
2780                        if (m/^(MANDB_MAP|MANPATH)\s+(\S+)/)
2781                        {
2782                                push(@manpath, $2);
2783                        }
2784                }
2785        }
2786        @manpath = split(/:/, $ENV{'MANPATH'}) unless (@manpath);
2787        @manpath = ("/usr/man") unless (@manpath);
2788}
2789
2790# Search through @manpath and construct @mandirs (non-empty subsections)
2791sub loadManDirs
2792{
2793        return if (@mandirs);
2794        print STDERR "Searching ",join(":", @manpath)," for mandirs\n" unless($cgiMode);
2795        foreach $tld (@manpath)
2796        {
2797                $tld =~ m/^(.*)$/;
2798                $tld = $1;      # untaint manpath
2799                if (opendir(DIR, $tld))
2800                {
2801                        # foreach $d (<$tld/man[0-9a-z]*>)
2802                        foreach $d (sort readdir(DIR))
2803                        {
2804                                if ($d =~ m/^man\w/ && -d "$tld/$d")
2805                                {
2806                                        push (@mandirs, "$tld/$d");
2807                                }
2808                        }
2809                        closedir DIR;
2810                }
2811        }
2812}
2813
2814##### Utility to search manpath for a given command #####
2815
2816sub findPage
2817{
2818        $request = $_[0];
2819        $request =~ s,^/,,;
2820        @multipleMatches = ();
2821
2822        $file = $_[0];
2823        return $file if (-f $file || -f "$file.gz" || -f "$file.bz2");
2824
2825        # Search the path for the requested man page, which may be of the form:
2826        # "/usr/man/man1/ls.1", "ls.1" or "ls".
2827        ($page,$sect) = ($request =~ m/^(.+)\.([^.]+)$/);
2828        $sect = "\L$sect";
2829
2830        # Search the specified section first (if specified)
2831        if ($sect)
2832        {
2833                foreach $md (@manpath)
2834                {
2835                        $dir = $md;
2836                        $file = "$dir/man$sect/$page.$sect";
2837                        push(@multipleMatches, $file) if (-f $file || -f "$file.gz" || -f "$file.bz2");
2838                }
2839        }
2840        else
2841        {
2842                $page = $request;
2843        }
2844        if (@multipleMatches == 1)
2845        {
2846                return pop @multipleMatches;
2847        }
2848
2849        # If not found need to search through each directory
2850        loadManDirs();
2851        foreach $dir (@mandirs)
2852        {
2853                ($s) = ($dir =~ m/man([0-9A-Za-z]+)$/);
2854                $file = "$dir/$page.$s";
2855                push(@multipleMatches, $file) if (-f $file || -f "$file.gz" || -f "$file.bz2");
2856                $file = "$dir/$request";
2857                push(@multipleMatches, $file) if (-f $file || -f "$file.gz" || -f "$file.bz2");
2858                if ($sect && "$page.$sect" ne $request)
2859                {
2860                        $file = "$dir/$page.$sect";
2861                        push(@multipleMatches, $file) if (-f $file || -f "$file.gz" || -f "$file.bz2");
2862                }
2863        }
2864        if (@multipleMatches == 1)
2865        {
2866                return pop @multipleMatches;
2867        }
2868        if (@multipleMatches > 1)
2869        {
2870                return "";
2871        }
2872        # Ok, didn't find it using section numbers. Perhaps there's a page with the
2873        # right name but wrong section number lurking there somewhere. (This search is slow)
2874        # eg. page.1x in man1 (not man1x) directory
2875        foreach $dir (@mandirs)
2876        {
2877                opendir(DIR, $dir);
2878                foreach $f (readdir DIR)
2879                {
2880                        if ($f =~ m/^$page\./)
2881                        {
2882                                $f =~ s/\.(gz|bz2)$//;
2883                                push(@multipleMatches, "$dir/$f");
2884                        }
2885                }
2886        }
2887        if (@multipleMatches == 1)
2888        {
2889                return pop @multipleMatches;
2890        }
2891        return "";
2892}
2893
2894sub loadPerlPages
2895{
2896        my ($dir,$f,$name,@files);
2897        loadManDirs();
2898        return if (%perlPages);
2899        foreach $dir (@mandirs)
2900        {
2901                if (opendir(DIR, $dir))
2902                {
2903                        @files = sort readdir DIR;
2904                        foreach $f (@files)
2905                        {
2906                                next if ($f eq "." || $f eq ".." || $f !~ m/\./);
2907                                next unless ("$dir/$f" =~ m/perl/);
2908                                $f =~ s/\.(gz|bz2)$//;
2909                                ($name) = ($f =~ m,(.+)\.[^.]*$,);
2910                                $perlPages{$name} = "$dir/$f";
2911                        }
2912                        closedir DIR;
2913                }
2914        }
2915        delete $perlPages{'perl'};      # too ubiquitous to be useful
2916}
2917
2918sub fmtTime
2919{
2920    my $time = $_[0];
2921    my @days = qw (Sun Mon Tue Wed Thu Fri Sat);
2922    my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
2923    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$istdst) = localtime($time);
2924    return sprintf ("%s, %02d %s %4d %02d:%02d:%02d GMT",
2925         $days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec);
2926}
2927