Escape

ReportSchema

The document shape. Every object with fixed keys is a closed set, so an unknown key anywhere is a definition error. The same shape ships as a JSON Schema, quario/schema.json, which Validating definitions sets beside plan.

The document

Field Type Meaning
$schema string Where an editor finds the document’s JSON Schema. The engine checks the type and reads nothing else. Top level only.
data string Required. The JSONPath selecting the rows the report walks.
params Record<string, JsonValue | ParamDeclaration> The report’s parameters, read as $.params. A bare value is the parameter’s value, and a plain object declares one. A render may supply either.
where `=${string}` Drops rows that fail it, before aggregates.
sort SortKey[] Orders the whole set, applied left to right.
take number Keeps this many rows after the sort. Integer >= 1. Dropped rows leave aggregates and runners too.
aggregates Record<string, ReducerSpec> Report-level aggregates, injected bare under $.
run Record<string, ReducerSpec> Running accumulators, read as run.<name> on detail rows.
style ReportStyleDeclarations The report default. family and size only.
header Item[] or { height, items } The report header band. The object form pins a box from the page top.
empty Item[] Replaces the whole body when no rows remain.
columns number Flows the body in this many page columns. Integer >= 2, and no nesting.
groups Group[] Group bands, outermost first.
detail Item[] or TableDetail The per-row band.
footer Item[] The report footer band.
page PageBands Furniture paginated targets draw on every page.
required Partial<Record<RequirableName, true>> Declarations this document can’t do without, checked by plan against the targets you name.

ParamDeclaration

signature
interface ParamDeclaration {
  default?: JsonValue
  type?: ParamType
  label?: string
}

type ParamType = 'string' | 'number' | 'boolean' | 'array' | 'object'

A declaration without a default is a required parameter, and the engine refuses a render that supplies none before any expression runs. A null default beside a type makes it optional. label is the text a person reads for the parameter, and the engine never renders it. A value that’s itself an object goes under default, because any plain object here is a declaration.

Group

signature
interface Group {
  name: string
  by: `=${string}`
  break?: 'before' | 'between' | 'after' | 'around'
  reset?: 'page'
  label?: string
  collapsed?: boolean
  columns?: number
  sort?: SortKey[]
  take?: number
  instances?: { sort?: SortKey[]; take?: number }
  aggregates?: Record<string, ReducerSpec>
  run?: Record<string, ReducerSpec>
  header?: Item[]
  footer?: Item[]
}

name is the handle its bands and everything nested inside them read. It must be a plain identifier, unique across the document, and can’t be @, $, run, loop or page. A group aggregate can’t take the name key. It folds the rows alone, and reading a sibling aggregate through the group’s handle is a render error.

break says where this group’s instances turn a page. Every boundary between consecutive instances turns. "before" adds the run’s leading edge, "after" its trailing edge, "around" both, and "between" neither. reset: "page" restarts page.number / page.total at each instance and turns no page itself, so it requires break to be "before" or "around".

instances orders and cuts the group’s instances, as its sort and take order and cut the rows inside each one. An instances.sort key reads the group’s own handle, the handles around it and $. @ and run are unbound there. A dropped instance never renders, and the aggregates around it still count it. Without instances, instances render in first-seen order. Selecting rows has the order of operations.

label names an instance for navigation rather than for the page. It’s a template resolved once per instance in group scope. There the instance’s own handle and $ bind, and @ stays unbound as in any group band. A target with somewhere to put a name spends it there: a PDF bookmark, the viewer’s outline row, a worksheet tab. The others withdraw it, so no page changes. A value that isn’t a template string is a definition error, and a hollow instance stays unlisted whether it declares one or not.

collapsed asks that the instance open shut. It renders its header and footer and leaves out the content between them. Nothing touches the aggregates, so the rows stay in the partition and a footer still totals them. It’s one literal, true or false, and an expression is a definition error.

TableDetail

signature
interface TableDetail {
  header?: TableHeaderBox
  row?: TableRow
  columns: [TableColumn, ...TableColumn[]]
  total?: TotalBlock
}

TableColumn

signature
interface TableColumn extends Omit<Cell, 'style'> {
  header?: string | TableHeader
  width?: number
  style?: CellStyleDeclarations
}

width is a literal percentage from 0 to 100, and it’s the border-box share.

TableHeader, TableRow, TableHeaderBox

signature
interface TableHeader { value: CellValue; style?: StyleDeclarations; span?: number }
interface TableRow { visible?: ExpressionValue<boolean>; style?: RowStyleDeclarations }
interface TableHeaderBox { style?: RowStyleDeclarations }

A row’s style resolves onto that row’s cells on every target.

TotalBlock, TotalRow, TotalCell

signature
interface TotalBlock extends TableRow { rows: [TotalRow, ...TotalRow[]] }
interface TotalRow {
  cells: [TotalCell, ...TotalCell[]]
  visible?: ExpressionValue<boolean>
  style?: RowStyleDeclarations
}
type TotalCell = Omit<Cell, 'style'> & { span?: number; style?: CellStyleDeclarations }

What every total row shares goes on the block. Each row must cover the columns exactly once, counting span.

Items

Cell and TextItem

signature
interface Cell {
  value: CellValue
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}
interface TextItem extends Omit<Cell, 'style'> {
  type: 'text'
  style?: StackedStyleDeclarations
}
type CellValue = string | StyledRun[]
interface StyledRun {
  value: string
  style?: RunStyleDeclarations
}

A cell value is one template string or a non-empty array of styled runs. A cell written as one string is one run, so a one-run array says exactly what that string says. A run’s style is literal only, never an expression, and a run has no visible, since {{#if}} already conditions its text. A CR, LF, or CRLF in the evaluated text is a hard line break.

ImageItem

signature
interface ImageItem {
  type: 'image'
  source: `=${string}`
  fit?: 'natural' | 'width'
  alt?: string
  visible?: ExpressionValue<boolean>
  style?: ImageStyleDeclarations
}

source must be an expression resolving to PNG, JPEG, or SVG bytes. An SVG states its size through absolute width and height on its root, or a viewBox, and the XLSX and DOCX targets refuse one. A nullish source skips the item, so an optional logo needs no guard. style covers background, align, and the box and flow spacing, and nothing else. alt is one template string, the one cell value the run form doesn’t reach.

SplitItem

signature
interface SplitItem {
  type: 'split'
  slots: [SplitSlot, SplitSlot, ...SplitSlot[]]
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}

Two slots at minimum. A slot is a text or image item plus an optional width percentage, and it takes no flow spacing. Splits don’t nest, and neither appears in a table or total cell. The split’s style is the layer under its slots, and it ranks above the band’s role default.

GridItem

signature
interface GridItem {
  type: 'grid'
  columns: [GridColumn, GridColumn, ...GridColumn[]]
  rows: [GridRow, ...GridRow[]]
  visible?: ExpressionValue<boolean>
}

interface GridColumn {
  width?: number
  style?: SlotStyleDeclarations
}

interface GridRow {
  slots?: GridSlot[]
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}

type GridSlot = Omit<SplitSlot, 'width'>

Two columns and one row at minimum. A row is a split without type, and it reaches the event stream as one. A column’s width follows a slot’s rules, and its style is the layer under each slot in that position. A slot takes no width of its own. A row naming fewer slots than there are columns leaves the rest empty, so {} is a spacer line.

Item

signature
type Item = TextItem | ImageItem | SplitItem | GridItem

PageBands

signature
interface PageBands {
  header?: Item[]
  footer?: Item[]
  margin?: number | PageMargin
  size?: PaperName
  orientation?: Orientation
}

interface PageMargin { top: number; right: number; bottom: number; left: number }
type PaperName = 'A4' | 'letter'
type Orientation = 'portrait' | 'landscape'

margin, size and orientation are the paper. Each key may come from the document or from the target’s options, and declaring the same key in both is a render error. orientation turns whichever paper is in force. A [width, height] size is a target option only.

SortKey

signature
interface SortKey {
  by: `=${string}`
  dir?: 'asc' | 'desc'
}

dir defaults to "asc". The sort is stable. Keys compare with JavaScript ordering, so keep a key one type where the order matters.

Supporting types

signature
type ExpressionValue<T = unknown> = T | `=${string}`
type ReducerSpec = string
type JsonValue =
  | null | boolean | number | string
  | JsonValue[]
  | { [key: string]: JsonValue }

A leading = on any property other than a cell value makes it an expression. A parameter is a JSON value and nothing else, whether it’s a bare value, a default or a value a render supplies. A Date, a Map, a NaN or an Infinity is an error.