ref: 60ecd07e6d3f5786c8723dc9172c35d580fdadc8
dir: /appl/wm/brutus/table.b/
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; }