Event stream
What a render emits. Nothing in the stream is markup-escaped, because escaping belongs to the target at its own edge.
ReportEventStream
interface ReportEventStream {
(data?: unknown): Generator<ReportEvent, void, undefined>
readonly names: readonly string[]
readonly functions: readonly FunctionSignature[]
readonly paths: readonly QueryPath[]
}What a target’s compile receives.
ReportEvent
type ReportEvent =
| ReportStartEvent
| ItemEvent
| ImageEvent
| SplitStartEvent
| SplitEndEvent
| GroupStartEvent
| GroupEndEvent
| TableStartEvent
| RowEvent
| TotalRowEvent
| TableEndEvent
| ReportEndEventReportStartEvent
interface ReportStartEvent {
type: 'report-start'
params: Record<string, JsonValue>
aggregates: Record<string, unknown>
page?: PageBandRenderers
columns?: number
style?: Record<string, unknown>
marking?: string
margin?: number
headerHeight?: number
locale?: string
currency?: string
timeZone?: string
}marking carries the unlicensed wording when the instance has no valid key. A
target settles what it needs from this event in its handler, before pulling the
stream further.
ItemEvent and ImageEvent
interface ItemEvent {
type: 'item'
role: ItemRole
path: string
tokens: Token[]
style?: Record<string, unknown>
run?: Record<string, unknown>
}
interface ImageEvent {
type: 'image'
role: ItemRole
path: string
bytes: Uint8Array
format: 'png' | 'jpeg'
width: number
height: number
fit: ImageFit
alt?: Token[]
style?: Record<string, unknown>
run?: Record<string, unknown>
}The engine reads format, width and height off the bytes’ own header, so
no target parses the PNG IHDR or JPEG frame header for itself. Bytes too short
to state a size never reach a target at all: that’s one render error on every
target alike. Past the size the engine has vouched for nothing — bytes a target
can’t write are that target’s render error, minted with imageError.
SplitStartEvent and SplitEndEvent
interface SplitStartEvent {
type: 'split-start'
role: ItemRole
slots: { width?: number }[]
style?: Record<string, unknown>
}
interface SplitEndEvent { type: 'split-end' }GroupStartEvent and GroupEndEvent
interface GroupStartEvent {
type: 'group-start'
name: string
path: string
depth: number
key: unknown
aggregates: Record<string, unknown>
break?: 'page'
reset?: 'page'
columns?: number
}
interface GroupEndEvent { type: 'group-end'; name: string; depth: number }TableStartEvent, RowEvent, TotalRowEvent, TableEndEvent
interface TableStartEvent {
type: 'table-start'
path: string
header: { cells: EventCell[]; style?: Record<string, unknown> }
columns: { width?: number }[]
}
interface RowEvent {
type: 'row'
cells: EventCell[]
style?: Record<string, unknown>
run?: Record<string, unknown>
}
interface TotalRowEvent {
type: 'total-row'
cells: EventCell[]
style?: Record<string, unknown>
}
interface TableEndEvent { type: 'table-end' }A row’s style still arrives on the row, and a cell’s on the cell. The box is
the exception, resolved onto the cells before any target sees them.
Cells and tokens
EventCell
interface EventCell {
tokens: Token[]
style?: Record<string, unknown>
span?: number
path?: string
}span is present only when a cell covers more than one column.
Token
interface TokenStyle { style?: Record<string, unknown> }
type PageField = 'page.number' | 'page.total'
type LiteralToken = SjabloonLiteralToken & TokenStyle
type ValueToken = SjabloonValueToken & TokenStyle & { field?: PageField }
type Token = LiteralToken | ValueTokenA cell arrives as tokens rather than as a string. A cell whose template is one
interpolation is a single value token holding the value itself. That’s how a
spreadsheet target writes a real numeric cell while a markup target writes
escaped text. Join them with text(), or take the typed value with typed().
A token may carry style, the resolved declarations for that stretch of the
cell. That’s the cell's own with its run's layered over them, already
composed. It’s present only where it differs from the cell's, so a consumer
that never reads it stays correct. styledRuns(tokens) groups a cell back into
its runs.
A value token carries field where its interpolation is exactly
{{ page.number }} or {{ page.total }}. The value still holds the number this
render saw. field names which page value the token stands for, so a target
whose own document format numbers pages can write a live field there instead.
Anything computed from a page value ({{ page.number + 1 }}) is an ordinary
value and carries no field. Write the bare token wherever a live number
matters. A target that paginates itself, or not at all, ignores the key.
Page bands
interface PageInfo { number: number; total: number }
type PageBandEvent = ItemEvent | ImageEvent | SplitStartEvent | SplitEndEvent
interface PageBandRenderers {
header?: (page: PageInfo) => PageBandEvent[]
footer?: (page: PageInfo) => PageBandEvent[]
}Page bands aren’t walked. A paginating target calls the renderers from
report-start once it knows the page numbers. An unpaginated target ignores
them.
ItemRole
type ItemRole =
| 'report-header'
| 'empty'
| 'group-header'
| 'detail'
| 'group-footer'
| 'report-footer'
| 'page-header'
| 'page-footer'isReportBand(role) answers whether a role names one of the report’s own bands
rather than a group instance’s.
Driving the stream
type WalkHandlers = {
[K in ReportEvent['type']]?: (event: Extract<ReportEvent, { type: K }>) => void
}
function walk(events: Iterable<ReportEvent>, handlers: WalkHandlers): Promise<void>
function breathe(): Promise<void>A missing handler ignores that event. Handlers are synchronous, and a promise
one returns isn’t awaited. A target running a loop of its own calls breathe
between batches so a long report never blocks the host.