shithub: purgatorio

ref: 3efb5bbb4061056e523858b134c555949591efe2
dir: /appl/wm/brutus/table.b/

View raw version
implement Brutusext;

# <Extension table tablefile>

include "sys.m";
	sys: Sys;

include "draw.m";
	draw: Draw;
	Point, Font, Rect: import draw;

include "tk.m";
	tk: Tk;

include "tkclient.m";
	tkclient: Tkclient;

include "bufio.m";

include "string.m";
	S: String;

include "html.m";
	html: HTML;
	Lex, Attr, RBRA, Data, Ttable, Tcaption, Tcol, Ttr, Ttd: import html;

include "brutus.m";
	Size6, Size8, Size10, Size12, Size16, NSIZE,
	Roman, Italic, Bold, Type, NFONT, NFONTTAG,
	Example, List, Listelem, Heading, Nofill, Author, Title,
	DefFont, DefSize, TitleFont, TitleSize, HeadingFont, HeadingSize: import Brutus;

include "brutusext.m";

Name: con "Table";

# alignment types
Anone, Aleft, Acenter, Aright, Ajustify, Atop, Amiddle, Abottom, Abaseline: con iota;

# A cell has a number of Lines, each of which has a number of Items.
# Each Item is a string in one font.
Item: adt
{
	itemid: int;	# canvas text item id
	s: string;
	fontnum: int;	# (style*NumSizes + size)
	pos: Point;	# nw corner of text item, relative to line origin
	width: int;		# of s, in pixels,  when displayed in font
	line: cyclic ref Line;   # containing line
	prev: cyclic ref Item;
	next: cyclic ref Item;
};

Line: adt
{
	items: cyclic ref Item;
	pos: Point;	# nw corner of Line relative to containing cell;
	height: int;
	ascent: int;
	width: int;
	cell: cyclic ref Tablecell;  # containing cell
	next: cyclic ref Line;
};

Align: adt
{
	halign: int;
	valign: int;
};

Tablecell: adt
{
	cellid: int;
	content: array of ref Lex;
	lines: cyclic ref Line;
	rowspan: int;
	colspan: int;
	nowrap: int;
	align: Align;
	width: int;
	height: int;
	ascent: int;
	row: int;
	col: int;
	pos: Point;	# nw corner of cell, in canvas coords
};

Tablegcell: adt
{
	cell: ref Tablecell;
	drawnhere: int;
};

Tablerow: adt
{
	cells: list of ref Tablecell;
	height: int;
	ascent: int;
	align: Align;
	pos: Point;
	rule: int;			# width of rule below row, if > 0
	ruleids: list of int;	# canvas ids of lines used to draw rule
};

Tablecol: adt
{
	width: int;
	align: Align;
	pos: Point;
	rule: int;			# width of rule to right of col, if > 0
	ruleids: list of int;	# canvas ids of lines used to draw rule
};

Table: adt
{
	nrow: int;
	ncol: int;
	ncell: int;
	width: int;
	height: int;
	capcell: ref Tablecell;
	border: int;
	brectid: int;
	cols: array of ref Tablecol;
	rows: array of ref Tablerow;
	cells: list of ref Tablecell;
	grid: array of array of ref Tablegcell;
	colw: array of int;
	rowh: array of int;
};

# Font stuff

DefaultFnum: con (DefFont*NSIZE + Size10);

fontnames := array[NFONTTAG] of {
	"/fonts/lucidasans/unicode.6.font",
	"/fonts/lucidasans/unicode.7.font",
	"/fonts/lucidasans/unicode.8.font",
	"/fonts/lucidasans/unicode.10.font",
	"/fonts/lucidasans/unicode.13.font",
	"/fonts/lucidasans/italiclatin1.6.font",
	"/fonts/lucidasans/italiclatin1.7.font",
	"/fonts/lucidasans/italiclatin1.8.font",
	"/fonts/lucidasans/italiclatin1.10.font",
	"/fonts/lucidasans/italiclatin1.13.font",
	"/fonts/lucidasans/boldlatin1.6.font",
	"/fonts/lucidasans/boldlatin1.7.font",
	"/fonts/lucidasans/boldlatin1.8.font",
	"/fonts/lucidasans/boldlatin1.10.font",
	"/fonts/lucidasans/boldlatin1.13.font",
	"/fonts/lucidasans/typelatin1.6.font",
	"/fonts/lucidasans/typelatin1.7.font",
	"/fonts/pelm/latin1.9.font",
	"/fonts/pelm/ascii.12.font",
	"/fonts/pelm/ascii.16.font"
};

fontrefs := array[NFONTTAG] of ref Font;
fontused := array[NFONTTAG] of { DefaultFnum => 1, * => 0};

# TABHPAD, TABVPAD are extra space between columns, rows
TABHPAD: con 10;
TABVPAD: con 4;

tab: ref Table;
top: ref Tk->Toplevel;
display: ref Draw->Display;
canv: string;

init(asys: Sys, adraw: Draw, nil: Bufio, atk: Tk, aw: Tkclient)
{
	sys = asys;
	draw = adraw;
	tk = atk;
	tkclient = aw;
	html = load HTML HTML->PATH;
	S = load String String->PATH;
}

create(parent: string, t: ref Tk->Toplevel, name, args: string): string
{
	if(html == nil)
		return "can't load HTML module";
	top = t;
	display = t.image.display;
	canv = name;
	err := tk->cmd(t, "canvas " + canv);
	if(len err > 0 && err[0] == '!')
		return err_ret(err);

	spec: array of ref Lex;
	(spec, err) = getspec(parent, args);
	if(err != "")
		return err_ret(err);

	err = parsetab(spec);
	if(err != "")
		return err_ret(err);

	err = build();
	if(err != "")
		return err_ret(err);
	return "";
}

err_ret(s: string) : string
{
	return Name + ": " + s;
}

getspec(parent, args: string) : (array of ref Lex, string)
{
	(n, argl) := sys->tokenize(args, " ");
	if(n != 1)
		return (nil, "usage: " + Name + " file");
	(filebytes, err) := readfile(fullname(parent, hd argl));
	if(err != "")
		return (nil, err);
	return(html->lex(filebytes, HTML->UTF8, 1), "");
}

readfile(path: string): (array of byte, string)
{
	fd := sys->open(path, sys->OREAD);
	if(fd == nil)
		return (nil, sys->sprint("can't open %s, the error was: %r", path));
	(ok, d) := sys->fstat(fd);
	if(ok < 0)
		return (nil, sys->sprint("can't stat %s, the error was: %r", path));
	if(d.mode & Sys->DMDIR)
		return (nil, sys->sprint("%s is a directory", path));

	l := int d.length;
	buf := array[l] of byte;
	tot := 0;
	while(tot < l) {
		need := l - tot;
		n := sys->read(fd, buf[tot:], need);
		if(n <= 0)
			return (nil, sys->sprint("error reading %s, the error was: %r", path));
		tot += n;
	}
	return (buf, "");
}

# Use HTML 3.2 table spec as external representation
# (But no th cells, width specs; and extra "rule" attribute
# for col and tr meaning that a rule of given width is to
# follow the given column or row).
# DTD elements:
#	table: - O (caption?, col*, tr*)
#	caption: - - (%text+)
#	col: - O empty
#	tr: - O td*
#	td: - O (%body.content)
parsetab(toks: array of ref Lex) : string
{
	tabletlex := toks[0];
	n := len toks;
	(tlex, i) := nexttok(toks, n, 0);

	# caption
	capcell: ref Tablecell = nil;
	if(tlex != nil && tlex.tag == Tcaption) {
		for(j := i+1; j < n; j++) {
			tlex = toks[j];
			if(tlex.tag == Tcaption + RBRA)
				break;
		}
		if(j >= n)
			return syntax_err(tlex, j);
		if(j > i+1) {
			captoks := toks[i+1:j];
			(caplines, e) := lexes2lines(captoks);
			if(e != nil)
				return e;
			# we ignore caption now
#			capcell = ref Tablecell(0, captoks, caplines, 1, 1, 1, Align(Anone, Anone),
#						0, 0, 0, 0, 0, Point(0,0));
		}
		(tlex, i) = nexttok(toks, n, j);
	}

	# col*
	cols: list of ref Tablecol = nil;
	while(tlex != nil && tlex.tag == Tcol) {
		col := makecol(tlex);
		if(col.align.halign == Anone)
			col.align.halign = Aleft;
		cols = col :: cols;
		(tlex, i) = nexttok(toks, n, i);
	}
	cols = revcols(cols);

	body : list of ref Tablerow = nil;
	cells : list of ref Tablecell = nil;
	cellid := 0;
	rows: list of ref Tablerow = nil;

	# tr*
	while(tlex != nil && tlex.tag == Ttr) {
		currow := ref Tablerow(nil, 0, 0, makealign(tlex), Point(0,0), makelinew(tlex, "rule"), nil);
		rows = currow :: rows;

		# td*
		(tlex, i) = nexttok(toks, n, i);
		while(tlex != nil && tlex.tag == Ttd) {
			rowspan := 1;
			(rsfnd, rs) := html->attrvalue(tlex.attr, "rowspan");
			if(rsfnd && rs != "")
				rowspan = int rs;
			colspan := 1;
			(csfnd, cs) := html->attrvalue(tlex.attr, "colspan");
			if(csfnd && cs != "")
				colspan = int cs;
			nowrap := 0;
			(nwfnd, nil) := html->attrvalue(tlex.attr, "nowrap");
			if(nwfnd)
				nowrap = 1;
			align := makealign(tlex);
			for(j := i+1; j < n; j++) {
				tlex = toks[j];
				tg := tlex.tag;
				if(tg == Ttd + RBRA || tg == Ttd || tg == Ttr + RBRA || tg == Ttr)
					break;
			}
			if(j == n)
				tlex = nil;
			content: array of ref Lex = nil;
			if(j > i+1)
				content = toks[i+1:j];
			(lines, err) := lexes2lines(content);
			if(err != "")
				return err;
			curcell := ref Tablecell(cellid, content, lines, rowspan, colspan, nowrap, align, 0, 0, 0, 0, 0, Point(0,0));
			currow.cells = curcell :: currow.cells;
			cells = curcell :: cells;
			cellid++;
			if(tlex != nil && tlex.tag == Ttd + RBRA)
				(tlex, i) = nexttok(toks, n, j);
			else
				i = j;
		}
		if(tlex != nil && tlex.tag == Ttr + RBRA)
			(tlex, i) = nexttok(toks, n, i);
	}
	if(tlex == nil || tlex.tag != Ttable + RBRA)
		return syntax_err(tlex, i);

	# now reverse all the lists that were built in reverse order
	# and calculate nrow, ncol

	rows = revrowl(rows);
	nrow := len rows;
	rowa := array[nrow] of ref Tablerow;
	ncol := 0;
	r := 0;
	for(rl := rows; rl != nil; rl = tl rl) {
		row := hd rl;
		rowa[r++] = row;
		rcols := 0;
		cl := row.cells;
		row.cells = nil;
		while(cl != nil) {
			c := hd cl;
			row.cells = c :: row.cells;
			rcols += c.colspan;
			cl = tl cl;
		}
		if(rcols > ncol)
			ncol = rcols;
	}
	cells = revcelll(cells);

	cola := array[ncol] of ref Tablecol;
	for(c := 0; c < ncol; c++) {
		if(cols != nil) {
			cola[c] = hd cols;
			cols = tl cols;
		}
		else
			cola[c] = ref Tablecol(0, Align(Anone, Anone), Point(0,0), 0, nil);
	}

	if(tabletlex.tag != Ttable)
		return syntax_err(tabletlex, 0);
	border := makelinew(tabletlex, "border");
	tab = ref Table(nrow, ncol, cellid, 0, 0, capcell, border, 0, cola, rowa, cells, nil, nil, nil);

	return "";
}

syntax_err(tlex: ref Lex, i: int) : string
{
	if(tlex == nil)
		return "syntax error in table: premature end";
	else
		return "syntax error in table at token " + string i + ": " + html->lex2string(tlex);
}

# next token after toks[i], skipping whitespace
nexttok(toks: array of ref Lex, ntoks, i: int) : (ref Lex, int)
{
	i++;
	if(i >= ntoks)
		return (nil, i);
	t := toks[i];
	while(t.tag == Data) {
		if(S->drop(t.text, " \t\n\r") != "")
			break;
		i++;
		if(i >= ntoks)
			return (nil, i);
		t = toks[i];
	}
# sys->print("nexttok returning (%s,%d)\n", html->lex2string(t), i);
	return(t, i);
}

makecol(tlex: ref Lex) : ref Tablecol
{
	return ref Tablecol(0, makealign(tlex), Point(0,0), makelinew(tlex, "rule"), nil);
}

makelinew(tlex: ref Lex, aname: string) : int
{
	ans := 0;
	(fnd, val) := html->attrvalue(tlex.attr, aname);
	if(fnd) {
		if(val == "")
			ans = 1;
		else
			ans = int val;
	}
	return ans;
}

makealign(tlex: ref Lex) : Align
{
	(nil,h) := html->attrvalue(tlex.attr, "align");
	(nil,v) := html->attrvalue(tlex.attr, "valign");
	hal := align_val(h, Anone);
	val := align_val(v, Anone);
	return Align(hal, val);
}

align_val(sal: string, dflt: int) : int
{
	ans := dflt;
	case sal {
		"left" => ans = Aleft;
		"center" => ans = Acenter;
		"right" => ans = Aright;
		"justify" => ans = Ajustify;
		"top" => ans = Atop;
		"middle" => ans = Amiddle;
		"bottom" => ans = Abottom;
		"baseline" => ans = Abaseline;
	}
	return ans;
}

revcols(l : list of ref Tablecol) : list of ref Tablecol
{
	ans : list of ref Tablecol = nil;
	while(l != nil) {
		ans = hd l :: ans;
		l = tl l;
	}
	return ans;
}

revrowl(l : list of ref Tablerow) : list of ref Tablerow
{
	ans : list of ref Tablerow = nil;
	while(l != nil) {
		ans = hd l :: ans;
		l = tl l;
	}
	return ans;
}

revcelll(l : list of ref Tablecell) : list of ref Tablecell
{
	ans : list of ref Tablecell = nil;
	while(l != nil) {
		ans = hd l :: ans;
		l = tl l;
	}
	return ans;
}

revintl(l : list of int) : list of int
{
	ans : list of int = nil;
	while(l != nil) {
		ans = hd l :: ans;
		l = tl l;
	}
	return ans;
}

# toks should contain only Font (i.e., size) and style changes, along with text.
lexes2lines(toks: array of ref Lex) : (ref Line, string)
{
	n := len toks;
	(tlex, i) := nexttok(toks, n, -1);
	ans: ref Line = nil;
	if(tlex == nil)
		return(ans, "");
	curline : ref Line = nil;
	curitem : ref Item = nil;
	stylestk := DefFont :: nil;
	sizestk := DefSize :: nil;
	f := DefaultFnum;
	fontstk:= f :: nil;
	for(;;) {
		if(i >= n)
			break;
		tlex = toks[i++];
		case tlex.tag {
		Data =>
			text := tlex.text;
			while(text != "") {
				if(curline == nil) {
					curline = ref Line(nil, Point(0,0), 0, 0, 0, nil, nil);
					ans = curline;
				}
				s : string;
				(s, text) = S->splitl(text, "\n");
				if(s != "") {
					f = hd fontstk;
					it := ref Item(0, s, f, Point(0,0), 0, curline, curitem, nil);
					if(curitem == nil)
						curline.items = it;
					else
						curitem.next = it;
					curitem = it;
				}
				if(text != "") {
					text = text[1:];
					curline.next = ref Line(nil, Point(0,0), 0, 0, 0, nil, nil);
					curline = curline.next;
					curitem = nil;
				}
			}
		HTML->Tfont =>
			(fnd, ssize) := html->attrvalue(tlex.attr, "size");
			if(fnd && len ssize > 0) {
				# HTML size 3 == our Size10
				sz := (int ssize) + (Size10 - 3);
				if(sz < 0 || sz >= NSIZE)
					return (nil, "bad font size " + ssize);
				sizestk = sz :: sizestk;
				fontstk = fnum(hd stylestk, sz) :: fontstk;
			}
			else
				return (nil, "bad font command: no size");
		HTML->Tfont + RBRA =>
			fontstk = tl fontstk;
			sizestk = tl sizestk;
			if(sizestk == nil)
				return (nil, "unmatched </FONT>");
		HTML->Tb =>
			stylestk = Bold :: stylestk;
			fontstk = fnum(Bold, hd sizestk) :: fontstk;
		HTML->Ti =>
			stylestk = Italic :: stylestk;
			fontstk = fnum(Italic, hd sizestk) :: fontstk;
		HTML->Ttt =>
			stylestk = Type :: stylestk;
			fontstk = fnum(Type, hd sizestk) :: fontstk;
		HTML->Tb + RBRA or HTML->Ti + RBRA or HTML->Ttt + RBRA =>
			fontstk = tl fontstk;
			stylestk = tl stylestk;
			if(stylestk == nil)
				return (nil, "unmatched </B>, </I>, or </TT>");
		}
	}
	return (ans, "");
}

fnum(fstyle, fsize: int) : int
{
	ans := fstyle*NSIZE + fsize;
	fontused[ans] = 1;
	return ans;
}

loadfonts() : string
{
	for(i := 0; i < NFONTTAG; i++) {
		if(fontused[i] && fontrefs[i] == nil) {
			fname := fontnames[i];
			f := Font.open(display, fname);
			if(f == nil)
				return sys->sprint("can't open font %s: %r", fname);
			fontrefs[i] = f;
		}
	}
	return "";
}

# Find where each cell goes in nrow x ncol grid
setgrid()
{
	gcells := array[tab.nrow] of { * => array[tab.ncol] of { * => ref Tablegcell(nil, 1)} };

	# 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 := array[tab.ncol] of { * => 0};
	rowspancell := array[tab.ncol] of ref Tablecell;

	ri := 0;
	ci := 0;
	for(ri = 0; ri < tab.nrow; ri++) {
		row := tab.rows[ri];
		cl := row.cells;
		for(ci = 0; ci < tab.ncol; ) {
			if(rowspancnt[ci] > 0) {
				gcells[ri][ci].cell = rowspancell[ci];
				gcells[ri][ci].drawnhere = 0;
				rowspancnt[ci]--;
				ci++;
			}
			else {
				if(cl == nil) {
					ci++;
					continue;
				}
				c := hd cl;
				cl = tl cl;
				cspan := c.colspan;
				if(cspan == 0) {
					cspan = tab.ncol - ci;
					c.colspan = cspan;
				}
				rspan := c.rowspan;
				if(rspan == 0) {
					rspan = tab.nrow - ri;
					c.rowspan = rspan;
				}
				c.row = ri;
				c.col = ci;
				for(i := 0; i < cspan && ci < tab.ncol; i++) {
					gcells[ri][ci].cell = c;
					if(i > 0)
						gcells[ri][ci].drawnhere = 0;
					if(rspan > 1) {
						rowspancnt[ci] = rspan-1;
						rowspancell[ci] = c;
					}
					ci++;
				}
			}
		}
	}
	tab.grid = gcells;
}

build() : string
{
	ri, ci: int;

#	sys->print("\n\ninitial table\n"); printtable();
	if(tab.ncol == 0 || tab.nrow == 0)
		return "";

	setgrid();

	err := loadfonts();
	if(err != "")
		return err;

	for(cl := tab.cells; cl != nil; cl = tl cl)
		cell_geom(hd cl);

	for(ci = 0; ci < tab.ncol; ci++)
		col_geom(ci);

	for(ri = 0; ri < tab.nrow; ri++)
		row_geom(ri);

	caption_geom();

	table_geom();
#	sys->print("\n\ntable after geometry set\n"); printtable();

	h := tab.height;
	w := tab.width;
	if(tab.capcell != nil) {
		h += tab.capcell.height;
		if(tab.capcell.width > w)
			w = tab.capcell.width;
	}

	err = tk->cmd(top, canv + " configure -width " + string w
		+ " -height " + string h);
	if(len err > 0 && err[0] == '!')
		return err;
	err = create_cells();
	if(err != "")
		return err;
	err = create_border();
	if(err != "")
		return err;
	err = create_rules();
	if(err != "")
		return err;
	err = create_caption();
	if(err != "")
		return err;
	tk->cmd(top, "update");

	return "";
}

create_cells() : string
{
	for(cl := tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		cpos := c.pos;
		for(l := c.lines; l != nil; l = l.next) {
			lpos := l.pos;
			for(it := l.items; it != nil; it = it.next) {
				ipos := it.pos;
				pos := ipos.add(lpos.add(cpos));
				fnt := fontrefs[it.fontnum];
				v := tk->cmd(top, canv + " create text " + string pos.x + " "
					+ string pos.y + " -anchor nw -font " + fnt.name
					+ " -text '" + it.s);
				if(len v > 0 && v[0] == '!')
					return v;
				it.itemid = int v;
			}
		}
	}
	return "";
}

create_border() : string
{
	bd := tab.border;
	if(bd > 0) {
		x1 := string (bd / 2);
		y1 := x1;
		x2 := string (tab.width - bd/2 -1);
		y2 := string (tab.height - bd/2 -1);
		v := tk->cmd(top, canv + " create rectangle "
			+ x1 + " " + y1 + " " + x2 + " " + y2 + " -width " + string bd);
		if(len v > 0 && v[0] == '!')
			return v;
		tab.brectid = int v;
	}
	return "";
}

create_rules() : string
{
	ci, ri, i: int;
	err : string;
	c : ref Tablecell;
	for(ci = 0; ci < tab.ncol; ci++) {
		col := tab.cols[ci];
		rw := col.rule;
		if(rw > 0) {
			x := col.pos.x + col.width + TABHPAD/2 - rw/2;
			ids: list of int = nil;
			startri := 0;
			for(ri = 0; ri < tab.nrow; ri++) {
				c = tab.grid[ri][ci].cell;
				if(c.col+c.colspan-1 > ci) {
					# rule would cross a spanning cell at this column
					if(ri > startri) {
						(err, i) = create_col_rule(startri, ri-1, x, rw);
						if(err != "")
							return err;
						ids = i :: ids;
					}
					startri = ri+1;
				}
			}
			if(ri > startri)
				(err, i) = create_col_rule(startri, ri-1, x, rw);
			ids = i :: ids;
			col.ruleids = revintl(ids);
		}
	}
	for(ri = 0; ri < tab.nrow; ri++) {
		row := tab.rows[ri];
		rw := row.rule;
		if(rw > 0) {
			y := row.pos.y + row.height + TABVPAD/2 - rw/2;
			ids: list of int = nil;
			startci := 0;
			for(ci = 0; ci < tab.ncol; ci++) {
				c = tab.grid[ri][ci].cell;
				if(c.row+c.rowspan-1 > ri) {
					# rule would cross a spanning cell at this row
					if(ci > startci) {
						(err, i) = create_row_rule(startci, ci-1, y, rw);
						if(err != "")
							return err;
						ids = i :: ids;
					}
					startci = ci+1;
				}
			}
			if(ci > startci)
				(err, i) = create_row_rule(startci, ci-1, y, rw);
			ids = i :: ids;
			row.ruleids = revintl(ids);
		}
	}
	return "";
}

create_col_rule(topri, botri, x, rw: int) : (string, int)
{
	y1, y2: int;
	if(topri == 0)
		y1 = 0;
	else
		y1 = tab.rows[topri].pos.y - TABVPAD/2;
	if(botri == tab.nrow-1)
		y2 = tab.height;
	else
		y2 = tab.rows[botri].pos.y + tab.rows[botri].height + TABVPAD/2;
	sx := string x;
	v := tk->cmd(top, canv + " create line " + sx + " "
		+ string y1 + " " + sx + " " + string y2 + " -width " + string rw);
	if(len v > 0 && v[0] == '!')
		return (v, 0);
	return ("", int v);
}

create_row_rule(leftci, rightci, y, rw: int) : (string, int)
{
	x1, x2: int;
	if(leftci == 0)
		x1 = 0;
	else
		x1 = tab.cols[leftci].pos.x - TABHPAD/2;
	if(rightci == tab.ncol-1)
		x2 = tab.width;
	else
		x2 = tab.cols[rightci].pos.x + tab.cols[rightci].width + TABHPAD/2;
	sy := string y;
	v := tk->cmd(top, canv + " create line " + string x1 + " "
		+ sy + " " + string x2 + " " + sy + " -width " + string rw);
	if(len v > 0 && v[0] == '!')
		return (v, 0);
	return ("", int v);
}

create_caption() : string
{
	if(tab.capcell == nil)
		return "";
	cpos := Point(0, tab.height + 2*TABVPAD);
	for(l := tab.capcell.lines; l != nil; l = l.next) {
		lpos := l.pos;
		for(it := l.items; it != nil; it = it.next) {
			ipos := it.pos;
			pos := ipos.add(lpos.add(cpos));
			fnt := fontrefs[it.fontnum];
			v := tk->cmd(top, canv + " create text " + string pos.x + " "
				+ string pos.y + " -anchor nw -font " + fnt.name
				+ " -text '" + it.s);
			if(len v > 0 && v[0] == '!')
				return v;
			it.itemid = int v;
		}
	}
	return "";
}

# Assuming row and col geoms correct, set row, col, and cell origins
table_geom()
{
	row: ref Tablerow;
	col: ref Tablecol;
	orig := Point(0,0);
	bd := tab.border;
	if(bd > 0)
		orig = orig.add(Point(TABHPAD+bd, TABVPAD+bd));
	o := orig;
	for(ci := 0; ci < tab.ncol; ci++) {
		col = tab.cols[ci];
		col.pos = o;
		o.x += col.width + col.rule;
		if(ci < tab.ncol-1)
			o.x += TABHPAD;
	}
	if(bd > 0)
		o.x += TABHPAD + bd;
	tab.width = o.x;

	o = orig;
	for(ri := 0; ri < tab.nrow; ri++) {
		row = tab.rows[ri];
		row.pos = o;
		o.y += row.height + row.rule;
		if(ri < tab.nrow-1)
			o.y += TABVPAD;
	}
	if(bd > 0)
		o.y += TABVPAD + bd;
	tab.height = o.y;

	if(tab.capcell != nil) {
		tabw := tab.width;
		if(tab.capcell.width > tabw)
			tabw = tab.capcell.width;
		for(l := tab.capcell.lines; l != nil; l = l.next)
			l.pos.x += (tabw - l.width)/2;
	}

	for(cl := tab.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		row = tab.rows[c.row];
		col = tab.cols[c.col];
		x := col.pos.x;
		y := row.pos.y;
		w := spanned_col_width(c.col, c.col+c.colspan-1);
		case (cellhalign(c)) {
		Aright =>
			x += w - c.width;
		Acenter =>
			x += (w - c.width) / 2;
		}
		h := spanned_row_height(c.row, c.row+c.rowspan-1);
		case (cellvalign(c)) {
		Abottom =>
			y += h - c.height;
		Anone or Amiddle =>
			y += (h - c.height) / 2;
		Abaseline =>
			y += row.ascent - c.ascent;
		}
		c.pos = Point(x,y);
	}
}

spanned_col_width(firstci, lastci: int) : int
{
	firstcol := tab.cols[firstci];
	if(firstci == lastci)
		return firstcol.width;
	lastcol := tab.cols[lastci];
	return (lastcol.pos.x + lastcol.width - firstcol.pos.x);
}

spanned_row_height(firstri, lastri: int) : int
{
	firstrow := tab.rows[firstri];
	if(firstri == lastri)
		return firstrow.height;
	lastrow := tab.rows[lastri];
	return (lastrow.pos.y + lastrow.height - firstrow.pos.y);
}

# Assuming cell geoms are correct, set col widths.
# This code is sloppy for spanned columns;
# it will allocate too much space for them because
# inter-column pad is ignored, and it may make
# narrow columns wider than they have to be.
col_geom(ci: int)
{
	col := tab.cols[ci];
	col.width = 0;
	for(ri := 0; ri < tab.nrow; ri++) {
		c := tab.grid[ri][ci].cell;
		if(c == nil)
			continue;
		cwd := c.width / c.colspan;
		if(cwd > col.width)
			col.width = cwd;
	}
}

# Assuming cell geoms are correct, set row heights
row_geom(ri: int)
{
	row := tab.rows[ri];
	# find rows's global height and ascent
	h := 0;
	a := 0;
	n : int;
	for(cl := row.cells; cl != nil; cl = tl cl) {
		c := hd cl;
		al := cellvalign(c);
		if(al == Abaseline) {
			n = c.ascent;
			if(n > a) {
				h += (n - a);
				a = n;
			}
			n = c.height - c.ascent;
			if(n > h-a)
				h = a + n;
		}
		else {
			n = c.height;
			if(n > h)
				h = n;
		}
	}
	row.height = h;
	row.ascent = a;
}

cell_geom(c: ref Tablecell)
{
	width := 0;
	o := Point(0,0);
	for(l := c.lines; l != nil; l = l.next) {
		line_geom(l, o);
		o.y += l.height;
		if(l.width > width)
			width = l.width;
	}
	c.width = width;
	c.height = o.y;
	if(c.lines != nil)
		c.ascent = c.lines.ascent;
	else
		c.ascent = 0;

	al := cellhalign(c);
	if(al == Acenter || al == Aright) {
		for(l = c.lines; l != nil; l = l.next) {
			xdelta := c.width - l.width;
			if(al == Acenter)
				xdelta /= 2;
			l.pos.x += xdelta;
		}
	}
}

caption_geom()
{
	if(tab.capcell != nil) {
		o := Point(0,TABVPAD);
		width := 0;
		for(l := tab.capcell.lines; l != nil; l = l.next) {
			line_geom(l, o);
			o.y += l.height;
			if(l.width > width)
				width = l.width;
		}
		tab.capcell.width = width;
		tab.capcell.height = o.y + 4*TABVPAD;
	}
}

line_geom(l: ref Line, o: Point)
{
	# find line's global height and ascent
	h := 0;
	a := 0;
	for(it := l.items; it != nil; it = it.next) {
		fnt := fontrefs[it.fontnum];
		n := fnt.ascent;
		if(n > a) {
			h += (n - a);
			a = n;
		}
		n = fnt.height - fnt.ascent;
		if(n > h-a)
			h = a + n;
	}
	l.height = h;
	l.ascent = a;
	# set positions
	l.pos = o;
	for(it = l.items; it != nil; it = it.next) {
		fnt := fontrefs[it.fontnum];
		it.width = fnt.width(it.s);
		it.pos.x = o.x;
		o.x += it.width;
		it.pos.y = a - fnt.ascent;
	}
	l.width = o.x;
}

cellhalign(c: ref Tablecell) : int
{
	a := c.align.halign;
	if(a == Anone)
		a = tab.cols[c.col].align.halign;
	return a;
}

cellvalign(c: ref Tablecell) : int
{
	a := c.align.valign;
	if(a == Anone)
		a = tab.rows[c.row].align.valign;
	return a;
}

# table debugging
printtable()
{
	if(tab == nil) {
		sys->print("no table\n");
		return;
	}
	sys->print("Table %d rows, %d cols width %d height %d\n",
			tab.nrow, tab.ncol, tab.width, tab.height);
	if(tab.capcell != nil)
		sys->print("  caption: "); printlexes(tab.capcell.content, "    ");
	sys->print("  cols:\n"); printcols(tab.cols);
	sys->print("  rows:\n"); printrows(tab.rows);
}

align2string(al: int) : string
{
	s := "";
	case al {
		Anone => s = "none";
		Aleft => s = "left";
		Acenter => s = "center";
		Aright => s = "right";
		Ajustify => s = "justify";
		Atop => s = "top";
		Amiddle => s = "middle";
		Abottom => s = "bottom";
		Abaseline => s = "baseline";
	}
	return s;
}

printcols(cols: array of ref Tablecol)
{
	n := len cols;
	for(i := 0 ; i < n; i++) {
		c := cols[i];
		sys->print(" width %d align = %s,%s pos (%d,%d) rule %d\n", c.width,
			align2string(c.align.halign), align2string(c.align.valign), c.pos.x, c.pos.y, c.rule);
	}
}

printrows(rows: array of ref Tablerow)
{
	n := len rows;
	for(i := 0; i < n; i++) {
		tr := rows[i];
		sys->print("      row height %d ascent %d align=%s,%s pos (%d,%d) rule %d\n", tr.height, tr.ascent,
			align2string(tr.align.halign), align2string(tr.align.valign), tr.pos.x, tr.pos.y, tr.rule);
		for(cl := tr.cells; cl != nil; cl = tl cl) {
			c := hd cl;
			sys->print("        cell %d width %d height %d ascent %d align=%s,%s\n",
				c.cellid, c.width, c.height, c.ascent,
				align2string(c.align.halign), align2string(c.align.valign));
			sys->print("             pos (%d,%d) rowspan=%d colspan=%d nowrap=%d\n",
				c.pos.x, c.pos.y, c.rowspan, c.colspan, c.nowrap);
			printlexes(c.content, "        ");
			printlines(c.lines);
		}
	}
}

printlexes(lexes: array of ref Lex, indent: string)
{
	for(i := 0; i < len lexes; i++)
		sys->print("%s%s\n", indent, html->lex2string(lexes[i]));
}

printlines(l: ref Line)
{
	if(l == nil)
		return;
	sys->print("lines: \n");
	while(l != nil) {
		sys->print("          Line: pos (%d,%d), height %d ascent %d\n", l.pos.x, l.pos.y, l.height, l.ascent);
		printitems(l.items);
		l = l.next;
	}
}

printitems(i: ref Item)
{
	while(i != nil) {
		sys->print("            '%s' id %d fontnum %d w %d, pos (%d,%d)\n", i.s, i.itemid, i.fontnum,
			i.width, i.pos.x, i.pos.y);
		i = i.next;
	}
}

printgrid(g: array of array of ref Tablegcell)
{
	nr := len g;
	nc := len g[0];
	for(r := 0; r < nr; r++) {
		for(c := 0; c < nc; c++) {
			x := g[r][c];
			cell := x.cell;
			suf := " ";
			if(x.drawnhere == 0)
				suf = "*";
			if(cell == nil)
				sys->print("     %s", suf);
			else
				sys->print("%5d%s", cell.cellid, suf);
		}
		sys->print("\n");
	}
}

# Return (table in correct format, error string)
cook(parent: string, fmt: int, args: string) : (ref Celem, string)
{
	(spec, err) := getspec(parent, args);
	if(err != "")
		return (nil, err);
	if(fmt == FHtml)
		return cookhtml(spec);
	else
		return cooklatex(spec);
}

# Return (table as latex, error string)
# BUG: cells spanning multiple rows not handled correctly
# (all their contents go in the first row of span, though hrules properly broken)
cooklatex(spec: array of ref Lex) : (ref Celem, string)
{
	s : string;
	ci, ri: int;
	err := parsetab(spec);
	if(err != "")
		return (nil, err_ret(err));

	setgrid();

	ans := ref Celem(SGML, "", nil, nil, nil, nil);
	cur : ref Celem = nil;
	cur = add(ans, cur, specialce("\\begin{tabular}[t]{" + lcolspec() + "}\n"));
	if(tab.border) {
		if(tab.border == 1)
			s = "\\hline\n";
		else
			s = "\\hline\\hline\n";
		cur = add(ans, cur, specialce(s));
	}
	for(ri = 0; ri < tab.nrow; ri++) {
		row := tab.rows[ri];
		ci = 0;
		anyrowspan := 0;
		for(cl := row.cells; cl != nil; cl = tl cl) {
			c := hd cl;
			while(ci < c.col) {
				cur = add(ans, cur, specialce("&"));
				ci++;
			}
			mcol := 0;
			if(c.colspan > 1) {
				cur = add(ans, cur, specialce("\\multicolumn{" + string c.colspan + "}{" +
						lnthcolspec(ci, ci+c.colspan-1, c.align.halign) + "}{"));
				mcol = 1;
			}
			else if(c.align.halign != Anone) {
				cur = add(ans, cur, specialce("\\multicolumn{1}{" +
						lnthcolspec(ci, ci, c.align.halign) + "}{"));
				mcol = 1;
			}
			if(c.rowspan > 1)
				anyrowspan = 1;
			cur = addlconvlines(ans, cur, c);
			if(mcol) {
				cur = add(ans, cur, specialce("}"));
				ci += c.colspan-1;
			}
		}
		while(ci++ < tab.ncol-1)
			cur = add(ans, cur, specialce("&"));
		if(ri < tab.nrow-1 || row.rule > 0 || tab.border > 0)
			cur = add(ans, cur, specialce("\\\\\n"));
		if(row.rule) {
			if(anyrowspan) {
				startci := 0;
				for(ci = 0; ci < tab.ncol; ci++) {
					c := tab.grid[ri][ci].cell;
					if(c.row+c.rowspan-1 > ri) {
						# rule would cross a spanning cell at this row
						if(ci > startci)
							cur = add(ans, cur, specialce("\\cline{" +
								string (startci+1) + "-" + string ci + "}"));
						startci = ci+1;
					}
				}
				if(ci > startci)
					cur = add(ans, cur, specialce("\\cline{" +
						string (startci+1) + "-" + string ci + "}"));
			}
			else
				cur = add(ans, cur, specialce("\\hline\n"));
		}
	}
	if(tab.border) {
		if(tab.border == 1)
			s = "\\hline\n";
		else
			s = "\\hline\\hline\n";
		cur = add(ans, cur, specialce(s));
	}
	cur = add(ans, cur, specialce("\\end{tabular}\n"));

	if(ans != nil)
		ans = ans.contents;
	return (ans, "");
}

lcolspec() : string
{
	ans := "";
	for(ci := 0; ci < tab.ncol; ci++)
		ans += lnthcolspec(ci, ci, Anone);
	return ans;
}

lnthcolspec(ci, cie, al: int) : string
{
	ans := "";
	if(ci == 0) {
		if(tab.border == 1)
			ans = "|";
		else if(tab.border > 1)
			ans = "||";
	}
	col := tab.cols[ci];
	if(al == Anone)
		al = col.align.halign;
	case al {
	Acenter =>
		ans += "c";
	Aright =>
		ans += "r";
	* =>
		ans += "l";
	}
	if(ci == cie) {
		if(col.rule == 1)
			ans += "|";
		else if(col.rule > 1)
			ans += "||";
	}
	if(cie == tab.ncol - 1) {
		if(tab.border == 1)
			ans += "|";
		else if(tab.border > 1)
			ans += "||";
	}
	return ans;
}

addlconvlines(par, tail: ref Celem, c: ref Tablecell) : ref Celem
{
	line := c.lines;
	if(line == nil)
		return tail;
	multiline := 0;
	if(line.next != nil) {
		multiline = 1;
		val := "";
		case cellvalign(c) {
		Abaseline or Atop => val = "[t]";
		Abottom => val = "[b]";
		}
		hal := "l";
		case cellhalign(c) {
		Aright => hal = "r";
		Acenter => hal = "c";
		}
		# The @{}'s in the colspec eliminate extra space before and after result
		tail = add(par, tail, specialce("\\begin{tabular}" + val + "{@{}" + hal + "@{}}\n"));
	}
	while(line != nil) {
		for(it := line.items; it != nil; it = it.next) {
			fnum := it.fontnum;
			f := fnum / NSIZE;
			sz := fnum % NSIZE;
			grouped := 0;
			if((f != DefFont || sz != DefSize) && (it.prev!=nil || it.next!=nil)) {
				tail = add(par, tail, specialce("{"));
				grouped = 1;
			}
			if(f != DefFont) {
				fcmd := "";
				case f {
				Roman => fcmd = "\\rmfamily ";
				Italic => fcmd = "\\itshape ";
				Bold => fcmd = "\\bfseries ";
				Type => fcmd = "\\ttfamily ";
				}
				tail = add(par, tail, specialce(fcmd));
			}
			if(sz != DefSize) {
				szcmd := "";
				case sz {
				Size6 => szcmd = "\\footnotesize ";
				Size8 => szcmd = "\\small ";
				Size10 => szcmd = "\\normalsize ";
				Size12 => szcmd = "\\large ";
				Size16 => szcmd = "\\Large ";
				}
				tail = add(par, tail, specialce(szcmd));
			}
			tail = add(par, tail, textce(it.s));
			if(grouped)
				tail = add(par, tail, specialce("}"));
		}
		ln := line.next;
		if(multiline && ln != nil)
			tail = add(par, tail, specialce("\\\\\n"));
		line = line.next;
	}
	if(multiline)
		tail = add(par, tail, specialce("\\end{tabular}\n"));
	return tail;
}

# Return (table as html, error string)
cookhtml(spec: array of ref Lex) : (ref Celem, string)
{
	n := len spec;
	ans := ref Celem(SGML, "", nil, nil, nil, nil);
	cur : ref Celem = nil;
	for(i := 0; i < n; i++) {
		tok := spec[i];
		if(tok.tag == Data)
			cur = add(ans, cur, textce(tok.text));
		else {
			s := html->lex2string(spec[i]);
			cur = add(ans, cur, specialce(s));
		}
	}
	if(ans != nil)
		ans = ans.contents;
	return (ans, "");
}

textce(s: string) : ref Celem
{
	return ref Celem(Text, s, nil, nil, nil, nil);
}

specialce(s: string) : ref Celem
{
	return ref Celem(Special, s, nil, nil, nil, nil);
}

add(par, tail: ref Celem, e: ref Celem) : ref Celem
{
	if(tail == nil) {
		par.contents = e;
		e.parent = par;
	}
	else
		tail.next = e;
	e.prev = tail;
	return e;
}

fullname(parent, file: string): string
{
	if(len parent==0 || (len file>0 && (file[0]=='/' || file[0]=='#')))
		return file;

	for(i:=len parent-1; i>=0; i--)
		if(parent[i] == '/')
			return parent[0:i+1] + file;
	return file;
}