/***********************************************************************
*                                                                      *
*               This software is part of the ast package               *
*           Copyright (c) 1996-2006 AT&T Knowledge Ventures            *
*                      and is licensed under the                       *
*                  Common Public License, Version 1.0                  *
*                      by AT&T Knowledge Ventures                      *
*                                                                      *
*                A copy of the License is available at                 *
*            http://www.opensource.org/licenses/cpl1.0.txt             *
*         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
*                                                                      *
*              Information and Software Systems Research               *
*                            AT&T Research                             *
*                           Florham Park NJ                            *
*                                                                      *
*                 Glenn Fowler <gsf@research.att.com>                  *
*                                                                      *
***********************************************************************/
#pragma prototyped
/*
 * Glenn Fowler
 * AT&T Labs Research
 *
 * troff to html filter
 *
 * NOTE: not handled in state.groff mode
 *
 *	\?anything\?
 */

static const char usage[] =
"[-?\n@(#)$Id: troff2html (AT&T Labs Research) 2004-04-26 $\n]"
USAGE_LICENSE
"[+NAME?troff2html - convert troff/groff input to html]"
"[+DESCRIPTION?\btroff2html\b converts \btroff\b(1) (or \bgroff\b(1),"
"	depending on the processing mode) input documents to an \bhtml\b"
"	document on the standard output. Although a full \atroff\a parse"
"	is done, many features are ignored in the mapping to \bhtml\b.]"
"[+?The \atroff\a \bt\b condition test evaluates \btrue\b, the \bn\b"
"	condition tests evaluates \bfalse\b, and the \agroff\a compatibility"
"	register \b\\\\n[.C]]\b evaluates to 0 (enable \agroff\a parsing)"
"	if it is referenced before the first \agroff\a \b.cp\b (compatibility"
"	mode) request.]"
"[+?The generated \bhtml\b has properly nested begin/end tags, even though most"
"	browsers don't care.]"

"[i:identify?Reads identification options from \afile\a. Unknown options"
"	are silently ignored. See the \b.xx\b request below for a description"
"	of the options.]:[file]"
"[I:include?Appends \adirectory\a to the list of directories searched"
"	for \b--macros\b and \b.so\b request files.]:[directory]"
"[m:macros?Locates and reads the macro package file \apackage\a. In order"
"	to accomodate different \atroff\a installation styles the file search"
"	order is fairly involved:]:[package]{"
"		[+./package?]"
"		[+directory/package?for all \b--include\b directories]"
"		[+directory/tmac.package?for all \b--include\b directories]"
"		[+../lib/tmac/tmac.package?for all directories on \b$PATH\b]"
"		[+../lib/html/package?for all directories on \b$PATH\b]"
"		[+../lib/html/mpackage.tr?for all directories on \b$PATH\b]"
"		[+../share/groff/tmac/tmac.package?for all directories on \b$PATH\b]"
"		[+../groff/share/groff/tmac/tmac.package?for all directories on \b$PATH\b]"
"}"
"[r:register?Initializes the number \aregister\a to \aN\a.]:[registerN]"
"[s:script?Reads \ascript\a as if it came from a file. \b--script='.nr A 123'\b"
"	is equivalent to \b-rA123\b.]:[script]"
"[v:verbose?Enables verbose error and warning messages. Following \atroff\a"
"	tradition, \btroff2html\b by default does not warn about unknown"
"	requests; \b--verbose\b enables such warnings.]"

"[+EXTENSIONS?\b.xx\b \aname\a[=\avalue\a]] is a special \btroff2html\b"
"	request that handles program tracing, \bhtml\b extensions and \atroff\a"
"	macro package magic that went way past the author's willingness"
"	to understand. Supported operations are:]{"
"	[+author=text?Specifies the contact information for the document"
"		HEAD section.]"
"	[+background=URL?Specifies the document background URL.]"
"	[+debug=level?The debug trace \alevel\a; higher levels produce"
"		more output.]"
"	[+get=+-register?Traces each \bget\b for the named number"
"		\aregister\a. \b-\b turns tracing off.]"
"	[+hot='word ...'?Adds (\b+\b) or removes (\b-\b) \aword\a ... from"
"		the hot-word list. Constructs that match (\ahot-word\a ..."
"		\aSWITCH-FONT\a text \aSWITCH-FONT\a ... ) adds \atext\a as"
"		a hot link to another portion of the document. \brefer\b and"
"		\bsee\b are the default hot words. Case is ignored when"
"		matching hot words.]"
"	[+logo=URL?Specifies the logo/banner image URL that is centered"
"		at the top of the document.]"
"	[+mailto=address?Sets the email \aaddress\a to send comments and"
"		suggestions.]"
"	[+meta.name?Emits the \bhtml\b tag \b<META name=\b\"\aname\a\""
"		\bcontent=\b\"\acontent\a\"\b>\b.]"
"	[+package=text?\atext\a is prepended to the \bhtml\b document title.]"
"	[+set=+-register?Traces each \bset\b for the named number"
"		\aregister\a. \b-\b turns tracing off.]"
"	[+title=text?Sets the document title.]"
"}"
"[+?Local URL links are generated for all top level headings. These can be"
"	referenced by embedding the benign (albeit convoluted) \atroff\a"
"	construct \\h'0*\\w\"label\"'text\\g'0', where \alabel\a is the"
"	local link label and \atext\a is the hot link text. If \alabel\a"
"	and \atext\a are the same then use \\h'0*1'text\\h'0'.]"

"\n"
"\n[ file ... ]\n"
"\n"
"[+SEE ALSO?\bgroff\b(1), \bhtml2rtf\b(1), \bman\b(1), \bmm\b(1),"
"	\bmm2html\b(1), \btroff\b(1)]"
;

#include "troff2html.h"

#include <error.h>
#include <hashkey.h>
#include <ls.h>
#include <tm.h>

/*
 * intermediate code
 *
 *	CODE_0
 *	CODE_1 <op>
 *	CODE_2 <data> <op>
 *	CODE_n <length> <op> <length-data>
 */

#define DEFAULT_cc	'.'
#define DEFAULT_c2	'\''
#define DEFAULT_ec	'\\'
#define DEFAULT_ft	1
#define DEFAULT_pc	'%'
#define DEFAULT_ps	10
#define DEFAULT_vs	12

#define CODE_1		1
#define CODE_2		2
#define CODE_n		3
#define CODE_0		4

#define END(x)		((x)|OP_END)
#define LABEL(x)	((x)|OP_LABEL)
#define OP(x)		((x)&077)

#define OP_END		0200
#define OP_LABEL	0100

#define OP_a		1
#define OP_body		2
#define OP_br		3
#define OP_cc		4
#define OP_center	5
#define OP_comment	6
#define OP_dd		7
#define OP_div		8
#define OP_dl		9
#define OP_dt		10
#define OP_fn		11
#define OP_ft1		12
#define OP_ft2		13
#define OP_ft3		14
#define OP_ft4		15
#define OP_ft5		16
#define OP_h2		17
#define OP_h3		18
#define OP_h4		19
#define OP_head		20
#define OP_hr		21
#define OP_html		22
#define OP_link		23
#define OP_p		24
#define OP_pre		25
#define OP_ps		26
#define OP_sub		27
#define OP_sup		28
#define OP_ta		29
#define OP_tab		30
#define OP_tab_data	31
#define OP_tab_head	32
#define OP_tab_row	33
#define OP_title	34
#define OP_RAW		35

#define OP_ft		(OP_ft1-1)

#define ARG_ALIGN(a)	(((a)&3)-1)
#define ARG_ATTR(a)	((a)&0x0fff)

#define ARG_left	0x0001
#define ARG_center	0x0002
#define ARG_right	0x0003

#define ARG_compact	0x0010
#define ARG_wide	0x0020

#define ATT_INDEX(a)	(((a)&0x0fff)-1)

#define ATT_NUMBER	0x8000

#define ATT_background	1
#define ATT_href	2
#define ATT_lref	3
#define ATT_id		4
#define ATT_name	5
#define ATT_size	(ATT_NUMBER|6)
#define ATT_src		7

#define LINE		0x2000
#define LIST		0x4000
#define STACK		0x8000

#define DIVERTED()	(state.out_top>state.out_stack)
#undef	EOF
#define EOF		0
#define INDEX(a,b)	(((a)<<8)|(b))
#define ISFILE()	((state.in_top-1)->ip)
#define GETC()		(*state.in++)
#define GROFFINIT()	do{if(!state.groff_init)state.groff|=state.groff_init=1;}while(0)
#define UNGETC(c)	(*--state.in=(c))

#define UNGET_MAX	(SF_BUFSIZE/8)

State_t			state;

/*
 * push input file/string
 */

static void
pushin(char* name, int line, Sfio_t* ip, char* data, Arg_t* arg)
{
	register Pushin_t*	pp;
	int			n;

	if (state.in_top >= &state.in_stack[elementsof(state.in_stack)])
		error(3, "input stack overflow");
	pp = state.in_top++;
	pp->in = state.in;
	if (!(pp->ip = ip))
	{
		if (!(state.in = (unsigned char*)data) || !*state.in)
		{
			state.in = pp->in;
			--state.in_top;
			return;
		}
	}
	else if (!(pp->buf = oldof(0, unsigned char, SF_BUFSIZE, UNGET_MAX + 1)))
		error(ERROR_SYSTEM|3, "out of space [file pushin]");
	else
	{
		pp->buf += UNGET_MAX;
		if ((n = sfread(ip, pp->buf, SF_BUFSIZE)) <= 0)
		{
			if (ip && ip != sfstdin)
				sfclose(ip);
			state.in_top--;
			return;
		}
		pp->end = pp->buf + n;
		*pp->end++ = EOF;
		state.in = pp->buf;
	}
	pp->file = error_info.file;
	if (name)
	{
		if (!state.original.file)
		{
			state.original.file = error_info.file;
			state.original.line = error_info.line;
		}
		error_info.file = name;
	}
	pp->line = error_info.line;
	if (line < 0)
		state.noline++;
	else
	{
		if (line > 0)
			error_info.line = line;
		if (state.noline > 0)
			state.noline++;
	}
	if (arg)
	{
		pp->mac = state.mac;
		state.mac = arg;
		if (pp->top.sp)
			sfstrseek(pp->top.sp, 0, SEEK_SET);
		else if (!(pp->top.sp = sfstropen()))
			error(ERROR_SYSTEM|3, "out of space [macro call]");
		pp->tag = state.tag;
		state.tag = &pp->top;
	}
	else
		pp->tag = 0;
}

static long	expression(char*, char**, int);

/*
 * return the next expression operand
 */

static long
operand(register char* s, char** e, int scale)
{
	register int	c;
	register long	n;
	register long	f;
	int		abs;
	int		neg;
	long		d;
	char*		x;

	while (isspace(*s))
		s++;
	if (abs = *s == '|')
		s++;
	if (neg = *s == '-')
		s++;
	else if (*s == '+')
		s++;
	d = 1;
	if (*s == '(')
	{
		n = (state.groff && isalpha(*(s + 1)) && *(s + 2) == ';') ?
			expression(s + 3, &x, *(s + 1)) :
			expression(s + 1, &x, scale);
		s = x;
		if (*s == ')')
			s++;
		c = *s++;
	}
	else
	{
		n = 0;
		while ((c = *s++) >= '0' && c <= '9')
			n = n * 10 + c - '0';
		if (c == '.')
		{
			f = 0;
			while ((c = *s++) >= '0' && c <= '9')
			{
				d *= 10;
				n *= 10;
				f = f * 10 + c - '0';
			}
			n += f;
		}
	}
	for (;;)
	{
		switch (c)
		{
		case 'P':
			n *= 72;
			break;
		case 'c':
			n *= 170;
			break;
		case 'i':
			n *= 432;
			break;
		case 'm':
			n *= 8 * state.env->ps.current;
			break;
		case 'n':
		case 'w':
			n *= 6 * state.env->ps.current;
			break;
		case 'p':
			n *= 6;
			break;
		case 's':
		case 'u':
		case 'z':
			break;
		case 'v':
			n *= 6 * state.env->vs.current;
			break;
		default:
			s--;
			if (c = scale)
			{
				scale = 0;
				continue;
			}
			break;
		}
		break;
	}
	n /= d;
	if (e)
	{
		while (isspace(*s))
			s++;
		*e = s;
	}
	if (abs)
		return 1;
	if (neg)
		n = -n;
	return n;
}

/*
 * convert units to scale
 */

static long
convert(long n, int up, int scale)
{
	long	m;

	m = n;
	switch (scale)
	{
	case 'M':
		if (up)
			n *= state.env->ps.current / 12;
		else
			n /= state.env->ps.current / 12;
		break;
	case 'P':
		if (up)
			n *= 72;
		else
			n /= 72;
		break;
	case 'c':
		if (up)
			n *= 170;
		else
			n /= 170;
		break;
	case 'i':
		if (up)
			n *= 432;
		else
			n /= 432;
		break;
	case 'm':
		if (up)
			n *= 8 * state.env->ps.current;
		else
			n /= 8 * state.env->ps.current;
		break;
	case 'n':
	case 'w':
		if (up)
			n *= 6 * state.env->ps.current;
		else
			n /= 6 * state.env->ps.current;
		break;
	case 'p':
		if (up)
			n *= 6;
		else
			n /= 6;
		break;
	case 's':
	case 'u':
	case 'z':
		break;
	case 'v':
		if (up)
			n *= 6 * state.env->vs.current;
		else
			n /= 6 * state.env->vs.current;
		break;
	}
	return n ? n : (m > 0) ? 1 : 0;
}

/*
 * evaluate numeric expression in s
 */

static long
expression(char* s, char** e, int scale)
{
	long	n;
	long	m;

	n = operand(s, &s, scale);
	for (;;)
	{
		switch (*s++)
		{
		case CODE_2:
			s++;
			/*FALLTHROUGH*/
		case CODE_1:
			s++;
			/*FALLTHROUGH*/
		case CODE_0:
			continue;
		case '+':
			n += operand(s, &s, scale);
			continue;
		case '-':
			n -= operand(s, &s, scale);
			continue;
		case '/':
			if (m = operand(s, &s, scale))
				n /= m;
			else
				n = 0;
			continue;
		case '*':
			n *= operand(s, &s, scale);
			continue;
		case '%':
			if (m = operand(s, &s, scale))
				n %= m;
			else
				n = 0;
			continue;
		case '<':
			if (state.groff && *s == '?')
			{
				m = operand(s + 1, &s, scale);
				if (m < n)
					n = m;
			}
			else if (*s == '=')
				n = n <= operand(s + 1, &s, scale);
			else
				n = n < operand(s, &s, scale);
			continue;
		case '>':
			if (state.groff && *s == '?')
			{
				m = operand(s + 1, &s, scale);
				if (m > n)
					n = m;
			}
			else if (*s == '=')
				n = n >= operand(s + 1, &s, scale);
			else
				n = n > operand(s, &s, scale);
			continue;
		case '=':
			if (*s == '=')
				s++;
			n = n == operand(s, &s, scale);
			continue;
		case '&':
			if (*s == '&')
				s++;
			n = (operand(s, &s, scale) > 0) && (n > 0);
			continue;
		case ':':
			if (*s == ':')
				s++;
			n = (operand(s, &s, scale) > 0) || (n > 0);
			continue;
		default:
			s--;
			break;
		}
		break;
	}
	if (e)
	{
		while (isspace(*s))
			s++;
		*e = s;
	}
	if (scale)
		n = convert(n, 0, scale);
	return n;
}

/*
 * evaluate a conditional expression
 */

static long
cond(register char* s, char** e)
{
	register int	c;
	register int	i;
	register int	q;
	register long	v;
	register char*	t;
	char*		u;

	while (isspace(*s))
		s++;
	if (i = *s == '!')
		s++;
	while (isspace(*s))
		s++;
	switch (c = *s)
	{
	case '0':
	case '1':
	case '2':
	case '3':
	case '4':
	case '5':
	case '6':
	case '7':
	case '8':
	case '9':
	case '-':
	case '+':
	case '(':
		v = expression(s, &u, 0) > 0;
		s = u;
		break;
	case 'c':
		s++;
		if (*s == 'c' && *(s + 1) == 'h')
			s += 2;
		v = 0;
		break;
	case 'd':
	case 'r':
		t = s;
		while (*++s && !isspace(*s));
		q = *s;
		*s = 0;
		*t = c == 'd' ? '.' : 'n';
		v = hashget(state.symbols, t) != 0;
		*t = c;
		*s = q;
		break;
	case 'g':
		GROFFINIT();
		v = state.groff != 0;
		s++;
		break;
	case 'o':
	case 't':
		v = 1;
		s++;
		break;
	default:
		if (isalpha(c))
		{
			v = 0;
			s++;
		}
		else
		{
			for (u = ++s; *s && *s != c; s++);
			if (*s)
			{
				*s++ = 0;
				for (t = s; *s && *s != c; s++);
				if (*s)
					*s++ = 0;
				v = !strcmp(u, t);
			}
			else 
				v = 0;
		}
		break;
	}
	if (i)
		v = !v;
	while (isspace(*s))
		s++;
	if (e)
		*e = s;
	return v;
}

static void	expand(Sfio_t*, char*);

/*
 * while test with expansion
 */

static long
test(register char* s, char** e, int* f)
{
	register char*	t;
	char*		q;
	int		n;
	long		v;

	if (state.ec)
	{
		t = s;
		while (t = strchr(t, state.ec))
			if (*++t != '{' && *t != '}')
				break;
		if (t)
		{
			expand(state.arg, s);
			sfputr(state.tmp, sfstruse(state.arg), 0);
			s = sfstruse(state.tmp);
		}
	}
	v = cond(s, &q);
	n = 0;
	for (t = q; *t; t++)
	{
		if (*t == state.ec)
		{
			if (*(t + 1) == '{' || isspace(*(t + 1)))
			{
				t++;
				n = COND_BLOCK;
			}
			else
				break;
		}
		else if (!isspace(*t))
			break;
	}
	if (e)
		*e = t;
	if (f)
		*f = n;
	return v;
}

/*
 * pop input stream
 * return non-0 on EOF
 */

static int
popin(void)
{
	register Pushin_t*	pp;
	int			n;

	if (state.in_top <= state.in_stack)
	{
		state.in--;
		return 1;
	}
	pp = state.in_top;
	if (pp->loop)
	{
		state.in = (unsigned char*)sfstrbase(pp->loop);
		return 0;
	}
	if (--pp == state.in_stack)
	{
		if (state.cond.level > 1 && state.verbose)
			error(1, "%d missing .el request%s", state.cond.level - 1, state.cond.level == 2 ? "" : "s");
		state.cond.level = 0;
	}
	if (pp->ip)
	{
		if (state.in < pp->end)
			return 0;
		if ((n = sfread(pp->ip, pp->buf, SF_BUFSIZE)) > 0)
		{
			pp->end = pp->buf + n;
			*pp->end++ = EOF;
			state.in = pp->buf;
			return 0;
		}
		if (pp->ip && pp->ip != sfstdin)
			sfclose(pp->ip);
		free(pp->buf - UNGET_MAX);
	}
	state.in_top = pp;
	if (state.noline > 0)
		state.noline--;
	if ((error_info.file = pp->file) == state.original.file)
	{
		state.original.file = 0;
		state.original.line = 0;
	}
	error_info.line = pp->line;
	if (pp->tag)
	{
		state.tag = pp->tag;
		state.mac = pp->mac;
	}
	state.in = pp->in;
	return 0;
}

/*
 * copy <type><name> into buf of size n
 */

static char*
nam(int type, register char* name, char* buf, size_t n)
{
	register char*	t = buf;
	register char*	e = t + n - 1;

	*t++ = type;
	if ((*t++ = *name++) && (*t++ = *name))
	{
		if (state.groff)
			while (t < e && (*t++ = *++name));
		else
			*t = 0;
	}
	return buf;
}

/*
 * return number register pointer given name
 * pointer created if not found
 */

static Num_t*
num(register char* s)
{
	register Num_t*	np;
	char		buf[MAXNAME];

	if (!(np = (Num_t*)hashget(state.symbols, nam('n', s, buf, sizeof(buf)))))
	{
		if (!(np = newof(0, Num_t, 1, 0)))
			error(ERROR_SYSTEM|3, "out of space [number]");
		np->name = hashput(state.symbols, NiL, np);
		np->format = '1';
		np->increment = 1;
	}
	return np;
}

/*
 * lookup or create (force!=0) io stream handle
 */

static Stream_t*
iop(char* s, int force)
{
	register Stream_t*	sp;
	char			buf[MAXNAME];

	if (!(sp = (Stream_t*)hashget(state.symbols, nam('s', s, buf, sizeof(buf)))))
	{
if (!force) error(1, "iop %s lookup failed", buf);
		if (!force)
			return 0;
		if (!(sp = newof(0, Stream_t, 1, 0)))
			error(ERROR_SYSTEM|3, "out of space [stream]");
		hashput(state.symbols, NiL, sp);
	}
	return sp;
}

/*
 * push output stream np
 */

static void
pushout(Sfio_t* np)
{
	if (state.out_top >= &state.out_stack[elementsof(state.out_stack)])
		error(3, "output stack overflow");
	*state.out_top++ = state.out;
	iop("stdout", 1)->sp = state.out = np;
}

/*
 * pop ouput stream
 * if retain>0 then strdup() buffer
 * append an extra 0 for easy arg '\n' append by if/ie/el
 */

static char*
popout(void)
{
	char*	s;

	if (!DIVERTED())
		return 0;
	if (state.out == state.tag->sp)
		sfputc(state.out, 0);
	if (state.out == state.nul)
		s = 0;
	else
		s = sfstruse(state.out);
	iop("stdout", 1)->sp = state.out = *--state.out_top;
	return s;
}

/*
 * return next character with possible line join
 */

static int
nextchar(void)
{
	int	c;
	int	n;

	for (;;)
	{
		if ((c = GETC()) == EOF)
		{
			if (popin())
				break;
			continue;
		}
		if (c == state.ec)
		{
		escape:
			switch (n = GETC())
			{
			case EOF:
				if (popin())
					break;
				goto escape;
			case '\n':
				error_info.line++;
				continue;
			case 't':
				c = '\t';
				break;
			case '"':
				for (;;)
				{
					switch (c = GETC())
					{
					case EOF:
						if (popin())
							break;
						continue;
					case '\n':
						break;
					default:
						if (c == state.ec)
						{
						escape_2:
							switch (n = GETC())
							{
							case EOF:
								if (popin())
									break;
								goto escape_2;
							case '\n':
								error_info.line++;
								continue;
							default:
								continue;
							}
							break;
						}
						continue;
					}
					break;
				}
				break;
			default:
				UNGETC(n);
				break;
			}
			break;
		}
		break;
	}
	return c;
}

/*
 * call a macro
 */

static void
macro(Tag_t* tp, Arg_t* ap)
{
	if (tp->body)
		pushin(tp->file, tp->line, NiL, tp->body, ap);
}

/*
 * set macro value
 */

static void
set(Tag_t* mp, char* value, int append)
{
	int	n;

	if (append && mp->body)
	{
		sfputr(state.tmp, mp->body, -1);
		sfputr(state.tmp, value, -1);
		value = sfstruse(state.tmp);
	}
	n = strlen(value) + 1;
	if (mp->size < n)
	{
		if (mp->size)
			free(mp->body);
		mp->size = n = roundof(n, 64);
		if (!(mp->body = newof(0, char, n, 0)))
			error(ERROR_SYSTEM|3, "out of space [set value]");
	}
	strcpy(mp->body, value);
	mp->call = macro;
	mp->flags |= TAG_PASS;
	if (!mp->line)
	{
		mp->line = error_info.line;
		mp->file = error_info.file;
	}
	if (mp->flags & TAG_TRACE_SET)
		error(2, "set macro %s `%s'", mp->name + 1, mp->body);
}

/*
 * return macro pointer given name
 * pointer created if not found
 */

static Tag_t*
mac(char* name)
{
	register Tag_t*	mp;
	char		buf[MAXNAME];

	if (!(mp = (Tag_t*)hashget(state.symbols, nam('.', name, buf, sizeof(buf)))))
	{
		if (!(mp = newof(0, Tag_t, 1, 0)))
			error(ERROR_SYSTEM|3, "out of space [mac]");
		mp->name = hashput(state.symbols, NiL, mp);
	}
	return mp;
}

static Env_t*
env(char* s)
{
	register Env_t*	v;
	char*		e;
	long		n;
	char		buf[MAXNAME];

	n = expression(s, &e, 0);
	if (*e)
		sfsprintf(buf, sizeof(buf), "E%s", s);
	else
		sfsprintf(buf, sizeof(buf), "E%d", n);
	if (!(v = (Env_t*)hashget(state.symbols, buf)))
	{
		if (!(v = newof(0, Env_t, 1, 0)))
			error(ERROR_SYSTEM|3, "out of space [environment]");
		v->name = hashput(state.symbols, NiL, v);
	}
	if (v->generation < state.generation)
	{
		v->generation = state.generation;
		v->c2 = DEFAULT_c2;
		v->cc = DEFAULT_cc;
		v->ft.current = v->ft.previous = DEFAULT_ft;
		v->nf = 0;
		v->ps.current = v->ps.previous = DEFAULT_ps;
		v->ss = 0;
		v->vs.current = v->vs.previous = DEFAULT_vs;
	}
	return v;
}

/*
 * get macro value by name
 */

static char*
get(char* name)
{
	register Tag_t*	mp;

	return (mp = (Tag_t*)hashget(state.symbols, name)) && mp->body ? mp->body : "";
}

/*
 * open name for reading
 * looking in state.dirs if needed
 * verbose<0 for -m option
 */

static Sfio_t*
find(char* name, char** found, int verbose)
{
	register char*		s;
	register Sfio_t*	sp;
	char*			path;
	char*			t;
	char*			x;
	int			i;
	Dir_t*			dp;
	char			buf[PATH_MAX];

	static const char*	pathdirs[] =
				{
					"lib/tmac/tmac.%s",
					"lib/tmac/%s",
					"lib/html/%s",
					"lib/html/m%s.tr",
					"share/groff/tmac/tmac.%s",
					"share/groff/tmac/%s",
					"groff/share/groff/tmac/tmac.%s",
					"groff/share/groff/tmac/%s",
				};

	if (verbose >= 0 && (sp = sfopen(NiL, name, "r")))
	{
		sfprintf(state.tmp, "/%s", name);
		path = sfstruse(state.tmp);
		goto hit;
	}
	if (*name != '/')
	{
		if (*name == '.' && (x = getenv("HOME")))
		{
			sfprintf(state.tmp, "/%s/%s", x, name);
			path = sfstruse(state.tmp);
			if (sp = sfopen(NiL, path + 1, "r"))
				goto hit;
		}
		for (dp = state.dirs; dp; dp = dp->next)
		{
			if (verbose >= 0)
			{
				sfprintf(state.tmp, "/%s/%s", dp->name, name);
				path = sfstruse(state.tmp);
				if (sp = sfopen(NiL, path + 1, "r"))
					goto hit;
			}
			sfprintf(state.tmp, "/%s/tmac.%s", dp->name, name);
			path = sfstruse(state.tmp);
			if (sp = sfopen(NiL, path + 1, "r"))
				goto hit;
		}
		for (i = 0; i < elementsof(pathdirs); i++)
		{
			sfprintf(state.tmp, pathdirs[i], name);
			path = sfstruse(state.tmp);
			if (pathpath(buf + 1, path, "", PATH_REGULAR|PATH_READ) && (sp = sfopen(NiL, buf + 1, "r")))
			{
				*(path = buf) = '/';
				goto hit;
			}
		}
	}
	if (verbose > 0)
	{
		/*
		 * some systems provide
		 *	/usr/lib/tmac.mXXX
		 * that references
		 *	/usr/lib/macros/mXXX[tn]
		 * that is not provided
		 */

		if (*(s = name) == '/')
			while (s = strchr(s, '/'))
				if (!strncmp(++s, "macros/", 7) && *(s += 7))
				{
					t = s + strlen(s) - 1;
					x = strchr(s, '.') ? "" : ".tr";
					while (*s)
					{
						sfprintf(state.tmp, "lib/html/%s%s", s, x);
						path = sfstruse(state.tmp);
						if (pathpath(buf + 1, path, "", PATH_REGULAR|PATH_READ) && (sp = sfopen(NiL, buf + 1, "r")))
						{
							*(path = buf) = '/';
							goto hit;
						}
						if (*t != 't')
							break;
						*t-- = 0;
					}
				}
		error(ERROR_SYSTEM|2, "%s: cannot read", name);
	}
	else if (verbose < 0)
		error(ERROR_SYSTEM|3, "-m%s: cannot find macro package", name);
	return 0;
 hit:
	message((-1, "FIND %s => %s", name, path + 1));
	if (found)
		*found = hashput(state.symbols, path, path + 1) + 1;
	s = path + strlen(path);
	while (s > path + 2)
		if (*--s == '/')
		{
			if (!strcmp(s, "/lib"))
			{
				*s = 0;
				set(mac(".P"), path + 1, 0);
				break;
			}
			*s = 0;
		}
	return sp;
}

/*
 * generate intermediate CODE_0
 */

static void
code_0(void)
{
	sfputc(state.out, CODE_0);
}

/*
 * generate intermediate CODE_1
 */

static void
code_1(int code)
{
	sfputc(state.out, CODE_1);
	sfputc(state.out, code);
}

/*
 * generate intermediate CODE_2
 */

static void
code_2(int code, int data)
{
	sfputc(state.out, CODE_2);
	sfputc(state.out, data);
	sfputc(state.out, code);
}

/*
 * generate intermediate CODE_n
 */

static void
code_n(int code, char* s)
{
	int	n;

	sfputc(state.out, CODE_n);
	n = s ? (strlen(s) + 1) : 1;
	if (n > 0177)
		sfputc(state.out, ((n >> 8) & 0177) | 0200);
	sfputc(state.out, n & 0177);
	sfputc(state.out, code);
	if (s)
		sfputr(state.out, s, 0);
	else
		sfputc(state.out, 0);
}

/*
 * join all args from n on into 1 arg
 */

static char*
join(Arg_t* ap, int n)
{
	if (n < 0 || ap->argc < n)
		return 0;
	n++;
	while (ap->argc >= n)
		*(ap->argv[ap->argc--] - 1) = ' ';
	return ap->argv[n - 1];
}

/*
 * output error message line
 */

static void
notify(register char* s)
{
	register int	c;
	Sfoff_t		p;

	p = sftell(sfstderr);
	for (;;)
	{
		switch (c = *s++)
		{
		case 0:
			break;
		case CODE_0:
			continue;
		case CODE_1:
			s++;
			continue;
		case CODE_2:
			c = *s++;
			if (*s++ == OP_cc)
				sfputc(sfstderr, c);
			continue;
		default:
			sfputc(sfstderr, c);
			continue;
		}
		break;
	}
	if (sftell(sfstderr) != p)
		sfputc(sfstderr, '\n');
}

/*
 * add trap s to xp
 */

static void
trap(Trap_t** xp, char* s)
{
	Trap_t*	ip;
	Trap_t*	pp;

	for (pp = 0, ip = *xp; ip; pp = ip, ip = ip->next)
		if (streq(ip->name + 1, s))
			return;
	if (!(ip = newof(0, Trap_t, 1, strlen(s) + 1)))
		error(ERROR_SYSTEM|3, "out of space [trap]");
	*ip->name = '.';
	strcpy(ip->name + 1, s);
	if (pp)
		pp->next = ip;
	else
		*xp = ip;
}

/*
 * trigger traps on xp
 */

static void
trigger(Trap_t** xp)
{
	Trap_t*	ip;
	Trap_t*	pp;

	if (!DIVERTED() && (ip = *xp))
	{
		state.t = 1;
		do
		{
			sfputr(state.req, ip->name, '\n');
			pp = ip;
			ip = ip->next;
			free(pp);
		} while (ip);
		*xp = 0;
		pushin(xp == &state.fini ? "[*EOF*]" : "[*TRAP*]", 1, NiL, sfstruse(state.req), NiL);
	}
}

/*
 * return value for register name with type
 * and increment/decrement i={0,'+','-'}
 */

static char*
value(char* name, int type, int i)
{
	register Num_t*	np;
	register char*	b;
	register char*	x;
	register long	n;
	register long	m;
	Tag_t*		tp;
	char		buf[8];

	b = hashget(state.symbols, name);
	switch (type)
	{
	case 'g':
		if (!(np = (Num_t*)b))
			return "";
		b = np->value;
		if (isalnum(np->format))
			*b++ = np->format;
		else
		{
			for (n = 0; n < np->format; n++)
				*b++ = '0';
			*b++ = '1';
		}
		*b = 0;
		return np->value;
	case 'j':
		return "";
	case 'n':
		break;
	case '.':
		if (!(tp = (Tag_t*)b))
			return 0;
		if (tp->flags & TAG_TRACE_GET)
			error(2, "get macro %s `%s'", tp->name + 1, tp->body);
		return tp->body;
	default:
		return b;
	}
	if (!b)
	{
		name = strcpy(buf, "n");
		if (!(b = (char*)hashget(state.symbols, name)))
			return "0";
	}
	np = (Num_t*)b;
	if (np->flags & TAG_TRACE_GET)
		error(2, "get register %s %d %d", np->name + 1, np->number, np->increment);
	if (np->internal)
	{
		switch (INDEX(name[1], name[2]))
		{
		case INDEX('%',0):
			n = np->number;
			break;
		case INDEX('.','$'):
			n = state.mac ? state.mac->argc : 0;
			break;
		case INDEX('.','C'):
			GROFFINIT();
			n = state.groff == 0;
			break;
		case INDEX('.','F'):
			return state.original.file ? state.original.file : error_info.file;
		case INDEX('.','H'):
			n = 3000;
			break;
		case INDEX('.','L'):
			n = 1;
			break;
		case INDEX('.','P'):
			n = 1;
			break;
		case INDEX('.','R'):
			n = 256;
			break;
		case INDEX('.','V'):
			n = 6000;
			break;
		case INDEX('.','c'):
			if (name[3] == 'e')
			{
				n = state.it.center;
				break;
			}
			/*FALLTHROUGH*/
		case INDEX('c','.'):
			n = state.original.line ? state.original.line : error_info.line;
			break;
		case INDEX('.','e'):
			if (name[3] == 'v')
				n = state.it.center; /*GROFF: string valued?*/
			break;
		case INDEX('.','f'):
			n = state.env->ft.current;
			break;
		case INDEX('.','g'):
			GROFFINIT();
			n = state.groff != 0;
			break;
		case INDEX('.','i'):
			n = state.env->in.current;
			break;
		case INDEX('.','k'):
			n = 100;
			break;
		case INDEX('.','l'):
			n = state.env->ll.current;
			break;
		case INDEX('.','n'):
			n = state.n;
			break;
		case INDEX('.','p'):
			if (name[3] == 'n')
				n = 2;
			else if (name[3] == 's' && name[4] == 'r')
				n = state.env->ps.previous;
			else
				n = state.env->ps.current;
			break;
		case INDEX('.','r'):
			n = 0;
			break;
		case INDEX('.','s'):
			if (name[3] == 'r')
				n = state.env->ps.previous;
			else
				n = state.env->ps.current;
			break;
		case INDEX('.','t'):
			n = state.t;
			break;
		case INDEX('.','u'):
			n = !state.env->nf;
			break;
		case INDEX('.','v'):
			if (name[3] == 'p')
				n = 1;
			else
				n = 6 * state.env->vs.current;
			break;
		case INDEX('.','w'):
			if (name[3] == 'a')
				n = 0;
			else
				n = 6 * state.env->ps.current;
			break;
		case INDEX('.','x'):
			n = 19980401;
			break;
		case INDEX('.','y'):
			n = 1;
			break;
		case INDEX('.','z'):
			return state.divert ? state.divert->tag->name : "";
		case INDEX('d','l'):
			if (state.test & 0x1000) n = 0; else
			n = state.dl;
			break;
		case INDEX('d','n'):
			if (state.test & 0x2000) n = 0; else
			n = state.dn;
			break;
		case INDEX('l','n'):
			if (state.test & 0x4000) n = 0; else
			n = state.ln;
			break;
		case INDEX('n','l'):
			if (!(state.test & 0x8000)) n = 0; else
			n = state.nl;
			break;
		default:
			n = 0;
			break;
		}
		np->number = n;
	}
	if (i)
	{
		switch (i)
		{
		case '+':
			np->number += np->increment;
			break;
		case '-':
			np->number -= np->increment;
			break;
		}
		if (np->flags & TAG_TRACE_SET)
			error(2, "set register %s %d %d", np->name + 1, np->number, np->increment);
	}
	n = np->number;
	b = np->value;
	switch (np->format)
	{
	case '1':
		sfsprintf(b, sizeof(np->value), "%ld", np->number);
		break;
	case 'A':
	case 'a':
		x = islower(np->format) ? "abcdefghijklmnopqrstuvwxyz" : "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		if (n < 0)
		{
			n = -n;
			*b++ = '-';
		}
		for (m = 1; m > 0 && m < n; m *= 26);
		if (m <= 0)
			sfsprintf(b, sizeof(buf) - 1, "<%ld>", n);
		else
		{
			for (; m > 0; m /= 26)
			{
				i = n / m;
				n -= m * i;
				*b++ = x[i];
			}
			*b = 0;
		}
		break;
	case 'I':
	case 'i':
		x = islower(np->format) ? "zwmdclxvi" : "ZWMDCLXVI";
		if (n <= -40000 || n >= 40000)
			sfsprintf(b, sizeof(buf), "<%ld>", n);
		else if (!n)
			sfsprintf(b, sizeof(buf), "0");
		else
		{
			if (n < 0)
			{
				n = -n;
				*b++ = '-';
			}
			while (n >= 10000)
			{
				n -= 10000;
				*b++ = x[0];
			}
			for (i = 1000; i > 0; i /= 10, x += 2)
			{
				m = n / i;
				n -= m * i;
				switch (m)
				{
				case 9:
					*b++ = x[2];
					*b++ = x[0];
					break;
				case 8:
					*b++ = x[1];
					*b++ = x[2];
					*b++ = x[2];
					*b++ = x[2];
					break;
				case 7:
					*b++ = x[1];
					*b++ = x[2];
					*b++ = x[2];
					break;
				case 6:
					*b++ = x[1];
					*b++ = x[2];
					break;
				case 5:
					*b++ = x[1];
					break;
				case 4:
					*b++ = x[2];
					*b++ = x[1];
					break;
				case 3:
					*b++ = x[2];
					/*FALLTHROUGH*/
				case 2:
					*b++ = x[2];
					/*FALLTHROUGH*/
				case 1:
					*b++ = x[2];
					break;
				}
			}
			*b = 0;
		}
		break;
	default:
		sfsprintf(b, sizeof(np->value), "%0*ld", np->format, np->number);
		break;
	}
	return np->value;
}

/*
 * return the value for typed register
 */

static char*
interpolate(int type)
{
	register int	c;
	register char*	b;
	register char*	x;
	register char*	t;
	int		i;
	int		k;
	char		buf[MAXNAME];

	i = k = 0;
	b = buf;
	*b++ = (type == 'g' || type == 'j') ? 'f' : type;
	x = buf + 2;
	do
	{
		switch (c = GETC())
		{
		case EOF:
			if (popin())
				return 0;
			continue;
		case '\n':
			error_info.line++;
			break;
		case '(':
			if (x == buf + 2)
			{
				x++;
				continue;
			}
			break;
		case '[':
			if (state.groff && x == buf + 2)
			{
				x = buf + sizeof(buf) - 2;
				k = 1;
				continue;
			}
			break;
		case ']':
			if (k)
				goto done;
			break;
		case '-':
		case '+':
			if (type == 'n' && b == buf + 1)
			{
				i = c;
				continue;
			}
			break;
		default:
			if (c == state.ec)
			{
			escape:
				switch (c = GETC())
				{
				case EOF:
					if (popin())
						return 0;
					goto escape;
				case '\n':
					error_info.line++;
					continue;
				case '*':
					c = '.';
					/*FALLTHROUGH*/
				case 'n':
					if (t = interpolate(c))
						while (b < x && (*b = *t++))
							b++;
					continue;
				}
			}
			break;
		}
		*b++ = c;
	} while (b < x);
 done:
	*b = 0;
	return value(buf, type, i);
}

/*
 * switch to font s
 */

static void
ft(char* s)
{
	int	c;

	if (!s || (c = *s) < '0' || c > '5')
		c = 0;
	else
		c -= '0';
	if (!c)
		c = state.env->ft.previous;
	state.env->ft.previous = state.env->ft.current;
	state.env->ft.current = c;
	code_1(OP_ft + c);
}

/*
 * switch to point size n
 */

static void
ps(int n)
{
	state.env->ps.previous = state.env->ps.current;
	state.env->ps.current = n;
	code_2(OP_ps, n);
}

/*
 * sync list state
 */

static void
li(int force)
{
	List_t*	list = state.list;

	state.it.dc = 0;
	if (state.divert || state.env->nf)
	{
		if (state.env->nf)
			state.it.dl = 0;
		return;
	}
	if (force)
	{
		while (state.env->in.current < state.list->in)
		{
			message((-2, "DROP %d %d %d:%d %d:%d", state.list - state.list_stack, state.list->dl, state.list->ti, state.list->in, force ? state.env->ti.current : 0, state.env->in.current));
			if (state.list->dl)
				code_1(END(OP_dl));
			state.list--;
		}
	}
	else
		message((-2, "TEST %d %d %d:%d %d:%d ops=%d df=%d dc=%d dl=%d", state.list - state.list_stack, state.list->dl, state.list->ti, state.list->in, force ? state.env->ti.current : 0, state.env->in.current, state.list - list, force, state.it.dc, state.it.dl));
	if (state.env->in.current > state.list->in || force > 0 && state.env->ti.current > state.list->in)
	{
		if (state.list >= &state.list_stack[elementsof(state.list_stack)-1])
			error(2, "list stack overflow");
		else
			state.list++;
		state.list->dl = 0;
		state.list->in = state.env->in.current;
		state.list->ti = state.env->in.current > state.list->in ? state.env->in.current : state.env->ti.current;
		state.it.dl++;
	}
	else if (!force && state.env->in.current < state.list->in)
		state.it.dc = 1;
	message((-2, "LIST %d %d %d:%d %d:%d ops=%d df=%d dc=%d dl=%d", state.list - state.list_stack, state.list->dl, state.list->ti, state.list->in, force ? state.env->ti.current : 0, state.env->in.current, state.list - list, force, state.it.dc, state.it.dl));
}

/*
 * pop inline traps
 */

static void
it(void)
{
	register List_t*	p;

	if (state.env->nf)
	{
		if (state.env->in.current > 0)
			sfputc(state.out, '\t');
	}
	else if (!DIVERTED())
	{
		if (state.it.dc && ISFILE())
			li(-1);
		p = state.list;
		while (state.it.dl > 0)
		{
			state.it.dl--;
			for (; p >= state.list_stack; p--)
				if (!p->dl)
				{
					p->dl = 1;
					code_1(OP_dl);
					break;
				}
		}
		if (state.it.dt)
		{
			state.it.dt = 0;
			code_1((state.list - state.list_stack) <= 1 ? LABEL(OP_dt) : OP_dt);
			state.it.dd = 1;
		}
		else if (state.it.dd)
		{
			state.it.dd = 0;
			code_1(OP_dd);
		}
	}
}

/*
 * initialize number register
 */

static void
nr(char* s, int v, int format, int internal)
{
	register Num_t*	np;

	np = num(s);
	np->number = v;
	np->format = format;
	if (internal > 0)
		np->internal = internal;
}

/*
 * set time vars to t
 */

static void
tm(time_t t)
{
	Tm_t*	xp;

	state.date = t;
	xp = tmmake(&t);
	nr("dw", xp->tm_wday + 1, 0, 0);
	nr("dy", xp->tm_mday, 0, 0);
	nr("mo", xp->tm_mon + 1, 0, 0);
	nr("yr", xp->tm_year % 100, 2, 0);
	nr("YR", 1900 + xp->tm_year, 4, 0);
}

/*
 * expand if expression s into op
 */

static void
expand(register Sfio_t* op, register char* s)
{
	int	c;
	int	d;
	int	q;
	int	v;
	int	w;
	int	z;
	char*	t;
	char*	b;
	char	buf[MAXNAME];

	for (;;)
	{
		switch (c = *s++)
		{
		case 0:
			break;
		case CODE_2:
			sfputc(op, c);
			c = *s++;
			/*FALLTHROUGH*/
		case CODE_1:
			sfputc(op, c);
			c = *s++;
			/*FALLTHROUGH*/
		case CODE_0:
			sfputc(op, c);
			continue;
		default:
			if (c == state.ec)
				switch (w = c = *s++)
			{
			case 0:
				s--;
				break;
			case 'A':
			case 'C':
			case 'L':
			case 'N':
			case 'R':
			case 'V':
			case 'X':
			case 'Y':
			case 'Z':
			case 'b':
			case 'h':
			case 'l':
			case 'o':
			case 'w':
			case 'x':
				v = 0;
				c = *s++;
				if (c == '(')
					z = 2;
				else
				{
					z = -1;
					if (state.groff && c == '[')
						c = ']';
				}
				b = s;
				for (;;)
				{
					switch (d = *s++)
					{
					case 0:
						s--;
						break;
					case CODE_2:
						s++;
						/*FALLTHROUGH*/
					case CODE_1:
						if (*s++ == OP_cc)
							v++;
						/*FALLTHROUGH*/
					case CODE_0:
						continue;
					default:
						if (d == state.ec)
						{
							switch (d = *s++)
							{
							case 0:
								s--;
								break;
							case 'A':
							case 'C':
							case 'L':
							case 'N':
							case 'R':
							case 'V':
							case 'X':
							case 'Y':
							case 'Z':
							case 'b':
							case 'h':
							case 'l':
							case 'o':
							case 'w':
							case 'x':
								q = *s++;
								for (;;)
								{
									switch (d = *s++)
									{
									case 0:
										s--;
										break;
									case CODE_2:
										s++;
										/*FALLTHROUGH*/
									case CODE_1:
										s++;
										/*FALLTHROUGH*/
									case CODE_0:
										continue;
									default:
										if (d == q)
											break;
										continue;
									}
									break;
								}
								v++;
								break;
							case 'f':
							case 's':
								if (*s)
									s++;
								break;
							case 'g':
							case 'j':
								t = buf;
								*t++ = 'n';
								if ((*t++ = *s++) == '(')
								{
									*(t - 1) = *s++;
									*t++ = *s++;
								}
								*t = 0;
								if (t = value(buf, d, 0))
									v += strlen(t);
								break;
							}
						}
						else if (z > 0)
							z--;
						else if (d == c || z == 0)
							break;
						v++;
						continue;
					}
					break;
				}
				switch (w)
				{
				case 'A':
					sfprintf(op, "1");
					break;
				case 'R':
				case 'X':
				case 'V':
					c = *(s - 1);
					*(s - 1) = 0;
					switch (w)
					{
					case 'R':
						/*HERE*/
						break;
					case 'V':
						if (t = getenv(b))
							sfprintf(op, "%s", t);
						break;
					case 'X':
						/*HERE*/
						break;
					}
					*(s - 1) = c;
					break;
				case 'w':
					sfprintf(op, "%d", convert(v, 1, w));
					break;
				}
				break;
			case 'g':
			case 'j':
				t = buf;
				*t++ = 'n';
				if ((*t++ = *s++) == '(')
				{
					*(t - 1) = *s++;
					*t++ = *s++;
				}
				*t = 0;
				if (t = value(buf, c, 0))
					sfputr(op, t, -1);
				break;
			default:
				sfputc(op, state.ec);
				sfputc(op, c);
				break;
			}
			else
				sfputc(op, c);
			continue;
		}
		break;
	}
}

/*
 * remove macro/register s
 */

static void
rm(char* s)
{
	register Tag_t*	mp;
	char		buf[MAXNAME];

	if (mp = (Tag_t*)hashget(state.symbols, nam('.', s, buf, sizeof(buf))))
	{
		if (mp->flags & TAG_TRACE_SET)
			error(2, "remove macro %s", mp->name + 1);
		if (mp->size)
			free(mp->body);
		if (!(mp->flags & TAG_STATIC))
			free(mp);
		hashput(state.symbols, NiL, NiL);
	}
	if (mp = (Tag_t*)hashget(state.symbols, nam('n', s, buf, sizeof(buf))))
	{
		if (mp->flags & TAG_TRACE_SET)
			error(2, "remove register %s", mp->name + 1);
		if (mp->size)
			free(mp->body);
		if (!(mp->flags & TAG_STATIC))
			free(mp);
		hashput(state.symbols, NiL, NiL);
	}
}

/*
 * skip one conditional block
 * if op!=0 then copy block data to s
 */

static void
skip(char* s, int f, register Sfio_t* op)
{
	register int	c;
	register int	n;

	n = (f & COND_BLOCK) != 0;
	if (s)
		pushin(NiL, 0, NiL, s, NiL);
	else if (!n)
		return;
	for (;;)
	{
		switch (c = GETC())
		{
		case EOF:
			if (s && n <= 0)
				break;
			if (popin())
				break;
			continue;
		case CODE_2:
			if (op)
				sfputc(op, c);
			c = GETC();
			/*FALLTHROUGH*/
		case CODE_1:
			if (op)
				sfputc(op, c);
			c = GETC();
			/*FALLTHROUGH*/
		case CODE_0:
			if (op)
				sfputc(op, c);
			continue;
		case '\n':
			if (op)
				sfputc(op, c);
			error_info.line++;
			if (n <= 0)
				break;
			continue;
		default:
			if (op)
				sfputc(op, c);
			if (c == state.ec)
			{
			escape:
				switch (c = GETC())
				{
				case EOF:
					if (popin())
						break;
					goto escape;
				case '\n':
					if (op)
						sfputc(op, c);
					error_info.line++;
					continue;
				case '{':
					if (op)
						sfputc(op, c);
					n++;
					continue;
				case '}':
					if (op)
						sfputc(op, c);
					if (--n <= 0)
					{
						switch (c = nextchar())
						{
						case '\n':
							if (op)
								sfputc(op, c);
							error_info.line++;
							break;
						default:
							UNGETC(c);
							break;
						}
						break;
					}
					continue;
				default:
					if (op)
						sfputc(op, c);
					continue;
				}
				break;
			}
			continue;
		}
		break;
	}
	if (n > 0 && state.verbose)
		error(2, "unbalanced \\{ ... \\} block");
}

/*
 * internal groff requests
 */

static void
groff_aln(Tag_t* tp, Arg_t* ap)
{
	Tag_t*	xp;
	char	buf[MAXNAME];

	if (ap->argc != 2)
		error(1, "%s: two arguments expected", ap->argv[0]);
	else if (!(xp = (Tag_t*)hashget(state.symbols, nam('n', ap->argv[2], buf, sizeof(buf)))))
		error(1, "%s: not defined", buf);
	else
		hashput(state.symbols, nam('n', ap->argv[1], buf, sizeof(buf)), xp);
}

static void
groff_als(Tag_t* tp, Arg_t* ap)
{
	Tag_t*	xp;
	char	buf[MAXNAME];

	if (ap->argc != 2)
		error(1, "%s: two arguments expected", ap->argv[0]);
	else if (!(xp = (Tag_t*)hashget(state.symbols, nam('.', ap->argv[2], buf, sizeof(buf)))))
		error(1, "%s: not defined", buf);
	else
		hashput(state.symbols, nam('.', ap->argv[1], buf, sizeof(buf)), xp);
}

static void
groff_asciify(Tag_t* tp, Arg_t* ap)
{
	static int	warned;

	if (!warned++)
		error(1, "%s: not implemented yet", tp->name);
}

static void
groff_break(Tag_t* tp, Arg_t* ap)
{
	register Pushin_t*	pp;

	for (pp = state.in_top; pp >= state.in_stack; pp--)
		if (pp->loop)
		{
			sfclose(pp->loop);
			pp->loop = 0;
			return;
		}
	error(1, "%s: outside of loop", tp->name);
}

static void
groff_chop(Tag_t* tp, Arg_t* ap)
{
	register char*	s;
	register Tag_t*	mp;

	if (ap->argc >= 1)
	{
		mp = mac(ap->argv[1]);
		if ((s = mp->body) && *s)
			*(s + strlen(s) - 1) = 0;
	}
}

static void
groff_close(Tag_t* tp, Arg_t* ap)
{
	Stream_t*	sp;

	if (ap->argc < 1)
	{
		error(1, "%s: one argument expected", ap->argv[0]);
		return;
	}
	if (!(sp = iop(ap->argv[1], 0)))
	{
		error(1, "%s: %s: stream not open", ap->argv[0], ap->argv[1]);
		return;
	}
	sfclose(sp->sp);
	sp->sp = 0;
}

static void
groff_continue(Tag_t* tp, Arg_t* ap)
{
	register Pushin_t*	pp;

	for (pp = state.in_top; pp >= state.in_stack; pp--)
		if (pp->loop)
		{
			pp->in = (unsigned char*)sfstrbase(pp->loop);
			return;
		}
	error(1, "%s: outside of loop", tp->name);
}

static void
groff_cp(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	if (ap->argc < 1 || expression(ap->argv[1], NiL, 0))
		state.groff &= 2;
	else
		state.groff |= 1;
}

static void
groff_open(Tag_t* tp, Arg_t* ap)
{
	char*		path;
	char*		mode;
	Stream_t*	sp;

	if (ap->argc < 2)
	{
		error(1, "%s: two arguments expected", ap->argv[0]);
		return;
	}
	mode = strlen(ap->argv[0]) > 5 ? (ap->argv[0] + 5) : "w";
	path = ap->argv[2];
	sp = iop(ap->argv[1], 1);
	if (*mode == 'w' || !streq(sp->path, path))
	{
		if (sp->sp)
		{
			sfclose(sp->sp);
			sp->sp = 0;
		}
		if (sp->path)
			free(sp->path);
		if (!(sp->path = strdup(path)))
			error(ERROR_SYSTEM|3, "out of space [stream path]");
	}
	if (!sp->sp && !(sp->sp = sfopen(NiL, path, mode)))
		error(ERROR_SYSTEM|2, "%s: %s: \"%s\" mode open error", ap->argv[0], path, mode);
}

static void
groff_pso(Tag_t* tp, Arg_t* ap)
{
	char*	s;
	Sfio_t*	sp;

	if (s = join(ap, 1))
	{
		if (!(sp = sfpopen(NiL, s, "r")))
			error(ERROR_SYSTEM|2, "%s: %s: command error", ap->argv[0], s);
		else
			pushin(NiL, 0, sp, NiL, NiL);
	}
}

static void
groff_rnn(Tag_t* tp, Arg_t* ap)
{
	Num_t*	mp;
	char*	s;

	NoP(tp);
	if (ap->argc >= 2)
	{
		s = ap->argv[2];
		rm(s);
		*--s = 'n';
		mp = num(ap->argv[1]);
		hashput(state.symbols, NiL, NiL);
		mp->name = hashput(state.symbols, s, mp);
	}
}

static void
groff_shift(Tag_t* tp, Arg_t* ap)
{
	register int	n;
	register int	i;
	register Arg_t*	pp;

	if (pp = state.mac)
	{
		n = (ap->argc >= 1) ? expression(ap->argv[1], NiL, 0) : 1;
		i = 0;
		while (n < pp->argc)
			pp->argv[++i] = pp->argv[++n];
		pp->argc = i;
	}
}

static void
groff_sy(Tag_t* tp, Arg_t* ap)
{
	char*	s;

	nr("systat", (long)((s = join(ap, 1)) ? system(s) : 0), 0, 0);
}

static void
groff_while(Tag_t* tp, Arg_t* ap)
{
	register char*	s;
	register char*	t;
	Sfio_t*		op;

	if (s = join(ap, 1))
	{
		if (!(op = sfstropen()))
			error(ERROR_SYSTEM|3, "out of space [%s]", tp->name);
		sfputr(op, ".ie", ' ');
		sfputr(op, s, '\n');
		t = s + strlen(s);
		while (t > s && isspace(*--t));
		if (t > s && *t == '{' && *(t - 1) == state.ec)
			skip(NiL, COND_BLOCK, op);
		sfputr(op, "\n.el .break\n", 0);
		pushin(NiL, 0, NiL, sfstrbase(op), NiL);
		state.in_top->loop = op;
	}
}

static void
groff_write(Tag_t* tp, Arg_t* ap)
{
	char*		s;
	Stream_t*	sp;

	if (ap->argc < 1)
	{
		error(1, "%s: at least one argument expected", ap->argv[0]);
		return;
	}
	if (!(sp = iop(ap->argv[1], 0)))
	{
		error(1, "%s: %s: stream not open", ap->argv[0], ap->argv[1]);
		return;
	}
	if (!(s = join(ap, 2)))
		s = "";
	if (*s == '"')
		s++;
	if (sfputr(sp->sp, s, '\n') < 0)
		error(ERROR_SYSTEM|2, "%s: %s: write error", ap->argv[0], ap->argv[1]);
}

/*
 * internal troff requests
 */

static void
troff_ab(Tag_t* tp, Arg_t* ap)
{
	char*	s;

	NoP(tp);
	if (!(s = join(ap, 1)))
		s = "User abort";
	notify(s);
	exit(1);
}

static void
troff_ad(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	if (ap->argc >= 1)
		switch (ap->argv[1][0])
		{
		case 'b':
		case 'B':
		case 'n':
		case 'N':
			break;
		case 'c':
		case 'C':
			break;
		case 'l':
		case 'L':
			break;
		case 'r':
		case 'R':
			break;
		}
}

static void
troff_af(Tag_t* tp, Arg_t* ap)
{
	Num_t*	np;
	char*	s;

	NoP(tp);
	if (ap->argc >= 2)
	{
		np = num(ap->argv[1]);
		switch (*(s = ap->argv[2]))
		{
		case '0':
			for (np->format = 0; *s++ == '0'; np->format++);
			break;
		case 'a':
		case 'A':
		case 'i':
		case 'I':
			np->format = *s;
			break;
		case '1':
		default:
			np->format = '1';
			break;
		}
	}
}

static void
troff_br(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	NoP(ap);
	if (state.it.dd)
	{
		state.it.dd = 0;
		code_1(OP_dd);
	}
	code_1(OP_br);
}

static void
troff_c2(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	state.env->c2 = (ap->argc >= 1) ? ap->argv[1][0] : DEFAULT_c2;
}

static void
troff_cc(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	state.env->cc = (ap->argc >= 1) ? ap->argv[1][0] : DEFAULT_cc;
}

static void
troff_ce(Tag_t* tp, Arg_t* ap)
{
	int	n;
	int	r;

	NoP(tp);
	n = (ap->argc >= 1) ? expression(ap->argv[1], NiL, 0) : 1;
	r = *(ap->argv[0] + 1) == 'r';
	if (n >= 1)
	{
		if (state.it.center && state.it.right != r)
		{
			state.it.center = 0;
			code_1(state.it.right ? END(OP_p) : END(OP_center));
		}
		if (!state.it.center)
		{
			if (r)
				code_2(OP_p, ARG_right);
			else
				code_1(OP_center);
		}
	}
	else
	{
		if (state.it.right)
			code_1(END(OP_p));
		else if (state.it.center)
			code_1(END(OP_center));
		n = 0;
		r = 0;
	}
	state.it.center = n;
	state.it.right = r;
}

static void
troff_cf(Tag_t* tp, Arg_t* ap)
{
	Sfio_t*	sp;

	if (ap->argc >= 1 && (sp = find(ap->argv[1], NiL, 1)))
	{
		sfmove(sp, state.out, SF_UNBOUND, -1);
		sfclose(sp);
	}
}

static void	troff_wh(Tag_t*, Arg_t*);

static void
troff_ch(Tag_t* tp, Arg_t* ap)
{
	char*	s;
	Trap_t*	xp;

	NoP(tp);
	NoP(ap);
	if (ap->argc == 1)
	{
		for (xp = state.trap; xp; xp = xp->next)
			if (streq(xp->name + 1, ap->argv[1]))
				xp->name[0] = 0;
	}
	else if (ap->argc >= 2)
	{
		s = ap->argv[1];
		ap->argv[1] = (state.test & 1) ? "1" : ap->argv[2];
		ap->argv[2] = s;
		troff_wh(tp, ap);
	}
}

static void
troff_de(Tag_t* tp, Arg_t* ap)
{
	register Tag_t*	mp;

	if (ap->argc >= 1)
	{
		mp = mac(ap->argv[1]);
		if (mp->body && tp->name[1] == 'a')
			sfputr(state.tmp, mp->body, -1);
		mp->file = error_info.file;
		mp->line = error_info.line;
		mp->flags |= TAG_PASS;
		mp->call = macro;
		state.define = mp;
		state.end = (Tag_t*)hashget(state.symbols, "..");
		state.pass = 1;
		pushout(state.tmp);
	}
}

static void
troff_di(Tag_t* tp, Arg_t* ap)
{
	Divert_t*	dp;
	int		n;

	if (!ap->argc)
	{
		if (dp = state.divert)
		{
			state.dl = state.env->dl;
			state.dn = state.env->dn;
			if (dp->env->ft.current != state.env->ft.current)
				code_1(OP_ft + dp->env->ft.current);
			if (dp->env->ps.current != state.env->ps.current)
				code_2(OP_ps, dp->env->ps.current);
			state.env = dp->env;
			state.pass = 0;
			state.divert = dp->next;
			if ((n = sfstrtell(state.out)) > 0 && *(sfstrbase(state.out) + n - 1) != '\n')
				sfputc(state.out, '\n');
			set(dp->tag, popout(), 0);
			message((-2, "%s: pop diversion `%s'", dp->tag->name, dp->tag->body));
			sfstrclose(dp->sp);
			free(dp);
		}
	}
	else if (!(dp = newof(0, Divert_t, 1, 0)) || !(dp->sp = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [divert]");
	else
	{
		dp->tag = mac(ap->argv[1]);
		if (dp->tag->body && tp->name[2] == 'a')
			sfputr(dp->sp, dp->tag->body, -1);
		dp->next = state.divert;
		state.divert = dp;
		state.pass = 1;
		pushout(dp->sp);
		dp->env = state.env;
		dp->environment = *state.env;
		state.env = &dp->environment;
		state.env->dl = 0;
		state.env->dn = 0;
	}
}

static void
troff_ds(Tag_t* tp, Arg_t* ap)
{
	char*	v;

	if (v = join(ap, 2))
		set(mac(ap->argv[1]), v, tp->name[1] == 'a');
}

static void
troff_ec(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	state.ec = state.eo = (ap->argc >= 1) ? ap->argv[1][0] : DEFAULT_ec;
}

static void
troff_eo(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	if (state.ec)
	{
		state.eo = state.ec;
		state.ec = 0;
	}
}

static void
troff_ev(Tag_t* tp, Arg_t* ap)
{
	register Env_t*	oe;
	register Env_t*	ne;

	if (ap->argc < 1)
	{
		if (state.env_sp <= state.env_stack)
			error(2, "%s: stack underflow", tp->name);
		else
			state.env_sp--;
	}
	else if (state.env_sp >= &state.env_stack[elementsof(state.env_stack)])
		error(2, "%s: stack overflow", tp->name);
	else
		*++state.env_sp = env(ap->argv[1]);
	oe = state.env;
	ne = state.env = *state.env_sp;
	if (oe != ne)
	{
		if (oe->ft.current != ne->ft.current)
			code_1(OP_ft + ne->ft.current);
		if (oe->nf != ne->nf)
			code_1(ne->nf ? OP_pre : END(OP_pre));
		if (oe->ps.current != ne->ps.current)
			code_2(OP_ps, ne->ps.current);
	}
}

static void
troff_em(Tag_t* tp, Arg_t* ap)
{
	int	i;

	NoP(tp);
	for (i = 1; i <= ap->argc; i++)
		trap(&state.fini, ap->argv[i]);
}

static void
troff_fi(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	NoP(ap);
	if (state.env->nf)
	{
		state.env->nf = 0;
		code_1(END(OP_pre));
	}
}

static void
troff_fp(Tag_t* tp, Arg_t* ap)
{
	char*	s;
	char*	t;

	NoP(tp);
	if (ap->argc >= 2)
	{
		s = ap->argv[1];
		*--s = 'f';
		t = ap->argv[1];
		*--t = 'f';
		hashput(state.symbols, s, hashget(state.symbols, t));
	}
}

static void
troff_ft(Tag_t* tp, Arg_t* ap)
{
	char*	s;

	NoP(tp);
	if (ap->argc >= 1)
	{
		s = ap->argv[1];
		*--s = 'f';
		s = (char*)hashget(state.symbols, s);
	}
	else s = 0;
	ft(s);
}

static void
troff_ie(Tag_t* tp, Arg_t* ap)
{
	int	f;
	int	q;
	long	v;
	char*	s;
	char*	t;
	char*	e;

	if (tp->name[1] == 'e')
	{
		if (!state.cond.level || !(state.cond.flags[state.cond.level] & COND_IE))
		{
			if (state.verbose)
				error(2, "%s: no matching .ie", tp->name);
			if (!state.cond.level)
				state.cond.level++;
			state.cond.flags[state.cond.level] = COND_IE|COND_KEPT;
		}
		f = COND_EL;
	}
	else
	{
		if (state.cond.level >= elementsof(state.cond.flags) - 1)
			error(3, "%s: conditional stack too deep", tp->name);
		f = tp->name[2] == 'f' ? COND_IF : COND_IE;
	}
	if (error_info.trace <= -5)
	{
		int	g = state.cond.flags[state.cond.level];

		error(-5, "IE +++ %s level=%d |%s%s%s%s%s", tp->name, state.cond.level, (g & COND_IF) ? "IF|" : "", (g & COND_IE) ? "IE|" : "", (g & COND_BLOCK) ? "BLOCK|" : "", (g & COND_SKIP) ? "SKIP|" : "", (g & COND_KEPT) ? "KEPT|" : "");
	}
	if (s = join(ap, 1))
	{
		if (state.ec)
		{
			t = s;
			while (t = strchr(t, state.ec))
				if (*++t != '{' && *t != '}')
					break;
			if (t)
			{
				if (state.test & 0x100)
					error(2, "EXPAND +++ `%s'", s);
				expand(state.arg, s);
				sfputr(ap->sp, sfstruse(state.arg), 0);
				s = sfstruse(ap->sp);
				if (state.test & 0x100)
					error(2, "EXPAND --- `%s'", s);
			}
		}
		for (;;)
		{
			if (f & COND_EL)
				v = !(state.cond.flags[state.cond.level] & COND_KEPT);
			else
			{
				v = cond(s, &e);
				s = e;
			}
			if (v || s[0] != state.env->cc && s[0] != state.env->c2 || s[1] != 'i')
				break;
			q = f;
			if (s[2] == 'e')
				f = COND_IE|COND_KEPT|COND_SKIP;
			else if (s[2] == 'f')
				f = COND_IF|COND_SKIP;
			else
				break;
			if (q & COND_EL)
				state.cond.level--;
		}
		if (*s == state.ec && state.ec && *(s + 1) == '{')
		{
			s += 2;
			f |= COND_BLOCK;
		}
		while (isspace(*s))
			s++;
		if (!*s)
			s = 0;
		else
			*(s + strlen(s)) = '\n';
	}
	if (!v)
	{
		f |= COND_SKIP;
		skip(s, f, NiL);
	}
	else
	{
		if (s)
		{
			pushin(NiL, 0, NiL, s, ap);
			error_info.line--;
		}
		if (f & (COND_EL|COND_IE))
			f |= COND_KEPT;
	}
	if (f & COND_EL)
	{
		if ((f & (COND_SKIP|COND_BLOCK)) == COND_BLOCK)
			state.cond.flags[state.cond.level] = COND_IE|COND_EL|COND_KEPT|COND_BLOCK;
		else
			state.cond.level--;
	}
	else if ((f & COND_IE) || (f & (COND_BLOCK|COND_SKIP)) == COND_BLOCK)
		state.cond.flags[++state.cond.level] = f;
	if (error_info.trace <= -5)
	{
		int	g = state.cond.flags[state.cond.level];

		error(-5, "IE --- %s level=%d |%s%s%s%s%s", tp->name, state.cond.level, (g & COND_IF) ? "IF|" : "", (g & COND_IE) ? "IE|" : "", (g & COND_BLOCK) ? "BLOCK|" : "", (g & COND_SKIP) ? "SKIP|" : "", (g & COND_KEPT) ? "KEPT|" : "");
	}
}

static void
troff_ignore(Tag_t* tp, Arg_t* ap)
{
	char*	s;

	if (!state.end)
	{
		*tp->name = 'e';
		state.end = (s = (char*)hashget(state.symbols, tp->name)) ? (Tag_t*)hashget(state.symbols, s) : (Tag_t*)0;
		*tp->name = '.';
		if (state.end)
		{
			sfprintf(state.out, "<BR>%s ... %s OMITTED<BR>\n", tp->name, state.end->name);
			state.pass = 1;
			pushout(state.nul);
		}
	}
}

static void
troff_in(Tag_t* tp, Arg_t* ap)
{
	int	n;

	NoP(tp);
	if (ap->argc < 1)
		n = state.env->in.previous;
	else
	{
		n = expression(ap->argv[1], NiL, 'u');
		switch (ap->argv[1][0])
		{
		case '-':
		case '+':
			n += state.env->in.current;
			break;
		}
	}
	state.env->in.previous = state.env->in.current;
	state.env->in.current = n;
	li(0);
}

static void
troff_it(Tag_t* tp, Arg_t* ap)
{
	char*	s;
	char*	t;

	NoP(tp);
	state.it.count = 0;
	if (ap->argc >= 2 && (state.it.count = expression(ap->argv[1], NiL, 0)) > 0 && *(s = ap->argv[2]))
	{
		t = state.it.trap;
		*t++ = '.';
		*t++ = *s++;
		if (*s)
		{
			*t++ = *s;
			if (state.groff)
				while (t < &state.it.trap[sizeof(state.it.trap)-2] && (*t = *++s))
					t++;
		}
		*t++ = '\n';
		*t = 0;
	}
}

static void
troff_ll(Tag_t* tp, Arg_t* ap)
{
	int	n;

	NoP(tp);
	if (ap->argc < 1)
		n = state.env->ll.previous;
	else
	{
		n = expression(ap->argv[1], NiL, 'u');
		switch (ap->argv[1][0])
		{
		case '-':
		case '+':
			n += state.env->ll.current;
			break;
		}
	}
	state.env->ll.previous = state.env->ll.current;
	state.env->ll.current = n;
	li(0);
}

static void
troff_ne(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	state.t = (ap->argc < 1) ? 1 : INT_MAX;
}

static void
troff_nf(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	NoP(ap);
	if (!state.env->nf)
	{
		if (!state.it.interrupt)
			it();
		li(-1);
		state.env->nf = 1;
		code_1(OP_pre);
	}
}

static void
troff_nr(Tag_t* tp, Arg_t* ap)
{
	int	i;
	long	n;
	char*	s;
	Num_t*	np;

	NoP(tp);
	if (ap->argc >= 2)
	{
		np = num(ap->argv[1]);
		if ((i = *(s = ap->argv[2])) == '+' || i == '-')
			s++;
		n = expression(s, NiL, 0);
		switch (i)
		{
		case '-':
			n = np->number - n;
			break;
		case '+':
			n = np->number + n;
			break;
		}
		np->number = n;
		if (np->internal)
			switch (INDEX(np->name[1], np->name[2]))
			{
			case INDEX('c','.'):
				if (state.original.line)
					state.original.line = n;
				else
					error_info.line = n;
				break;
			case INDEX('d','l'):
				state.dl = n;
				break;
			case INDEX('d','n'):
				state.dn = n;
				break;
			case INDEX('l','n'):
				state.ln = n;
				break;
			case INDEX('n','l'):
				state.nl = n;
				break;
			}
		if (ap->argc >= 3)
			np->increment = expression(ap->argv[3], NiL, 0);
		if (np->flags & TAG_TRACE_SET)
			error(2, "set register %s %d %d", np->name + 1, np->number, np->increment);
	}
}

static void
troff_pc(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	state.pc = (ap->argc >= 1) ? ap->argv[1][0] : 0;
}

static void
troff_ps(Tag_t* tp, Arg_t* ap)
{
	int	n;

	NoP(tp);
	if (ap->argc < 1)
		n = state.env->ps.previous;
	else
	{
		n = expression(ap->argv[1], NiL, 'p');
		switch (ap->argv[1][0])
		{
		case '-':
		case '+':
			n += state.env->ps.current;
			break;
		default:
			if (!n)
				n = state.env->ps.previous;
			break;
		}
	}
	if (n > 0)
		ps(n);
}

static void
troff_rm(Tag_t* tp, Arg_t* ap)
{
	int	i;

	NoP(tp);
	for (i = 1; i <= ap->argc; i++)
		rm(ap->argv[i]);
}

static void
troff_rn(Tag_t* tp, Arg_t* ap)
{
	Tag_t*	mp;
	char*	s;

	NoP(tp);
	if (ap->argc >= 2)
	{
		s = ap->argv[2];
		rm(s);
		*--s = '.';
		mp = mac(ap->argv[1]);
		hashput(state.symbols, NiL, NiL);
		mp->name = hashput(state.symbols, s, mp);
	}
}

static void
rr(char* name)
{
	register Num_t*	np;
	char		buf[MAXNAME];

	if (np = (Num_t*)hashget(state.symbols, nam('.', name, buf, sizeof(buf))))
	{
		if (np->internal)
			return;
		if (np->flags & TAG_TRACE_SET)
			error(2, "remove register %s", np->name + 1);
		free(np);
		hashput(state.symbols, NiL, NiL);
	}
}

static void
troff_rr(Tag_t* tp, Arg_t* ap)
{
	int	i;

	NoP(tp);
	for (i = 1; i <= ap->argc; i++)
		rr(ap->argv[i]);
}

static void
troff_so(Tag_t* tp, Arg_t* ap)
{
	Sfio_t*	sp;
	char*	path;

	NoP(tp);
	if (ap->argc >= 1 && (sp = find(ap->argv[1], &path, 1)))
		pushin(path, 1, sp, NiL, NiL);
}

static void
troff_sp(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	if (state.it.dd)
	{
		state.it.dd = 0;
		code_1(OP_dd);
	}
	code_1((ap->argc < 1 || expression(ap->argv[1], NiL, 'v') > 0) ? OP_p : OP_br);
}

static void
troff_ta(Tag_t* tp, Arg_t* ap)
{
	int		i;
	unsigned char	ta[elementsof(state.ta)];

	NoP(tp);
	if (ap->argc < 1)
	{
		state.ta[0] = 8;
		i = 1;
	}
	else
	{
		if (ap->argc >= elementsof(ta))
			ap->argc = elementsof(ta) - 1;
		for (i = 0; i < ap->argc; i++)
			ta[i] = expression(ap->argv[i+1], NiL, 'u');
	}
	ta[i] = 0;
	code_n(OP_ta, (char*)ta);
}

static void
troff_ti(Tag_t* tp, Arg_t* ap)
{
	int	n;

	NoP(tp);
	if (ap->argc < 1)
		n = state.env->ti.previous;
	else
	{
		n = expression(ap->argv[1], NiL, 'u');
		switch (ap->argv[1][0])
		{
		case '-':
		case '+':
			n += state.env->ti.current;
			break;
		}
	}
	state.env->ti.previous = state.env->ti.current;
	state.env->ti.current = n;
	li(1);
	if (state.list > state.list_stack && state.env->ti.current < state.list->in)
		state.it.dt = 1;
}

static void
troff_tl(Tag_t* tp, Arg_t* ap)
{
	register int	c;
	register int	q;
	register char*	s;
	register char*	t;
	int		n;

	NoP(tp);
	if (s = join(ap, 1))
	{
		if (state.head)
			state.footer = 1;
		else
		{
			state.head = 1;
			code_1(END(OP_head));
			code_1(OP_body);
		}
		code_1(OP_h3);
		code_2(OP_tab, ARG_wide);
		code_1(OP_tab_row);
		if (q = *s++)
			for (n = 1; n <= 3; n++)
			{
				code_2(OP_tab_head, n);
				if (n == 1)
					code_2(OP_cc, ' ');
				while ((c = *s++) != q)
				{
					if (!c)
					{
						s--;
						break;
					}
					if (c != state.pc)
						sfputc(state.out, c);
					else if (t = value("n%", 'n', 0))
						sfputr(state.out, t, -1);
				}
				if (n == 1)
					code_2(OP_cc, ' ');
			}
		code_1(END(OP_tab));
		code_1(END(OP_h3));
		sfputc(state.out, '\n');
	}
}

static void
troff_tm(Tag_t* tp, Arg_t* ap)
{
	char*	s;

	NoP(tp);
	if (s = join(ap, 1))
		notify(s);
}

static void
troff_vs(Tag_t* tp, Arg_t* ap)
{
	int	n;

	NoP(tp);
	if (ap->argc < 1)
		n = state.env->vs.previous;
	else
	{
		n = expression(ap->argv[1], NiL, 'p');
		switch (ap->argv[1][0])
		{
		case '-':
		case '+':
			n += state.env->vs.current;
			break;
		default:
			if (!n)
				n = state.env->vs.previous;
			break;
		}
	}
	if (n > 0)
	{
		state.env->vs.previous = state.env->vs.current;
		state.env->vs.current = n;
	}
}

/*
 * return 1 if any non-diverted text escaped
 */

static int
text(void)
{
	register char*	s;
	register char*	e;

	if (state.it.text)
		return 1;
	s = sfstrbase(state.out);
	e = sfstrseek(state.out, 0, SEEK_CUR);
	while (s < e)
	{
		switch (*s++)
		{
		case CODE_2:
			s++;
			/*FALLTHROUGH*/
		case CODE_1:
			s++;
			/*FALLTHROUGH*/
		case ' ':
		case '\t':
		case '\n':
		case CODE_0:
			continue;
		default:
			message((-9, "TEXT begin"));
			return 1;
		}
	}
	return 0;
}

static void
troff_wh(Tag_t* tp, Arg_t* ap)
{
	int		i;
	Trap_t**	xp;

	if (ap->argc > 1)
	{
		if (!(i = expression(ap->argv[1], NiL, 'u')))
		{
			if (!state.it.text && text())
				state.it.text = 1;
			if (state.it.text)
				i = -1;
		}
		xp = (i < 0) ? &state.fini : &state.trap;
		for (i = 2; i <= ap->argc; i++)
			trap(xp, ap->argv[i]);
	}
}

static void
hot(register char* s, int add)
{
	register Dir_t*	x;
	register Dir_t*	p;

	for (p = 0, x = state.hot; x; p = x, x = x->next)
		if (streq(s, x->name))
		{
			if (!add)
			{
				if (p)
					p->next = x->next;
				else
					state.hot = state.hot->next;
				free(x);
			}
			return;
		}
	if (add)
	{
		if (!(x = newof(0, Dir_t, 1, strlen(s) + 1)))
			error(ERROR_SYSTEM|3, "out of space [hot]");
		strcpy(x->name = (char*)x + sizeof(*x), s);
		x->next = state.hot;
		state.hot = x;
	}
}

#define OPT_SWITCH	1
#define OPT_begin	2
#define OPT_debug	3
#define OPT_end		4
#define OPT_footnote	5
#define OPT_get		6
#define OPT_hot		7
#define OPT_label	8
#define OPT_link	9
#define OPT_test	10
#define OPT_toolbar	11
#define OPT_set		12

typedef struct
{
	const char*	name;
	char**		value;
	int		index;
} Option_t;

static Option_t	options[] =
{
	"author",	&state.author,		0,
	"background",	&state.background,	0,
	"begin",	0,			OPT_begin,
	"company",	&state.company,		0,
	"corporation",	&state.corporation,	0,
	"debug",	0,			OPT_debug,
	"end",		0,			OPT_end,
	"footnote",	0,			OPT_footnote,
	"get",		0,			OPT_get,
	"hot",		0,			OPT_hot,
	"label",	0,			OPT_label,
	"link",		0,			OPT_link,
	"location",	&state.location,	0,
	"logo",		&state.logo,		0,
	"mailto",	&state.mailto,		0,
	"organization",	&state.organization,	0,
	"package",	&state.package,		0,
	"set",		0,			OPT_set,
	"test",		0,			OPT_test,
	"title",	&state.title,		0,
	"toolbar",	&state.toolbar,		OPT_toolbar,
	"verbose",	(char**)&state.verbose,	OPT_SWITCH,
	0,		0,			0
};

/*
 * called by stropt() to set name=value
 */

static int
setopt(void* a, const void* x, register int n, const char* v)
{
	register char*		s;
	register char*		t;
	register Option_t*	p = (Option_t*)x;
	register int		f;

	if (p)
		switch (p->index)
		{
		case OPT_begin:
			sfprintf(state.tmp, "<!-- %s -->", v + n);
			code_n(OP_RAW, sfstruse(state.tmp));
			break;
		case OPT_debug:
			error_info.trace = n ? -expression((char*)v, NiL, 'u') : 0;
			break;
		case OPT_end:
			sfprintf(state.tmp, "<!-- /%s -->", v + n);
			code_n(OP_RAW, sfstruse(state.tmp));
			break;
		case OPT_footnote:
			if (!state.out)
				error(1, "%s: option valid from document body only", p->name);
			else
			{
				/*
				 * NOTE: mm .FS/.FE is so convoluted that I
				 *       punted to this; trying to decipher
				 *	 it prompted .xx [debug|get|set] in
				 *	 the first place; I'll get back to a
				 *	 real solution on a meetingless day.
				 */

				if (n)
				{
					register char*	b;
					long		m;

					m = sfstrtell(state.out);
					b = sfstrbase(state.out);
					s = b + m;
					while (s > b)
						if (!isspace(*--s))
						{
							s++;
							break;
						}
					t = s;
					while (s > b)
						if (isspace(*--s))
						{
							s++;
							break;
						}
					sfprintf(state.tmp, "FN%ld\t%-.*s", m, t - s, s);
					sfstrseek(state.out, s - b, SEEK_SET);
					code_n(OP_link, sfstruse(state.tmp));
					sfprintf(state.tmp, "FN%ld", m);
					code_n(OP_fn, sfstruse(state.tmp));
				}
				else
					code_1(END(OP_fn));
			}
			break;
		case OPT_get:
		case OPT_hot:
		case OPT_set:
			switch (p->index)
			{
			case OPT_get:
				f = TAG_TRACE_GET;
				break;
			case OPT_hot:
				f = 0;
				break;
			case OPT_set:
				f = TAG_TRACE_SET;
				break;
			}
			s = (char*)v;
			do
			{
				while (isspace(*s))
					s++;
				for (t = s;; t++)
					if (!*t)
					{
						t = 0;
						break;
					}
					else if (isspace(*t))
					{
						*t++ = 0;
						break;
					}
				if (!s[0])
					break;
				if (s[0] == '+' && !s[1])
					n = 1;
				else if (s[0] == '-' && !s[1])
					n = 0;
				else if (!f)
					hot(s, n);
				else if (n)
				{
					num(s)->flags |= f;
					mac(s)->flags |= f;
				}
				else
				{
					num(s)->flags &= ~f;
					mac(s)->flags &= ~f;
				}
			} while (s = t);
			break;
		case OPT_label:
			if (!state.out)
				error(1, "%s: option valid from document body only", p->name);
			else if (n)
			{
				sfprintf(state.tmp, "%s\t", v);
				code_n(LABEL(OP_link), sfstruse(state.tmp));
			}
			break;
		case OPT_link:
			if (!state.out)
				error(1, "%s: option valid from document body only", p->name);
			else if (n)
				code_n(OP_link, (char*)v);
			break;
		case OPT_test:
			if (n)
				state.test |= expression((char*)v, NiL, 'u');
			else
				state.test &= ~expression((char*)v, NiL, 'u');
			break;
		case OPT_SWITCH:
			*((int*)p->value) = n && expression((char*)v, NiL, 'u');
			break;
		default:
			if (*p->value)
				free(*p->value);
			*p->value = n && v ? strdup(v) : (char*)0;
			break;
		}
	else if (a)
	{
		if (strneq(v, "meta.", 5))
		{
			sfprintf(state.tmp, "<META name=\"%s\" content=\"%s\">", v + 5, v + n);
			code_n(OP_RAW, sfstruse(state.tmp));
		}
		else
			error(1, "%s: unknown option", v);
	}
	return 0;
}

static void
troff_xx(Tag_t* tp, Arg_t* ap)
{
	NoP(tp);
	stropt(join(ap, 1), options, sizeof(*options), setopt, options);
}

#define COMMENT		001
#define COPY		002
#define EAT		004
#define RAW		010
#define STRING		020

#define ONE() \
	do \
	{\
		if (cc <= 0) \
		{ \
			if (cc < 0) \
				cc++; \
			if (!argc && !state.it.interrupt) \
				it(); \
		} \
		cc++; \
	} while (0)

/*
 * convert troff file ip to intermediate file op
 */

static void
process(char* file, Sfio_t* ip, Sfio_t* op)
{
	register int	c;
	register int	cc;
	register int	lastc;
	register char*	s;
	int		argc;
	int		quote;
	int		n;
	int		m;
	unsigned char*	map;
	Tag_t*		tp;
	int		argv[ARGS];
	struct stat	st;
	char		buf[MAXNAME];

	pushin(file, 1, ip, NiL, NiL);
	state.generation++;
	*(state.env_sp = state.env_stack) = state.env = env("0");
	state.it.dd = state.it.dl = state.it.dt = 0;
	state.link = 0;
	iop("stdout", 1)->sp = state.out = op;
	state.pass = 0;
	if (ip && !fstat(sffileno(ip), &st))
		tm(st.st_mtime);
	argc = 0;
	argv[0] = 0;
	cc = 0;
	lastc = 0;
	quote = 0;
	map = ccmap(CC_NATIVE, CC_ASCII);
	for (;;)
	{
		switch (c = GETC())
		{
		case EOF:
			if (popin())
				goto done;
			continue;
		case CODE_n:
			if (ISFILE())
			{
				cc++;
				code_2(OP_cc, c);
			}
			else
			{
				sfputc(state.out, c);
				n = GETC();
				sfputc(state.out, n);
				if (n & 0200)
				{
					n = (n & 0177) << 8;
					c = GETC();
					sfputc(state.out, c);
					n |= c;
				}
				do
				{
					c = GETC();
					sfputc(state.out, c);
				} while (n-- > 0);
			}
			continue;
		case CODE_2:
			if (ISFILE())
			{
				cc++;
				code_2(OP_cc, c);
			}
			else
			{
				m = GETC();
				n = GETC();
				if (cc <= 0 && (n == OP_cc || n >= OP_ft1 && n <= OP_ft5 || n == OP_ps))
					ONE();
				sfputc(state.out, c);
				sfputc(state.out, m);
				sfputc(state.out, n);
			}
			continue;
		case CODE_1:
			if (ISFILE())
			{
				cc++;
				code_2(OP_cc, c);
			}
			else
			{
				sfputc(state.out, c);
				switch (c = GETC())
				{
				case OP_br:
				case OP_p:
					cc = 0;
					break;
				}
				sfputc(state.out, c);
			}
			continue;
		case CODE_0:
			if (ISFILE())
			{
				cc++;
				code_2(OP_cc, c);
			}
			else
			{
				ONE();
				sfputc(state.out, c);
			}
			continue;
		case '"':
			if (argc && !(quote & RAW))
			{
				lastc = c;
				switch ((quote & STRING) ? (c = nextchar()) : EOF)
				{
				case '"':
					break;
				default:
					UNGETC(c);
					/*FALLTHROUGH*/
				case EOF:
					quote ^= STRING;
					continue;
				}
			}
			break;
		case '\n':
			if (state.noline)
			{
				if (isspace(lastc))
					continue;
				c = ' ';
				break;
			}
			if (quote & COMMENT)
			{
				quote &= ~COMMENT;
				if (quote & EAT)
				{
					quote &= ~EAT;
					c = ' ';
				}
				popout();
			}
			if (argc)
			{
				cc = 0;
				lastc = c;
				quote = 0;
				state.groff &= 1;
				state.pass = 0;
				if (!(s = popout()))
				{
					argc = 0;
					continue;
				}
				if (!s[argv[--argc]] && argc > 0)
					argc--;
				for (state.tag->argc = argc; argc >= 0; argc--)
					state.tag->argv[argc] = s + argv[argc];
				argc = 0;
				if (error_info.trace <= -4)
				{
					if (state.end)
						sfprintf(state.tmp, "    ");
					sfprintf(state.tmp, "%s", state.tag->argv[0]);
					if (!tp)
						sfprintf(state.tmp, " [UNKNOWN]");
					for (n = 1; n <= state.tag->argc; n++)
						sfprintf(state.tmp, " `%s'", state.tag->argv[n]);
					error(-4, "%s:%d: %s", error_info.file, error_info.line, sfstruse(state.tmp));
				}
				if (tp)
				{
					error_info.line++;
					if (tp->call)
						(*tp->call)(tp, state.tag);
					if (tp->flags & TAG_TRIGGER)
					{
						tp->flags &= ~TAG_TRIGGER;
						trigger(&state.trap);
					}
				}
				else if (state.verbose)
				{
					switch (state.tag->argv[0][1])
					{
					case 0:
						break;
					case '#':
						if (!state.tag->argv[0][2])
							break;
						/*FALLTHROUGH*/
					default:
						if (error_info.trace >= 0)
							error(1, "%s: unknown request", state.tag->argv[0]);
						sfprintf(state.tmp, "UNKNOWN REQUEST %s", join(state.tag, 0));
						code_n(OP_comment, sfstruse(state.tmp));
						break;
					}
					error_info.line++;
				}
				continue;
			}
			error_info.line++;
			n = 6 * state.env->vs.current;
			state.nl += n;
			state.env->dn += n;
			n = 6 * state.env->ps.current;
			if (n > state.env->dl)
				state.env->dl = n;
			if (!DIVERTED())
			{
				state.ln++;
				if (state.it.text || cc > 1)
					state.it.text++;
				else
				{
					cc = 0;
					continue;
				}
			}
			state.it.interrupt = 0;
			if (state.it.center > 0)
			{
				if (!--state.it.center)
					code_1(state.it.right ? END(OP_p) : END(OP_center));
				else
					code_1(OP_br);
			}
			if (state.it.count > 0 && !--state.it.count)
			{
				sfputc(state.out, '\n');
				pushin(NiL, 0, NiL, state.it.trap, NiL);
				cc = 0;
				continue;
			}
			cc = -2;
			break;
		case ' ':
		case '\t':
			if (argc)
			{
				if (!quote || (quote == RAW || quote == (COPY|RAW)) && argc == 1)
				{
					if (lastc != ' ' && argc < elementsof(argv))
					{
						sfputc(state.out, 0);
						argv[argc++] = sftell(state.out);
						lastc = ' ';
					}
					continue;
				}
			}
			break;
		default:
			if (c == state.ec)
			{
			escape:
				switch (c = GETC())
				{
				case EOF:
					if (popin())
					{
						c = '\n';
						break;
					}
					goto escape;
				case '\n':
					if (ISFILE())
						error_info.line++;
					else
						UNGETC(c);
					continue;
				case 'a':
					if (state.pass)
						goto passthrough;
					c = 1;
					break;
				case 'c':
					if (state.pass || (quote & RAW))
						goto passthrough;
					if ((c = nextchar()) == '\n')
					{
						message((-9, "INTERRUPT %s:%d: it.center=%d it.count=%d it.dt", error_info.file, error_info.line, state.it.center, state.it.count));
						state.it.interrupt = 1;
						error_info.line++;
						cc = 0;
						continue;
					}
					UNGETC(c);
					continue;
				case 'd':
					if (state.env->ss <= 0)
					{
						state.env->ss--;
						code_1(OP_sub);
					}
					else
					{
						state.env->ss++;
						code_1(END(OP_sup));
					}
					continue;
				case 'e':
				case 'E':
					if (state.pass)
						goto passthrough;
					ONE();
					code_2(OP_cc, state.eo);
					lastc = state.eo;
					continue;
				case 'f':
					if (state.pass || (quote & RAW))
						goto passthrough;
					ft(interpolate('f'));
					continue;
				case 'g':
				case 'j':
					if (state.pass || (quote & RAW))
						goto passthrough;
					pushin(NiL, 0, NiL, interpolate(c), NiL);
					continue;
				case 'k':
					nextchar();
					continue;
				case '*':
					c = '.';
					goto interp;
				case '[':
					if (!state.groff)
						goto passthrough;
				case '(':
					if (state.pass)
						goto passthrough;
					UNGETC(c);
					/*FALLTHROUGH*/
				case 'n':
				interp:
					if (quote & COPY)
						goto passthrough;
					if (s = interpolate(c))
						pushin(NiL, -1, NiL, s, NiL);
					continue;
				case 'p':
				case 'r':
				case 'z':
					continue;
				case 's':
					if (state.pass || (quote & RAW))
						goto passthrough;
					switch (c = nextchar())
					{
					case '-':
					case '+':
						m = c;
						c = nextchar();
						break;
					default:
						m = 0;
						break;
					}
					switch (c)
					{
					case '(':
						n = 1;
						sfputc(state.tmp, c);
						break;
					case '\'':
						n = c;
						goto size_long;
					case '[':
						n = ']';
					size_long:
						c = nextchar();
						if (!m)
							switch (c)
							{
							case '-':
							case '+':
								m = c;
								c = nextchar();
								break;
							}
						while (c != EOF && c != n)
						{
							sfputc(state.tmp, c);
							c = nextchar();
						}
						goto size_eval;
					default:
						n = 0;
						break;
					}
					if (c == '0' && m)
					{
						if ((n = nextchar()) == '\'')
						{
							while ((c = nextchar()) != EOF && c != '\'')
								sfputc(state.tmp, c);
							code_n(m == '+' ? OP_link : LABEL(OP_link), sfstruse(state.tmp));
							continue;
						}
						UNGETC(n);
						n = 0;
					}
					if (c != EOF)
					{
						if (n)
						{
							for (;; c = nextchar())
							{
								switch (c)
								{
								case EOF:
									break;
								case '(':
									sfputc(state.tmp, c);
									n++;
									continue;
								case ')':
									sfputc(state.tmp, c);
									if (--n <= 0)
										break;
									continue;
								default:
									sfputc(state.tmp, c);
									continue;
								}
								break;
							}
						size_eval:
							n = expression(sfstruse(state.tmp), NiL, 'p');
						}
						else
							n = isdigit(c) ? (c - '0') : 0;
						if (!n)
							n = state.env->ps.previous;
						else switch (m)
						{
						case '-':
							n = state.env->ps.current - n;
							break;
						case '+':
							n = state.env->ps.current + n;
							break;
						}
						if (n > 0)
							ps(n);
					}
					continue;
				case 't':
					if (state.pass)
						goto passthrough;
					c = '\t';
					break;
				case 'u':
					if (state.env->ss >= 0)
					{
						state.env->ss++;
						code_1(OP_sup);
					}
					else
					{
						state.env->ss--;
						code_1(END(OP_sub));
					}
					continue;
				case 'v':
				case 'w':
					if (state.pass || (quote & RAW))
						goto passthrough;
					/*FALLTHROUGH*/
				case 'b':
				case 'h':
				case 'l':
				case 'L':
				case 'o':
				case 'x':
					if ((n = nextchar()) != EOF)
					{
						while ((m = nextchar()) != n)
							sfputc(state.arg, m);
						s = sfstruse(state.arg);
						switch (c)
						{
						case 'h':
							if (*s++ == '0')
							{
								/*UNDENT...*/
	if ((c = *s++) == '*' || c == '/')
	{
		state.link = c == '*' ? OP_link : LABEL(OP_link);
		if (*s++ == '\\' && *s++ == 'w' && *s++ == '"')
		{
			if ((c = strlen(s)) > 0 && s[--c] == '"')
				s[c] = 0;
			sfputr(state.ref, s, '\t');
		}
		pushout(state.ref);
	}
	else if (!c && state.link)
	{
		code_n(state.link, popout());
		state.link = 0;
	}
								/*...INDENT*/
							}
							/* yep, this is a grade A hack, even for this file */
							if ((c = nextchar()) == state.ec)
							{
								if ((n = nextchar()) == 'c')
									break;
								UNGETC(n);
							}
							UNGETC(c);
							break;
						case 'v':
							c = expression(s, NiL, 0) >= 0 ? 'd' : 'u';
							sfprintf(state.arg, "%c%c", state.ec, c);
							pushin(NiL, 0, NiL, sfstruse(state.arg), NiL);
							break;
						case 'w':
							n = convert(strlen(s), 1, 'n');
							sfprintf(state.arg, "%d", n);
							pushin(NiL, 0, NiL, sfstruse(state.arg), NiL);
							break;
						}
					}
					continue;
				case '$':
					if (state.mac)
					{
						c = nextchar();
						if (c == '(')
						{
							if ((c = nextchar()) != EOF)
							{
								sfputc(state.tmp, c);
								if ((c = nextchar()) != EOF)
									sfputc(state.tmp, c);
							}
							goto arg_eval;
						}
						else if (c == '[')
						{
							while ((c = nextchar()) != EOF && c != ']')
								sfputc(state.tmp, c);
						arg_eval:
							c = expression(sfstruse(state.tmp), NiL, 0);
							if (c >= 0 && c <= state.mac->argc)
								pushin(NiL, -argc, NiL, state.mac->argv[c], NiL);
						}
						else if (c == '@')
						{
							for (c = 1; c < state.mac->argc; c++)
								sfprintf(state.tmp, "\"%s\" ", state.mac->argv[c]);
							if (c == state.mac->argc)
								sfprintf(state.tmp, "\"%s\"", state.mac->argv[c]);
							pushin(NiL, -argc, NiL, sfstruse(state.tmp), NiL);
						}
						else if (c < '0' || c > '9')
						{
							for (c = 1; c < state.mac->argc; c++)
								sfputr(state.tmp, state.mac->argv[c], ' ');
							if (c == state.mac->argc)
								sfputr(state.tmp, state.mac->argv[c], -1);
							pushin(NiL, -argc, NiL, sfstruse(state.tmp), NiL);
						}
						else if ((c -= '0') <= state.mac->argc)
							pushin(NiL, -argc, NiL, state.mac->argv[c], NiL);
					}
					continue;
				case '{':
					for (;;)
					{
						if ((n = GETC()) == EOF)
						{
							if (popin())
								break;
							continue;
						}
						if (n == state.ec)
						{
						escape_splice:
							switch (m = GETC())
							{
							case EOF:
								if (popin())
									break;
								goto escape_splice;
							case '\n':
								UNGETC(m);
								break;
							default:
								UNGETC(m);
								UNGETC(n);
								break;
							}
							break;
						}
						else
							UNGETC(n);
						break;
					}
					if (state.pass)
						goto passthrough;
					continue;
				case '}':
					if (state.end || !(state.cond.flags[state.cond.level] & COND_BLOCK))
						goto passthrough;
					if (state.cond.flags[state.cond.level] & (COND_EL|COND_IF))
						state.cond.level--;
					else
						state.cond.flags[state.cond.level] &= ~COND_BLOCK;
					continue;
				case '0':
				case '|':
				case '^':
				case ' ':
				case '/':
				case ',':
				case '~':
					if (state.pass)
						goto passthrough;
					ONE();
					code_2(OP_cc, ' ');
					continue;
				case '&':
					if (state.pass || (quote & RAW))
						goto passthrough;
					if (!cc)
						cc = -1;
					code_0();
					continue;
				case '-':
				case '%':
					if (state.pass)
						goto passthrough;
					ONE();
					code_2(OP_cc, 45);
					continue;
				case '"':
					if (!quote)
					{
						quote |= COMMENT;
						pushout(state.nul);
					}
					cc++;
					continue;
				case '#':
					if (!quote)
					{
						quote |= COMMENT|EAT;
						pushout(state.nul);
					}
					cc++;
					continue;
				case '!':
					if (!cc)
					{
						cc++;
						continue;
					}
					break;
				default:
					if (c == state.ec)
						break;
					if ((c == state.env->cc || c == state.env->c2) && !cc)
						goto request;
					if (state.pass)
						goto passthrough;
					break;
				}
				break;
			passthrough:
				ONE();
				sfputc(state.out, state.ec);
				break;
			}
			else if ((c == state.env->cc || c == state.env->c2) && !cc)
			{
			request:
				n = c;
				s = buf;
				*s++ = '.';
				if ((c = nextchar()) != EOF)
				{
					while (c == ' ' || c == '\t')
						c = nextchar();
					if (c == state.ec || c == '\n')
						UNGETC(c);
					else if (c != EOF)
					{
						*s++ = c;
						while (s < &buf[sizeof(buf)-1] && (c = nextchar()) != EOF)
						{
							if (c == state.ec || isspace(c))
							{
								UNGETC(c);
								break;
							}
							*s++ = c;
							if (!state.groff)
							{
								if ((c = nextchar()) != EOF)
								{
									UNGETC(c);
									if (!isspace(c))
										pushin(NiL, 0, NiL, " ", NiL);
								}
								break;
							}
						}
					}
				}
				*s = 0;
				tp = (Tag_t*)hashget(state.symbols, buf);
				if (tp && (tp->flags & TAG_DO))
				{
					state.groff |= 2;
					c = n;
					goto request;
				}
				if (state.end)
				{
					if (tp == state.end)
					{
						state.end = 0;
						state.pass = 0;
						s = popout();
						if (tp = state.define)
						{
							state.define = 0;
							set(tp, s, 0);
						}
						quote |= COMMENT;
						pushout(state.nul);
					}
					else
					{
						pushin(NiL, 0, NiL, buf + 1, NiL);
						c = n;
					}
					break;
				}
				if (tp)
				{
					if ((tp->flags & TAG_BREAK) && n == state.env->cc)
					{
						if (!DIVERTED() && (state.it.text || text()))
							state.it.text++;
						if (state.it.interrupt)
						{
							message((-9, "BREAK %s:%d: it.center=%d it.count=%d", error_info.file, error_info.line, state.it.center, state.it.count));
							state.it.interrupt = 0;
							sfputc(state.out, '\n');
							ONE();
						}
						tp->flags |= TAG_TRIGGER;
					}
					if (tp->flags & TAG_COPY)
						quote |= COPY|RAW;
					else if (tp->flags & TAG_RAW)
						quote |= RAW;
					if (tp->flags & TAG_PASS)
						state.pass = 1;
				}
				else
					state.pass = 1;
				argc = 1;
				pushout(state.tag->sp);
				sfputr(state.out, buf, -1);
				cc = s - buf;
				continue;
			}
			else if ((n = ccmapchr(map, c)) > 0177)
			{
				ONE();
				code_2(OP_cc, n & 0377);
				continue;
			}
			break;
		}
		ONE();
		sfputc(state.out, c);
		lastc = c;
	}
 done:
	if (state.end)
	{
		if (state.define)
		{
			error(2, "%s macro definition end tag %s not found", state.define->name, state.end->name);
			state.define = 0;
		}
		else
			error(2, "group end tag %s not found", state.end->name);
		state.end = 0;
	}
	while (DIVERTED())
		popout();
}

static Tag_t tags[] =
{
	".",		0,		0,			0,0,0,0,
	".'",		0,		0,			0,0,0,0,
	".''",		0,		0,			0,0,0,0,
	"..",		0,		0,			0,0,0,0,
	".EN",		0,		0,			0,0,0,0,
	".EQ",		troff_ignore,	0,			0,0,0,0,
	".TE",		0,		0,			0,0,0,0,
	".TS",		troff_ignore,	0,			0,0,0,0,
	".\"",		0,		0,			0,0,0,0,
	".\\\"",0,			0,			0,0,0,0,
	".ab",		troff_ab,	0,			0,0,0,0,
	".ad",		troff_ad,	TAG_PASS,		0,0,0,0,
	".af",		troff_af,	0,			0,0,0,0,
	".al",		0,		0,			0,0,0,0,
	".aln",		groff_aln,	0,			0,0,0,0,
	".als",		groff_als,	0,			0,0,0,0,
	".am",		troff_de,	TAG_PASS,		0,0,0,0,
	".as",		troff_ds,	TAG_PASS,		0,0,0,0,
	".asciify",	groff_asciify,	0,			0,0,0,0,
	".backtrace",	0,		0,			0,0,0,0,
	".bd",		0,		0,			0,0,0,0,
	".blm",		0,		0,			0,0,0,0,
	".bp",		0,		TAG_BREAK,		0,0,0,0,
	".br",		troff_br,	TAG_BREAK,		0,0,0,0,
	".break",	groff_break,	0,			0,0,0,0,
	".c2",		troff_c2,	0,			0,0,0,0,
	".cc",		troff_cc,	0,			0,0,0,0,
	".ce",		troff_ce,	TAG_BREAK,		0,0,0,0,
	".cf",		troff_cf,	0,			0,0,0,0,
	".cflags",	0,		0,			0,0,0,0,
	".ch",		troff_ch,	0,			0,0,0,0,
	".char",	0,		0,			0,0,0,0,
	".chop",	groff_chop,	0,			0,0,0,0,
	".close",	groff_close,	0,			0,0,0,0,
	".continue",	groff_continue,	0,			0,0,0,0,
	".cp",		groff_cp,	0,			0,0,0,0,
	".cs",		0,		0,			0,0,0,0,
	".cu",		0,		0,			0,0,0,0,
	".da",		troff_di,	TAG_PASS,		0,0,0,0,
	".de",		troff_de,	TAG_PASS,		0,0,0,0,
	".di",		troff_di,	TAG_PASS,		0,0,0,0,
	".do",		0,		TAG_DO,			0,0,0,0,
	".ds",		troff_ds,	TAG_PASS,		0,0,0,0,
	".dt",		troff_wh,	TAG_PASS,		0,0,0,0,
	".ec",		troff_ec,	0,			0,0,0,0,
	".eo",		troff_eo,	0,			0,0,0,0,
	".el",		troff_ie,	TAG_PASS|TAG_RAW,	0,0,0,0,
	".em",		troff_em,	TAG_PASS,		0,0,0,0,
	".ev",		troff_ev,	TAG_PASS,		0,0,0,0,
	".fam",		0,		0,			0,0,0,0,
	".fc",		0,		0,			0,0,0,0,
	".fi",		troff_fi,	TAG_BREAK,		0,0,0,0,
	".fl",		0,		TAG_BREAK,		0,0,0,0,
	".fp",		troff_fp,	0,			0,0,0,0,
	".fspecial",	0,		0,			0,0,0,0,
	".ft",		troff_ft,	0,			0,0,0,0,
	".ftr",		0,		0,			0,0,0,0,
	".hcode",	0,		0,			0,0,0,0,
	".hla",		0,		0,			0,0,0,0,
	".hlm",		0,		0,			0,0,0,0,
	".hpf",		0,		0,			0,0,0,0,
	".hw",		0,		0,			0,0,0,0,
	".hy",		0,		0,			0,0,0,0,
	".hym",		0,		0,			0,0,0,0,
	".hys",		0,		0,			0,0,0,0,
	".ie",		troff_ie,	TAG_PASS|TAG_RAW,	0,0,0,0,
	".if",		troff_ie,	TAG_PASS|TAG_RAW,	0,0,0,0,
	".ig",		troff_ignore,	TAG_PASS,		0,0,0,0,
	".in",		troff_in,	TAG_BREAK,		0,0,0,0,
	".it",		troff_it,	0,			0,0,0,0,
	".kern",	0,		0,			0,0,0,0,
	".lc",		0,		0,			0,0,0,0,
	".lf",		0,		0,			0,0,0,0,
	".ll",		troff_ll,	0,			0,0,0,0,
	".ls",		0,		0,			0,0,0,0,
	".lt",		0,		0,			0,0,0,0,
	".mk",		0,		0,			0,0,0,0,
	".mso",		troff_so,	0,			0,0,0,0,
	".na",		0,		0,			0,0,0,0,
	".ne",		troff_ne,	0,			0,0,0,0,
	".nf",		troff_nf,	TAG_BREAK,		0,0,0,0,
	".nh",		0,		0,			0,0,0,0,
	".nm",		0,		0,			0,0,0,0,
	".nn",		0,		0,			0,0,0,0,
	".nr",		troff_nr,	0,			0,0,0,0,
	".nroff",	0,		0,			0,0,0,0,
	".ns",		0,		0,			0,0,0,0,
	".open",	groff_open,	0,			0,0,0,0,
	".opena",	groff_open,	0,			0,0,0,0,
	".os",		0,		0,			0,0,0,0,
	".pc",		troff_pc,	0,			0,0,0,0,
	".pl",		0,		0,			0,0,0,0,
	".pm",		0,		0,			0,0,0,0,
	".pn",		0,		0,			0,0,0,0,
	".pnr",		0,		0,			0,0,0,0,
	".po",		0,		0,			0,0,0,0,
	".ps",		troff_ps,	0,			0,0,0,0,
	".pso",		groff_pso,	0,			0,0,0,0,
	".ptr",		0,		0,			0,0,0,0,
	".rchar",	0,		0,			0,0,0,0,
	".rj",		troff_ce,	0,			0,0,0,0,
	".rm",		troff_rm,	0,			0,0,0,0,
	".rn",		troff_rn,	0,			0,0,0,0,
	".rnn",		groff_rnn,	0,			0,0,0,0,
	".rr",		troff_rr,	0,			0,0,0,0,
	".rs",		0,		0,			0,0,0,0,
	".rt",		0,		0,			0,0,0,0,
	".shc",		0,		0,			0,0,0,0,
	".shift",	groff_shift,	0,			0,0,0,0,
	".so",		troff_so,	0,			0,0,0,0,
	".sp",		troff_sp,	TAG_BREAK,		0,0,0,0,
	".special",	0,		0,			0,0,0,0,
	".ss",		0,		0,			0,0,0,0,
	".sty",		0,		0,			0,0,0,0,
	".sv",		0,		0,			0,0,0,0,
	".sy",		groff_sy,	0,			0,0,0,0,
	".ta",		troff_ta,	TAG_BREAK,		0,0,0,0,
	".tc",		0,		0,			0,0,0,0,
	".ti",		troff_ti,	TAG_BREAK,		0,0,0,0,
	".tkf",		0,		0,			0,0,0,0,
	".tl",		troff_tl,	0,			0,0,0,0,
	".tm",		troff_tm,	0,			0,0,0,0,
	".tr",		0,		0,			0,0,0,0,
	".trf",		0,		0,			0,0,0,0,
	".trnt",	0,		0,			0,0,0,0,
	".troff",	0,		0,			0,0,0,0,
	".uf",		0,		0,			0,0,0,0,
	".ul",		0,		0,			0,0,0,0,
	".vpt",		0,		0,			0,0,0,0,
	".vs",		troff_vs,	0,			0,0,0,0,
	".warn",	0,		0,			0,0,0,0,
	".wh",		troff_wh,	TAG_PASS,		0,0,0,0,
	".while",	groff_while,	TAG_PASS|TAG_COPY,	0,0,0,0,
	".write",	groff_write,	0,			0,0,0,0,
	".xx",		troff_xx,	TAG_RAW,		0,0,0,0,
};

static Var_t vars[] =
{
	"(**",	"*",
	"(+-",	"\261",
	"(ap",	"~",
	"(bu",	"\267",
	"(bv",	"|",
	"(co",	"\251",
	"(dg",	"\247",
	"(fm",	"'",
	"(lq",	"``",
	"(rg",	"\256",
	"(rq",	"''",
	"(sq",	"\244",
	"eEQ",	".EN",
	"eTS",	".TE",
	"eig",	"..",
	"f",	"",
	"f1",	"1",
	"f2",	"2",
	"f3",	"3",
	"f5",	"5",
	"fB",	"3",
	"fCW",	"5",
	"fF",	"5",
	"fI",	"2",
	"fL",	"5",
	"fM",	"5",
	"fR",	"1",
	"fX",	"5",
};

static Dir_t dot =
{
	0,	".",
};

/*
 * initialize the global data
 */

static void
init(void)
{
	register int	i;

	state.groff |= 2;
	state.tag = &state.top;
	if (!(state.top.sp = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [top buffer]");
	if (!(state.arg = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [arg buffer]");
	if (!(state.nul = sfopen(NiL, "/dev/null", "w")))
		error(ERROR_SYSTEM|3, "out of space [nul buffer]");
	if (!(state.ref = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [ref buffer]");
	if (!(state.req = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [req buffer]");
	if (!(state.tmp = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [tmp buffer]");
	if (!(state.symbols = hashalloc(NiL, HASH_name, "symbols", 0)))
		error(ERROR_SYSTEM|3, "out of space [symbol hash]");
	for (i = 0; i < elementsof(vars); i++)
		if (!hashput(state.symbols, vars[i].name, vars[i].value))
			error(ERROR_SYSTEM|3, "out of space [var hash put]");
	for (i = 0; i < elementsof(tags); i++)
	{
		tags[i].flags |= TAG_STATIC;
		if (!hashput(state.symbols, tags[i].name, &tags[i]))
			error(ERROR_SYSTEM|3, "out of space [tag hash put]");
	}
	hashset(state.symbols, HASH_ALLOCATE);
	nr("%", 1, 0, 1);
	nr(".$", 0, 0, 1);
	nr(".A", 0, 0, 1);
	nr(".C", 0, 0, 1);
	nr(".F", 0, 0, 1);
	nr(".H", 0, 0, 1);
	nr(".L", 0, 0, 1);
	nr(".P", 0, 0, 1);
	nr(".T", 0, 0, 1);
	nr(".R", 0, 0, 1);
	nr(".V", 0, 0, 1);
	nr(".a", 0, 0, 1);
	nr(".b", 0, 0, 1);
	nr(".c", 0, 0, 1);
	nr(".ce", 0, 0, 1);
	nr(".d", 0, 0, 1);
	nr(".ev", 0, 0, 1);
	nr(".f", 0, 0, 1);
	nr(".g", 0, 0, 1);
	nr(".i", 0, 0, 1);
	nr(".in", 0, 0, 1);
	nr(".j", 0, 0, 1);
	nr(".k", 0, 0, 1);
	nr(".l", 0, 0, 1);
	nr(".ll", 0, 0, 1);
	nr(".n", 0, 0, 1);
	nr(".pn", 0, 0, 1);
	nr(".ps", 0, 0, 1);
	nr(".psr", 0, 0, 1);
	nr(".s", 0, 0, 1);
	nr(".sr", 0, 0, 1);
	nr(".t", 0, 0, 1);
	nr(".u", 0, 0, 1);
	nr(".v", 0, 0, 1);
	nr(".vpt", 0, 0, 1);
	nr(".w", 0, 0, 1);
	nr(".warn", 0, 0, 1);
	nr(".x", 0, 0, 1);
	nr(".y", 0, 0, 1);
	nr(".z", 0, 0, 1);
	nr("c.", 0, 0, 1);
	nr("dl", 0, 0, 1);
	nr("dn", 0, 0, 1);
	nr("ln", 0, 0, 1);
	nr("nl", 0, 0, 1);
	nr("systat", 0, 0, 0);
	tm(NiL);
	hot("see", 1);
	hot("refer", 1);
	state.ec = state.eo = DEFAULT_ec;
	state.in = (unsigned char*)"";
	state.in_top = state.in_stack;
	state.out_top = state.out_stack;
	state.tag_top = state.tag_stack;
	state.pc = DEFAULT_pc;
	state.list = state.list_stack;
	state.ta[0] = 8;
	state.ta[1] = 0;
	state.t = 1;
	iop("stderr", 1)->sp = sfstderr;
	state.groff &= 1;
}

/*
 * convert intermediate code in s to html on op
 */

static const char*	opt_align[] =
{
	"LEFT", "CENTER", "RIGHT",
};

static const char*	opt_attribute[] =
{
	"BACKGROUND", "HREF", "HREF", "ID", "NAME", "SIZE", "SRC",
};

static const char*	tag_name[] =
{
	0,		/* unknown	*/
	"A",		/* OP_a		*/
	"BODY",		/* OP_body	*/
	"BR",		/* OP_br	*/
	0,		/* OP_cc	*/
	"CENTER",	/* OP_center	*/
	0,		/* OP_comment	*/
	"DD",		/* OP_dd	*/
	"DIV",		/* OP_div	*/
	"DL",		/* OP_dl	*/
	"DT",		/* OP_dt	*/
	"FN",		/* OP_fn	*/
	0,		/* OP_ft1	*/
	"EM",		/* OP_ft2	*/
	"STRONG",	/* OP_ft3	*/
	0,		/* OP_ft4	*/
	"TT",		/* OP_ft5	*/
	"H2",		/* OP_h2	*/
	"H3",		/* OP_h3	*/
	"H4",		/* OP_h4	*/
	"HEAD",		/* OP_head	*/
	"HR",		/* OP_hr	*/
	"HTML",		/* OP_html	*/
	0,		/* OP_link	*/
	"P",		/* OP_p		*/
	"PRE",		/* OP_pre	*/
	"FONT",		/* OP_ps	*/
	"SUB",		/* OP_sub	*/
	"SUP",		/* OP_sup	*/
	"TABLE",	/* OP_tab	*/
	"TD",		/* OP_tab_data	*/
	"TH",		/* OP_tab_head	*/
	"TR",		/* OP_tab_row	*/
	"TITLE",	/* OP_title	*/
};

/*
 * emit tag and optionally check stack
 */

static void
tag(Sfio_t* op, int index, register int flags, int att, char* att_str, int att_num)
{
	register int		n;
	register int		m;
	register unsigned char*	sp;

	if (index & OP_END)
	{
		index |= OP_LABEL;
		sp = state.tag_top;
		m = 1;
		for (;;)
		{
			if (sp <= state.tag_stack)
			{
				error(2, "tag stack underflow trying to match <%s>", tag_name[OP(index)]);
				sfprintf(sfstderr, "stack contents:\n");
				sp = state.tag_top;
				while (--sp >= state.tag_stack)
					sfprintf(sfstderr, "\t<%s%s%s>\n", (*sp & OP_END) ? "/" : "", tag_name[OP(*sp)], (*sp & OP_LABEL) ? " label=1" : "");
				return;
			}
			n = *--sp;
			if (!(n & OP_END))
			{
				if (n == OP_pre && OP(index) != OP_pre)
				{
					m = 0;
					break;
				}
				n |= OP_END|OP_LABEL;
				if (tag_name[OP(n)])
				{
					flags |= LIST;
					sfprintf(op, "</%s>", tag_name[OP(n)]);
				}
				*sp = n;
			}
			if (n == index)
			{
				*sp &= ~OP_LABEL;
				break;
			}
			m = 0;
		}
		if (m)
		{
			if ((flags & (LINE|LIST)) == (LINE|LIST))
				sfputc(op, '\n');
			state.tag_top = sp;
		}
	}
	else
	{
		if (flags & STACK)
		{
			if (state.tag_top >= &state.tag_stack[elementsof(state.tag_stack)])
				error(3, "tag stack overflow");
			*state.tag_top++ = tag_name[index] ? index : END(index);
		}
		if (tag_name[index])
		{
			sfprintf(op, "<%s", tag_name[index]);
			if (att && ((att & ATT_NUMBER) || att_str))
			{
				sfprintf(op, " %s=", opt_attribute[ATT_INDEX(att)]);
				if (att & ATT_NUMBER)
				{
					if (att == ATT_size)
					{
						if (att_num < 0)
						{
							att_num = -att_num;
							sfputc(op, '-');
						}
						else
							sfputc(op, '+');
						att_num = (att_num + 5) / 6;
					}
					sfprintf(op, "%d", att_num);
				}
				else
					sfprintf(op, "\"%s%s\"", att == ATT_lref ? "#" : "", att_str);
			}
			if (ARG_ATTR(flags))
			{
				if ((n = ARG_ALIGN(flags)) >= 0)
					sfprintf(op, " align=%s", opt_align[n]);
				if (flags & ARG_compact)
					sfputr(op, " COMPACT", -1);
				if (flags & ARG_wide)
					sfputr(op, " width=100%", -1);
			}
			if (index == OP_body)
			{
				if (state.background)
					sfprintf(op, " background=\"%s\"", state.background);
				if (state.logo)
					sfprintf(op, ">\n<CENTER><IMG src=\"%s\"></CENTER", state.logo);
			}
			sfputc(op, '>');
			if (flags & LINE)
				sfputc(op, '\n');
		}
	}
}

/*
 * if OP_dl follows OP_ft,OP_ps then do it now
 */

static void
peeklist(Sfio_t* op, register char* s)
{
	for (;;)
	{
		switch (*s++)
		{
		case 0:
			break;
		case CODE_0:
			continue;
		case CODE_2:
			s++;
			/*FALLTHROUGH*/
		case CODE_1:
			switch (*s++)
			{
			case OP_ft1:
			case OP_ft2:
			case OP_ft3:
			case OP_ft4:
			case OP_ft5:
			case OP_ps:
				continue;
			case OP_dl:
				*--s = CODE_0;
				*--s = CODE_0;
				tag(op, OP_dl, STACK|ARG_compact, 0, NiL, 0);
				continue;
			}
			break;
		case ' ':
		case '\t':
		case '\n':
			continue;
		default:
			break;
		}
		break;
	}
}

#define P() \
	col = br = p = 0

#define DATA() \
	do \
	{ \
		if (li) \
		{ \
			if (li == 1) \
				li = 0; \
			P(); \
		} \
		else if (p) \
		{ \
			P(); \
			sfputr(op, "<P>", '\n'); \
		} \
		else if (br) \
		{ \
			P(); \
			sfputr(op, "<BR>", '\n'); \
		} \
	} while (0)

static void
html(register unsigned char* s, Sfio_t* op)
{
	register int		c;
	register int		br = 0;
	register int		col = 0;
	register int		li = 0;
	register int		p = 0;
	register int		nf = 0;
	register unsigned char*	v;
	unsigned char*		t;
	unsigned char*		u;
	int			n;
	int			m;
	int			ta;
	int			ts;
	int			ft = 1;
	int			hot = 0;
	int			label = 0;
	int			ps = DEFAULT_ps;
	int			ps_set = 0;
	Dir_t*			x;
	Sfio_t*			subject;
	char			a[2];

	sfputr(op, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">", '\n');
	tag(op, OP_html, STACK|LINE, 0, NiL, 0);
	tag(op, OP_head, STACK|LINE, 0, NiL, 0);
	t = (unsigned char*)strchr(usage, '\n') + 5;
	sfprintf(op, "<META name=\"generator\" content=\"%-.*s", strchr((char*)t, '\n') - (char*)t, t);
	for (x = state.macros; x; x = x->next)
		sfprintf(op, " -m%s", x->name);
	sfputr(op, "\">\n", -1);
	tag(op, OP_title, STACK, 0, NiL, 0);
	if (!(subject = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [subject]");
	if (state.package)
		sfputr(subject, state.package, ' ');
	if (state.title)
		sfputr(subject, state.title, -1);
	else
	{
		if (state.input)
		{
			if (t = (unsigned char*)strrchr(state.input, '/'))
				t++;
			else
				t = (unsigned char*)state.input;
			sfputr(subject, (char*)t, -1);
		}
		if (state.macros)
			sfprintf(subject, " m%s document", state.macros->name);
	}
	sfputr(op, sfstruse(subject), -1);
	tag(op, END(OP_title), STACK|LINE, 0, NiL, 0);
	if (state.author)
		sfprintf(op, "<AUTHOR>%s</AUTHOR>\n", state.author);
	if (!state.head)
	{
		tag(op, END(OP_head), STACK|LINE, 0, NiL, 0);
		tag(op, OP_body, STACK|LINE, 0, NiL, 0);
	}
	for (;;)
	{
		switch (*s++)
		{
		case 0:
			break;
		case '&':
			DATA();
			col++;
			sfputr(op, "&amp;", -1);
			continue;
		case '<':
			DATA();
			col++;
			sfputr(op, "&lt;", -1);
			continue;
		case '>':
			DATA();
			col++;
			sfputr(op, "&gt;", -1);
			continue;
		case CODE_0:
			continue;
		case CODE_2:
			c = *s++;
			/*FALLTHROUGH*/
		case CODE_1:
			if (!nf)
			{
				n = *s ^ OP_END;
				for (v = s + 1;;)
				{
					switch (*v++)
					{
					case '\n':
					case ' ':
					case '\t':
						continue;
					case CODE_0:
						continue;
					case CODE_1:
						if ((m = *v++) == n)
						{
							n = 0;
							s = v;
							break;
						}
						else if (m != OP_br && m != OP_p)
							break;
						continue;
					case CODE_2:
						if (*v++ != ' ' || *v++ != OP_cc)
							break;
						continue;
					default:
						break;
					}
					break;
				}
				if (!n)
					continue;
			}
			switch (m = *s++)
			{
			case END(OP_a):
				tag(op, m, STACK, 0, NiL, 0);
				*--s = a[1];
				*--s = a[0];
				break;
			case OP_body:
				tag(op, m, STACK|LINE, 0, NiL, 0);
				tag(op, OP_hr, LINE, 0, NiL, 0);
				col = 0;
				goto compact;
			case OP_br:
				while (*s == ' ')
					s++;
				if (!li)
				{
					if (nf)
						goto compact;
					else
						br++;
				}
				break;
			case OP_cc:
				DATA();
				sfputc(op, '&');
				switch (c)
				{
				case '&':
					sfputr(op, "amp", -1);
					break;
				case '<':
					sfputr(op, "lt", -1);
					break;
				case '>':
					sfputr(op, "gt", -1);
					break;
				case ' ':
					sfputr(op, "nbsp", -1);
					break;
				default:
					sfprintf(op, "#%03d", c);
					break;
				}
				sfputc(op, ';');
				col++;
				break;
			case OP_center:
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				tag(op, m, STACK|c, 0, NiL, 0);
				if (!state.head)
				{
					tag(op, OP_h2, STACK, 0, NiL, 0);
					for (;;)
					{
						switch (n = *s++)
						{
						case 0:
						case '\n':
						case CODE_1:
						case CODE_2:
						case CODE_n:
							s--;
							break;
						default:
							sfputc(op, n);
							continue;
						}
						break;
					}
					tag(op, END(OP_h2), STACK, 0, NiL, 0);
					tag(op, OP_h4, STACK, 0, NiL, 0);
				}
				break;
			case END(OP_center):
				col = 0;
				if (!state.head)
				{
					state.head = 1;
					tag(op, END(OP_h4), STACK, 0, NiL, 0);
				}
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case LABEL(OP_dd):
				tag(op, END(OP_a), STACK, 0, NiL, 0);
				tag(op, END(OP_h3), STACK, 0, NiL, 0);
				/*FALLTHROUGH*/
			case OP_dd:
				li = 1;
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				tag(op, OP_dd, 0, 0, NiL, 0);
				break;
			case OP_div:
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				tag(op, m, STACK|c, 0, NiL, 0);
				break;
			case END(OP_div):
				col = 0;
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case OP_dl:
				li = 0;
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				tag(op, m, STACK|LINE|ARG_compact, 0, NiL, 0);
				break;
			case END(OP_dl):
				v = s;
				for (;;)
				{
					switch (*v++)
					{
					case CODE_0:
						continue;
					case CODE_1:
						switch (*v++)
						{
						case OP_dl:
							*(v - 2) = *(v - 1) = CODE_0;
							v = 0;
							break;
						default:
							continue;
						}
						break;
					case CODE_2:
						v += 2;
						continue;
					default:
						break;
					}
					break;
				}
				if (v)
				{
					li = 0;
					col = 0;
					tag(op, m, STACK|LINE, 0, NiL, 0);
				}
				break;
			case LABEL(OP_dt):
				label = 1;
				n = 1;
				/*FALLTHROUGH*/
			case OP_dt:
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				if (p)
				{
					P();
					tag(op, OP_p, LINE, 0, NiL, 0);
				}
				v = s;
				for (;;)
				{
					switch (*v++)
					{
					case CODE_0:
						continue;
					case CODE_2:
						v++;
						/*FALLTHROUGH*/
					case CODE_1:
						switch (*v++)
						{
						case OP_dl:
							*(v - 2) = *(v - 1) = CODE_0;
							tag(op, OP_dl, STACK|LINE|ARG_compact, 0, NiL, 0);
							if (!label)
								break;
							continue;
						case OP_dd:
							if (label)
								*(v - 1) = LABEL(OP_dd);
							break;
						default:
							continue;
						}
						break;
					case ' ':
					case '\t':
					case '\n':
						if (!label)
							break;
						if (!n)
						{
							n = 1;
							sfputc(state.tmp, ' ');
						}
						continue;
					case '"':
						if (!label)
							break;
						continue;
					default:
						if (!label)
							break;
						n = 0;
						sfputc(state.tmp, *(v - 1));
						continue;
					}
					break;
				}
				li = 2;
				tag(op, OP_dt, LINE, 0, NiL, 0);
				if (label)
				{
					label = 0;
					n = sfstrtell(state.tmp);
					v = (unsigned char*)sfstruse(state.tmp);
					while (--n > 0 && (isspace(v[n]) || v[n] == '.'));
					v[n + 1] = 0;
					if (isdigit(*v))
						tag(op, OP_hr, LINE, 0, NiL, 0);
					tag(op, OP_h3, STACK, 0, NiL, 0);
					tag(op, OP_a, STACK, ATT_name, (char*)v, 0);
				}
				break;
			case END(OP_fn):
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case OP_ft1:
			case OP_ft2:
			case OP_ft3:
			case OP_ft4:
			case OP_ft5:
				if (hot)
				{
					int	ob = 0;
					int	cb = 0;
					int	p = 0;
					int	r = ATT_lref;
					int	z = 0;

					v = s;
					for (;;)
					{
						switch (n = *v++)
						{
						case 0:
							hot = 0;
							break;
						case CODE_0:
							continue;
						case CODE_2:
							v++;
							/*FALLTHROUGH*/
						case CODE_1:
							n = *v++;
							if (!p && n >= OP_ft1 && n <= OP_ft5)
								p = -1;
							continue;
						case '(':
						case '[':
						case '{':
						case '<':
							if (n == ob)
								p++;
							else if (p <= 0)
							{
								r = ATT_href;
								p = 1;
								switch (ob = n)
								{
								case '(':
									cb = ')';
									break;
								case '[':
									cb = ']';
									break;
								case '{':
									cb = '}';
									break;
								case '<':
									cb = '>';
									break;
								}
							}
							z = 0;
							sfputc(state.tmp, n);
							continue;
						case ')':
						case ']':
						case '}':
						case '>':
							if (p <= 0)
							{
								v--;
								break;
							}
							z = 0;
							sfputc(state.tmp, n);
							if (n == cb && !--p)
								break;
							continue;
						case ' ':
						case '\t':
						case '\n':
							if (!z)
							{
								z = 1;
								sfputc(state.tmp, ' ');
							}
							continue;
						case '"':
							continue;
						default:
							if (p < 0)
							{
								v--;
								break;
							}
							z = 0;
							sfputc(state.tmp, n);
							continue;
						}
						break;
					}
					n = sfstrtell(state.tmp);
					if (!*(t = (unsigned char*)sfstruse(state.tmp)))
						hot = 0;
					if (hot)
					{
						hot = 0;
						while (--n > 0 && (isspace(t[n]) || t[n] == '.'));
						t[n + 1] = 0;
						tag(op, OP_a, STACK, r, (char*)t, 0);
						a[0] = v[0];
						v[0] = CODE_1;
						a[1] = v[1];
						v[1] = END(OP_a);
					}
				}
				c = m - OP_ft;
				if (c != ft)
				{
					peeklist(op, (char*)s);
					if (ft != 1)
						tag(op, END(OP_ft + ft), STACK, 0, NiL, 0);
					ft = c;
					if (ft != 1)
						tag(op, OP_ft + ft, STACK, 0, NiL, 0);
				}
				break;
			case OP_h2:
			case OP_h3:
			case OP_h4:
			case END(OP_h2):
			case END(OP_h3):
			case END(OP_h4):
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case OP_head:
			case END(OP_head):
				col = 0;
				tag(op, m, STACK|LINE, 0, NiL, 0);
				goto compact;
			case OP_hr:
				col = 0;
				tag(op, OP_hr, LINE, 0, NiL, 0);
				goto compact;
			case OP_p:
				while (*s == ' ')
					s++;
				if (c)
				{
					if (col)
					{
						col = 0;
						sfputc(op, '\n');
					}
					P();
					tag(op, m, STACK|c, 0, NiL, 0);
				}
				else if (!li)
				{
					if (nf)
						goto compact;
					else
						p++;
				}
				break;
			case END(OP_p):
				col = 0;
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case OP_pre:
				if (!nf)
				{
					nf = 03;
					tag(op, m, STACK, 0, NiL, 0);
				}
				goto compact;
			case END(OP_pre):
				if (nf)
				{
					nf = 02;
					tag(op, m, STACK, 0, NiL, 0);
				}
				goto compact;
			case OP_ps:
				if (c != ps)
				{
					peeklist(op, (char*)s);
					if (ps_set)
					{
						ps = ps_set;
						ps_set = 0;
						tag(op, END(OP_ps), STACK, 0, NiL, 0);
					}
					if (n = c - ps)
					{
						ps_set = ps;
						ps = c;
						tag(op, OP_ps, STACK, ATT_size, NiL, n);
					}
				}
				break;
			case OP_sub:
			case END(OP_sub):
			case OP_sup:
			case END(OP_sup):
				tag(op, m, STACK, 0, NiL, 0);
				break;
			case OP_tab:
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				tag(op, m, STACK|c, 0, NiL, 0);
				goto compact;
			case END(OP_tab):
				tag(op, m, STACK|LINE, 0, NiL, 0);
				break;
			case OP_tab_data:
			case OP_tab_head:
			case OP_tab_row:
				tag(op, m, c, 0, NiL, 0);
				break;
			default:
				if (!(v = (unsigned char*)tag_name[OP(m)]))
				{
					sfprintf(state.tmp, "(%d)", OP(m));
					v = (unsigned char*)sfstruse(state.tmp);
				}
				error(2, "internal error: <%s%s%s %d> ignored", (m & OP_END) ? "/" : "", v, (m & OP_LABEL) ? " label=" : "", c);
				break;
			compact:
				if (col)
					sfputc(op, '\n');
				P();
				v = s;
				for (;;)
				{
					switch (*s++)
					{
					case 0:
						break;
					case '\n':
						p++;
						col = 0;
						v = s;
						continue;
					case ' ':
					case '\t':
						if (!nf)
							break;
						col = 1;
						continue;
					case CODE_0:
						continue;
					case CODE_1:
						switch (*s)
						{
						case OP_pre:
							if (!nf)
								break;
							/*FALLTHROUGH*/
						case OP_br:
						case OP_p:
							s++;
							p++;
							col = 0;
							v = s;
							continue;
						case OP_body:
							p = 0;
							col = 0;
							break;
						}
						break;
					}
					if (col)
					{
						col = 0;
						s = v;
					}
					else
						s--;
					break;
				}
				if (nf > 1)
					nf &= 01;
				else if (nf && p)
					sfputc(op, '\n');
				p = 0;
				break;
			}
			c = 0;
			continue;
		case CODE_n:
			n = *s++;
			if (n & 0200)
			{
				n = (n & 0177) << 8;
				n |= *s++;
			}
			c = *s++;
			v = s;
			s += n;
			switch (c)
			{
			case OP_comment:
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				sfprintf(op, "<!--\"%s\"-->\n", v);
				break;
			case OP_fn:
				tag(op, c, STACK, ATT_id, (char*)v, 0);
				break;
			case OP_link:
				DATA();
				if (u = (unsigned char*)strchr((char*)v, '\t'))
					*u++ = 0;
				else
					u = 0;
				t = v;
				while (isdigit(*v))
					v++;
				while (isalpha(*v))
					v++;
				if (*v == ':' || *v == '/' || *v == '.' || *(v + 1) == '/')
				{
					if (!u)
						u = v + 1;
					v = (unsigned char*)"";
				}
				else
				{
					if (!u)
						u = t;
					v = (unsigned char*)"#";
				}
				sfprintf(op, "<A href=\"%s%s\">%s</A>", v, t, u);
				break;
			case LABEL(OP_link):
				DATA();
				if (u = (unsigned char*)strchr((char*)v, '\t'))
					*u++ = 0;
				else
					u = v;
				sfprintf(op, "<A name=\"%s\">%s</A>", v, u);
				break;
			case OP_ta:
				strcpy((char*)state.ta, (char*)v);
				break;
			case OP_RAW:
				DATA();
				if (col)
				{
					col = 0;
					sfputc(op, '\n');
				}
				sfputr(op, (char*)v, '\n');
				break;
			}
			continue;
		case ' ':
			if (nf)
			{
				col++;
				sfputc(op, *(s - 1));
			}
			else
			{
				while (isspace(*s))
					s++;
				if (col >= 70)
				{
					col = 0;
					sfputc(op, '\n');
				}
				else if (col > 0)
				{
					col++;
					sfputc(op, ' ');
				}
			}
			continue;
		case '\t':
			if (nf)
			{
				ta = state.ta[ts = 0];
				while (col >= ta)
				{
					if (state.ta[ts+1])
						ts++;
					ta += state.ta[ts];
				}
				do
				{
					sfputc(op, ' ');
				} while (++col < ta);
			}
			else
			{
				col++;
				sfputc(op, '\t');
			}
			continue;
		case '\n':
			if (nf)
			{
				v = s;
				col = 0;
				for (;;)
				{
					switch (*v++)
					{
					case 0:
						break;
					case '\n':
						continue;
					case ' ':
					case '\t':
						continue;
					case CODE_0:
						continue;
					case CODE_1:
						switch (*v++)
						{
						case OP_br:
						case OP_p:
						case OP_pre:
							continue;
						case END(OP_pre):
							col = 1;
							s = v - 2;
							break;
						}
						break;
					}
					break;
				}
				if (col)
				{
					col = 0;
					continue;
				}
				sfputc(op, '\n');
			}
			else
			{
				while (isspace(*s))
					s++;
				if (col >= 70)
				{
					col = 0;
					sfputc(op, '\n');
				}
				else if (col > 0)
				{
					col++;
					sfputc(op, ' ');
				}
			}
			continue;
		case '(':
			if (hot)
				hot = 0;
			else
				for (x = state.hot; x; x = x->next)
				{
					v = s;
					u = (unsigned char*)x->name;
					do
					{
						if (!*u)
						{
							if (isspace(*v))
							{
								hot = 1;
								goto data;
							}
							break;
						}
					} while (*u++ == *v++);
				}
			goto data;
		case ')':
			hot = 0;
			goto data;
		case '.':
			if (!nf && isspace(*s))
			{
				while (isspace(*s))
					s++;
				col = 0;
				sfputc(op, '.');
				sfputc(op, '\n');
				continue;
			}
			/*FALLTHROUGH*/
		default:
		data:
			DATA();
			col++;
			sfputc(op, *(s - 1));
			continue;
		}
		break;
	}
	if (col)
		sfputc(op, '\n');
	if (state.mailto)
		sfprintf(op, "<P>Send comments and suggestions to <A href=\"mailto:%s?subject=%s\">%s</A>.\n", state.mailto, state.mailto, sfstrbase(subject));
	if (state.author || state.corporation || state.company || state.location)
	{
		t = (unsigned char*)"<P>";
		u = (unsigned char*)"<BR>";
		if (state.author)
		{
			sfprintf(op, "%s%s\n", t, state.author);
			t = u;
		}
		if (state.organization)
		{
			sfprintf(op, "%s%s\n", t, state.organization);
			t = u;
		}
		if (state.corporation || state.company)
		{
			sfputr(op, (char*)t, -1);
			t = u;
			if (state.corporation)
				sfputr(op, state.corporation, state.company ? ' ' : '\n');
			if (state.company)
				sfputr(op, state.company, '\n');
		}
		if (state.address)
		{
			sfprintf(op, "%s%s\n", t, state.address);
			t = u;
		}
		if (state.location)
		{
			sfprintf(op, "%s%s\n", t, state.location);
			t = u;
		}
		if (state.phone)
		{
			sfprintf(op, "%s%s\n", t, state.phone);
			t = u;
		}
	}
	sfstrclose(subject);
	if (!state.footer)
		sfprintf(op, "<P>%s\n", fmttime("%B %d, %Y", state.date));
	if (state.toolbar && (subject = find(state.toolbar, NiL, 1)))
	{
		sfmove(subject, sfstdout, SF_UNBOUND, -1);
		sfclose(subject);
	}
	tag(op, END(OP_body), STACK, 0, NiL, 0);
	tag(op, END(OP_html), STACK, 0, NiL, 0);
	sfputc(op, '\n');
}

main(int argc, char** argv)
{
	register int		n;
	register char*		s;
	char*			v;
	Dir_t*			x;
	Dir_t*			lastdir;
	Dir_t*			lastmac;
	Sfio_t*			ip;
	Sfio_t*			op;
	Sfio_t*			script;

	NoP(argc);
	error_info.id = "troff2html";
	state.dirs = lastdir = &dot;
	init();
	script = 0;
	for (;;)
	{
		switch (optget(argv, usage))
		{
		case 0:
			break;
		case 'i':
			if (!(op = sfopen(NiL, opt_info.arg, "r")))
				error(ERROR_SYSTEM|2, "%s: cannot read", opt_info.arg);
			else
			{
				if (!(s = sfreserve(op, SF_UNBOUND, 0)) || (n = sfvalue(op)) <= 0 || s[n - 1] != '\n')
					error(1, "%s: invalid info file", opt_info.arg);
				else
				{
					s[n] = 0;
					stropt(s, options, sizeof(*options), setopt, NiL);
				}
				sfclose(op);
			}
			continue;
		case 'm':
			if (!(x = newof(0, Dir_t, 1, 0)))
				error(ERROR_SYSTEM|3, "out of space [macros]");
			x->name = opt_info.arg;
			if (state.macros)
				lastmac = lastmac->next = x;
			else
				state.macros = lastmac = x;
			continue;
		case 'r':
			if (*(s = opt_info.arg))
			{
				opt_info.num = expression(s + 1, NiL, 0);
				s[1] = 0;
				nr(s, opt_info.num, 0, 0);
			}
			continue;
		case 's':
			if (!script && !(script = sfstropen()))
				error(ERROR_SYSTEM|3, "out of space [script]");
			sfputr(script, opt_info.arg, '\n');
			continue;
		case 'v':
			state.verbose = 1;
			continue;
		case 'I':
			if (streq(opt_info.arg, "-"))
				dot.name[0] = 0;
			else if (!(x = newof(0, Dir_t, 1, 0)))
				error(ERROR_SYSTEM|3, "out of space [dir]");
			else
			{
				x->name = opt_info.arg;
				lastdir = lastdir->next = x;
			}
			continue;
		case '?':
			error(ERROR_USAGE|4, "%s", opt_info.arg);
			continue;
		case ':':
			if (opt_info.name[1] != '-')
			{
				error(2, "%s", opt_info.arg);
				continue;
			}
			if (v = strchr(argv[opt_info.index - 1], '='))
				v = sfprints("%s=%s", opt_info.name + 2, fmtquote(v + 1, "\"", "\"", strlen(v + 1), FMT_ALWAYS));
			else
				v = opt_info.name + 2;
			stropt(v, options, sizeof(*options), setopt, options);
			continue;
		}
		break;
	}
	if (!dot.name[0])
		state.dirs = state.dirs->next;
	argv += opt_info.index;
	if (error_info.errors)
		error(ERROR_USAGE|4, "%s", optusage(NiL));
	if (!(op = sfstropen()))
		error(ERROR_SYSTEM|3, "out of space [output]");
	if (script)
	{
		pushin("script", 1, NiL, sfstruse(script), NiL);
		process(NiL, NiL, op);
		sfstrclose(script);
	}
	for (x = state.macros; x; x = x->next)
		if (ip = find(x->name, &v, -1))
			process(v, ip, op);
	if (!(state.input = *argv))
		process(NiL, sfstdin, op);
	else
		while (s = *argv++)
			if (ip = find(s, &v, 1))
				process(v, ip, op);
	if (state.out)
	{
		if (state.it.center)
		{
			state.it.center = 0;
			code_1(END(OP_center));
		}
		while (state.list > state.list_stack)
		{
			if (state.list->dl)
				code_1(END(OP_dl));
			state.list--;
		}
		code_1(OP_hr);
		trigger(&state.fini);
		process(NiL, NiL, op);
		html((unsigned char*)sfstruse(op), sfstdout);
	}
	exit(error_info.errors != 0);
}
