#!/usr/bin/perl -T # manServer - Unix man page to HTML converter # Rolf Howarth, rolf@squarebox.co.uk # Version 1.07 16 July 2001 # Version 1.07+ma1 2006-03-31 Matthias Andree # add trailing slash of URLs # support https, too $version = "1.07+ma1"; $manServerUrl = "manServer $version"; use Socket; $ENV{'PATH'} = "/bin:/usr/bin"; initialise(); $request = shift @ARGV; # Usage: manServer [-dn] filename | manServer [-s port] $root = ""; $cgiMode = 0; $bodyTag = "BODY bgcolor=#F0F0F0 text=#000000 link=#0000ff vlink=#C000C0 alink=#ff0000"; if ($ENV{'GATEWAY_INTERFACE'} ne "") { *OUT = *STDOUT; open(LOG, ">>/tmp/manServer.log"); chmod(0666, '/tmp/manServer.log'); $root = $ENV{'SCRIPT_NAME'}; $url = $ENV{'PATH_INFO'}; if ($ENV{'REQUEST_METHOD'} eq "POST") { $args = ; chop $args; } else { $args = $ENV{'QUERY_STRING'}; } $url .= "?".$args if ($args); $cgiMode = 1; $date = &fmtTime(time); $remoteHost = $ENV{'REMOTE_HOST'} || $ENV{'REMOTE_ADDR'}; $referer = $ENV{'HTTP_REFERER'}; $userAgent = $ENV{'HTTP_USER_AGENT'}; print LOG "$date\t$remoteHost\t$url\t$referer\t$userAgent\n"; processRequest($url); } elsif ($request eq "-s" || $request eq "") { *LOG = *STDERR; startServer(); } else { $cmdLineMode = 1; if ($request =~ m/^-d(\d)/) { $debug = $1; $request = shift @ARGV; } *OUT = *STDOUT; *LOG = *STDERR; $file = findPage($request); man2html($file); } exit(0); ##### Mini HTTP Server #### sub startServer { ($port) = @ARGV; $port = 8888 unless $port; $sockaddr = 'S n a4 x8'; ($name, $aliases, $proto) = getprotobyname('tcp'); ($name, $aliases, $port) = getservbyname($port, 'tcp') unless $port =~ /^\d+$/; while(1) { $this = pack($sockaddr, AF_INET, $port, "\0\0\0\0"); select(NS); $| = 1; select(stdout); socket(S, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; if (bind(S, $this)) { last; } else { print STDERR "Failed to bind to port $port: $!\n"; ++$port; } } listen(S, 5) || die "connect: $!"; select(S); $| = 1; select(stdout); while(1) { print LOG "Waiting for connection on port $port\n"; ($addr = accept(NS,S)) || die $!; #print "accept ok\n"; ($af,$rport,$inetaddr) = unpack($sockaddr,$addr); @inetaddr = unpack('C4',$inetaddr); print LOG "Got connection from ", join(".",@inetaddr), "\n"; while () { if (m/^GET (\S+)/) { $url = $1; } last if (m/^\s*$/); } *OUT = *NS; processRequest($url); close NS ; } } sub processRequest { $url = $_[0]; print LOG "Request = $url, root = $root\n"; if ( ($url =~ m/^([^?]*)\?(.*)$/) || ($url =~ m/^([^&]*)&(.*)$/) ) { $request = $1; $args = $2; } else { $request = $url; $args = ""; } @params = split(/[=&]/, $args); for ($i=0; $i<=$#params; ++$i) { $params[$i] =~ tr/+/ /; $params[$i] =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack("C",hex($1))/eg; } %params = @params; $request = $params{'q'} if ($params{'q'}); $searchType = $params{'t'}; $debug = $params{'d'}; $processed = 0; $file = ""; if ($searchType) { print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; print OUT "

Searching not yet implemented

\n"; print LOG "Searching not implemented\n"; $processed = 1; } elsif ($request eq "/" || $request eq "") { print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; print LOG "Home page\n"; homePage(); $processed = 1; } elsif ($request =~ m,^/.*/$,) { print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; print LOG "List directory\n"; listDir($request); $processed = 1; } elsif (-f $request || -f "$request.gz" || -f "$request.bz2") { # Only allow fully specified files if they're in our manpath foreach $md (@manpath) { $dir = $md; if (substr($request,0,length($dir)) eq $dir) { print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; man2html($request); $processed = 1; last; } } } else { $file = findPage($request); if (@multipleMatches) { print OUT "HTTP/1.0 200 Ok\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; print LOG "Multiple matches\n"; printMatches(); $processed = 1; } elsif ($file) { print OUT "HTTP/1.0 301 Redirected\n" unless ($cgiMode); $file .= "&d=$debug" if ($debug); print OUT "Location: $root$file\n\n"; print LOG "Redirect to $root$file\n"; $processed = 1; } } unless ($processed) { print OUT "HTTP/1.0 404 Not Found\n" unless ($cgiMode); print OUT "Content-type: text/html\n\n"; print OUT "\nNot Found\n<$bodyTag>\n"; print OUT "


Not Found

\nFailed to find man page /$request\n"; print OUT "


Main Index\n\n"; print STDERR "Failed to find /$request\n" unless ($cgiMode); } } sub homePage { print OUT "Manual Pages - Main Index <$bodyTag>


Manual Reference Pages - Main Index

\n"; $uname = `uname -s -r`; if (! $?) { $hostname = `hostname`; print OUT "$uname pages on $hostname

\n"; } # print OUT "\n"; print OUT "Command name:

\n"; loadManDirs(); foreach $dir (@mandirs) { ($section) = ($dir =~ m/man([0-9A-Za-z]+)$/); print OUT "$dir" ; print OUT "- $sectionName{$section}" if ($sectionName{$section}); print OUT "
\n"; } print OUT "


Generated by $manServerUrl from local unix man pages.\n\n"; } sub listDir { foreach $md (@manpath) { $dir = $md; if (substr($request,0,length($dir)) eq $dir) { $request =~ s,/$,,; ($section) = ($request =~ m/man([0-9A-Za-z]+)$/); $sectionName = $sectionName{$section}; $sectionName = "Manual Reference Pages" unless ($sectionName); print OUT "Contents of $request\n<$bodyTag>\n"; print OUT "


$sectionName - Index of $request

\n"; print OUT "
\n"; print OUT "Command name:

\n"; if (opendir(DIR, $request)) { @files = sort readdir DIR; foreach $f (@files) { next if ($f eq "." || $f eq ".." || $f !~ m/\./); $f =~ s/\.(gz|bz2)$//; # ($name) = ($f =~ m,/([^/]*)$,); print OUT "$f \n"; } closedir DIR; } print OUT "

Main Index\n\n"; print OUT "


Generated by $manServerUrl from local unix man pages.\n\n"; return; } } print OUT "

Directory $request not known

\n"; } sub printMatches { print OUT "Ambiguous Request '$request'\n<$bodyTag>\n"; print OUT "


Ambiguous Request '$request'

\nPlease select one of the following pages:

"; foreach $f (@multipleMatches) { print OUT "$f
\n"; } print OUT "

Main Index\n\n"; } ##### Process troff input using man macros into HTML ##### sub man2html { $file = $_[0]; $srcfile = $file; $zfile = $file; if (! -f $file) { if (-f "$file.gz") { $zfile = "$file.gz"; $zcat = "/usr/bin/zcat"; $zcat = "/bin/zcat" unless (-x $zcat); $srcfile = "$zcat $zfile |"; $srcfile =~ m/^(.*)$/; $srcfile = $1; # untaint } elsif (-f "$file.bz2") { $zfile = "$file.bz2"; $srcfile = "/usr/bin/bzcat $zfile |"; $srcfile =~ m/^(.*)$/; $srcfile = $1; # untaint } } print LOG "man2html $file\n"; $foundNroffTag = 0; loadContents($file); unless (open(SRC, $srcfile)) { print OUT "

Failed to open $file

\n"; print STDERR "Failed to open $srcfile\n"; return; } ($dir,$page,$sect) = ($file =~ m,^(.*)/([^/]+)\.([^.]+)$,); $troffTable = 0; %macro = (); %renamedMacro = (); %deletedMacro = (); @indent = (); @tabstops = (); $indentLevel = 0; $prevailingIndent = 6; $trapLine = 0; $blockquote = 0; $noSpace = 0; $firstSection = 0; $eqnStart = ""; $eqnEnd = ""; $eqnMode = 0; %eqndefs = (); $defaultNm = ""; $title = $file; $title = "Manual Page - $page($sect)" if ($page && $sect); $_ = getLine(); if (m/^.so (man.*)$/) { # An .so include on the first line only is replaced by the referenced page. # (See elsewhere for processing of included sections that occur later in document.) man2html("$dir/../$1"); return; } $perlPattern = ""; if ($file =~ m/perl/) { &loadPerlPages(); $perlPattern = join('|', grep($_ ne $page, keys %perlPages)); } print OUT "\n$title\n<$bodyTag>\n"; if ($foundNroffTag) { do { preProcessLine(); processLine(); } while(getLine()); endNoFill(); endParagraph(); } else { # Special case where input is not nroff at all but is preformatted text $sectionName = "Manual Reference Pages"; $sectionNumber = $sect; $left = "Manual Page"; $right = "Manual Page"; $macroPackage = "(preformatted text)"; $pageName = "$page($sect)"; $saveCurrentLine = $_; outputPageHead(); $_ = $saveCurrentLine; print OUT "
\n";
		do
		{
			print OUT $_;
		}
		while(getLine());
		print OUT "
\n"; } outputPageFooter(); } sub outputPageHead { plainOutput( "
\n" ); outputLine( "


$sectionName  - $pageName

\n" ); plainOutput( "
\n" ); } sub outputPageFooter { if ($pageName) { unless ($cmdLineMode) { plainOutput( "
\n" ); plainOutput( "Jump to page    or go to Top of page | \n" ); plainOutput( "Section $sectionNumber | \n" ); plainOutput( "Main Index.\n" ); plainOutput( "\n" ); } endBlockquote(); outputLine("


\n
$left $pageName $right
"); } plainOutput("Generated by $manServerUrl from $zfile $macroPackage.\n\n"); } sub outputContents { print OUT "

CONTENTS

\n"; blockquote(); for ($id=1; $id<=$#contents; ++$id) { $name = $contents[$id]; $pre = ""; $pre = "     " if ($name =~ m/^ /); $pre .= "     " if ($name =~ m/^ /); $name =~ s,^\s+,,; next if ($name eq "" || $name =~ m,^/,); unless ($name =~ m/[a-z]/) { $name = "\u\L$name"; $name =~ s/ (.)/ \u\1/g; } outputLine("$pre$name
\n"); } endBlockquote(); } # First pass to extract table of contents sub loadContents { @contents = (); %contents = (); # print STDERR "SRCFILE = $srcfile\n"; open(SRC, $srcfile) || return; while () { preProcessLine(); $foundNroffTag = $foundNroffTag || (m/^\.(\\\"|TH|so) /); if (m/^\.(S[HShs]) ([A-Z].*)\s*$/) { $foundNroffTag = 1; $c = $1; $t = $2; $t =~ s/"//g; $id = @contents; if ($c eq "SH" || $c eq "Sh") { push(@contents, $t); } elsif ($t =~ m/\\f/) { $t =~ s/\\f.//g; push(@contents, " $t"); } else { push(@contents, " $t"); } $contents{"\U$t"} = $id; } } close SRC; } # Preprocess $_ sub preProcessLine { # Remove spurious white space to canonicise the input chop; $origLine = $_; s, $,,g; s,^',.,; # treat non breaking requests as if there was a dot s,^\.\s*,\.,; if ($eqnMode == 1) { if (m/$eqnEnd/) { s,^(.*?)$eqnEnd,&processEqnd($1),e; $eqnMode = 0; } else { &processEqns($_); } } if ($eqnStart && $eqnMode==0) { s,$eqnStart(.*?)$eqnEnd,&processEqnd($1),ge; if (m/$eqnStart/) { s,$eqnStart(.*)$,&processEqns($1),e; $eqnMode = 1; } } # XXX Note: multiple levels of escaping aren't handled properly, eg. \\*.. as a macro argument # should get interpolated as string but ends up with a literal '\' being copied through to output. s,\\\\\*q,",g; # treat mdoc \\*q as special case s,\\\\,_DBLSLASH_,g; s,\\ ,_SPACE_,g; s,\s*\\".*$,,; s,\\$,,; # Then apply any variable substitutions and escape < and > # (which has to be done before we start inserting tags...) s,\\\*\((..),$vars{$1},ge; s/\\\*([*'`,^,:~].)/$vars{$1}||"\\*$1"/ge; s,\\\*(.),$vars{$1},ge; # Expand special characters for the first time (eg. \(<- s,\\\((..),$special{$1}||"\\($1",ge; s,<,<,g; s,>,>,g; # Interpolate width and number registers s,\\w(.)(.*?)\1,&width($2),ge; s,\\n\((..),&numreg($1),ge; s,\\n(.),&numreg($1),ge; } # Undo slash escaping, normally done at output stage, also in macro defn sub postProcessLine { s,_DBLSLASH_,\\,g; s,_SPACE_, ,g; } # Rewrite the line, expanding escapes such as font styles, and output it. # The line may be a plain text troff line, or it might be the expanded output of a # macro in which case some HTML tags may already have been inserted into the text. sub outputLine { $_ = $_[0]; print OUT "\n" if ($debug>1); if ($needBreak) { plainOutput("
\n"); lineBreak(); } if ($textSinceBreak && !$noFill && $_ =~ m/^\s/) { plainOutput("
\n"); lineBreak(); } s,\\&\.,.,g; # \&. often used to escape dot at start of line s,\\\.,.,g; s,\\\^,,g; s,\\\|,,g; s,\\c,,g; s,\\0, ,g; s,\\t,\t,g; s,\\%, ,g; s,\\{,,g; s,\\},,g; s,\\$,,g; s,\\e,\,g; s,\\([-+_~#[]),\1,g; # Can't implement local motion tags s,\\[hv](.).*?\1,,g; s,\\z,,g; # Font changes, super/sub-scripts and font size changes s,\\(f[^(]|f\(..|u|d|s[-+]?\d),&inlineStyle($1),ge; # Overstrike if (m/\\o/) { # handle a few special accent cases we know how to deal with s,\\o(.)([aouAOU])"\1,\\o\1\2:\1,g; s,\\o(.)(.)\\(.)\1,\\o\1\2\3\1,g; s;\\o(.)([A-Za-z])(['`:,^~])\1;\\o\1\3\2\1;g; #s,\\o(.)(.*?)\1,"".($vars{$2}||$2)."",ge; s,\\o(.)(.*?)\1,$vars{$2}||$2,ge; } # Bracket building (ignore) s,\\b(.)(.*?)\1,\2,g; s,\\`,`,g; s,\\',',g; s,',’,g; s,`,‘,g; # Expand special characters introduced by eqn s,\\\((..),$special{$1}||"\\($1",ge; s,\\\((..),\\($1,g unless (m,^\.,); # Don't know how to handle other escapes s,(\\[^&]),\1,g unless (m,^\.,); postProcessLine(); # Insert links for http, ftp and mailto URLs # Recognised URLs are sequence of alphanumerics and special chars like / and ~ # but must finish with an alphanumeric rather than punctuation like "." s,\b(https?://[-\w/~:@.%#+$?=]+[\w/]),\1,g; s,\b(ftp://[-\w/~:@.%#+$?=]+),\1,g; s,([-_A-Za-z0-9.]+@[A-Za-z][-_A-Za-z0-9]*\.[-_A-Za-z0-9.]+),\1,g; # special case for things like 'perlre' as it's so useful but the # pod-generated pages aren't very parser friendly... if ($perlPattern && ! m/\1,g; } # Do this late so \& can be used to suppress conversion of URLs etc. s,\\&,,g; # replace tabs with spaces to next multiple of 8 if (m/\t/) { $tmp = $_; $tmp =~ s/<[^>]*>//g; $tmp =~ s/&[^;]*;/@/g; @tmp = split(/\t/, $tmp); $pos = 0; for ($i=0; $i<=$#tmp; ++$i) { $pos += length($tmp[$i]); $tab[$i] = 0; $tab[$i] = 8 - $pos%8 unless (@tabstops); foreach $ts (@tabstops) { if ($pos < $ts) { $tab[$i] = $ts-$pos; last; } } $pos += $tab[$i]; } while (m/\t/) { s,\t," " x (shift @tab),e; } } $textSinceBreak = $_ unless ($textSinceBreak); print OUT $_; } # Output a line consisting purely of HTML tags which shouldn't be regarded as # a troff output line. sub plainOutput { print OUT $_[0]; } # Output the original line for debugging sub outputOrigLine { print OUT "\n"; } # Use this to read the next input line (buffered to implement lookahead) sub getLine { $lookaheadPtr = 0; if (@lookahead) { $_ = shift @lookahead; return $_; } $_ = ; } # Look ahead to peek at the next input line sub _lookahead { # set lookaheadPtr to 0 to re-read the lines we've looked ahead at if ($lookaheadPtr>=0 && $lookaheadPtr <= $#lookahead) { return $lookahead[$lookaheadPtr++]; } $lookaheadPtr = -1; $ll = ; push(@lookahead, $ll); return $ll; } # Consume the last line that was returned by lookahead sub consume { --$lookaheadPtr; if ($lookaheadPtr>=0 && $lookaheadPtr <= $#lookahead) { $removed = $lookahead[$lookaheadPtr]; @lookahead = (@lookahead[0..$lookaheadPtr-1],@lookahead[$lookaheadPtr+1..$#lookahead]); } else { $removed = pop @lookahead; } chop $removed; plainOutput("\n"); } # Look ahead skipping comments and other common non-text tags sub lookahead { $ll = _lookahead(); while ($ll =~ m/^\.(\\"|PD|IX|ns)/) { $ll = _lookahead(); } return $ll; } # Process $_, expaning any macros into HTML and calling outputLine(). # If necessary, this method can read more lines of input from (.ig & .de) # The following state variables are used: # ... sub processLine { $doneLine = 1; # By default, this counts as a line for trap purposes s,^\.if t ,,; s,^\.el ,,; # conditions assumed to evaluate false, so else must be true... if ($troffTable) { processTable(); } elsif ($eqnMode == 2) { plainOutput("\n"); processEqns($_); } elsif (m/^\./) { processMacro(); } else { processPlainText(); } if ($doneLine) { # Called after processing (most) input lines to decrement trapLine. This is needed # to implement the .it 1 trap after one line for .TP, where the first line is outdented if ($trapLine > 0) { --$trapLine; if ($trapLine == 0) { &$trapAction; } } } } # Process plain text lines sub processPlainText { if ($_ eq "") { lineBreak(); plainOutput("

\n"); return; } s,(\\f[23BI])([A-Z].*?)(\\f.),$1.($contents{"\U$2"}?"$2":$2).$3,ge; if ($currentSection eq "SEE ALSO" && ! $cmdLineMode) { # Some people don't use BR or IR for see also refs s,(^|\s)([-.A-Za-z_0-9]+)\s?\(([0-9lL][0-9a-zA-Z]*)\),\1$2($3),g; } outputLine("$_\n"); } # Process macros and built-in directives sub processMacro { outputOrigLine(); # Place macro arguments (space delimited unless within ") into @p # Remove " from $_, place command in $c, remainder in $joined @p = grep($_ !~ m/^\s*$/, split(/("[^"]*"|\s+)/) ); grep(s/"//g, @p); $_ = join(" ", @p); $p[0] =~ s/^\.//; $c = $p[0]; $joined = join(" ", @p[1..$#p]); $joined2 = join(" ", @p[2..$#p]); $joined3 = join(" ", @p[3..$#p]); if ($macro{$c}) # Expand macro { # Get full macro text $macro = $macro{$c}; # Interpolate arguments $macro =~ s,\\\$(\d),$p[$1],ge; #print OUT "\n"; foreach $_ (split(/\n/, $macro)) { $_ .= "\n"; preProcessLine(); processLine(); } $doneLine = 0; return; } elsif ($renamedMacro{$c}) { $c = $renamedMacro{$c}; } if ($c eq "ds") # Define string { $vars{$p[1]} = $joined2; $doneLine = 0; } elsif ($c eq "nr") # Define number register { $number{$p[1]} = evalnum($joined2); $doneLine = 0; } elsif ($c eq "ti") # Temporary indent { plainOutput("   "); } elsif ($c eq "rm") { $macroName = $p[1]; if ($macro{$macroName}) { delete $macro{$macroName}; } else { $deletedMacro{$macroName} = 1; } } elsif ($c eq "rn") { $oldName = $p[1]; $newName = $p[2]; $macro = $macro{$oldName}; if ($macro) { if ($newName =~ $reservedMacros && ! $deletedMacro{$newName}) { plainOutput("\n"); } else { $macro{$newName} = $macro; delete $deletedMacro{$newName}; } delete $macro{$oldName}; } else { # Support renaming of reserved macros by mapping occurrences of new name # to old name after macro expansion so that built in definition is still # available, also mark the name as deleted to override reservedMacro checks. plainOutput("\n"); $renamedMacro{$newName} = $oldName; $deletedMacro{$oldName} = 1; } } elsif ($c eq "de" || $c eq "ig") # Define macro or ignore { $macroName = $p[1]; if ($c eq "ig") { $delim = ".$p[1]"; } else { $delim = ".$p[2]"; } $delim = ".." if ($delim eq "."); # plainOutput("\n"); $macro = ""; $_ = getLine(); preProcessLine(); while ($_ ne $delim) { postProcessLine(); outputOrigLine(); $macro .= "$_\n"; $_ = getLine(); last if ($_ eq ""); preProcessLine(); } outputOrigLine(); # plainOutput("\n"); if ($c eq "de") { if ($macroName =~ $reservedMacros && ! $deletedMacro{$macroName}) { plainOutput("\n"); } else { $macro{$macroName} = $macro; delete $deletedMacro{$macroName}; } } } elsif ($c eq "so") # Source { plainOutput("

[Include document $p[1]]

\n"); } elsif ($c eq "TH" || $c eq "Dt") # Man page title { endParagraph(); $sectionNumber = $p[2]; $sectionName = $sectionName{"\L$sectionNumber"}; $sectionName = "Manual Reference Pages" unless ($sectionName); $pageName = "$p[1] ($sectionNumber)"; outputPageHead(); if ($c eq "TH") { $right = $p[3]; $left = $p[4]; $left = $osver unless ($left); $macroPackage = "using man macros"; } else { $macroPackage = "using doc macros"; } } elsif ($c eq "Nd") { outputLine("- $joined\n"); } elsif ($c eq "SH" || $c eq "SS" || $c eq "Sh" || $c eq "Ss") # Section/subsection { lineBreak(); endNoFill(); endParagraph(); $id = $contents{"\U$joined"}; $currentSection = $joined; if ($c eq "SH" || $c eq "Sh") { endBlockquote(); if ($firstSection++==1) # after first 'Name' section { outputContents(); } outputLine( "\n\n

$joined

\n\n\n" ); blockquote(); } elsif ($joined =~ m/\\f/) { $joined =~ s/\\f.//g; $id = $contents{"\U$joined"}; outputLine( "\n

$joined

\n" ); } else { endBlockquote(); outputLine( "\n\n

    $joined

\n
\n" ); blockquote(); } lineBreak(); } elsif ($c eq "TX" || $c eq "TZ") # Document reference { $title = $title{$p[1]}; $title = "Document [$p[1]]" unless ($title); outputLine( "\\fI$title\\fP$joined2\n" ); } elsif ($c eq "PD") # Line spacing { $noSpace = ($p[1] eq "0"); $doneLine = 0; } elsif ($c eq "TS") # Table start { unless ($macroPackage =~ /tbl/) { if ($macroPackage =~ /eqn/) { $macroPackage =~ s/eqn/eqn & tbl/; } else { $macroPackage .= " with tbl support"; } } resetStyles(); endNoFill(); $troffTable = 1; $troffSeparator = "\t"; plainOutput( "

\n" ); } elsif ($c eq "EQ") # Eqn start { unless ($macroPackage =~ /eqn/) { if ($macroPackage =~ /tbl/) { $macroPackage =~ s/tbl/tbl & eqn/; } else { $macroPackage .= " with eqn support"; } } $eqnMode = 2; } elsif ($c eq "ps") # Point size { plainOutput(&sizeChange($p[1])); } elsif ($c eq "ft") # Font change { plainOutput(&fontChange($p[1])); } elsif ($c eq "I" || $c eq "B") # Single word font change { $id = $contents{"\U$joined"}; if ($id && $joined =~ m/^[A-Z]/) { $joined = "$joined"; } outputLine( "\\f$c$joined\\fP " ); plainOutput("\n") if ($noFill); } elsif ($c eq "SM") # Single word smaller { outputLine("\\s-1$joined\\s0 "); $doneLine = 0 unless ($joined); } elsif ($c eq "SB") # Single word bold and small { outputLine("\\fB\\s-1$joined\\s0\\fP "); } elsif (m/^\.[BI]R (\S+)\s?\(\s?([0-9lL][0-9a-zA-Z]*)\s?\)(.*)$/) { # Special form, .BR is generally used for references to other pages # Annoyingly, some people have more than one per line... # Also, some people use .IR ... for ($i=1; $i<=$#p; $i+=2) { $pair = $p[$i]." ".$p[$i+1]; if ($p[$i+1] eq "(") { $pair .= $p[$i+2].$p[$i+3]; $i += 2; } if ($pair =~ m/^(\S+)\s?\(\s?([0-9lL][0-9a-zA-Z]*)\s?\)(.*)$/) { if ($cmdLineMode) { outputLine( "\\fB$1\\fR($2)$3\n" ); } else { outputLine( "$1($2)$3\n" ); } } else { outputLine( "$pair\n" ); } } } elsif ($c eq "BR" || $c eq "BI" || $c eq "IB" || $c eq "IR" || $c eq "RI" || $c eq "RB") { $f1 = (substr($c ,0,1)); $f2 = (substr($c,1,1)); # Check if first param happens to be a section name $id = $contents{"\U$p[1]"}; if ($id && $p[1] =~ m/^[A-Z]/) { $p[1] = "$p[1]"; } for ($i=1; $i<=$#p; ++$i) { $f = ($i%2 == 1) ? $f1 : $f2; outputLine("\\f$f$p[$i]"); } outputLine("\\fP "); plainOutput("\n") if ($noFill); } elsif ($c eq "nf" || $c eq "Bd") # No fill { startNoFill(); } elsif ($c eq "fi" || $c eq "Ed") # Fill { endNoFill(); } elsif ($c eq "HP") { $indent = evalnum($p[1]); if ($trapOnBreak) { plainOutput("
\n"); } else { # Outdent first line, ie. until next break $trapOnBreak = 1; $trapAction = *trapHP; newParagraph($indent); plainOutput( "   " ); } # End an existing HP/TP/IP/RS row sub endRow { if ($indent[$indentLevel] > 0) { lineBreak(); plainOutput( "\n" ); } } # Called when we output a line break tag. Only needs to be called once if # calling plainOutput, but should call before and after if using outputLine. sub lineBreak { $needBreak = 0; $textSinceBreak = 0; } # Called to reset all indents and pending paragraphs (eg. at the start of # a new top level section). sub endParagraph { ++$indentLevel; while ($indentLevel > 0) { --$indentLevel; if ($indent[$indentLevel] > 0) { endRow(); setIndent(0); } } } # Interpolate a number register (possibly autoincrementing) sub numreg { return 0 + $number{$_[0]}; } # Evaluate a numeric expression sub evalnum { $n = $_[0]; return "" if ($n eq ""); if ($n =~ m/i$/) # inches { $n =~ s/i//; $n *= 10; } return 0+$n; } sub setIndent { $tsb = $textSinceBreak; $indent = evalnum($_[0]); if ($indent==0 && $_[0] !~ m/^0/) { $indent = 6; } plainOutput("\n") if ($debug); if ($indent[$indentLevel] != $indent) { lineBreak(); if ($indent[$indentLevel] > 0) { plainOutput("") unless ($noSpace); plainOutput("
\n" ); $colState = 2; } } elsif ($c eq "IP") { $trapOnBreak = 0; $tag = $p[1]; $indent = evalnum($p[2]); newParagraph($indent); outputLine("\n$tag\n\n"); $colState = 1; lineBreak(); } elsif ($c eq "TP") { $trapOnBreak = 0; $trapLine = 1; # Next line is tag, then next column $doneLine = 0; # (But don't count this line) $trapAction = *trapTP; $indent = evalnum($p[1]); $tag = lookahead(); chop $tag; $i = ($indent ? $indent : $prevailingIndent) ; $w = width($tag); if ($w > $i) { plainOutput("\n") if ($debug); newParagraph($indent); $trapAction = *trapHP; plainOutput( "\n" ); $colState = 2; } else { newParagraph($indent); plainOutput( "\n" ); $colState = 0; } $body = lookahead(); $lookaheadPtr = 0; if ($body =~ m/^\.[HILP]?P/) { chop $body; plainOutput("\n"); $trapLine = 0; } } elsif ($c eq "LP" || $c eq "PP" || $c eq "P" || $c eq "Pp") # Paragraph { $trapOnBreak = 0; $prevailingIndent = 6; if ($indent[$indentLevel] > 0 && $docListStyle eq "") { $line = lookahead(); if ($line =~ m/^\.(TP|IP|HP)/) { plainOutput("\n"); } elsif ($line =~ m/^\.RS/) { plainOutput("

\n"); } else { endRow(); $foundTag = ""; $lookaheadPtr = 0; do { $line = lookahead(); if ($line =~ m/^\.(TP|HP|IP|RS)( \d+)?/) { $indent = $2; $indent = $prevailingIndent unless ($2); if ($indent == $indent[$indentLevel]) { $foundTag = $1; } $line = ""; } } while ($line ne "" && $line !~ m/^\.(RE|SH|SS|PD)/); $lookaheadPtr = 0; if ($foundTag) { plainOutput("\n"); plainOutput("

\n"); $colState = 2; } else { plainOutput("\n"); setIndent(0); } } } else { plainOutput("

\n"); } lineBreak(); } elsif ($c eq "br") # Break { if ($trapOnBreak) { # Should this apply to all macros that cause a break? $trapOnBreak = 0; &$trapAction(); } $needBreak = 1 if ($textSinceBreak); } elsif ($c eq "sp") # Space { lineBreak(); plainOutput("

\n"); } elsif ($c eq "RS") # Block indent start { if ($indentLevel==0 && $indent[0]==0) { blockquote(); } else { $indent = $p[1]; $indent = $prevailingIndent unless ($indent); if ($indent > $indent[$indentLevel] && !$extraIndent) { $extraIndent = 1; ++$indentLevel; $indent[$indentLevel] = 0; setIndent($indent-$indent[$indentLevel-1]); plainOutput("

\n"); $colState = 1; } elsif ($indent < $indent[$indentLevel] || $colState==2) { endRow(); setIndent($indent); plainOutput("
\n"); $colState = 1; } ++$indentLevel; $indent[$indentLevel] = 0; } $prevailingIndent = 6; } elsif ($c eq "RE") # Block indent end { if ($extraIndent) { endRow(); setIndent(0); --$indentLevel; $extraIndent = 0; } if ($indentLevel==0) { endParagraph(); if ($blockquote>0) { plainOutput("\n"); --$blockquote; } } else { endRow(); setIndent(0); --$indentLevel; } $prevailingIndent = $indent[$indentLevel]; $prevailingIndent = 6 unless($prevailingIndent); } elsif ($c eq "DT") # default tabs { @tabstops = (); } elsif ($c eq "ta") # Tab stops { @tabstops = (); for ($i=0; $i<$#p; ++$i) { $ts = $p[$i+1]; $tb = 0; if ($ts =~ m/^\+/) { $tb = $tabstops[$i-1]; $ts =~ s/^\+//; } $ts = evalnum($ts); $tabstops[$i] = $tb + $ts; } plainOutput("\n") if ($debug); } elsif ($c eq "It") # List item (mdoc) { lineBreak(); if ($docListStyle eq "-tag") { endRow() unless($multilineIt); if ($tagWidth) { setIndent($tagWidth); } else { setIndent(6); $width = ""; # let table take care of own width } if ($p[1] eq "Xo") { plainOutput("
"); } else { $tag = &mdocStyle(@p[1..$#p]); $body = lookahead(); if ($body =~ m/^\.It/) { $multilineItNext = 1; } else { $multilineItNext = 0; } if ($multilineIt) { outputLine("
\n$tag\n"); } elsif ($multilineItNext || $tagWidth>0 && width($tag)>$tagWidth) { outputLine("
$tag\n"); $colState = 2; } else { outputLine("
$tag\n"); $colState = 1; } if ($multilineItNext) { $multilineIt = 1; } else { $multilineIt = 0; if ($colState==2) { plainOutput("
 \n"); } else { plainOutput("\n"); } } } } else { plainOutput("
  • "); } lineBreak(); } elsif ($c eq "Xc") { if ($docListStyle eq "-tag") { plainOutput("
  •  \n"); } } elsif ($c eq "Bl") # Begin list (mdoc) { push @docListStyles, $docListStyle; if ($p[1] eq "-enum") { plainOutput("
      \n"); $docListStyle = $p[1]; } elsif($p[1] eq "-bullet") { plainOutput("
        \n"); $docListStyle = $p[1]; } else { $docListStyle = "-tag"; if ($p[2] eq "-width") { $tagWidth = width($p[3]); if ($tagWidth < 6) { $tagWidth = 6; } } else { $tagWidth = 0; } $multilineIt = 0; } } elsif ($c eq "El") # End list { if ($docListStyle eq "-tag") { endRow(); setIndent(0); } elsif ($docListStyle eq "-bullet") { plainOutput("
      \n"); } else { plainOutput("
    \n"); } $docListStyle = pop @docListStyles; } elsif ($c eq "Os") { $right = $joined; } elsif ($c eq "Dd") { $left = $joined; } elsif ($c eq "Sx") # See section { $id = $contents{"\U$joined"}; if ($id && $joined =~ m/^[A-Z]/) { outputLine("".&mdocStyle(@p[1..$#p])."\n"); } else { my $x = &mdocStyle(@p[1..$#p]); $x =~ s/^ //; outputLine($x."\n"); } } elsif (&mdocCallable($c)) { my $x = &mdocStyle(@p); $x =~ s/^ //; outputLine($x."\n"); } elsif ($c eq "Bx") { outputLine("BSD $joined\n"); } elsif ($c eq "Ux") { outputLine("Unix $joined\n"); } elsif ($c eq "At") { outputLine("AT&T $joined\n"); } elsif ($c =~ m/[A-Z][a-z]/) # Unsupported doc directive { outputLine("
    .$c $joined\n"); } elsif ($c eq "") # Empty line (eg. troff comment) { $doneLine = 0; } else # Unsupported directive { # Unknown macros are ignored, and don't count as a line as far as trapLine goes $doneLine = 0; plainOutput("\n"); } } sub trapTP { $lookaheadPtr = 0; $body = lookahead(); if ($body =~ m/^\.TP/) { consume(); $trapLine = 1; # restore TP trap $doneLine = 0; # don't count this line plainOutput("
    \n"); } else { plainOutput("
    \n"); $colState = 1; } lineBreak(); } sub trapHP { $lookaheadPtr = 0; $body = lookahead(); if ($body =~ m/^\.([TH]P)/) { consume(); # Restore appropriate type of trap if ($1 eq "TP") { $trapLine = 1; $doneLine = 0; # don't count this line } else { $trapOnBreak = 1; } plainOutput("
    \n"); } else { plainOutput("
    \n"); $colState = 1; } lineBreak(); } sub newParagraph { $indent = $_[0]; endRow(); startRow($indent); } sub startRow { $indent = $_[0]; $indent = $prevailingIndent unless ($indent); $prevailingIndent = $indent; setIndent($indent); plainOutput( "
    "); } if ($indent > 0) { endNoFill(); $border = ""; $border = " border=1" if ($debug>2); #plainOutput("

    ") unless ($indent[$indentLevel] > 0); plainOutput("0); if ($noSpace) { plainOutput(" cellpadding=0 cellspacing=0>\n"); } else { plainOutput(" cellpadding=3>".($tsb ? "\n\n" : "\n") ); } #$width = " width=".($indent*5); # causes text to be chopped if too big $percent = $indent; if ($indentLevel > 0) { $percent = $indent * 100 / (100-$indentLevel[0]); } $width = " width=$percent%"; } $indent[$indentLevel] = $indent; } } # Process mdoc style macros recursively, as one of the macro arguments # may itself be the name of another macro to invoke. sub mdocStyle { return "" unless @_; my ($tag, @param) = @_; my ($rest, $term); # Don't format trailing punctuation if ($param[$#param] =~ m/^[.,;:]$/) { $term = pop @param; } if ($param[$#param] =~ m/^[)\]]$/) { $term = (pop @param).$term; } if ($param[0] =~ m,\\\\,) { print STDERR "$tag: ",join(",", @param),"\n"; } $rest = &mdocStyle(@param); if ($tag eq "Op") { $rest =~ s/ //; # remove first space return " \\fP[$rest]$term"; } elsif ($tag eq "Xr") # cross reference { my $p = shift @param; my $url = $p; if (@param==1) { $url .= ".".$param[0]; $rest = "(".$param[0].")"; } else { $rest = &mdocStyle(@param); } if ($cmdLineMode) { return " ".$p."".$rest.$term; } else { return " ".$p."".$rest.$term; } } elsif ($tag eq "Fl") { my ($sofar); while (@param) { $f = shift @param; if ($f eq "Ns") # no space { chop $sofar; } elsif (&mdocCallable($f)) { unshift @param, $f; return $sofar.&mdocStyle(@param).$term; } else { $sofar .= "-$f " } } return $sofar.$term; } elsif ($tag eq "Pa" || $tag eq "Er" || $tag eq "Fn" || $tag eq "Dv") { return "\\fC$rest\\fP$term"; } elsif ($tag eq "Ad" || $tag eq "Ar" || $tag eq "Em" || $tag eq "Fa" || $tag eq "St" || $tag eq "Ft" || $tag eq "Va" || $tag eq "Ev" || $tag eq "Tn" || $tag eq "%T") { return "\\fI$rest\\fP$term"; } elsif ($tag eq "Nm") { $defaultNm = $param[0] unless ($defaultNm); $rest = $defaultNm unless ($param[0]); return "\\fB$rest\\fP$term"; } elsif ($tag eq "Ic" || $tag eq "Cm" || $tag eq "Sy") { return "\\fB$rest\\fP$term"; } elsif ($tag eq "Ta") # Tab { # Tabs are used inconsistently so this is the best we can do. Columns won't line up. Tough. return "      $rest$term"; } elsif ($tag eq "Ql") { $rest =~ s/ //; return "`$rest'$term"; } elsif ($tag eq "Dl") { return "

        $rest$term

    \n"; } elsif ($tag =~ m/^[ABDEOPQS][qoc]$/) { $lq = ""; $rq = ""; if ($tag =~ m/^A/) { $lq = "<"; $rq = ">"; } elsif ($tag =~ m/^B/) { $lq = "["; $rq = "]"; } elsif ($tag =~ m/^D/) { $lq = "\""; $rq = "\""; } elsif ($tag =~ m/^P/) { $lq = "("; $rq = ")"; } elsif ($tag =~ m/^Q/) { $lq = "\""; $rq = "\""; } elsif ($tag =~ m/^S/) { $lq = "\\'"; $rq = "\\'"; } elsif ($tag =~ m/^O/) { $lq = "["; $rq = "]"; } if ($tag =~ m/^.o/) { $rq = ""; } if ($tag =~ m/^.c/) { $lq = ""; } $rest =~ s/ //; return $lq.$rest.$rq.$term ; } elsif (&mdocCallable($tag)) # but not in list above... { return $rest.$term; } elsif ($tag =~ m/^[.,;:()\[\]]$/) # punctuation { return $tag.$rest.$term; } elsif ($tag eq "Ns") { return $rest.$term; } else { return " ".$tag.$rest.$term; } } # Determine if a macro is mdoc parseable/callable sub mdocCallable { 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])$/); } # Estimate the output width of a string sub width { local($word) = $_[0]; $word =~ s,<[/A-Z][^>]*>,,g; # remove any html tags $word =~ s/^\.\S+\s//; $word =~ s/\\..//g; $x = length($word); $word =~ s/[ ()|.,!;:"']//g; # width of punctuation is about half a character return ($x + length($word)) / 2; } # Process a tbl table (between TS/TE tags) sub processTable { if ($troffTable == "1") { @troffRowDefs = (); @tableRows = (); $hadUnderscore = 0; while(1) { outputOrigLine(); if (m/;\s*$/) { $troffSeparator = quotemeta($1) if (m/tab\s*\((.)\)/); } else { s/\.\s*$//; s/\t/ /g; s/^[^lrcan^t]*//; # remove any 'modifiers' coming before tag # delimit on tags excluding s (viewed as modifier of previous column) s/([lrcan^t])/\t$1/g; s/^\t//; push @troffRowDefs, $_; last if ($origLine =~ m/\.\s*$/); } $_ = getLine(); preProcessLine(); } $troffTable = 2; return; } s/$troffSeparator/\t/g; if ($_ eq ".TE") { endTblRow(); flushTable(); $troffTable = 0; plainOutput("

    \n"); } elsif ($_ eq ".T&") { endTblRow(); flushTable(); $troffTable = 1; } elsif (m/[_=]/ && m/^[_=\t]*$/ && $troffCol==0) { if (m/^[_=]$/) { flushTable(); plainOutput("\n"); $hadUnderscore = 1; } elsif ($troffCol==0 && @troffRowDefs) { # Don't output a row, but this counts as a row as far as row defs go $rowDef = shift @troffRowDefs; @troffColDefs = split(/\t/, $rowDef); } } elsif (m/^\.sp/ && $troffCol==0 && !$hadUnderscore) { flushTable(); plainOutput("\n"); } elsif ($_ eq ".br" && $troffMultiline) { $rowref->[$troffCol] .= "
    \n"; } elsif ($_ !~ m/^\./) { $rowref = $tableRows[$#tableRows]; # reference to current row (last row in array) if ($troffCol==0 && @troffRowDefs) { $rowDef = shift @troffRowDefs; if ($rowDef =~ m/^[_=]/) { $xxx = $_; flushTable(); plainOutput("\n"); $hadUnderscore = 1; $_ = $xxx; $rowDef = shift @troffRowDefs; } @troffColDefs = split(/\t/, $rowDef); } if ($troffCol == 0 && !$troffMultiline) { $rowref = []; push(@tableRows, $rowref); #plainOutput(""); } #{ if (m/T}/) { $troffMultiline = 0; } if ($troffMultiline) { $rowref->[$troffCol] .= "$_\n"; return; } @columns = split(/\t/, $_); plainOutput("\n") if ($debug); while ($troffCol <= $#troffColDefs && @columns > 0) { $def = $troffColDefs[$troffCol]; $col = shift @columns; $col =~ s/\s*$//; $align = ""; $col = "\\^" if ($col eq "" && $def =~ m/\^/); $col = " " if ($col eq ""); $style1 = ""; $style2 = ""; if ($col ne "\\^") { if ($def =~ m/[bB]/ || $def =~ m/f3/) { $style1 = "\\fB"; $style2 = "\\fP"; } if ($def =~ m/I/ || $def =~ m/f2/) { $style1 = "\\fI"; $style2 = "\\fP"; } } if ($def =~ m/c/) { $align = " align=center"; } if ($def =~ m/[rn]/) { $align = " align=right"; } $span = $def; $span =~ s/[^s]//g; if ($span) { $align.= " colspan=".(length($span)+1); } #{ if ($col =~ m/T}/) { $rowref->[$troffCol] .= "$style2"; ++$troffCol; } elsif ($col =~ m/T{/) #} { $col =~ s/T{//; #} $rowref->[$troffCol] = "$style1$col"; $troffMultiline = 1; } else { $rowref->[$troffCol] = "$style1$col$style2"; ++$troffCol; } } endTblRow() unless ($troffMultiline); } } sub endTblRow { return if ($troffCol == 0); while ($troffCol <= $#troffColDefs) { $rowref->[$troffCol] = " "; #print OUT " "; ++$troffCol; } $troffCol = 0; #print OUT "\n" } sub flushTable { plainOutput("\n") if ($debug); # Treat rows with first cell blank or with more than one vertically # spanned row as a continuation of the previous line. # Note this is frequently a useful heuristic but isn't foolproof. for($r=0; $r<$#tableRows; ++$r) { $vspans = 0; for ($c=0; $c<=$#{$tableRows[$r+1]}; ++$c) {++$vspans if ($tableRows[$r+1][$c] =~ m,\\\^,);} if ((($vspans>1) || ($tableRows[$r+1][0] =~ m, ,)) && $#{$tableRows[$r]} == $#{$tableRows[$r+1]} && 0) { if ($debug) { plainOutput("\n"); plainOutput("\n"); plainOutput("\n"); } for ($c=0; $c<=$#{$tableRows[$r]}; ++$c) { $tableRows[$r][$c] .= $tableRows[$r+1][$c]; $tableRows[$r][$c] =~ s,\\\^,,g; # merging is stronger than spanning! $tableRows[$r][$c] =~ s,,
    ,; } @tableRows = (@tableRows[0..$r], @tableRows[$r+2 .. $#tableRows]); --$r; # process again } } # Turn \^ vertical span requests into rowspan tags for($r=0; $r<$#tableRows; ++$r) { for ($c=0; $c<=$#{$tableRows[$r]}; ++$c) { $r2 = $r+1; while ( $r2<=$#tableRows && ($tableRows[$r2][$c] =~ m,\\\^,) ) { ++$r2; } $rs = $r2-$r; if ($rs > 1) { plainOutput("\n") if ($debug); $tableRows[$r][$c] =~ s/=0; --$c) { if ($tableRows[$r][$c] =~ m/[$c]." -->\n") if ($debug); @$rowref = (@{$rowref}[0..$c-1], @{$rowref}[$c+1..$#$rowref]); } } } } # Finally, output the cells that are left for($r=0; $r<=$#tableRows; ++$r) { plainOutput("\n"); for ($c=0; $c <= $#{$tableRows[$r]}; ++$c) { outputLine($tableRows[$r][$c]); } plainOutput("\n"); } @tableRows = (); $troffCol = 0; plainOutput("\n") if ($debug); } # Use these for all font changes, including .ft, .ps, .B, .BI, .SM etc. # Need to add a mechanism to stack up these changes so tags match: ... etc. sub pushStyle { $result = ""; $type = $_[0]; $tag = $_[1]; print OUT "\n" if ($debug>1); r
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <html>
    <head>
     <meta http-equiv="Content-Type" content="text/html;charset=iso-8859-1">
     <title>TRIO</title>
     <link href="trio.css" rel="stylesheet" type="text/css">
    </head>
    <body>
    <!-- Generated by Doxygen 1.2.12 -->
    <center>
    <a class="qindex" href="index.html">Main Page</a> &nbsp; <a class="qindex" href="modules.html">Modules</a> &nbsp; </center>
    <hr><h1>Formatted Printing Functions.</h1>Variations of formatted printing functions. 
    <a href="#_details">More...</a><table border=0 cellpadding=0 cellspacing=0>
    <tr><td colspan=2><br><h2>Functions</h2></td></tr>
    <tr><td nowrap align=right valign=top>int&nbsp;</td><td valign=bottom><a class="el" href="group___printf.html#a0">trio_printf</a> (const char *format,...)</td></tr>
    <tr><td>&nbsp;</td><td><font size=-1><em>Print to standard output stream.</em> <a href="#a0">More...</a><em></em></font><br><br></td></tr>
    <tr><td nowrap align=right valign=top>int&nbsp;</td><td valign=bottom><a class="el" href="group___printf.html#a1">trio_vprintf</a> (const char *format, va_list args)</td></tr>
    <tr><td>&nbsp;</td><td><font size=-1><em>Print to standard output stream.</em> <a href="#a1">More...</a><em></em></font><br><br></td></tr>
    <tr><td nowrap align=right valign=top>int&nbsp;</td><td valign=bottom><a class="el" href="group___printf.html#a2">trio_printfv</a> (const char *format, trio_pointer_t *args)</td></tr>
    <tr><td>&nbsp;</td><td><font size=-1><em>Print to standard output stream.</em> <a href="#a2">More...</a><em></em></font><br><br></td></tr>
    <tr><td nowrap align=right valign=top>int&nbsp;</td><td valign=b