Wit

Tables

A Wit table is one node. Hand it rows and it draws a full <table> — caption, header, body — with no grid of pipes to align. Rows can come as inline text, as a collection of records, or as

/
child nodes when cells need links and emphasis. tableis dispatched to a dedicated renderer, so it does not follow the generic id/class allowlist — the parameters below are the whole surface.

Which form?

Pick a form
You haveUseHeader comes from
rows typed out as textinline CSVrow 0 by default
a collection + column orderschema-as-arraythe schema keys
a collection + custom labelsschema-as-recordthe schema labels
a collection and want auto columnsrows-by-refthe first record's keys
cells with links or emphasisbody form (row/col nodes)first row unless header false

Inline rows

Pass a list of rows in the rows parameter. Row 0 is the header; every later row is a body row. A caption rides above the table.

source
@table |rows [[Format, Extension], [Web page, .html], [Markdown, .md], [Print, .pdf]]| |caption Output formats|
result
Output formats
FormatExtension
Web page.html
Markdown.md
Print.pdf

Emits:

<table><caption>Output formats</caption><thead><tr><th>Format</th><th>Extension</th></tr></thead><tbody><tr><td>Web page</td><td>.html</td></tr><tr><td>Markdown</td><td>.md</td></tr><tr><td>Print</td><td>.pdf</td></tr></tbody></table>

Another, with the caption pipe closed before table@:

@table |rows [[Client, Hours], [One to One, 62.75], [Couple, 0.75]]| |caption Practicum| table@
→ <table><caption>Practicum</caption><thead><tr><th>Client</th><th>Hours</th></tr></thead><tbody><tr><td>One to One</td><td>62.75</td></tr><tr><td>Couple</td><td>0.75</td></tr></tbody></table>
Fixture:

19-tables/inline-csv.wit.

Drop the header with |header false| — the first row becomes a plain body row:

@table |rows [[Web page, .html], [Markdown, .md]]| |header false|
→ <table><tbody><tr><td>Web page</td><td>.html</td></tr><tr><td>Markdown</td><td>.md</td></tr></tbody></table>
Fixture:

19-tables/no-header.wit.

From a data collection

Point rows at a collection of records and each record becomes a row. With no schema, columns are auto-derived from the first record's keys. The renderer prefers the expander's typed collection over literal text.

source
#packages: [ { name - parser, role - reads source }, { name - runtime, role - resolves data } ]

@table |rows @packages|
result
namerole
parserreads source
runtimeresolves data

Emits headings name / role over the two records. A second example, with the full emit:

#sales: [ { region - West, total - 100 }, { region - East, total - 90 } ]

@table |rows @sales| table@
→ <table><thead><tr><th>region</th><th>total</th></tr></thead><tbody><tr><td>West</td><td>100</td></tr><tr><td>East</td><td>90</td></tr></tbody></table>

To choose and label the columns yourself, add a schema.

Schema-as-array (positional)

Schema keys pick record fields in order and become the headings; every row is data (no header consumed).

source
#sites: [{ name - Dunmore Head, status - operational }, { name - Carrick Point, status - maintenance }] !!

@table |schema [name, status]| |rows @sites|
result
namestatus
Dunmore Headoperational
Carrick Pointmaintenance

Emits headings name / status over the two records. Fixture: 19-tables/schema-array.wit.

Schema-as-record (labelled)

The record key selects the field; the record valueis the column heading.

source
@table |schema { name - Site, status - Status }| |rows [[Dunmore Head, operational], [Carrick Point, maintenance]]|
result
SiteStatus
Dunmore Headoperational
Carrick Pointmaintenance

Emits headings Site / Status (not name / status). The same works pointing at a record collection — |schema { name - Package, role - Role }| over @packages. Fixture: 19-tables/schema-record.wit.

Loading the records from a file at build time works the same way; see Data.

Rich cells: the body form

The only form whose cells hold rich content — links, emphasis, nested nodes — rather than plain strings. Leave off the rows parameter and the renderer reads the body: a @row per row, a @col per cell.

source
@table |caption Modules|
  @row @col Module col@ @col Notes col@ row@
  @row @col Parser col@ @col Reads *source*; see @a |href /docs/reference/syntax/| the syntax a@. col@ row@
table@
result
Modules
ModuleNotes
ParserReads source; see the syntax.

Emits:

<table><caption>Modules</caption><thead><tr><th>Module</th><th>Notes</th></tr></thead><tbody><tr><td>Parser</td><td>Reads <strong>source</strong>; see <a href="/docs/reference/syntax/">the syntax</a>.</td></tr></tbody></table>

The first @row is the header — its cells render as <th> — unless you pass header false. A @th cell is always a header cell, even inside a body row. The HTML-style names @tr / @td / @th work as aliases for @row / @col, and the whole row can sit on one line: @row @col a col@ @col b col@ row@.

@row and @col are interchangeable: the outer node is the row, the inner one the cell, whichever keyword you use. So @col @row a row@ @row b row@ col@ builds the same row as @row @col a col@ @col b col@ row@.

Data-driven rows with rich cells

This is the form the others can't reach: rows generated from data, with cells that still hold nodes. Wrap the @row line in (each … as …) and pull fields into each cell body.

source
#modules: [ { name - Parser, status - stable }, { name - Runtime, status - stable } ]

@table |caption Modules|
  @row @col Module col@ @col Status col@ row@
  (each @modules as m)
  @row @col @m.name col@ @col @em @m.status em@ col@ row@
  (end)
table@
result
Modules
ModuleStatus
Parserstable
Runtimestable

Emits:

<table><caption>Modules</caption><thead><tr><th>Module</th><th>Status</th></tr></thead><tbody><tr><td>Parser</td><td><em>stable</em></td></tr><tr><td>Runtime</td><td><em>stable</em></td></tr></tbody></table>

The header row stays fixed; the loop appends one body row per record. Put an (if …) inside the loop to filter which rows appear — here only the stable modules:

source
#modules: [ { name - Parser, status - stable }, { name - Sandbox, status - draft } ]

@table |caption Stable modules|
  @row @col Module col@ @col Status col@ row@
  (each @modules as m)
  (if @m.status is stable)
  @row @col @m.name col@ @col @em @m.status em@ col@ row@
  (end)
  (end)
table@
result
Stable modules
ModuleStatus
Parserstable

The draft row drops out; only Parser survives:

<table><caption>Stable modules</caption><thead><tr><th>Module</th><th>Status</th></tr></thead><tbody><tr><td>Parser</td><td><em>stable</em></td></tr></tbody></table>

One limitation: data resolves in the body, not in a parameter

A field reference resolves inside a cell body@col @m.name col@ works — but not inside a parameter. A link's hrefcannot yet be @m.url:

@col @a |href @m.url| @m.name a@ col@   ✗ the href is not resolved
@col @a |href /modules/parser| @m.name a@ col@   ✓ literal href, data in the text

Keep every data-driven value in the cell body and write parameters as literals.

Header and caption controls

Header controls
ParamEffect
caption textprepends a caption element
header falseno header row
header Nuse row index N as the header
header [a b c
explicit header cells — all rows become data

Edge cases and value rules

Edge cases
CaseBehaviourFixture
empty or missing rowsempty table element19 empty
multi-line cell contentpreserved19 multiline-cell
number or boolean cellString(value)19 tables
null cellempty cell19 tables
comma inside a cellsplits the cell — avoidverified
quoting a cellnot supported inline — quotes stay literalverified
@table table@ → <table></table>

Centering is theme, not renderer

The default HTML pathway centres tables via .wit-doc table { border-collapse: collapse; margin: 1em auto } in the theme. --raw ships a reset-only base, so it does not centre — add your own CSS. Fixture: 19-tables/* and the theme.

See also