# Almost Free Text (AFT) Parser
#
# Copyright (C) 1996-2002  Todd A. Coram. All rights reserved.
#
# This perl script parses aft documents and produces output formatted according
# to an aft 'element' file.  See aft-refman.aft  for additional information.
#

use strict; 

package AFT;

use vars qw ($VERSION);

# Initializations of globals.

my $author = '';
my $title = '';
$VERSION="v5.0793";

my $version=
  "Almost Free Text $VERSION; Copyright 1996-2002 Todd Coram. All rights reserved.";

my $aft_advert =
  "This document was generated using {-AFT $VERSION\@http://www.maplefish.com/todd/aft.html-}";

my $autonumber =0;		# prefix sections with nested numbers 
my $element_type="html";	# Default

my $verbose = 0;		# Spew out lots of rambling commentary?
my $outputfile='';		# Output file

my $tabstop=8;			# Default number of spaces constituting a tab.


# Holds file handle for table of contents output
#
my $tocout;

#
# Global State variables
#
my $mode = 
{
 inTable => 0,			# Are we in table mode?
 needTableHeaders => 0,		# Used to keep track of table building.
 inQuote => 0,			# Are we in quote mode?
 inVerbatim => 0,		# Are we in verbatim mode?
 blockedVerbatim => 0,		# Are we in blocked verbatim mode?
 filteredVerbatim => 0,		# Are we in filtered verbatim mode?
 inParagraph => 0,		# Paragraph mode indicator
 inListElement => 0,		# Are we inside of a list element?
 currentSectionLevel => 0,      # Current section we are in.
 haveSections => 0,		# True if we ever go into sections...
};

my $tableCaption = '';		# Holds current table's caption.
my @listStack;			# A stack of lists as we nest.
my @sectionStack;		# Keeps track of nesting sections.
my $sectionNumber = Autonum->new();

#
# Paragraph State variables
#
my $paragraph =
{
 small => 0,
 strong => 0,
 emphasize => 0,
 teletype => 0,
};

# Pragma variables (set from inside documents)
#
my %pragma_prevar = ();	# variables expanded before filtering.


# AFT functions
#
my @Functions = (
	      \&handleBlockedVerbatim, # Must be before first...
	      \&handleComments, # Must be first
	      \&handleTitlePreamble, # Must be second
	      \&handleIncludes,
	      \&handleImage,
	      \&handleRuler,
	      \&handleSections,
	      \&handleLists,
	      \&handleCenteredText,
	      \&handleQuotedText,
	      \&handleTable,
	      \&handleVerbatim,
	     );
 
my $holdingPreamble = 1;

# Run AFT from the command line. (The normal way to invoke AFT)
#
sub main {
    &parseCommandLine();
    &loadElementFile(&elementFile());

    # Try and open output file and set it as the default output.
    #
    open(OUT, ">$outputfile") || die "Can't open $outputfile for output!\n";
    select(OUT);

    # Say it loud, say it proud..
    #
    print(STDERR 
      "$version\n  Writing $element_type output into $outputfile using $INC{elementFile()}.\n") 
     if $verbose;


    &begin();

    # Process each file supplied on the command line.
    #
    FILE: foreach my $filename (@ARGV) {
     &processFile($filename);
    }

    &end();

    close(OUT);
    # If we wrote a table of contents, close it.
    $tocout && (close($tocout));

    my $postProcessor = $AFT_OUTPUT::elem{'PostProcessor'};
    if ($postProcessor) {
	print (STDERR "\nPost Processing with '$postProcessor'\n");
	eval $postProcessor or die "Can't post process $outputfile $!\n";
    }
    exit 0; 
}


sub begin {
    # Initialize our state.
    #
    &resetStates();

    # Hold onto the preamble 'til we see if *Title and *Author info is present.
    #
    $holdingPreamble = 1;
}

# Run AFT on a file given its filename.
#
sub doFile {
    my($filename) = @_;
    &loadElementFile(&elementFile());
    &begin();
    &processFile($filename);
    &end();
}

# Run AFT on a single line supplied as a string (with a reference filename
# and line number-- both can be fake.)
#
sub doString {
	local $_ = shift;
	my ($fname, $lcnt) = @_;
	
    # Convert every $tabstop spaces into a tab... e.g. /\ {4}/
	s/\ {$tabstop}/\t/g if (!$mode->{blockedVerbatim});
    

    # Iterate through all functions until one satisfies the input.
    #
    foreach my $function (@Functions) {
      if ($function->($fname, $lcnt, $_)) {
		return;
      }
    }
    
    # All non-tabbed, non-sectional, non-special lines end up here.
    #
    
    # Always reset states
    # (take us out of whatever mode we may have been in).
    #
    &resetStates();
    
    
    # Now handle a special case... We need to detect blank lines to
    # determine whether we should end paragraph mode.
    #
    &resetParagraph(), return if $_ eq '';
    
    # Otherwise, if not in paragraph mode, enter paragraph mode now.
    #
    (&enterParagraph()) if !$mode->{inParagraph};
    
    # and  kick out the filtered line.
    #
    &output(&filter($_)."\n");
}	
	
# Run AFT on a file given its file handle.
#
sub doFH_File {
    my($fh, $filename) = @_;
    &loadElementFile(&elementFile());
    &begin();
    &processFH_File($fh, $filename);
    &end();
}


sub end {
    # End all modes.
    #
    &resetStates();

    # If we ever entered sections...
    if ($mode->{haveSections}) {
      &enterSection(0);
      &output($AFT_OUTPUT::elem{'EndSectLevel1'}."\n");
    }

    # End output file with Postamble..
    #
    &output($AFT_OUTPUT::filePostamble."\n", aft => &filter($aft_advert));
}

sub parseCommandLine {
    ## Process the command line options.
    #
    my $usage=
    "Usage:\n aft [--autonumber] [--verbose] [--output=file | --output=-] [--type=output_type] infile ..";

    use Getopt::Long;
    GetOptions ("output=s" => \$outputfile, # output file name
    	    "verbose!" => \$verbose, # output type (html, etc)
    	    "type=s" => \$element_type, # output type (html, etc)
 	    "autonumber!" => \$autonumber, # section numbers
    	    "tabstop=i" => \$tabstop); # number of spaces = tab

    (@ARGV == 0) && do {
        print (STDERR "$version\n$usage\n");
        exit 2;
    };

    if ($outputfile eq '') {
        # Use first input filename to construct output filename.
	#
        $outputfile = $ARGV[0];
	$outputfile =~ s/\.\w+$//; # remove last '.' and anything following.
	$outputfile .= ".$element_type";
    }

}

sub elementFile {
    return "aft-".$element_type.".pm";
}

# loadElementFile(file) - load the supplied element file name.
#
sub loadElementFile {
    my $elementfile = shift;
    # This is more of an '#include' than a package import.
    eval
	{require $elementfile};	# Sets the above 3 variables in a subroutine
				# called initElements() and adds 2 additional
				# subroutines: prefilter() and postfilter().
    die "Can't locate $elementfile. \n\t(I looked in: @INC)\n" if $@;

    # Initialize elements;
    #
    &AFT_OUTPUT::initElements();
}

# processFile(fname) - Locate, open and process the supplied file.
#
sub processFile {
  my($fname) = @_;
  local *IN;
  if (!open(*IN, $fname)) {
    $fname .= ".aft";		# maybe we just got a a base name?
    open(*IN, $fname) || ((warn "Can't open $fname: $!\n"), return -1);
  }
  
  # Do that voodoo that you do so well.
  #
  print (STDERR "\nProcessing $fname.\n[") if $verbose;
  &processFH_File(*IN, $fname);
  
  # Done with it, so close it.
  #
  close (*IN);
  print (STDERR "]\nFinished processing $fname.\n") if $verbose;
  return 0;
}


# processFH_File (fh,fname) - Process the supplied file by the handle.
#
sub processFH_File {
  my($fh, $fname) = @_;
  my $lcnt  = 0;		# line count

 LINE:
  while (<$fh>) {
    $lcnt++;
    chop;
    &doString($_, $fname, $lcnt);
  }
}


##### Functions
#


# Handle comments and comment commands
#
sub handleComments {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  # Handle Strike lines (X---)
  #
  /^X-{3,}([^\-].*)/ && do {
    &output($AFT_OUTPUT::elem{'StrikeLine'}."\n",line => $1);
    return 1;
  };

  # Handle comments and comment commands (pragmas).
  #

  /^[C\#]-{3,}([^\-].*)/ && do {
      # See if there is stuff we need to pass directly through the filters.
      # #---PASS-'ID' text
      #
      $1 =~ /PASS-(\w+)\s+(.*)/ && do {
	  if ($AFT_OUTPUT::elem{'ID'} eq $1) {
	      &output($2);
	  } 
	  return 1;
      };
  
      # Set a pragma variable..
      # #---SET[-ID] var=value
      #
      $1 =~ /SET(\s?|-\w+)\s*(\w+)\s*=\s*(.*)/ && do {
	  if ("-$AFT_OUTPUT::elem{'ID'}" eq $1) {
	      $AFT_OUTPUT::pragma_postvar{$2} = $3;
	  } else {
	      $pragma_prevar{$2} = $3;
	  }
	  return 1;
      };
      
    # See if we need to adjust tabstop.
    # #---TABSTOP=N
    #
    $1 =~ /TABSTOP=(\d+)/ && do {
      $tabstop = $1;
      print (STDERR "\n[$fname($lcnt): TABSTOP set to $tabstop spaces.]\n");
      return 1;
    };

    &output($AFT_OUTPUT::elem{'CommentLine'}."\n",line => $1);
    return 1;			# regular comment
  };

  return 0;			# no comments encountered
}

# Handle *Title, *Author and preamble output.
#
sub handleTitlePreamble {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  # *Title:
  #
  /^\*Title:\s*/ && do {
    $title = &filter($');
    return 1;
  };

  # *Author:
  #
  /^\*Author:\s*/ && do {
    $author = &filter($');
    return 1;
  };

  # Output the preamble if we have been holding on to it.
  #
  if ($holdingPreamble) {
    $holdingPreamble = 0;
    &output($AFT_OUTPUT::filePreamble."\n",
	    title => $title, author => $author, version => $version);
    # Now print out title and author if they were collected.
    # If *Title and *Author were the first two lines in the document,
    # then we held the preamble until they were collected.
    # Else we assume that they are not available, so we just print
    # the preamble.
    if ($title) {
      &output($AFT_OUTPUT::elem{"Title"}."\n", title => $title);
    }
    if ($author) {
      &output($AFT_OUTPUT::elem{"Author"}."\n", author => $author);
    }
  }
  return 0;			# if writing preamble, then we didn't handle
				# anything, so just continue.
}

#  Handle *Insert:, *Include:, *File:,*See File and table of contents.
#
sub handleIncludes {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  /^\*(Insert|See File|Include|File):\s*/ && do {
    &processFile($');
    return 1;
  };
    
  # *TOC:  (table of contents)
  #
  /^\*(TOC)/ && do {
    # If there is no automatic table of contents markup, then generate
    # an AFT style markup.
    if ($AFT_OUTPUT::elem{$1} eq '') {
      &generateTOC($fname);
    } else {
      &output($AFT_OUTPUT::elem{$1}."\n");
    }
    return 1;
  };
  return 0;
}

# Handle *Image: and it's variations.
#
sub handleImage {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  /^\*(Image|Image-left|Image-center|Image-right):\s*(\S+)/ && do {
    &output($AFT_OUTPUT::elem{$1}."\n", image =>$2);
    return 1;
  };
  return 0;
}

# Handle ------
#
sub handleRuler {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  /^\-{4,}/ && do {
    &output($AFT_OUTPUT::elem{'HorizontalLine'}."\n");
    return 1;
  };

  return 0;
}

# Handle *, **, ***, **** (sections) and
# ^*, ^**, ^***, ^**** (sections referencing TOC)
#
sub handleSections {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  /^(\^*\*{1,4})\s*(.+?)\s*$/ && do {
#    my($sname) = &filter($');
      my($sname) = $2;
    if ($mode->{haveSections} eq 0) {
      &output($AFT_OUTPUT::elem{'BeginSectLevel1'}."\n");
      $mode->{haveSections} = 1;
    }
    &enterSection(length $1);
      
    $sectionNumber->incr(length $1);
    my $number = $sectionNumber->dotted();
    my $full_sname = $sname;
    $full_sname = "$number. $sname" if $autonumber;
 
    print (STDERR "]\n[$full_sname ") if $verbose;

    # print section name
    #
    &output($AFT_OUTPUT::elem{$1}."\n", section => $sname, 
	    text => &filter($full_sname), number => $number);
    
    # Save the section for the TOC file.
    #
    my($level) = $1;
    $level =~ tr/*^/\t/d;
    
#    $tocout && (print ($tocout "$level"."* {-$sname\@$number-}\n"));
    $tocout && (print ($tocout "$level"."* {-$full_sname\@$sname-}\n"));
    return 1;
  };
    
  return 0;
}

# List Mode
#
sub handleLists {
  my $fname = shift; my $lcnt = shift; local($_) = @_;
    
  # Only do this if not in verbatim/quote mode and we parse one of the 
  # following:
  #	<tab>*
  #	<tab>[text]
  #	<tab>number.
  #	<tab>number)
  #     <tab>#)
  #     <tab>#.
  #
  (!$mode->{inVerbatim} && !$mode->{inQuote} && 
   (/^(\t{1,})(\*|\[[^\]]+\]|\#[.\)]|\d+[.\)])/)) && do {
     my $rest_of_line = $';
     my $list = '';
     my ($le, $name);
     my $newLevel = length($1);
     my $curListLevel = scalar @listStack;
     my $currentList = '';
     
     if ($curListLevel gt 0) {
       $currentList = $listStack[$#listStack];
     }
     
     if ($2 =~ /^\*/) {
       $list = 'Bullet';
       $le = &prepareOutput($AFT_OUTPUT::elem{'BulletListElement'});
     } elsif ($2 =~ /^\[([^\]]*)\]/) {
       $name = $1,
       $list = 'Named', 		
       $le = &prepareOutput($AFT_OUTPUT::elem{'NamedListElement'}, 
			    name => &filter($name));
     } else {
       $list = 'Numbered';
       $le = &prepareOutput($AFT_OUTPUT::elem{'NumberedListElement'});
     }
     # Are we nesting yet?
     #
     while ($curListLevel < $newLevel) {
       # Increase nest level
       #
       push(@listStack,$list);
       &endListElement();
       &output($AFT_OUTPUT::elem{'Start'.$list.'List'}."\n");

       $curListLevel++;
       $currentList = $list;
     } 
     while ($curListLevel > $newLevel) {
       # Retreat to a previous level
       #
       &endListElement();
       $currentList = pop(@listStack);

       &output($AFT_OUTPUT::elem{'End'.$currentList.'List'}."\n");

       $curListLevel--;
       $currentList = pop(@listStack);
       push(@listStack, $currentList);
     }
     if ($list ne $currentList) {
       # Changing horses... A new list type.
       #
       &endListElement();
       $currentList = pop(@listStack);

       &output($AFT_OUTPUT::elem{'End'.$currentList.'List'}."\n");
       push(@listStack,$list);
       $currentList = $list;
       &output($AFT_OUTPUT::elem{'Start'.$list.'List'}."\n");
     }
     &endListElement();

     $mode->{inListElement} = 1;
     &output($le);		# output element line
     &output(&filter($rest_of_line));
     return 1;
   };
    
  # Print a continuation of list element if in list mode and tabbed...
  #
  if (scalar(@listStack) && /^\t\s*/) {
    &output(' '.&filter($'));
    return 1;
  }

  &endListElement();
  return 0;
}

# Terminate list element.
#
sub endListElement {
  if ($mode->{inListElement}) {
    &output($AFT_OUTPUT::elem{'End'.$listStack[$#listStack].'ListElement'}."\n");
    $mode->{inListElement} = 0;
  }
}

# Handle centered text.
#
sub handleCenteredText {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  (!scalar(@listStack) && !$mode->{inVerbatim} && 
   !$mode->{inQuote} && /^\t{2,}/) && do {
    &resetStates();	
    &output($AFT_OUTPUT::elem{'Center'}."\n", center => &filter($'));
    return 1;
  };
    
  return 0;
}

# Handle quoted text.
#
sub handleQuotedText {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  (!$mode->{inVerbatim} && /^\t\#\s*/) && do {
    if (!$mode->{inQuote}) {		# if we aren't in quote mode yet...
	&resetStates();
	&output($AFT_OUTPUT::elem{'StartQuote'}."\n");
	$mode->{inQuote} = 1;
      }
    &output(&filter($')."\n");
    return 1;
  };
  return 0;
}

# Handle tables
#
sub handleTable {
  my $fname = shift; my $lcnt = shift; local($_) = @_;

  # If not in verbatim or quote mode, try table...
  #
  (!$mode->{inVerbatim} && !$mode->{inQuote} && /^\t\!/)  && do {
    my $ecnt;			# Number of elements.
    my @elements;
    my $ftype;

    # First thing is first... Are we in the table yet?
    #
    !$mode->{inTable} && do {
      &resetStates();		# start clean
      $mode->{inTable} = 1;
      
      # Don't really start the table yet. We need to know how many
      # columns we will be dealing with.  Expect table headers next
      # time through.
      #
      $mode->{needTableHeaders} = 1;
      $' =~ /([^\!]*)/;
      
      # The first thing we got was a caption. Save it for later.
      #
      $tableCaption = &filter($1);
      
      return 1;
    };
    
    # Separator line !--------!
    #
    if ($' =~ /[\-]+!(\r|$)/) {
      return 1;
    }

    # We should be in Table mode now. The first thing we should do
    # is split up columns into individual elements. Ignore bogus
    # trailing column. If we got less than 2 elements, this ain't no
    # table!
    #
    if (($ecnt = (@elements = split ("!", $', 100)) - 1) < 2) {
      print (STDERR 
	     "\n$fname($lcnt): Weirdness in a table... not enough columns.\n");
      return 1;
    }
    
    # Okay, if this is the 2nd time through then we are looking for
    # table headers...
    #
    if ($mode->{needTableHeaders}) {
      # We got the column count ($ecnt) above, so we assume that
      # it will stay consistent. If not, that's someone else's
      # problem.
      #
      
      &output($AFT_OUTPUT::elem{'StartTable'}."\n", columns =>$ecnt);
      &output($AFT_OUTPUT::elem{'TableCaption'}."\n", caption => $tableCaption);
      $mode->{needTableHeaders} = 0;	# don't need them anymore
      $ftype = $AFT_OUTPUT::elem{'TableHeader'}; # short hand 
    } else {
      $ftype = $AFT_OUTPUT::elem{'TableElement'}; # short hand
    }
    
    &output($AFT_OUTPUT::elem{'TableRowStart'});
    # Now loop through each column element and spit it out.
    #
    my $item;
    foreach $item (@elements) {
      &output($ftype, stuff => &filter($item)) if $item;
    }
    
    # End of Table Row
    #
    &output($AFT_OUTPUT::elem{'TableRowEnd'}."\n");
    return 1;
  };
  return 0;
}


# Handle verbatim issues.
#
sub handleBlockedVerbatim {
  my $fname = shift; my $lcnt = shift; local($_) = @_;
    if ($mode->{blockedVerbatim} || $mode->{filteredVerbatim}) {
	&handleVerbatim($fname,$lcnt,$_);
	return 1;
    }
    return  0;			# drop thru
}

sub handleVerbatim {
  my $fname = shift; my $lcnt = shift; local($_) = @_;
    
  # Check to see if we should get out of blocked/filtered verbatim mode.
  #
  /^\^>>/ && do {
    # Get out of blocked and filtered verbatim mode
    &resetStates();
    return 1;
  };
  
  # Verbatim Text (and yes, even Quoted Text continuations)
  #
  (/(^\t|^\^\<\<\w*)/ || 
   $mode->{blockedVerbatim} || $mode->{filteredVerbatim}) && do {
    # First, are we starting fresh?
    #
    (!$mode->{inVerbatim} && !$mode->{inQuote}) && do {
      &resetStates();		# start clean
      $mode->{inVerbatim} = 1;
      
      # We are just entering the blocked verbatim mode, 
      # so just remember this and don't print this line.
      #
      if ($1 =~ /\^\<\</) {
	if ($' =~ /[Ff]/) {
	  $mode->{filteredVerbatim} = 1;
	  &output($AFT_OUTPUT::elem{'StartFilteredVerbatim'}."\n");
	} else {
	  $mode->{blockedVerbatim} = 1;
	  &output($AFT_OUTPUT::elem{'StartBlockedVerbatim'}."\n");
	}
	return 1;
      }			# else
      &output($AFT_OUTPUT::elem{'StartVerbatim'}."\n");
    };
    
    # In quote mode? Just kick out filtered text.
    #
    &output(&filter($')."\n"), return 1 if $mode->{inQuote};
    
    # We must be in a verbatim mode...
    #
    
    # Kill the first tab
    #
    s/^\t//g if (!($mode->{blockedVerbatim} || $mode->{filteredVerbatim}));
    
    
    # Now change all tabs to 8 spaces.
    #
    s/\t/        /g if (!$mode->{blockedVerbatim});

    # Can we really filter FilterVerbatim?
    #
    if ($mode->{filteredVerbatim} && 
	($AFT_OUTPUT::elem{'FullFilterFilteredVerbatim?'} =~ /[Yy]/)) {
      &output(&filter($_)."\n");
    } else {
      if ($AFT_OUTPUT::elem{'PreFilterVerbatim?'} =~ /[Yy]/) {
	&output(&AFT_OUTPUT::prefilter($_)."\n");
      } else {
	&output($_."\n");	# output 'as is'
      }
    }
    return 1;
  };

  return 0;
}


# Generate and possibly include a table of contents file.
#
sub generateTOC {
  # Try and open a table of contents file
  #
  my ($fname) = @_;
  my $tocfile = $fname."-TOC";
  print (STDERR "\t Looking for a table of contents file...\n") if $verbose;
  
  open(TOCIN, $tocfile) && do {
    # Read it in.
    #
    print (STDERR "\t Reading table of contents from $tocfile...")if $verbose;
    &processFH_File(*TOCIN, $tocfile);
    close(TOCIN);
    &output("\n\n");
    print (STDERR "Done.\n") if $verbose;
  };
  if ($verbose) {
    print (STDERR "\t Generating a new $tocfile.\n");
    print (STDERR "\t You may want to re-run aft again to include it if\n");
    print (STDERR "\t any sections were added or removed in your document.\n");
  }
  open(TOCOUT,">$tocfile");
  $tocout = *TOCOUT;
  print (TOCOUT "C--- AFT Table of Contents (auto generated)\n");
}


# filter(line) - processes line against macros and filters, returns filtered
# 	line.
#
sub filter {
  my($line) = @_;
  
  # Expand any prefilter pragma symbols.
  #
  #
  foreach my $key (keys(%pragma_prevar)) {
      my $val = $pragma_prevar{$key};
      $line =~ s/\%$key\%/$val/g;
  }
  
  # Now do the prefilters substitutions.
  #
  $line = &AFT_OUTPUT::prefilter($line);
  
  # Now take care of the font tricks.
  #
 FONT: {
    # First, protect ||  __  and ~~ - 19sep96
    # 
    $line =~ s/__/%UnDeRLiNE%/g;
    $line =~ s/\|\|/%PiPe%/g;
    $line =~ s/\~\~/%TiLdE%/g;
    $line =~ s/''''/%QuOtE%/g;
    
    # Do the paragraph modes first.  We only enter such a mode when a
    # start sequence is at the beginning of the line and no closure is
    # available for that line (no other same type sequences).
    #
    $line =~ /^_[^\s_][^_]+$/ && do {
      $paragraph->{strong} = 1;
      $line =~ s/^_/$AFT_OUTPUT::elem{'StartStrong'}/;
      last FONT;
    };
    ($paragraph->{strong} && $line =~ /_$/) && do {
      $paragraph->{strong} = 0;
      $line =~ s/_$/$AFT_OUTPUT::elem{'EndStrong'}/;
      last FONT;
    };
    $line =~ /^\~[^\s\~][^\~]+$/ && do {
      $paragraph->{small} = 1;
      $line =~ s/^\~/$AFT_OUTPUT::elem{'StartSmall'}/;
      last FONT;
    };
    ($paragraph->{small} && $line =~ /\~$/) && do {
      $paragraph->{small} = 0;
      $line =~ s/\~$/$AFT_OUTPUT::elem{'EndSmall'}/;
      last FONT;
    };
    $line =~ /^\|[^\s\|][^\|]+$/ && do {
      $paragraph->{teletype} = 1;
      $line =~ s/^\|/$AFT_OUTPUT::elem{'StartTeletype'}/;
      last FONT;
    };
    ($paragraph->{teletype} && $line =~ /\|$/) && do {
      $paragraph->{teletype} = 0;
      $line =~ s/\|$/$AFT_OUTPUT::elem{'EndTeletype'}/;
      last FONT;
    };
    $line =~ /^''/ && do {
      if (!($line =~ /^''.+''/)) {
	$paragraph->{emphasis} = 1;
	$line =~ s/^''/$AFT_OUTPUT::elem{'StartEmphasis'}/;
	last FONT;
      }
    };
    ($paragraph->{emphasis} && $line =~ /''$/) && do {
      $paragraph->{emphasis} = 0;
      $line =~ s/''$/$AFT_OUTPUT::elem{'EndEmphasis'}/;
      last FONT;
    };
  } 

  # Now do the single line sequences... 
  # Teletype takes precedence over emphasis and strong.
  #
  $line =~ s/\|([^\|]+)\|/$AFT_OUTPUT::elem{'StartTeletype'}$1$AFT_OUTPUT::elem{'EndTeletype'}/g;
  $line =~ s/\~([^\~]+)\~/$AFT_OUTPUT::elem{'StartSmall'}$1$AFT_OUTPUT::elem{'EndSmall'}/g;
  $line =~ s/_([^_]+)_/$AFT_OUTPUT::elem{'StartStrong'}$1$AFT_OUTPUT::elem{'EndStrong'}/g;
  $line =~ s/''(.*?)''/$AFT_OUTPUT::elem{'StartEmphasis'}$1$AFT_OUTPUT::elem{'EndEmphasis'}/g;

  # Now fix _ ~ and |   - 19sep96
  # 
  $line =~ s/%UnDeRLiNE%/_/g;
  $line =~ s/%PiPe%/\|/g;
  $line =~ s/%TiLdE%/\~/g;
  $line =~ s/%QuOtE%/''/g;
  
  # Handle hyper links
  #
  $line = &handleLinks($line);

  # Handle footnote references
  #
  #$line = &handleNotes($line);

  # Post-filter now. Pass its return up to the caller of filter().
  #
  &AFT_OUTPUT::postfilter($line);
}


sub handleNotes {
  my($line) = @_;
  # look for [* ... ] or [# .. ]
  # $line =~ s/\[([#\*])(.*)\]
  return $line;
}

# Handle the various types of links we can regex.
#
sub handleLinks {
  my($line) = @_;

  BEGIN {
    # Construct the rather complex regex for simple http addresses.
    # We use a BEGIN block because we only want to do it once.
    my $_safe = q/$\-_@.&+~/;
    my $_extra = q/#!*,/;
    my $_alpha = q/A-Za-z/;
    my $_digit = q/0-9/;
    my $_esc = q/%/;
    my $_seg = "[$_alpha$_digit$_safe$_extra$_esc]+";
    my $_path = "(?:/$_seg)+";
    my $_params = "$_seg";
    my $_name = "[$_alpha$_digit][$_alpha$_digit\-]+";
    my $_hostname = "$_name(?:\\.$_name)+";
    my $_port = ":[0-9]+";
    $AFT::httpaddr = "(?:ftp|file|https?)://$_hostname(?:$_port)?(?:$_path)?";
  }

  # Handle plain old URLs terminated by brackets, spaces, periods and 
  # generally any character not listed in $_seg
  #
  $line =~ s/(^|[\s\(])($AFT::httpaddr)/
   "$1".(&prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$2", text => "$2")."$3")/eg;

#  $line =~ s/(^|[\s\(])((ftp|mailto)\:[^\s\)]+)/
#    "$1".&prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$2", text => "$2")/eg;

  # Handle old AFT style Links
  $line =~ s/{\+((http|https|file|ftp|mailto)\:[^{}]+)\+}/
    &prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$1", text => "$1")/eg;
  
  $line =~ s/{\-([^\@{}]+)[\@]((http|https|file|ftp|mailto)\:[^{}]+)\-}/
    &prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$2", text => "$1")/eg;
  
  $line =~ s/{\+\:([^{}]+)\+}/
    &prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$1", text => "$1")/eg;
  
  $line =~ s/{\-([^\@{}]+)[\@]\:([^{}]+)\-}/
    &prepareOutput($AFT_OUTPUT::elem{'URL'},target => "$2", text => "$1")/eg;
  
  $line =~ s/{\+([^{}]+)\+}/
    &prepareOutput($AFT_OUTPUT::elem{'InternalReference'},
		   target => "$1", text => "$1")/eg;
  
  $line =~ s/{\-([^\@{}]+)\-}/
    &prepareOutput($AFT_OUTPUT::elem{'InternalReference'},
		   target => "$1", text => "$1")/eg;
  
  $line =~ s/{\-([^\@{}]+)[\@]([^{}]+)\-}/
    &prepareOutput($AFT_OUTPUT::elem{'InternalReference'},
		   target => "$2", text => "$1")/eg;
  
  $line =~ s/\}\+([^{}]+)\+\{/
    &prepareOutput($AFT_OUTPUT::elem{'Target'},
		   target => "$1", text => "$1")/eg;
  
  $line =~ s/\}\-([^{}]+)\-\{/
    &prepareOutput($AFT_OUTPUT::elem{'Target'},
		   target => "$1", text =>$AFT_OUTPUT::elem{'NBSPACE'})/eg;
  return $line;
}

# enterParagraph () - enter paragraph mode.
#
sub enterParagraph {
  $mode->{inParagraph} = 1;
  &output($AFT_OUTPUT::elem{'StartParagraph'}."\n");
}

# resetParagraph () - reset paragraph mode.
#
sub resetParagraph {
  print (STDERR ".") if $verbose;
  &output($AFT_OUTPUT::elem{'EndSmall'}."\n") if $paragraph->{small};
  &output($AFT_OUTPUT::elem{'EndStrong'}."\n") if $paragraph->{strong};
  &output($AFT_OUTPUT::elem{'EndEmphasis'}."\n") if $paragraph->{emphasis};
  &output($AFT_OUTPUT::elem{'EndTeletype'}."\n") if $paragraph->{teletype};
  &output($AFT_OUTPUT::elem{'EndParagraph'}."\n") if $mode->{inParagraph};
  $paragraph->{small} = 0;
  $paragraph->{strong} = 0;
  $paragraph->{emphasis} = 0;
  $paragraph->{teletype} = 0;
  $mode->{inParagraph} = 0;
}

# enterSection(level) - If we are nesting into a subsection, just keep track.
# Otherwise, unwind the stack of sections (outputing EndSection for each).
# Why keep a stack instead of a running level index? Unwinding can get tricky
# if the user does something like:
#  * Section
#  *** Section
#  ** Section 
#  **** Section
#  * Section
#
sub enterSection {
	
  BEGIN {
    # These keys are new. Don't choke if they don't exist. Don't
    # whine yet, just ignore them for now.
    #
    foreach my $name (qw(BeginSectLevel1 BeginSectLevel2 BeginSectLevel3
                         BeginSectLevel4
			 EndSectLevel1 EndSectLevel2 EndSectLevel3
                         EndSectLevel4)) {
      if (!defined($AFT_OUTPUT::elem{$name})) {
	$AFT_OUTPUT::elem{$name} = "";
      }
    }
  }
	
  my ($newsectlevel) = @_;
  &resetParagraph();

  # Do the section and section "level" mode popping...
  #
  if ($mode->{currentSectionLevel} ge $newsectlevel) {
    while (@sectionStack gt 0 and $sectionStack[-1] ge $newsectlevel) {
      $mode->{currentSectionLevel} = pop(@sectionStack); 
      &output($AFT_OUTPUT::elem{'EndSect'.$mode->{currentSectionLevel}}."\n");
      if ($mode->{currentSectionLevel} gt 3 and $newsectlevel le 3) {
        &output($AFT_OUTPUT::elem{'EndSectLevel4'}."\n");
      } elsif ($mode->{currentSectionLevel} gt 2 and $newsectlevel le 2) {
	&output($AFT_OUTPUT::elem{'EndSectLevel3'}."\n");
      } elsif ($mode->{currentSectionLevel} gt 1 and $newsectlevel le 1) {
	&output($AFT_OUTPUT::elem{'EndSectLevel2'}."\n");
      }
    }
  }

  # Do the section and section "level" pushing...
  #
  if (($mode->{currentSectionLevel} le 3) and ($newsectlevel gt 3)) {
    &output($AFT_OUTPUT::elem{'BeginSectLevel4'}."\n");
  } elsif (($mode->{currentSectionLevel} le 2) and ($newsectlevel gt 2)) {
    &output($AFT_OUTPUT::elem{'BeginSectLevel3'}."\n");
  } elsif (($mode->{currentSectionLevel} le 1) and ($newsectlevel gt 1)) {
    &output($AFT_OUTPUT::elem{'BeginSectLevel2'}."\n");
  }	
  $mode->{currentSectionLevel} = $newsectlevel;
  push(@sectionStack, $newsectlevel);
}

# resetStates () - reset our state to near-normal (paragraph mode is not
# 	affected by this subroutine).
#
sub resetStates {
  # Are we in the middle of a table?
  #
  $mode->{inTable} && (!$mode->{needTableHeaders} && 
		       &output($AFT_OUTPUT::elem{'EndTable'}."\n"));
  
  # Since we can only be in one mode at a time, make like a big switch...
  #
 MODE: {
    &output($AFT_OUTPUT::elem{'EndBlockedVerbatim'}."\n"), last MODE 
      if $mode->{blockedVerbatim};
    &output($AFT_OUTPUT::elem{'EndFilteredVerbatim'}."\n"), last MODE 
      if $mode->{filteredVerbatim};
    &output($AFT_OUTPUT::elem{'EndVerbatim'}."\n"), last MODE 
      if $mode->{inVerbatim};
    &output($AFT_OUTPUT::elem{'EndQuote'}."\n"), last MODE 
      if $mode->{inQuote};

    &endListElement();
    while (my $list = pop(@listStack)) {
      &output($AFT_OUTPUT::elem{'End'.$list.'List'}."\n");
    }
  }
  # Now just reset all the variables.
  # 
  $mode->{needTableHeaders} = 0;
  $mode->{inTable}= 0;
  $mode->{inQuote} = 0;
  $mode->{inVerbatim} = 0;
  $mode->{blockedVerbatim} = 0;
  $mode->{filteredVerbatim} = 0;
}


# Print out a line of text (possibly with substitutions).
#
# Usage:  output(text [, key => value]..);
#
# %key% is replaced by value everywhere in text.
#
sub output {
  print &prepareOutput(@_);
}

# Prepare a line of text for output.
#
# Usage:  prepareOutput(text [, key => value]..);
#
# %key% is replaced by value everywhere in text.
#
sub prepareOutput {
  my $str = shift;
  my ($var, $val);

  while (@_) {
    $var = shift;
    $val = shift;
    $str =~ s/\%$var\%/$val/g;
  }

  # Expand the document defined pragma variables.
  #
  foreach my $key (keys(%AFT_OUTPUT::pragma_postvar)) {
      $val = $AFT_OUTPUT::pragma_postvar{$key};
      $str =~ s/\%$key\%/$val/g;
  }
  return $str;
}


# Numbered Heads Initializaton
#
BEGIN {
  
  package Autonum;
  
  
  # usage: 
  #
  #   $num = Autonum->new;
  #   foreach  (qw/ 1 2 2 3 3 1 2 3 1/ ) {
  #     $num->incr($_, '.');
  #     print $num->dotted() , ':', "\n";
  #   }
  
  sub new {
    my ($class) = @_;
    my $self = { stack => [] };
    return bless $self, $class;
  }
  
  # returns the counter for current $level
  sub incr {
    my ($self, $level) = @_;
    # truncate and reset child numbers
    splice @{$self->{stack}}, $level;
    # 0 index
    return ++$self->{stack}->[$level - 1];
  }
  
  sub dotted {
    my ($self, $dot) = @_;
    $dot ||= '.';		# optional
    #                 v--- in case we skip levels, put a 0 in the gap.
    return join($dot, map {$_ || '0'} @{$self->{stack}});  #  . $dot;
  }
  
  
  # just the numbers, no punc
  sub list {
    my $self = shift;
    #      v--- in case we skip levels, put a 0 in the gap.
    return map {$_ || '0'} @{$self->{stack}};
  }
}
  
return 1;
