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
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
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
interface TableDetail {
header?: TableHeaderBox
row?: TableRow
columns: [TableColumn, ...TableColumn[]]
total?: TotalBlock
}TableColumn
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
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
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
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
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
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
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
type Item = TextItem | ImageItem | SplitItem | GridItemPageBands
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
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
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.