quario 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,441 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **`imageError(path, said, cause)`**, the mint a render target raises an image
15
+ failure through. The engine vouches for an image's magic numbers and no
16
+ further, so every failure past them belongs to whichever target was sizing or
17
+ embedding the file — and each was wording it its own way. This names the item
18
+ that asked for the bytes and keeps the failing class and message behind it,
19
+ so one report reads the same however it is rendered. It is not a diagnostic:
20
+ `isDiagnostic` does not answer for one.
21
+
22
+ - **Formatted cells render dramatically faster.** The engine built a fresh
23
+ `Intl` formatter for every presented cell and now keeps them, keyed on
24
+ everything a formatter is built from — the kind, the locale, the digit count,
25
+ the currency code, and for a date its form and timezone. A report of a
26
+ hundred thousand formatted cells in mixed currencies went from 3.2 s to
27
+ 0.2 s rendering to HTML. Nothing about what a cell presents changes.
28
+
29
+ The store is bounded and empties past its bound rather than growing, so a
30
+ report presenting an unusual number of distinct formats costs what it always
31
+ did and never a formatter belonging to another cell.
32
+
33
+ - **`format` takes a digit count, and a date takes a form.** The declaration is
34
+ still a closed kind, and now the kind may carry the one modifier it
35
+ understands: `number`, `percent` and `currency` take `digits`, and `date`
36
+ takes `form`.
37
+
38
+ ```jsonc
39
+ { "header": "Rate", "value": "{{ @.rate }}",
40
+ "style": { "format": { "kind": "percent", "digits": 3 } } }
41
+ { "header": "Total", "value": "{{ @.amt }}",
42
+ "style": { "format": { "kind": "currency", "digits": 0 }, "currency": "EUR" } }
43
+ { "header": "Due", "value": "{{ @.due }}",
44
+ "style": { "format": { "kind": "date", "form": "long" } } }
45
+ ```
46
+
47
+ The bare kind stays the shorthand — `"number"` means
48
+ `{ "kind": "number" }` — so no existing declaration has to change.
49
+
50
+ `digits` is a whole number from 0 to 20 and overrides the count the kind
51
+ would otherwise present, **a currency's minor units included**: the example
52
+ above shows `€1,235` on the page and builds `"EUR"#,##0` in the worksheet.
53
+ The minor units remain the default, so a JPY cell still shows no decimals
54
+ beside a EUR one that shows two.
55
+
56
+ On `percent`, `digits` counts the places of the **presented percentage**, not
57
+ of the stored fraction: `digits: 2` on `0.12345` gives `12.35%`. Making the
58
+ stored value agree still takes `round(x, 4)` — the two count different
59
+ things, a factor of 100 apart.
60
+
61
+ `form` is one of `short`, `medium`, `long`, `full`. It is honoured exactly in
62
+ HTML and PDF, and **approximated in the worksheet**: a sheet date is a real
63
+ typed date its reader re-presents, so the shape is yours while the language a
64
+ month or weekday name is spelled in follows whoever opens the file. Pinning
65
+ that would stop the sheet reading naturally for its reader.
66
+
67
+ A modifier a kind does not understand is a definition error naming the cell —
68
+ `{ "kind": "date", "digits": 2 }` fails, as does a `digits` outside 0 to 20.
69
+ `format` layers whole: a cell that names it restates the whole declaration, so
70
+ naming a kind again resets the modifier. Only the whole declaration may be an
71
+ `=` expression; a computed count is written
72
+ `"={'kind':'number','digits':@.dp}"`. Custom `#,##0.00` patterns still belong
73
+ in `funcs`, and a fraction-digit count no longer needs one.
74
+
75
+ - **`sort` is stable**: rows whose keys compare equal keep the order they
76
+ arrived in. The engine has always sorted this way; the promise is new.
77
+
78
+ - `currencyOf(style, options)` joins the package's exports beside `format()`.
79
+ It answers which currency code a cell wears — its own when it declares one,
80
+ else the instance default — for a target that needs the code itself rather
81
+ than presented text. A cell that declared a code the engine could not accept
82
+ carries `style.currency` as `null`, so `style.currency ?? yourDefault` is the
83
+ wrong spelling and this helper is the right one.
84
+
85
+ - **A cell may name its own currency.** `currency` is a new style declaration
86
+ beside `format` — a three-letter ISO 4217 code saying what a money cell is
87
+ denominated in.
88
+
89
+ ```jsonc
90
+ { "header": "Amount", "value": "{{ @.amt }}",
91
+ "style": { "format": "currency", "currency": "=@.ccy" } }
92
+ ```
93
+
94
+ Until now the code lived only on the instance, so a report whose rows arrive
95
+ in different currencies — a revenue listing, a statement, an outstanding-
96
+ invoices table — had to format through `funcs`, and that costs the value: a
97
+ formatter returns a string, so the cell leaves the typed seam and the
98
+ worksheet gets text with no number format. A declared code keeps the number.
99
+
100
+ The cell's own code beats the instance's, which stays the default for
101
+ documents that only ever hold one denomination. Locale and timezone do not
102
+ follow it into the document: a locale describes the reader, while a currency
103
+ code describes the value. The declaration takes an expression like any other,
104
+ so a total under a group keyed by currency reads `"=byCcy.key"`. It shares
105
+ `format`'s sites exactly — text items, column cells, headers and totals, and
106
+ a column's own `style.currency` declares an amount column once — and, like
107
+ `format`, it is refused on an image, on `row.style` and on the report
108
+ default. It is unread without `format: "currency"`.
109
+
110
+ A literal that is not three uppercase letters is a definition error. An
111
+ expression that resolves to something else stays lenient, and the cell then
112
+ renders as plain display text: it does **not** fall back to the instance's
113
+ currency, because labelling a figure in a denomination nobody named is worse
114
+ than not formatting it.
115
+
116
+ - **`round`, `floor`, `ceil` and `abs` are callable from any expression**,
117
+ without registering anything. `round(x, n)` gives `x` to `n` decimal places;
118
+ `n` defaults to 0 and must be a number from 0 to 100; a fraction in it
119
+ truncates. This is the lever a report needs when arithmetic across exact values lands on an inexact one:
120
+ a receipt whose `{{ $.subtotal + $.vatLow + $.vatHigh }}` computes
121
+ `83.75999999999999` writes `83.76` once the sum is wrapped in `round(…, 2)`.
122
+ It matters in the two targets that keep the number rather than presenting it
123
+ — the workbook stores the value, and a CSV field shows it as text with no
124
+ number format to hide behind. A registered function of the same name still
125
+ wins, so nothing an existing report does changes.
126
+
127
+ They round the **decimal you wrote**, not the binary value it is stored as:
128
+ `round(2.675, 2)` is `2.68` and `round(0.615, 2)` is `0.62`. Note that a
129
+ `percent` cell presents its value multiplied by 100, so matching a
130
+ two-decimal percent display takes `round(x, 4)`. A value that is not a finite
131
+ number passes through untouched rather than becoming zero; an `n` outside the
132
+ range is an error naming the cell it came from.
133
+
134
+ - `split-start` events carry `path`, the split definition's schema path, as
135
+ `item` and `image` events already do. A consumer can now name the definition
136
+ behind a split without tracking position.
137
+
138
+ - `fractionDigits(kind, options)` is exported beside `format()`: how many
139
+ fraction digits a kind presents, or nothing for a kind that carries no
140
+ count. It is the table the official targets read, and it is public so a
141
+ consumer writing its own target presents the same digits they do.
142
+
143
+ ### Changed
144
+
145
+ - **A `date` cell with no `form` now presents `medium`, not the runtime's own
146
+ default.** A `date` that names no form used to render whatever the platform's
147
+ default date format was — under `en-US`, `8/14/2026` — which matched none of
148
+ the four forms you can now ask for. It renders `Aug 14, 2026` instead, and
149
+ `14 Aug 2026` under `en-IE`. In the worksheet the number format moves from
150
+ `yyyy-mm-dd` to `dd mmm yyyy`.
151
+
152
+ If you want the old shape back, say so: `{ "kind": "date", "form": "short" }`
153
+ is the nearest of the four. This is the one change here that moves output
154
+ under an existing report.
155
+
156
+ - **`style.format` crosses the event stream resolved, not as authored.** A
157
+ custom target reading `style.format` now sees `{ kind, digits }` or
158
+ `{ kind, form }` where it saw a bare string: `"number"` arrives as
159
+ `{ kind: "number", digits: 2 }` and `"date"` as
160
+ `{ kind: "date", form: "medium" }`. A `currency` whose code cannot be read
161
+ arrives as `{ kind: "currency" }` with no count at all, which is the signal
162
+ to write no number format. Read `style.format.kind` where you read
163
+ `style.format`, and take the digit count off the declaration rather than
164
+ computing one.
165
+
166
+ - **`fractionDigits` takes the whole style.** Its first argument was the kind
167
+ and is now the style, matching `format()` and `currencyOf()`.
168
+ `fractionDigits("number")` becomes
169
+ `fractionDigits({ format: "number" })`. Targets no longer need it at all —
170
+ the count rides on the declaration the stream carries.
171
+
172
+ - **`typed()`'s second argument is the resolved declaration.** Pass
173
+ `style.format` as it arrives on the stream; it reads the kind from the
174
+ object. The shorthand string is still accepted, and omitting the argument is
175
+ unchanged.
176
+
177
+ - **A rejected box value is now dropped from the resolved style rather than
178
+ carried on it.** A `border*` or `padding*` declaration written as an `=`
179
+ expression used to cross the render stream with whatever the expression
180
+ answered, even a value the vocabulary rejects; every other name was already
181
+ dropped. It is now dropped too, so one rule covers the whole vocabulary and
182
+ `currency` is its only exception.
183
+
184
+ Nothing a report renders changes. A border side is still won whole by the
185
+ node that names any of its three parts — that is now settled from the
186
+ declarations rather than from what they resolve to, so a rejected part still
187
+ cannot hand the node its row's other two. A side left incomplete still
188
+ contributes nothing rather than becoming a solid black stroke, and a rejected
189
+ padding still falls to the target's own inset.
190
+
191
+ What changes is what a consumer reading the stream itself sees. Where a cell
192
+ had `style.borderTopColor` holding an unusable value, the key is now absent:
193
+
194
+ ```js
195
+ // before
196
+ cell.style; // { borderTopWidth: 1, borderTopColor: "2-6-A-2-2" }
197
+ // after
198
+ cell.style; // { borderTopWidth: 1 }
199
+ ```
200
+
201
+ Every target in this release already checked such values before reading them
202
+ and is unaffected. A consumer that inferred border-side ownership from which
203
+ keys were present should read the declarations instead; one that validates
204
+ what it reads needs no change.
205
+
206
+ - **`format()` now takes the cell's resolved `style` instead of its `format`
207
+ kind.** The signature is `format(value, style, options)`, where it was
208
+ `format(value, kind, options)`. This is what lets the helper read a cell's own
209
+ `currency` code alongside the kind. Callers pass one property less:
210
+
211
+ ```js
212
+ // before
213
+ format(token.value, style?.format, intl);
214
+ // after
215
+ format(token.value, style, intl);
216
+ ```
217
+
218
+ A caller holding only a kind wraps it as `{ format: kind }`.
219
+
220
+ - **A table's `total` is now a block holding its rows**, so what belongs to the
221
+ totals as a whole can be said once. Before, `total` was the array of rows
222
+ itself:
223
+
224
+ ```jsonc
225
+ "total": [{ "cells": [...] }, { "cells": [...] }]
226
+ ```
227
+
228
+ Now it is an object whose `rows` is that same array, plus a `style` and a
229
+ `visible` of its own:
230
+
231
+ ```jsonc
232
+ "total": {
233
+ "style": { "paddingTop": 0, "paddingBottom": 0 },
234
+ "rows": [{ "cells": [...] }, { "cells": [...] }]
235
+ }
236
+ ```
237
+
238
+ The rows themselves are unchanged — still `{ "cells", "style?", "visible?" }`,
239
+ still covering every column exactly once. A `total` that is still an array is
240
+ a definition error reading `detail.total: expected an object`, and a `total`
241
+ with no `rows` reads `detail.total.rows: expected an array`. Total cell paths
242
+ gain the one segment: `detail.total.rows[r].cells[i]`, which is what a
243
+ `total-row` cell now carries as its `path` and what a located problem names.
244
+
245
+ The block's `style` is the layer below each row's — box under box, every
246
+ other declaration under the row's own — and takes a total row's vocabulary,
247
+ the whole style set less flow spacing and `format`. Where the block, a row
248
+ and a cell all speak, the innermost one that names any of a border side's
249
+ three keys takes that side whole, exactly as a cell already did over a row;
250
+ padding layers per name. Exact `false` on the block's `visible` omits the
251
+ totals entirely: the rows are never evaluated, none of them votes on a
252
+ width-less column, and the output is what leaving `total` out gives.
253
+
254
+ This is why the change earns a break: a table's header row has `detail.header`
255
+ and its data rows have `detail.row`, but the totals — the one kind whose rows
256
+ a document writes out one by one — had no level above them, so "these sit at
257
+ the band's line pitch, not the table's row pitch" had to be spelled on every
258
+ row. `columns` is unaffected and needs no such block: a declaration reaching
259
+ every column reaches every data cell, which is what `detail.row`'s style
260
+ already says from the other side.
261
+
262
+ - **`table-start` now carries the header row**, instead of a `header` cell on
263
+ each column that a custom target had to collect for itself. The event's
264
+ `header` is `{ cells, style? }` — the same shape a `row` or `total-row`
265
+ event has — where `cells` holds one cell per column no neighbour's `span`
266
+ covers, so it is shorter than `columns` whenever a header spans. Each header
267
+ cell carries its column's `path`, as a row cell does; a spanning header takes
268
+ the first column it covers. What is left on `columns` is the geometry: one
269
+ entry per column, holding a `width` percentage when the document authored
270
+ one. Nothing an existing report produces changes in any of the shipped
271
+ targets — this is the seam between the engine and a target, not the document.
272
+
273
+ ```js
274
+ // before
275
+ const headers = event.columns
276
+ .filter((column) => column.header)
277
+ .map((column) => Object.assign({}, column.header, { path: column.path }));
278
+ const headerStyle = event.style;
279
+
280
+ // after
281
+ const headers = event.header.cells;
282
+ const headerStyle = event.header.style;
283
+ ```
284
+
285
+ `columns[i].header`, `columns[i].path` and `table-start.style` are gone. A
286
+ target that reads only `columns[i].width` is unaffected.
287
+
288
+ - **`format` now presents a fixed number of fraction digits, and every target
289
+ presents the same number.** Until now the digits were whatever each edge
290
+ happened to default to: `format: "number"` showed up to three decimals in
291
+ HTML and PDF and exactly two in a worksheet, and `format: "percent"` showed
292
+ none in HTML and PDF against the worksheet's two — so one cell in one
293
+ document read `12%` in the PDF and `12.35%` in the workbook beside it.
294
+
295
+ `number` and `percent` are now **two places, always**, in every target that
296
+ presents. `currency` is its currency's own minor units, so a JPY amount has
297
+ none and a BHD amount has three, where every currency previously got two.
298
+ Author-visible, with no document change:
299
+
300
+ | value | before | now |
301
+ | ------------------ | ----------- | ---------- |
302
+ | `number` `1000` | `1,000` | `1,000.00` |
303
+ | `number` `0.12345` | `0.123` | `0.12` |
304
+ | `percent` `0.21` | `21%` | `21.00%` |
305
+ | `currency` JPY | `¥1,234.57` | `¥1,235` |
306
+
307
+ Only the digit count converges. Grouping separators, a currency symbol's
308
+ position and a date's shape still follow each target's own conventions, and
309
+ in a worksheet they follow whoever opens the file. `date` is unchanged.
310
+ A report that needs different digits formats through `funcs`, as before.
311
+
312
+ - A `currency` code that is not a readable currency now presents nothing in
313
+ every target, rather than the worksheet inventing a number format from it
314
+ while the other targets fell back to the unformatted value.
315
+
316
+ ### Fixed
317
+
318
+ - **A split slot is typed with the declarations the traversal accepts.**
319
+ `SplitSlot` reused the band items' style types, so TypeScript refused
320
+ `valign` on an image slot — legal there, and only there — and admitted
321
+ `spaceBefore`/`spaceAfter` on a slot, which the engine rejects. Two new
322
+ exported types, `SlotStyleDeclarations` and `SlotImageStyleDeclarations`,
323
+ now say what a slot may declare.
324
+
325
+ - **A page band closure is typed as what it returns.** `PageBandRenderers`
326
+ declared `header`/`footer` as returning item and image events only, while a
327
+ split in a page band has always come back as its bracket — `split-start`,
328
+ one event per slot, `split-end`. The closures now return `PageBandEvent[]`,
329
+ a new exported union naming all four, so a TypeScript host reading a page
330
+ band no longer types `role` onto a `split-end` that carries none.
331
+
332
+ - **A computed style value the schema does not accept now leaves the layer
333
+ below in place**, which is what the schema has always said it does —
334
+ "contributes nothing, same as omit". The name is dropped from the resolved
335
+ style rather than passed on, so no target has to decide what an unreadable
336
+ value means.
337
+
338
+ Before, the value reached each target and they disagreed. A detail item with
339
+ `"bold": "=@.flag"` over a `1` rendered bold in a PDF and a worksheet but not
340
+ in HTML. On a report header, where those two targets carry a heavier default
341
+ of their own, a `0` rendered _not_ bold in both while HTML left the header
342
+ bold. The same held for every other declaration: a report header whose
343
+ `"size": "=@.pt"` resolved to `"big"` rendered at 14pt in HTML, at the body
344
+ size in a PDF, and at no size at all in a worksheet. Now all three keep the
345
+ layer below in every case, and only a real `false` turns a flag off.
346
+
347
+ This covers `family`, `size`, the five flags, `color`, `background`, `align`,
348
+ `valign`, `format`, `spaceBefore` and `spaceAfter`, and only where the value
349
+ comes from an `=` expression — a literal has always been checked when the
350
+ report was compiled. It reaches inside a split too: a slot whose `format`
351
+ expression resolves to something other than the four kinds now keeps the
352
+ split's own kind instead of overwriting it.
353
+
354
+ Two declarations are deliberately unchanged. A border or padding name is kept
355
+ rather than dropped, because a node that names any part of a border side
356
+ takes that side whole from its row — dropping the name would hand the node
357
+ the row's other two and draw an edge nobody declared. And a `currency` code
358
+ that is not a code still crosses as `null` rather than disappearing, so a
359
+ cell that named a denomination is never presented under another one.
360
+
361
+ - A `format` kind that an `=` expression resolved to the name of a built-in
362
+ object member — `"toString"`, `"constructor"` — now contributes nothing and
363
+ the cell falls back to display text, as the documented rule says any
364
+ unrecognised kind does. Before, such a cell could render `[object Undefined]`.
365
+ A literal was never affected: it is checked against the four kinds.
366
+
367
+ - **A pinned report header now refuses a lead on every band that can sit under
368
+ its box, and only where the document settles which one that is.** When
369
+ `header` declares a `height`, an authored `spaceBefore` on the first item of
370
+ the band below the box would push that band off the pin, so it is refused.
371
+ Which band is below the box was read too narrowly, and which item leads it
372
+ too eagerly.
373
+
374
+ Reports that used to validate and now do not: a lead on the `empty` band's
375
+ first item, and a lead on a group header below the first one. Both were
376
+ already refused at render, but only on the render that reached them — an
377
+ `empty` band's lead detonated the first time a filter emptied the row set,
378
+ on a report that had validated clean for months. Both are definition errors
379
+ now, on every render.
380
+
381
+ Reports that used to be refused and now validate: a lead on an item that may
382
+ not appear at all — one whose `visible` is an expression, or an image whose
383
+ `source` yields nothing. Such an item was blamed as though it always
384
+ occupied, so a document that renders correctly could not be compiled. The
385
+ check now stays silent wherever only the data decides which item comes first,
386
+ and the render refuses it at the moment the answer exists.
387
+
388
+ Two smaller repairs come with it. A `spaceBefore` that is not a paintable
389
+ number — negative, `NaN`, infinite — no longer produces two problems on one
390
+ key, one telling you to make it `0` and one telling you it is not a valid
391
+ measurement; only the second is reported, because it is the one that is
392
+ wrong. And a `split` that leads a band now names itself in the error rather
393
+ than reporting against the report header, which is a band this rule never
394
+ covers.
395
+
396
+ ## [0.5.0] - 2026-09-05
397
+
398
+ ### Added
399
+
400
+ - **`STYLE_NAMES`**, every name in the style vocabulary as a read-only list,
401
+ in the order the specification's table lists them. A tool that offers the
402
+ vocabulary — the editor's style rail — reads it from here instead of
403
+ carrying a copy that has to learn each new name.
404
+ - **`span` lets a cell cover several table columns**, so a label reaches across
405
+ them instead of being pushed sideways by empty cells. A literal positive
406
+ integer; absent is 1. It is legal on the two cells a document writes: a
407
+ column's `header`, where the columns it covers omit their own `header`, and a
408
+ total row's cells, whose spans must sum to the column count exactly. A data
409
+ row's cells are the columns' own, so there is no cell there to carry one — a
410
+ line that runs across the table is a `split` item below it. A span covers and
411
+ does not vote: a cell over more than one column has no say in their widths,
412
+ and a column nothing votes on takes the cell-padding floor.
413
+ - **`valign`** joins the style vocabulary: `"top"`, `"middle"` or `"bottom"`,
414
+ a literal or an `=` expression like `align`. It is legal exactly where a box
415
+ is taller than its content asked for — table cells, headers, totals, `row`,
416
+ and split slots, text or image — and a definition error on a stacked item and
417
+ on a band image, whose boxes have no such slack. A row's or a split's layers
418
+ under its cells' or slots' own. Undeclared is not a declaration: each target
419
+ keeps its own default.
420
+
421
+ ### Changed
422
+
423
+ - **A table row's `style` now resolves onto that row's cells**, the box
424
+ included, instead of meaning something different on every output. Before,
425
+ padding and border on `detail.header`, `detail.row` or a total row's `style`
426
+ reached no cell at all: a fragment dropped them, a worksheet turned them into
427
+ an edge on each cell, and a page drew one box around the row. Now they layer
428
+ under each cell's own, the way `bold` and `valign` on a row already did, so
429
+ one declaration means one thing everywhere.
430
+ What this makes possible is declining the cell padding a row at a time:
431
+ `"paddingTop": 0, "paddingBottom": 0` on `detail.row` sets the pitch for
432
+ every data row, where before it had to be written on each cell.
433
+ Two things move for documents that already declared a box on a row.
434
+ A row's `borderLeft` is now an edge on each of the row's cells rather than
435
+ one at the row's outer left — write it on the first column's cells to get the
436
+ single edge back. And a row's border now occupies height, as a cell's border
437
+ always has, so a bordered row is taller by its border's width.
438
+ A border side is won whole by the cell: a cell naming any of that side's
439
+ three keys takes the side entirely, so a cell that failed soft on one name
440
+ does not inherit the row's other two.
441
+ On the event stream, a `row`, `total-row` or `table-start` `style` now
442
+ carries only what layers by ordinary means, and a row whose whole block was
443
+ box carries no `style` at all.
444
+
10
445
  ## [0.4.0] - 2026-09-03
11
446
 
12
447
  ### Changed
package/README.md CHANGED
@@ -51,9 +51,10 @@ surface at compile time, so compile at startup and render in your request path.
51
51
 
52
52
  ### `quario(options?)`
53
53
 
54
- Creates a configured instance with host-level controls: `{ query?, license? }`. Returns
55
- `{ report, license }`. `license` settles with this instance's key verification as
56
- `{ licensed, licensee?, id? }`.
54
+ Creates a configured instance with host-level controls: `{ query?, license?, locale?, currency?,
55
+ timeZone? }` — the query budget, the license key, and the `format` configuration (`en-US`, no
56
+ default currency, UTC). Returns `{ report, plan, license }`. `license` settles with this instance's
57
+ key verification as `{ licensed, licensee?, id? }`.
57
58
 
58
59
  ### `report(schema, functions?)`
59
60
 
@@ -62,7 +63,8 @@ Compiles a report and returns the compiled report. `stream(data)` is the raw eve
62
63
  (e.g. `html()` from `@quario/html`) or your own.
63
64
 
64
65
  A render is async: quario awaits key verification before the target sees its first event. A
65
- malformed target throws at compile time, like any other definition error.
66
+ malformed target throws synchronously from `render`, before the first event; definition problems
67
+ throw earlier, at `report()`.
66
68
 
67
69
  The data pre-pass (select, filter, sort, aggregate) runs when you call the renderer, because a
68
70
  report header may interpolate a report aggregate. Event emission pulls on demand, so a consumer
@@ -72,27 +74,29 @@ The compiled report carries metadata:
72
74
 
73
75
  ```js
74
76
  report.names; // free variable names the expressions read, excluding engine anchors
75
- report.functions; // registry function names the definition calls
77
+ report.functions; // { name, arity } per registry function the definition calls
76
78
  report.paths; // padvinder's deeply frozen dependency topology for the `data` query
77
79
  ```
78
80
 
79
81
  Events arrive in render order: `report-start`, report `header` items, then either the `empty`
80
82
  items or the group/detail walk, then `footer` items, `report-end`. Group instances bracket their
81
83
  content with `group-start`/`group-end`; a table detail yields `table-start`, one `row` per visible
82
- row, an optional `total-row`, and `table-end`.
83
-
84
- | Event | Carries |
85
- | -------------- | ------------------------------------------------------------------------------------------- |
86
- | `report-start` | `params`, resolved report `aggregates`, optional `page` band closures, `columns`, `marking` |
87
- | `item` | `role`, `path`, `tokens`, optional `style`, `run` |
88
- | `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
89
- | `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `reset`, `columns` |
90
- | `group-end` | `name`, `depth` |
91
- | `table-start` | `path`, `columns` (each `header`, `path`, optional `width`) |
92
- | `row` | `cells`, optional `style`, `run` |
93
- | `total-row` | `cells` |
94
- | `table-end` | - |
95
- | `report-end` | - |
84
+ row, one `total-row` per emitted total row, and `table-end`.
85
+
86
+ | Event | Carries |
87
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `report-start` | `params`, resolved report `aggregates`, optional `page` band closures, `columns`, `style` (the report default), `marking`, `margin`, `headerHeight`, and the instance's `locale` / `currency` / `timeZone` when set |
89
+ | `item` | `role`, `path`, `tokens`, optional `style`, `run` |
90
+ | `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
91
+ | `split-start` | `role`, `slots` (each an optional `width`), optional `style`; one `item` or `image` per slot follows, then `split-end` |
92
+ | `split-end` | - |
93
+ | `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `reset`, `columns` |
94
+ | `group-end` | `name`, `depth` |
95
+ | `table-start` | `path`, `header` (`cells`, optional `style`), `columns` (each an optional `width`) |
96
+ | `row` | `cells`, optional `style`, `run` |
97
+ | `total-row` | `cells`, optional `style` |
98
+ | `table-end` | - |
99
+ | `report-end` | - |
96
100
 
97
101
  **Cells carry tokens.** A cell is `{ tokens, style? }`. Each token is either `{ literal }`
98
102
  (static template text, verbatim) or `{ value }` (one interpolation's _pre-format_ value, the
@@ -106,19 +110,21 @@ own edge.
106
110
  **`report-start.marking`** carries the evaluation wording when the render is unlicensed, or while
107
111
  verification is still settling. Licensed streams omit it. Targets place the marking; they do not
108
112
  author its wording. **`columns`** on `report-start` / `group-start` is the declared
109
- page column count when present. No shipped target honors it yet;
113
+ page column count when present. `@quario/pdf` and `@quario/html` lay page columns out;
110
114
  xlsx never will.
111
115
 
112
116
  ### `text(tokens)`
113
117
 
114
- Joins a token stream to display text: literals verbatim, values as `String(value ?? "")`, run
115
- styles ignored. Re-exported from sjabloon so consumers do not hand-roll the join.
118
+ Joins a token stream to display text: literals verbatim, values through `display()` (a `Date`
119
+ as ISO 8601, nullish as the empty string), run styles ignored. Re-exported from sjabloon so consumers do not hand-roll the join.
116
120
 
117
- ### `typed(tokens)`
121
+ ### `typed(tokens, kind?)`
118
122
 
119
123
  Exactly one value token holding a finite number, a boolean, or a valid `Date` keeps that
120
124
  pre-stringify value. Anything else, including a lone null, reports `undefined` and joins to
121
- display text. Spreadsheet consumers use this for real numeric cells;
125
+ display text. Passing the cell's resolved `format` kind opts into the seam's one coercion: under
126
+ `"date"`, an RFC 3339 string revives to the `Date` it names. Spreadsheet consumers use this for
127
+ real numeric cells;
122
128
  [`@quario/csv`](https://www.npmjs.com/package/@quario/csv) is the short form.
123
129
 
124
130
  ### `walk(events, handlers)` / `breathe()`
@@ -132,6 +138,24 @@ itself.
132
138
  `breathe()` is that hand-back alone. Await it between batches of a loop you own; `walk` already
133
139
  calls it for you.
134
140
 
141
+ ### Presentation helpers
142
+
143
+ A target that stringifies imports these rather than restating them, so every surface presents a
144
+ cell the same way:
145
+
146
+ - `display(value)` — the scalar rule `text()` joins with: a `Date` as ISO 8601 UTC, nullish as
147
+ the empty string, everything else `String(value)`.
148
+ - `format(value, style?, options?)` — presents a token under the cell's resolved `format`
149
+ declaration (its kind and modifier, and for `currency` the cell's own code); `undefined` when the kind does not apply, so the caller falls back to
150
+ `display()`.
151
+ - `fractionDigits(style?, options?)` — the digit count a resolved `format` declaration presents:
152
+ the kind's own (two for `number` and `percent`, a currency's minor units for `currency`) unless
153
+ the declaration's `digits` overrides it; `undefined` where there is no count.
154
+ - `currencyOf(style?, options?)` — which code a money cell wears: its own, else the instance's.
155
+ - `isReportBand(role)` — whether a role names one of the report's own bands rather than a
156
+ group's.
157
+ - `STYLE_NAMES` — the closed style vocabulary, in the spec's order.
158
+
135
159
  ### `validate(schema, functions?)`
136
160
 
137
161
  Checks a definition without rendering it. Returns every problem as a path-prefixed string; an
@@ -172,7 +196,7 @@ query budget failures) `limit` and `actual`.
172
196
 
173
197
  ```js
174
198
  try {
175
- await renderer(data);
199
+ await report.render(html(), data);
176
200
  } catch (e) {
177
201
  if (isDiagnostic(e)) console.error("report problem:", e.message);
178
202
  else throw e; // one of your own functions failed