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 readPaths: ReadPaths
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 | PageMargin
size?: PaperName
orientation?: Orientation
headerHeight?: number
locale?: string
currency?: string
timeZone?: string
}marking carries the unlicensed wording when the instance has no valid key.
margin, size and orientation are the document’s paper, present only where
it declared them and in the shape the author wrote. 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' | 'svg'
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, JPEG frame header, or SVG root 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 vouches for nothing. Bytes a
target can’t write are that target’s render error, which it mints with
imageError.
SplitStartEvent and SplitEndEvent
interface SplitStartEvent {
type: 'split-start'
role: ItemRole
path: string
slots: { width?: number }[]
style?: Record<string, unknown>
}
interface SplitEndEvent { type: 'split-end' }A grid reaches the stream as one split per
row, and path then names the row. A target that draws splits draws a grid
with no change.
GroupStartEvent and GroupEndEvent
interface GroupStartEvent {
type: 'group-start'
name: string
path: string
depth: number
key: unknown
aggregates: Record<string, unknown>
break?: 'page'
breakAfter?: 'page'
reset?: 'page'
columns?: number
label?: string
collapsed?: true
}
interface GroupEndEvent {
type: 'group-end'
name: string
depth: number
hollow?: true
}The stream states the two page edges, never the four positions a group’s
break names. break is present where a page turns before the instance, and
breakAfter where one turns after it.
label is the group’s label resolved for this instance. It’s absent where
the group declares none, so a target deriving a name of its own keeps deriving
it.
collapsed is present, and only ever true, when the group asks that the
instance open shut. A target with somewhere to put a row a reader can open
again honours it. A target without such a place reads the stream through
collapse() instead.
hollow is present, and only ever true, when the instance’s bands emitted
nothing between its brackets. Such an instance occupies nothing, turns no page
by opening, and earns no outline entry. An instance holding nothing but hollow
instances is hollow too.
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. The engine resolves it 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[]
}The engine doesn’t walk page bands. 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 WalkEvent = ReportEvent | SplitEvent
type WalkHandlers = {
[K in WalkEvent['type']]?: (event: Extract<WalkEvent, { type: K }>) => void
}
function walk(events: Iterable<WalkEvent>, handlers: WalkHandlers): Promise<void>
function splits(events: Iterable<ReportEvent>): Generator<WalkEvent, void>
function breathe(): Promise<void>A missing handler ignores that event. Handlers are synchronous, and walk
doesn’t await a promise one returns. A target running a loop of its own calls breathe
between batches so a long report never blocks the host.
splits folds each split bracket into one split event carrying the opening
event’s fields and its slot events under items. Everything else passes
through unchanged and lazily. Read walk(splits(events), handlers) and register
a split handler rather than a split-start and split-end pair. The stream
itself doesn’t change.
interface SplitEvent extends Omit<SplitStartEvent, 'type'> {
type: 'split'
items: (ItemEvent | ImageEvent)[]
}