Validation Rules
The IR validator enforces structural correctness before any writer runs.
Every error carries a rule id, a location (section, row group, row, column), and a message. Rule ids are the strings below, both in Display output ([COL_COUNT] …) and when errors are serialized.
Limits
| Rule | Description |
|---|---|
LIMIT_EXCEEDED |
config.table_cols > 16,384, a section has more than 1,048,576 rows, or the table has more than 50 million cells. Checked first; the other checks are skipped when a limit is exceeded. Limits are configurable from Rust (validate_with_limits). |
Structural Rules
| Rule | Description |
|---|---|
COL_COUNT |
Every row (header, data and summary) has exactly config.table_cols cell objects; spanned positions hold placeholders |
ROW_COUNT |
thead has config.header_rows rows; data rows (not summary rows) total config.body_rows |
COLSPEC_LENGTH |
column_spec has config.table_cols entries |
STUB_CONTIGUOUS |
The stub is a block at the left: config.stub_cols is at most config.table_cols, and no cell flagged is_stub starts to the right of the stub columns |
STYLE_REFS_VALID |
Every style_id refers to a style definition or composition |
FOOTNOTE_REFS_VALID |
Every footnote_mark refers to a defined footnote, wherever it appears: cells (including summary rows), header lines, group labels, and the notes themselves |
SUMMARY_REQUIRES_STUB |
A group with summary rows requires config.stub_cols >= 1 |
UNKNOWN_VALUE |
A keyword field holds a value outside its allowed set (see Keyword values) |
INVALID_COLOR |
A color field is not a CSS color (see Colors) |
INVALID_LENGTH |
A length field does not parse, or uses a form its field does not allow (see Lengths) |
INVALID_STYLE_ID |
A style id (a key of styles.defs or styles.compositions) is empty or uses a character other than ASCII letters, digits, _ and - |
Span Rules
Spans are checked per section: the header, each group’s data rows, and each group’s summary rows. A span never crosses a section.
| Rule | Description |
|---|---|
SPAN_ZERO_VALUE |
colspan or rowspan is 0 |
SPAN_OVERFLOW_RIGHT |
col + colspan > table_cols: span exceeds the right edge |
SPAN_OVERFLOW_BOTTOM |
row + rowspan > section rows: span exceeds the section |
SPAN_OVERLAP |
Two spans claim the same (row, col) position |
SPAN_PLACEHOLDER_MISMATCH |
A placeholder that no cell’s colspan or rowspan covers |
SPAN_GAP |
A grid position with no cell object at all. A row with too few cells is already reported as COL_COUNT (and its section’s spans are then not checked), so validate() reports this only through OccupancyGrid::materialize used directly |
SPAN_PLACEHOLDER_HAS_CONTENT |
A placeholder cell has non-empty content |
Keyword values
Keyword fields are matched ignoring case and surrounding whitespace, and the aliases below are accepted. An unrecognized value still parses: it is kept verbatim (and round-trips unchanged) and reported as UNKNOWN_VALUE, so every bad value is listed at once. Known values are written back in their canonical spelling.
| Field | Allowed values (aliases) |
|---|---|
column_spec[].align, text_align |
left (start), center (centre), right (end), justify, char |
vertical_align |
top, middle, bottom, baseline, super, sub |
row.role |
column_label, spanner_label, summary_row (summary), grand_summary_row (grand_summary) |
cell.scope |
col, colgroup, row, rowgroup |
config.page_break_mode |
avoid, allow, force-between-groups (force_between_groups) |
border.*.style |
none, hidden, solid, dashed, dotted, double |
font_weight |
normal, bold, bolder, lighter, or a number 1–1000 (JSON string or number) |
font_style |
normal, italic, oblique |
text_transform |
none, uppercase, lowercase, capitalize |
text_decoration |
none, underline, overline, line-through |
white_space |
normal, nowrap, pre, pre-wrap, pre-line, break-spaces |
word_break |
normal, break-all, keep-all, break-word |
overflow, config.container_overflow |
visible, hidden, clip, scroll, auto |
text_overflow |
clip, ellipsis |
conditional selector.row_parity |
even, odd |
conditional selector.scope |
tbody (body), thead (head), table (all) |
typed_value.type, cell.data_type |
string, number, integer, boolean, date, datetime, time, duration (plus common aliases such as int, bool, float) |
Colors
color, background_color and border.*.color (in style definitions, composition overrides and conditional styles) accept, ignoring case and surrounding whitespace:
| Form | Examples |
|---|---|
| CSS named color (all 148) | red, steelblue, RebeccaPurple, grey |
transparent |
paints nothing |
| Hex | #abc, #abcd, #aabbcc, #aabbccdd |
rgb() / rgba(), comma syntax |
rgb(255, 0, 0), rgba(255, 0, 0, 0.5), rgb(100%, 50%, 0%) |
rgb() / rgba(), space syntax |
rgb(255 0 0), rgb(255 0 0 / 50%) |
hsl() / hsla() |
hsl(120, 100%, 25%), hsl(120deg 100% 25% / 0.5); hue units deg, grad, rad, turn |
Out-of-range channels clamp, as in CSS. Writers receive the parsed color, never the source text: HTML and SVG emit #RRGGBB (or rgba() when translucent), Typst keeps alpha as #RRGGBBAA, and formats without alpha (LaTeX, RTF, DOCX, PPTX, ANSI) drop it. A fully transparent color paints nothing in every format.
Lengths
A length is a finite number followed by a unit, px, pt, em, rem, in, cm, mm, % or fr (case-insensitive, no space before the unit), or auto. A bare 0 is allowed; any other number needs a unit. Each field accepts only the forms that make sense for it:
| Field | Units | % |
fr |
auto |
Negative |
|---|---|---|---|---|---|
column_spec[].width |
✓ | ✓ | ✓ | ✓ | |
column_spec[].min_width, max_width |
✓ | ✓ | |||
config.table_width, container_width, container_height |
✓ | ✓ | ✓ | ||
image width, height (any content) |
✓ | ✓ | ✓ | ||
style min_width, max_width, padding.* |
✓ | ✓ | |||
style indent |
✓ | ✓ | ✓ | ||
style border.*.width |
✓ | ||||
style font_size |
✓ | ✓ |
font_size also accepts the CSS size keywords xx-small, x-small, small, medium, large, x-large, xx-large, xxx-large, smaller and larger.
How Validation Works
The validator materializes a 2D occupancy grid for each section:
- Allocate a
rows × colsgrid - Walk every cell in document order
- For each cell with
rowspan=R, colspan=Cat position(r, c):- Claim all positions
(r..r+R, c..c+C) - If already occupied →
SPAN_OVERLAPerror - If out of bounds →
SPAN_OVERFLOW_RIGHT/SPAN_OVERFLOW_BOTTOMerror
- Claim all positions
- Report any position no cell covers as
SPAN_GAP
Error Output
Each error includes the rule name, location, and a human-readable message:
[COL_COUNT] tbody group 0 row 0 has 4 cells but config.table_cols is 3
[SPAN_OVERFLOW_RIGHT] cell at (row=1, col=4) has colspan=3 but table has 5 columns
[SPAN_PLACEHOLDER_HAS_CONTENT] placeholder cell at (row=2, col=1) has non-empty content
[INVALID_LENGTH] column_spec[1].width is "-5px", which is not valid here: it is negative (expected a non-negative length (px, pt, em, rem, in, cm, mm), or a percentage, or a fraction (fr), or auto)
Usage
gridwell validate table.json
# Exit code 0 = valid, 1 = errors foundlet errors = table.validate();
// errors: Vec<ValidationError>errors = table.validate()
# errors: list[str]errors <- gw_validate(tbl)
# errors: character vector