Escape
HomeDocsEvent stream

Event stream

What a render emits. Nothing in the stream is markup-escaped, because escaping belongs to the target at its own edge.

ReportEventStream

signature
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

signature
type ReportEvent =
  | ReportStartEvent
  | ItemEvent
  | ImageEvent
  | SplitStartEvent
  | SplitEndEvent
  | GroupStartEvent
  | GroupEndEvent
  | TableStartEvent
  | RowEvent
  | TotalRowEvent
  | TableEndEvent
  | ReportEndEvent

ReportStartEvent

signature
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

signature
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

signature
interface SplitStartEvent {
  type: 'split-start'
  role: ItemRole
  slots: { width?: number }[]
  style?: Record<string, unknown>
}

interface SplitEndEvent { type: 'split-end' }

GroupStartEvent and GroupEndEvent

signature
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

signature
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

signature
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

signature
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 | ValueToken

A 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

signature
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

signature
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

signature
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.

The seam every target sits behind.

npm install quario
Getting started
© 2026 quario · KvK 61815977