ftp.nice.ch/pub/next/unix/editor/elvis-2.0.N.bs.tar.gz#/elvis-2.0.N.bs/dmmarkup.c

This is dmmarkup.c in view mode; [Download] [Up]

/* dmmarkup.c */
/* Copyright 1995 by Steve Kirkendall */

char id_dmmarkup[] = "$Id: dmmarkup.c,v 2.43 1996/09/18 16:42:14 steve Exp $";

/* This file contains some fairly generic text formatting code -- generic
 * in the sense that it can be easily tweaked to format a variety of types
 * marked-up text.  Currently, it supports useful subsets of NROFF and HTML
 * instructions.
 */

#include "elvis.h"
#ifdef DISPLAY_MARKUP



#define GRANULARITY 64	/* number of LINEINFOs to allocate at a time */



typedef struct
{
	CHAR	text[200];	/* raw text of token */
	long	offset[200];	/* offsets of characters */
	int	nchars;		/* number of characters in text[] */
	int	width;		/* normal displayed width of text[] */
	struct markup_s	*markup;/* info about markup token */
} TOKEN;

typedef struct markup_s
{
	char	*name;		/* name of the markup */
	char	attr[8];	/* attributes of markup */
	BOOLEAN	(*fn)P_((TOKEN *));/* ptr to special function */	
} MARKUP;
#define TITLE	attr[0]		/* in title: -, N, Y */
#define BREAKLN	attr[1]		/* line break: -, 0, 1, 2, c, or p */
#define INDENT	attr[2]		/* -, <, >, or a number */
#define LIST	attr[3]		/* in list: -, N, Y, # */
#define FONT	attr[4]		/* font: -, =, n, b, u, i, f, e, N, B, U, I, F, E */
#define FILL	attr[5]		/* Y=fill, N=preformatted, -=no chg. */
#define DEST	attr[6]		/* S=section, P=paragraph, T=<tab> key */

typedef struct
{
	long	 offset;	/* offset of start of line */
	struct
	{
	    unsigned indent : 8;  /* indentation amount, in spaces */
	    unsigned listcnt : 8; /* Counter for nest#1 numbered list; 0=not numbered */
	    unsigned nest : 6;	  /* nesting level of list/menu; 0=not in list*/
	    unsigned prefmt : 1;  /* 1=literal whitespace, 0=fill */
	    unsigned graphic : 1; /* 1=replace |-^. with graphic chars */
	    unsigned midline: 1;  /* 1=after a newline, 0=after other char */
	    unsigned reduce : 1;  /* 1=fewer newlines, 0=normal qty newlines */
	    unsigned deffont : 3; /* index into "nbiufe" of default font char */
	    unsigned curfont : 3; /* index into "nbiufe" of current font char */
	}	state;
} LINEINFO;

typedef struct
{
	TOKEN	*(*get)P_((CHAR **));	/* mode-dependent get() */
	void	(*escape)P_((TOKEN *));	/* mode-dependent escape() */
	LINEINFO *line;			/* line array */
	long	nlines;			/* number of lines in line array */
	long	endtitle;		/* offset of the end of the title */
	CHAR	*title;			/* title of document, or NULL */
	CHAR	**defs;			/* macros within the text */
} MUINFO;

static BOOLEAN	first;	/* is this the first token on this line? */
static BOOLEAN	anyspc;	/* has whitespace been encountered? */
static BOOLEAN	title;	/* collecting characters of the title */
static BOOLEAN	list;	/* o_list */
static int	textwidth;/* o_columns */
static int	tabstop;/* o_tabstop */
static int	listind;/* o_shiftwidth/2, or 2 if shiftwidth<=4 */
static int	col;	/* logical column number */
static MUINFO	*mui;	/* pointer to muinfo */

static BOOLEAN	prefmt;	/* True=literal whitespace, False=fill */
static BOOLEAN	graphic;/* True=replace |-^. with graphic chars */
static BOOLEAN	midline;/* False=after newline, True=after other character */
static BOOLEAN	reduce;	/* True=fewer newlines, False=normal qty newlines */
static char	deffont;/* default font */
static char	curfont;/* current font */
static int	indent;	/* indentation amount */
static int	nest;	/* nesting level of list/menu; 0=not in list */
static int	listcnt;/* Counter for nest#1 numbered list; 0=not numbered */

/* These variables store the string and font collected by the manarg() function
 * and output by the manput() function.  The "manlen" variable should be
 * initialized to 0 before the first call to manarg().  NOTE: These variables
 * and the manput() function are also used by htmlimg().
 */
static int	manlen;		/* length of "mantext" string */
static CHAR	mantext[80];	/* buffer, holds args from .XX macro */
static long	manoffset[80];	/* holds offsets of mantext[] chars */
static char	manfont[80];	/* holds fonts of mantext[] chars */

/* Forward declarations of some functions which are static to this file */
static void	htmlescape P_((TOKEN *tok));
static BOOLEAN	htmlhr P_((TOKEN *token));
static BOOLEAN	htmlimg P_((TOKEN *token));
static BOOLEAN	htmlpre P_((TOKEN *token));
static BOOLEAN	htmlli P_((TOKEN *token));
static BOOLEAN	htmlinput P_((TOKEN *token));
static void	htmlmarkup P_((TOKEN *token));
static TOKEN	*htmlget P_((CHAR **refp));
static DMINFO	*htmlinit P_((WINDOW win));
static CHAR	*htmltagatcursor P_((WINDOW win, MARK cursor));
static MARK	htmltagload P_((CHAR *tagname, MARK from));
static MARK	htmltagnext P_((MARK cursor));
static void	manescape P_((TOKEN *tok));
static int	manarg P_((TOKEN *token, int start, _char_ font, BOOLEAN spc));
static BOOLEAN	manput P_((void));
static BOOLEAN	manTH P_((TOKEN *token));
static BOOLEAN	manSH P_((TOKEN *token));
static BOOLEAN	manBI P_((TOKEN *token));
static BOOLEAN	manIP P_((TOKEN *token));
static void	manmarkup P_((TOKEN *token));
static TOKEN	*manget P_((CHAR **refp));
static DMINFO	*maninit P_((WINDOW win));
static void	countchar P_((CHAR *p, long qty, _char_ font, long offset));
static BOOLEAN	put P_((TOKEN *token));
static void	term P_((DMINFO *info));
static long	mark2col P_((WINDOW w, MARK mark, BOOLEAN cmd));
static MARK	move P_((WINDOW w, MARK from, long linedelta, long column, BOOLEAN cmd));
static MARK	setup P_((MARK top, long cursor, MARK bottom, DMINFO *info));
static MARK	image P_((WINDOW w, MARK line, DMINFO *info, void (*draw)(CHAR *p, long qty, _char_ font, long offset)));
static void	header P_((WINDOW w, int pagenum, DMINFO *info, void (*draw)(CHAR *p, long qty, _char_ font, long offset)));
static int	start P_((WINDOW win, MARK from, void (*draw)(CHAR *p, long qty, _char_ font, long offset)));
static void	storestate P_((long offset, LINEINFO *dest));
static void	findtitle P_((BUFFER buf));

/* Only a single TOKEN is ever really needed at one time */
static TOKEN	rettok;

/* Offset of cursor.  This affects the expansion of escapes, and the visibility
 * of markups.
 */
static long	cursoff;

/* Offset of a space character, if "anyspc" is True. */
static long	spcoffset;

/* This the drawchar pointer points to a function for outputting a single
 * character.
 */
static void	(*drawchar) P_((CHAR *p, long qty, _char_ font, long offset));

/* Special characters.  These are stored in variables rather than macros so
 * that we can pass their address to (*drawchar)().
 */
static CHAR	hyphen = '-';
static CHAR	newline = '\n';
static CHAR	formfeed = '\f';
static CHAR	vtab = '\013';
static CHAR	space = ' ';
static CHAR	bullet = '*';


/*----------------------------------------------------------------------------*/
/* HTML-specific functions and variables                                      */

/* Replace entities such as &lt; with their single-character equivelent. */
static void htmlescape(token)
	TOKEN	*token;	/* a token whose text is to be expanded */
{
	char	*src, *dst;
	long	*off;
	size_t	len;
	int	width;

	/* step through the string */
	for (src = dst = (char *)token->text, off = token->offset, width = 0;
	     src < (char *)&token->text[token->nchars];
	     src++, off++, width++)
	{
		/* if not a &, then this can't be an escape */
		if (*src != '&')
			goto NoEscape;

		/* find the length of this escape's name */
		for (len = 1; src[len] != ';'; len++)
		{
			if (!src[len] || isspace(src[len]))
				goto NoEscape;
		}
		len++;

		/* if the cursor is on this escape, then don't expand it */
		if ((o_showmarkups && off[0] <= cursoff && cursoff <= off[len - 1]) || list)
		{
			/* Tweak the value of "width" so that after all of
			 * this escape's characters have been counted, "width"
			 * will have been incremented by only 1 since that's
			 * how wide the escape would normally be.
			 */
			width -= (len - 1);

			/* Don't expand the escape */
			goto NoEscape;
		}

		/* recognize it? */
		if (len == 4 && (!strncmp(&src[1], "lt", 2) || !strncmp(&src[1], "LT", 2)))
			*dst++ = '<';
		else if (len == 4 && (!strncmp(&src[1], "gt", 2) || !strncmp(&src[1], "GT", 2)))
			*dst++ = '>';
		else if (len == 5 && (!strncmp(&src[1], "amp", 3) || !strncmp(&src[1], "AMP", 3)))
			*dst++ = '&';
		else if (len == 6 && (!strncmp(&src[1], "quot", 4) || !strncmp(&src[1], "QUOT", 4)))
			*dst++ = '"';
		else if (len == 6 && (!strncmp(&src[1], "nbsp", 4) || !strncmp(&src[1], "NBSP", 4)))
			*dst++ = ' ';
		else if (len == 7 && !strncmp(&src[1], "AElig", 5))
			*dst++ = digraph('E', 'A');
		else if (len == 7 && !strncmp(&src[1], "aelig", 5))
			*dst++ = digraph('e', 'a');
		else if (len == 5 && !strncmp(&src[1], "ETH", 3))
			*dst++ = digraph('-', 'D');
		else if (len == 5 && !strncmp(&src[1], "eth", 3))
			*dst++ = digraph('-', 'd');
		else if (len == 7 && !strncmp(&src[1], "THORN", 5))
			*dst++ = digraph('T', 'P');
		else if (len == 7 && !strncmp(&src[1], "thorn", 5))
			*dst++ = digraph('t', 'p');
		else if (len == 7 && !strncmp(&src[1], "szlig", 5))
			*dst++ = digraph('s', 'z');
		else if (len == 8 && !strncmp(&src[2], "grave", 5))
			*dst++ = digraph('`', (_CHAR_)src[1]);
		else if (len == 8 && !strncmp(&src[2], "acute", 5))
			*dst++ = digraph('\'', (_CHAR_)src[1]);
		else if (len == 7 && !strncmp(&src[2], "circ", 4))
			*dst++ = digraph('^', (_CHAR_)src[1]);
		else if (len == 8 && !strncmp(&src[2], "tilde", 5))
			*dst++ = digraph('~', (_CHAR_)src[1]);
		else if (len == 6 && !strncmp(&src[2], "uml", 3))
			*dst++ = digraph('"', (_CHAR_)src[1]);
		else if (len == 7 && !strncmp(&src[2], "ring", 4))
			*dst++ = digraph('*', (_CHAR_)src[1]);
		else if (len == 8 && !strncmp(&src[2], "cedil", 5))
			*dst++ = digraph(',', (_CHAR_)src[1]);
		else if (len == 8 && !strncmp(&src[2], "slash", 5))
			*dst++ = digraph('/', (_CHAR_)src[1]);
		else if (src[1] == '#')
			*dst++ = atoi(&src[2]);
		else if (len == 6 && (!strncmp(&src[1], "copy", 4) || !strncmp(&src[1], "COPY", 4)))
			*dst++ = digraph('O', 'c');
		else if (len == 5 && (!strncmp(&src[1], "reg", 3) || !strncmp(&src[1], "REG", 3)))
			*dst++ = digraph('O', 'r');
		else if (len == 7 && (!strncmp(&src[1], "ldquo", 5) || !strncmp(&src[1], "LDQUO", 5)
				|| !strncmp(&src[1], "rdquo", 5) || !strncmp(&src[1], "RDQUO", 5)))
			*dst++ = '"';
		else if (len == 7 && (!strncmp(&src[1], "lsquo", 5) || !strncmp(&src[1], "LSQUO", 5)))
			*dst++ = '`';
		else if (len == 7 && (!strncmp(&src[1], "rsquo", 5) || !strncmp(&src[1], "RSQUO", 5)))
			*dst++ = '\'';
		else
		{
			/* Tweak the value of "width" so that after all of
			 * this escape's characters have been counted, "width"
			 * will have been incremented by only 1 since that's
			 * how wide the escape would normally be if it was
			 * recognized.
			 */
			width -= (len - 1);

			/* Don't expand the escape */
			goto NoEscape;
		}

		/* Skip past the escape sequence. */
		token->offset[(int)(dst - (char *)token->text) - 1] = *off;
		src += len - 1;
		off += len - 1;
		/* plus one more at the for-loop */
		continue;

NoEscape:
		/* Not an escape -- copy it literally */
		*dst++ = *src;
		token->offset[(int)(dst - (char *)token->text) - 1] = *off;
	}

	/* compute the new length */
	token->nchars = (int)(dst - (char *)token->text);
	token->text[token->nchars] = '\0';
	token->width = width;
}

/* output a horizontal rule */
static BOOLEAN htmlhr(token)
	TOKEN	*token;
{
	int	len;
	long	offset;

	assert(col == 0 && textwidth > 0);

	/* Normally the hrule is as wide as the line, but if the cursor is on
	 * the "<hr>" token then we want to shrink it slightly so that "<hr>"
	 * will fit on the same row of the window.  This allows the window to
	 * be updated more efficiently.
	 */
	len = textwidth;
	if (list || (o_showmarkups && token->offset[0] <= cursoff && cursoff <= token->offset[token->nchars - 1]))
	{
		/* leave room for showing the <hr> tag */
		len -= textwidth - token->nchars;
		offset = -1;
	}
	else
	{
		/* make it as wide as the screen; the <hr> won't show */
		len = textwidth;
		offset = token->offset[0];
	}
	if (len < 3)
		len = 3;

	/* draw the hrule */
	(*drawchar)(&space, -1, 'g', -1);
	(*drawchar)(&hyphen, -(len - 2), 'g', offset);
	col = len - 1;
	return False;
}

/* Output the "alt" text from an <img> tag */
static BOOLEAN htmlimg(token)
	TOKEN	*token;
{
	int	i, j;

	/* look for an "alt=..." argument */
	for (i = 5; i < token->nchars && CHARncmp(&token->text[i - 5], toCHAR(" alt="), 5); i++)
	{
	}

	/* decide how to display this image */
	if (i >= token->nchars)
	{
		/* there is no "alt=..." string, so display the whole tag */
		i = 0;
		j = token->nchars;
	}
	else if (token->text[i] == '"')
	{
		/* the "alt=" argument has a quoted argument */
		i++;
		for (j = i; j < token->nchars && token->text[j] != '"'; j++)
		{
		}
	}
	else
	{
		/* the "alt=" argument is unquoted */
		for (j = i; j < token->nchars && token->text[j] != ' ' && token->text[j] != '>'; j++)
		{
		}
	}

	/* copy it into the manput() arguments, and then put it. */
	for (manlen = 0; i < j && manlen < QTY(mantext); i++, manlen++)
	{
		mantext[manlen] = token->text[i];
		manoffset[manlen] = token->offset[i];
		manfont[manlen] = 'b';
	}
	return manput();
}


/* Set the modeinfo's "graphic" flag if <pre graphic> */
static BOOLEAN htmlpre(token)
	TOKEN	*token;
{
	int	i;

	for (graphic = False, i = 0; i < token->nchars; i++)
	{
		if (token->text[i] == 'g')
		{
			graphic = True;
			break;
		}
	}
	return False;
}

/* List items are preceded by a less-indented number or bullet */
static BOOLEAN htmlli(token)
	TOKEN	*token;
{
	CHAR	buf[10];
	int	len;

	assert(col == 0);

	/* output a bullet or count */
	if (nest == 1 && listcnt > 0)
	{
		/* convert item# to characters */
		long2CHAR(buf, (long)listcnt++);
		CHARcat(buf, toCHAR(")"));
		len = CHARlen(buf);

		/* output whitespace for indentation */
		if (indent - len > 1)
		{
			(*drawchar)(&space, 1 + len - indent, 'n', -1);
			col += indent - len - 1;
		}

		/* output the item number */
		(*drawchar)(buf, len, 'n', -1);
		col += len;
	}
	else
	{
		/* output whitespace for indentation */
		if (indent > 2)
		{
			(*drawchar)(&space, 2 - indent, 'n', -1);
			col += indent - 2;
		}

		/* output a bullet */
		(*drawchar)(&bullet, 1, 'g', -1);
		col++;
	}

	/* Note: We would like to do an assert(mui->col == mui->indent - 1)
	 * here, but if the number/bullet doesn't fit within the indentation
	 * space then our indentation might be off.  So we won't.
	 */
	return False;
}

/* Form elements are shown as reverse-video areas */
static BOOLEAN htmlinput(token)
	TOKEN	*token;
{
	int	height;	/* 1 for input, 2 for textarea */
	int	width;	/* displayed width of item */
	int	vallen;	/* length of the value */
	int	validx;	/* index into token->text[] of initial value */
	BOOLEAN	button;	/* does this form item appear to be a button? */
	BOOLEAN	radio;	/* does this form item appear to be a radio button? */
	char	font;	/* font - 'E' for buttons, or 'N' for any other */
	int	mycol;
	int	i;

	/* parse the arguments */
	height = (token->text[1] == 't') ? 3 : 1;
	width = vallen = validx = 0;
	button = radio = False;
	font = 'u';
	for (i = 4; i < token->nchars; i++)
	{
		if (!CHARncmp(&token->text[i], toCHAR("value="), 6))
		{
			i += 6;
			if (token->text[i] == '"')
			{
				i++;
				validx = i;
				for (vallen = i; i < token->nchars && token->text[i] != '"'; i++)
				{
				}
			}
			else
			{
				validx = i;
				for (vallen = i; i < token->nchars && !isspace(token->text[i]); i++)
				{
				}
			}
			vallen = i - vallen;
		}
		else if (!CHARncmp(&token->text[i], toCHAR("size="), 5)
		      || !CHARncmp(&token->text[i], toCHAR("cols="), 5))
		{
			i += 5;
			width = atoi(tochar8(&token->text[i]));
		}
		else if (!CHARncmp(&token->text[i], toCHAR("type="), 5))
		{
			i += 5;
			if (token->text[i] == '"')
				i++;
			if (!CHARncmp(&token->text[i], toCHAR("checkbox"), 8)
			 || !CHARncmp(&token->text[i], toCHAR("CHECKBOX"), 8))
			{
				/* CHECKBOX button */
				button = radio = True;
				i += 8;
			}
			else if (!CHARncmp(&token->text[i], toCHAR("hidden"), 6)
			      || !CHARncmp(&token->text[i], toCHAR("HIDDEN"), 6))
			{
				/* HIDDEN field -- do nothing with it */
				return False;
			}
			else if (!CHARncmp(&token->text[i], toCHAR("radio"), 5)
			      || !CHARncmp(&token->text[i], toCHAR("RADIO"), 5))
			{
				/* RADIO button */
				button = radio = True;
				i += 5;
			}
			else if (token->text[i] != 't' && token->text[i] != 'T')
			{
				/* not TEXT, probably SUBMIT or RESET button */
				button = True;
				font = 'B';
			}
		}
		else if (!CHARncmp(&token->text[i], toCHAR("checked"), 7))
		{
			font = 'N';
			i += 7;
		}
	}

	/* Most buttons are always as wide as their value, but radio & checkbox
	 * buttons only need to show a single character.
	 */
	if (radio)
	{
		vallen = 1;
	}
	if (button)
	{
		width = vallen;
	}

	/* remember the column */
	mycol = col;
	if (mycol < indent)
		mycol = indent;
	else if (anyspc)
	{
		mycol++;
		anyspc = False;
	}

	/* will it fit on this line? */
	if (!first && mycol + width > textwidth)
	{
		/* no it won't */
		(*drawchar)(&newline, 1, 'n', -1);
		col = 0;
		return True;
	}

	/* output the image */
	for (i = 1; i <= height; i++)
	{
		if (col > mycol)
		{
			(*drawchar)(&newline, 1, 'n', -1);
			col = 0;
		}
		if (col < mycol)
		{
			(*drawchar)(&space, col - mycol , 'n', -1);
			col = mycol;
		}
		if (vallen > 0)
			(*drawchar)(&token->text[validx], vallen, font, token->offset[validx]);
		(*drawchar)(&space, vallen - width, font, token->offset[token->nchars - 1]);
		col += width;
	}
	anyspc = False;

	return False;
}

/* Look up an html markup token in a table */
static void htmlmarkup(token)
	TOKEN	*token;	/* the token to lookup */
{
	static MARKUP	tbl[] =
	{

		/* Tag		 Effects	Function	*/
		/*               TBILFFD                        */
		{ "html",	"Y-2-NY-"			},
		{ "/html",	"N-2-NY-"			},
		{ "head",	"Y-2-NY-"			},
		{ "/head",	"N-2-NY-"			},
		{ "title",	"Y-2-NY-"			},
		{ "/title",	"N-2-NY-"			},
		{ "body",	"N-2-NY-"			},
		{ "/body",	"N-2-NY-"			},
		{ "h1",		"Np0-BYS"			},
		{ "/h1",	"N12-NY-"			},
		{ "h2",		"Nc1-BYS"			},
		{ "/h2",	"N12-NY-"			},
		{ "h3",		"N12-BYS"			},
		{ "/h3",	"N02-NY-"			},
		{ "h4",		"N12-IY-"			},
		{ "/h4",	"N02-NY-"			},
		{ "h5",		"N12-IY-"			},
		{ "/h5",	"N02-NY-"			},
		{ "h6",		"N12-IY-"			},
		{ "/h6",	"N02-NY-"			},
		{ "p",		"N1--NYP"			},
		{ "hr",		"N02--Y-",	htmlhr		},
		{ "img",	"N------",	htmlimg		},
		{ "br",		"N0---Y-"			},
		{ "table",	"N02-NY-"			},
		{ "/table",	"N02-NY-"			},
		{ "tr",		"N02--Y-"			},
		{ "th",		"N-=-BY-"			},
		{ "td",		"N-=-NY-"			},
		{ "blockquote",	"N14-NYP"			},
		{ "/blockquote","N12-NY-"			},
		{ "pre",	"N0--FNP",	htmlpre		},
		{ "/pre",	"N0--NY-"			},
		{ "dir",	"N0>-FNP",	htmlpre		},
		{ "/dir",	"N0<-NY-"			},
		{ "xmp",	"N0>-FN-",	htmlpre		},
		{ "/xmp",	"N0<-NY-"			},
		{ "dl",		"N-2-NYS"			},
		{ "/dl",	"N02-NY-"			},
		{ "dt",		"N12-BYP"			},
		{ "dd",		"N03-NY-"			},
		{ "ol",		"N->#-YP"			},
		{ "/ol",	"N0<N-Y-"			},
		{ "ul",		"N->Y-YP"			},
		{ "/ul",	"N0<N-Y-"			},
		{ "menu",	"N->Y-Y-"			},
		{ "/menu",	"N-<N-Y-"			},
		{ "li",		"N0-----",	htmlli		},
		{ "input",	"N-----T",	htmlinput	},
		{ "textarea",	"N-----T",	htmlinput	},
		{ "a",		"N---u-T"			},
		{ "/a",		"N---=--"			},
		{ "cite",	"N---i--"			},
		{ "/cite",	"N---=--"			},
		{ "dfn",	"N---i--"			},
		{ "/dfn",	"N---=--"			},
		{ "em",		"N---i--"			},
		{ "/em",	"N---=--"			},
		{ "kbd",	"N---b--"			},
		{ "/kbd",	"N---=--"			},
		{ "strong",	"N---b--"			},
		{ "/strong",	"N---=--"			},
		{ "var",	"N---i--"			},
		{ "/var",	"N---=--"			},
		{ "address",	"N---i--"			},
		{ "/address",	"N---=--"			},
		{ "b",		"N---b--"			},
		{ "/b",		"N---=--"			},
		{ "i",		"N---i--"			},
		{ "/i",		"N---=--"			},
		{ "u",		"N---u--"			},
		{ "/u",		"N---=--"			},
		{ "tt",		"N---f--"			},
		{ "/tt",	"N---=--"			},
		{ (char *)0,	"N------"			}
	};
	MARKUP	*scan;	/* used for scanning the tbl[] array */
	int	len;	/* length of the markup */

	/* find the length of the markup's name */
	assert(token->nchars > 1 && token->text[0] == '<');
	for (len = 1;
	     len < token->nchars &&
		((len == 1 && token->text[len] == '/') || isalnum(token->text[len]));
	     len++)
	{
	}
	len--; /* since we started at 1 */

	/* look it up in the table */
	for (scan = tbl;
	     scan->name &&
		(strlen(scan->name) != (unsigned)len ||
		    strncmp(scan->name, (char *)token->text + 1, (size_t)len));
	     scan++)
	{
	}

	/* remember it */
	token->markup = scan;
}

/* Read the next token. */
static TOKEN *htmlget(refp)
	CHAR	**refp;	/* address of a (CHAR *) used for scanning */
{
	long	offset;
	BOOLEAN lower;

	/* if the CHAR pointer is NULL, then return NULL */
	if (!*refp)
		return NULL;

	/* Get first character of token */
	offset = markoffset(scanmark(refp));
	rettok.text[0] = **refp;
	rettok.offset[0] = offset++;
	rettok.nchars = 1;
	rettok.markup = NULL;
	scannext(refp);

	/* If '<' then token is a markup */
	if (rettok.text[0] == '<')
	{
		/* This is a markup.  Collect characters up to next '>' */
		for (lower = True; *refp && **refp != '>'; offset++, scannext(refp))
		{
			/* if token text is full, then skip this char */
			if (rettok.nchars >= QTY(rettok.text) - 2)
				continue;

			/* Store the character.  This is a little complex
			 * because we want to convert uppercase tags and
			 * parameter names to lowercase, but leave parameter
			 * values unchanged.  Also, any whitespace character
			 * should be displayed as a space.
			 */
			if (**refp == '=')
				lower = False;
			else if (isspace(**refp))
				lower = True;

			if (**refp <= ' ')
				rettok.text[rettok.nchars] = ' ';
			else if (isupper(**refp) && lower)
				rettok.text[rettok.nchars] = tolower(**refp);
			else
				rettok.text[rettok.nchars] = **refp;
			rettok.offset[rettok.nchars++] = offset;
		}

		/* store the terminating '>' */
		rettok.text[rettok.nchars] = '>';
		rettok.offset[rettok.nchars] = offset;
		rettok.nchars++;
		rettok.text[rettok.nchars] = '\0';
		if (*refp)
			scannext(refp);

		/* lookup the markup */
		htmlmarkup(&rettok);

		/* when computing line breaks, assume this markup is hidden */
		rettok.width = 0;
	}
	else if (rettok.text[0] <= ' ')
	{
		/* This is a whitespace token.  Each whitespace token contains
		 * a SINGLE whitespace character.  Control characters other
		 * than '\t' and '\n' are treated as spaces.
		 */
		if (rettok.text[0] != '\t' && rettok.text[0] != '\n')
		{
			rettok.text[0] = ' ';
		}

		/* when computing line breaks, assume this whitespace shows */
		rettok.width = 1;
	}
	else
	{
		/* This is a word.  Collect characters up to next whitespace */
		for (;
		     *refp 
			&& rettok.nchars < QTY(rettok.text) - 1
			&& !isspace(**refp)
			&& **refp != '<';
		     offset++, scannext(refp))
		{
			rettok.text[rettok.nchars] = **refp;
			rettok.offset[rettok.nchars] = offset;
			rettok.nchars++;
		}

		/* For now, when computing line breaks, assume each character
		 * of this word is normally visible.  When character escapes
		 * are processed, this may change.
		 */
		rettok.width = rettok.nchars;
	}

	/* Mark the end of the text, and return the token */
	rettok.text[rettok.nchars] = '\0';
	return &rettok;
}


/* start the mode, and allocate dminfo */
static DMINFO *htmlinit(win)
	WINDOW	win;
{
	long	cursoffset;	/* offset of cursor */
	TOKEN	*token;
	MARKBUF	top;
	CHAR	*p;

	/* inherit some functions from dmnormal */
	dmhtml.wordmove = dmnormal.wordmove;

	/* allocate the info struct */
	mui = (MUINFO *)safealloc(1, sizeof(MUINFO));
	mui->get = htmlget;
	mui->escape = htmlescape;
	mui->line = safealloc(GRANULARITY, sizeof(LINEINFO));
	mui->nlines = 1; /* every buffer has at least one line */

	/* temporarily move the cursor someplace harmless */
	cursoffset = markoffset(win->cursor);
	marksetoffset(win->cursor, o_bufchars(markbuffer(win->cursor)));

	/* format the buffer */
	win->mi = (DMINFO *)mui;
	(void)start(win, marktmp(top, markbuffer(win->cursor), 0), NULL);
	p = scanalloc(&p, &top);
	for (token = (*mui->get)(&p);
	     token;
	     token = (*mui->get)(&p))
	{
		/* if cursor is at position 0, and this is a text token,
		 * then move the cursor here.  This is done because otherwise
		 * the cursor would always start on an ugly formatting code.
		 */
		if (cursoffset == 0L 
		 && !title
		 && !isspace(token->text[0])
		 && !token->markup)
		{
			cursoffset = token->offset[0];
		}

		/* output the token.  If it forces a new line, remember where
		 * that new line started.
		 */
		if (put(token))
		{
			assert(first == True && col == 0);
			storestate(token->offset[0], NULL);
			(void)put(token);
		}
	}
	scanfree(&p);

	/* locate the title */
	findtitle(markbuffer(win->cursor));

	/* set the cursor back to its original position, or its adjusted one */
	marksetoffset(win->cursor, cursoffset);

	/* Done! */
	return (DMINFO *)mui;
}


/* Return a dynamically-allocated string containing the name of the tag at
 * the cursor, or NULL if the cursor isn't on a tag.
 */
static CHAR *htmltagatcursor(win, cursor)
	WINDOW	win;
	MARK	cursor;
{
	CHAR	*ret;	/* the return value */
	CHAR	*p;	/* used for scanning */
	TOKEN	*token;	/* a token from the buffer */
	TOKEN	anchor;	/* copy of last <a...> or </a> token */
	long	endoffset;/* where to stop scanning */
	int	i;	/* index into mui->line */
	MARKBUF	tmp;

	/* We need to find the last <a ...> or </a> tag which occurs before
	 * before the cursor.  Since we can't read tokens backward, we must
	 * start scanning at the beginning of the line and read forward until
	 * we pass the cursor, remembering the last <a ...> or </a> as we go.
	 * If we haven't found one by the time we pass the cursor, then we
	 * need to restart scanning at the beginning of the preceding line.
	 */

	/* find the start of the cursor's line */
	mui = (MUINFO *)win->mi;
	if (mui->nlines == 0)
		return NULL;
	endoffset = markoffset(cursor) + 1;
	for (i = 1; i < mui->nlines && mui->line[i].offset < endoffset; i++)
	{
	}
	i--; /* the above loop went one line too far */

	/* read forward from the start of the line, watching for <a ...> and
	 * </a> tags.  Stop when we know we've found the last one before the
	 * cursor.
	 */
	anchor.text[0] = 0;
	do
	{
		scanalloc(&p, marktmp(tmp, markbuffer(cursor), mui->line[i].offset));
		while ((token = htmlget(&p)) != NULL && token->offset[0] < endoffset)
		{
			if (token->markup && (!CHARncmp(token->text, toCHAR("<a href="), 8) || !CHARcmp(token->text, toCHAR("</a>"))))
			{
				anchor = *token;
			}
		}
		scanfree(&p);
		endoffset = mui->line[i].offset;
	} while (anchor.text[0] == 0 && --i >= 0);

	/* If we found an <a ...> tag with an href parameter, then generate
	 * a dynamically-allocated copy of the URL.
	 */
	ret = NULL;
	if (!CHARncmp(anchor.text, toCHAR("<a href="), 8))
	{
		p = &anchor.text[8];
		if (*p == '"')
		{
			while (*++p != '"')
				buildCHAR(&ret, *p);
		}
		else
		{
			do
			{
				buildCHAR(&ret, *p);
			} while (*++p != ' ' && *p != '>');
		}
	}

	/* return the URL or a NULL pointer */
	return ret;
}


static MARK htmltagload(tagname, from)
	CHAR	*tagname;
	MARK	from;		/* where the cursor is while we're loading */
{
 static	MARKBUF	retmark;	/* the return value */
	char	*filename;	/* name of file containing tag */
	CHAR	*anchorname;	/* name of tag's anchor */
	CHAR	*p;
	TOKEN	*token;
	int	i, j;

	/* separate the tagname into filename and anchorname */
	filename = safealloc((int)CHARlen(tagname) + 1, sizeof(char));
	for (anchorname = tagname, i = 0;
	     *anchorname && *anchorname != '#';
	     anchorname++, i++)
	{
		filename[i] = *anchorname;
		if (!CHARncmp(anchorname, toCHAR("://"), 3))
		{
			msg(MSG_ERROR, "non-local URLs aren't supported yet");
			safefree(filename);
			return NULL;
		}
	}
	filename[i] = 0;

	/* Skip the '#' at the start of the anchor name, if any */
	if (*anchorname == '#')
		anchorname++;

	/* Load the file into a buffer.  If the filename isn't a full pathname
	 * then assume that the file is in the same directory as the current
	 * file.  In fact, check to see if it is the same buffer first!
	 */
	if (!filename[0]
	    || (from
		&& o_filename(markbuffer(from))
		&& !CHARcmp(o_filename(markbuffer(from)), toCHAR(filename))))
	{
		marktmp(retmark, markbuffer(from), 0);
	}
	else if (from && isalpha(*filename) && o_filename(markbuffer(from)))
	{
		marktmp(retmark,
			bufload(NULL,
				dirpath(dirdir(tochar8(o_filename(markbuffer(from)))),
					filename), False),
			0);
	}
	else
	{
		marktmp(retmark, bufload(NULL, filename, False), 0);
	}
	safefree(filename);
	if (!markbuffer(&retmark) || o_bufchars(markbuffer(&retmark)) == 0)
	{
		return NULL;
	}

	/* scan forward from top for an anchor token with this name */
	if (*anchorname)
	{
		i = (int)CHARlen(anchorname);
		scanalloc(&p, &retmark);
#if 0
		for (token = htmlget(&p);
		     token
			&& (CHARncmp(token->text, toCHAR("<a name="), 8)
				|| (token->text[8] == '"'
					? CHARncmp(&token->text[9], anchorname, (size_t)i) || token->text[9 + i] != '"'
					: CHARncmp(&token->text[8], anchorname, (size_t)i) || token->text[8 + i] != '>'));
		     token = htmlget(&p))
		{
		}
#else
		for (token = htmlget(&p); token; token = htmlget(&p))
		{
			/* ignore if not markup or not a possible target */
			if (!token->markup || token->markup->DEST == '-')
				continue;

			/* scan for "id=" or "name=" */
			for (j = 2; j < token->nchars; j++)
			{
				if (!CHARncmp(&token->text[j], toCHAR(" name="), 6))
				{
					j += 6;
					break;
				}
				if (!CHARncmp(&token->text[j], toCHAR(" id="), 4))
				{
					j += 4;
					break;
				}
			}
			if (j >= token->nchars)
				continue;

			/* compare to sought name.  Beware of quotes */
			if (token->text[j] == '"'
			    ? !CHARncmp(&token->text[++j], anchorname, (size_t)i)
				&& token->text[j + i] == '"'
			    : !CHARncmp(&token->text[j], anchorname, (size_t)i)
				&& (token->text[j + i] == '>' || token->text[j + i] == ' '))
			{
				break;
			}
		}
#endif

		/* skip to the following non-whitespace text token */
		while (token && (token->markup || isspace(token->text[0])))
		{
			token = htmlget(&p);
		}

		/* did we find the tag? */
		if (token)
		{
			marksetoffset(&retmark, token->offset[0]);
		}
		else
		{
			msg(MSG_WARNING, "[S]anchor $1 not found", anchorname);
		}
		scanfree(&p);
	}

	return &retmark;
}


static MARK htmltagnext(cursor)
	MARK	cursor;
{
	CHAR	*p;	/* used for scanning */
	TOKEN	*token;	/* a token from the text */
 static MARKBUF	ret;	/* return value */

	/* read forward to next <a ...> token with an href parameter */
	for (scanalloc(&p, cursor);
	     (token = htmlget(&p)) != NULL
		&& (!token->markup
			|| token->markup->DEST != 'T'
			|| !CHARncmp(token->text, "<a name=", 8)
			|| token->offset[0] == markoffset(cursor));
	     )
	{
	}

	/* if we found an <a ...> token, then skip ahead to the next text token
	 * or the </a> token.
	 */
	if (p && !CHARncmp(token->text, toCHAR("<a "), 3))
	{
		while ((token = htmlget(&p)) != NULL
			&& isspace(token->text[0]))
		{
		}
	}
	scanfree(&p);

	/* if we couldn't find anyplace, return NULL */
	if (!token)
	{
		return NULL;
	}

	/* else construct a mark for the start of the last token */
	return marktmp(ret, markbuffer(cursor), token->offset[0]);
}

/*----------------------------------------------------------------------------*/
/* "man" mode functions                                                       */

/* This function interprets codes in a text token, converting them to regular
 * text.  Note that none of the codes can change the font.
 */
static void manescape(token)
	TOKEN	*token;
{
	int	i, j;
	TOKEN	temp;

	/* if the cursor is in this somewhere, then don't change it but we
	 * still need to compute the width so change a temporary copy.
	 */
	if (cursoff >= token->offset[0] && cursoff <= token->offset[token->nchars - 1])
	{
		temp = *token;
		temp.offset[0] = cursoff + 1;
		manescape(&temp);
		token->width = temp.width;
		return;
	}

	/* for each character... */
	for (i = j = 0; i < token->nchars; i++)
	{
		/* most characters are copied as-is */
		if (token->text[i] != '\\')
		{
			token->text[j] = token->text[i];
			token->offset[j++] = token->offset[i];
		}
		else if (i + 1 < token->nchars)
		{
			switch (token->text[++i])
			{
			  case '|':
			  case '&':
				/* delete the \| or \& */
				break;

			  case 's':
				/* delete the \s and the number that follows */
				if (token->text[i + 1] == '+' || token->text[i + 1] == '-')
					i++;
				do
				{
					i++;
				} while (isdigit(token->text[i]));
				i--;
				break;

			  case '*':
			  case 'n':
				/* keep the \* or \n as-is */
				token->text[j] = '\\';
				token->offset[j++] = token->offset[i - 1];
				token->text[j] = token->text[i];
				token->offset[j++] = token->offset[i];
				break;

			  case 'e':
				/* convert \e to backslash */
				token->text[j] = '\\';
				token->offset[j++] = token->offset[i];
				break;

			  default:
				/* convert \X to just plain X */
				token->text[j] = token->text[i];
				token->offset[j++] = token->offset[i];
			}
		}
	}

	/* mark the end of the string */
	token ->width = token->nchars = j;
	token->text[j] = '\0';
}

/* This function is used to collect the arguments to a .XX macro together
 * as a string with a parallel array storing the font.
 */
static int manarg(token, start, font, spc)
	TOKEN	*token;	/* the token to parse */
	int	start;	/* where to begin scanning */
	_char_	font;	/* initial font of arg */
	BOOLEAN	spc;	/* insert a space before the word? */
{
	BOOLEAN	quote;	/* is this arg enclosed in quotes? */

	/* skip leading whitespace */
	while (isspace(token->text[start]))
	{
		start++;
	}

	/* is this arg quoted? */
	quote = (BOOLEAN)(token->text[start] == '"');
	if (quote)
		start++;

	/* are we supposed to insert a space? */
	if (spc && start < token->nchars - 2)
	{
		assert(start > 0);
		mantext[manlen] = ' ';
		manoffset[manlen] = token->offset[start - 1];
		manfont[manlen] = 'b';
		manlen++;
	}

	/* collect text to end of arg, or until mantext[] is full */
	while (manlen < QTY(mantext) - 1
		&& start < token->nchars - 2
		&& (quote ? token->text[start] != '"' : !isspace(token->text[start])))
	{
		/* handle \fX */
		if (token->text[start] == '\\' && token->text[start + 1] == 'f')
		{
			switch (token->text[start + 2])
			{
			  case '1':
			  case 'B':	font = 'b';	break;
			  case '2':
			  case 'I':	font = 'i';	break;
			  default:	font = 'n';	break;
			}
			start += 3;
		}
		else if (token->text[start] == '\\' && start < token->nchars - 2)
		{
			switch (token->text[++start])
			{
			  case '|':
			  case '&':
				break;

			  case 'e':
				mantext[manlen] = '\\';
				manoffset[manlen] = token->offset[start];
				manfont[manlen] = font;
				manlen++;
				break;

			  case 's':
				/* delete the \s and the number that follows */
				if (token->text[start] == '+' || token->text[start] == '-')
					start++;
				do
				{
					start++;
				} while (isdigit(token->text[start]));
				start--;
				break;

			  default:
				mantext[manlen] = token->text[start];
				manoffset[manlen] = token->offset[start];
				manfont[manlen] = font;
				manlen++;
			}
			start++;
		}
		else if (token->text[start] <= ' ')
		{
			mantext[manlen] = ' ';
			manoffset[manlen] = token->offset[start];
			manfont[manlen] = font;
			manlen++;
			start++;
		}
		else
		{
			mantext[manlen] = token->text[start];
			manoffset[manlen] = token->offset[start];
			manfont[manlen] = font;
			manlen++;
			start++;
		}
	}

	/* skip the closing quote (if quoted) */
	if (quote && token->text[start] == '"')
		start++;

	/* return the index of the end of the arg */
	return start;
}


/* This function either outputs mantext[] and returns False, or (if mantext[]
 * is too wide to fit on this line) it outputs a newline and returns True.
 */
static BOOLEAN manput()
{
	int	i, start;

	/* if it won't fit, then output a newline instead */
	if (!first && col + manlen > textwidth - listind)
	{
		/* output a newline */
		(*drawchar)(&newline, 1, 'n', anyspc ? spcoffset : -1);
		col = 0;
		first = True;
		return True;
	}

	/* It will fit.  If we need to adjust our indent, do it now */
	if (col < indent)
	{
		(*drawchar)(&space, col - indent, 'n', anyspc ? spcoffset : -1);
		col = indent;
		anyspc = False;
	}

	/* Output a space between tokens, usually. */
	if (anyspc)
	{
		(*drawchar)(&space, 1, 'n', spcoffset);
		col++;
		anyspc = False;
	}

	/* Output mantext[] in as few chunks as possible */
	for (start = 0; start < manlen; start = i)
	{
		for (i = start + 1;
		     i < manlen
			&& manfont[i] == manfont[start]
			&& manoffset[i] == manoffset[i - 1] + 1;
		     i++)
		{
		}
		(*drawchar)(&mantext[start], (long)(i - start), manfont[start], manoffset[start]);
	}
	col += manlen;

	return False;
}

/* This function implements the .TH macro, which declares the page's title */
static BOOLEAN manTH(token)
	TOKEN	*token;
{
	int	i;

	/* combine the first & second args as the document name */
	manlen = 0;
	i = manarg(token, 3, 'n', False);
	mantext[manlen++] = '(';
	(void)manarg(token, i, 'n', False);
	mantext[manlen++] = ')';
	mantext[manlen] = '\0';

	/* If the title is different, then store it now */
	if (!mui->title || CHARcmp(mui->title, mantext))
	{
		if (mui->title)
			safefree(mui->title);
		mui->title = CHARdup(mantext);
	}

	return False;
}

/* This function implements the .SH and .SS macros */
static BOOLEAN manSH(token)
	TOKEN	*token;
{
	long	nloff;	/* offset of newline */
	int	i;

	/* Get the arguments, as the section title */
	manlen = 0;
	for (i = 4; i < token->nchars; i++)
	{
		i = manarg(token, i, 'b', (BOOLEAN)(i != 4));
	}

	/* If the cursor is located in this token, or we're doing some sort
	 * of move operation, then pretend that the offsets of all the
	 * generated characters are -1.
	 */
	nloff = token->offset[token->nchars - 1];
	if ((o_showmarkups && token->offset[0] <= cursoff && cursoff <= token->offset[token->nchars - 1])
		|| list || drawchar == countchar)
	{
		for (i = 0; i < manlen; i++)
			manoffset[i] = -1;
		nloff = -1;
	}

	/* output the title, followed by a newline */
	anyspc = False;
	manput();
	(*drawchar)(&newline, 1, 'n', nloff);
	col = 0;
	first = True;
	anyspc = False;
	reduce = True;

	/* force the indentation to be the default */
	indent = listind * 2;

	/* Return False even though we did output a newline.  The newline
	 * (and the text before it) is considered to be part of the same
	 * "line" as far as movement is concerned.
	 */
	return False;
}

/* This function implements the font-changing macros such as .BI, .RB, and
 * so on.  The resulting text is treated as a single word; if it can't fit
 * on the current line, then this function merely outputs a newline.
 */
static BOOLEAN manBI(token)
	TOKEN	*token;
{
	char	font1, font2;
	int	start;

	/* choose the fonts & start, based on macro name */
	switch (token->text[1])
	{
	  case 'B':	font1 = 'b';	break;
	  case 'I':	font1 = 'i';	break;
	  default:	font1 = 'n';
	}
	switch (token->text[2])
	{
	  case 'B':	font2 = 'b';	start = 4;	break;
	  case 'I':	font2 = 'i';	start = 4;	break;
	  case 'S':
	  case 'R':	font2 = 'n';	start = 4;	break;
	  default:	font2 = font1;	start = 3;
	}

	/* collect the args, with their fonts */
	manlen = 0;
	start = manarg(token, start, font1, False);
	start = manarg(token, start, font2, (BOOLEAN)(font1 == font2));
	start = manarg(token, start, font1, (BOOLEAN)(font1 == font2));
	start = manarg(token, start, font2, (BOOLEAN)(font1 == font2));
	start = manarg(token, start, font1, (BOOLEAN)(font1 == font2));
	start = manarg(token, start, font2, (BOOLEAN)(font1 == font2));

	/* If the cursor is on this token, then just tweak its width and
	 * return False so the remaining token-putting code will handle
	 * line-wrap.  Else perform the token's output.
	 */
	if (list || (o_showmarkups && token->offset[0] <= cursoff && cursoff <= token->offset[token->nchars - 1]))
	{
		token->width = manlen + 1; /* "+ 1" to allow for whitespace */
		return False;
	}
	else if (manput())
	{
		return True;
	}

	/* assume there should be some whitespace after this */
	anyspc = True;
	spcoffset = token->offset[token->nchars - 1];
	return False;
}

/* This function implements the .IP macro.  It outputs its first argument,
 * and then increases the indentation.  If necessary, it will then output
 * a newline character, but this newline character isn't considered to be
 * the end of "line" as far as moving the cursor is concerned; it is just
 * a funny type of wrapping.
 */
static BOOLEAN manIP(token)
	TOKEN	*token;
{
	int	i, start;
	char	font, font2;

	/* if this token is going to be displayed anyway, then don't bother
	 * performing its output.
	 */
	if (list || (o_showmarkups && token->offset[0] <= cursoff && cursoff <= token->offset[token->nchars - 1]))
		return False;

	/* get the first arg, as the paragraph tag */
	manlen = 0;
	start = manarg(token, 4, 'n', False);

	/* For .TP (but not .IP) get other args as part of the tag, too */
	if (token->text[1] == 'T')
	{
		/* if first word was .B or .I, then change font */
		if (manlen == 2 && mantext[0] == '.' && (mantext[1] == 'B' || mantext[1] == 'I'))
		{
			font = font2 = tolower(mantext[1]);
			manlen = 0;
		}
		else if (manlen == 3 && mantext[0] == '.')
		{
			font = tolower(mantext[1]);
			if (font != 'b' && font != 'i')
				font = 'n';
			font2 = tolower(mantext[2]);
			if (font2 != 'b' && font2 != 'i')
				font2 = 'n';
			manlen = 0;
		}
		else
		{
			font = font2 = 'n';
		}

		/* Copy the remaining args, in the desired font[s].  If the
		 * fonts are different, then delete whitespace from between
		 * arguments.
		 */
		for (i = 0; i < 5; i++)
		{
			start = manarg(token, start, (i & 1) ? font2 : font, (BOOLEAN)(font == font2 && manlen > 0));
		}
	}

	/* output the paragraph tag */
	(void)manput();

	/* increase the indentation by one full tab for .IP, two for .TP */
	if (token->text[1] == 'T')
		indent += 4 * listind;
	else
		indent += 2 * listind;

	/* If necessary, output a newline as part of the text */
	if (col >= indent)
	{
		(*drawchar)(&newline, 1, 'n', token->offset[token->nchars - 1]);
		col = 0;
	}

	/* return False, even if we output a newline */
	return False;
}

/* Look up a man markup token in a table */
static void manmarkup(token)
	TOKEN	*token;	/* the token to lookup */
{
	static MARKUP	tbl[] =
	{

		/* Tag		 Effects	Function	*/
		/*               TBILFFD                        */
		{ "\\\"",	"Y------"			},
		{ "TH",		"Y----Y-",	manTH		},
		{ "SH",		"Nc0-NYS",	manSH		},
		{ "SS",		"N11-NYS",	manSH		},
		{ "B",		"N------",	manBI		},
		{ "I",		"N------",	manBI		},
		{ "SM",		"N------",	manBI		},
		{ "BI",		"N------",	manBI		},
		{ "IB",		"N------",	manBI		},
		{ "RI",		"N------",	manBI		},
		{ "IR",		"N------",	manBI		},
		{ "BR",		"N------",	manBI		},
		{ "RB",		"N------",	manBI		},
		{ "BS",		"N------",	manBI		},
		{ "SB",		"N------",	manBI		},
		{ "IP",		"N12-NYP",	manIP		},
		{ "TP",		"N12-NYP",	manIP		},
		{ "PP",		"N12-NYP"			},
		{ "P",		"N12-NYP"			},
		{ "LP",		"N12-NYP"			},
		{ "RS",		"N->----"			},
		{ "RE",		"N-<----"			},
		{ "br",		"N0-----"			},
		{ "sp",		"N1-----"			},
		{ "nf",		"N0---N-"			},
		{ "fi",		"N0---Y-"			},
		{ "DS",		"N1---N-"			},
		{ "DE",		"N0---Y-"			},
		{ (char *)0,	"N------"			}
	};
	MARKUP	*scan;	/* used for scanning the tbl[] array */

	/* look it up in the table */
	for (scan = tbl;
	     scan->name &&
		(strncmp(scan->name, (char *)token->text + 1, strlen(scan->name))
		|| isalnum(token->text[strlen(scan->name) + 1]));
	     scan++)
	{
	}

	/* remember it */
	token->markup = scan;
}


/* Get the next token from a "man" document, or return NULL at end. */
static TOKEN *manget(refp)
	CHAR	**refp;
{
	MARK	back;	/* address of a backslash */
	long	offset;	/* offset of character that *refp points to */
 static	MARKUP	fontchg;/* describes font change markups */

	/* Initialize "back" just to silence a compiler warning */
	back = NULL;

	/* if the CHAR pointer is NULL, then return NULL */
	if (!*refp)
		return NULL;

	/* Get first character of token */
	offset = markoffset(scanmark(refp));
	rettok.text[0] = **refp;
	rettok.offset[0] = offset++;
	rettok.nchars = 1;
	rettok.width = 0;
	rettok.markup = NULL;
	scannext(refp);

	/* If '.' at the start of a line, then token is a markup */
	if ((rettok.text[0] == '.' || rettok.text[0] == '\'') && !midline)
	{
		/* This is a markup.  Collect characters up to next '\n' */
		for (;
		     *refp && rettok.nchars < QTY(rettok.text) - 3 && **refp != '\n';
		     offset++, scannext(refp))
		{
			rettok.text[rettok.nchars] = **refp;
			rettok.offset[rettok.nchars++] = offset;
		}

		/* If this is ".TP" then read the following line as part of
		 * this token.
		 */
		if (!CHARncmp(rettok.text, toCHAR(".TP"), 3)
		 && *refp
		 && **refp == '\n'
		 && scannext(refp))
		{
			/* the newline after ".TP" becomes a space */
			rettok.nchars = 3;
			rettok.text[rettok.nchars] = ' ';
			rettok.offset[rettok.nchars] = offset;
			rettok.nchars++;
			offset++;

			for (;
			     *refp && rettok.nchars < QTY(rettok.text) - 3 && **refp != '\n';
			     offset++, scannext(refp))
			{
				rettok.text[rettok.nchars] = **refp;
				rettok.offset[rettok.nchars++] = offset;
			}
		}

		/* store the terminating '\n' as "^J" */
		rettok.text[rettok.nchars] = '^';
		rettok.offset[rettok.nchars] = offset;
		rettok.nchars++;
		rettok.text[rettok.nchars] = 'J';
		rettok.offset[rettok.nchars] = offset;
		rettok.nchars++;
		rettok.text[rettok.nchars] = '\0';
		if (*refp)
			scannext(refp);

		/* lookup the markup */
		manmarkup(&rettok);

		/* remember that we stopped after a newline */
		midline = False;
	}
	else if (*refp && rettok.text[0] == '\\' && **refp == 'f')
	{
		/* This is a font-change macro.  Get the final character */
		rettok.text[1] = 'f';
		rettok.offset[1] = offset++;
		scannext(refp);
		rettok.text[2] = (*refp ? **refp : 'R');
		rettok.offset[2] = offset++;
		rettok.nchars = 3;
		if (*refp)
			scannext(refp);

		/* construct a simple MARKUP struct */
		fontchg.name = tochar8(rettok.text);
		strcpy(fontchg.attr, "----n--");
		switch (rettok.text[2])
		{
		  case '1':
		  case 'B':	fontchg.FONT = 'b';	break;
		  case '2':
		  case 'I':	fontchg.FONT = 'i';	break;
		  case 'P':	fontchg.FONT = '=';	break;
		  default:	fontchg.FONT = 'n';	break;
		}
		rettok.markup = &fontchg;

		/* no newline at the end of this token! */
		midline = True;
	}
	else if (rettok.text[0] <= ' ')
	{
		/* This is a whitespace token.  Each whitespace token contains
		 * a SINGLE whitespace character.  Control characters other
		 * than '\t' and '\n' are treated as spaces.
		 */
		if (rettok.text[0] != '\t' && rettok.text[0] != '\n')
			rettok.text[0] = ' ';

		/* remember if this is a newline or not */
		midline = (BOOLEAN)(rettok.text[0] != '\n');

		/* assume this whitespace will show, for computing line break */
		rettok.width = 1;
	}
	else
	{
		/* This is a word.  Collect chars up to next whitespace or
		 * "\fX" string.
		 */
		for (;
		     *refp 
			&& rettok.nchars < QTY(rettok.text) - 1
			&& !isspace(**refp)
			&& (rettok.nchars < 2
				|| rettok.text[rettok.nchars - 2] != '\\'
				|| rettok.text[rettok.nchars - 1] != 'f');
		     offset++, scannext(refp))
		{
			if (**refp == '\\')
				back = scanmark(refp);
			rettok.text[rettok.nchars] = **refp;
			rettok.offset[rettok.nchars] = offset;
			rettok.nchars++;
		}

		/* if this ended with a \fX then we need to adjust *refp */
		if (rettok.nchars >= 2
			&& rettok.text[rettok.nchars - 2] == '\\'
			&& rettok.text[rettok.nchars - 1] == 'f')
		{
			scanseek(refp, back);
			rettok.nchars -= 2;
		}

		/* this didn't end with a newline */
		midline = True;

		/* For now, assume all characters of this work will be visible.
		 * When escapes are processed, this may change.
		 */
		rettok.width = rettok.nchars;
	}

	/* Mark the end of the text, and return the token */
	rettok.text[rettok.nchars] = '\0';
	return &rettok;
}

static DMINFO *maninit(win)
	WINDOW	win;
{
	TOKEN	*token;
	MARKBUF	top;
	CHAR	*p;
	long	cursoffset;

	/* inherit some functions from dmnormal */
	dmman.wordmove = dmnormal.wordmove;
	dmman.tagatcursor = dmnormal.tagatcursor;
	dmman.tagload = dmnormal.tagload;
	dmman.tagnext = dmnormal.tagnext;

	/* allocate the info struct */
	mui = (MUINFO *)safealloc(1, sizeof(MUINFO));
	mui->get = manget;
	mui->escape = manescape;
	mui->line = safealloc(GRANULARITY, sizeof(LINEINFO));
	mui->nlines = 1; /* every buffer has at least one line */

	/* move the cursor someplace harmless, so it won't affect formatting */
	cursoffset = markoffset(win->cursor);
	marksetoffset(win->cursor, o_bufchars(markbuffer(win->cursor)));

	/* format the buffer */
	win->mi = (DMINFO *)mui;
	(void)start(win, marktmp(top, markbuffer(win->cursor), 0), NULL);
	p = scanalloc(&p, &top);
	for (token = (*mui->get)(&p);
	     token;
	     token = (*mui->get)(&p))
	{
		/* if cursor is at position 0, and this is a text token,
		 * then move the cursor here.  This is done because otherwise
		 * the cursor would always start on an ugly formatting code.
		 */
		if (cursoffset == 0L 
		 && !title
		 && !isspace(token->text[0])
		 && !token->markup)
		{
			cursoffset = token->offset[0];
		}

		/* output the token.  If it forces a new line, remember where
		 * that new line started.
		 */
		if (put(token))
		{
			assert(first == True && col == 0);
			storestate(token->offset[0], NULL);
			(void)put(token);
		}
	}
	scanfree(&p);

	/* restore the cursor position */
	marksetoffset(win->cursor, cursoffset);

	/* Done! */
	return (DMINFO *)mui;
}

/*----------------------------------------------------------------------------*/
/* Generic markup functions                                                   */

/* This function performs a single token.  If the token is markup, then the
 * side effects are performed and the function (if any) is called.  If text,
 * or if the cursor is in the token, then the text of the token is output.
 * It is assumed that drawchar and cursoff are set before this function is
 * called.  Returns False if the token fits on this line.  If it didn't fit,
 * or was a markup that caused a line break, then it returns True in which
 * case put() should be called again for the same token to start generating
 * the next line.
 */
static BOOLEAN put(token)
	TOKEN	*token;
{
	char	tmpfont;
	CHAR	tmpch, lch, rch;
	int	i, origcol;
	BOOLEAN hascursor;	/* indicates whether this token contains cursor */

	/* determine whether the cursor is in this token.  If the "list"
	 * option is set, then pretend that all tokens contain the cursor.
	 */
	hascursor = (BOOLEAN)(list || (o_showmarkups && token->offset[0] <= cursoff && cursoff <= token->offset[token->nchars - 1]));

	/* is it a markup? */
	if (token->markup)
	{
		/* if this token will cause a line break, and no line break
		 * was already pending anyway, then do the line break and
		 * nothing else.  We'll do the rest of this markup's job
		 * at the start of the next line.
		 */
		if (token->markup->BREAKLN != '-' && !first)
		{
			(*drawchar)(&newline, 1, 'n', anyspc ? spcoffset : -1);
			reduce = (BOOLEAN)(col == 0);
			col = 0;
			first = True;
			return True;
		}

		/* do all the standard effects */
		switch (token->markup->TITLE)
		{
		  case 'Y': title = True;	break;
		  case 'N': title = False;	break;
		}
		switch (token->markup->BREAKLN)
		{
		  case '0':
			reduce = True;
			break;

		  case '1':
			if (!reduce)
				(*drawchar)(&newline, 1, 'n', anyspc ? spcoffset : -1);
			reduce = True;
			break;

		  case '2':
			(*drawchar)(&newline, reduce ? 1 : 2, 'n', anyspc ? spcoffset : -1);
			reduce = True;
			break;

		  case 'c':
			(*drawchar)(&vtab, 1, 'n', -1);
			reduce = True;
			break;

		  case 'p':
			(*drawchar)(&formfeed, 1, 'n', -1);
			reduce = True;
			break;
		}
		switch (token->markup->INDENT)
		{
		  case '<':
			indent = (indent < listind ? 0 : indent - listind);
			break;

		  case '>':
			indent += listind;
			break;

		  case '=':
			if (col < tabstop)
				indent = tabstop;
			else
				indent = col + 2 * tabstop - ((col + tabstop) % (2 * tabstop));
			break;

		  case '0':
		  case '1':
		  case '2':
		  case '3':
		  case '4':
			indent = (token->markup->INDENT - '0') * listind;
			break;
		}
		switch (token->markup->LIST)
		{
		  case 'N':
			if (nest > 0)
				nest--;
			break;

		  case 'Y':
			nest++;
			if (nest == 1)
				listcnt = 0;
			break;

		  case '#':
			nest++;
			if (nest == 1)
				listcnt = 1;
			break;
		}
		switch (token->markup->FONT)
		{
		  case '=': curfont = deffont;				break;
		  case 'n':
		  case 'b':
		  case 'u':
		  case 'f':
		  case 'e':
		  case 'i': curfont = token->markup->FONT;		break;
		  case 'N':
		  case 'B':
		  case 'U':
		  case 'F':
		  case 'E':
		  case 'I': curfont = deffont = tolower(token->markup->FONT); break;
		}
		switch (token->markup->FILL)
		{
		  case 'Y': graphic = prefmt = False;	break;
		  case 'N': prefmt = True;		break;
		}

		/* If there is a function, call it too.  If the function
		 * returns True, then act as though the markup caused a
		 * newline.
		 */
		if (token->markup->fn)
		{
			if ((*token->markup->fn)(token))
				return True;
		}

		/* if the cursor isn't on this token, don't show it */
		if (!hascursor)
		{
			first = False;
			return False;
		}
	}
	else
	{
		reduce = False;
	}

	/* no token causes visible output when in "title" mode, unless the
	 * cursor happens to be located on it.
	 */
	if (title && !hascursor)
	{
		first = False;
		return False;
	}

	/* Newlines are handled differently that other whitespace, when in
	 * prefmt mode.  This is because newlines mark the end of one line,
	 * so they become the first token of the next line... but when we
	 * get to the next line we don't want to process the newline again,
	 * or we'd get into an endless loop.  Here we catch the case were
	 * we are calling put() the second time.
	 */
	if (prefmt && token->text[0] == '\n' && first)
	{
		first = False;
		anyspc = True;
		spcoffset = token->offset[0];
		return False;
	}

	/* Expand any escape sequences in the token's text */
	if (mui->escape && !token->markup)
		(*mui->escape)(token);

	/* Is it whitespace?  Are we supposed to adjust the text formatting? */
	if (isspace(token->text[0]) && !prefmt)
	{
		/* Just set a flag indicating that a space has been
		 * encountered.  Also remember its offset if it is the first
		 * space encountered, or if the cursor is on this space.
		 */
		if (!anyspc || hascursor)
			spcoffset = token->offset[0];
		anyspc = True;
		first = False;
		return False;
	}

	/* if not a markup, and it won't fit on the current line, then output
	 * a '\n' and return True so it'll appear on the next line.
	 */
	else if (col + token->width + 1 > textwidth - 4
		&& !prefmt && col != 0 && !token->markup)
	{
		(*drawchar)(&newline, 1, 'n', anyspc ? spcoffset : -1);
		col = 0;
		anyspc = False;
		first = True;
		return True;
	}

	/* if we need to adjust our indent, do it now */
	if (col < indent)
	{
		(*drawchar)(&space, col - indent, 'n', anyspc ? spcoffset : -1);
		col = indent;
		anyspc = False;
	}

	/* Output a space between tokens, usually. */
	if (anyspc)
	{
		(*drawchar)(&space, 1, 'n', spcoffset);
		col++;
		anyspc = False;
	}

	/* remember our current column, so we can pretend visible markups are
	 * zero characters wide later.
	 */
	origcol = col;

	/* Output the token's text.  Watch for whitespace and maybe graphics. */
	for (i = 0; i < token->nchars; i++)
	{
		switch (token->text[i])
		{
		  case '\t':
			/* convert to spaces */
			col = tabstop - (col - indent) % tabstop;
			(*drawchar)(&space, -col, curfont, token->offset[0]);
			col += origcol;
			break;

		  case '\n':
			/* newlines are tricky.  If we get here, then we must
			 * be in no-fill mode, so the newline should be output.
			 * However, we'll need to be on the lookout for this
			 * same newline next time.
			 */
			(*drawchar)(&newline, 1, 'n', -1);
			col = 0;
			anyspc = True;
			first = True;
			return True;

		  case '|':
		  case '.':
		  case '^':
		  case '-':
			tmpfont = curfont;
			tmpch = token->text[i];
			if (graphic)
			{
				lch = (i > 0 ? token->text[i - 1] : ' ');
				rch = (i < token->nchars - 1 ? token->text[i + 1] : ' ');
				if (lch == '-' && rch == '-')
				{
					if (tmpch == '|')	tmpch = '5';
					else if (tmpch == '.')	tmpch = '8';
					else if (tmpch == '^')	tmpch = '2';
					tmpfont = 'g';
				}
				else if (lch == '-')
				{
					if (tmpch == '|')	tmpch = '6';
					else if (tmpch == '.')	tmpch = '9';
					else if (tmpch == '^')	tmpch = '3';
					tmpfont = 'g';
				}
				else if (rch == '-')
				{
					if (tmpch == '|')	tmpch = '4';
					else if (tmpch == '.')	tmpch = '7';
					else if (tmpch == '^')	tmpch = '1';
					tmpfont = 'g';
				}
				else if (tmpch == '|')
				{
					tmpfont = 'g';
				}
			}
			(*drawchar)(&tmpch, 1, tmpfont, token->offset[i]);
			col++;
			break;

		  default:
			(*drawchar)(&token->text[i], 1, token->markup ? 'e' : curfont, token->offset[i]);
			col++;
		}
	}

	/* we won't need any more implied spaces between tokens until we hit
	 * the next whitespace token in fill mode.
	 */
	anyspc = False;

	/* markup tokens aren't supposed to affect the logical column number */
#if 0
	if (token->markup)
	{
		col = origcol;
	}
#else
	col = origcol + token->width;
#endif

	/* Done! */
	first = False;
	return False;
}



/* This is a version of drawchar() which does nothing except count the column
 * position.  It is used by mark2col() to figure out which column the cursor
 * is on, and move() to find the offset for a given column.
 */
static long physcol;	/* physical column counter - initialize to 0 */
static long wantoffset;	/* desired offset, whose column we'll be tracking */
static long outcol;	/* physical column of wantoffset - initialize to -1 */
static long wantcol;	/* desired column, whose offsets we'll be tracking */
static long outoffset;	/* offset of char at a given column - init to -1 */
static long prevoffset;	/* previous offset which was >= 0 */

static void countchar(p, qty, font, offset)
	CHAR	*p;	/* the character being output */
	long	qty;	/* quantity of characters */
	_char_	font;	/* the font to show it in (ignored) */
	long	offset;	/* which buffer char this corresponds to */
{
	long	delta;
	register CHAR ch;

	/* a negative qty indicates that the same character repeats */
	if (qty < 0)
	{
		delta = 0;
		qty = -qty;
	}
	else
	{
		delta = 1;
	}

	/* for each character... */
	for ( ; qty > 0; qty--, p += delta, offset += delta)
	{
		/* copy *p into a register variable */
		ch = *p;

		/* if this is the desired offset, remember its column */
		if (offset >= wantoffset && outcol < 0)
			outcol = physcol;

		/* if this offset >= 0, then remember it */
		if (offset >= 0)
			prevoffset = offset;

		/* process the character */
		if (ch == '\n' || ch == '\f' || ch == '\013')
		{
			if (outoffset == -1)
			{
				outoffset = prevoffset;
			}
			physcol = 0;
		}
		else
		{
			if (physcol >= wantcol && outoffset < 0)
			{
				outoffset = prevoffset;
			}
			physcol++;
		}
	}
}


/*----------------------------------------------------------------------------*/
/* Elvis display mode functions                                               */

/* end the mode, and free the modeinfo */
static void term(info)
	DMINFO	*info;	/* window-specific info about mode */
{
	MUINFO	*mui = (MUINFO *)info;

	if (mui->title)
		safefree(mui->title);
	if (mui->line)
		safefree(mui->line);
	safefree(info);
}

/* Convert a mark to a screen-relative column number */
static long mark2col(w, mark, cmd)
	WINDOW	w;	/* window where buffer is shown */
	MARK	mark;	/* mark to convert */
	BOOLEAN	cmd;	/* if True, we're in command mode; else input mode */
{
	CHAR	*p;	/* used for scanning */
	TOKEN	*token;
	MARKBUF	tmp;
	int	i;

	/* Start scanning at the start of the line which contains this mark.
	 * Count characters until we find the mark's offset, or until the
	 * end of the line is reached.
	 */
	i = start(w, mark, NULL);
	p = scanalloc(&p, marktmp(tmp, markbuffer(mark), mui->line[i].offset));
	while ((token = (*mui->get)(&p)) != NULL && !put(token) && outcol < 0)
	{
	}
	scanfree(&p);
	return (outcol>=0 ? outcol : physcol);
}


/* Move vertically, and to a given column (or as close to column as possible) */
static MARK move(w, from, linedelta, column, cmd)
	WINDOW	w;		/* window where buffer is shown */
	MARK	from;		/* old location */
	long	linedelta;	/* line movement */
	long	column;		/* desired column number */
	BOOLEAN	cmd;		/* if True, we're in command mode; else input mode */
{
	int	i, j;
	CHAR	*p;
	TOKEN	*token;
 static MARKBUF	tmp;

	/* find the start of the line containing the mark */
	i = start(w, from, NULL);

	/* Add linedelta to the index, being careful not to move outside
	 * the array.
	 */
	i += linedelta;
	if (i >= mui->nlines)
		i = mui->nlines - 1;
	if (i < 0)
		i = 0;

	/* Move to the given column, or as close as possible */
	if (column == 0)
	{
		outoffset = mui->line[i].offset;
	}
	else if (column >= textwidth)
	{
		if (i < mui->nlines - 1)
			outoffset = mui->line[i + 1].offset - 1;
		else
			outoffset = o_bufchars(markbuffer(from)) - 1;
	}
	else
	{
		j = start(w, marktmp(tmp, markbuffer(from), mui->line[i].offset), NULL);
		assert(j == i);
		wantcol = column;
		scanalloc(&p, marktmp(tmp, markbuffer(from), mui->line[j].offset));
		while ((token = (*mui->get)(&p)) != NULL && !put(token) && outoffset < 0)
		{
		}
		scanfree(&p);
	}

	/* return the found mark */
	if (outoffset < mui->line[i].offset)
		outoffset = mui->line[i].offset;
	return marktmp(tmp, markbuffer(from), outoffset);
}

/* Choose a line to appear at the top of the screen, and return its mark.
 * Also, initialize the info for the next line.
 */
static MARK setup(top, cursor, bottom, info)
	MARK	top;	/* where previous image started */
	long	cursor;	/* offset of cursor */
	MARK	bottom;	/* where previous image ended */
	DMINFO	*info;	/* window-specific info about mode */
{
	WINDOW	win;	/* window we're probably doing this for */
	int	topi, bottomi; /* line indicies of the top & bottom */
 static MARKBUF	tmp;
	int	i;

	/* I kind of wish I has passed "win" as an argument.  Oh, well,
	 * its easy enough to find the window.
	 */
	win = NULL;
	do
	{
		win = winofbuf(win, markbuffer(top));
		assert(win);
	} while (win->mi != info);

	/* we can optimize if "nolist noshowmarkup" */
	dmhtml.canopt = dmman.canopt = (BOOLEAN)(!o_list(win) && !o_showmarkups);
	
	/* find the line indicies of the top & bottom marks, and the cursor */
	/* NOTE: This could have been implemented more efficiently! */
	topi = start(win, top, NULL);
	bottomi = start(win, bottom, NULL);
	i = start(win, marktmp(tmp, markbuffer(top), cursor), NULL);

	/* if the cursor is between top & bottom, great!  Else we need to
	 * either scroll or jump.
	 */
	if (topi <= i && i < bottomi)
	{
		/* great! */
	}
	else if (topi - o_nearscroll <= i && i < topi)
	{
		topi = i;
	}
	else if (bottomi <= i && i < bottomi + o_nearscroll)
	{
		topi = i - o_lines(win) - 1;
		/* NOTE: if sidescrolling is disabled and the window is narrow,
		 * then the above computation may set topi back farther than
		 * necessary.  This is inefficient, but doesn't result in any
		 * visible problems because the Draw module will compensate by
		 * slop scrolling.
		 */
	}
	else
	{
		topi = i - o_lines(win) / 2;
		win->di->logic = DRAW_CENTER;
	}

	/* topi should never be negative */
	if (topi < 0)
		topi = 0;

	/* return the top of the starting line */
	return marktmp(tmp, markbuffer(top), mui->line[topi].offset);
}

static MARK image(w, line, info, draw)
	WINDOW	w;		/* window where drawing will go */
	MARK	line;		/* start of line to draw */
	DMINFO	*info;		/* window-specific info about mode */
	void	(*draw)P_((CHAR *p, long qty, _char_ font, long offset));
				/* function for drawing a single character */
{
	CHAR	*p;
	TOKEN	*token;
 static MARKBUF	tmp;

	/* generate the line image, using the given "draw" function */
	(void)start(w, line, draw);
	scanalloc(&p, line);
	for (token = (*mui->get)(&p); token && !put(token); token = (*mui->get)(&p))
	{
	}
	scanfree(&p);

	/* if we hit the end of the buffer, and didn't output a newline,
	 * then output a newline now.
	 */
	if (!token)
	{
		(*draw)(&newline, 1, 'n', anyspc ? spcoffset : -1);
	}

	/* return the offset of the first token of the next line */
	if (token)
		return marktmp(tmp, markbuffer(line), token->offset[0]);
	else
		return marktmp(tmp, markbuffer(line), o_bufchars(markbuffer(line)));
}


static void header(w, pagenum, info, draw)
	WINDOW	w;	/* window from which we're printing */
	int	pagenum;/* page number */
	DMINFO	*info;	/* drawing state */
	void	(*draw)P_((CHAR *p, long qty, _char_ font, long offset));
{
	CHAR	pg[20];	/* page number, as a text string */
	CHAR	*title;	/* title of the document */
	CHAR	*sides;	/* text to show on left & right sides of page */
	CHAR	*middle;/* text to show in middle of page */
	int	slen;	/* length of side string */
	int	mlen;	/* length of middle string */
	long	gap1;	/* width of gap between left side & middle */
	long	gap2;	/* width of gap between middle & right side */

	assert(info == (DMINFO *)mui);

	/* covert page number to text */
	long2CHAR(pg, (long)pagenum);

	/* find the title of the document */
	title = mui->title ? mui->title
			   : o_bufname(markbuffer(w->cursor));

	/* Find the widths of things */

	/* figure out what goes where */
	if (mui->escape == manescape)
	{
		sides = title;
		middle = pg;
	}
	else
	{
		sides = pg;
		middle = title;
	}

	/* find the lengths of things */
	slen = CHARlen(sides);
	mlen = CHARlen(middle);
	if (slen + 2 + mlen + 2 + slen > textwidth)
	{
		mlen = textwidth - 4 - 2 * slen;
		if (mlen <= 0)
		{
			mlen = CHARlen(middle);
			slen = (textwidth - 4 - mlen) / 2;
		}
	}
	gap1 = (textwidth - mlen) / 2 - slen;
	gap2 = textwidth - mlen - 2 * slen - gap1;

	/* Output the parts of the headings */
	(*draw)(sides, slen, 'n', 0);
	(*draw)(&space, -gap1, 'n', -1);
	(*draw)(middle, mlen, 'n', 0);
	(*draw)(&space, -gap2, 'n', -1);
	(*draw)(sides, slen, 'n', 0);

	/* End the header line, and then skip one or two more lines */
	(*draw)(&newline, pagenum==1 ? -2L : -3L, 'n', -1);
	reduce = True;
}


/*----------------------------------------------------------------------------*/
/* Maintenance functions                                                      */

/* store the offset and parse state in "dest"... or append it to mui->line
 * if "dest" is a NULL pointer.
 */
static void storestate(offset, dest)
	long	offset;
	LINEINFO *dest;
{
	/* if "dest" is NULL, then we'll be appending */
	if (!dest)
	{
		/* we may need to reallocate mui->line */
		if (mui->nlines % GRANULARITY == 0)
		{
			dest = mui->line;
			mui->line = safealloc((int)(mui->nlines + GRANULARITY), sizeof(LINEINFO));
			if (dest)
			{
				memcpy(mui->line, dest, (size_t)mui->nlines * sizeof(LINEINFO));
				safefree(dest);
			}
		}

		/* locate the end of mui->line[], and increment mui->nlines */
		dest = &mui->line[mui->nlines++];
	}

	/* store the info */
	dest->offset = offset;
	dest->state.prefmt = prefmt ? 1 : 0;
	dest->state.graphic = graphic ? 1 : 0;
	dest->state.midline = midline ? 1 : 0;
	dest->state.reduce = reduce ? 1 : 0;
	switch (deffont)
	{
	  case 'b':	dest->state.deffont = 1;	break;
	  case 'u':	dest->state.deffont = 2;	break;
	  case 'i':	dest->state.deffont = 3;	break;
	  case 'f':	dest->state.deffont = 4;	break;
	  case 'e':	dest->state.deffont = 5;	break;
	  default:	dest->state.deffont = 0;
	}
	switch (curfont)
	{
	  case 'b':	dest->state.curfont = 1;	break;
	  case 'u':	dest->state.curfont = 2;	break;
	  case 'i':	dest->state.curfont = 3;	break;
	  case 'f':	dest->state.curfont = 4;	break;
	  case 'e':	dest->state.curfont = 5;	break;
	  default:	dest->state.curfont = 0;
	}
	dest->state.indent = indent;
	dest->state.nest = nest;
	dest->state.listcnt = listcnt;
}


/* This function copies sets "mui" and the other parsing variables from the
 * line information of a given window, for a given starting point.  This
 * function also sets "drawchar" function pointer to a given value; if no
 * "drawchar" function is given, then it uses the internal dummy function
 * and sets up that function's initial state.
 *
 * Returns the index into mui->line[] of the starting line
 */
static int start(win, from, draw)
	WINDOW	win;	/* window to draw for */
	MARK	from;	/* starting point */
	void	(*draw) P_((CHAR *p, long qty, _char_ font, long offset));
{
	int	i;

	/* find the start of the line which contains "from" */
	mui = (MUINFO *)win->mi;
	for (i = 0; i < mui->nlines && mui->line[i].offset <= markoffset(from); i++)
	{
	}
	if (i > 0) i--; /* the above loop took us one line too far */

	/* copy that line's parsing state into parsing variables */
	prefmt = (BOOLEAN)mui->line[i].state.prefmt;
	graphic = (BOOLEAN)mui->line[i].state.graphic;
	midline = (BOOLEAN)mui->line[i].state.midline;
	reduce = (BOOLEAN)mui->line[i].state.reduce;
	deffont = "nbuife"[mui->line[i].state.deffont];
	curfont = "nbuife"[mui->line[i].state.curfont];
	indent = mui->line[i].state.indent;
	nest = mui->line[i].state.nest;
	listcnt = mui->line[i].state.listcnt;

	/* initialize other variables, too */
	first = True;
	anyspc = False;
	title = False; /* nothing that causes a linebreak can appear in title */
	list = o_list(win);
#if 1
	textwidth = o_columns(win);
#else
	textwidth = o_textwidth(markbuffer(win->cursor));
	if (textwidth == 0)
		textwidth = 80;
#endif
	tabstop = o_tabstop(markbuffer(win->cursor));
	listind = o_shiftwidth(markbuffer(win->cursor)) / 2;
	if (listind < 2)
		listind = 2;
	col = 0;
	cursoff = markoffset(win->cursor);

	/* set up the drawchar function pointer */
	if (draw)
	{
		drawchar = draw;
	}
	else
	{
		drawchar = countchar;
		physcol = 0;
		wantoffset = markoffset(from);
		outcol = -1;
		wantcol = 0;
		outoffset = -1;
		prevoffset = -1;
	}

	return i;
}


/* Locate the title in a given buffer, and store it in mui->title.  Also
 * sets mui->endtitle to an appropriate value.  It is assumed that the mui
 * pointer has been initialized before this function is called.
 */
static void findtitle(buf)
	BUFFER	buf;	/* the buffer to scan */
{
	CHAR	*p;	/* used for scanning the buffer */
	TOKEN	*token;	/* a token from the buffer */
	MARKBUF	tmp;
	int	i;

	/* free the old title, if any */
	if (mui->title)
	{
		safefree(mui->title);
		mui->title = NULL;
	}

	/* read the first non-whitespace token from the buffer */
	scanalloc(&p, marktmp(tmp, buf, 0));
	do
	{
		token = (*mui->get)(&p);
	} while (token && isspace(token->text[0]));

	/* if no token, then no title */
	if (!token)
	{
		mui->endtitle = o_bufchars(buf);
		scanfree(&p);
		return;
	}

	/* if token doesn't explicitly allow title to follow, then no title */
	if (!token->markup || token->markup->TITLE != 'Y')
	{
		mui->endtitle = token->offset[0];
		scanfree(&p);
		return;
	}

	/* Collect characters from text tokens, up to next markup which
	 * can't appear in the title.
	 */
	while ((token = (*mui->get)(&p)) != NULL && (!token->markup || token->markup->TITLE == 'Y'))
	{
		/* if text, then append it to title, with a leading blank */
		if (!token->markup && !isspace(token->text[0]))
		{
			if (mui->title)
				buildCHAR(&mui->title, ' ');
			for (i = 0; i < token->nchars; i++)
				buildCHAR(&mui->title, token->text[i]);
		}
	}
	scanfree(&p);
}


/* Adjust the line tables of any window whose main buffer has been altered.
 * This function is meant to be called only from bufreplace().
 */
void dmmuadjust(from, to, delta)
	MARK	from;	/* old start of text */
	MARK	to;	/* old end of text */
	long	delta;	/* difference between old "to" and new "to" offsets */
{
	WINDOW	win;	/* used for scanning windows */
	CHAR	*p;	/* used for scanning text */
	TOKEN	*token;	/* used for scanning text */
	LINEINFO li;	/* buffer for generating LINEINFO record */
	LINEINFO *old;	/* old value of mui->line (previous line array) */
#if 0
	int	noldlines;/* old value of mui->nlines */
#endif
	MARKBUF	tmp;
	int	i;

#ifdef DEBUG_MARKUP
	fprintf(stderr, "dmmuadjust({%s,%ld}, {%s,%ld}, %ld)\n",
		o_bufname(markbuffer(from)), markoffset(from),
		o_bufname(markbuffer(to)), markoffset(to),
		delta);
#endif

	/* for each window that uses this buffer... */
	for (win = winofbuf(NULL, markbuffer(from));
	     win;
	     win = winofbuf(win, markbuffer(from)))
	{
		/* ignore if not in a markup display mode */
		if (win->md->setup != setup)
		{
			continue;
		}

		/* If necessary, rescan the title of this buffer */
		mui = (MUINFO *)win->mi;
		if (mui->endtitle <= markoffset(from))
			findtitle(markbuffer(from));

		/* Start on the line BEFORE the one where changes begin.
		 * This is because if we insert a small word at the front of
		 * a line, it might fit at the end of the preceding line.
		 */
		i = start(win, from, (void(*)P_((CHAR *,long,_char_,long)))0);
		if (i > 0) i--;

		/* Pretend the cursor is someplace harmless.  This is done
		 * because the cursor position affects the appearance of the
		 * text, and sometimes this affects where line breaks occur.
		 */
		cursoff = o_bufchars(markbuffer(from));

		/* regenerate that preceding line */
		p = scanalloc(&p, marktmp(tmp, markbuffer(from), mui->line[i].offset));
		do
		{
			token = (*mui->get)(&p);
		} while (token && !put(token));

		/* regenerate the line where the change took place */
		if (token && token->offset[token->nchars - 1] != o_bufchars(markbuffer(from)) - 1)
		{
			/* but first, store results of preceding line's scan */
			i++;
			storestate(token->offset[0], &mui->line[i]);

			/* okay, now scan the changed line.  We already have
			 * the first token
			 */
			while (token && !put(token))
			{
				token = (*mui->get)(&p);
			}
		}

		/* If we hit the end of the buffer, then no adjustments are
		 * necessary.  This may leave superfluous data at the end of
		 * the array, but who cares? 
		 */
		if (!token || token->offset[token->nchars - 1] == o_bufchars(markbuffer(from)) - 1)
		{
			mui->nlines = i + 1;
			scanfree(&p);
#ifdef DEBUG_MARKUP
			fprintf(stderr, "\tNo adjustment needed; hit end of buffer\n");
#endif
			continue;
		}

		/* If the new line ended with the same (adjusted) offset and
		 * same state, then no adjustments are necessary except to
		 * the offsets.
		 */
		storestate(token->offset[0], &li);
		if (i + 1 < mui->nlines
		 && mui->line[i + 1].offset + delta == token->offset[0]
		 && !memcmp(&li.state, &mui->line[i + 1].state, sizeof li.state)
		 && mui->line[i + 1].offset >= markoffset(to))
		{
			if (delta == 0)
			{
#ifdef DEBUG_MARKUP
				fprintf(stderr, "\tNo adjustment needed; same state and delta=0\n");
#endif
				scanfree(&p);
				continue;
			}

			for (i++; i < mui->nlines; i++)
			{
				mui->line[i].offset += delta;
			}
			scanfree(&p);
#ifdef DEBUG_MARKUP
			fprintf(stderr, "\tAdjusted the offsets only!\n");
#endif
			continue;
		}
		
		/* Adjustments are going to be complex.  Start a new array,
		 * but don't throw the old one out yet because it may save
		 * us some work.
		 */
		old = mui->line;
#if 0
		noldlines = mui->nlines;
#endif
		mui->line = safealloc((i + 1 + GRANULARITY) - ((i + 1) % GRANULARITY), sizeof(LINEINFO));
		for (mui->nlines = 0; mui->nlines <= i; mui->nlines++)
		{
			mui->line[mui->nlines] = old[mui->nlines];
		}
		mui->line[mui->nlines++] = li;

		/* regenerate lines until we have same (adjusted) offset and
		 * state as we had before the change.
		 */ 
		do
		{
			/* Regenerate a line */
			put(token);
			do
			{
				token = (*mui->get)(&p);
			} while (token && !put(token));
			if (!token)
			{
				break;
			}

			/* Append its state to the new array.  */
			storestate(token->offset[0], (LINEINFO *)0);

			/* if the line's offset comes after the adjusted "to"
			 * offset, and there is an old state & adjusted offset
			 * which match, then just copy the old info.
			 */
			/*!!!*/
		} while (token);
		scanfree(&p);
		safefree(old);
#ifdef DEBUG_MARKUP
		fprintf(stderr, "\tRegenerated info for every following line\n");
#endif
	}
}


/*----------------------------------------------------------------------------*/
DISPMODE dmhtml =
{
	"html",
	"WWW hypertext",
	False,	/* display generating can't be optimized */
	False,	/* shouldn't use standard wordwrap */
	0,	/* no window options */
	NULL,
	0,	/* no global options */
	NULL,
	NULL,
	htmlinit,
	term,
	mark2col,
	move,
	NULL,	/* wordmove will be set to dmnormal.wordmove in init() */
	setup,
	image,
	header,
	NULL,	/* no autoindent */
	htmltagatcursor,
	htmltagload,
	htmltagnext
};

DISPMODE dmman =
{
	"man",
	"nroff -man",
	False,	/* display generating can't be optimized */
	False,	/* shouldn't use standard wordwrap */
	0,	/* no window options */
	NULL,
	0,	/* no global options */
	NULL,
	NULL,
	maninit,
	term,
	mark2col,
	move,
	NULL,	/* wordmove will be set to dmnormal.wordmove in init() */
	setup,
	image,
	header,
	NULL,	/* no autoindent */
	NULL,	/* tagatcursor will be set to dmnormal.tagatcursor in init() */
	NULL,	/* tagload will be set to dmnormal.tagload in init() */
	NULL,	/* tagnext will be set to dmnormal.tagnext in init() */
};
#endif /* DISPLAY_MARKUP */

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.