shithub: riscv

ref: bb2060028e616df560628cf6f1e6e5d196d50833
dir: /sys/src/libhtml/build.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <ctype.h>
#include <html.h>
#include "impl.h"

// A stack for holding integer values
enum {
	Nestmax = 256	// max nesting level of lists, font styles, etc.
};

struct Stack {
	int		n;				// next available slot (top of stack is stack[n-1])
	int		slots[Nestmax];	// stack entries
};

// Parsing state
struct Pstate
{
	Pstate*	next;			// in stack of Pstates
	int		skipping;		// true when we shouldn't add items
	int		skipwhite;		// true when we should strip leading space
	int		curfont;		// font index for current font
	int		curfg;		// current foreground color
	Background	curbg;	// current background
	int		curvoff;		// current baseline offset
	uchar	curul;		// current underline/strike state
	uchar	curjust;		// current justify state
	int		curanchor;	// current (href) anchor id (if in one), or 0
	int		curstate;		// current value of item state
	int		literal;		// current literal state
	int		inpar;		// true when in a paragraph-like construct
	int		adjsize;		// current font size adjustment
	Item*	items;		// dummy head of item list we're building
	Item*	lastit;		// tail of item list we're building
	Item*	prelastit;		// item before lastit
	Stack	fntstylestk;	// style stack
	Stack	fntsizestk;		// size stack
	Stack	fgstk;		// text color stack
	Stack	ulstk;		// underline stack
	Stack	voffstk;		// vertical offset stack
	Stack	listtypestk;	// list type stack
	Stack	listcntstk;		// list counter stack
	Stack	juststk;		// justification stack
	Stack	hangstk;		// hanging stack
};

struct ItemSource
{
	Docinfo*		doc;
	Pstate*		psstk;
	int			nforms;
	int			ntables;
	int			nanchors;
	int			nframes;
	Formfield*	curfield;
	Form*		curform;
	Map*		curmap;
	Table*		tabstk;
	Kidinfo*		kidstk;
};

// Some layout parameters
enum {
	FRKIDMARGIN = 6,	// default margin around kid frames
	IMGHSPACE = 0,	// default hspace for images (0 matches IE, Netscape)
	IMGVSPACE = 0,	// default vspace for images
	FLTIMGHSPACE = 2,	// default hspace for float images
	TABSP = 5,		// default cellspacing for tables
	TABPAD = 1,		// default cell padding for tables
	LISTTAB = 1,		// number of tabs to indent lists
	BQTAB = 1,		// number of tabs to indent blockquotes
	HRSZ = 2,			// thickness of horizontal rules
	SUBOFF = 4,		// vertical offset for subscripts
	SUPOFF = 6,		// vertical offset for superscripts
	NBSP = 160		// non-breaking space character
};

// These tables must be sorted
static StringInt align_tab[] = {
	{L"baseline",	ALbaseline},
	{L"bottom",	ALbottom},
	{L"center",	ALcenter},
	{L"char",		ALchar},
	{L"justify",	ALjustify},
	{L"left",		ALleft},
	{L"middle",	ALmiddle},
	{L"right",		ALright},
	{L"top",		ALtop}
};
#define NALIGNTAB (sizeof(align_tab)/sizeof(StringInt))

static StringInt input_tab[] = {
	{L"button",	Fbutton},
	{L"checkbox",	Fcheckbox},
	{L"file",		Ffile},
	{L"hidden",	Fhidden},
	{L"image",	Fimage},
	{L"password",	Fpassword},
	{L"radio",		Fradio},
	{L"reset",		Freset},
	{L"submit",	Fsubmit},
	{L"text",		Ftext}
};
#define NINPUTTAB (sizeof(input_tab)/sizeof(StringInt))

static StringInt clear_tab[] = {
	{L"all",	IFcleft|IFcright},
	{L"left",	IFcleft},
	{L"right",	IFcright}
};
#define NCLEARTAB (sizeof(clear_tab)/sizeof(StringInt))

static StringInt fscroll_tab[] = {
	{L"auto",	FRhscrollauto|FRvscrollauto},
	{L"no",	FRnoscroll},
	{L"yes",	FRhscroll|FRvscroll},
};
#define NFSCROLLTAB (sizeof(fscroll_tab)/sizeof(StringInt))

static StringInt shape_tab[] = {
	{L"circ",		SHcircle},
	{L"circle",		SHcircle},
	{L"poly",		SHpoly},
	{L"polygon",	SHpoly},
	{L"rect",		SHrect},
	{L"rectangle",	SHrect}
};
#define NSHAPETAB (sizeof(shape_tab)/sizeof(StringInt))

static StringInt method_tab[] = {
	{L"get",		HGet},
	{L"post",		HPost}
};
#define NMETHODTAB (sizeof(method_tab)/sizeof(StringInt))

static Rune* roman[15]= {
	L"I", L"II", L"III", L"IV", L"V", L"VI", L"VII", L"VIII", L"IX", L"X",
	L"XI", L"XII", L"XIII", L"XIV", L"XV"
};
#define NROMAN 15

// List number types
enum {
	LTdisc, LTsquare, LTcircle, LT1, LTa, LTA, LTi, LTI
};

enum {
	SPBefore = 2,
	SPAfter = 4,
	BL = 1,
	BLBA = (BL|SPBefore|SPAfter)
};

// blockbrk[tag] is break info for a block level element, or one
// of a few others that get the same treatment re ending open paragraphs
// and requiring a line break / vertical space before them.
// If we want a line of space before the given element, SPBefore is OR'd in.
// If we want a line of space after the given element, SPAfter is OR'd in.

static uchar blockbrk[Numtags]= {
	[Taddress] BLBA, [Tblockquote] BLBA, [Tcenter] BL,
	[Tdir] BLBA, [Tdiv] BL, [Tdd] BL, [Tdl] BLBA,
	[Tdt] BL, [Tform] BLBA,
	// headings and tables get breaks added manually
	[Th1] BL, [Th2] BL, [Th3] BL,
	[Th4] BL, [Th5] BL, [Th6] BL,
	[Thr] BL, [Tisindex] BLBA, [Tli] BL, [Tmenu] BLBA,
	[Tol] BLBA, [Tp] BLBA, [Tpre] BLBA,
	[Tul] BLBA
};

enum {
	AGEN = 1
};

// attrinfo is information about attributes.
// The AGEN value means that the attribute is generic (applies to almost all elements)
static uchar attrinfo[Numattrs]= {
	[Aid] AGEN, [Aclass] AGEN, [Astyle] AGEN, [Atitle] AGEN,
	[Aonblur] AGEN, [Aonchange] AGEN, [Aonclick] AGEN,
	[Aondblclick] AGEN, [Aonfocus] AGEN, [Aonkeypress] AGEN,
	[Aonkeyup] AGEN, [Aonload] AGEN, [Aonmousedown] AGEN,
	[Aonmousemove] AGEN, [Aonmouseout] AGEN, [Aonmouseover] AGEN,
	[Aonmouseup] AGEN, [Aonreset] AGEN, [Aonselect] AGEN,
	[Aonsubmit] AGEN, [Aonunload] AGEN
};

static uchar scriptev[Numattrs]= {
	[Aonblur] SEonblur, [Aonchange] SEonchange, [Aonclick] SEonclick,
	[Aondblclick] SEondblclick, [Aonfocus] SEonfocus, [Aonkeypress] SEonkeypress,
	[Aonkeyup] SEonkeyup, [Aonload] SEonload, [Aonmousedown] SEonmousedown,
	[Aonmousemove] SEonmousemove, [Aonmouseout] SEonmouseout, [Aonmouseover] SEonmouseover,
	[Aonmouseup] SEonmouseup, [Aonreset] SEonreset, [Aonselect] SEonselect,
	[Aonsubmit] SEonsubmit, [Aonunload] SEonunload
};

// Color lookup table
static StringInt color_tab[] = {
	{L"aqua", 0x00FFFF},
	{L"black",  0x000000},
	{L"blue", 0x0000CC},
	{L"fuchsia", 0xFF00FF},
	{L"gray", 0x808080},
	{L"green", 0x008000},
	{L"lime", 0x00FF00},
	{L"maroon", 0x800000},
	{L"navy", 0x000080,},
	{L"olive", 0x808000},
	{L"purple", 0x800080},
	{L"red", 0xFF0000},
	{L"silver", 0xC0C0C0},
	{L"teal", 0x008080},
	{L"white", 0xFFFFFF},
	{L"yellow", 0xFFFF00}
};
#define NCOLORS (sizeof(color_tab)/sizeof(StringInt))

static StringInt 		*targetmap;
static int			targetmapsize;
static int			ntargets;

static int buildinited = 0;

#define SMALLBUFSIZE 240
#define BIGBUFSIZE 2000

int	dbgbuild = 0;
int	warn = 0;

static Align		aalign(Token* tok);
static int			acolorval(Token* tok, int attid, int dflt);
static void			addbrk(Pstate* ps, int sp, int clr);
static void			additem(Pstate* ps, Item* it, Token* tok);
static void			addlinebrk(Pstate* ps, int clr);
static void			addnbsp(Pstate* ps);
static void			addtext(Pstate* ps, Rune* s);
static Dimen		adimen(Token* tok, int attid);
static int			aflagval(Token* tok, int attid);
static int			aintval(Token* tok, int attid, int dflt);
static Rune*		astrval(Token* tok, int attid, Rune* dflt);
static int			atabval(Token* tok, int attid, StringInt* tab, int ntab, int dflt);
static int			atargval(Token* tok, int dflt);
static int			auintval(Token* tok, int attid, int dflt);
static Rune*		aurlval(Token* tok, int attid, Rune* dflt, Rune* base);
static Rune*		aval(Token* tok, int attid);
static void			buildinit(void);
static Pstate*		cell_pstate(Pstate* oldps, int ishead);
static void			changehang(Pstate* ps, int delta);
static void			changeindent(Pstate* ps, int delta);
static int			color(Rune* s, int dflt);
static void			copystack(Stack* tostk, Stack* fromstk);
static int			dimprint(char* buf, int nbuf, Dimen d);
static Pstate*		finishcell(Table* curtab, Pstate* psstk);
static void			finish_table(Table* t);
static void			freeanchor(Anchor* a);
static void			freedestanchor(DestAnchor* da);
static void			freeform(Form* f);
static void			freeformfield(Formfield* ff);
static void			freeitem(Item* it);
static void			freepstate(Pstate* p);
static void			freepstatestack(Pstate* pshead);
static void			freescriptevents(SEvent* ehead);
static void			freetable(Table* t);
static Map*		getmap(Docinfo* di, Rune* name);
static Rune*		getpcdata(Token* toks, int tokslen, int* ptoki);
static Pstate*		lastps(Pstate* psl);
static Rune*		listmark(uchar ty, int n);
static int			listtyval(Token* tok, int dflt);
static Align		makealign(int halign, int valign);
static Background	makebackground(Rune* imgurl, int color);
static Dimen		makedimen(int kind, int spec);
static Anchor*		newanchor(int index, Rune* name, Rune* href, int target, Anchor* link);
static Area*		newarea(int shape, Rune* href, int target, Area* link);
static DestAnchor*	newdestanchor(int index, Rune* name, Item* item, DestAnchor* link);
static Docinfo*		newdocinfo(void);
static Genattr*		newgenattr(Rune* id, Rune* class, Rune* style, Rune* title, SEvent* events);
static Form*		newform(int formid, Rune* name, Rune* action,
					int target, int method, Form* link);
static Formfield*	newformfield(int ftype, int fieldid, Form* form, Rune* name,
					Rune* value, int size, int maxlength, Formfield* link);
static Item*		newifloat(Item* it, int side);
static Item*		newiformfield(Formfield* ff);
static Item*		newiimage(Rune* src, Rune* altrep, int align, int width, int height,
					int hspace, int vspace, int border, int ismap, Map* map);
static Item*		newirule(int align, int size, int noshade, int color, Dimen wspec);
static Item*		newispacer(int spkind);
static Item*		newitable(Table* t);
static ItemSource*	newitemsource(Docinfo* di);
static Item*		newitext(Rune* s, int fnt, int fg, int voff, int ul);
static Kidinfo*		newkidinfo(int isframeset, Kidinfo* link);
static Option*		newoption(int selected, Rune* value, Rune* display, Option* link);
static Pstate*		newpstate(Pstate* link);
static SEvent*		newscriptevent(int type, Rune* script, SEvent* link);
static Table*		newtable(int tableid, Align align, Dimen width, int border,
					int cellspacing, int cellpadding, Background bg, Token* tok, Table* link);
static Tablecell*	newtablecell(int cellid, int rowspan, int colspan, Align align, Dimen wspec,
					int hspec, Background bg, int flags, Tablecell* link);
static Tablerow*	newtablerow(Align align, Background bg, int flags, Tablerow* link);
static Dimen		parsedim(Rune* s, int ns);
static void			pop(Stack* stk);
static void			popfontsize(Pstate* ps);
static void			popfontstyle(Pstate* ps);
static void			popjust(Pstate* ps);
static int			popretnewtop(Stack* stk, int dflt);
static int			push(Stack* stk, int val);
static void			pushfontsize(Pstate* ps, int sz);
static void			pushfontstyle(Pstate* ps, int sty);
static void			pushjust(Pstate* ps, int j);
static Item*		textit(Pstate* ps, Rune* s);
static Rune*		removeallwhite(Rune* s);
static void			resetdocinfo(Docinfo* d);
static void			setcurfont(Pstate* ps);
static void			setcurjust(Pstate* ps);
static void			setdimarray(Token* tok, int attid, Dimen** pans, int* panslen);
static Rune*		stringalign(int a);
static void			targetmapinit(void);
static int			toint(Rune* s);
static int			top(Stack* stk, int dflt);
static void			trim_cell(Tablecell* c);
static int			validalign(Align a);
static int			validdimen(Dimen d);
static int			validformfield(Formfield* f);
static int			validhalign(int a);
static int			validptr(void* p);
static int			validStr(Rune* s);
static int			validtable(Table* t);
static int			validtablerow(Tablerow* r);
static int			validtablecol(Tablecol* c);
static int			validtablecell(Tablecell* c);
static int			validvalign(int a);
static int			Iconv(Fmt *f);

static void
buildinit(void)
{
	fmtinstall('I', Iconv);
	targetmapinit();
	buildinited = 1;
}

static ItemSource*
newitemsource(Docinfo* di)
{
	ItemSource*	is;
	Pstate*	ps;

	ps = newpstate(nil);
	if(di->mediatype != TextHtml) {
		ps->curstate &= ~IFwrap;
		ps->literal = 1;
		pushfontstyle(ps, FntT);
	}
	is = (ItemSource*)emalloc(sizeof(ItemSource));
	is->doc = di;
	is->psstk = ps;
	is->nforms = 0;
	is->ntables = 0;
	is->nanchors = 0;
	is->nframes = 0;
	is->curfield = nil;
	is->curform = nil;
	is->curmap = nil;
	is->tabstk = nil;
	is->kidstk = nil;
	return is;
}

static void
linkitems(Docinfo *di, Item *it)
{
	Formfield *ff;
	Tablecell *c;
	Table *tt;

	while(it != nil){
		switch(it->tag) {
		case Iimagetag:
			/* link image to docinfo */
			((Iimage*)it)->nextimage = di->images;
			di->images = (Iimage*)it;
			break;
		case Iformfieldtag:
			/* link formfield to form */
			ff = ((Iformfield*)it)->formfield;
			if(ff != nil && ff->form != nil){
				for(ff = ff->form->fields; ff != nil; ff = ff->next){
					if(ff == ((Iformfield*)it)->formfield)
						goto Next;
					if(ff->next == nil)
						break;
				}
				((Iformfield*)it)->formfield->next = nil;
				if(ff != nil){
					ff->next = ((Iformfield*)it)->formfield;
					ff = ff->next;
				} else {
					ff = ((Iformfield*)it)->formfield;
					ff->form->fields = ff;
				}
				linkitems(di, ff->image);
			}
			break;
		case Itabletag:
			/* link table to docinfo */
			tt = ((Itable*)it)->table;
			if(tt == nil)
				break;
			tt->tabletok = nil;
			tt->next = di->tables;
			di->tables = tt;
			linkitems(di, tt->caption);
			for(c = tt->cells; c != nil; c = c->next)
				linkitems(di, c->content);
			break;
		case Ifloattag:
			linkitems(di, ((Ifloat*)it)->item);
			break;
		}
	Next:
		it = it->next;
	}
}

static Item *getitems(ItemSource* is, uchar* data, int datalen);

// Parse an html document and create a list of layout items.
// Allocate and return document info in *pdi.
// When caller is done with the items, it should call
// freeitems on the returned result, and then
// freedocinfo(*pdi).
Item*
parsehtml(uchar* data, int datalen, Rune* pagesrc, int mtype, int chset, Docinfo** pdi)
{
	Item *it;
	Docinfo*	di;
	ItemSource*	is;

	di = newdocinfo();
	di->src = _Strdup(pagesrc);
	di->base = _Strdup(pagesrc);
	di->mediatype = mtype;
	di->chset = chset;
	*pdi = di;
	is = newitemsource(di);
	it = getitems(is, data, datalen);
	freepstatestack(is->psstk);
	free(is);
	return it;
}

// Get a group of tokens for lexer, parse them, and create
// a list of layout items.
// When caller is done with the items, it should call
// freeitems on the returned result.
static Item*
getitems(ItemSource* is, uchar* data, int datalen)
{
	int	i;
	int	j;
	int	nt;
	int	pt;
	int	doscripts;
	int	tokslen;
	int	toki;
	int	h;
	int	sz;
	int	method;
	int	n;
	int	nblank;
	int	norsz;
	int	bramt;
	int	sty;
	int	nosh;
	int	color;
	int	oldcuranchor;
	int	dfltbd;
	int	v;
	int	hang;
	int	isempty;
	int	tag;
	int	brksp;
	int	target;
	uchar	brk;
	uchar	flags;
	uchar	align;
	uchar	al;
	uchar	ty;
	uchar	ty2;
	Pstate*	ps;
	Pstate*	nextps;
	Pstate*	outerps;
	Table*	curtab;
	Token*	tok;
	Token*	toks;
	Docinfo*	di;
	Item*	ans;
	Item*	img;
	Item*	ffit;
	Item*	tabitem;
	Rune*	s;
	Rune*	t;
	Rune*	name;
	Rune*	enctype;
	Rune*	usemap;
	Rune*	prompt;
	Rune*	equiv;
	Rune*	val;
	Rune*	nsz;
	Rune*	script;
	Map*	map;
	Form*	frm;
	Kidinfo*	kd;
	Kidinfo*	ks;
	Kidinfo*	pks;
	Dimen	wd;
	Option*	option;
	Table*	tab;
	Tablecell*	c;
	Tablerow*	tr;
	Formfield*	field;
	Formfield*	ff;
	Rune*	href;
	Rune*	src;
	Rune*	scriptsrc;
	Rune*	bgurl;
	Rune*	action;
	Background	bg;

	if(!buildinited)
		buildinit();
	doscripts = 0;	// for now
	ps = is->psstk;
	curtab = is->tabstk;
	di = is->doc;
	toks = _gettoks(data, datalen, di->chset, di->mediatype, &tokslen);
	toki = 0;
	for(; toki < tokslen; toki++) {
		tok = &toks[toki];
		if(dbgbuild > 1)
			fprint(2, "build: curstate %ux, token %T\n", ps->curstate, tok);
		tag = tok->tag;
		brk = 0;
		brksp = 0;
		if(tag < Numtags) {
			brk = blockbrk[tag];
			if(brk&SPBefore)
				brksp = 1;
		}
		else if(tag < Numtags + RBRA) {
			brk = blockbrk[tag - RBRA];
			if(brk&SPAfter)
				brksp = 1;
		}
		if(brk) {
			addbrk(ps, brksp, 0);
			if(ps->inpar) {
				popjust(ps);
				ps->inpar = 0;
			}
		}
		// check common case first (Data), then switch statement on tag
		if(tag == Data) {
			// Lexing didn't pay attention to SGML record boundary rules:
			// \n after start tag or before end tag to be discarded.
			// (Lex has already discarded all \r's).
			// Some pages assume this doesn't happen in <PRE> text,
			// so we won't do it if literal is true.
			// BUG: won't discard \n before a start tag that begins
			// the next bufferful of tokens.
			s = tok->text;
			n = _Strlen(s);
			if(!ps->literal) {
				i = 0;
				j = n;
				if(toki > 0) {
					pt = toks[toki - 1].tag;
					// IE and Netscape both ignore this rule (contrary to spec)
					// if previous tag was img
					if(pt < Numtags && pt != Timg && j > 0 && s[0] == '\n')
						i++;
				}
				if(toki < tokslen - 1) {
					nt = toks[toki + 1].tag;
					if(nt >= RBRA && nt < Numtags + RBRA && j > i && s[j - 1] == '\n')
						j--;
				}
				if(i > 0 || j < n) {
					t = s;
					s = _Strsubstr(s, i, j);
					free(t);
					n = j-i;
				}
			}
			if(ps->skipwhite) {
				_trimwhite(s, n, &t, &nt);
				if(t == nil) {
					free(s);
					s = nil;
				}
				else if(t != s) {
					t = _Strndup(t, nt);
					free(s);
					s = t;
				}
				if(s != nil)
					ps->skipwhite = 0;
			}
			tok->text = nil;		// token doesn't own string anymore
			if(s != nil)
				addtext(ps, s);
		}
		else
			switch(tag) {
			// Some abbrevs used in following DTD comments
			// %text = 	#PCDATA
			//		| TT | I | B | U | STRIKE | BIG | SMALL | SUB | SUP
			//		| EM | STRONG | DFN | CODE | SAMP | KBD | VAR | CITE
			//		| A | IMG | APPLET | FONT | BASEFONT | BR | SCRIPT | MAP
			//		| INPUT | SELECT | TEXTAREA
			// %block = P | UL | OL | DIR | MENU | DL | PRE | DL | DIV | CENTER
			//		| BLOCKQUOTE | FORM | ISINDEX | HR | TABLE
			// %flow = (%text | %block)*
			// %body.content = (%heading | %text | %block | ADDRESS)*

			// <!ELEMENT A - - (%text) -(A)>
			// Anchors are not supposed to be nested, but you sometimes see
			// href anchors inside destination anchors.
			case Ta:
				if(ps->curanchor != 0) {
					if(warn)
						fprint(2, "warning: nested <A> or missing </A>\n");
					ps->curanchor = 0;
				}
				name = aval(tok, Aname);
				href = aurlval(tok, Ahref, nil, di->base);
				// ignore rel, rev, and title attrs
				if(href != nil) {
					target = atargval(tok, di->target);
					di->anchors = newanchor(++is->nanchors, name, href, target, di->anchors);
					if(name != nil)
						name = _Strdup(name);	// for DestAnchor construction, below
					ps->curanchor = is->nanchors;
					ps->curfg = push(&ps->fgstk, di->link);
					ps->curul = push(&ps->ulstk, ULunder);
				}
				if(name != nil) {
					// add a null item to be destination
					additem(ps, newispacer(ISPnull), tok);
					di->dests = newdestanchor(++is->nanchors, name, ps->lastit, di->dests);
				}
				break;

			case Ta+RBRA :
				if(ps->curanchor != 0) {
					ps->curfg = popretnewtop(&ps->fgstk, di->text);
					ps->curul = popretnewtop(&ps->ulstk, ULnone);
					ps->curanchor = 0;
				}
				break;

			// <!ELEMENT APPLET - - (PARAM | %text)* >
			// We can't do applets, so ignore PARAMS, and let
			// the %text contents appear for the alternative rep
			case Tapplet:
			case Tapplet+RBRA:
				if(warn && tag == Tapplet)
					fprint(2, "warning: <APPLET> ignored\n");
				break;

			// <!ELEMENT AREA - O EMPTY>
			case Tarea:
				map = di->maps;
				if(map == nil) {
					if(warn)
						fprint(2, "warning: <AREA> not inside <MAP>\n");
					continue;
				}
				map->areas = newarea(atabval(tok, Ashape, shape_tab, NSHAPETAB, SHrect),
					aurlval(tok, Ahref, nil, di->base),
					atargval(tok, di->target),
					map->areas);
				setdimarray(tok, Acoords, &map->areas->coords, &map->areas->ncoords);
				break;

			// <!ELEMENT (B|STRONG) - - (%text)*>
			case Tb:
			case Tstrong:
				pushfontstyle(ps, FntB);
				break;

			case Tb+RBRA:
			case Tcite+RBRA:
			case Tcode+RBRA:
			case Tdfn+RBRA:
			case Tem+RBRA:
			case Tkbd+RBRA:
			case Ti+RBRA:
			case Tsamp+RBRA:
			case Tstrong+RBRA:
			case Ttt+RBRA:
			case Tvar+RBRA :
			case Taddress+RBRA:
				popfontstyle(ps);
				break;

			// <!ELEMENT BASE - O EMPTY>
			case Tbase:
				t = di->base;
				di->base = aurlval(tok, Ahref, di->base, di->base);
				if(t != nil)
					free(t);
				di->target = atargval(tok, di->target);
				break;

			// <!ELEMENT BASEFONT - O EMPTY>
			case Tbasefont:
				ps->adjsize = aintval(tok, Asize, 3) - 3;
				break;

			// <!ELEMENT (BIG|SMALL) - - (%text)*>
			case Tbig:
			case Tsmall:
				sz = ps->adjsize;
				if(tag == Tbig)
					sz += Large;
				else
					sz += Small;
				pushfontsize(ps, sz);
				break;

			case Tbig+RBRA:
			case Tsmall+RBRA:
				popfontsize(ps);
				break;

			// <!ELEMENT BLOCKQUOTE - - %body.content>
			case Tblockquote:
				changeindent(ps, BQTAB);
				break;

			case Tblockquote+RBRA:
				changeindent(ps, -BQTAB);
				break;

			// <!ELEMENT BODY O O %body.content>
			case Tbody:
				ps->skipping = 0;
				bg = makebackground(nil, acolorval(tok, Abgcolor, di->background.color));
				bgurl = aurlval(tok, Abackground, nil, di->base);
				if(bgurl != nil) {
					if(di->backgrounditem != nil)
						freeitem(di->backgrounditem);
					di->backgrounditem = (Iimage*)newiimage(bgurl, nil, ALnone, 0, 0, 0, 0, 0, 0, nil);
				}
				ps->curbg = bg;
				di->background = bg;
				di->text = acolorval(tok, Atext, di->text);
				di->link = acolorval(tok, Alink, di->link);
				di->vlink = acolorval(tok, Avlink, di->vlink);
				di->alink = acolorval(tok, Aalink, di->alink);
				if(di->text != ps->curfg) {
					ps->curfg = di->text;
					ps->fgstk.n = 0;
				}
				break;

			case Tbody+RBRA:
				// HTML spec says ignore things after </body>,
				// but IE and Netscape don't
				// ps.skipping = 1;
				break;

			// <!ELEMENT BR - O EMPTY>
			case Tbr:
				addlinebrk(ps, atabval(tok, Aclear, clear_tab, NCLEARTAB, 0));
				break;

			// <!ELEMENT CAPTION - - (%text;)*>
			case Tcaption:
				if(curtab == nil) {
					if(warn)
						fprint(2, "warning: <CAPTION> outside <TABLE>\n");
					continue;
				}
				if(curtab->caption != nil) {
					if(warn)
						fprint(2, "warning: more than one <CAPTION> in <TABLE>\n");
					continue;
				}
				ps = newpstate(ps);
				curtab->caption_place = atabval(tok, Aalign, align_tab, NALIGNTAB, ALtop);
				break;

			case Tcaption+RBRA:
				nextps = ps->next;
				if(curtab == nil || nextps == nil) {
					if(warn)
						fprint(2, "warning: unexpected </CAPTION>\n");
					continue;
				}
				if(curtab->caption != nil)
					freeitems(curtab->caption);
				curtab->caption = ps->items->next;
				ps->items->next = nil;
				freepstate(ps);
				ps = nextps;
				break;

			case Tcenter:
			case Tdiv:
				if(tag == Tcenter)
					al = ALcenter;
				else
					al = atabval(tok, Aalign, align_tab, NALIGNTAB, ps->curjust);
				pushjust(ps, al);
				break;

			case Tcenter+RBRA:
			case Tdiv+RBRA:
				popjust(ps);
				break;

			// <!ELEMENT DD - O  %flow >
			case Tdd:
				if(ps->hangstk.n == 0) {
					if(warn)
						fprint(2, "warning: <DD> not inside <DL\n");
					continue;
				}
				h = top(&ps->hangstk, 0);
				if(h != 0)
					changehang(ps, -10*LISTTAB);
				else
					addbrk(ps, 0, 0);
				push(&ps->hangstk, 0);
				break;

			//<!ELEMENT (DIR|MENU) - - (LI)+ -(%block) >
			//<!ELEMENT (OL|UL) - - (LI)+>
			case Tdir:
			case Tmenu:
			case Tol:
			case Tul:
				changeindent(ps, LISTTAB);
				push(&ps->listtypestk, listtyval(tok, (tag==Tol)? LT1 : LTdisc));
				push(&ps->listcntstk, aintval(tok, Astart, 1));
				break;

			case Tdir+RBRA:
			case Tmenu+RBRA:
			case Tol+RBRA:
			case Tul+RBRA:
				if(ps->listtypestk.n == 0) {
					if(warn)
						fprint(2, "warning: %T ended no list\n", tok);
					continue;
				}
				addbrk(ps, 0, 0);
				pop(&ps->listtypestk);
				pop(&ps->listcntstk);
				changeindent(ps, -LISTTAB);
				break;

			// <!ELEMENT DL - - (DT|DD)+ >
			case Tdl:
				changeindent(ps, LISTTAB);
				push(&ps->hangstk, 0);
				break;

			case Tdl+RBRA:
				if(ps->hangstk.n == 0) {
					if(warn)
						fprint(2, "warning: unexpected </DL>\n");
					continue;
				}
				changeindent(ps, -LISTTAB);
				if(top(&ps->hangstk, 0) != 0)
					changehang(ps, -10*LISTTAB);
				pop(&ps->hangstk);
				break;

			// <!ELEMENT DT - O (%text)* >
			case Tdt:
				if(ps->hangstk.n == 0) {
					if(warn)
						fprint(2, "warning: <DT> not inside <DL>\n");
					continue;
				}
				h = top(&ps->hangstk, 0);
				pop(&ps->hangstk);
				if(h != 0)
					changehang(ps, -10*LISTTAB);
				changehang(ps, 10*LISTTAB);
				push(&ps->hangstk, 1);
				break;

			// <!ELEMENT FONT - - (%text)*>
			case Tfont:
				sz = top(&ps->fntsizestk, Normal);
				if(_tokaval(tok, Asize, &nsz, 0)) {
					if(_prefix(L"+", nsz))
						sz = Normal + _Strtol(nsz+1, nil, 10) + ps->adjsize;
					else if(_prefix(L"-", nsz))
						sz = Normal - _Strtol(nsz+1, nil, 10) + ps->adjsize;
					else if(nsz != nil)
						sz = Normal + (_Strtol(nsz, nil, 10) - 3);
				}
				ps->curfg = push(&ps->fgstk, acolorval(tok, Acolor, ps->curfg));
				pushfontsize(ps, sz);
				break;

			case Tfont+RBRA:
				if(ps->fgstk.n == 0) {
					if(warn)
						fprint(2, "warning: unexpected </FONT>\n");
					continue;
				}
				ps->curfg = popretnewtop(&ps->fgstk, di->text);
				popfontsize(ps);
				break;

			// <!ELEMENT FORM - - %body.content -(FORM) >
			case Tform:
				if(is->curform != nil) {
					if(warn)
						fprint(2, "warning: <FORM> nested inside another\n");
					continue;
				}
				action = aurlval(tok, Aaction, di->base, di->base);
				s = aval(tok, Aid);
				name = astrval(tok, Aname, s);
				if(s)
					free(s);
				target = atargval(tok, di->target);
				method = atabval(tok, Amethod, method_tab, NMETHODTAB, HGet);
				if(warn && _tokaval(tok, Aenctype, &enctype, 0) &&
						_Strcmp(enctype, L"application/x-www-form-urlencoded"))
					fprint(2, "form enctype %S not handled\n", enctype);
				frm = newform(++is->nforms, name, action, target, method, di->forms);
				di->forms = frm;
				is->curform = frm;
				break;

			case Tform+RBRA:
				if(is->curform == nil) {
					if(warn)
						fprint(2, "warning: unexpected </FORM>\n");
					continue;
				}
				is->curform = nil;
				break;

			// <!ELEMENT FRAME - O EMPTY>
			case Tframe:
				ks = is->kidstk;
				if(ks == nil) {
					if(warn)
						fprint(2, "warning: <FRAME> not in <FRAMESET>\n");
					continue;
				}
				ks->kidinfos = kd = newkidinfo(0, ks->kidinfos);
				kd->src = aurlval(tok, Asrc, nil, di->base);
				kd->name = aval(tok, Aname);
				if(kd->name == nil)
					kd->name = runesmprint("_fr%d", ++is->nframes);
				kd->marginw = auintval(tok, Amarginwidth, 0);
				kd->marginh = auintval(tok, Amarginheight, 0);
				kd->framebd = auintval(tok, Aframeborder, 1);
				kd->flags = atabval(tok, Ascrolling, fscroll_tab, NFSCROLLTAB, kd->flags);
				norsz = aflagval(tok, Anoresize);
				if(norsz)
					kd->flags |= FRnoresize;
				break;

			// <!ELEMENT FRAMESET - - (FRAME|FRAMESET)+>
			case Tframeset:
				ks = newkidinfo(1, nil);
				pks = is->kidstk;
				if(pks == nil)
					di->kidinfo = ks;
				else  {
					ks->next = pks->kidinfos;
					pks->kidinfos = ks;
				}
				ks->nextframeset = pks;
				is->kidstk = ks;
				setdimarray(tok, Arows, &ks->rows, &ks->nrows);
				if(ks->nrows == 0) {
					ks->rows = (Dimen*)emalloc(sizeof(Dimen));
					ks->nrows = 1;
					ks->rows[0] = makedimen(Dpercent, 100);
				}
				setdimarray(tok, Acols, &ks->cols, &ks->ncols);
				if(ks->ncols == 0) {
					ks->cols = (Dimen*)emalloc(sizeof(Dimen));
					ks->ncols = 1;
					ks->cols[0] = makedimen(Dpercent, 100);
				}
				break;

			case Tframeset+RBRA:
				if(is->kidstk == nil) {
					if(warn)
						fprint(2, "warning: unexpected </FRAMESET>\n");
					continue;
				}
				ks = is->kidstk;
				// put kids back in original order
				// and add blank frames to fill out cells
				n = ks->nrows*ks->ncols;
				nblank = n - _listlen((List*)ks->kidinfos);
				while(nblank-- > 0)
					ks->kidinfos = newkidinfo(0, ks->kidinfos);
				ks->kidinfos = (Kidinfo*)_revlist((List*)ks->kidinfos);
				is->kidstk = is->kidstk->nextframeset;
				if(is->kidstk == nil) {
					// end input
					ans = nil;
					goto return_ans;
				}
				break;

			// <!ELEMENT H1 - - (%text;)*>, etc.
			case Th1:
			case Th2:
			case Th3:
			case Th4:
			case Th5:
			case Th6:
				bramt = 1;
				if(ps->items == ps->lastit)
					bramt = 0;
				addbrk(ps, bramt, IFcleft|IFcright);
				sz = Verylarge - (tag - Th1);
				if(sz < Tiny)
					sz = Tiny;
				pushfontsize(ps, sz);
				sty = top(&ps->fntstylestk, FntR);
				if(tag == Th1)
					sty = FntB;
				pushfontstyle(ps, sty);
				pushjust(ps, atabval(tok, Aalign, align_tab, NALIGNTAB, ps->curjust));
				ps->skipwhite = 1;
				break;

			case Th1+RBRA:
			case Th2+RBRA:
			case Th3+RBRA:
			case Th4+RBRA:
			case Th5+RBRA:
			case Th6+RBRA:
				addbrk(ps, 1, IFcleft|IFcright);
				popfontsize(ps);
				popfontstyle(ps);
				popjust(ps);
				break;

			case Thead:
				// HTML spec says ignore regular markup in head,
				// but Netscape and IE don't
				// ps.skipping = 1;
				break;

			case Thead+RBRA:
				ps->skipping = 0;
				break;

			// <!ELEMENT HR - O EMPTY>
			case Thr:
				al = atabval(tok, Aalign, align_tab, NALIGNTAB, ALcenter);
				sz = auintval(tok, Asize, HRSZ);
				wd = adimen(tok, Awidth);
				if(dimenkind(wd) == Dnone)
					wd = makedimen(Dpercent, 100);
				nosh = aflagval(tok, Anoshade);
				color = acolorval(tok, Acolor, 0);
				additem(ps, newirule(al, sz, nosh, color, wd), tok);
				addbrk(ps, 0, 0);
				break;

			case Ti:
			case Tcite:
			case Tdfn:
			case Tem:
			case Tvar:
			case Taddress:
				pushfontstyle(ps, FntI);
				break;

			// <!ELEMENT IMG - O EMPTY>
			case Timg:
				map = nil;
				oldcuranchor = ps->curanchor;
				if(_tokaval(tok, Ausemap, &usemap, 0)) {
					if(!_prefix(L"#", usemap)) {
						if(warn)
							fprint(2, "warning: can't handle non-local map %S\n", usemap);
					}
					else {
						map = getmap(di, usemap+1);
						if(ps->curanchor == 0) {
							di->anchors = newanchor(++is->nanchors, nil, nil, di->target, di->anchors);
							ps->curanchor = is->nanchors;
						}
					}
				}
				align = atabval(tok, Aalign, align_tab, NALIGNTAB, ALbottom);
				dfltbd = 0;
				if(ps->curanchor != 0)
					dfltbd = 2;
				src = aurlval(tok, Asrc, nil, di->base);
				if(src == nil) {
					if(warn)
						fprint(2, "warning: <img> has no src attribute\n");
					ps->curanchor = oldcuranchor;
					continue;
				}
				img = newiimage(src,
						aval(tok, Aalt),
						align,
						auintval(tok, Awidth, 0),
						auintval(tok, Aheight, 0),
						auintval(tok, Ahspace, IMGHSPACE),
						auintval(tok, Avspace, IMGVSPACE),
						auintval(tok, Aborder, dfltbd),
						aflagval(tok, Aismap),
						map);
				if(align == ALleft || align == ALright) {
					additem(ps, newifloat(img, align), tok);
					// if no hspace specified, use FLTIMGHSPACE
					if(!_tokaval(tok, Ahspace, &val, 0))
						((Iimage*)img)->hspace = FLTIMGHSPACE;
				}
				else {
					ps->skipwhite = 0;
					additem(ps, img, tok);
				}
				ps->curanchor = oldcuranchor;
				break;

			// <!ELEMENT INPUT - O EMPTY>
			case Tinput:
				ps->skipwhite = 0;
				if(is->curform == nil) {
					if(warn)
						fprint(2, "<INPUT> not inside <FORM>\n");
					continue;
				}
				field = newformfield(
						atabval(tok, Atype, input_tab, NINPUTTAB, Ftext),
						++is->curform->nfields,
						is->curform,
						aval(tok, Aname),
						aval(tok, Avalue),
						auintval(tok, Asize, 0),
						auintval(tok, Amaxlength, 1000),
						nil);
				if(aflagval(tok, Achecked))
					field->flags = FFchecked;

				switch(field->ftype) {
				case Ftext:
				case Fpassword:
				case Ffile:
					if(field->size == 0)
						field->size = 20;
					break;

				case Fcheckbox:
					if(field->name == nil) {
						if(warn)
							fprint(2, "warning: checkbox form field missing name\n");
						continue;
					}
					if(field->value == nil)
						field->value = _Strdup(L"1");
					break;

				case Fradio:
					if(field->name == nil || field->value == nil) {
						if(warn)
							fprint(2, "warning: radio form field missing name or value\n");
						continue;
					}
					break;

				case Fsubmit:
					if(field->value == nil)
						field->value = _Strdup(L"Submit");
					if(field->name == nil)
						field->name = _Strdup(L"_no_name_submit_");
					break;

				case Fimage:
					src = aurlval(tok, Asrc, nil, di->base);
					if(src == nil) {
						if(warn)
							fprint(2, "warning: image form field missing src\n");
						continue;
					}
					// width and height attrs aren't specified in HTML 3.2,
					// but some people provide them and they help avoid
					// a relayout
					field->image = newiimage(src,
						astrval(tok, Aalt, L"Submit"),
						atabval(tok, Aalign, align_tab, NALIGNTAB, ALbottom),
						auintval(tok, Awidth, 0), auintval(tok, Aheight, 0),
						0, 0, 0, 0, nil);
					break;

				case Freset:
					if(field->value == nil)
						field->value = _Strdup(L"Reset");
					break;

				case Fbutton:
					if(field->value == nil)
						field->value = _Strdup(L" ");
					break;
				}
				ffit = newiformfield(field);
				additem(ps, ffit, tok);
				if(ffit->genattr != nil)
					field->events = ffit->genattr->events;
				break;

			// <!ENTITY ISINDEX - O EMPTY>
			case Tisindex:
				ps->skipwhite = 0;
				prompt = astrval(tok, Aprompt, L"Index search terms:");
				target = atargval(tok, di->target);
				additem(ps, textit(ps, prompt), tok);
				frm = newform(++is->nforms,
						nil,
						_Strdup(di->base),
						target,
						HGet,
						di->forms);
				di->forms = frm;
				ff = newformfield(Ftext,
						++frm->nfields,
						frm,
						_Strdup(L"_ISINDEX_"),
						nil,
						50,
						1000,
						nil);
				additem(ps, newiformfield(ff), tok);
				addbrk(ps, 1, 0);
				break;

			// <!ELEMENT LI - O %flow>
			case Tli:
				if(ps->listtypestk.n == 0) {
					if(warn)
						fprint(2, "<LI> not in list\n");
					continue;
				}
				ty = top(&ps->listtypestk, 0);
				ty2 = listtyval(tok, ty);
				if(ty != ty2) {
					ty = ty2;
					push(&ps->listtypestk, ty2);
				}
				v = aintval(tok, Avalue, top(&ps->listcntstk, 1));
				if(ty == LTdisc || ty == LTsquare || ty == LTcircle)
					hang = 10*LISTTAB - 3;
				else
					hang = 10*LISTTAB - 1;
				changehang(ps, hang);
				addtext(ps, listmark(ty, v));
				push(&ps->listcntstk, v + 1);
				changehang(ps, -hang);
				ps->skipwhite = 1;
				break;

			// <!ELEMENT MAP - - (AREA)+>
			case Tmap:
				if(_tokaval(tok, Aname, &name, 0))
					is->curmap = getmap(di, name);
				break;

			case Tmap+RBRA:
				map = is->curmap;
				if(map == nil) {
					if(warn)
						fprint(2, "warning: unexpected </MAP>\n");
					continue;
				}
				map->areas = (Area*)_revlist((List*)map->areas);
				break;

			case Tmeta:
				if(ps->skipping)
					continue;
				if(_tokaval(tok, Ahttp_equiv, &equiv, 0)) {
					val = aval(tok, Acontent);
					n = _Strlen(equiv);
					if(!_Strncmpci(equiv, n, L"refresh"))
						di->refresh = val;
					else if(!_Strncmpci(equiv, n, L"content-script-type")) {
						n = _Strlen(val);
						if(!_Strncmpci(val, n, L"javascript")
						   || !_Strncmpci(val, n, L"jscript1.1")
						   || !_Strncmpci(val, n, L"jscript"))
							di->scripttype = TextJavascript;
						else {
							if(warn)
								fprint(2, "unimplemented script type %S\n", val);
							di->scripttype = UnknownType;
						}
					}
				}
				break;

			// Nobr is NOT in HMTL 4.0, but it is ubiquitous on the web
			case Tnobr:
				ps->skipwhite = 0;
				ps->curstate &= ~IFwrap;
				break;

			case Tnobr+RBRA:
				ps->curstate |= IFwrap;
				break;

			// We do frames, so skip stuff in noframes
			case Tnoframes:
				ps->skipping = 1;
				break;

			case Tnoframes+RBRA:
				ps->skipping = 0;
				break;

			// We do scripts (if enabled), so skip stuff in noscripts
			case Tnoscript:
				if(doscripts)
					ps->skipping = 1;
				break;

			case Tnoscript+RBRA:
				if(doscripts)
					ps->skipping = 0;
				break;

			// <!ELEMENT OPTION - O (	//PCDATA)>
			case Toption:
				if(is->curform == nil || is->curform->fields == nil) {
					if(warn)
						fprint(2, "warning: <OPTION> not in <SELECT>\n");
					continue;
				}
				field = is->curform->fields;
				if(field->ftype != Fselect) {
					if(warn)
						fprint(2, "warning: <OPTION> not in <SELECT>\n");
					continue;
				}
				val = aval(tok, Avalue);
				option = newoption(aflagval(tok, Aselected), val, nil, field->options);
				field->options = option;
				option->display =  getpcdata(toks, tokslen, &toki);
				if(val == nil)
					option->value = _Strdup(option->display);
				break;

			// <!ELEMENT P - O (%text)* >
			case Tp:
				pushjust(ps, atabval(tok, Aalign, align_tab, NALIGNTAB, ps->curjust));
				ps->inpar = 1;
				ps->skipwhite = 1;
				break;

			case Tp+RBRA:
				break;

			// <!ELEMENT PARAM - O EMPTY>
			// Do something when we do applets...
			case Tparam:
				break;

			// <!ELEMENT PRE - - (%text)* -(IMG|BIG|SMALL|SUB|SUP|FONT) >
			case Tpre:
				ps->curstate &= ~IFwrap;
				ps->literal = 1;
				ps->skipwhite = 0;
				pushfontstyle(ps, FntT);
				break;

			case Tpre+RBRA:
				ps->curstate |= IFwrap;
				if(ps->literal) {
					popfontstyle(ps);
					ps->literal = 0;
				}
				break;

			// <!ELEMENT SCRIPT - - CDATA>
			case Tscript:
				if(doscripts) {
					if(!di->hasscripts) {
						if(di->scripttype == TextJavascript) {
							// TODO: initialize script if nec.
							// initjscript(di);
							di->hasscripts = 1;
						}
					}
				}
				if(!di->hasscripts) {
					if(warn)
						fprint(2, "warning: <SCRIPT> ignored\n");
					ps->skipping = 1;
				}
				else {
					scriptsrc = aurlval(tok, Asrc, nil, di->base);
					script = nil;
					if(scriptsrc != nil) {
						if(warn)
							fprint(2, "warning: non-local <SCRIPT> ignored\n");
						free(scriptsrc);
					}
					else {
						script = getpcdata(toks, tokslen, &toki);
					}
					if(script != nil) {
						if(warn)
							fprint(2, "script ignored\n");
						free(script);
					}
				}
				break;

			case Tscript+RBRA:
				ps->skipping = 0;
				break;

			// <!ELEMENT SELECT - - (OPTION+)>
			case Tselect:
				if(is->curform == nil) {
					if(warn)
						fprint(2, "<SELECT> not inside <FORM>\n");
					continue;
				}
				is->curfield = field = newformfield(Fselect,
					++is->curform->nfields,
					is->curform,
					aval(tok, Aname),
					nil,
					auintval(tok, Asize, 0),
					0,
					nil);
				if(aflagval(tok, Amultiple))
					field->flags = FFmultiple;
				ffit = newiformfield(field);
				additem(ps, ffit, tok);
				if(ffit->genattr != nil)
					field->events = ffit->genattr->events;
				// throw away stuff until next tag (should be <OPTION>)
				s = getpcdata(toks, tokslen, &toki);
				if(s != nil)
					free(s);
				break;

			case Tselect+RBRA:
				if(is->curform == nil || is->curfield == nil) {
					if(warn)
						fprint(2, "warning: unexpected </SELECT>\n");
					continue;
				}
				field = is->curfield;
				if(field->ftype != Fselect)
					continue;
				// put options back in input order
				field->options = (Option*)_revlist((List*)field->options);
				is->curfield = nil;
				break;

			// <!ELEMENT (STRIKE|U) - - (%text)*>
			case Tstrike:
			case Tu:
				ps->curul = push(&ps->ulstk, (tag==Tstrike)? ULmid : ULunder);
				break;

			case Tstrike+RBRA:
			case Tu+RBRA:
				if(ps->ulstk.n == 0) {
					if(warn)
						fprint(2, "warning: unexpected %T\n", tok);
					continue;
				}
				ps->curul = popretnewtop(&ps->ulstk, ULnone);
				break;

			// <!ELEMENT STYLE - - CDATA>
			case Tstyle:
				if(warn)
					fprint(2, "warning: unimplemented <STYLE>\n");
				ps->skipping = 1;
				break;

			case Tstyle+RBRA:
				ps->skipping = 0;
				break;

			// <!ELEMENT (SUB|SUP) - - (%text)*>
			case Tsub:
			case Tsup:
				if(tag == Tsub)
					ps->curvoff += SUBOFF;
				else
					ps->curvoff -= SUPOFF;
				push(&ps->voffstk, ps->curvoff);
				sz = top(&ps->fntsizestk, Normal);
				pushfontsize(ps, sz - 1);
				break;

			case Tsub+RBRA:
			case Tsup+RBRA:
				if(ps->voffstk.n == 0) {
					if(warn)
						fprint(2, "warning: unexpected %T\n", tok);
					continue;
				}
				ps->curvoff = popretnewtop(&ps->voffstk, 0);
				popfontsize(ps);
				break;

			// <!ELEMENT TABLE - - (CAPTION?, TR+)>
			case Ttable:
				ps->skipwhite = 0;
				tab = newtable(++is->ntables,
						aalign(tok),
						adimen(tok, Awidth),
						aflagval(tok, Aborder), 
						auintval(tok, Acellspacing, TABSP),
						auintval(tok, Acellpadding, TABPAD),
						makebackground(nil, acolorval(tok, Abgcolor, ps->curbg.color)),
						tok,
						is->tabstk);
				is->tabstk = tab;
				curtab = tab;
				break;

			case Ttable+RBRA:
				if(curtab == nil) {
					if(warn)
						fprint(2, "warning: unexpected </TABLE>\n");
					continue;
				}
				isempty = (curtab->cells == nil);
				if(isempty) {
					if(warn)
						fprint(2, "warning: <TABLE> has no cells\n");
				}
				else {
					ps = finishcell(curtab, ps);
					if(curtab->rows != nil)
						curtab->rows->flags = 0;
					finish_table(curtab);
				}
				ps->skipping = 0;
				if(!isempty) {
					tabitem = newitable(curtab);
					al = curtab->align.halign;
					switch(al) {
					case ALleft:
					case ALright:
						additem(ps, newifloat(tabitem, al), tok);
						break;
					default:
						if(al == ALcenter)
							pushjust(ps, ALcenter);
						addbrk(ps, 0, 0);
						if(ps->inpar) {
							popjust(ps);
							ps->inpar = 0;
						}
						additem(ps, tabitem, curtab->tabletok);
						if(al == ALcenter)
							popjust(ps);
						break;
					}
				}
				if(is->tabstk == nil) {
					if(warn)
						fprint(2, "warning: table stack is wrong\n");
				}
				else
					is->tabstk = is->tabstk->next;
				curtab = is->tabstk;
				if(!isempty)
					addbrk(ps, 0, 0);
				break;

			// <!ELEMENT (TH|TD) - O %body.content>
			// Cells for a row are accumulated in reverse order.
			// We push ps on a stack, and use a new one to accumulate
			// the contents of the cell.
			case Ttd:
			case Tth:
				if(curtab == nil) {
					if(warn)
						fprint(2, "%T outside <TABLE>\n", tok);
					continue;
				}
				if(ps->inpar) {
					popjust(ps);
					ps->inpar = 0;
				}
				ps = finishcell(curtab, ps);
				tr = nil;
				if(curtab->rows != nil)
					tr = curtab->rows;
				if(tr == nil || !tr->flags) {
					if(warn)
						fprint(2, "%T outside row\n", tok);
					tr = newtablerow(makealign(ALnone, ALnone),
							makebackground(nil, curtab->background.color),
							TFparsing,
							curtab->rows);
					curtab->rows = tr;
				}
				ps = cell_pstate(ps, tag == Tth);
				flags = TFparsing;
				if(aflagval(tok, Anowrap)) {
					flags |= TFnowrap;
					ps->curstate &= ~IFwrap;
				}
				if(tag == Tth)
					flags |= TFisth;
				c = newtablecell(curtab->cells==nil? 1 : curtab->cells->cellid+1,
						auintval(tok, Arowspan, 1),
						auintval(tok, Acolspan, 1), 
						aalign(tok), 
						adimen(tok, Awidth),
						auintval(tok, Aheight, 0),
						makebackground(nil, acolorval(tok, Abgcolor, tr->background.color)),
						flags,
						curtab->cells);
				curtab->cells = c;
				ps->curbg = c->background;
				if(c->align.halign == ALnone) {
					if(tr->align.halign != ALnone)
						c->align.halign = tr->align.halign;
					else if(tag == Tth)
						c->align.halign = ALcenter;
					else
						c->align.halign = ALleft;
				}
				if(c->align.valign == ALnone) {
					if(tr->align.valign != ALnone)
						c->align.valign = tr->align.valign;
					else
						c->align.valign = ALmiddle;
				}
				c->nextinrow = tr->cells;
				tr->cells = c;
				break;

			case Ttd+RBRA:
			case Tth+RBRA:
				if(curtab == nil || curtab->cells == nil) {
					if(warn)
						fprint(2, "unexpected %T\n", tok);
					continue;
				}
				ps = finishcell(curtab, ps);
				break;

			// <!ELEMENT TEXTAREA - - (	//PCDATA)>
			case Ttextarea:
				if(is->curform == nil) {
					if(warn)
						fprint(2, "<TEXTAREA> not inside <FORM>\n");
					continue;
				}
				field = newformfield(Ftextarea,
					++is->curform->nfields,
					is->curform,
					aval(tok, Aname),
					nil,
					0,
					0,
					nil);
				field->rows = auintval(tok, Arows, 3);
				field->cols = auintval(tok, Acols, 50);
				field->value = getpcdata(toks, tokslen, &toki);
				if(warn && toki < tokslen - 1 && toks[toki + 1].tag != Ttextarea + RBRA)
					fprint(2, "warning: <TEXTAREA> data ended by %T\n", &toks[toki + 1]);
				ffit = newiformfield(field);
				additem(ps, ffit, tok);
				if(ffit->genattr != nil)
					field->events = ffit->genattr->events;
				break;

			// <!ELEMENT TITLE - - (	//PCDATA)* -(%head.misc)>
			case Ttitle:
				di->doctitle = getpcdata(toks, tokslen, &toki);
				if(warn && toki < tokslen - 1 && toks[toki + 1].tag != Ttitle + RBRA)
					fprint(2, "warning: <TITLE> data ended by %T\n", &toks[toki + 1]);
				break;

			// <!ELEMENT TR - O (TH|TD)+>
			// rows are accumulated in reverse order in curtab->rows
			case Ttr:
				if(curtab == nil) {
					if(warn)
						fprint(2, "warning: <TR> outside <TABLE>\n");
					continue;
				}
				if(ps->inpar) {
					popjust(ps);
					ps->inpar = 0;
				}
				ps = finishcell(curtab, ps);
				if(curtab->rows != nil)
					curtab->rows->flags = 0;
				curtab->rows = newtablerow(aalign(tok),
					makebackground(nil, acolorval(tok, Abgcolor, curtab->background.color)),
					TFparsing,
					curtab->rows);
				break;

			case Ttr+RBRA:
				if(curtab == nil || curtab->rows == nil) {
					if(warn)
						fprint(2, "warning: unexpected </TR>\n");
					continue;
				}
				ps = finishcell(curtab, ps);
				tr = curtab->rows;
				if(tr->cells == nil) {
					if(warn)
						fprint(2, "warning: empty row\n");
					curtab->rows = tr->next;
					tr->next = nil;
					free(tr);
				}
				else
					tr->flags = 0;
				break;

			// <!ELEMENT (TT|CODE|KBD|SAMP) - - (%text)*>
			case Ttt:
			case Tcode:
			case Tkbd:
			case Tsamp:
				pushfontstyle(ps, FntT);
				break;

			// Tags that have empty action
			case Tabbr:
			case Tabbr+RBRA:
			case Tacronym:
			case Tacronym+RBRA:
			case Tarea+RBRA:
			case Tbase+RBRA:
			case Tbasefont+RBRA:
			case Tbr+RBRA:
			case Tdd+RBRA:
			case Tdt+RBRA:
			case Tframe+RBRA:
			case Thr+RBRA:
			case Thtml:
			case Thtml+RBRA:
			case Timg+RBRA:
			case Tinput+RBRA:
			case Tisindex+RBRA:
			case Tli+RBRA:
			case Tlink:
			case Tlink+RBRA:
			case Tmeta+RBRA:
			case Toption+RBRA:
			case Tparam+RBRA:
			case Ttextarea+RBRA:
			case Ttitle+RBRA:
				break;


			// Tags not implemented
			case Tbdo:
			case Tbdo+RBRA:
			case Tbutton:
			case Tbutton+RBRA:
			case Tdel:
			case Tdel+RBRA:
			case Tfieldset:
			case Tfieldset+RBRA:
			case Tiframe:
			case Tiframe+RBRA:
			case Tins:
			case Tins+RBRA:
			case Tlabel:
			case Tlabel+RBRA:
			case Tlegend:
			case Tlegend+RBRA:
			case Tobject:
			case Tobject+RBRA:
			case Toptgroup:
			case Toptgroup+RBRA:
			case Tspan:
			case Tspan+RBRA:
				if(warn) {
					if(tag > RBRA)
						tag -= RBRA;
					fprint(2, "warning: unimplemented HTML tag: %S\n", tagnames[tag]);
				}
				break;

			default:
				if(warn)
					fprint(2, "warning: unknown HTML tag: %S\n", tok->text);
				break;
			}
	}
	// some pages omit trailing </table>
	while(curtab != nil) {
		if(warn)
			fprint(2, "warning: <TABLE> not closed\n");
		if(curtab->cells != nil) {
			ps = finishcell(curtab, ps);
			if(curtab->cells == nil) {
				if(warn)
					fprint(2, "warning: empty table\n");
			}
			else {
				if(curtab->rows != nil)
					curtab->rows->flags = 0;
				finish_table(curtab);
				ps->skipping = 0;
				additem(ps, newitable(curtab), curtab->tabletok);
				addbrk(ps, 0, 0);
			}
		}
		if(is->tabstk != nil)
			is->tabstk = is->tabstk->next;
		curtab = is->tabstk;
	}
	outerps = lastps(ps);
	ans = outerps->items->next;
	freeitem(outerps->items);
	// note: ans may be nil and di->kids not nil, if there's a frameset!
	outerps->items = newispacer(ISPnull);
	outerps->lastit = outerps->items;
	outerps->prelastit = nil;
	is->psstk = ps;
	if(ans != nil && di->hasscripts) {
		// TODO evalscript(nil);
		;
	}

return_ans:
	if(dbgbuild) {
		assert(validitems(ans));
		if(ans == nil)
			fprint(2, "getitems returning nil\n");
		else
			printitems(ans, "getitems returning:");
	}
	linkitems(di, ans);
	_freetokens(toks, tokslen);
	return ans;
}

// Concatenate together maximal set of Data tokens, starting at toks[toki+1].
// Lexer has ensured that there will either be a following non-data token or
// we will be at eof.
// Return emallocd trimmed concatenation, and update *ptoki to last used toki
static Rune*
getpcdata(Token* toks, int tokslen, int* ptoki)
{
	Rune*	ans;
	Rune*	p;
	Rune*	trimans;
	int	anslen;
	int	trimanslen;
	int	toki;
	Token*	tok;

	ans = nil;
	anslen = 0;
	// first find length of answer
	toki = (*ptoki) + 1;
	while(toki < tokslen) {
		tok = &toks[toki];
		if(tok->tag == Data) {
			toki++;
			anslen += _Strlen(tok->text);
		}
		else
			break;
	}
	// now make up the initial answer
	if(anslen > 0) {
		ans = _newstr(anslen);
		p = ans;
		toki = (*ptoki) + 1;
		while(toki < tokslen) {
			tok = &toks[toki];
			if(tok->tag == Data) {
				toki++;
				p = _Stradd(p, tok->text, _Strlen(tok->text));
			}
			else
				break;
		}
		*p = 0;
		_trimwhite(ans, anslen, &trimans, &trimanslen);
		if(trimanslen != anslen) {
			p = ans;
			ans = _Strndup(trimans, trimanslen);
			free(p);
		}
	}
	*ptoki = toki-1;
	return ans;
}

// If still parsing head of curtab->cells list, finish it off
// by transferring the items on the head of psstk to the cell.
// Then pop the psstk and return the new psstk.
static Pstate*
finishcell(Table* curtab, Pstate* psstk)
{
	Tablecell*	c;
	Pstate* psstknext;

	c = curtab->cells;
	if(c != nil) {
		if((c->flags&TFparsing)) {
			psstknext = psstk->next;
			if(psstknext == nil) {
				if(warn)
					fprint(2, "warning: parse state stack is wrong\n");
			}
			else {
				if(c->content != nil)
					freeitems(c->content);
				c->content = psstk->items->next;
				psstk->items->next = nil;
				c->flags &= ~TFparsing;
				freepstate(psstk);
				psstk = psstknext;
			}
		}
	}
	return psstk;
}

// Make a new Pstate for a cell, based on the old pstate, oldps.
// Also, put the new ps on the head of the oldps stack.
static Pstate*
cell_pstate(Pstate* oldps, int ishead)
{
	Pstate*	ps;
	int	sty;

	ps = newpstate(oldps);
	ps->skipwhite = 1;
	ps->curanchor = oldps->curanchor;
	copystack(&ps->fntstylestk, &oldps->fntstylestk);
	copystack(&ps->fntsizestk, &oldps->fntsizestk);
	ps->curfont = oldps->curfont;
	ps->curfg = oldps->curfg;
	ps->curbg = oldps->curbg;
	copystack(&ps->fgstk, &oldps->fgstk);
	ps->adjsize = oldps->adjsize;
	if(ishead) {
		sty = ps->curfont%NumSize;
		ps->curfont = FntB*NumSize + sty;
	}
	return ps;
}

// Return a new Pstate with default starting state.
// Use link to add it to head of a list, if any.
static Pstate*
newpstate(Pstate* link)
{
	Pstate*	ps;

	ps = (Pstate*)emalloc(sizeof(Pstate));
	ps->curfont = DefFnt;
	ps->curfg = Black;
	ps->curbg.image = nil;
	ps->curbg.color = White;
	ps->curul = ULnone;
	ps->curjust = ALleft;
	ps->curstate = IFwrap;
	ps->items = newispacer(ISPnull);
	ps->lastit = ps->items;
	ps->prelastit = nil;
	ps->next = link;
	return ps;
}

// Return last Pstate on psl list
static Pstate*
lastps(Pstate* psl)
{
	assert(psl != nil);
	while(psl->next != nil)
		psl = psl->next;
	return psl;
}

// Add it to end of ps item chain, adding in current state from ps.
// Also, if tok is not nil, scan it for generic attributes and assign
// the genattr field of the item accordingly.
static void
additem(Pstate* ps, Item* it, Token* tok)
{
	int	aid;
	int	any;
	Rune*	i;
	Rune*	c;
	Rune*	s;
	Rune*	t;
	Attr*	a;
	SEvent*	e;

	if(ps->skipping) {
		if(warn)
			fprint(2, "warning: skipping item: %I\n", it);
		freeitem(it);
		return;
	}
	it->anchorid = ps->curanchor;
	it->state |= ps->curstate;
	if(tok != nil) {
		any = 0;
		i = nil;
		c = nil;
		s = nil;
		t = nil;
		e = nil;
		for(a = tok->attr; a != nil; a = a->next) {
			aid = a->attid;
			if(!attrinfo[aid])
				continue;
			switch(aid) {
			case Aid:
				i = a->value;
				break;

			case Aclass:
				c = a->value;
				break;

			case Astyle:
				s = a->value;
				break;

			case Atitle:
				t = a->value;
				break;

			default:
				assert(aid >= Aonblur && aid <= Aonunload);
				e = newscriptevent(scriptev[a->attid], a->value, e);
				break;
			}
			a->value = nil;
			any = 1;
		}
		if(any)
			it->genattr = newgenattr(i, c, s, t, e);
	}
	ps->curstate &= ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
	ps->prelastit = ps->lastit;
	ps->lastit->next = it;
	ps->lastit = it;
}

// Make a text item out of s,
// using current font, foreground, vertical offset and underline state.
static Item*
textit(Pstate* ps, Rune* s)
{
	assert(s != nil);
	return newitext(s, ps->curfont, ps->curfg, ps->curvoff + Voffbias, ps->curul);
}

// Add text item or items for s, paying attention to
// current font, foreground, baseline offset, underline state,
// and literal mode.  Unless we're in literal mode, compress
// whitespace to single blank, and, if curstate has a break,
// trim any leading whitespace.  Whether in literal mode or not,
// turn nonbreaking spaces into spacer items with IFnobrk set.
//
// In literal mode, break up s at newlines and add breaks instead.
// Also replace tabs appropriate number of spaces.
// In nonliteral mode, break up the items every 100 or so characters
// just to make the layout algorithm not go quadratic.
//
// addtext assumes ownership of s.
static void
addtext(Pstate* ps, Rune* s)
{
	int	n;
	int	i;
	int	j;
	int	k;
	int	col;
	int	c;
	int	nsp;
	Item*	it;
	Rune*	ss;
	Rune*	p;
	Rune	buf[SMALLBUFSIZE];

	assert(s != nil);
	n = runestrlen(s);
	i = 0;
	j = 0;
	if(ps->literal) {
		col = 0;
		while(i < n) {
			if(s[i] == '\n') {
				if(i > j) {
					// trim trailing blanks from line
					for(k = i; k > j; k--)
						if(s[k - 1] != ' ')
							break;
					if(k > j)
						additem(ps, textit(ps, _Strndup(s+j, k-j)), nil);
				}
				addlinebrk(ps, 0);
				j = i + 1;
				col = 0;
			}
			else {
				if(s[i] == '\t') {
					col += i - j;
					nsp = 8 - (col%8);
					// make ss = s[j:i] + nsp spaces
					ss = _newstr(i-j+nsp);
					p = _Stradd(ss, s+j, i-j);
					p = _Stradd(p, L"        ", nsp);
					*p = 0;
					additem(ps, textit(ps, ss), nil);
					col += nsp;
					j = i + 1;
				}
				else if(s[i] == NBSP) {
					if(i > j)
						additem(ps, textit(ps, _Strndup(s+j, i-j)), nil);
					addnbsp(ps);
					col += (i - j) + 1;
					j = i + 1;
				}
			}
			i++;
		}
		if(i > j) {
			if(j == 0 && i == n) {
				// just transfer s over
				additem(ps, textit(ps, s), nil);
			}
			else {
				additem(ps, textit(ps, _Strndup(s+j, i-j)), nil);
				free(s);
			}
		}
	}
	else {	// not literal mode
		if((ps->curstate&IFbrk) || ps->lastit == ps->items)
			while(i < n) {
				c = s[i];
				if(c >= 256 || !isspace(c))
					break;
				i++;
			}
		p = buf;
		for(j = i; i < n; i++) {
			assert(p+i-j < buf+SMALLBUFSIZE-1);
			c = s[i];
			if(c == NBSP) {
				if(i > j)
					p = _Stradd(p, s+j, i-j);
				if(p > buf)
					additem(ps, textit(ps, _Strndup(buf, p-buf)), nil);
				p = buf;
				addnbsp(ps);
				j = i + 1;
				continue;
			}
			if(c < 256 && isspace(c)) {
				if(i > j)
					p = _Stradd(p, s+j, i-j);
				*p++ = ' ';
				while(i < n - 1) {
					c = s[i + 1];
					if(c >= 256 || !isspace(c))
						break;
					i++;
				}
				j = i + 1;
			}
			if(i - j >= 100) {
				p = _Stradd(p, s+j, i+1-j);
				j = i + 1;
			}
			if(p-buf >= 100) {
				additem(ps, textit(ps, _Strndup(buf, p-buf)), nil);
				p = buf;
			}
		}
		if(i > j && j < n) {
			assert(p+i-j < buf+SMALLBUFSIZE-1);
			p = _Stradd(p, s+j, i-j);
		}
		// don't add a space if previous item ended in a space
		if(p-buf == 1 && buf[0] == ' ' && ps->lastit != nil) {
			it = ps->lastit;
			if(it->tag == Itexttag) {
				ss = ((Itext*)it)->s;
				k = _Strlen(ss);
				if(k > 0 && ss[k] == ' ')
					p = buf;
			}
		}
		if(p > buf)
			additem(ps, textit(ps, _Strndup(buf, p-buf)), nil);
		free(s);
	}
}

// Add a break to ps->curstate, with extra space if sp is true.
// If there was a previous break, combine this one's parameters
// with that to make the amt be the max of the two and the clr
// be the most general. (amt will be 0 or 1)
// Also, if the immediately preceding item was a text item,
// trim any whitespace from the end of it, if not in literal mode.
// Finally, if this is at the very beginning of the item list
// (the only thing there is a null spacer), then don't add the space.
static void
addbrk(Pstate* ps, int sp, int clr)
{
	int	state;
	Rune*	l;
	int		nl;
	Rune*	r;
	int		nr;
	Itext*	t;
	Rune*	s;

	state = ps->curstate;
	clr = clr|(state&(IFcleft|IFcright));
	if(sp && !(ps->lastit == ps->items))
		sp = IFbrksp;
	else
		sp = 0;
	ps->curstate = IFbrk|sp|(state&~(IFcleft|IFcright))|clr;
	if(ps->lastit != ps->items) {
		if(!ps->literal && ps->lastit->tag == Itexttag) {
			t = (Itext*)ps->lastit;
			_splitr(t->s, _Strlen(t->s), notwhitespace, &l, &nl, &r, &nr);
			// try to avoid making empty items
			// but not crucial f the occasional one gets through
			if(nl == 0 && ps->prelastit != nil) {
				freeitems(ps->lastit);
				ps->lastit = ps->prelastit;
				ps->lastit->next = nil;
				ps->prelastit = nil;
			}
			else {
				s = t->s;
				if(nl == 0) {
					// need a non-nil pointer to empty string
					// (_Strdup(L"") returns nil)
					t->s = emalloc(sizeof(Rune));
					t->s[0] = 0;
				}
				else
					t->s = _Strndup(l, nl);
				if(s)
					free(s);
			}
		}
	}
}

// Add break due to a <br> or a newline within a preformatted section.
// We add a null item first, with current font's height and ascent, to make
// sure that the current line takes up at least that amount of vertical space.
// This ensures that <br>s on empty lines cause blank lines, and that
// multiple <br>s in a row give multiple blank lines.
// However don't add the spacer if the previous item was something that
// takes up space itself.
static void
addlinebrk(Pstate* ps, int clr)
{
	int	obrkstate;
	int	b;

	// don't want break before our null item unless the previous item
	// was also a null item for the purposes of line breaking
	obrkstate = ps->curstate&(IFbrk|IFbrksp);
	b = IFnobrk;
	if(ps->lastit != nil) {
		if(ps->lastit->tag == Ispacertag) {
			if(((Ispacer*)ps->lastit)->spkind == ISPvline)
				b = IFbrk;
		}
	}
	ps->curstate = (ps->curstate&~(IFbrk|IFbrksp))|b;
	additem(ps, newispacer(ISPvline), nil);
	ps->curstate = (ps->curstate&~(IFbrk|IFbrksp))|obrkstate;
	addbrk(ps, 0, clr);
}

// Add a nonbreakable space
static void
addnbsp(Pstate* ps)
{
	// if nbsp comes right where a break was specified,
	// do the break anyway (nbsp is being used to generate undiscardable
	// space rather than to prevent a break)
	if((ps->curstate&IFbrk) == 0)
		ps->curstate |= IFnobrk;
	additem(ps, newispacer(ISPhspace), nil);
	// but definitely no break on next item
	ps->curstate |= IFnobrk;
}

// Change hang in ps.curstate by delta.
// The amount is in 1/10ths of tabs, and is the amount that
// the current contiguous set of items with a hang value set
// is to be shifted left from its normal (indented) place.
static void
changehang(Pstate* ps, int delta)
{
	int	amt;

	amt = (ps->curstate&IFhangmask) + delta;
	if(amt < 0) {
		if(warn)
			fprint(2, "warning: hang went negative\n");
		amt = 0;
	}
	ps->curstate = (ps->curstate&~IFhangmask)|amt;
}

// Change indent in ps.curstate by delta.
static void
changeindent(Pstate* ps, int delta)
{
	int	amt;

	amt = ((ps->curstate&IFindentmask) >> IFindentshift) + delta;
	if(amt < 0) {
		if(warn)
			fprint(2, "warning: indent went negative\n");
		amt = 0;
	}
	ps->curstate = (ps->curstate&~IFindentmask)|(amt << IFindentshift);
}

// Push val on top of stack, and also return value pushed
static int
push(Stack* stk, int val)
{
	if(stk->n == Nestmax) {
		if(warn)
			fprint(2, "warning: build stack overflow\n");
	}
	else
		stk->slots[stk->n++] = val;
	return val;
}

// Pop top of stack
static void
pop(Stack* stk)
{
	if(stk->n > 0)
		--stk->n;
}

//Return top of stack, using dflt if stack is empty
static int
top(Stack* stk, int dflt)
{
	if(stk->n == 0)
		return dflt;
	return stk->slots[stk->n-1];
}

// pop, then return new top, with dflt if empty
static int
popretnewtop(Stack* stk, int dflt)
{
	if(stk->n == 0)
		return dflt;
	stk->n--;
	if(stk->n == 0)
		return dflt;
	return stk->slots[stk->n-1];
}

// Copy fromstk entries into tostk
static void
copystack(Stack* tostk, Stack* fromstk)
{
	int n;

	n = fromstk->n;
	tostk->n = n;
	memmove(tostk->slots, fromstk->slots, n*sizeof(int));
}

static void
popfontstyle(Pstate* ps)
{
	pop(&ps->fntstylestk);
	setcurfont(ps);
}

static void
pushfontstyle(Pstate* ps, int sty)
{
	push(&ps->fntstylestk, sty);
	setcurfont(ps);
}

static void
popfontsize(Pstate* ps)
{
	pop(&ps->fntsizestk);
	setcurfont(ps);
}

static void
pushfontsize(Pstate* ps, int sz)
{
	push(&ps->fntsizestk, sz);
	setcurfont(ps);
}

static void
setcurfont(Pstate* ps)
{
	int	sty;
	int	sz;

	sty = top(&ps->fntstylestk, FntR);
	sz = top(&ps->fntsizestk, Normal);
	if(sz < Tiny)
		sz = Tiny;
	if(sz > Verylarge)
		sz = Verylarge;
	ps->curfont = sty*NumSize + sz;
}

static void
popjust(Pstate* ps)
{
	pop(&ps->juststk);
	setcurjust(ps);
}

static void
pushjust(Pstate* ps, int j)
{
	push(&ps->juststk, j);
	setcurjust(ps);
}

static void
setcurjust(Pstate* ps)
{
	int	j;
	int	state;

	j = top(&ps->juststk, ALleft);
	if(j != ps->curjust) {
		ps->curjust = j;
		state = ps->curstate;
		state &= ~(IFrjust|IFcjust);
		if(j == ALcenter)
			state |= IFcjust;
		else if(j == ALright)
			state |= IFrjust;
		ps->curstate = state;
	}
}

// Do final rearrangement after table parsing is finished
// and assign cells to grid points
static void
finish_table(Table* t)
{
	int	ncol;
	int	nrow;
	int	r;
	Tablerow*	rl;
	Tablecell*	cl;
	int*	rowspancnt;
	Tablecell**	rowspancell;
	int	ri;
	int	ci;
	Tablecell*	c;
	Tablecell*	cnext;
	Tablerow*	row;
	Tablerow*	rownext;
	int	rcols;
	int	newncol;
	int	k;
	int	j;
	int	cspan;
	int	rspan;
	int	i;

	rl = t->rows;
	t->nrow = nrow = _listlen((List*)rl);
	t->rows = (Tablerow*)emalloc(nrow * sizeof(Tablerow));
	ncol = 0;
	r = nrow - 1;
	for(row = rl; row != nil; row = rownext) {
		// copy the data from the allocated Tablerow into the array slot
		t->rows[r] = *row;
		rownext = row->next;
		free(row);
		row = &t->rows[r];
		r--;
		rcols = 0;
		c = row->cells;

		// If rowspan is > 1 but this is the last row,
		// reset the rowspan
		if(c != nil && c->rowspan > 1 && r == nrow-2)
			c->rowspan = 1;

		// reverse row->cells list (along nextinrow pointers)
		row->cells = nil;
		while(c != nil) {
			cnext = c->nextinrow;
			c->nextinrow = row->cells;
			row->cells = c;
			rcols += c->colspan;
			c = cnext;
		}
		if(rcols > ncol)
			ncol = rcols;
	}
	t->ncol = ncol;
	t->cols = (Tablecol*)emalloc(ncol * sizeof(Tablecol));

	// Reverse cells just so they are drawn in source order.
	// Also, trim their contents so they don't end in whitespace.
	t->cells = (Tablecell*)_revlist((List*)t->cells);
	for(c = t->cells; c != nil; c= c->next)
		trim_cell(c);
	t->grid = (Tablecell***)emalloc(nrow * sizeof(Tablecell**));
	for(i = 0; i < nrow; i++)
		t->grid[i] = (Tablecell**)emalloc(ncol * sizeof(Tablecell*));

	// The following arrays keep track of cells that are spanning
	// multiple rows;  rowspancnt[i] is the number of rows left
	// to be spanned in column i.
	// When done, cell's (row,col) is upper left grid point.
	rowspancnt = (int*)emalloc(ncol * sizeof(int));
	rowspancell = (Tablecell**)emalloc(ncol * sizeof(Tablecell*));
	for(ri = 0; ri < nrow; ri++) {
		row = &t->rows[ri];
		cl = row->cells;
		ci = 0;
		while(ci < ncol || cl != nil) {
			if(ci < ncol && rowspancnt[ci] > 0) {
				t->grid[ri][ci] = rowspancell[ci];
				rowspancnt[ci]--;
				ci++;
			}
			else {
				if(cl == nil) {
					ci++;
					continue;
				}
				c = cl;
				cl = cl->nextinrow;
				cspan = c->colspan;
				rspan = c->rowspan;
				if(ci + cspan > ncol) {
					// because of row spanning, we calculated
					// ncol incorrectly; adjust it
					newncol = ci + cspan;
					t->cols = (Tablecol*)erealloc(t->cols, newncol * sizeof(Tablecol));
					rowspancnt = (int*)erealloc(rowspancnt, newncol * sizeof(int));
					rowspancell = (Tablecell**)erealloc(rowspancell, newncol * sizeof(Tablecell*));
					k = newncol-ncol;
					memset(t->cols+ncol, 0, k*sizeof(Tablecol));
					memset(rowspancnt+ncol, 0, k*sizeof(int));
					memset(rowspancell+ncol, 0, k*sizeof(Tablecell*));
					for(j = 0; j < nrow; j++) {
						t->grid[j] = (Tablecell**)erealloc(t->grid[j], newncol * sizeof(Tablecell*));
						memset(t->grid[j], 0, k*sizeof(Tablecell*));
					}
					t->ncol = ncol = newncol;
				}
				c->row = ri;
				c->col = ci;
				for(i = 0; i < cspan; i++) {
					t->grid[ri][ci] = c;
					if(rspan > 1) {
						rowspancnt[ci] = rspan - 1;
						rowspancell[ci] = c;
					}
					ci++;
				}
			}
		}
	}
	free(rowspancnt);
	free(rowspancell);
}

// Remove tail of cell content until it isn't whitespace.
static void
trim_cell(Tablecell* c)
{
	int	dropping;
	Rune*	s;
	Rune*	x;
	Rune*	y;
	int		nx;
	int		ny;
	Item*	p;
	Itext*	q;
	Item*	pprev;

	dropping = 1;
	while(c->content != nil && dropping) {
		p = c->content;
		pprev = nil;
		while(p->next != nil) {
			pprev = p;
			p = p->next;
		}
		dropping = 0;
		if(!(p->state&IFnobrk)) {
			if(p->tag == Itexttag) {
				q = (Itext*)p;
				s = q->s;
				_splitr(s, _Strlen(s), notwhitespace, &x, &nx, &y, &ny);
				if(nx != 0 && ny != 0) {
					q->s = _Strndup(x, nx);
					free(s);
				}
				break;
			}
		}
		if(dropping) {
			if(pprev == nil)
				c->content = nil;
			else
				pprev->next = nil;
			freeitem(p);
		}
	}
}

// Caller must free answer (eventually).
static Rune*
listmark(uchar ty, int n)
{
	Rune*	s;
	Rune*	t;
	int	n2;
	int	i;

	s = nil;
	switch(ty) {
	case LTdisc:
	case LTsquare:
	case LTcircle:
		s = _newstr(1);
		s[0] = (ty == LTdisc)? 0x2022		// bullet
			: ((ty == LTsquare)? 0x220e	// filled square
			    : 0x2218);				// degree
		s[1] = 0;
		break;

	case LT1:
		s = runesmprint("%d.", n);
		break;

	case LTa:
	case LTA:
		n--;
		i = 0;
		if(n < 0)
			n = 0;
		s = _newstr((n <= 25)? 2 : 3);
		if(n > 25) {
			n2 = n%26;
			n /= 26;
			if(n2 > 25)
				n2 = 25;
			s[i++] = n2 + (ty == LTa)? 'a' : 'A';
		}
		s[i++] = n + (ty == LTa)? 'a' : 'A';
		s[i++] = '.';
		s[i] = 0;
		break;

	case LTi:
	case LTI:
		if(n >= NROMAN) {
			if(warn)
				fprint(2, "warning: unimplemented roman number > %d\n", NROMAN);
			n = NROMAN;
		}
		t = roman[n - 1];
		n2 = _Strlen(t);
		s = _newstr(n2+1);
		for(i = 0; i < n2; i++)
			s[i] = (ty == LTi)? tolower(t[i]) : t[i];
		s[i++] = '.';
		s[i] = 0;
		break;
	}
	return s;
}

// Find map with given name in di.maps.
// If not there, add one, copying name.
// Ownership of map remains with di->maps list.
static Map*
getmap(Docinfo* di, Rune* name)
{
	Map*	m;

	for(m = di->maps; m != nil; m = m->next) {
		if(!_Strcmp(name, m->name))
			return m;
	}
	m = (Map*)emalloc(sizeof(Map));
	m->name = _Strdup(name);
	m->areas = nil;
	m->next = di->maps;
	di->maps = m;
	return m;
}

// Transfers ownership of href to Area
static Area*
newarea(int shape, Rune* href, int target, Area* link)
{
	Area* a;

	a = (Area*)emalloc(sizeof(Area));
	a->shape = shape;
	a->href = href;
	a->target = target;
	a->next = link;
	return a;
}

// Return string value associated with attid in tok, nil if none.
// Caller must free the result (eventually).
static Rune*
aval(Token* tok, int attid)
{
	Rune*	ans;

	_tokaval(tok, attid, &ans, 1);	// transfers string ownership from token to ans
	return ans;
}

// Like aval, but use dflt if there was no such attribute in tok.
// Caller must free the result (eventually).
static Rune*
astrval(Token* tok, int attid, Rune* dflt)
{
	Rune*	ans;

	if(_tokaval(tok, attid, &ans, 1))
		return ans;	// transfers string ownership from token to ans
	else
		return _Strdup(dflt);
}

// Here we're supposed to convert to an int,
// and have a default when not found
static int
aintval(Token* tok, int attid, int dflt)
{
	Rune*	ans;

	if(!_tokaval(tok, attid, &ans, 0) || ans == nil)
		return dflt;
	else
		return toint(ans);
}

// Like aintval, but result should be >= 0
static int
auintval(Token* tok, int attid, int dflt)
{
	Rune* ans;
	int v;

	if(!_tokaval(tok, attid, &ans, 0) || ans == nil)
		return dflt;
	else {
		v = toint(ans);
		return v >= 0? v : 0;
	}
}

// int conversion, but with possible error check (if warning)
static int
toint(Rune* s)
{
	int ans;
	Rune* eptr;

	ans = _Strtol(s, &eptr, 10);
	if(warn) {
		if(*eptr != 0) {
			eptr = _Strclass(eptr, notwhitespace);
			if(eptr != nil)
				fprint(2, "warning: expected integer, got %S\n", s);
		}
	}
	return ans;
}

// Attribute value when need a table to convert strings to ints
static int
atabval(Token* tok, int attid, StringInt* tab, int ntab, int dflt)
{
	Rune*	aval;
	int	ans;

	ans = dflt;
	if(_tokaval(tok, attid, &aval, 0)) {
		if(!_lookup(tab, ntab, aval, _Strlen(aval), &ans)) {
			ans = dflt;
			if(warn)
				fprint(2, "warning: name not found in table lookup: %S\n", aval);
		}
	}
	return ans;
}

// Attribute value when supposed to be a color
static int
acolorval(Token* tok, int attid, int dflt)
{
	Rune*	aval;
	int	ans;

	ans = dflt;
	if(_tokaval(tok, attid, &aval, 0))
		ans = color(aval, dflt);
	return ans;
}

// Attribute value when supposed to be a target frame name
static int
atargval(Token* tok, int dflt)
{
	int	ans;
	Rune*	aval;

	ans = dflt;
	if(_tokaval(tok, Atarget, &aval, 0)){
		ans = targetid(aval);
	}
	return ans;
}

// special for list types, where "i" and "I" are different,
// but "square" and "SQUARE" are the same
static int
listtyval(Token* tok, int dflt)
{
	Rune*	aval;
	int	ans;
	int	n;

	ans = dflt;
	if(_tokaval(tok, Atype, &aval, 0)) {
		n = _Strlen(aval);
		if(n == 1) {
			switch(aval[0]) {
			case '1':
				ans = LT1;
				break;
			case 'A':
				ans = LTA;
				break;
			case 'I':
				ans = LTI;
				break;
			case 'a':
				ans = LTa;
				break;
			case 'i':
				ans = LTi;
			default:
				if(warn)
					fprint(2, "warning: unknown list element type %c\n", aval[0]);
			}
		}
		else {
			if(!_Strncmpci(aval, n, L"circle"))
				ans = LTcircle;
			else if(!_Strncmpci(aval, n, L"disc"))
				ans = LTdisc;
			else if(!_Strncmpci(aval, n, L"square"))
				ans = LTsquare;
			else {
				if(warn)
					fprint(2, "warning: unknown list element type %S\n", aval);
			}
		}
	}
	return ans;
}

// Attribute value when value is a URL, possibly relative to base.
// FOR NOW: leave the url relative.
// Caller must free the result (eventually).
static Rune*
aurlval(Token* tok, int attid, Rune* dflt, Rune* base)
{
	Rune*	ans;
	Rune*	url;

	USED(base);
	ans = nil;
	if(_tokaval(tok, attid, &url, 0) && url != nil)
		ans = removeallwhite(url);
	if(ans == nil)
		ans = _Strdup(dflt);
	return ans;
}

// Return copy of s but with all whitespace (even internal) removed.
// This fixes some buggy URL specification strings.
static Rune*
removeallwhite(Rune* s)
{
	int	j;
	int	n;
	int	i;
	int	c;
	Rune*	ans;

	j = 0;
	n = _Strlen(s);
	for(i = 0; i < n; i++) {
		c = s[i];
		if(c >= 256 || !isspace(c))
			j++;
	}
	if(j < n) {
		ans = _newstr(j);
		j = 0;
		for(i = 0; i < n; i++) {
			c = s[i];
			if(c >= 256 || !isspace(c))
				ans[j++] = c;
		}
		ans[j] = 0;
	}
	else
		ans = _Strdup(s);
	return ans;
}

// Attribute value when mere presence of attr implies value of 1,
// but if there is an integer there, return it as the value.
static int
aflagval(Token* tok, int attid)
{
	int	val;
	Rune*	sval;

	val = 0;
	if(_tokaval(tok, attid, &sval, 0)) {
		val = 1;
		if(sval != nil)
			val = toint(sval);
	}
	return val;
}

static Align
makealign(int halign, int valign)
{
	Align	al;

	al.halign = halign;
	al.valign = valign;
	return al;
}

// Make an Align (two alignments, horizontal and vertical)
static Align
aalign(Token* tok)
{
	return makealign(
		atabval(tok, Aalign, align_tab, NALIGNTAB, ALnone),
		atabval(tok, Avalign, align_tab, NALIGNTAB, ALnone));
}

// Make a Dimen, based on value of attid attr
static Dimen
adimen(Token* tok, int attid)
{
	Rune*	wd;

	if(_tokaval(tok, attid, &wd, 0))
		return parsedim(wd, _Strlen(wd));
	else
		return makedimen(Dnone, 0);
}

// Parse s[0:n] as num[.[num]][unit][%|*]
static Dimen
parsedim(Rune* s, int ns)
{
	int	kind;
	int	spec;
	Rune*	l;
	int	nl;
	Rune*	r;
	int	nr;
	int	mul;
	int	i;
	Rune*	f;
	int	nf;
	int	Tkdpi;
	Rune*	units;

	kind = Dnone;
	spec = 0;
	_splitl(s, ns, L"^0-9", &l, &nl, &r, &nr);
	if(nl != 0) {
		spec = 1000*_Strtol(l, nil, 10);
		if(nr > 0 && r[0] == '.') {
			_splitl(r+1, nr-1, L"^0-9", &f, &nf, &r, &nr);
			if(nf != 0) {
				mul = 100;
				for(i = 0; i < nf; i++) {
					spec = spec + mul*(f[i]-'0');
					mul = mul/10;
				}
			}
		}
		kind = Dpixels;
		if(nr != 0) {
			if(nr >= 2) {
				Tkdpi = 100;
				units = r;
				r = r+2;
				nr -= 2;
				if(!_Strncmpci(units, 2, L"pt"))
					spec = (spec*Tkdpi)/72;
				else if(!_Strncmpci(units, 2, L"pi"))
					spec = (spec*12*Tkdpi)/72;
				else if(!_Strncmpci(units, 2, L"in"))
					spec = spec*Tkdpi;
				else if(!_Strncmpci(units, 2, L"cm"))
					spec = (spec*100*Tkdpi)/254;
				else if(!_Strncmpci(units, 2, L"mm"))
					spec = (spec*10*Tkdpi)/254;
				else if(!_Strncmpci(units, 2, L"em"))
					spec = spec*15;
				else {
					if(warn)
						fprint(2, "warning: unknown units %C%Cs\n", units[0], units[1]);
				}
			}
			if(nr >= 1) {
				if(r[0] == '%')
					kind = Dpercent;
				else if(r[0] == '*')
					kind = Drelative;
			}
		}
		spec = spec/1000;
	}
	else if(nr == 1 && r[0] == '*') {
		spec = 1;
		kind = Drelative;
	}
	return makedimen(kind, spec);
}

static void
setdimarray(Token* tok, int attid, Dimen** pans, int* panslen)
{
	Rune*	s;
	Dimen*	d;
	int	k;
	int	nc;
	Rune* a[SMALLBUFSIZE];
	int	an[SMALLBUFSIZE];

	if(_tokaval(tok, attid, &s, 0)) {
		nc = _splitall(s, _Strlen(s), L", ", a, an, SMALLBUFSIZE);
		if(nc > 0) {
			d = (Dimen*)emalloc(nc * sizeof(Dimen));
			for(k = 0; k < nc; k++) {
				d[k] = parsedim(a[k], an[k]);
			}
			*pans = d;
			*panslen = nc;
			return;
		}
	}
	*pans = nil;
	*panslen = 0;
}

static Background
makebackground(Rune* imageurl, int color)
{
	Background bg;

	bg.image = imageurl;
	bg.color = color;
	return bg;
}

static Item*
newitext(Rune* s, int fnt, int fg, int voff, int ul)
{
	Itext* t;

	assert(s != nil);
	t = (Itext*)emalloc(sizeof(Itext));
	t->tag = Itexttag;
	t->s = s;
	t->fnt = fnt;
	t->fg = fg;
	t->voff = voff;
	t->ul = ul;
	return (Item*)t;
}

static Item*
newirule(int align, int size, int noshade, int color, Dimen wspec)
{
	Irule* r;

	r = (Irule*)emalloc(sizeof(Irule));
	r->tag = Iruletag;
	r->align = align;
	r->size = size;
	r->noshade = noshade;
	r->color = color;
	r->wspec = wspec;
	return (Item*)r;
}

// Map is owned elsewhere.
static Item*
newiimage(Rune* src, Rune* altrep, int align, int width, int height,
		int hspace, int vspace, int border, int ismap, Map* map)
{
	Iimage* i;
	int	state;

	state = 0;
	if(ismap)
		state = IFsmap;
	i = (Iimage*)emalloc(sizeof(Iimage));
	i->tag = Iimagetag;
	i->state = state;
	i->imsrc = src;
	i->altrep = altrep;
	i->align = align;
	i->imwidth = width;
	i->imheight = height;
	i->hspace = hspace;
	i->vspace = vspace;
	i->border = border;
	i->map = map;
	i->ctlid = -1;
	return (Item*)i;
}

static Item*
newiformfield(Formfield* ff)
{
	Iformfield* f;

	f = (Iformfield*)emalloc(sizeof(Iformfield));
	f->tag = Iformfieldtag;
	f->formfield = ff;
	return (Item*)f;
}

static Item*
newitable(Table* tab)
{
	Itable* t;

	t = (Itable*)emalloc(sizeof(Itable));
	t->tag = Itabletag;
	t->table = tab;
	return (Item*)t;
}

static Item*
newifloat(Item* it, int side)
{
	Ifloat* f;

	f = (Ifloat*)emalloc(sizeof(Ifloat));
	f->tag = Ifloattag;
	f->state = IFwrap;
	f->item = it;
	f->side = side;
	return (Item*)f;
}

static Item*
newispacer(int spkind)
{
	Ispacer* s;

	s = (Ispacer*)emalloc(sizeof(Ispacer));
	s->tag = Ispacertag;
	s->spkind = spkind;
	return (Item*)s;
}

// Free one item (caller must deal with next pointer)
static void
freeitem(Item* it)
{
	Iimage* ii;
	Genattr* ga;

	if(it == nil)
		return;

	switch(it->tag) {
	case Itexttag:
		free(((Itext*)it)->s);
		break;
	case Iimagetag:
		ii = (Iimage*)it;
		free(ii->imsrc);
		free(ii->altrep);
		break;
	case Iformfieldtag:
		freeformfield(((Iformfield*)it)->formfield);
		break;
	case Itabletag:
		freetable(((Itable*)it)->table);
		break;
	case Ifloattag:
		freeitem(((Ifloat*)it)->item);
		break;
	}
	ga = it->genattr;
	if(ga != nil) {
		free(ga->id);
		free(ga->class);
		free(ga->style);
		free(ga->title);
		freescriptevents(ga->events);
		free(ga);
	}
	free(it);
}

// Free list of items chained through next pointer
void
freeitems(Item* ithead)
{
	Item* it;
	Item* itnext;

	it = ithead;
	while(it != nil) {
		itnext = it->next;
		freeitem(it);
		it = itnext;
	}
}

static void
freeformfield(Formfield* ff)
{
	Option* o;
	Option* onext;

	if(ff == nil)
		return;

	free(ff->name);
	free(ff->value);
	freeitem(ff->image);
	for(o = ff->options; o != nil; o = onext) {
		onext = o->next;
		free(o->value);
		free(o->display);
	}
	free(ff);
}

static void
freetable(Table* t)
{
	int i;
	Tablecell* c;
	Tablecell* cnext;

	if(t == nil)
		return;

	// We'll find all the unique cells via t->cells and next pointers.
	// (Other pointers to cells in the table are duplicates of these)
	for(c = t->cells; c != nil; c = cnext) {
		cnext = c->next;
		freeitems(c->content);
		free(c);
	}
	if(t->grid != nil) {
		for(i = 0; i < t->nrow; i++)
			free(t->grid[i]);
		free(t->grid);
	}
	free(t->rows);
	free(t->cols);
	freeitems(t->caption);
	free(t);
}

static void
freeform(Form* f)
{
	if(f == nil)
		return;

	free(f->name);
	free(f->action);
	// Form doesn't own its fields (Iformfield items do)
	free(f);
}

static void
freeforms(Form* fhead)
{
	Form* f;
	Form* fnext;

	for(f = fhead; f != nil; f = fnext) {
		fnext = f->next;
		freeform(f);
	}
}

static void
freeanchor(Anchor* a)
{
	if(a == nil)
		return;

	free(a->name);
	free(a->href);
	free(a);
}

static void
freeanchors(Anchor* ahead)
{
	Anchor* a;
	Anchor* anext;

	for(a = ahead; a != nil; a = anext) {
		anext = a->next;
		freeanchor(a);
	}
}

static void
freedestanchor(DestAnchor* da)
{
	if(da == nil)
		return;

	free(da->name);
	free(da);
}

static void
freedestanchors(DestAnchor* dahead)
{
	DestAnchor* da;
	DestAnchor* danext;

	for(da = dahead; da != nil; da = danext) {
		danext = da->next;
		freedestanchor(da);
	}
}

static void
freearea(Area* a)
{
	if(a == nil)
		return;
	free(a->href);
	free(a->coords);
}

static void freekidinfos(Kidinfo* khead);

static void
freekidinfo(Kidinfo* k)
{
	if(k->isframeset) {
		free(k->rows);
		free(k->cols);
		freekidinfos(k->kidinfos);
	}
	else {
		free(k->src);
		free(k->name);
	}
	free(k);
}

static void
freekidinfos(Kidinfo* khead)
{
	Kidinfo* k;
	Kidinfo* knext;

	for(k = khead; k != nil; k = knext) {
		knext = k->next;
		freekidinfo(k);
	}
}

static void
freemap(Map* m)
{
	Area* a;
	Area* anext;

	if(m == nil)
		return;

	free(m->name);
	for(a = m->areas; a != nil; a = anext) {
		anext = a->next;
		freearea(a);
	}
	free(m);
}

static void
freemaps(Map* mhead)
{
	Map* m;
	Map* mnext;

	for(m = mhead; m != nil; m = mnext) {
		mnext = m->next;
		freemap(m);
	}
}

void
freedocinfo(Docinfo* d)
{
	if(d == nil)
		return;
	free(d->src);
	free(d->base);
	free(d->doctitle);
	freeitem(d->backgrounditem);
	free(d->refresh);
	freekidinfos(d->kidinfo);
	freeanchors(d->anchors);
	freedestanchors(d->dests);
	freeforms(d->forms);
	freemaps(d->maps);
	// tables, images, and formfields are freed when
	// the items pointing at them are freed
	free(d);
}

static void
freepstate(Pstate* p)
{
	freeitems(p->items);
	free(p);
}

static void
freepstatestack(Pstate* pshead)
{
	Pstate* p;
	Pstate* pnext;

	for(p = pshead; p != nil; p = pnext) {
		pnext = p->next;
		freepstate(p);
	}
}

static int
Iconv(Fmt *f)
{
	Item*	it;
	Itext*	t;
	Irule*	r;
	Iimage*	i;
	Ifloat*	fl;
	int	state;
	Formfield*	ff;
	Rune*	ty;
	Tablecell*	c;
	Table*	tab;
	char*	p;
	int	cl;
	int	hang;
	int	indent;
	int	bi;
	int	nbuf;
	char	buf[BIGBUFSIZE];

	it = va_arg(f->args, Item*);
	bi = 0;
	nbuf = sizeof(buf);
	state = it->state;
	nbuf = nbuf-1;
	if(state&IFbrk) {
		cl = state&(IFcleft|IFcright);
		p = "";
		if(cl) {
			if(cl == (IFcleft|IFcright))
				p = " both";
			else if(cl == IFcleft)
				p = " left";
			else
				p = " right";
		}
		bi = snprint(buf, nbuf, "brk(%d%s)", (state&IFbrksp)? 1 : 0, p);
	}
	if(state&IFnobrk)
		bi += snprint(buf+bi, nbuf-bi, " nobrk");
	if(!(state&IFwrap))
		bi += snprint(buf+bi, nbuf-bi, " nowrap");
	if(state&IFrjust)
		bi += snprint(buf+bi, nbuf-bi, " rjust");
	if(state&IFcjust)
		bi += snprint(buf+bi, nbuf-bi, " cjust");
	if(state&IFsmap)
		bi += snprint(buf+bi, nbuf-bi, " smap");
	indent = (state&IFindentmask) >> IFindentshift;
	if(indent > 0)
		bi += snprint(buf+bi, nbuf-bi, " indent=%d", indent);
	hang = state&IFhangmask;
	if(hang > 0)
		bi += snprint(buf+bi, nbuf-bi, " hang=%d", hang);

	switch(it->tag) {
	case Itexttag:
		t = (Itext*)it;
		bi += snprint(buf+bi, nbuf-bi, " Text '%S', fnt=%d, fg=%x", t->s, t->fnt, t->fg);
		break;

	case Iruletag:
		r = (Irule*)it;
		bi += snprint(buf+bi, nbuf-bi, "Rule size=%d, al=%S, wspec=", r->size, stringalign(r->align));
		bi += dimprint(buf+bi, nbuf-bi, r->wspec);
		break;

	case Iimagetag:
		i = (Iimage*)it;
		bi += snprint(buf+bi, nbuf-bi,
			"Image src=%S, alt=%S, al=%S, w=%d, h=%d hsp=%d, vsp=%d, bd=%d, map=%S",
			i->imsrc, i->altrep? i->altrep : L"", stringalign(i->align), i->imwidth, i->imheight,
			i->hspace, i->vspace, i->border, i->map? i->map->name : L"");
		break;

	case Iformfieldtag:
		ff = ((Iformfield*)it)->formfield;
		if(ff->ftype == Ftextarea)
			ty = L"textarea";
		else if(ff->ftype == Fselect)
			ty = L"select";
		else {
			ty = _revlookup(input_tab, NINPUTTAB, ff->ftype);
			if(ty == nil)
				ty = L"none";
		}
		bi += snprint(buf+bi, nbuf-bi, "Formfield %S, fieldid=%d, formid=%d, name=%S, value=%S",
			ty, ff->fieldid, ff->form->formid, ff->name? ff->name : L"",
			ff->value? ff->value : L"");
		break;

	case Itabletag:
		tab = ((Itable*)it)->table;
		bi += snprint(buf+bi, nbuf-bi, "Table tableid=%d, width=", tab->tableid);
		bi += dimprint(buf+bi, nbuf-bi, tab->width);
		bi += snprint(buf+bi, nbuf-bi, ", nrow=%d, ncol=%d, ncell=%d, totw=%d, toth=%d\n",
			tab->nrow, tab->ncol, tab->ncell, tab->totw, tab->toth);
		for(c = tab->cells; c != nil; c = c->next)
			bi += snprint(buf+bi, nbuf-bi, "Cell %d.%d, at (%d,%d) ",
					tab->tableid, c->cellid, c->row, c->col);
		bi += snprint(buf+bi, nbuf-bi, "End of Table %d", tab->tableid);
		break;

	case Ifloattag:
		fl = (Ifloat*)it;
		bi += snprint(buf+bi, nbuf-bi, "Float, x=%d y=%d, side=%S, it=%I",
			fl->x, fl->y, stringalign(fl->side), fl->item);
		bi += snprint(buf+bi, nbuf-bi, "\n\t");
		break;

	case Ispacertag:
		p = "";
		switch(((Ispacer*)it)->spkind) {
		case ISPnull:
			p = "null";
			break;
		case ISPvline:
			p = "vline";
			break;
		case ISPhspace:
			p = "hspace";
			break;
		}
		bi += snprint(buf+bi, nbuf-bi, "Spacer %s ", p);
		break;
	}
	bi += snprint(buf+bi, nbuf-bi, " w=%d, h=%d, a=%d, anchor=%d\n",
			it->width, it->height, it->ascent, it->anchorid);
	buf[bi] = 0;
	return fmtstrcpy(f, buf);
}

// String version of alignment 'a'
static Rune*
stringalign(int a)
{
	Rune*	s;

	s = _revlookup(align_tab, NALIGNTAB, a);
	if(s == nil)
		s = L"none";
	return s;
}

// Put at most nbuf chars of representation of d into buf,
// and return number of characters put
static int
dimprint(char* buf, int nbuf, Dimen d)
{
	int	n;
	int	k;

	n = 0;
	n += snprint(buf, nbuf, "%d", dimenspec(d));
	k = dimenkind(d);
	if(k == Dpercent)
		buf[n++] = '%';
	if(k == Drelative)
		buf[n++] = '*';
	return n;
}

void
printitems(Item* items, char* msg)
{
	Item*	il;

	fprint(2, "%s\n", msg);
	il = items;
	while(il != nil) {
		fprint(2, "%I", il);
		il = il->next;
	}
}

static Genattr*
newgenattr(Rune* id, Rune* class, Rune* style, Rune* title, SEvent* events)
{
	Genattr* g;

	g = (Genattr*)emalloc(sizeof(Genattr));
	g->id = id;
	g->class = class;
	g->style = style;
	g->title = title;
	g->events = events;
	return g;
}

static Formfield*
newformfield(int ftype, int fieldid, Form* form, Rune* name,
		Rune* value, int size, int maxlength, Formfield* link)
{
	Formfield* ff;

	ff = (Formfield*)emalloc(sizeof(Formfield));
	ff->ftype = ftype;
	ff->fieldid = fieldid;
	ff->form = form;
	ff->name = name;
	ff->value = value;
	ff->size = size;
	ff->maxlength = maxlength;
	ff->ctlid = -1;
	ff->next = link;
	return ff;
}

// Transfers ownership of value and display to Option.
static Option*
newoption(int selected, Rune* value, Rune* display, Option* link)
{
	Option *o;

	o = (Option*)emalloc(sizeof(Option));
	o->selected = selected;
	o->value = value;
	o->display = display;
	o->next = link;
	return o;
}

static Form*
newform(int formid, Rune* name, Rune* action, int target, int method, Form* link)
{
	Form* f;

	f = (Form*)emalloc(sizeof(Form));
	f->formid = formid;
	f->name = name;
	f->action = action;
	f->target = target;
	f->method = method;
	f->nfields = 0;
	f->fields = nil;
	f->next = link;
	return f;
}

static Table*
newtable(int tableid, Align align, Dimen width, int border,
	int cellspacing, int cellpadding, Background bg, Token* tok, Table* link)
{
	Table* t;

	t = (Table*)emalloc(sizeof(Table));
	t->tableid = tableid;
	t->align = align;
	t->width = width;
	t->border = border;
	t->cellspacing = cellspacing;
	t->cellpadding = cellpadding;
	t->background = bg;
	t->caption_place = ALbottom;
	t->caption_lay = nil;
	t->tabletok = tok;
	t->next = link;
	return t;
}

static Tablerow*
newtablerow(Align align, Background bg, int flags, Tablerow* link)
{
	Tablerow* tr;

	tr = (Tablerow*)emalloc(sizeof(Tablerow));
	tr->align = align;
	tr->background = bg;
	tr->flags = flags;
	tr->next = link;
	return tr;
}

static Tablecell*
newtablecell(int cellid, int rowspan, int colspan, Align align, Dimen wspec, int hspec,
		Background bg, int flags, Tablecell* link)
{
	Tablecell* c;

	c = (Tablecell*)emalloc(sizeof(Tablecell));
	c->cellid = cellid;
	c->lay = nil;
	c->rowspan = rowspan;
	c->colspan = colspan;
	c->align = align;
	c->flags = flags;
	c->wspec = wspec;
	c->hspec = hspec;
	c->background = bg;
	c->next = link;
	return c;
}

static Anchor*
newanchor(int index, Rune* name, Rune* href, int target, Anchor* link)
{
	Anchor* a;

	a = (Anchor*)emalloc(sizeof(Anchor));
	a->index = index;
	a->name = name;
	a->href = href;
	a->target = target;
	a->next = link;
	return a;
}

static DestAnchor*
newdestanchor(int index, Rune* name, Item* item, DestAnchor* link)
{
	DestAnchor* d;

	d = (DestAnchor*)emalloc(sizeof(DestAnchor));
	d->index = index;
	d->name = name;
	d->item = item;
	d->next = link;
	return d;
}

static SEvent*
newscriptevent(int type, Rune* script, SEvent* link)
{
	SEvent* ans;

	ans = (SEvent*)emalloc(sizeof(SEvent));
	ans->type = type;
	ans->script = script;
	ans->next = link;
	return ans;
}

static void
freescriptevents(SEvent* ehead)
{
	SEvent* e;
	SEvent* nexte;

	e = ehead;
	while(e != nil) {
		nexte = e->next;
		free(e->script);
		free(e);
		e = nexte;
	}
}

static Dimen
makedimen(int kind, int spec)
{
	Dimen d;

	if(spec&Dkindmask) {
		if(warn)
			fprint(2, "warning: dimension spec too big: %d\n", spec);
		spec = 0;
	}
	d.kindspec = kind|spec;
	return d;
}

int
dimenkind(Dimen d)
{
	return (d.kindspec&Dkindmask);
}

int
dimenspec(Dimen d)
{
	return (d.kindspec&Dspecmask);
}

static Kidinfo*
newkidinfo(int isframeset, Kidinfo* link)
{
	Kidinfo*	ki;

	ki = (Kidinfo*)emalloc(sizeof(Kidinfo));
	ki->isframeset = isframeset;
	if(!isframeset) {
		ki->flags = FRhscrollauto|FRvscrollauto;
		ki->marginw = FRKIDMARGIN;
		ki->marginh = FRKIDMARGIN;
		ki->framebd = 1;
	}
	ki->next = link;
	return ki;
}

static Docinfo*
newdocinfo(void)
{
	Docinfo*	d;

	d = (Docinfo*)emalloc(sizeof(Docinfo));
	resetdocinfo(d);
	return d;
}

static void
resetdocinfo(Docinfo* d)
{
	memset(d, 0, sizeof(Docinfo));
	d->background = makebackground(nil, White);
	d->text = Black;
	d->link = Blue;
	d->vlink = Blue;
	d->alink = Blue;
	d->target = FTself;
	d->chset = ISO_8859_1;
	d->scripttype = TextJavascript;
	d->frameid = -1;
}

// Use targetmap array to keep track of name <-> targetid mapping.
// Use real malloc(), and never free
static void
targetmapinit(void)
{
	int l;

	targetmapsize = 10;
	l = targetmapsize*sizeof *targetmap;
	targetmap = emalloc(l);
	memset(targetmap, 0, l);
	targetmap[0].key = _Strdup(L"_top");
	targetmap[0].val = FTtop;
	targetmap[1].key = _Strdup(L"_self");
	targetmap[1].val = FTself;
	targetmap[2].key = _Strdup(L"_parent");
	targetmap[2].val = FTparent;
	targetmap[3].key = _Strdup(L"_blank");
	targetmap[3].val = FTblank;
	ntargets = 4;
}

int
targetid(Rune* s)
{
	int i;
	int n;

	n = _Strlen(s);
	if(n == 0)
		return FTself;
	for(i = 0; i < ntargets; i++)
		if(_Strcmp(s, targetmap[i].key) == 0)
			return targetmap[i].val;
	if(i == targetmapsize) {
		targetmapsize += 10;
		targetmap = erealloc(targetmap, targetmapsize*sizeof(StringInt));
	}
	targetmap[i].key = _Strdup(s);
	targetmap[i].val = i;
	ntargets++;
	return i;
}

Rune*
targetname(int targid)
{
	int i;

	for(i = 0; i < ntargets; i++)
		if(targetmap[i].val == targid)
			return targetmap[i].key;
	return L"?";
}

// Convert HTML color spec to RGB value, returning dflt if can't.
// Argument is supposed to be a valid HTML color, or "".
// Return the RGB value of the color, using dflt if s
// is nil or an invalid color.
static int
color(Rune* s, int dflt)
{
	int v;
	Rune* rest;

	if(s == nil)
		return dflt;
	if(_lookup(color_tab, NCOLORS, s, _Strlen(s), &v))
		return v;
	if(s[0] == '#')
		s++;
	v = _Strtol(s, &rest, 16);
	if(*rest == 0)
		return v;
	return dflt;
}

// Debugging

#define HUGEPIX 10000

// A "shallow" validitem, that doesn't follow next links
// or descend into tables.
static int
validitem(Item* i)
{
	int ok;
	Itext* ti;
	Irule* ri;
	Iimage* ii;
	Ifloat* fi;
	int a;

	ok = (i->tag >= Itexttag && i->tag <= Ispacertag) &&
		(i->next == nil || validptr(i->next)) &&
		(i->width >= 0 && i->width < HUGEPIX) &&
		(i->height >= 0 && i->height < HUGEPIX) &&
		(i->ascent > -HUGEPIX && i->ascent < HUGEPIX) &&
		(i->anchorid >= 0) &&
		(i->genattr == nil || validptr(i->genattr));
	// also, could check state for ridiculous combinations
	// also, could check anchorid for within-doc-range
	if(ok)
		switch(i->tag) {
		case Itexttag:
			ti = (Itext*)i;
			ok = validStr(ti->s) &&
				(ti->fnt >= 0 && ti->fnt < NumStyle*NumSize) &&
				(ti->ul == ULnone || ti->ul == ULunder || ti->ul == ULmid);
			break;
		case Iruletag:
			ri = (Irule*)i;
			ok = (validvalign(ri->align) || validhalign(ri->align)) &&
				(ri->size >=0 && ri->size < HUGEPIX);
			break;
		case Iimagetag:
			ii = (Iimage*)i;
			ok = (ii->imsrc == nil || validptr(ii->imsrc)) &&
				(ii->width >= 0 && ii->width < HUGEPIX) &&
				(ii->height >= 0 && ii->height < HUGEPIX) &&
				(ii->imwidth >= 0 && ii->imwidth < HUGEPIX) &&
				(ii->imheight >= 0 && ii->imheight < HUGEPIX) &&
				(ii->altrep == nil || validStr(ii->altrep)) &&
				(ii->map == nil || validptr(ii->map)) &&
				(validvalign(ii->align) || validhalign(ii->align)) &&
				(ii->nextimage == nil || validptr(ii->nextimage));
			break;
		case Iformfieldtag:
			ok = validformfield(((Iformfield*)i)->formfield);
			break;
		case Itabletag:
			ok = validptr((Itable*)i);
			break;
		case Ifloattag:
			fi = (Ifloat*)i;
			ok = (fi->side == ALleft || fi->side == ALright) &&
				validitem(fi->item) &&
				(fi->item->tag == Iimagetag || fi->item->tag == Itabletag);
			break;
		case Ispacertag:
			a = ((Ispacer*)i)->spkind;
			ok = a==ISPnull || a==ISPvline || a==ISPhspace || a==ISPgeneral;
			break;
		default:
			ok = 0;
		}
	return ok;
}

// "deep" validation, that checks whole list of items,
// and descends into tables and floated tables.
// nil is ok for argument.
int
validitems(Item* i)
{
	int ok;
	Item* ii;

	ok = 1;
	while(i != nil && ok) {
		ok = validitem(i);
		if(ok) {
			if(i->tag == Itabletag) {
				ok = validtable(((Itable*)i)->table);
			}
			else if(i->tag == Ifloattag) {
				ii = ((Ifloat*)i)->item;
				if(ii->tag == Itabletag)
					ok = validtable(((Itable*)ii)->table);
			}
		}
		if(!ok) {
			fprint(2, "invalid item: %I\n", i);
		}
		i = i->next;
	}
	return ok;
}

static int
validformfield(Formfield* f)
{
	int ok;

	ok = (f->next == nil || validptr(f->next)) &&
		(f->ftype >= 0 && f->ftype <= Ftextarea) &&
		f->fieldid >= 0 &&
		(f->form == nil || validptr(f->form)) &&
		(f->name == nil || validStr(f->name)) &&
		(f->value == nil || validStr(f->value)) &&
		(f->options == nil || validptr(f->options)) &&
		(f->image == nil || validitem(f->image)) &&
		(f->events == nil || validptr(f->events));
	// when all built, should have f->fieldid < f->form->nfields,
	// but this may be called during build...
	return ok;
}

// "deep" validation -- checks cell contents too
static int
validtable(Table* t)
{
	int ok;
	int i, j;
	Tablecell* c;

	ok = (t->next == nil || validptr(t->next)) &&
		t->nrow >= 0 &&
		t->ncol >= 0 &&
		t->ncell >= 0 &&
		validalign(t->align) &&
		validdimen(t->width) &&
		(t->border >= 0 && t->border < HUGEPIX) &&
		(t->cellspacing >= 0 && t->cellspacing < HUGEPIX) &&
		(t->cellpadding >= 0 && t->cellpadding < HUGEPIX) &&
		validitems(t->caption) &&
		(t->caption_place == ALtop || t->caption_place == ALbottom) &&
		(t->totw >= 0 && t->totw < HUGEPIX) &&
		(t->toth >= 0 && t->toth < HUGEPIX) &&
		(t->tabletok == nil || validptr(t->tabletok));
	// during parsing, t->rows has list;
	// only when parsing is done is t->nrow set > 0
	if(ok && t->nrow > 0 && t->ncol > 0) {
		// table is "finished"
		for(i = 0; i < t->nrow && ok; i++) 
			ok = validtablerow(t->rows+i);
		for(j = 0; j < t->ncol && ok; j++)
			ok = validtablecol(t->cols+j);
		for(c = t->cells; c != nil && ok; c = c->next)
			ok = validtablecell(c);
		for(i = 0; i < t->nrow && ok; i++)
			for(j = 0; j < t->ncol && ok; j++)
				ok = validptr(t->grid[i][j]);
	}
	return ok;
}

static int
validvalign(int a)
{
	return a == ALnone || a == ALmiddle || a == ALbottom || a == ALtop || a == ALbaseline;
}

static int
validhalign(int a)
{
	return a == ALnone || a == ALleft || a == ALcenter || a == ALright ||
			a == ALjustify || a == ALchar;
}

static int
validalign(Align a)
{
	return validhalign(a.halign) && validvalign(a.valign);
}

static int
validdimen(Dimen d)
{
	int ok;
	int s;

	ok = 0;
	s = d.kindspec&Dspecmask;
	switch(d.kindspec&Dkindmask) {
	case Dnone:
		ok = s==0;
		break;
	case Dpixels:
		ok = s < HUGEPIX;
		break;
	case Dpercent:
	case Drelative:
		ok = 1;
		break;
	}
	return ok;
}

static int
validtablerow(Tablerow* r)
{
	return (r->cells == nil || validptr(r->cells)) &&
		(r->height >= 0 && r->height < HUGEPIX) &&
		(r->ascent > -HUGEPIX && r->ascent < HUGEPIX) &&
		validalign(r->align);
}

static int
validtablecol(Tablecol* c)
{
	return c->width >= 0 && c->width < HUGEPIX
		&& validalign(c->align);
}

static int
validtablecell(Tablecell* c)
{
	int ok;

	ok = (c->next == nil || validptr(c->next)) &&
		(c->nextinrow == nil || validptr(c->nextinrow)) &&
		(c->content == nil || validptr(c->content)) &&
		(c->lay == nil || validptr(c->lay)) &&
		c->rowspan >= 0 &&
		c->colspan >= 0 &&
		validalign(c->align) &&
		validdimen(c->wspec) &&
		c->row >= 0 &&
		c->col >= 0;
	if(ok) {
		if(c->content != nil)
			ok = validitems(c->content);
	}
	return ok;
}

static int
validptr(void* p)
{
	// TODO: a better job of this.
	// For now, just dereference, which cause a bomb
	// if not valid
	static char c;

	c = *((char*)p);
	return 1;
}

static int
validStr(Rune* s)
{
	return s != nil && validptr(s);
}