Tables

Tables

The table model is similar to that of HTML. You combine cells into rows which form a table to be typeset. In HTML for example you can create a simple table with this code:

<table>
  <tr>
    <td>Hello,</td>
    <td>world</td>
  </tr>
  <tr>
    <td>Hello,</td>
    <td>user</td>
  </tr>
</table>

So a table is built row by row. In boxes and glue, the table consists of table rows (TableRow) and cells (TableCell). The contents of each cell is a sequence of Paragraph structs and the contents of a row is a sequence of cells.

Table cell

type TableCell struct {
	BackgroundColor   *color.Color
	BorderTopColor    *color.Color
	BorderBottomColor *color.Color
	BorderLeftColor   *color.Color
	BorderRightColor  *color.Color
	Contents []any
	BorderTopWidth    bag.ScaledPoint
	BorderBottomWidth bag.ScaledPoint
	BorderLeftWidth   bag.ScaledPoint
	BorderRightWidth  bag.ScaledPoint
	CalculatedWidth   bag.ScaledPoint
	CalculatedHeight  bag.ScaledPoint
	// SpecifiedWidth is the cell's declared width (CSS `width` on a
	// <td>/<th>, percentages already resolved against the table width).
	// Zero means "not specified". CSS 2.1 §17.5.2.2 folds it into the
	// automatic layout as a lower bound for the column, so a cell can
	// widen its column beyond the content but never clips content that
	// needs more room. Cells spanning several columns are ignored.
	SpecifiedWidth bag.ScaledPoint
	// MinHeight is the cell's declared height (CSS `height` on a
	// <td>/<th>), measured on the outside of the cell including padding
	// and borders. Like the row's MinHeight it is a lower bound only
	// (CSS 2.1 §17.5.3), zero means "not specified".
	MinHeight     bag.ScaledPoint
	HAlign        HorizontalAlignment
	VAlign        VerticalAlignment
	ExtraColspan  int
	ExtraRowspan  int
	PaddingTop    bag.ScaledPoint
	PaddingBottom bag.ScaledPoint
	PaddingLeft   bag.ScaledPoint
	PaddingRight  bag.ScaledPoint
	IsHeader bool // true for <th> cells
	// ID is the source element's id, copied onto the vlist the cell builds.
	ID string
}

Table row

type TableRow struct {
	// ID is the source element's id, copied onto the hlist the row builds.
	ID string
	Cells            []*TableCell
	CalculatedHeight bag.ScaledPoint
	// MinHeight is the declared height of the row (CSS `height` on a
	// <tr>). CSS 2.1 §17.5.3 treats it as a lower bound: the row grows
	// to fit its cells but never shrinks below it.
	MinHeight bag.ScaledPoint
	// FixedHeight, when not zero, is the height of the row whatever its
	// cells hold (Word's "exact" row height), and MinHeight on the row and
	// its cells is ignored. Content that does not fit draws past the row in
	// the direction its VAlign leaves open, while background and borders
	// keep the row height. Rows are drawn in order, so content that runs
	// into the next row is painted over by that row's background. A row
	// with a fixed height never breaks inside, and a rowspan's missing
	// height goes to the rows of the span that are not fixed.
	FixedHeight bag.ScaledPoint
	VAlign      VerticalAlignment
	// BreakInside lets the row break across pages or frames: BuildTable
	// gives its hlist a RowSplitter in Attributes["_split"]. A row a rowspan
	// reaches into or out of stays whole. When a nested table in the row
	// breaks, its header rows are not repeated in the rest.
	BreakInside bool
}
type Table struct {
	FontFamily *FontFamily
	Rows    TableRows
	ColSpec []ColSpec
	// BorderModel is how the borders of neighbouring cells meet. The zero
	// value is BorderModelCollapse: the border between two cells is drawn
	// once, as wide as the wider of the two, and the table's own border
	// (BorderTopWidth and friends) is merged into the cells on the edge.
	// BorderModelSeparate draws every border of every cell and keeps the
	// cells apart by BorderSpacingHorizontal and BorderSpacingVertical,
	// also between the outer cells and the edge of the table; the table's
	// own border is then not drawn here but left to the caller, who knows
	// the table's padding and background.
	BorderModel             BorderModel
	BorderSpacingHorizontal bag.ScaledPoint
	BorderSpacingVertical   bag.ScaledPoint
	BorderTopWidth          bag.ScaledPoint
	BorderRightWidth        bag.ScaledPoint
	BorderBottomWidth       bag.ScaledPoint
	BorderLeftWidth         bag.ScaledPoint
	BorderTopColor          *color.Color
	BorderRightColor        *color.Color
	BorderBottomColor       *color.Color
	BorderLeftColor         *color.Color
	MaxWidth bag.ScaledPoint
	FontSize bag.ScaledPoint
	Leading  bag.ScaledPoint
	Stretch    bool
	HeaderRows int // number of initial rows that are header rows (from <thead>)
	FooterRows int // number of trailing rows that are footer rows (from <tfoot>)
}

HeaderRows specifies how many of the initial rows are header rows (typically from <thead>). When a table spans multiple pages, these rows are automatically repeated at the top of each continuation page. The header repetition is handled by the page breaker (e.g. htmlbag’s outputGroupNodes): BuildTable stores a closure in the VList’s attributes that can rebuild the header rows fresh for each new page.

Stretch controls whether the table expands to fill MaxWidth. When false, the table is only as wide as its content requires.

func (fe *Document) BuildTable(tbl *Table) ([]*node.VList, error)

BuildTable returns a single VList containing all table rows. If HeaderRows is set, the VList carries metadata that enables the page breaker to split the table across pages and repeat headers automatically. The caller does not need to handle splitting: the page breaker does it during page output.

Rows that break inside

A row is set as one piece. A page breaker moves it to the next page whole, and a row taller than the page runs past its foot. A row with BreakInside set may break inside instead: BuildTable gives its hlist a RowSplitter in Attributes["_split"], which the page breaker calls with the room that is left.

type RowSplitter func(avail bag.ScaledPoint) (first, rest *node.HList, ok bool)

first is the part of the row that fits, rest what is left of every cell. The rest has a splitter of its own, so a row can run over as many pages as it needs. ok is false when not even one line of the row fits, and the page breaker moves the row as it would any other. Calling the splitter leaves the row as it is, so a page breaker can try a break and then choose another one.

The cells break between their lines, between paragraphs and between the rows of a nested table, and inside a nested row that may break as well, but never between the rows a rowspan joins. Both parts keep the cells’ borders, padding and background, every cell as tall as the tallest, and the rest draws the line above the row again.

Some rows stay whole even with BreakInside: a row that a rowspan reaches into or out of, since the spanning cell is drawn with its first row. A list packed to a fixed height is not broken either, and the header rows of a nested table are not repeated after a break.