#! perl
#
# Generate a man page from sections of a Texinfo manual.
#
# Copyright 2004, 2006
#                The Free Software Foundation,
#                Derek R. Price,
#                & Ximbiot <http://ximbiot.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.



# Need Perl 5.005 or greater for re 'eval'.
require 5.005;

# The usual.
use strict;
use IO::File;



###
### GLOBALS
###
my $texi_num = 0; # Keep track of how many texinfo files have been encountered.
my @parent;       # This needs to be global to be used inside of a regex later.
my $nk;           # Ditto.
my $ret;          # The RE match Type, used in debug prints.
my $debug = 0;    # Debug mode?



###
### FUNCTIONS
###
sub debug_print
{
	print @_ if $debug;
}



sub keyword_mode
{
	my ($keyword, $file) = @_;

	return "\\fR"
		if $keyword =~ /^(|r|t)$/;
	return "\\fB"
		if $keyword =~ /^(strong|sc|code|file|samp)$/;
	return "\\fI"
		if $keyword =~ /^(emph|var|dfn)$/;
	die "no handler for keyword \`$keyword', found at line $. of file \`$file'\n";
}



# Return replacement for \@$keyword{$content}.
sub do_keyword
{
	my ($file, $parent, $keyword, $content) = @_;

	return "`$content\\(aq in the CVS manual"
		if $keyword eq "ref";
	return "see node `$content\\(aq in the CVS manual"
		if $keyword =~ /^p?xref$/;
	return "\\fP\\fP$content"
		if $keyword =~ /^splitrcskeyword$/;

	my $endmode = keyword_mode $parent;
	my $startmode = keyword_mode $keyword, $file;

	return "$startmode$content$endmode";
}



###
### MAIN
###
for my $file (@ARGV)
{
	my $fh = new IO::File "< $file"
		or die "Failed to open file \`$file': $!";

	if ($file !~ /\.(texinfo|texi|txi)$/)
	{
		print stderr "Passing \`$file' through unprocessed.\n";
		# Just cat any file that doesn't look like a Texinfo source.
		while (my $line = $fh->getline)
		{
			print $line;
		}
		next;
	}

	print stderr "Processing \`$file'.\n";
	$texi_num++;
	my $gotone = 0;
	my $inblank = 0;
	my $indent = 0;
	my $inexample = 0;
	my $inmenu = 0;
	my $intable = 0;
	my $last_header = "";
	my @table_headers;
	my @table_footers;
	my $table_header = "";
	my $table_footer = "";
	my $last;
	while ($_ = $fh->getline)
	{
		if (!$gotone && /^\@c ----- START MAN $texi_num -----$/)
		{
			$gotone = 1;
			next;
		}

		# Skip ahead until our man section.
		next unless $gotone;

		# If we find the end tag we are done.
		last if /^\@c ----- END MAN $texi_num -----$/;

		# Need to do this everywhere.  i.e., before we print example
		# lines, since literal back slashes can appear there too.
		s/\\/\\\\/g;
		s/^\./\\&./;
		s/([\s])\./$1\\&./;
		s/'/\\(aq/g;
		s/`/\\`/g;
		s/(?<!-)---(?!-)/\\(em/g;
		s/\@bullet({}|\b)/\\(bu/g;
		s/\@dots({}|\b)/\\&.../g;

		# Examples should be indented and otherwise untouched
		if (/^\@example$/)
		{
			$indent += 2;
			print qq{.SP\n.PD 0\n};
			$inexample = 1;
			next;
		}
		if ($inexample)
		{
			if (/^\@end example$/)
			{
				$indent -= 2;
				print qq{\n.PD\n.IP "" $indent\n};
				$inexample = 0;
				next;
			}
			if (/^[ 	]*$/)
			{
				print ".SP\n";
				next;
			}

			# Preserve the newline.
			$_ = qq{.IP "" $indent\n} . $_;
		}

		# Compress blank lines into a single line.  This and its
		# corresponding skip purposely bracket the @menu and comment
		# removal so that blanks on either side of a menu are
		# compressed after the menu is removed.
		if (/^[ 	]*$/)
		{
			$inblank = 1;
			next;
		}

		# Not used
		if (/^\@(ignore|menu)$/)
		{
			$inmenu++;
			next;
		}
		# Delete menu contents.
		if ($inmenu)
		{
			next unless /^\@end (ignore|menu)$/;
			$inmenu--;
			next;
		}

		# Remove comments
		next if /^\@c(omment)?\b/;

		# Ignore includes.
		next if /^\@include\b/;

		# It's okay to ignore this keyword - we're not using any
		# first-line indent commands at all.
		next if s/^\@noindent\s*$//;

		# @need is only significant in printed manuals.
		next if s/^\@need\s+.*$//;

		# If we didn't hit the previous check and $inblank is set, then
		# we just finished with some number of blanks.  Print the man
		# page blank symbol before continuing processing of this line.
		if ($inblank)
		{
			print ".SP\n";
			$inblank = 0;
		}

		# Chapter headers.
		$last_header = $1 if s/^\@node\s+(.*)$/.SH "$1"/;
		if (/^\@appendix\w*\s+(.*)$/)
		{
			my $content = $1;
			$content =~ s/^$last_header(\\\(em|\s+)?//;
			next if $content =~ /^\s*$/;
			s/^\@appendix\w*\s+.*$/.SS "$content"/;
		}

		# Tables are similar to examples, except we need to handle the
		# keywords.
		if (/^\@(itemize|table)(\s+(.*))?$/)
		{
			$indent += 2;
			push @table_headers, $table_header;
			push @table_footers, $table_footer;
			my $content = $3;
			if (/^\@itemize/)
			{
				my $bullet = $content;
				$table_header = qq{.IP "$bullet" $indent\n};
				$table_footer = "";
			}
			else
			{
				my $hi = $indent - 2;
				$table_header = qq{.IP "" $hi\n};
				$table_footer = qq{\n.IP "" $indent};
				if ($content)
				{
					$table_header .= "$content\{";
					$table_footer = "\}$table_footer";
				}
			}
			$intable++;
			next;
		}

		if ($intable)
		{
			if (/^\@end (itemize|table)$/)
			{
				$table_header = pop @table_headers;
				$table_footer = pop @table_footers;
				$indent -= 2;
				$intable--;
				next;
			}
			s/^\@itemx?(\s+(.*))?$/$table_header$2$table_footer/;
			# Fall through so the rest of the table lines are
			# processed normally.
		}

		# Index entries.
		s/^\@cindex\s+(.*)$/.IX "$1"/;

		$_ = "$last$_" if $last;
		undef $last;

		# Trap keywords
		$nk = qr/
				\@(\w+)\{
				(?{ debug_print "$ret MATCHED $&\nPUSHING $1\n";
				    push @parent, $1; })      # Keep track of the last keyword
				                              # keyword we encountered.
				((?>
					[^{}]|(?<=\@)[{}]     # Non-braces...
						|             #    ...or...
					(??{ $nk })           # ...nested keywords...
				)*)                           # ...without backtracking.
				\}
				(?{ debug_print "$ret MATCHED $&\nPOPPING ",
				                pop (@parent), "\n"; })            # Lose track of the current keyword.
			/x;

		$ret = "m//";
		if (/\@\w+\{(?:[^{}]|(?<=\@)[{}]|(??{ $nk }))*$/)
		{
			# If there is an opening keyword on this line without a
			# close bracket, we need to find the close bracket
			# before processing the line.  Set $last to append the
			# next line in the next pass.
			$last = $_;
			next;
		}

		# Okay, the following works somewhat counter-intuitively.  $nk
		# processes the whole line, so @parent gets loaded properly,
		# then, since no closing brackets have been found for the
		# outermost matches, the innermost matches match and get
		# replaced first.
		#
		# For example:
		#
		# Processing the line:
		#
		#   yadda yadda @code{yadda @var{foo} yadda @var{bar} yadda}
		#
		# Happens something like this:
		#
		# 1. Ignores "yadda yadda "
		# 2. Sees "@code{" and pushes "code" onto @parent.
		# 3. Ignores "yadda " (backtracks and ignores "yadda yadda
		#                      @code{yadda "?)
		# 4. Sees "@var{" and pushes "var" onto @parent.
		# 5. Sees "foo}", pops "var", and realizes that "@var{foo}"
		#    matches the overall pattern ($nk).
		# 6. Replaces "@var{foo}" with the result of:
		#
		#      do_keyword $file, $parent[$#parent], $1, $2;
		#
		#    which would be "\Ifoo\B", in this case, because "var"
		#    signals a request for italics, or "\I", and "code" is
		#    still on the stack, which means the previous style was
		#    bold, or "\B".
		#
		# Then the while loop restarts and a similar series of events
		# replaces "@var{bar}" with "\Ibar\B".
		#
		# Then the while loop restarts and a similar series of events
		# replaces "@code{yadda \Ifoo\B yadda \Ibar\B yadda}" with
		# "\Byadda \Ifoo\B yadda \Ibar\B yadda\R".
		#
		$ret = "s///";
		@parent = ("");
		while (s/$nk/do_keyword $file, $parent[$#parent], $1, $2/e)
		{
			# Do nothing except reset our last-replacement
			# tracker - the replacement regex above is handling
			# everything else.
			debug_print "FINAL MATCH $&\n";
			@parent = ("");
		}

		# Finally, unprotect texinfo special characters.
		s/\@://g;
		s/\@([{}])/$1/g;

		# Verify we haven't left commands unprocessed.
		die "Unprocessed command at line $. of file \`$file': "
		    . ($1 ? "$1\n" : "<EOL>\n")
			if /^(?>(?:[^\@]|\@\@)*)\@(\w+|.|$)/;

		# Unprotect @@.
		s/\@\@/\@/g;

		# And print whatever's left.
		print $_;
	}
}
