/*
 * - Empty and line-drawing cells might span, but that's not handled.
 * - Need to handle table cell spacing.  But that's a table attribute in
 *   HTML and a column attribute in tbl.
 * - Perhaps tags to use for headers should be read from file to allow
 *   better user-level style configuration.
 * - SetCFA() doesn't do much with center/adjust interaction.
 * - Need a FileInfo() equivalent here for error messages.
 * - Need to set initial style when the body starts?
 * - Wrapper needs function to call during loop and termination value
 *   to look for.
 * - Size changes - ignore in title?
 * - Should be able to override the fonts file, both at compile time, and
 *   from the command line.  Of course, since fonts are currently ignored,
 *   this doesn't matter much yet.
 */

/*
 * tc2html - read troffcvt output, convert to HTML
 *
 * Syntax: tc2html [-D] [-E] [-T title] [file] ...
 *
 * -D	enable debug code
 * -E	echo tokens as they are read
 * -T	specify document title
 *
 * The main trick for this postprocessor is to get troffcvt to write
 * output that's useful for determining where HTML entities like titles,
 * paragraphs, and lists begin and/or end.  For figuring out these
 * higher-level constructs, it doesn't do much good to know just
 * low-level things like breaks and font changes.  The way to do this
 * is to use an action file with troffcvt that maps macro calls to
 * "\html xxx" lines, where xxx is an HTML entity name.
 *
 * 03 Jan 97	Paul DuBois	dubois@primate.wisc.edu
 *
 * 03 Jan 97
 * - Created
 */

#include	<stdio.h>
#include	<ctype.h>
#ifndef	STRING_H
#define	STRING_H <string.h>
#endif
#include	STRING_H

#include	"portlib.h"
#include	"etm.h"
#include	"memmgr.h"
#include	"tokenscan.h"

#include	"tcgen.h"
#include	"tcunix.h"
#include	"tcread.h"
#include	"tctbl.h"

#include	"tc2html.h"



static void	WriterInit (void);
static int	BeginFile (void);

static void	ReadFile (void);
static void	ControlLine (void);
static void	SetFont (char *name, short inDisplay);
static void	SetCFA (short newCenter, short newFill, short newAdjust);
static void	PlainText (void);
static void	SpecialText (void);
static void	ResetTabs (void);
static void	SetTabStop (long pos, char type);

static int	ReadSpecials (char *filename);
static SpChar	*LookupSpecial (char *name);
static KeyMap	*LookupKey (char *p);


static void	DoAnchor (short newAnchor, short argc, char **argv);
static void	DoElement (short newElt, short argc, char **argv);
static void	BeginDocHead (void);
static void	BeginDocBody (void);
static void	TerminateDefList (void);
static void	FlushTOC (void);
static void	PushElt (short eltType, short misc);
static void	PopElt (void);
static void	AddBreak (void);
static void	AddPara (void);
static void	AddLine (long length, short direction);
static void	AddStr (char *p);
static void	AddLitStr (char *p);
static void	AddChar (char c);
static void	AddTitleChar (char c);
static void	AddHeaderChar (char c);
static void	AddTOCChar (char c);
static void	StrAddStr (char *buf, int size, char *str);

static void	TableBegin (short argc, char **argv);
static void	TableEnd (void);
static void	TableRowBegin (short argc, char **argv);
static void	TableRowEnd (void);
static void	TableRowLine (short argc, char **argv);
static void	TableCellInfo (short argc, char **argv);
static void	TableCellBegin (short argc, char **argv);
static void	TableCellEnd (void);
static void	TableEmptyCell (void);
static void	TableSpannedCell (void);
static void	TableCellLine (short argc, char **argv);
static void	CellBegin (char type, short vspan, short hspan, char vadjust);

static void	ControlOut (char *p, short brkBefore, short brkAfter);
static void	TextStrOut (char *p);
static void	TextChOut (int c);
static void	HTMLChOut (char *p);
static void	StrOut (char *p);
static void	ChOut (int c);
static void	Flush (void);
static char	*CharToHTML (int c);
static int	AllowOutput (int yesno);

static int	ReadFonts (char *filename);
static Font	*LookupFont (char *name);



static KeyMap	keyMap[] =
{
	{ "anchor-href",	keyAnchor,	anchorHref },
	{ "anchor-name",	keyAnchor,	anchorName },
	{ "anchor-toc",		keyAnchor,	anchorTOC },
	{ "anchor-end",		keyAnchor,	anchorEnd },
	{ "title",		keyElement,	eltTitle },
	{ "para",		keyElement,	eltPara },
	{ "display",		keyElement,	eltDisplay },
	{ "display-end",	keyElement,	eltDisplayEnd },
	{ "display-indent",	keyElement,	eltDisplayIndent },
	{ "list",		keyElement,	eltList },
	{ "list-end",		keyElement,	eltListEnd },
	{ "list-item",		keyElement,	eltListItem },
	{ "header",		keyElement,	eltHeader },
	{ "header-end",		keyElement,	eltHeaderEnd },
	{ "blockquote",		keyElement,	eltBlockquote },
	{ "blockquote-end",	keyElement,	eltBlockquoteEnd },
	{ "definition-term",	keyElement,	eltDefTerm },
	{ "definition-desc",	keyElement,	eltDefDesc },
	{ "shift-right",	keyElement,	eltShiftRight },
	{ "shift-left",		keyElement,	eltShiftLeft },
	{ NULL,			keyUnknown,	0 }
};

static int	allowOutput = 1;	/* zero if output is throttled */

static long	resolution = 432;

/*
 * cCount is the number of characters on the current output line.  It's
 * incremented at the lowest output level, in ChOut().  tCount is the number
 * of text (non-tag) characters on the line.  It's used to determine when to
 * ignore leading spaces in TextChOut().  (Ignore when character to write
 * is a space and tCount is zero.)  etCount is the "effective" number of
 * text characters on a line.  For instance, ">" is written as "&gt;" -- this
 * increments tCount by 4, but etCount by only 1.  etCount is needed for
 * doing spacing for tabstops.
 */

static int	cCount = 0;
static int	tCount = 0;
static int	etCount = 0;

static Font	*fontList = (Font *) NULL;
static Font	*curFont = (Font *) NULL;

static SpChar	*spList = (SpChar *) NULL;

static int	debug = 0;
static int	echo = 0;


/*
 * docPart indicates which part of the document is being collected:
 * - inHead = document HEAD part
 * - inBody = document BODY part
 * - inNone = uninitialized
 *
 * bodyElt indicates which body element is being collected.  It's
 * irrelevant unless docPart is inBody.
 *
 * If output is ready to be written while docPart is inNone, the part is
 * initialized to inBody, using the default element of "generic paragraph."
 */

static short	docPart = inNone;
static short	bodyElt = eltUnknown;

static Elt	eltStack[maxElt];	/* element stack */
static short	eltTop = -1;		/* initially stack is empty */

static short	listDepth = 0;		/* list nesting depth */
static short	displayFlag = 0;	/* non-zero if in display */
static short	displayIndent = 0;	/* display indent */
static short	blockquoteFlag = 0;	/* non-zero if in block quote */
static short	anchorFlag = 0;		/* non-zero if in <a xxx> (turned on */
					/* by ahref, aname, or atoc) */
/*
 * This is non-NULL is an an href anchor is  pending (collected from the
 * input but not yet output).  This is used to "float" an anchor past any
 * whitespace that may follow it to the next non-white character.  (The
 * reason this is done is to avoid leading underlined whitespace, which is
 * ugly.)
 */

static char	*pendingAnchor = (char *) NULL;

/*
 * Table-of-contents stuff
 */

static TOC	*tocHead = (TOC *) NULL;	/* list of TOC entries */
static TOC	*tocTail = (TOC *) NULL;
static char	tocBuf[maxTOC] = "";		/* text of current TOC entry */
static short	tocCount = 0;			/* TOC entry index */
static short	tocLevel = 0;			/* level of current entry */

/*
 * Title text is saved in a buffer because it's used both for the document
 * title in the document head and for the initial heading in the document
 * body.  When tc2html detects that the document head -> document body
 * transition has been crossed, it writes the document head, writes the
 * beginning of the document body, and writes the initial header.
 *
 * Text intended for other structural elements of the body is written to the
 * output as soon as it has been seen.
 *
 * cmdLineTitle is non-null if a -T option is given on the command line to
 * override any document title found in the document.  (Mainly useful when
 * NO title is present in the document.)
 */

static char	title[maxTitle] = "";
static char	header[maxHeader] = "";
static char	*cmdLineTitle = (char *) NULL;

/*
 * Definition-list stuff.  Definition lists are terminated implicitly
 * by most structural markers except definition-list markers themselves.
 *
 * defListFlag values:
 * 0	not in a definition list
 * 1	collecting a term
 * 2	collecting a description
 *
 * emptyDefTerm is used during definition term collection to specify whether
 * or not any text has been found in the term.  If not, then a <BR> is written
 * when the term is closed to force a little whitespace.
 */

static short	defListFlag = 0;
static short	emptyDefTerm = 0;

/* right/left shift level */

static short	shiftLevel = 0;




/*
 * Per-table values
 */

static short	tblCols = 0;		/* number of columns in table */
static short	tblRows = 0;		/* number of rows in table */
static short	centerTbl= 0;		/* non-zero if table is centered */

/*
 * Per-cell values.
 */

static char	cellType[tblMaxCol];		/* cell type */
static short	cellVSpan[tblMaxCol];		/* cell vspan flag */
static short	cellHSpan[tblMaxCol];		/* cell hspan extent */
static char	cellVAdjust[tblMaxCol];		/* cell vertical adjust */
static short	cellBorder[tblMaxCol];		/* cell borders */

/*
 * Counters for keeping track of position within table.
 * tblColIdx counts horizontally during processing of \table-column-info
 * lines, during \table-cell-info lines, and while processing each row of
 * table data.  tblRowIdx counts rows of table data.
 */

static short	tblColIdx = 0;
static short	tblRowIdx = 0;



static short	adjust = adjLeft;	/* line adjustment mode */
static short	fill = 0;		/* non-zero if fill mode on */
static short	center = 0;		/* non-zero if centering on */
static short	underline = 0;		/* non-zero if underlining on */

static short	tabPos[maxTabStops];
static char	tabType[maxTabStops];
static short	tabCount = 0;


int
main (int argc, char *argv[])
{
int	inc;

	ETMInit (NULL);

	UnixSetProgPath (argv[0]);
	TCRSetOpenLibFileProc (UnixOpenLibFile);

	--argc;
	++argv;
	while (argc > 0 && argv[0][0] == '-')
	{
		inc = 1;
		if (strcmp (argv[0], "-D") == 0)
			debug = 0xffff;
		else if (strcmp (argv[0], "-E") == 0)
			echo = 1;
		else if (strncmp (argv[0], "-T", 2) == 0)
		{
			if (argv[0][2] != '\0')		/* -Ttitle */
				cmdLineTitle = &argv[0][2];
			else if (argc < 1)
				ETMPanic ("Missing title after -T");
			else
			{
				cmdLineTitle = argv[1];
				argc -= inc;
				argv += inc;
			}
		}
		else
			ETMPanic ("Unknown option: %s", argv[0]);
		argc -= inc;
		argv += inc;
	}

	if (debug || echo)
	{
		/* don't want output order mixed up by buffering */
		setbuf (stdout, (char *) NULL);
		setbuf (stderr, (char *) NULL);
	}

	WriterInit ();

	if (argc > 1)
		ETMPanic ("Only one input file allowed");

	if (argc == 0)		/* stdin */
		ReadFile ();
	else
	{
		if (freopen (argv[0], "r", stdin) == (FILE *) NULL)
			ETMMsg ("Cannot open: %s", argv[0]);
		else
			ReadFile ();
	}

	ETMEnd ();
	exit (0);
	/*NOTREACHED*/
}


static void
WriterInit (void)
{
	if (!ReadFonts (defaultFontFile))
		ETMPanic ("cannot find %s file", defaultFontFile);
	if (!ReadSpecials (specialCharFile))
		ETMPanic ("cannot find %s file", specialCharFile);
}


static int
BeginFile (void)
{
	(void) AllowOutput (1);
	ControlOut ("HTML", 1, 1);
	ControlOut ("!-- this file was generated by troffcvt and tc2html --",
									1, 1);
	/* initialize font info to initial font */
	/*SetFont ("R", 0);*/

	(void) AllowOutput (0);

	TCRInit ();

	return (1);
}


static void
ReadFile (void)
{
int	i;

	(void) BeginFile ();

	while (TCRGetToken () != tcrEOF)
	{
		if (echo)
		{
			ETMMsg ("class %d maj %d min %d <%s>",
				tcrClass, tcrMajor, tcrMinor, tcrArgv[0]);
			if (tcrClass == tcrControl || tcrClass == tcrSText)
			{
				for (i = 1; i < tcrArgc; i++)
					ETMMsg ("\t<%s>", tcrArgv[i]);
			}
		}
		switch (tcrClass)
		{
		case tcrControl:	ControlLine (); break;
		case tcrText:		PlainText (); break;
		case tcrSText:		SpecialText (); break;
		default:	ETMPanic ("ReadFile: unknown token %d <%s>",
							tcrClass, tcrArgv[0]);
		}
	}
	/*
	 * If document contains HEAD part only, write a fake paragraph
	 * to force HEAD flush
	 */
	if (docPart == inHead)
		DoElement (eltPara, 0, (char **) NULL);

	/*
	 * Terminate final font setting
	 * Close any dangling style attributes
	 * Close any dangling anchor
	 * Close any open definition list
	 * Undo any pending right shifts
	 * Close any open BODY elements
	 * Flush table of contents if there is one
	 * Close document BODY part
	 */
	if (docPart == inBody)
	{
		if (curFont != (Font *) NULL)
			StrOut (curFont->endTag);
		if (underline)
			ControlOut ("/UNDER", 1, 1);
		if (anchorFlag)
			DoAnchor (anchorEnd, 0, (char **) NULL);
		TerminateDefList ();
		while (shiftLevel-- > 0)
			ControlOut ("/UL", 1, 1);
		while (eltTop >= 0)
			PopElt ();
		SetCFA (0, 1, adjLeft);
		FlushTOC ();
		ControlOut ("/BODY", 1, 1);
	}
	ControlOut ("/HTML", 1, 1);
}


/*
 * Read special character list.  Each line has the character name
 * and the HTML string to write out for a given special character.
 */

static int
ReadSpecials (char *filename)
{
FILE	*f;
char	buf[bufSiz];
SpChar	*sp;
char	*name, *value;


	if ((f = TCROpenLibFile (filename, "r")) == (FILE *) NULL)
		return (0);
	while (TCRGetLine (buf, (int) sizeof (buf), f))
	{
		if (buf[0] == '#')
			continue;
		TSScanInit (buf);
		if ((name = TSScan ()) == (char *) NULL)	/* char name */
			continue;
		value = TSScan ();	/* NULL if missing */
		sp = New (SpChar);
		sp->spName = StrAlloc (name);
		/*
		 * If value missing, make NULL; this will cause writer
		 * to write "[[name]]" instead of dropping it.
		 */
		if (value == (char *) NULL)
			sp->spValue = (char *) NULL;
		else
			sp->spValue = StrAlloc (value);
		sp->spNext = spList;
		spList = sp;
	}
	(void) fclose (f);
	return (1);
}


static SpChar *
LookupSpecial (char *name)
{
SpChar	*sp;

	for (sp = spList; sp != (SpChar *) NULL; sp = sp->spNext)
	{
		if (strcmp (name, sp->spName) == 0)
			break;
	}
	return (sp);		/* NULL if not found */
}


static KeyMap *
LookupKey (char *p)
{
KeyMap	*kp;

	for (kp = keyMap; kp->name != (char *) NULL; kp++)
	{
		if (strcmp (p, kp->name) == 0)
			return (kp);
	}
	return ((KeyMap *) NULL);
}



static void
ControlLine (void)
{
KeyMap	*kp;
char	buf[bufSiz];
short	newCenter, newFill, newAdjust;
double	d;

	switch (tcrMajor)
	{
	default:
		ETMMsg ("ControlLine: bad control major code: %d <%s>",
						tcrMajor, tcrArgv[0]);
		break;
	case tcrCUnknown:
		/*
		 * Special \html controls are handled here
		 */
		if (strcmp (tcrArgv[0], "\\html") == 0)
		{
			kp = LookupKey (tcrArgv[1]);
			if (kp == (KeyMap *) NULL)
				ETMMsg ("unknown \\html key: %s", tcrArgv[1]);
			else
			{
				switch (kp->type)
				{
				default:
					ETMPanic ("ControlLine: logic error");
				case keyAnchor:
					DoAnchor (kp->subtype,
						tcrArgc - 2, &tcrArgv[2]);
					break;
				case keyElement:
					DoElement (kp->subtype,
						tcrArgc - 2, &tcrArgv[2]);
					break;
				}
			}
		}
		else
		{
			ETMMsg ("ControlLine: unknown control token: <%s>",
						tcrArgv[0]);
		}
		break;
	case tcrComment:
		break;
	case tcrSetupBegin:
		break;
	case tcrResolution:
		resolution = StrToLong (tcrArgv[1]);
		break;
	case tcrSetupEnd:
		(void) AllowOutput (1);
		break;
	case tcrInputLine:
		break;
	case tcrPass:
		/*
		 * \pass lines indicate information that should be passed
		 * straight through to the output
		 */
		Flush ();
		StrOut (tcrArgv[1]);
		Flush ();
		break;
	case tcrOther:
		break;
	case tcrBreak:
		AddBreak ();
		break;
	case tcrCFA:
		newCenter = center;
		newFill = fill;
		newAdjust = adjust;
		switch (tcrMinor)
		{
		case tcrNofill:
			newFill = 0;
			break;
		case tcrCenter:
			newCenter = 1;
			break;
		case tcrAdjFull:
		case tcrAdjLeft:
			newCenter = 0;
			newFill = 1;
			newAdjust = adjLeft;
			break;
		case tcrAdjRight:
			newCenter = 0;
			newFill = 1;
			newAdjust = adjRight;
			break;
		case tcrAdjCenter:
			newCenter = 1;
			break;
		}
		SetCFA (newCenter, newFill, newAdjust);
		break;
	case tcrPageLength:
		break;
	case tcrPageNumber:
		break;
	case tcrFont:
		if (tcrArgc < 2)
			ETMMsg ("missing font on \\font line");
		else
			SetFont (tcrArgv[1], displayFlag);
		break;
	case tcrPointSize:
		break;
	case tcrSpacing:
		break;
	case tcrLineSpacing:
		break;
	case tcrOffset:
		break;
	case tcrIndent:
		break;
	case tcrTempIndent:
		break;
	case tcrLineLength:
		break;
	case tcrTitleLength:
		break;
	case tcrTitleBegin:
		break;
	case tcrTitleEnd:
		break;
	case tcrSpace:
		/*
		 * For multi-line spacing, put out <BR>'s for all but last
		 * line.  If the spacing amount is less than a full line,
		 * ignore it.
		 *
		 * Assume 6 lines/inch in the calculation below.
		 */
		d = (StrToDouble (tcrArgv[1]) * 6.0 ) / resolution;
		while (d > 1)
		{
			AddBreak;
			--d;
		}
		if (d >= 1)
			AddPara();
		break;
	case tcrUnderline:
		break;
	case tcrCUnderline:
		break;
	case tcrNoUnderline:
		break;
	case tcrULineFont:
		break;
	case tcrBracketBegin:
		AddStr ("<BRACKET<");		/* ugly but makes it visible */
		break;
	case tcrBracketEnd:
		AddStr (">BRACKET>");
		break;
	case tcrBreakSpread:
		break;
	case tcrExtraSpace:
		break;
	case tcrLine:
		AddLine (StrToLong (tcrArgv[1]), tcrArgv[2][0]);
		break;
	case tcrMark:
		break;
	case tcrMotion:
		break;
	case tcrBeginOverstrike:
		AddStr ("<OVERSTRIKE<");	/* ugly but makes it visible */
		break;
	case tcrOverstrikeEnd:
		AddStr (">OVERSTRIKE>");
		break;
	case tcrBeginPage:
		AddPara ();
		break;
	case tcrZeroWidth:
		break;
	case tcrSpaceSize:
		break;
	case tcrConstantWidth:
		break;
	case tcrNoConstantWidth:
		break;
	case tcrNeed:
		break;
	case tcrEmbolden:
		break;
	case tcrSEmbolden:
		break;
	case tcrResetTabs:
		ResetTabs ();
		break;
	case tcrFirstTab:
		ResetTabs ();
		/* fall through... */
	case tcrNextTab:
		SetTabStop (StrToLong (tcrArgv[1]), tcrArgv[2][0]);
		break;
	case tcrHyphenate:
		break;
	case tcrDiversionBegin:
		break;
	case tcrDiversionAppend:
		break;
	case tcrDiversionEnd:
		break;
	case tcrTabChar:
		break;
	case tcrLeaderChar:
		break;
	case tcrTableBegin:
		TableBegin (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrTableEnd:
		TableEnd ();
		break;
	case tcrColumnInfo:
		/* ignore this */
		break;
	case tcrRowBegin:
		TableRowBegin (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrRowEnd:
		TableRowEnd ();
		break;
	case tcrRowLine:
		TableRowLine (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrCellInfo:
		TableCellInfo (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrCellBegin:
		TableCellBegin (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrCellEnd:
		TableCellEnd ();
		break;
	case tcrCellLine:
		TableCellLine (tcrArgc - 1, &tcrArgv[1]);
		break;
	case tcrEmptyCell:
		TableEmptyCell ();
		break;
	case tcrSpannedCell:
		TableSpannedCell ();
		break;
	}
}


static void
ResetTabs (void)
{
	tabCount = 0;
}


static void
SetTabStop (long pos, char type)
{
	if (tabCount >= maxTabStops)
	{
		ETMMsg ("too many tabstops");
		return;
	}
	/*
	 * Assume charsPerInch characters/inch
	 */
	tabPos[tabCount] = (pos * charsPerInch) / resolution;
	tabType[tabCount] = type;
	++tabCount;
}


/*
 * Set the font.  The tags to use depend on whether or not we're currently
 * in a display.
 */

static void
SetFont (char *name, short inDisplay)
{
Font	*fp;

	/* do nothing if font hasn't changed */

	if (curFont != (Font *) NULL
		&& strcmp (curFont->fontName, name) == 0
		&& curFont->inDisplay == inDisplay)
			return;

	if (docPart == inHead)		/* ignore title font changes */
		return;
	if (docPart == inNone)		/* part unknown - set to default */
		DoElement (eltPara, 0, (char **) NULL);

	if ((fp = LookupFont (name)) == (Font *) NULL)
	{
		ETMMsg ("no info for font <%s>", name);
		return;
	}

	/* turn off previous font, if one is known */

	if (curFont != (Font *) NULL)
	{
		if (!curFont->inDisplay)
			StrOut (curFont->endTag);
		else
			StrOut (curFont->dispEndTag);
	}

	/* switch to new font and turn it on */

	curFont = fp;
	curFont->inDisplay = inDisplay;
	if (!curFont->inDisplay)
		StrOut (curFont->beginTag);
	else
		StrOut (curFont->dispBeginTag);
}


static void
SetCFA (short newCenter, short newFill, short newAdjust)
{
char	buf[bufSiz];

	if (docPart == inNone)	/* just remember values, no output */
	{
		center = newCenter;
		fill = newFill;
		adjust = newAdjust;
		return;
	}
	buf[0] = '\0';
	if (center && !newCenter)	/* turn off centering */
		(void) strcat (buf, "</CENTER>");
	if (!center && newCenter)	/* turn on centering */
		(void) strcat (buf, "<CENTER>");

	center = newCenter;
	fill = newFill;
	adjust = newAdjust;

	if (buf[0] == '\0')		/* no output to write */
		return;

	switch (docPart)
	{
	default:
		ETMPanic ("SetCFA: logic error");
	case inHead:
		StrAddStr (header, sizeof (header), buf);
		break;
	case inBody:
		Flush ();
		StrOut (buf);
		Flush ();
		break;
	}
}



static void
PlainText (void)
{
	/*
	 * Line breaks are indicated explicitly by \break, so ignore
	 * linefeeds.
	 */
	if (tcrMajor != lf)
	{
		if (docPart == inNone)	/* part unknown - set to default */
			DoElement (eltPara, 0, (char **) NULL);
		AddChar (tcrMajor);
	}
}


static void
SpecialText (void)
{
SpChar	*sp;
char	buf[bufSiz];

	if (docPart == inNone)		/* part unknown - set to default */
		DoElement (eltPara, 0, (char **) NULL);
	if ((sp = LookupSpecial (&tcrArgv[0][1])) == (SpChar *) NULL
		|| sp->spValue == (char *) NULL)
	{
		sprintf (buf, "[[%s]]", &tcrArgv[0][1]);
		AddStr (buf);
	}
	else
		AddLitStr (sp->spValue);
}


/*
 * Have a new anchor tag
 */

static void
DoAnchor (short newAnchor, short argc, char **argv)
{
TOC	*tp;
char	buf[bufSiz];
short	misc;

	if (!allowOutput)
		return;

	if (newAnchor == anchorUnknown)	/* can't tell what to do, */
		return;			/* so do nothing */

	if (docPart == inNone)		/* part unknown - set to default */
		DoElement (eltPara, 0, (char **) NULL);

	switch (newAnchor)
	{
	case anchorHref:
		if (argc == 0)
		{
			ETMMsg ("problem: missing anchor link text");
			break;
		}
		if (anchorFlag)
		{
			ETMMsg ("problem: nested anchor, ignored");
			break;
		}
		sprintf (buf, "<A HREF=\"%s\">", argv[0]);
		switch (docPart)
		{
		default:
			ETMPanic ("DoAnchor: logic error 1");
		case inHead:
			StrAddStr (header, sizeof (header), buf);
			StrAddStr (header, sizeof (header), "\n");
			break;
		case inBody:
			pendingAnchor = StrAllocNP (buf);
			if (pendingAnchor == (char *) NULL)
				ETMPanic ("DoAnchor: cannot allocate anchor");
			break;
		}
		anchorFlag = 1;
		break;
	case anchorName:
		if (argc == 0)
		{
			ETMMsg ("problem: missing anchor name text");
			break;
		}
		if (anchorFlag)
		{
			ETMMsg ("problem: nested anchor, ignored");
			break;
		}
		sprintf (buf, "<A NAME=\"%s\">", argv[0]);
		switch (docPart)
		{
		default:
			ETMPanic ("DoAnchor: logic error 2");
		case inHead:
			StrAddStr (header, sizeof (header), buf);
			StrAddStr (header, sizeof (header), "\n");
			break;
		case inBody:
			Flush ();
			StrOut (buf);
			Flush ();
			break;
		}
		anchorFlag = 1;
		break;
	case anchorTOC:
		if (argc == 0)
		{
			ETMMsg ("problem: TOC entry with no level found, ignored");
			break;
		}
		if (anchorFlag)
		{
			ETMMsg ("problem: nested anchor, ignored");
			break;
		}
		/*
		 * Set up to collect the text and allocate a TOC strutcure.
		 * Terminate any current definition list; this assumes a
		 * TOC entry won't meaningfully occur in the middle of such.
		 */
		TerminateDefList ();
		tocBuf[0] = '\0';
		tp = (TOC *) AllocNP (sizeof (TOC));
		tp->tocNext = (TOC *) NULL;
		tp->tocStr = (char *) NULL;
		if (tp == (TOC *) NULL)
			ETMPanic ("DoAnchor: cannot allocate TOC entry");
		tocLevel = StrToShort (argv[0]);
		if (tocLevel < 1)
			tocLevel = 1;
		tp->tocLevel = tocLevel;
		if (tocHead == (TOC *) NULL)
			tocHead = tp;
		else
			tocTail->tocNext = tp;
		tocTail = tp;
		sprintf (buf, "<A NAME=TOC_%d>", ++tocCount);
		switch (docPart)
		{
		default:
			ETMPanic ("DoAnchor: logic error 3");
		case inHead:
			StrAddStr (header, sizeof (header), buf);
			StrAddStr (header, sizeof (header), "\n");
			break;
		case inBody:
			Flush ();
			StrOut (buf);
			Flush ();
			break;
		}
		anchorFlag = 1;
		break;
	case anchorEnd:
		if (!anchorFlag)
		{
			ETMMsg ("problem: unbalanced anchor-end, ignored");
			break;
		}
		switch (docPart)
		{
		default:
			ETMPanic ("DoAnchor: logic error 4");
		case inHead:
			StrAddStr (header, sizeof (header), "</A>\n");
			break;
		case inBody:
			/*
			 * If there is a pending anchor here, there will be
			 * an empty <A HREF=xxx></A> sequence; warn about it.
			 */
			if (pendingAnchor != (char *) NULL)
				ETMMsg ("problem: empty HREF anchor");
			ControlOut ("/A", 0, 0);
			break;
		}
		/* If tocLevel > 0, we were collecting a TOC entry */
		if (tocLevel > 0)
		{
			if (tocBuf[0] == '\0')
				ETMMsg ("problem: empty TOC entry");
			tocTail->tocStr = StrAllocNP (tocBuf);
			if (tocTail->tocStr == (char *) NULL)
				ETMPanic ("DoAnchor: cannot allocate TOC entry text");
			tocLevel = 0;
		}
		anchorFlag = 0;
		break;
	}
}


/*
 * Begin a new HTML element.  newElt is the type of element, and argc
 * and argv are the rest of the argument vector from the \html input
 * line.
 *
 * This has to detect structured element changes, shut down any current
 * element that's being collected that no longer is, and open up any element
 * that's now being collected that wasn't before.
 *
 * When the document head -> document body transition is crossed, flush the
 * document title and initial header.
 */

static void
DoElement (short newElt, short argc, char **argv)
{
short	misc;

	if (!allowOutput)
		return;

	if (newElt == eltUnknown)	/* can't tell what to do, */
		return;			/* so do nothing */
	/*
	 * First check for title -- it's the only HEAD element.  If it's
	 * present, this will cause the document HEAD part to contain a
	 * title and the text will also be used for the initial BODY part
	 * heading.
	 */
	if (newElt == eltTitle)
	{
		switch (docPart)
		{
		default:
			ETMPanic ("DoElement: logic error 1");
		case inNone:
			BeginDocHead ();
			docPart = inHead;
			break;
		case inHead:	/* title occurred after one was already seen */
			ETMMsg ("multiple titles found?");
			break;
		case inBody:	/* title occurred after body began? */
			ETMMsg ("out of place title found");
			break;
		}
		return;
	}
	/*
	 * All other elements are BODY elements.  If new element is the
	 * first body element we've seen, the HEAD part needs to be flushed
	 * and the BODY part needs to begin.  Shove a paragraph element on
	 * the stack right away to make sure the element stack is non-empty;
	 * this eliminates a lot of checking against that condition later.
	 */
	if (docPart != inBody)
	{
		BeginDocBody ();
		docPart = inBody;
		PushElt (eltPara, listNone);
		if (newElt == eltPara)	/* we just handled it! */
			return;
	}

	switch (newElt)
	{
	default:
		ETMPanic ("DoElement: logic error 2, element = %hd", newElt);
	case eltPara:
		TerminateDefList ();
		AddPara ();		/* add para to current element */
		break;
	case eltDisplay:
		/*TerminateDefList ();*/
		if (displayFlag)
			ETMMsg ("problem: display inside display");
		PushElt (eltDisplay, listNone);
		break;
	case eltDisplayEnd:
		if (eltStack[eltTop].eltType != eltDisplay)
			ETMMsg ("problem: display-end not inside display");
		else
			PopElt ();
		break;
	case eltDisplayIndent:
		if (argc == 0)
			displayIndent = 0;
		else
			displayIndent = StrToShort (argv[0]);
		break;
	case eltList:
		TerminateDefList ();
		if (displayFlag)
			ETMMsg ("problem: list inside display");
		PushElt (eltList, listUnordered);
		break;
	case eltListEnd:
		if (displayFlag)
			ETMMsg ("problem: list-end inside display");
		if (eltStack[eltTop].eltType != eltList)
			ETMMsg ("problem: list-end not inside list");
		else
			PopElt ();
		break;
	case eltListItem:
		if (displayFlag)
			ETMMsg ("problem: list-item inside display");
		if (listDepth == 0)
			ETMMsg ("problem: list-item not inside list");
		ControlOut ("LI", 1, 1);
		break;
	case eltHeader:
		TerminateDefList ();
		if (argc == 0)
		{
			ETMMsg ("header with no level found, ignored");
			break;
		}
		else
			misc = StrToShort (argv[0]);
		if (bodyElt == eltHeader)
			ETMMsg ("problem: nested headers found");
		if (displayFlag)
			ETMMsg ("problem: header inside display");
		if (listDepth)
			ETMMsg ("problem: header inside list");
		PushElt (eltHeader, misc);
		break;
	case eltHeaderEnd:
		if (eltStack[eltTop].eltType != eltHeader)
			ETMMsg ("problem: header-end w/o matching header, ignored");
		else
			PopElt ();
		break;
	case eltBlockquote:
		TerminateDefList ();
		if (blockquoteFlag)
			ETMMsg ("problem: nested blockquotes found");
		if (displayFlag)
			ETMMsg ("problem: blockquote inside display");
		PushElt (eltBlockquote, 0);
		break;
	case eltBlockquoteEnd:
		if (eltStack[eltTop].eltType != eltBlockquote)
			ETMMsg ("problem: blockquote-end w/o matching blockquote, ignored");
		else
			PopElt ();
		break;
	case eltDefTerm:		/* begin new definition term */
		if (displayFlag)
			ETMMsg ("problem: definition list inside display");
		if (listDepth)
			ETMMsg ("problem: definition list inside list");
		switch (defListFlag)
		{
		case defListNone:
			/*
			 * Start new list, new term
			 */
			ControlOut ("DL", 1, 1);
			ControlOut ("DT", 1, 1);
			break;
		case defListTerm:
			break;		/* ?? */
		case defListDesc:
			/*
			 * Close current description, start new term
			 */
			ControlOut ("/DD", 1, 1);
			ControlOut ("DT", 1, 1);
			break;
		}
		defListFlag = defListTerm;
		emptyDefTerm = 1;
		break;
	case eltDefDesc:		/* begin new definition description */
		if (displayFlag)
			ETMMsg ("problem: definition list end inside display");
		if (listDepth)
			ETMMsg ("problem: definition list end inside list");
		switch (defListFlag)
		{
		case defListNone:
			/*
			 * Start new list, put out empty term, start new
			 * description.  <BR> in the term forces a little
			 * space between this item and the previous.
			 */
			ControlOut ("DL", 1, 1);
			ControlOut ("DT", 1, 0);
			ControlOut ("BR", 0, 0);
			ControlOut ("/DT", 0, 1);
			ControlOut ("DD", 1, 1);
			break;
		case defListTerm:
			/*
			 * Close current term, start new description.
			 */
			if (emptyDefTerm)
				ControlOut ("BR", 0, 1);
			ControlOut ("/DT", 1, 1);
			ControlOut ("DD", 1, 1);
			break;
		case defListDesc:
			break;		/* ??? */
		}
		defListFlag = defListDesc;
		break;
	/*
	 * Right and left shift are implemented by <UL> and </UL>, which is
	 * no doubt a terrible way to do it.  It'll have to do until there's
	 * a better way.
	 */
	case eltShiftRight:
		TerminateDefList ();
		ControlOut ("UL", 1, 1);
		++shiftLevel;
		break;
	case eltShiftLeft:
		if (shiftLevel > 0)
		{
			TerminateDefList ();
			ControlOut ("/UL", 1, 1);
			--shiftLevel;
		}
		break;
	}
}


/*
 * Begin the document HEAD part.  Set up to start saving text in the title
 * and header buffers.  (Two buffers are used because line breaks and lines
 * are written to the header buffer as is, but as spaces and '/' characters
 * in the title -- this keeps the title all on a single line.)  When the
 * document BODY part begins, the title buffer is flushed to the document
 * HEAD part and the header buffer is written as the initial heading of the
 * BODY part.
 *
 * Should only be called from DoElement().
 */


static void
BeginDocHead (void)
{
	title[0] = '\0';
	header[0] = '\0';
	/* if centering is on, add <CENTER> to header */
	if (center)
		StrAddStr (header, sizeof (header), "<CENTER>\n");
	/* if underlining is on, add <UNDER> to header */
	if (underline)
		StrAddStr (header, sizeof (header), "\n<UNDER>\n");
}


/*
 * Begin the document BODY part.  First flush out the HEAD part, including
 * the document title if there is one.  Then put out the BODY tag and write
 * the initial header if there is one.
 *
 * Should only be called from DoElement().
 *
 * cmdLineTitle, if set, overrides any title that was found in the body.
 * However, if there is a title and it was found to be centered and/or
 * underlined, the command-line title will also be centered and/or
 * underlined.
 */

static void
BeginDocBody (void)
{
char	*p;

	/* drop trailing spaces on title */
	p = title + strlen (title) - 1;
	while (p > title && *(p-1) == ' ')
		*--p = '\0';
	/* put out HEAD if title isn't empty */
	if (cmdLineTitle != (char *) NULL || title[0] != '\0')
	{
		ControlOut ("HEAD", 1, 1);
		ControlOut (titleTag, 1, 1);
		if (cmdLineTitle != (char *) NULL)
		{
			for (p = cmdLineTitle; *p != '\0'; p++)
				StrOut (CharToHTML (*p));
		}
		else
			StrOut (title);
		ControlOut (endTitleTag, 1, 1);
		ControlOut ("/HEAD", 1, 1);
	}
	title[0] = '\0';
	ControlOut ("BODY", 1, 1);
	if (cmdLineTitle != (char *) NULL)
	{
		ControlOut (headerTag, 1, 1);
		if (center)
			ControlOut ("CENTER", 1, 1);
		if (underline)
			ControlOut ("UNDER", 1, 1);
		for (p = cmdLineTitle; *p != '\0'; p++)
			StrOut (CharToHTML (*p));
		if (center)
			ControlOut ("/CENTER", 1, 1);
		if (underline)
			ControlOut ("/UNDER", 1, 1);
		ControlOut (endHeaderTag, 1, 1);
	}
	else if (header[0] != '\0')
	{
		/*
		 * If centering is on, add an end-centering marker to the
		 * header.  Any following text in the body will get its
		 * own begin-centering marker.  Ditto for underlining.
		 */
		ControlOut (headerTag, 1, 1);
		StrOut (header);
		if (center)
			ControlOut ("/CENTER", 1, 1);
		if (underline)
			ControlOut ("/UNDER", 1, 1);
		ControlOut (endHeaderTag, 1, 1);
	}
	header[0] = '\0';
	if (center)
		ControlOut ("CENTER", 1, 1);
	if (underline)
		ControlOut ("UNDER", 1, 1);
}


static void
TerminateDefList (void)
{
	switch (defListFlag)
	{
	default:
		ETMPanic ("TerminateDefList: logic error");
	case defListNone:
		/* no current definition list - nothing to do */
		break;
	case defListTerm:
		/*
		 * Close current term, write empty description, close list.
		 * <BR> in the term forces a little space between this item
		 * and the previous in case the term is empty.
		 */
		if (emptyDefTerm)
			ControlOut ("BR", 0, 1);
		ControlOut ("/DT", 1, 1);
		ControlOut ("DD", 1, 0);
		ControlOut ("/DD", 0, 1);
		ControlOut ("/DL", 1, 1);
		break;
	case defListDesc:
		/*
		 * Close current description, close list
		 */
		ControlOut ("/DD", 0, 1);
		ControlOut ("/DL", 1, 1);
		break;
	}
	defListFlag = defListNone;
}


static void
FlushTOC (void)
{
TOC	*tp;
short	level = 0;
char	buf[bufSiz];

	if (tocHead == (TOC *) NULL)	/* no TOC */
		return;

	ControlOut ("!-- TOC BEGIN --", 1, 1);

	tocCount = 0;

	for (tp = tocHead; tp != (TOC *) NULL; tp = tp->tocNext)
	{
		/*
		 * If current level doesn't match entry level, decrease or
		 * increase current level to match.
		 */
		while (level > tp->tocLevel)
		{
			ControlOut ("/UL", 1, 1);
			--level;
		}
		while (level < tp->tocLevel)
		{
			ControlOut ("UL", 1, 1);
			++level;
		}
		ControlOut ("LI", 1, 1);
		sprintf (buf, "A HREF=#TOC_%d", ++tocCount);
		ControlOut (buf, 0, 0);
		StrOut (tp->tocStr);
		ControlOut ("/A", 0, 1);
	}
	while (level > 0)
	{
		ControlOut ("/UL", 1, 1);
		--level;
	}

	ControlOut ("!-- TOC END --", 1, 1);
}


static void
PushElt (short eltType, short misc)
{
char	buf[bufSiz];


	if (++eltTop > maxElt - 1)
		ETMPanic ("PushElt: element stack overflow");
	eltStack[eltTop].eltType = eltType;
	eltStack[eltTop].eltFont = curFont;
	eltStack[eltTop].eltMisc = misc;
	bodyElt = eltType;		/* set current body element type */
	/* begin new element */
	switch (bodyElt)
	{
	default:
		ETMPanic ("PushElt: logic error, type = %hd", bodyElt);
	case eltPara:
		ControlOut ("P", 1, 1);		/* AddPara()??? */
		break;
	case eltDisplay:
		displayFlag = 1;
		SetFont (displayFont, displayFlag);
		ControlOut ("PRE", 1, 1);
		break;
	case eltList:
		ControlOut ("UL", 1, 1);		/* DEPENDS ON TYPE */
		++listDepth;
		break;
	case eltHeader:
		sprintf (buf, "H%hd", misc);	/* misc = header level */
		ControlOut (buf, 1, 1);
		break;
	case eltBlockquote:
		ControlOut ("BLOCKQUOTE", 1, 1);
		++blockquoteFlag;
		break;
		break;
	}
}


static void
PopElt (void)
{
char	buf[bufSiz];

	if (eltTop < 0)
		ETMPanic ("PopElt: element stack underflow");

/* reset font, style if necessary */

	/* close current element */
	switch (eltStack[eltTop].eltType)
	{
	default:
		ETMPanic ("PopElt: logic error, type = %hd", eltStack[eltTop].eltType);
	case eltPara:
		/* nothing to do */
		break;
	case eltDisplay:
		ControlOut ("/PRE", 1, 1);
		displayFlag = 0;
		SetFont ("R", displayFlag);
		break;
	case eltList:
		ControlOut ("/UL", 1, 1);	/* DEPENDS ON TYPE */
		--listDepth;
		break;
	case eltHeader:
		/* eltMisc = header level */
		sprintf (buf, "/H%hd", eltStack[eltTop].eltMisc);
		ControlOut (buf, 1, 1);
		break;
	case eltBlockquote:
		ControlOut ("/BLOCKQUOTE", 1, 1);
		--blockquoteFlag;
		break;
	}
	eltTop--;
	bodyElt = eltStack[eltTop].eltType;	/* restore previous type */
	curFont = eltStack[eltTop].eltFont;
/*
 * SHOULD I DO THIS?
	if (curFont != (Font *) NULL)
		SetFont (curFont->fontName);
 */
}


/*
 * Perform a break.  The action to take here depends on the current state.
 */

static void
AddBreak (void)
{
	if (docPart == inNone)
	{
		ETMMsg ("leading break skipped");
		return;
	}
	if (docPart == inHead)
	{
		AddTitleChar (' ');
		StrAddStr (header, sizeof (header), "<BR>\n");
		return;
	}
	/* handle body elements */
	switch (bodyElt)
	{
	default:
		ETMPanic ("AddBreak: logic error, part %d", bodyElt);
	case eltPara:
	case eltList:
	case eltHeader:
	case eltBlockquote:
		ControlOut ("BR", 0, 1);
		break;
	case eltDisplay:
		TextChOut (lf);
		break;
	}
	if (tocLevel > 0)	/* TOC entry is being collected */
		StrAddStr (tocBuf, sizeof (tocBuf), "<BR>\n");
}


/*
 * This should only be called from DoElement().
 */

static void
AddPara (void)
{
	if (docPart == inNone)
	{
		ETMMsg ("leading space skipped");
		return;
	}
	if (docPart == inHead)
	{
		AddTitleChar (' ');	/* keep title all on one line */
		StrAddStr (header, sizeof (header), "<P>\n");
		return;
	}
	/* handle body elements */
	switch (bodyElt)
	{
	default:
		ETMPanic ("AddPara: logic error, elt %d", bodyElt);
	case eltPara:
	case eltList:
	case eltHeader:
	case eltBlockquote:
		ControlOut ("P", 1, 1);
		break;
	case eltDisplay:
		TextChOut (lf);
		break;
	}
	if (tocLevel > 0)	/* TOC entry is being collected */
		StrAddStr (tocBuf, sizeof (tocBuf), "<P>\n");
}


/*
 * Draw a line (horizontal only; ignore vertical).
 */

static void
AddLine (long length, short direction)
{
	if (direction != 'h')
		return;
	if (docPart == inNone)		/* part unknown - set to default */
		DoElement (eltPara, 0, (char **) NULL);
	if (docPart == inHead)
	{
		AddTitleChar ('/');	/* keep title all on one line */
		StrAddStr (header, sizeof (header), "<HR>\n");
		return;
	}
	/* handle body elements */
	switch (bodyElt)
	{
	default:
		ETMPanic ("AddLine: logic error, elt %d", bodyElt);
	case eltPara:
	case eltList:
	case eltHeader:
	case eltDisplay:
	case eltBlockquote:
		ControlOut ("HR", 1, 1);
		break;
	}
	/* don't collect anything for TOC here */
}


/*
 * Add a string literally to the output.  (No escaping of characters
 * that are special to HTML like & or < or >.)  len is the effective
 * length of the string, i.e., the number of characters what would be
 * displayed in a browser.  For example, the effective length of a
 * string like "&Aacute;" is one.
 */

static void
AddLitStr (char *p)
{
char	*p1, *p2;
char	c;

	if (p == (char *) NULL)
		return;
	switch (docPart)
	{
	default:
		ETMPanic ("AddLitStr: logic error");
	case inHead:
		StrAddStr (title, sizeof (title), p);
		StrAddStr (header, sizeof (header), p);
		break;
	case inBody:
		/*
		 * This is the ugly part.  Write characters of the string
		 * one by one.  If a "&" is seen, assume it's part of a "&xxx;"
		 * sequence and pass it to HTMLChOut() all at once.
		 * Be careful about sequences that may have the terminating
		 * ";" missing.
		 */
		p1 = p;
		while (*p1 != '\0')
		{
			if (*p1 != '&')		/* not a "&xxx;" sequence */
				TextChOut (*p1++);
			else
			{
				/* save beginning of "&xxx;" and find end */
				p2 = p1++;
				while (*p1 != ';' && *p1 != '\0')
					++p1;
				if (*p1 == ';')
				{
					c = *(++p1);	/* save next char */
					*p1 = '\0';
					HTMLChOut (p2);
					*p1 = c;
				}
				else	/* *p1 is '\0' */
				{
					ETMMsg ("malformed sequence, missing semicolon: <%s>", p2);
					HTMLChOut (p2);
					/* supply the missing semicolon */
					TextChOut (';');
				}
			}
		}
		break;
	}
	if (tocLevel > 0)
		StrAddStr (tocBuf, sizeof (tocBuf), p);
}


/*
 * Add a string to the output.  Escaping is done of characters
 * that are special to HTML like & or < or >.
 */

static void
AddStr (char *p)
{
	if (p == (char *) NULL)
		return;
	while (*p != '\0')
		AddChar (*p++);
}


/*
 * Add a character to the output.  Escaping is done of characters
 * that are special to HTML like & or < or >.
 */
static void
AddChar (char c)
{
	switch (docPart)
	{
	default:
		ETMPanic ("AddChar: logic error");
	case inHead:
		AddTitleChar (c);
		AddHeaderChar (c);
		break;
	case inBody:
		TextChOut (c);
		if (defListFlag == defListTerm)
			emptyDefTerm = 0;
		break;
	}
	if (tocLevel > 0)	/* TOC entry is being collected */
		AddTOCChar (c);
}


static void
AddTitleChar (char c)
{
	StrAddStr (title, sizeof (title), CharToHTML (c));
}


static void
AddHeaderChar (char c)
{
	StrAddStr (header, sizeof (header), CharToHTML (c));
}


static void
AddTOCChar (char c)
{
	StrAddStr (tocBuf, sizeof (tocBuf), CharToHTML (c));
}


static void
StrAddStr (char *buf, int size, char *str)
{

	/* check whether or not str can be added to buf */
	if (strlen (buf) + strlen (str) + 1 > size)
		ETMPanic ("StrAddStr: buffer overflow");
	(void) strcat (buf, str);
}


/*
 * Table element routines
 */


static void
TableBegin (short argc, char **argv)
{
	if (docPart == inNone)		/* part unknown - set to default */
		DoElement (eltPara, 0, (char **) NULL);
	if (argc != 8)
		ETMPanic ("TableBegin: wrong number of args: %hd", argc);
	tblCols = StrToShort (argv[tblColsArg]);
	tblRows = StrToShort (argv[tblRowsArg]);

	if (tblCols == 0)
		ETMPanic ("TableBegin: zero column table");
	if (tblCols > tblMaxCol)
		ETMPanic ("TableBegin: too many table columns (%hd), max = %d",
						tblCols, tblMaxCol);

	ControlOut ("BR", 1, 1);
	if (argv[tblAlignArg][0] == 'C')		/* center the table */
	{
		ControlOut ("CENTER", 1, 1);
		centerTbl = 1;
	}
	Flush ();
	StrOut ("<TABLE CELLPADDING=3");
	if (argv[tblExpandArg][0] == 'y')		/* expand */
		StrOut (" WIDTH=\"100%\"");
	/*
	 * Turn on cell borders.  This isn't really technically very
	 * accurate, but I think HTML tables look better that way.
	 */
	StrOut (" BORDER");
	ChOut ('>');
	Flush ();
}


static void
TableEnd (void)
{
	ControlOut ("/TABLE", 1, 1);
	if (centerTbl)
	{
		ControlOut ("/CENTER", 1, 1);
		centerTbl = 0;
	}
	ControlOut ("BR", 1, 1);
}


static void
TableRowBegin (short argc, char **argv)
{
	ControlOut ("TR", 1, 1);
	tblColIdx = 0;
}


static void
TableRowEnd (void)
{
	ControlOut ("/TR", 1, 1);
	++tblRowIdx;
}


static void
TableRowLine (short argc, char **argv)
{
	ControlOut ("TR", 1, 1);
	printf ("<TD COLSPAN=%hd></TD>\n", tblCols);
	ControlOut ("/TR", 1, 1);
	++tblRowIdx;
}


static void
TableCellInfo (short argc, char **argv)
{
short	vspan, hspan;

	if (argc != 5)
		ETMPanic ("TableCellInfo: wrong number of args: %hd", argc);

	cellType[tblColIdx] = argv[cellTypeArg][0];
	cellVSpan[tblColIdx] = StrToShort (argv[cellVSpanArg]);
	cellHSpan[tblColIdx] = StrToShort (argv[cellHSpanArg]);
	cellVAdjust[tblColIdx] = argv[cellVAdjustArg][0];
	/* border value is ignored */

	++tblColIdx;
	if (tblColIdx >= tblCols)
		tblColIdx = 0;
}


static void
TableCellBegin (short argc, char **argv)
{
	CellBegin (cellType[tblColIdx],
		cellVSpan[tblColIdx],
		cellHSpan[tblColIdx],
		cellVAdjust[tblColIdx]);
}


static void
TableCellEnd (void)
{
	ControlOut ("/TD", 1, 1);

	++tblColIdx;
}


static void
TableEmptyCell (void)
{
	CellBegin (cellType[tblColIdx],
		cellVSpan[tblColIdx],
		cellHSpan[tblColIdx],
		cellVAdjust[tblColIdx]);
	ControlOut ("BR", 0, 0);
	ControlOut ("/TD", 0, 1);

	++tblColIdx;
}


static void
TableSpannedCell (void)
{
	++tblColIdx;
}


/*
 * This is really poor.  What's a better way?
 */

static void
TableCellLine (short argc, char **argv)
{
	/* override type with 'C' to force centering of the line */
	CellBegin ('C',
		cellVSpan[tblColIdx],
		cellHSpan[tblColIdx],
		cellVAdjust[tblColIdx]);
	StrOut ("---");
	ControlOut ("/TD", 1, 1);

	++tblColIdx;
}


static void
CellBegin (char type, short vspan, short hspan, char vadjust)
{
	Flush ();
	StrOut ("<TD");
	/*
	 * Align the cell contents.  Default is left.  Numeric alignment isn't
	 * supported, so use right-alignment as the best substitute.
	 */
	switch (type)
	{
	default:
		break;
	case 'C':
		StrOut (" ALIGN=CENTER");
		break;
	case 'R':
	case 'N':
		StrOut (" ALIGN=RIGHT");
		break;
	}
	if (vadjust == 'T')
		StrOut (" VALIGN=TOP");
	if (vspan > 1)
		printf (" ROWSPAN=%hd", vspan);
	if (hspan > 1)
		printf (" COLSPAN=%hd", hspan);
	ChOut ('>');
	Flush ();
}


/*
 * basic output routines
 *
 * ControlOut() - write control word.
 * TextStrOut() - write out a string of paragraph text.  Does HTML escaping.
 * TextChOut() - write out a character of paragraph text.  Does HTML escaping.
 * StrOut() - write out a string.
 * ChOut() - write out a character.  ALL output (except ETM messages)
 * 	comes through this routine.
 * AllowOutput () - allow or throttle output.
 */


static void
ControlOut (char *p, short brkBefore, short brkAfter)
{
	if (p != (char *) NULL)
	{
		if (brkBefore)		/* break if not already */
			Flush ();	/* at beginning of line */
		ChOut ('<');
		StrOut (p);
		ChOut ('>');
		if (brkAfter)
			Flush ();
	}
}


static void
TextStrOut (char *p)
{
	if (p != (char *) NULL)
	{
		while (*p != '\0')
			TextChOut (*p++);
	}
}


/*
 * HTML-ize a character and return pointer to resulting string.
 * This keeps characters like < and > from being interpreted
 * specially by Web browsers.  Return value is a pointer into a
 * static buffer, and should be copied if the caller needs to hang
 * onto it for a while.
 */
	
static char *
CharToHTML (int c)
{
static char	buf[7];

	switch (c)
	{
	default:
		buf[0] = c;
		buf[1] = '\0';
		break;
	case '&':
		(void) strcpy (buf, "&amp;");
		break;
	case '<':
		(void) strcpy (buf, "&lt;");
		break;
	case '>':
		(void) strcpy (buf, "&gt;");
		break;
	case '"':
		(void) strcpy (buf, "&quot;");
		break;
	}
	return (buf);
}


/*
 * Write a character of document text.
 */
	
static void
TextChOut (int c)
{
	HTMLChOut (CharToHTML (c));
}


/*
 * Write a character of document text.  The character is passed in as
 * a string, which is assumed to be the HTML representation of a single
 * character (e.g., "x" for x, "&amp;" for &.
 * - Sets tCount to the number of text (non-tag) characters on the line.
 * - Sets etCount to the number of effective text (non-tag) characters.
 * - Sets cCount to the number of characters on the line (via ChOut ()).
 * - Does line wrapping if not in a display or in fill mode.
 * - Handles display indent.
 *
 * Characters written to produce display indent are not included in the
 * tCount or etCount line output character counts.  Tabbing in displays is
 * thus relative to the indent, not to the document left margin.
 */
	
static void
HTMLChOut (char *p)
{
int	len;
int	i;

	/*
	 * Check to make sure this is either a single character string or
	 * that it begins with &.
	 */

	if (*p == '\0' || (*p != '&' && *(p+1) != '\0'))
		ETMPanic ("HTMLChOut: logic error (%s)", p);

	/*
	 * Don't do line wrapping if collecting a display or
	 * in nofill mode.
	 */
	if (displayFlag || fill == 0)
	{
		/*
		 * If this is the start of a display line, put out spaces to
		 * add the leading indent.  Exception: if the character is a
		 * linefeed, the line is blank, so there's no need for leading
		 * spaces.
		 */
		if (displayFlag && tCount == 0 && *p != lf)
		{
			for (i = 0; i < displayIndent; i++)
				ChOut (' ');
			/*
			 * tCount and etCount are deliberately NOT set here,
			 * so tabs in displays are relative to the indent,
			 * not to the left margin.
			 */
		}
		if (*p == tab)			/* print a tab */
		{
			/*
			 * Assume worst case (that all tabstops are too small),
			 * in which case only print a single space.  If an
			 * appropriate tabstop is found, use it.
			 */
			len = 1;
			for (i = 0; i < tabCount; i++)
			{
				if (tabPos[i] > etCount)
				{
					len = tabPos[i] - etCount;
					break;
				}
			}
			for (i = 0; i < len; i++)
				ChOut (' ');
			tCount += len;
			etCount += len;
		}
		else				/* not a tab */
		{
			StrOut (p);
			if (*p != lf)
			{
				tCount += strlen (p);
				++etCount;
			}
		}
		return;
	}
	/*
	 * Not in display or fill mode.  Do line wrapping to keep output
	 * lines from becoming really long.  When that's done, it also
	 * becomes useful to ignore leading spaces that occur at the
	 * beginning of the next line.  Tabs are not processed if line
	 * wrapping occurs - just print a space.
	 */
	if (*p == tab)
		*p = ' ';
	if (*p == ' ' && tCount == 0)	/* leading space */
		return;
	if (*p == ' ' && cCount > 60)	/* break line */
		Flush ();		/* (sets cCount, tCount to zero */
	else
	{
		StrOut (p);
		tCount += strlen (p);
		++etCount;
	}
}


/*
 * StrOut() and ChOut() result in cCount being set.  If a linefeed is
 * printed, both cCount and tCount are set to zero.
 */

static void
StrOut (char *p)
{
	if (p != (char *) NULL)
	{
		while (*p != '\0')
			ChOut (*p++);
	}
}


static void
ChOut (int c)
{
	if (!allowOutput)
		return;
	if (c != ' ' && pendingAnchor != (char *) NULL)
	{
		if (fputs (pendingAnchor, stdout) == EOF)
			ETMPanic ("Write error, cannot continue");
		cCount += strlen (pendingAnchor);
		Free (pendingAnchor);
		pendingAnchor = (char *) NULL;
	}
	if (putc (c, stdout) == EOF)
		ETMPanic ("Write error, cannot continue");
	++cCount;
	if (c == lf)
	{
		cCount = 0;
		tCount = 0;
		etCount = 0;
	}
}


static void
Flush (void)
{
	if (cCount > 0)
		ChOut (lf);
}


static int
AllowOutput (int yesno)
{
int	prev = allowOutput;

	allowOutput = yesno;
	return (prev);
}


/*
 * Read font information file.  Format of lines is
 *
 * troff-font-name begin-tag end-tag begin-tag2 end-tag2
 *
 * begin-tag2 and end-tag2 are used when in displays
 *
 * For example:
 *	R	""	""	""	""
 *	I	<I>	</I>	<I>	</I>
 *	B	<B>	</B>	<B>	</B>
 *	CW	<TT>	</TT>	""	""
 */

static int
ReadFonts (char *filename)
{
FILE	*f;
char	buf[bufSiz];
Font	*fp;
char	*name, *begin, *end, *dbegin, *dend;

	if ((f = TCROpenLibFile (filename, "r")) == (FILE *) NULL)
		return (0);
	while (TCRGetLine (buf, (int) sizeof (buf), f))
	{
		if (buf[0] == '#')	/* skip comments */
			continue;
		TSScanInit (buf);
		name = TSScan ();
		begin = TSScan ();
		end = TSScan ();
		dbegin = TSScan ();
		dend = TSScan ();
		if (dend == (char *) NULL)
			continue;
		fp = New (Font);
		fp->fontName = StrAlloc (name);
		fp->beginTag = StrAlloc (begin);
		fp->endTag = StrAlloc (end);
		fp->dispBeginTag = StrAlloc (dbegin);
		fp->dispEndTag = StrAlloc (dend);
		fp->inDisplay = 0;
		fp->next = fontList;
		fontList = fp;
	}
	(void) fclose (f);
	return (1);
}


static Font *
LookupFont (char *name)
{
Font	*fp;

	for (fp = fontList; fp != (Font *) NULL; fp = fp->next)
	{
		if (strcmp (name, fp->fontName) == 0)
			break;
	}
	return (fp);		/* NULL if not found */
}
