#!/usr/bin/perl

my %db2x_config =
(
    'docbook2X-version' => q<0.8.8>,
    
    # Used by db2x_manxml and db2x_texixml only
    'utf8trans-program' => q</usr/local/bin/utf8trans>,
    'utf8trans-map-roff' => q</usr/local/share/docbook2X/charmaps/roff.charmap>,
    'utf8trans-map-texi' => q</usr/local/share/docbook2X/charmaps/texi.charmap>,
    'iconv-program' => q</usr/local/bin/iconv>,
    'makeinfo-program' => q</usr/bin/makeinfo>,
    
    # Used by docbook2X.pl only
    'db2x_xsltproc-program' => q</usr/local/bin/db2x_xsltproc>,
    'db2x_manxml-program' => q</usr/local/bin/db2x_manxml>,
    'db2x_texixml-program' => q</usr/local/bin/db2x_texixml>,
    
    # Used by db2x_xsltproc.pl only
    'stylesheets-catalog' => q</usr/local/share/docbook2X/xslt/catalog.xml>,
    'xsltproc-program' => q</usr/local/bin/xsltproc>,
    
    'sgml2xml-isoent-program' => q</usr/local/bin/sgml2xml-isoent>,

    'java-program' => q<>,
    'resolver-jars' => q<>,
    'saxon-jars' => q<>,
    'xalan-jars' => q<>,
    
    'xslt-processor' => q<libxslt>,
);

# vim:sw=4 sta showmatch

use strict;
use vars qw($VERSION);
$VERSION = '0.8.6';




package XML::Handler::SGMLSpl::Node;

sub new {
    my ($class, $type, $saxhash, $parent) = @_;
    $saxhash->{type} = $type;
    $saxhash->{parent} = $parent;
    $saxhash->{ext} = {};
    return bless($saxhash, $class);
}

# Element name
sub name {
    my ($self) = @_;
    if($self->{type} eq 'element') {
	return $self->{LocalName};
    } else {
	return undef;
    }
}

sub attr {
    my ($self, $name) = @_;
    if($name !~ /^\{/) { $name = '{}' . $name; }
    return $self->{Attributes}->{$name}->{Value};
}

sub within {
    my ($self,$name) = @_;
    for ($self = $self->{parent}; $self; $self = $self->{parent}) {
	return $self if ($self->name eq $name);
    }

    return undef;
}

sub in {
    my ($self,$name) = @_;
    if ($self->{parent} and $self->{parent}->name eq $name) {
	return $self->{parent};
    } else {
	return undef;
    }
}

sub ext { return shift->{ext} }
sub parent { return shift->{parent} }

# one of document, element, text, processing-instruction, comment, whitespace
sub type { return shift->{type} }




package XML::Handler::SGMLSpl;

sub new {
    my ($class, $user_data) = @_;
    my $self = {
	rules => {},
	mode => [ '' ],
        namespaces => {},       # public
	user_data => $user_data
    };
    return bless($self, $class);
}

#
# Rule parsing
#

sub add_rule {
    my $sub = pop;
    my ($self, $pattern, $mode) = @_;
    $mode = '' if !defined($mode);

    # Init hashes if not there already
    if(!defined $self->{rules}->{$mode}) {
	$self->{rules}->{$mode} = {
	    t_elem_open	 => {},
	    t_elem_close => {},
	    t_sdata => {},
	};
    }

    if($pattern eq 'text()') {
        $self->{rules}->{$mode}->{t_text} = $sub;
    } elsif($pattern eq 'processing-instruction()') {
        $self->{rules}->{$mode}->{t_pi} = $sub;
    } elsif($pattern eq 'comment()') {
        $self->{rules}->{$mode}->{t_comment} = $sub;
    } elsif($pattern eq 'sdata()') {
        $self->{rules}->{$mode}->{t_sdata}->{''} = $sub;
    } elsif($pattern eq '/<') {
        $self->{rules}->{$mode}->{t_doc_start} = $sub;
    } elsif($pattern eq '/>') {
        $self->{rules}->{$mode}->{t_doc_end} = $sub;
    } elsif($pattern =~ /^(\{([^}]+)\}(.+))<$/) {
        $self->{rules}->{$mode}->{t_elem_open}->{$1} = $sub;
    } elsif($pattern =~ /^(\{([^}]+)\}(.+))>$/) {
        $self->{rules}->{$mode}->{t_elem_close}->{$1} = $sub;
    } elsif($pattern =~ /^(([^:]+):)?([^:]+)<$/) {
        my $x = '{' . $self->{namespaces}->{$2} . '}' . $3;
        $self->{rules}->{$mode}->{t_elem_open}->{$x} = $sub;
    } elsif($pattern =~ /^(([^:]+):)?([^:]+)>$/) {
        my $x = '{' . $self->{namespaces}->{$2} . '}' . $3;
        $self->{rules}->{$mode}->{t_elem_close}->{$x} = $sub;
    } elsif($pattern =~ /^\|(.+)\|$/) {
        $self->{rules}->{$mode}->{t_sdata}->{$1} = $sub;
    } else {
        die "Unknown pattern type!";
    }
}

#
# Modes
#

sub push_mode {
    my ($self, $mode) = @_;
    push(@{$self->{mode}}, $mode);
    return $self->{mode}->[-2];
}

sub mode {
    my ($self) = @_;
    return $self->{mode}->[-1];
}

sub pop_mode {
    my ($self) = @_;
    return pop(@{$self->{mode}});
}

#
# Locators
#

sub set_document_locator {
    my ($self, $arg) = @_;
    $self->{locator} = $arg->{Locator};
}

sub get_locator {
    my ($self) = @_;
    return $self->{locator};
}

#
# Helpful utility method: displays location,
# with optional node address
#

sub warn_location
{
    my $self = shift;
    my $msg = pop;
    my $node = shift;

    my $location = $self->get_locator && $self->get_locator->location();

    my ($sysid, $linenum) = ('-', '');
    if($location) {
	$sysid = $location->{SystemId};
	$linenum = $location->{LineNumber};
    }

    my $nodeinfo;
    if(defined $node) {
	if($node->type() eq 'element') {
	    $nodeinfo = $node->name;
	} elsif($node->type eq 'text') {
	    $nodeinfo = $node->parent->name;
	}
    }

    if(defined $nodeinfo) {
	warn "$0:${sysid}:${linenum}:${nodeinfo}: ${msg}\n";
    } else {
	warn "$0:${sysid}:${linenum}: ${msg}\n";
    }
}
    


 

#
# Standard handlers: not intended for user
#

sub start_document {
    my ($self) = @_;

    my $doc = XML::Handler::SGMLSpl::Node->new(
		'document', {}, undef);
    $self->{current_node} = $doc;
    
    &{($self->{rules}->{$self->mode}->{t_doc_start} || sub{})}
	($self->{user_data}, $doc, $self);
}

sub end_document {
    my ($self) = @_;

    my $doc = $self->{current_node};
    $self->{current_node} = undef;
    
    return &{($self->{rules}->{$self->mode}->{t_doc_end} || sub{})}
	($self->{user_data}, $doc, $self);
}

sub start_element {
    my ($self, $arg) = @_;

    my $elem = XML::Handler::SGMLSpl::Node->new(
		    'element', $arg, $self->{current_node});
		    
    $self->{current_node} = $elem;

    $elem->{_last_mode} = $self->mode;

    my $key = '{' . $arg->{NamespaceURI} . '}' . $arg->{LocalName};
    my $default_key = '{' . $arg->{NamespaceURI} . '}*';
    
    my $elemrules = $self->{rules}->{$self->mode}->{t_elem_open};
    &{($elemrules->{$key} || $elemrules->{$default_key} || sub{})}
       ($self->{user_data}, $elem, $self);
}
    
sub end_element {
    my ($self, $arg) = @_;

    my $elem = $self->{current_node};

    my $key = '{' . $arg->{NamespaceURI} . '}' . $arg->{LocalName};
    my $default_key = '{' . $arg->{NamespaceURI} . '}*';

    # We always enter the end element handler with the same mode
    # as we had entered the start element handler.
    # In most cases this is the more sane behavior, even though
    # it's inconsistent.

    my $elemrules = $self->{rules}->{$elem->{_last_mode}}->{t_elem_close};
    &{($elemrules->{$key} || $elemrules->{$default_key} || sub{})}
       ($self->{user_data}, $elem, $self);
       
    $self->{current_node} = $elem->parent;
}

sub characters {
    my ($self, $arg) = @_;

    my $textnode = XML::Handler::SGMLSpl::Node->new(
		    'text', $arg, $self->{current_node});
    
    &{($self->{rules}->{$self->mode}->{t_text} || sub{})}
	($self->{user_data}, $textnode, $self);
}

sub processing_instruction {
    my ($self, $arg) = @_;

    my $pi = XML::Handler::SGMLSpl::Node->new(
		    'processing-instruction', $arg, $self->{current_node});
    
    &{($self->{rules}->{$self->mode}->{t_pi} || sub{})}
	($self->{user_data}, $pi, $self);
}

sub ignorable_whitespace {
    my ($self, $arg) = @_;

    my $textnode = XML::Handler::SGMLSpl::Node->new(
		    'whitespace', $arg, $self->{current_node});
    
    &{($self->{rules}->{$self->mode}->{t_text} || sub{})}
	($self->{user_data}, $textnode, $self);
}

sub comment {
    my ($self, $arg) = @_;

    my $comment = XML::Handler::SGMLSpl::Node->new(
		    'comment', $arg, $self->{current_node});
    
    &{($self->{rules}->{$self->mode}->{t_comment} || sub{})}
	($self->{user_data}, $comment, $self);
}

#
# SDATA entities (SGML)
#

sub internal_entity_ref {
    my ($self, $arg) = @_;

    my $sdata = XML::Handler::SGMLSpl::Node->new(
		    'sdata', $arg, $self->{current_node});

    my $sdatarules = $self->{rules}->{$self->mode}->{t_sdata};
    &{($sdatarules->{$arg->{LocalName}} || $sdatarules->{''} || sub{})}
       ($self->{user_data}, $sdata, $self);
}

# FIXME: Write a man page.

# Autoload methods go after =cut, and are processed by the autosplit program.

1;
# vim:sw=4 et sta showmatch

# db2x_texixml - convert Texi-XML to Texinfo
#                (See docbook2X documentation for details)
#
# (C) 2000-2004 Steve Cheng <stevecheng@users.sourceforge.net>
#
# See the COPYING file in the docbook2X distribution 
# for the copyright status of this software.
#      
# Note: db2x_texixml.pl does not run by itself!
#       It must be configured by including a config.pl file
#       which is done when building docbook2X.
#       In addition, the non-standard module 
#       XML::Handler::SGMLSpl must be explicitly loaded
#       when docbook2X is not installed.

package main;
use strict;


#############################################################################
#
# Option parsing
#
#############################################################################

use Getopt::Long;
Getopt::Long::Configure('bundling');
my $cmdoptions = {
    'encoding' => 'us-ascii',
    'list-files' => 0,
    'to-stdout' => 0,
    'output-dir' => '',
    'info' => 0,
    'plaintext' => 0,
    'utf8trans-program' => $db2x_config{'utf8trans-program'},
    'utf8trans-map' => $db2x_config{'utf8trans-map-texi'},
    'iconv-program' => $db2x_config{'iconv-program'},
    'makeinfo-program' => $db2x_config{'makeinfo-program'},
};

sub options_help {
    print "Usage: $0 [OPTION]... [FILE]...\n";
    print <<'end';
Make Texinfo documents from Texi-XML

  --encoding=ENCODING   Character encoding for Texinfo files
                        Default is US-ASCII
  --list-files          Write list of output files to stdout
  --to-stdout           Write output to stdout instead of to files
  --output-dir          Directory to write the output files
                        Default is current working directory
  --info                Pipe output to makeinfo, creating Info files directly
  --plaintext           Pipe output to makeinfo, creating plain text files
  
  These options set the location of auxiliary programs:
  --utf8trans-program=PATH, --utf8trans-map=PATH, 
  --iconv-program=PATH, --makeinfo-program=PATH

  --help                Show this help and exit
  --version             Show version and exit

See the db2x_texixml(1) manual page and the docbook2X documentation for
more details.
end
    exit 0;
}

sub options_version
{
    print "db2x_texixml (part of docbook2X " . 
            $db2x_config{'docbook2X-version'} . ")\n";
    print <<'end';
$Revision: 1.49 $ $Date: 2006/04/20 03:02:31 $
<URL:http://docbook2x.sourceforge.net/>

Copyright (C) 2000-2004 Steve Cheng
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
end
    exit 0;
}

$SIG{__WARN__} = sub { print STDERR "$0: " . $_[0]; };
if(!GetOptions($cmdoptions,
    'encoding=s',
    'list-files',
    'to-stdout',
    'output-dir=s',
    'info',
    'plaintext',
    'utf8trans-program=s',
    'utf8trans-map=s',
    'iconv-program=s',
    'makeinfo-program=s',
    'help', \&options_help,
    'version', \&options_version))
{
    print STDERR "Try \"$0 --help\" for more information.\n";
    exit 1;
}
$SIG{__WARN__} = undef;

#use XML::Handler::SGMLSpl;     # we link to this explicitly during building
my $texixmldata = { 'options' => $cmdoptions };
$texixml::templates = XML::Handler::SGMLSpl->new($texixmldata);
$texixml::templates->push_mode('file-unselected');
$texixml::templates->{namespaces}->{''}="http://docbook2x.sourceforge.net/xmlns/Texi-XML";
               



#############################################################################
#
# TexiWriter: output routines
#
#############################################################################

package TexiWriter;
require Exporter;
@TexiWriter::ISA = qw(Exporter);
@TexiWriter::EXPORT_OK = qw(texi_escape texi_arg_escape);

#
# Use TexiWriter on specified file
# Params: fh - an IO::Handle to send the output
#
sub new {
    my ($class, $fh) = @_;
    my $self = { fh => $fh, line_start => 1, output_buffers => [] };
    return bless($self, $class);
}

#
# Print text with whitespace folding
# (actually only newline folding)
# Usually need to escape text first
# Params: text - string to print
#
sub print_ws {
    my ($self, $text) = @_;
    
    foreach my $line (split(/(\n)/, $text)) {
        if($line eq "\n") {
            # Don't leave a blank line
            $self->{fh}->print("\n")
                unless $self->{line_start}++;
        } else {
            # Don't put any spaces at the beginning
            # of a line.  These cause makeinfo
            # to erroneously indent the text.
            $line =~ s/^[ \t]+// if $self->{line_start};

            # Collapse whitespace
            $line =~ tr/ \t/ /;

            if($line ne '') {
                $self->{fh}->print($line);
                $self->{line_start} = 0;
            }
        }
    }
}

#
# This is basically just a print, but
# with the special instruction that,
# if a '\n' is present at the beginning
# of the string, this means to start a new line,
# 
# i.e. if we are already at the beginning of the line
# we do not emit another '\n' and therefore create
# a blank line.
#
# This routine is useful for emitting "block" Texinfo
# commands that have to start at the beginning of the
# line. (Note: unlike RoffWriter's request method, 
# TexiWriter does not provide an explicit "do command" 
# method --- they are cumbersome and not necessary
# for the Texinfo format.)
#
# No escaping of the text is done.
# 
# Params: text - string to print
# 
sub output {
    my ($self, $text) = @_;

    if($text =~ s/^\n//) {
        $self->{fh}->print("\n") unless $self->{line_start}++;
    }

    return if $text eq '';

    $self->{fh}->print($text);
    $self->{line_start} = ($text =~ /\n$/);
}

#
# The following functions implement a simple stack
# of output buffers.  These are used to handle the arguments
# of a Texinfo @-command, when the content of the arguments
# is not immediately accessible in stream XML processing.
#
# The line-breaking semantics of regular output() are not implemented,
# because they do not make sense on strings.
#

sub output_buffer_push
{
    my ($self) = @_;
    unshift(@{$self->{output_buffers}}, "");
}

sub output_buffer_pop
{
    my ($self) = @_;
    return shift(@{$self->{output_buffers}});
}

sub savable_output {
    my ($self, $text) = @_;
    if(scalar(@{$self->{output_buffers}}) == 0) {
        return output($self, $text);
    } else {
        $self->{output_buffers}->[0] .= $text;
    }
}
        
    

#
# Print text without folding whitespace
# Usually need to escape text first
# Params: text - string to print
#
sub print {
    my ($self, $text) = @_;
    $self->{fh}->print($text);
    $self->{line_start} = ($text =~ /\n$/);
}

# Escape Texinfo syntax chars
#
sub texi_escape
{
    my $s = shift;
    $s =~ s/([\@\{\}])/\@$1/g;
    return $s;
}

sub texi_arg_escape 
{
    my $s = shift;
    $s =~ s/,/\@comma\{\}/g;
    return $s;
}

# Escape the ',' when output buffering is activated,
# because output buffering is typically used to handle
# arguments, and a literal ',' would be misinterpreted
# as an argument delimeter.

sub texi_arg_escape_conditional
{
    my ($self, $text) = @_;

    if(scalar(@{$self->{output_buffers}}) > 0) {
        $text =~ s/,/\@comma\{\}/g;
    }

    return $text;
}



#############################################################################
#
# Template rules
#
#############################################################################


package texixml;
import TexiWriter qw(texi_escape texi_arg_escape);

use IO::File;
use vars qw($templates);


    
    
##################################################
#
# A clean solution to the extra-newlines problem
#
##################################################

# In essence, texi_xml usually puts newlines only where you 
# expect a human editing a Texinfo file directly would.
# The heuristic works by checking if we are at the beginning
# of a line or not in our output ($texixml::newline_last),
# and if so, refrain from putting too many newlines
# (which would actually produce 2 or more blank lines).
#
# texi_xml also keeps track of what type of element (block, inline 
# or neither) it just processed, then makes line breaks only
# if it is required to separate them.
#
# This is not complete "whitespace collapsing", but since Texinfo
# is reasonably tolerant in its whitespace handling we don't need
# to have a model that collapses whitespace perfectly in every case.
# 
# 
sub block_start
{
    my ($self, $elem) = @_;

    if(scalar(@{$self->{tw}->{output_buffers}}) > 0) {
        die "$0: block_start called while saving output (this is a bug)";
    }

    $self->{tw}->output("\n\n")
        unless ($elem->in('listitem') and 
                $elem->parent->ext->{lastchild} eq '')
            or ($elem->in('entry') and
                $elem->parent->ext->{lastchild} eq '');
            # Don't put blank before the first block in 
            # varlistentries and entries of a multitable.

    $elem->parent->ext->{lastchild} = 'block';
}


sub mixed_inline_start
{
    my ($self, $node) = @_;
    
    if(scalar(@{$self->{tw}->{output_buffers}}) > 0) {
        return;
    }
    
    # Example:
    # <para>Warning<itemize>...</itemize>Do not indent this text
    # since it's part of the same paragraph</para>

    $self->{tw}->output("\n\n\@noindent\n") 
        if $node->parent->ext->{lastchild} eq 'block';

    $node->parent->ext->{lastchild} = 'inline';
}




##################################################
#
# Texinfo preamble and eof
#
##################################################

sub shell_quote
{
    join(' ', map { my $u = $_;
                    $u =~ s#([\$`"\\\n])#\\$1#g; 
                    '"' . $u . '"' } @_);
}

sub texi_openfile {
    my ($self, $basename) = @_;

    my $dir = $self->{options}->{'output-dir'};
    $dir =~ s/([^\/])$/$1\//;     # terminate with slash

    my $encoding = $self->{options}->{encoding};
    
    my $openstr = '';
        
    if(($encoding !~ /^utf|ucs/i or $encoding =~ s/\/\/TRANSLIT$//i)
        and $self->{options}->{'utf8trans-program'} ne '')
    {
        $openstr .= '| ' .
            shell_quote($self->{options}->{'utf8trans-program'}) . ' -- ' .
            shell_quote($self->{options}->{'utf8trans-map'}) . ' ';
    }
    
    if($encoding !~ /^utf-?8$/i
        and $self->{options}->{'iconv-program'} ne '')
    {
        $openstr .= '| ' .
            shell_quote($self->{options}->{'iconv-program'},
                        '-f', 'utf-8',
                        '-t', $encoding)
            . ' ';
    }

    if($self->{options}->{'plaintext'}) {
        my $filename = "${dir}${basename}.txt";
        $openstr .= '| ' . 
            shell_quote($self->{options}->{'makeinfo-program'}, 
                        '--no-headers');
        if(not $self->{options}->{'to-stdout'}) {
            print "$filename\n"
                if $self->{options}->{'list-files'};
            $openstr .= ' > ' . shell_quote($filename);
        }
    } elsif($self->{options}->{'info'}) {
        if(not $self->{options}->{'to-stdout'}) {
            $openstr .= '| ( cd ' 
                        . shell_quote($dir) . ' && exec ' 
                        . shell_quote($self->{options}->{'makeinfo-program'})
                        . ' )';
            print "${dir}${basename}.info\n"
                if $self->{options}->{'list-files'};
        } else {
            $openstr .= '| ' . shell_quote(
                                    $self->{options}->{'makeinfo-program'})
                             . ' -o -';
        }
    } else {
        my $filename = "${dir}${basename}.texi";
        if($openstr eq '') {
            if(not $self->{options}->{'to-stdout'}) {
                $openstr = $filename;
                # Trick from Perl FAQ to open file with arbitrary characters
                $openstr =~ s#^(\s)#./$1#;
                $openstr = ">${openstr}\0";
                print "$filename\n"
                    if $self->{options}->{'list-files'};
            } else {
                $openstr = '>-';
            }
        } else {
            $openstr .= '> ' . shell_quote($filename);
            print "$filename\n"
                if $self->{options}->{'list-files'};
        }
    }

    my $iof = new IO::File($openstr)
        or die "$0: error opening $openstr: $!\n";

    # Set output encoding to UTF-8 on Perl >=5.8.0
    # so it doesn't complain
    binmode($iof, ":utf8") unless $] < 5.008;

    return $iof;
}

$templates->add_rule('texinfoset<', 'file-unselected', sub {
    my ($self, $elem, $templates) = @_;
    $self->{node2id_map} = {};
    $self->{id2node_map} = {};
    $self->{id2file_map} = {};
    $self->{id_counter} = 1;
});
$templates->add_rule('texinfoset>', 'file-unselected', sub {});

$templates->add_rule('texinfo<', 'file-unselected', sub {
    my ($self, $elem, $templates) = @_;

    my $basename;
    if($elem->attr('file') ne '') {
        $basename = filename_escape($elem->attr('file'));
    } elsif($self->{inputfile} ne '-') {
        $basename = $self->{inputfile};
        # strip the path component, and extension
        $basename = $1 if $basename =~ /([^\/]+)$/;
        $basename =~ s/\.txml$//;
    } else {
        $basename = 'untitled';
    }

    $self->{fh} = texi_openfile($self, $basename);
    $self->{tw} = new TexiWriter($self->{fh});

    $self->{basename} = $basename;
    
    $self->{tw}->output("\\input texinfo\n");
    $self->{tw}->output("\n\@setfilename ${basename}.info\n");

    my $encoding = $self->{options}->{encoding};
    $encoding =~ s#//TRANSLIT$##i;
    $self->{tw}->output("\@documentencoding $encoding\n");

    $templates->pop_mode();
});

$templates->add_rule('texinfo>', 'file-unselected', sub {
    my ($self, $elem, $templates) = @_;
    
    $self->{tw}->output("\n\n\@bye\n");

    $self->{fh}->close
        or die $! ? "$0: error closing file/pipe: $!\n"
                  : "$0: program in pipeline exited with an error\n";

    $self->{fh} = undef;
    $self->{tw} = undef;
    
    $templates->push_mode('file-unselected');
});

$templates->add_rule('text()', 'file-unselected', \&illegal_text_handler);
$templates->add_rule('*<', 'file-unselected', \&illegal_element_handler);

sub illegal_text_handler {
    my ($self, $node, $templates) = @_;

    if($node->{Data} =~ /[^ \t\r\n]/) {
        $templates->warn_location($node, "character data is not allowed here");
    }
}
                    


##################################################
#
# Node name maps
#
##################################################

$templates->add_rule('nodenamemap<', 'file-unselected', sub {
    my ($self, $elem, $templates) = @_;
    $templates->push_mode('nodenamemap-mode');
});

$templates->add_rule('nodenamemap>', 'file-unselected', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
});

$templates->add_rule('nodenamemapentry<', 'nodenamemap-mode', sub {
    my ($self, $elem, $templates) = @_;
    $elem->ext->{nodenames} = [];
});
    
$templates->add_rule('nodenamemapentry>', 'nodenamemap-mode', sub {
    my ($self, $elem, $templates) = @_;

    my $id = $elem->attr('id');
    my $f = filename_escape($elem->attr('file'));
    my $nodename;

    foreach my $s (@{$elem->ext->{nodenames}}) {
        if(not exists $self->{node2id_map}->{"${f}/$s"}) {
            $nodename = $s;
            last;
        }
    }

    if(not defined $nodename) {
        if(scalar(@{$elem->ext->{nodenames}}) > 0) {
            for(my $i = 1; ; $i++) {
                $nodename = $elem->ext->{nodenames}->[0] . 
                                ' <' . $i . '>';
                last if not 
                    exists $self->{node2id_map}->{"${f}/$nodename"};
            }
        } elsif(not exists $self->{node2id_map}->{
                    $f . '/' . ($nodename = nodename_escape($id))}) {
        } else {
            $nodename = 'untitled node <' . $self->{id_counter}++ . '>';
        }
    }

    $self->{node2id_map}->{"${f}/$nodename"} = $id;
    $self->{id2node_map}->{$id} = $nodename;
    $self->{id2file_map}->{$id} = $f;
});

$templates->add_rule('nodename<', 'nodenamemap-mode', sub {
    my ($self, $elem, $templates) = @_;
    $elem->ext->{nodename} = '';
});

$templates->add_rule('nodename>', 'nodenamemap-mode', sub {
    my ($self, $elem, $templates) = @_;
    my $nodenames = $elem->parent->ext->{nodenames};
    my $s = nodename_escape($elem->ext->{nodename});
    push(@$nodenames, $s) unless $s eq '';
});

$templates->add_rule('text()', 'nodenamemap-mode', sub {
    my ($self, $node, $templates) = @_;
    if($node->in('nodename')) {
        $node->parent->ext->{nodename} .= $node->{Data};
    } else {
        &illegal_text_handler;
    }
});

$templates->add_rule('*<', 'nodenamemap-mode', \&illegal_element_handler);;



sub get_nodename
{
    my ($self, $elem, $nodename_attr, $id_attr, $optional) = @_;
    if($nodename_attr ne '') {
        return nodename_escape($nodename_attr);
    } else {
        my $f = $self->{id2file_map}->{$id_attr};
        if($f ne '' and $f ne $self->{basename}) {
            # Error, we expect this node to be in the same
            # file.
            $templates->warn_location($elem, "fatal error: node belongs to a different file");
            die;
        }
    
        return undef if($optional and $id_attr eq '');
        
        if($id_attr eq '') {
            $templates->warn_location($elem, "fatal error: neither a node name nor an ID was specified");
            die;
        }
        if(not exists($self->{id2node_map}->{$id_attr})) {
            $templates->warn_location($elem, "ID \"${id_attr}\" does not exist");
            return nodename_escape($id_attr);
        }
        
        return $self->{id2node_map}->{$id_attr};
    }
}

sub get_nodename_filename
{
    my ($self, $elem, $nodename_attr, $file_attr, $id_attr, $optional) = @_;
    if($nodename_attr ne '' or $file_attr ne '') {
        return (nodename_escape($nodename_attr), filename_escape($file_attr));
    } else {
        return undef if $optional and $id_attr eq '';
        
        if($id_attr eq '') {
            $templates->warn_location($elem, "fatal error: neither a node name nor an ID was specified");
            die;
        }
        if(not exists($self->{id2node_map}->{$id_attr})) {
            $templates->warn_location($elem, "fatal error: ID \"${id_attr}\" does not exist");
            return (nodename_escape($id_attr), '')
        }
        
        return ($self->{id2node_map}->{$id_attr},
                $self->{id2file_map}->{$id_attr});
    }
}
            

    


##################################################
#
# Simple title pages
#
##################################################

$templates->add_rule('settitle<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@settitle ");
    $templates->push_mode('single-line-mode');
});
$templates->add_rule('settitle>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});

$templates->add_rule('titlepage<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@titlepage\n");
});
$templates->add_rule('titlepage>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end titlepage\n");
});
$templates->add_rule('title<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@title ");
    $templates->push_mode('single-line-mode');
});
$templates->add_rule('title>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});
$templates->add_rule('subtitle<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@subtitle ");
    $templates->push_mode('single-line-mode');
});
$templates->add_rule('subtitle>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});
$templates->add_rule('author<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@author ");
    $templates->push_mode('single-line-mode');
});
$templates->add_rule('author>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});




##################################################
#
# Menus, nodes
#
##################################################

# Do escaping for nodenames:
# NOTE: stylesheets should do this if possible
# since there can be rare name clashes.
sub nodename_escape
{
    my $name = shift;
    for ($name) {
        tr/().,:/[]_;;/;
        tr/ \t\n/ /s;
        s/^ +//g;
        s/ +$//g;
    }
    return $name;
}

sub filename_escape
{
    my $s = shift;
    $s =~ tr/\//_/;
    return $s;
}


$templates->add_rule('node<', sub {
    my ($self, $elem, $templates) = @_;
    my $node = texi_escape(
                get_nodename($self, $elem, 
                    $elem->attr('name'), $elem->attr('id')));

    my $next = texi_escape(get_nodename($self, $elem,
            $elem->attr('next'), $elem->attr('nextid'), 'optional'));
    my $previous = texi_escape(get_nodename($self, $elem,
            $elem->attr('previous'), $elem->attr('previousid'), 'optional'));
    my $up = texi_escape(get_nodename($self, $elem,
            $elem->attr('up'), $elem->attr('upid'), 'optional'));

    if(defined($next) or defined($previous) or defined($up)) {
        if($node =~ /^[Tt]op$/ and $elem->attr('up') eq '') {
            $up = '(dir)';
        }

        $self->{tw}->output(
            "\n\n\@node ${node}, ${next}, ${previous}, ${up}\n");
    } else {
        $self->{tw}->output("\n\n\@node $node\n");
    }
});

$templates->add_rule('menu<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output("\@menu\n");
    $templates->push_mode('menu-mode');
});
$templates->add_rule('menu>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n\@end menu\n");
});
$templates->add_rule('detailmenu<', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output("\@detailmenu\n");
});
$templates->add_rule('detailmenu>', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end detailmenu\n");
});
$templates->add_rule('menuline<', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->push_mode('menu-saved-text-mode');
    $self->{tw}->output_buffer_push();
});
$templates->add_rule('menuline>', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();

    my $s = $self->{tw}->output_buffer_pop();

    $self->{tw}->output($s . "\n");
    $self->{tw}->output("\n\n") if($s eq '');
});

$templates->add_rule('menuentry<', 'menu-mode', sub {});
$templates->add_rule('menuentry>', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n");
});

$templates->add_rule('menuentrytitle<', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->push_mode('menu-saved-text-mode');
    $self->{tw}->output_buffer_push();
});

$templates->add_rule('menuentrytitle>', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();

    my $entry = $self->{tw}->output_buffer_pop();
    
    # Since the contents of @menu is supposed to be "pre-formatted",
    # Texinfo will be picky about extra spaces.
    # Eliminate them here.
    $entry =~ tr/ / /s;
    $entry =~ s/^ //;


    # Although the menu entry is not constrained to the set
    # of characters allowed for node names, the use of ':'
    # to separate the parts of menu entry implies that it
    # is not an allowed character.
    $entry =~ tr/:/;/;

    my ($node,$file) = get_nodename_filename($self, $elem->parent,
                            $elem->parent->attr('node'),
                            $elem->parent->attr('file'),
                            $elem->parent->attr('idref'),
                            $elem->parent->in('directory'));
    $node = texi_escape($node);
    $file = texi_escape($file);

    # The eventual output
    my $s;
    
    $self->{tw}->output("\n");
    
    if($file ne '' and ($node eq '' or $file ne $self->{basename})) {
        $s = "* ${entry}: (${file})${node}.";
    } else {
        if($entry eq $node) {
            $s = "* ${entry}::";
        } else {
            $s = "* ${entry}: ${node}.";
        }
    }

    $self->{tw}->output($s);

    $elem->parent->ext->{'entry_length'} = length($s);
});

use Text::Wrap ();
$templates->add_rule('menuentrydescrip<', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->push_mode('menu-saved-text-mode');
    $self->{tw}->output_buffer_push();
});
$templates->add_rule('menuentrydescrip>', 'menu-mode', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();

    my $text = $self->{tw}->output_buffer_pop();
    
    # Since the contents of @menu is supposed to be "pre-formatted",
    # Texinfo will be picky about extra spaces.
    # Eliminate them here.
    $text =~ tr/ / /s;
    $text =~ s/^ //;
        
    my $entry_length = $elem->parent->ext->{'entry_length'};
    
    my $first_line_padding = 
        $entry_length<32 ? 32-$entry_length : 3;
    my $first_line_overflow = 0;

    my $start_column = 
        $entry_length + $first_line_padding + 2;
        
    if($start_column > 50) {
        $first_line_overflow = 1;
        $start_column = 50;
    }


    $Text::Wrap::columns = 78 - $start_column;
    
    my @lines = split(/(\n)/, Text::Wrap::wrap("", "", $text));

    if(!$first_line_overflow) {
        my $first_line = shift @lines;
        if($first_line) {
            $self->{tw}->output((' ' x $first_line_padding) . $first_line);
        }
    } else {
        $self->{tw}->output("\n");
    }

    foreach my $line (@lines) {
        if($line eq "\n") {
            $self->{tw}->output("\n");
        } else {
            $self->{tw}->output((' ' x $start_column) . $line);
        }
    }
});
    
$templates->add_rule('text()', 'menu-mode', \&illegal_text_handler);
$templates->add_rule('*<', 'menu-mode', \&illegal_element_handler);



##################################################
#
# Info directory
#
##################################################

$templates->add_rule('directory<', sub {
    my ($self, $elem, $templates) = @_;
    
    # If creating plain text files, suppress the directory.
    # Really, makeinfo ought to do this, but it doesn't.
    if($self->{options}->{'plaintext'}) {
        $templates->push_mode('directory-suppress');
        return;
    }

    if(defined $elem->attr('category')) {
        $self->{tw}->output("\n\@dircategory " . 
            texi_escape($elem->attr('category')) . "\n");
    }
    
    $self->{tw}->output("\n\@direntry\n");
    $templates->push_mode('menu-mode');
});
$templates->add_rule('directory>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();

    return if $self->{options}->{'plaintext'};

    $templates->pop_mode();
    $self->{tw}->output("\n\@end direntry\n");
});

$templates->add_rule('text()', 'directory-suppress', sub {});
$templates->add_rule('*<', 'directory-suppress', sub {});

 
##################################################
#
# Internationalization
#
##################################################

$templates->add_rule('documentlanguage<', sub {
    my ($self, $elem, $templates) = @_;
    my $lstack = $self->{'language-stack'};

    if(defined $elem->attr('lang')) {
        $self->{tw}->output("\n\@documentlanguage " . $elem->attr('lang') . "\n");
        push(@$lstack, $elem->attr('lang'));
    } else {
        pop(@$lstack);
        $self->{tw}->output("\n\@documentlanguage " . $lstack->[-1] . "\n")
            if scalar(@$lstack);
    }
});




##################################################
#
# Inline elements
#
##################################################

sub inline_start_handler {
    my ($self, $elem, $templates) = @_;
    mixed_inline_start($self, $elem);
    $self->{tw}->savable_output('@'. $elem->name . '{');
}
sub inline_end_handler {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->savable_output('}');
}

foreach my $gi
    (qw(code samp cite email dfn file sc acronym emph strong key kbd var 
        env command option
        i b r t 
        footnote)) 
{
    $templates->add_rule("${gi}<", \&inline_start_handler);
    $templates->add_rule("${gi}>", \&inline_end_handler);

    $templates->add_rule("${gi}<", 'single-line-mode', \&inline_start_handler);
    $templates->add_rule("${gi}>", 'single-line-mode', \&inline_end_handler);

    $templates->add_rule("${gi}<", 'saved-text-mode', \&inline_start_handler);
    $templates->add_rule("${gi}>", 'saved-text-mode', \&inline_end_handler);
    
    $templates->add_rule("${gi}<", 'verbatim-mode', sub {});
    $templates->add_rule("${gi}>", 'verbatim-mode', sub {});
    $templates->add_rule("${gi}<", 'menu-saved-text-mode', sub {});
    $templates->add_rule("${gi}>", 'menu-saved-text-mode', sub {});
}

sub anchor_start {
    my ($self, $elem, $templates) = @_;
    mixed_inline_start($self, $elem);
    $self->{tw}->savable_output('@anchor{' 
        . texi_escape(get_nodename($self, $elem,
                $elem->attr('node'), $elem->attr('id')))
        . '}');
}

$templates->add_rule('anchor<', \&anchor_start);
$templates->add_rule('anchor<', 'single-line-mode', \&anchor_start);
$templates->add_rule('anchor<', 'saved-text-mode', \&anchor_start);
$templates->add_rule('anchor<', 'menu-saved-text-mode', \&anchor_start);
$templates->add_rule('anchor<', 'verbatim-mode', \&anchor_start);

$templates->add_rule('*<', 'single-line-mode', \&illegal_element_handler);
$templates->add_rule('*<', 'saved-text-mode', \&illegal_element_handler);
$templates->add_rule('*<', 'menu-saved-text-mode', \&illegal_element_handler);


    
##################################################
#
# Cross references, links
#
##################################################

sub crossref_start_handler {
    my ($self, $elem, $templates) = @_;
    mixed_inline_start($self, $elem);
    $self->{tw}->output_buffer_push();
    $templates->push_mode('saved-text-mode');
}

sub crossref_end_handler {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();

    # Syntax:
    # @ref{$node,$infolabel,$label,$file,$printmanual}
    # node - required
    # infolabel, label - optional
    # label is actually the inline content of this element
    # infofile, printmanual - optional

    my ($node, $file) = get_nodename_filename($self, $elem,
                            $elem->attr('node'),
                            $elem->attr('file'),
                            $elem->attr('idref'));
    
    my $infolabel = $elem->attr('infolabel');
    my $label = $self->{tw}->output_buffer_pop();

    # If the node and cross reference label turn out to be
    # the same, make the latter empty so info won't display it
    # twice.
    $label = '' if $node eq $label;
    $infolabel = '' if $node eq $infolabel;

    # Note: 
    # 1. Node names cannot contain commas anyway, so no 
    #    texi_arg_escape needed.
    # 2. label is not escaped here, because it already IS escaped.
    $node = texi_escape($node);
    $infolabel = texi_arg_escape(texi_escape($infolabel));
    $file = texi_arg_escape(texi_escape($file));
    
    my $printmanual = texi_arg_escape(
                        texi_escape($elem->attr('printmanual')));

    $self->{tw}->savable_output('@' . $elem->name . '{' . $node);

    if($file ne '' and $file ne $self->{basename}) {
        # Reference to another file
        $self->{tw}->savable_output(",$infolabel,$label,$file,$printmanual}");
    }
    else {
        # Same file
        if($label eq '' and $infolabel eq '') { 
            $self->{tw}->savable_output("}"); 
            return;
        } elsif($label eq '') { 
            $self->{tw}->savable_output(",$infolabel}"); 
        } else { 
            $self->{tw}->savable_output(",$infolabel,$label}"); 
        }
    }
}

foreach my $gi (qw(xref ref pxref)) {
    $templates->add_rule("${gi}<", \&crossref_start_handler);
    $templates->add_rule("${gi}>", \&crossref_end_handler);

    $templates->add_rule("${gi}<", 'single-line-mode', \&crossref_start_handler);
    $templates->add_rule("${gi}>", 'single-line-mode', \&crossref_end_handler);
    $templates->add_rule("${gi}<", 'saved-text-mode', \&crossref_start_handler);
    $templates->add_rule("${gi}>", 'saved-text-mode', \&crossref_end_handler);

    $templates->add_rule("${gi}<", 'verbatim-mode', sub {});
    $templates->add_rule("${gi}>", 'verbatim-mode', sub {});
    $templates->add_rule("${gi}<", 'menu-saved-text-mode', sub {});
    $templates->add_rule("${gi}>", 'menu-saved-text-mode', sub {});
}




##################################################
#
# URI references
#
##################################################

$templates->add_rule('uref<', sub {
    my ($self, $elem, $templates) = @_;
    mixed_inline_start($self, $elem);
    $self->{tw}->output_buffer_push();
    $templates->push_mode('saved-text-mode');
});

$templates->add_rule('uref>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    
    my $url = texi_escape($elem->attr('url'));
    my $text = $self->{tw}->output_buffer_pop();

    if($text eq '') {
        $self->{tw}->savable_output("\@uref{$url}");
    } else {
        $self->{tw}->savable_output("\@uref{$url,$text}");
    }
});

# FIXME
$templates->add_rule("uref<", 'single-line-mode', sub {});
$templates->add_rule("uref>", 'single-line-mode', sub {});
$templates->add_rule("uref<", 'saved-text-mode', sub {});
$templates->add_rule("uref>", 'saved-text-mode', sub {});

$templates->add_rule("uref<", 'verbatim-mode', sub {});
$templates->add_rule("uref>", 'verbatim-mode', sub {});
$templates->add_rule("uref<", 'menu-saved-text-mode', sub {});
$templates->add_rule("uref>", 'menu-saved-text-mode', sub {});



##################################################
#
# Sectioning elements
#
##################################################

sub section_start_handler {
    my ($self, $elem, $templates) = @_;
    $elem->parent->ext->{'lastchild'} = 'block';
    $self->{tw}->output("\n\@" . $elem->name . ' ');
    $templates->push_mode('single-line-mode');
}

sub section_end_handler {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
}

foreach my $gi
    (qw(chapter section subsection subsubsection
       majorheading chapheading heading subheading subsubheading
       top unnumbered unnumberedsec unnumberedsubsec unnumberedsubsubsec
       appendix appendixsec appendixsubsec appendixsubsubsec)) 
{
    $templates->add_rule("${gi}<", \&section_start_handler);
    $templates->add_rule("${gi}>", \&section_end_handler);
}




##################################################
#
# Paragraph
#
##################################################

$templates->add_rule('para<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
});
$templates->add_rule('para>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n");
});
    
        


##################################################
#
# Verbatim displays
#
################################################

sub verbatim_block_start_handler 
{
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output('@' . $elem->name . "\n");
    $templates->push_mode('verbatim-mode');
}
sub verbatim_block_end_handler
{
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n\@end " . $elem->name . "\n");
}


foreach my $gi
    (qw(example display format)) {
    $templates->add_rule("${gi}<", \&verbatim_block_start_handler);
    $templates->add_rule("${gi}>", \&verbatim_block_end_handler);
}

$templates->add_rule('*<', 'verbatim-mode', \&illegal_element_handler);

##################################################
#
# Quotation blocks
#
##################################################

sub quotation_block_start_handler {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output('@' . $elem->name . "\n");
}
sub quotation_block_end_handler {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end " . $elem->name . "\n");
}

foreach my $gi
    (qw(quotation cartouche flushleft flushright)) {
    $templates->add_rule("${gi}<", \&quotation_block_start_handler);
    $templates->add_rule("${gi}>", \&quotation_block_end_handler);
}




##################################################
#
# Lists
#
##################################################

$templates->add_rule('enumerate<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output("\@enumerate " . $elem->attr('begin') . "\n");
});

$templates->add_rule('enumerate>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end enumerate\n");
});

$templates->add_rule('itemize<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    
    if($elem->attr('markchar') ne '') {
        $self->{tw}->output("\@itemize ") 
            . texi_escape($elem->attr('markchar')) 
            . "\n";
    } else {
        $self->{tw}->output("\@itemize \@w\n");
    }
});

$templates->add_rule('itemize>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end itemize\n");
});

$templates->add_rule('varlist<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $self->{tw}->output("\@table \@asis\n");
});
$templates->add_rule('varlist>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end table\n");
});

$templates->add_rule('varlistentry<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
});
$templates->add_rule('term<', sub {
    my ($self, $elem, $templates) = @_;
    if($elem->parent->ext->{numterms}++) {
        $self->{tw}->output("\@itemx ");
    } else {
        $self->{tw}->output("\@item ");
    }
    $templates->push_mode('single-line-mode');
});
$templates->add_rule('term>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});

$templates->add_rule('listitem<', sub {
    my ($self, $elem, $templates) = @_;

    # listitem is used in both varlistentry and plain lists,
    # but the @item markup is supplied by <term> in the former 
    # case already.
    if($elem->parent->name ne 'varlistentry') {
        block_start($self, $elem);
        $self->{tw}->output("\@item\n");
    }
});




##################################################
#
# Tables
#
#################################################

$templates->add_rule('multitable<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);
    $elem->ext->{total_cols} = $elem->attr('cols');
    $elem->ext->{column_data} = [];
    $elem->ext->{colspec_current_colnum} = 0;
    $elem->ext->{colnames} = {};
    $elem->ext->{spannames} = {};
});

$templates->add_rule('colspec<', sub {
    my ($self, $elem, $templates) = @_;

    my $col;
    if($elem->attr('colnum')) {
        $col = $elem->attr('colnum');
    } else {
        $col = $elem->parent->ext->{colspec_current_colnum} + 1;
    }
    $elem->parent->ext->{colspec_current_colnum} = $col;

    if($elem->attr('colname') ne '') {
        $elem->parent->ext->{colnames}->{$elem->attr('colname')} = $col;
    }
    
    $elem->parent->ext->{column_data}->[$col-1] = 
        '' . $elem->attr('colwidth');
});

$templates->add_rule('spanspec<', sub {
    my ($self, $elem, $templates) = @_;

    $elem->parent->ext->{spannames}->{$elem->attr('spanname')}
        = [ $elem->attr('namest'), $elem->attr('nameend') ];
});

$templates->add_rule('tbody<', sub {
    my ($self, $elem, $templates) = @_;
    
    my $column_data = $elem->parent->ext->{column_data};
    my $totalcols = $elem->parent->ext->{total_cols};

    my @vspans = ();
    for(my $i = 0; $i < $totalcols; $i++) {
        push(@vspans, 0);
    }
    $elem->ext->{current_vspans} = \@vspans;
    
    my $proportsum = 0;
    for(my $i = 0; $i < $totalcols; $i++) {
        my $colwidth = $column_data->[$i];
        if($colwidth eq '') {
            $colwidth = $column_data->[$i] = '1*';
        }
        
        # Later we may support other types of width measure,
        # so proportional measures should be written
        # as "r*".
        $colwidth =~ s/\*\s*$//;
        $proportsum += $colwidth;
    }
   
    my $columnfractions = '';
    for(my $i = 0; $i < $totalcols; $i++) {
        my $colwidth = $column_data->[$i];
        $colwidth =~ s/\*\s*$//;
        $columnfractions .= $colwidth/$proportsum . ' ';
    }
    $columnfractions =~ s/ $//;
    
    $self->{tw}->output("\n\@multitable \@columnfractions $columnfractions\n");
});

$templates->add_rule('tbody>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@end multitable\n");
});

$templates->add_rule('row<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@item\n");

    $elem->ext->{current_colnum} = 0;
    tbl_advance_column($elem, $self->{tw}, 0, 1);
});
$templates->add_rule('row>', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n");
    my $vspans = $elem->parent->ext->{current_vspans};
    for(my $i = 0; $i < @$vspans; $i++) {
        $vspans->[$i]-- if $vspans->[$i] > 0;
    }
});

$templates->add_rule('entry<', sub {
    my ($self, $elem, $templates) = @_;

    my $tableext = $elem->parent->parent->parent->ext;
    my $namest; my $nameend;
    if($elem->attr('spanname')) {
        $namest = $tableext->{spannames}->{$elem->attr('spanname')}->[0];
        $nameend = $tableext->{spannames}->{$elem->attr('spanname')}->[1];
    } elsif($elem->attr('namest')) {
        $namest = $elem->attr('namest');
        $nameend = $elem->attr('nameend');
    }

    my $relative_advance = 1;
    my $colnum;
    if(defined $namest) {
        my $col_st = $colnum = $tableext->{colnames}->{$namest};
        my $col_end = $tableext->{colnames}->{$nameend};
        
        $relative_advance = $col_end - $col_st + 1;
    } 
    elsif($elem->attr('colname')) {
        $colnum = $tableext->{colnames}->{$elem->attr('colname')};
    }

    if(defined $colnum) {
        tbl_advance_column($elem->parent, $self->{tw}, $colnum);
    }
 
    $elem->ext->{relative_advance} = $relative_advance;

    if($elem->attr('morerows')) {
        if($elem->attr('morerows') !~ /^\d+$/) {
            warn_location($elem, "invalid morerows value --- ignoring\n");
        } else {
            for(my $i = 0; $i < $relative_advance; $i++) {
                $elem->parent->parent->ext->{current_vspans}->[
                    $elem->parent->ext->{current_colnum} - 1 + $i]
                        = $elem->attr('morerows') + 1;
            }
        }
    }
    
});

$templates->add_rule('entry>', sub {
    my ($self, $elem, $templates) = @_;
    tbl_advance_column($elem->parent, $self->{tw}, 
                       0, $elem->ext->{relative_advance});
});

sub tbl_advance_column
{
    my ($row, $tw, $new_colnum, $relative_advance) = @_;

    my $old_colnum = $row->ext->{current_colnum};
    my $total_cols = $row->parent->parent->ext->{total_cols};

    if($relative_advance) {
        my $vspans = $row->parent->ext->{current_vspans};
        for($new_colnum = $old_colnum + $relative_advance;
            $new_colnum <= $total_cols && ($vspans->[$new_colnum - 1] > 0);
            $new_colnum++)
        {}
    }
    elsif($new_colnum == -1) {
        $new_colnum = $total_cols + 1;
    }

    $row->ext->{current_colnum} = $new_colnum;

    $new_colnum = $total_cols if $new_colnum > $total_cols;
    $old_colnum = 1           if $old_colnum == 0;

    $tw->output('@tab ' x ($new_colnum - $old_colnum));
}

##################################################
#
# Graphics
#
##################################################

sub image_handler {
    my ($self, $elem, $templates) = @_;
    mixed_inline_start($self, $elem);
    
    my $filename = texi_escape($elem->attr('filename'));

    # The @image command has to have the basename and extension
    # separated, so do that.
    my $basename; my $extension;

    if($filename =~ /^(.+)(\.[^\.]+)$/) {
        $basename = $1;
        $extension = $2;
    } else {
        $basename = $filename;
        $extension = '';
    }

    if(defined $elem->attr('width') or
       defined $elem->attr('height'))
    {
        $self->{tw}->savable_output('@image{' . $basename . ',' . 
            texi_escape($elem->attr('width')) .
            ',' . 
            texi_escape($elem->attr('height')) .
            ',,' .
            $extension .
            '}');
    } 
    else {
        $self->{tw}->savable_output(
            '@image{' . $basename . ',,,,' . $extension . '}');
    }
}

$templates->add_rule('image<', \&image_handler);
$templates->add_rule('image<', 'single-line-mode', \&image_handler);
$templates->add_rule('image<', 'saved-text-mode', \&image_handler);
$templates->add_rule('image<', 'verbatim-mode', \&image_handler);
$templates->add_rule('image<', 'menu-saved-text-mode', sub {});




##################################################
#
# Vertical spacing
#
##################################################

$templates->add_rule('sp<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@sp " . $elem->attr('lines') . "\n");
});
$templates->add_rule('page<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n\@page\n");
});



##################################################
#
# Indices
#
##################################################

$templates->add_rule('indexterm<', sub {
    my ($self, $elem, $templates) = @_;

    # We allow indexterm at block level just like
    # DocBook.  When that happens, don't treat
    # it as an inline (hence no @noindent mantra).
    if($elem->parent->ext->{lastchild} ne 'block') {
        mixed_inline_start($self, $elem);
    }
    
    my $class = $elem->attr('class');
    $class = 'c' if $class eq 'cp';
    $class = 'f' if $class eq 'fn';
    $class = 'v' if $class eq 'vr';
    $class = 'k' if $class eq 'ky';
    $class = 'p' if $class eq 'pg';
    $class = 't' if $class eq 'tp';
    
    # @cindex has to start on a new line.
    # I don't know if we are in a middle of an inline
    # command (eg @{code}) that @cindex would work
    # and not disrupt the inline.  I'm just hoping it works.
    # If it doesn't, then it is a dumb limitation!
    
    $self->{tw}->output("\n\@" . $class . 'index ');

    # Are @-commands allowed for indexed terms?
    $templates->push_mode('single-line-mode');

});
$templates->add_rule('indexterm>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode();
    $self->{tw}->output("\n");
});

$templates->add_rule('printindex<', sub {
    my ($self, $elem, $templates) = @_;
    block_start($self, $elem);

    my $class = $elem->attr('class');
    $class = 'cp' if $class eq 'c';
    $class = 'fn' if $class eq 'f';
    $class = 'vr' if $class eq 'v';
    $class = 'ky' if $class eq 'k';
    $class = 'pg' if $class eq 'p';
    $class = 'tp' if $class eq 't';
    
    $self->{tw}->output("\n\@printindex " . $class . "\n");
});




##################################################
#
# Character data
#
##################################################

$templates->add_rule('text()', 'single-line-mode', sub {
    my ($self, $node, $templates) = @_;
    my $s = texi_escape($node->{Data});
    
    # Collapse spaces, no newlines.
    $s =~ tr/ \t\n/ /s;
    $s = $self->{tw}->texi_arg_escape_conditional($s);

    $self->{tw}->savable_output($s);
});

sub saved_text_mode_handler
{
    my ($self, $node, $templates) = @_;
    my $s = texi_escape($node->{Data});

    # Collapse spaces, no newlines.
    $s =~ tr/ \t\n/ /s;
    $s = $self->{tw}->texi_arg_escape_conditional($s);

    $self->{tw}->savable_output($s);
}

sub menu_saved_text_mode_handler
{
    my ($self, $node, $templates) = @_;
    my $s = texi_escape($node->{Data});

    # Collapse spaces, no newlines.
    $s =~ tr/ \t\n/ /s;

    $self->{tw}->savable_output($s);
}
$templates->add_rule('text()', 'saved-text-mode', 
    \&saved_text_mode_handler);

$templates->add_rule('text()', 'menu-saved-text-mode', 
    \&menu_saved_text_mode_handler);

$templates->add_rule('text()', 'menu-mode', sub {});

$templates->add_rule('text()', 'verbatim-mode', sub {
    my ($self, $node, $templates) = @_;
    my $s = texi_escape($node->{Data});

    $self->{tw}->print($s);
});

$templates->add_rule('text()', sub {
    my ($self, $node, $templates) = @_;
    
    my $s = texi_escape($node->{Data});
    
    mixed_inline_start($self, $node)
        unless $s =~ /^[ \t\r\n]+$/;
            # Whitespace used to separate element
            # in a non-mixed content model should
            # not cause any spurious breaks.

    $self->{tw}->print_ws($s);
});




##################################################
#
# Comments
#
##################################################

$templates->add_rule('comment<', sub {
    my ($self, $elem, $templates) = @_;
    $self->{tw}->output("\n");
    $self->{tw}->output_buffer_push();
    $templates->push_mode('comment-mode');
});

$templates->add_rule('comment>', sub {
    my ($self, $elem, $templates) = @_;
    $templates->pop_mode('comment-mode');
    
    foreach my $line (split(/\n/, $self->{tw}->output_buffer_pop())) {
        $self->{tw}->output("\@c $line\n");
    }
});

$templates->add_rule('text()', 'comment-mode', sub {
    my ($self, $node, $templates) = @_;
    my $s = $node->{Data};
    $self->{tw}->savable_output($s);
});




##################################################
#
# Processing instructions
#
##################################################

$templates->add_rule('processing-instruction()', sub {
    my ($self, $node, $templates) = @_;

    if($node->{Target} eq 'texinfo') {
        my $data = $node->{Data};
        $data =~ s/\&#xA;/\n/g;
        $data =~ s/\&#10;/\n/g;
        $self->{tw}->output($data);
    }
});




##################################################
#
# Catch unknown elements
#
##################################################

$templates->add_rule('*<', \&illegal_element_handler);
sub illegal_element_handler {
    my ($self, $node, $templates) = @_;
    $templates->warn_location($node, "element not allowed here");
};




#############################################################################
#
# Main 
#
#############################################################################

package main;

use XML::SAX::ParserFactory;

unshift(@ARGV, '-') unless @ARGV;
my $parser = XML::SAX::ParserFactory->parser(
        DocumentHandler => $texixml::templates);

foreach my $file (@ARGV)
{
    $texixmldata->{inputfile} = $file;
    if($file eq '-') {
        $parser->parse_file(\*STDIN);
    } else {
        $parser->parse_uri($file);
    }
}

