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:

  1. Allocate a rows × cols grid
  2. Walk every cell in document order
  3. For each cell with rowspan=R, colspan=C at position (r, c):
    • Claim all positions (r..r+R, c..c+C)
    • If already occupied → SPAN_OVERLAP error
    • If out of bounds → SPAN_OVERFLOW_RIGHT / SPAN_OVERFLOW_BOTTOM error
  4. 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 found
let errors = table.validate();
// errors: Vec<ValidationError>
errors = table.validate()
# errors: list[str]
errors <- gw_validate(tbl)
# errors: character vector