quario 0.8.0 → 0.10.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 +87 -0
- package/README.md +49 -29
- package/lib/band-height.js +11 -16
- package/lib/format.js +15 -23
- package/lib/host.js +89 -0
- package/lib/index.d.ts +65 -6
- package/lib/index.js +2 -1
- package/lib/license.js +2 -2
- package/lib/locate.js +5 -7
- package/lib/math.js +11 -20
- package/lib/plan.js +119 -74
- package/lib/reducers.js +46 -24
- package/lib/stream.js +72 -46
- package/lib/style.js +41 -69
- package/package.json +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# quario
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **A group's `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.
|
|
8
|
+
|
|
9
|
+
The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.
|
|
10
|
+
|
|
11
|
+
**`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.
|
|
12
|
+
|
|
13
|
+
The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.
|
|
14
|
+
|
|
15
|
+
The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.
|
|
16
|
+
|
|
17
|
+
The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.
|
|
18
|
+
|
|
19
|
+
- **Two new reducers: `first` and `last`.** They report the value an expression takes on the first and the last row in scope. Like every reducer, each one arrives on three surfaces at once: a declared aggregate on a report or a group, a runner under `run`, and a function you can call inline over a row's own array.
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
// the customer a region's rows open and close on
|
|
23
|
+
{ "aggregates": { "opened": "first:=@.customer", "closed": "last:=@.customer" } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
{{ first(@.lines, l => l.sku) }} the first line's article number
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
They are the first reducers that read the **order** of the rows they fold, so the spec now says what each scope hands them. Every scope reports the ends of the rows it folded, which is not always what the page ends on. A report folds after `where`, its `sort` and its `take`, and before it splits the rows into groups. A group that declares a `sort` and no `take` folds its partition before it orders that partition, so its handle holds the partition's ends. Declare `first` under a group's `run` when a detail row needs the row that group opens on as rendered.
|
|
31
|
+
|
|
32
|
+
An empty scope gives `null` for both, next to `avg`, `min` and `max`. Neither reducer skips a null: a scope whose first row holds `null` reports that `null`.
|
|
33
|
+
|
|
34
|
+
Reports that already render are unaffected. A document that spelled `first:=...` before was a definition error, and it now compiles.
|
|
35
|
+
|
|
36
|
+
- **A hollow instance's `group-end` carries `hollow: true`.** An instance whose bands emitted nothing between its two brackets is hollow, and a hollow instance is not there: no space, no gap, no page turned by opening one, no outline entry. The stream now says which instances those are, on the closing bracket, so a target reads one field rather than counting the events between the brackets for itself. An instance holding nothing but hollow instances is hollow too, and one holding a child that drew is not. The field is absent, never `false`, on an instance that drew.
|
|
37
|
+
|
|
38
|
+
`@quario/layout`, and through it `@quario/pdf`, the viewer and the editor, read the field and keep no tracker of their own. What they render is unchanged.
|
|
39
|
+
|
|
40
|
+
- **`splits(events)` folds a split bracket into one event.** A split reaches the event stream as `split-start`, one `item` or `image` per slot, then `split-end`, and every consumer that wanted a split whole kept that bracket for itself. `splits` does it once: read `walk(splits(events), handlers)` and register a `split` handler, which receives the opening event's `role`, `path`, `slots` and `style` plus the slot events in order under `items`. Every other event passes through unchanged, lazily.
|
|
41
|
+
|
|
42
|
+
The stream itself is unchanged. A consumer that registers no `split-start` handler still receives the slots as ordinary events and stacks them, as before. The `SplitStartEvent` declaration now names its `path`, which the stream always carried.
|
|
43
|
+
|
|
44
|
+
Every built-in target reads its splits this way now, so a custom target can too.
|
|
45
|
+
|
|
46
|
+
## 0.9.0
|
|
47
|
+
|
|
48
|
+
### Minor Changes
|
|
49
|
+
|
|
50
|
+
- **Every target factory now refuses an option it does not understand.** An unknown key, a key with
|
|
51
|
+
a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
|
|
52
|
+
factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
|
|
53
|
+
already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
|
|
54
|
+
the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
|
|
55
|
+
signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
|
|
56
|
+
one.
|
|
57
|
+
|
|
58
|
+
The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
|
|
59
|
+
`hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
|
|
60
|
+
fourth document property is one edit rather than three.
|
|
61
|
+
|
|
62
|
+
**What this changes for you.** One options object spread across several targets stops working if
|
|
63
|
+
any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
|
|
64
|
+
`html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
|
|
65
|
+
share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
|
|
66
|
+
TypeScript does not warn about this: excess-property checking fires on an object literal and not
|
|
67
|
+
on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
|
|
68
|
+
|
|
69
|
+
Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
|
|
70
|
+
`{ meta: { title: config.title } }` over a config that carries neither are both fine.
|
|
71
|
+
`html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
|
|
72
|
+
instead of turning path stamping on. A `fonts` mapping given as an array is refused by
|
|
73
|
+
`@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
|
|
74
|
+
|
|
75
|
+
Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
|
|
76
|
+
`instanceof Error` is unaffected. One that compares the constructor is not.
|
|
77
|
+
|
|
78
|
+
`@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
|
|
79
|
+
and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
|
|
80
|
+
is a property you did not write rather than one of the wrong type, and an inherited enumerable key
|
|
81
|
+
is no longer reported as an option you wrote.
|
|
82
|
+
|
|
83
|
+
`@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
|
|
84
|
+
object you passed no longer changes what a configured target writes.
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- The engine verifies a license key against a new signing key.
|
|
89
|
+
|
|
3
90
|
## 0.8.0
|
|
4
91
|
|
|
5
92
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -11,6 +11,18 @@ The engine does not escape text, emit HTML, or format pages or cells. Install a
|
|
|
11
11
|
want output. Use this package when you want to _build_ a target, or to consume report structure
|
|
12
12
|
as data.
|
|
13
13
|
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Install](#install)
|
|
17
|
+
- [Quick start](#quick-start)
|
|
18
|
+
- [API](#api)
|
|
19
|
+
- [The scope model](#the-scope-model)
|
|
20
|
+
- [Options](#options)
|
|
21
|
+
- [Writing a render target](#writing-a-render-target)
|
|
22
|
+
- [Content Security Policy](#content-security-policy)
|
|
23
|
+
- [Documentation](#documentation)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
14
26
|
## Install
|
|
15
27
|
|
|
16
28
|
```bash
|
|
@@ -63,7 +75,7 @@ Compiles a report and returns the compiled report. `stream(data)` is the raw eve
|
|
|
63
75
|
(e.g. `html()` from `@quario/html`) or your own.
|
|
64
76
|
|
|
65
77
|
A render is async: quario awaits key verification before the target sees its first event. A
|
|
66
|
-
malformed target throws synchronously from `render`, before the first event
|
|
78
|
+
malformed target throws synchronously from `render`, before the first event. Definition problems
|
|
67
79
|
throw earlier, at `report()`.
|
|
68
80
|
|
|
69
81
|
The data pre-pass (select, filter, sort, aggregate) runs when you call the renderer, because a
|
|
@@ -80,7 +92,7 @@ report.paths; // padvinder's deeply frozen dependency topology for the `data` qu
|
|
|
80
92
|
|
|
81
93
|
Events arrive in render order: `report-start`, report `header` items, then either the `empty`
|
|
82
94
|
items or the group/detail walk, then `footer` items, `report-end`. Group instances bracket their
|
|
83
|
-
content with `group-start`/`group-end
|
|
95
|
+
content with `group-start`/`group-end`. A table detail yields `table-start`, one `row` per visible
|
|
84
96
|
row, one `total-row` per emitted total row, and `table-end`.
|
|
85
97
|
|
|
86
98
|
| Event | Carries |
|
|
@@ -88,9 +100,9 @@ row, one `total-row` per emitted total row, and `table-end`.
|
|
|
88
100
|
| `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
101
|
| `item` | `role`, `path`, `tokens`, optional `style`, `run` |
|
|
90
102
|
| `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
|
|
91
|
-
| `split-start` | `role`, `slots` (each an optional `width`), optional `style
|
|
103
|
+
| `split-start` | `role`, `slots` (each an optional `width`), optional `style`. One `item` or `image` per slot follows, then `split-end` |
|
|
92
104
|
| `split-end` | - |
|
|
93
|
-
| `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `reset`, `columns`
|
|
105
|
+
| `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `breakAfter`, `reset`, `columns` |
|
|
94
106
|
| `group-end` | `name`, `depth` |
|
|
95
107
|
| `table-start` | `path`, `header` (`cells`, optional `style`), `columns` (each an optional `width`) |
|
|
96
108
|
| `row` | `cells`, optional `style`, `run` |
|
|
@@ -108,18 +120,18 @@ a literal and a value, so the cell is text only.
|
|
|
108
120
|
|
|
109
121
|
**A page-number token names itself.** A value token whose interpolation is exactly
|
|
110
122
|
`{{ page.number }}` or `{{ page.total }}` also carries `field`, holding that same string. The
|
|
111
|
-
value is the number
|
|
123
|
+
value is the number the render used for this page. `field` says which page value the token stands
|
|
112
124
|
for, so a target whose own document format numbers pages can write its own live field there
|
|
113
125
|
instead. Anything computed from them carries no `field`.
|
|
114
126
|
|
|
115
127
|
**Escaping is the consumer's job.** A target that embeds values in markup must escape them at its
|
|
116
128
|
own edge.
|
|
117
129
|
|
|
118
|
-
**`report-start.marking`** carries the evaluation wording when the render
|
|
119
|
-
verification is still settling. Licensed streams omit it. Targets place the marking
|
|
130
|
+
**`report-start.marking`** carries the evaluation wording when no license covers the render, or while
|
|
131
|
+
verification is still settling. Licensed streams omit it. Targets place the marking. They do not
|
|
120
132
|
author its wording. **`columns`** on `report-start` / `group-start` is the declared
|
|
121
133
|
[page column](https://getquario.com/docs/diving-deeper/bands/#flowing-in-columns) count when
|
|
122
|
-
present. `@quario/pdf` and `@quario/html` lay page columns out
|
|
134
|
+
present. `@quario/pdf` and `@quario/html` lay page columns out.
|
|
123
135
|
xlsx never will.
|
|
124
136
|
|
|
125
137
|
### `text(tokens)`
|
|
@@ -133,7 +145,7 @@ Exactly one value token holding a finite number, a boolean, or a valid `Date` ke
|
|
|
133
145
|
pre-stringify value. Anything else, including a lone null, reports `undefined` and joins to
|
|
134
146
|
display text. Passing the cell's resolved `format` kind opts into the seam's one coercion: under
|
|
135
147
|
`"date"`, an RFC 3339 string revives to the `Date` it names. Spreadsheet consumers use this for
|
|
136
|
-
real numeric cells
|
|
148
|
+
real numeric cells.
|
|
137
149
|
[`@quario/csv`](https://www.npmjs.com/package/@quario/csv) is the short form.
|
|
138
150
|
|
|
139
151
|
### `styledRuns(tokens)`
|
|
@@ -147,14 +159,22 @@ target cannot drift from them.
|
|
|
147
159
|
### `walk(events, handlers)` / `breathe()`
|
|
148
160
|
|
|
149
161
|
`walk` is the delivery driver every official target uses. Pass one render's event iterable and
|
|
150
|
-
per-event handlers keyed by type
|
|
151
|
-
the loop
|
|
162
|
+
per-event handlers keyed by type. A missing handler ignores that event. It pulls on demand,
|
|
163
|
+
returns the loop between batches, and delivers the opening event before pulling a second, so a
|
|
152
164
|
target can settle `report-start` (page bands, marking) there instead of draining the stream
|
|
153
165
|
itself.
|
|
154
166
|
|
|
155
|
-
`breathe()` is that
|
|
167
|
+
`breathe()` is that return alone. Await it between batches of a loop you own. `walk` already
|
|
156
168
|
calls it for you.
|
|
157
169
|
|
|
170
|
+
### `splits(events)`
|
|
171
|
+
|
|
172
|
+
A split reaches the stream as a bracket: `split-start`, one `item` or `image` per slot, `split-end`.
|
|
173
|
+
`splits` folds each bracket into one `split` event that carries the opening's fields and the slot
|
|
174
|
+
events under `items`, and passes every other event through, lazily. Read `walk(splits(events),
|
|
175
|
+
handlers)` and register a `split` handler where you want a split whole, and keep no bracket state of
|
|
176
|
+
your own. Every official target reads its splits this way.
|
|
177
|
+
|
|
158
178
|
### Presentation helpers
|
|
159
179
|
|
|
160
180
|
A target that stringifies imports these rather than restating them, so every surface presents a
|
|
@@ -163,11 +183,11 @@ cell the same way:
|
|
|
163
183
|
- `display(value)` — the scalar rule `text()` joins with: a `Date` as ISO 8601 UTC, nullish as
|
|
164
184
|
the empty string, everything else `String(value)`.
|
|
165
185
|
- `format(value, style?, options?)` — presents a token under the cell's resolved `format`
|
|
166
|
-
declaration (its kind and modifier, and for `currency` the cell's own code)
|
|
167
|
-
`display()`.
|
|
186
|
+
declaration (its kind and modifier, and for `currency` the cell's own code). It answers
|
|
187
|
+
`undefined` when the kind does not apply, so the caller falls back to `display()`.
|
|
168
188
|
- `fractionDigits(style?, options?)` — the digit count a resolved `format` declaration presents:
|
|
169
189
|
the kind's own (two for `number` and `percent`, a currency's minor units for `currency`) unless
|
|
170
|
-
the declaration's `digits` overrides it
|
|
190
|
+
the declaration's `digits` overrides it. It answers `undefined` where there is no count.
|
|
171
191
|
- `currencyOf(style?, options?)` — which code a money cell wears: its own, else the instance's.
|
|
172
192
|
- `isReportBand(role)` — whether a role names one of the report's own bands rather than a
|
|
173
193
|
group's.
|
|
@@ -176,8 +196,8 @@ cell the same way:
|
|
|
176
196
|
|
|
177
197
|
### `validate(schema, functions?)`
|
|
178
198
|
|
|
179
|
-
|
|
180
|
-
empty array means valid.
|
|
199
|
+
Validates a definition without rendering it. It returns every problem as a path-prefixed string.
|
|
200
|
+
An empty array means valid.
|
|
181
201
|
|
|
182
202
|
```js
|
|
183
203
|
validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
|
|
@@ -202,7 +222,7 @@ is not a problem at a lower severity, which is why it has no `diagnostic` — no
|
|
|
202
222
|
engine decided. Neither warning below locates into an authored source, so none carries a `source`
|
|
203
223
|
today. **A warning is never fatal**: a document carrying only warnings compiles and
|
|
204
224
|
renders, so `report` is `null` on `problems` alone. Two declarations warn today — a `currency` on
|
|
205
|
-
a cell whose `format` is not `"currency"`, and a table where every column
|
|
225
|
+
a cell whose `format` is not `"currency"`, and a table where the author sized every column and the widths
|
|
206
226
|
total under 100 — and the list is advisory and deliberately incomplete, so a quiet one is not a
|
|
207
227
|
promise that every declaration will be read. `validate()` returns problems only.
|
|
208
228
|
|
|
@@ -217,10 +237,10 @@ else console.error(problems[0].path, problems[0].message);
|
|
|
217
237
|
|
|
218
238
|
True when a caught value is a **located diagnostic**: an error xprsn, sjabloon or padvinder
|
|
219
239
|
minted — thrown by that engine, or re-thrown by quario with the engine original behind it.
|
|
220
|
-
Authentication
|
|
240
|
+
Authentication tests identity. An error that only matches the shape does not pass.
|
|
221
241
|
|
|
222
|
-
|
|
223
|
-
function's own throw
|
|
242
|
+
Location is not what the guard reads: quario's own verdicts on a document and a registered
|
|
243
|
+
function's own throw carry a location too, and neither is a diagnostic. Errors a report throws
|
|
224
244
|
name the path they failed at — and the offending source, where there is one — while keeping their
|
|
225
245
|
original type (`SyntaxError`, `TypeError`, `RangeError`). What a diagnostic adds on top is
|
|
226
246
|
metadata an engine vouches for: `code`, `start`/`end` offsets, and, for a query budget in place
|
|
@@ -246,7 +266,7 @@ try {
|
|
|
246
266
|
| `run.<name>` | Running accumulator values on the current detail row |
|
|
247
267
|
|
|
248
268
|
Each anchor is a distinct object. Absent reads are `null`, so `x == null` holds for a missing
|
|
249
|
-
field
|
|
269
|
+
field. Reading _through_ a null base still throws, so use `?.`.
|
|
250
270
|
|
|
251
271
|
## Options
|
|
252
272
|
|
|
@@ -254,15 +274,15 @@ field; reading _through_ a null base still throws, so use `?.`.
|
|
|
254
274
|
quario({ query: { maxNodes: 10_000, maxDepth: 64, maxResults: 1_000 } }).report(schema, functions);
|
|
255
275
|
```
|
|
256
276
|
|
|
257
|
-
`query` bounds the JSONPath data selection. Hosts set budgets through this API argument
|
|
258
|
-
|
|
277
|
+
`query` bounds the JSONPath data selection. Hosts set budgets through this API argument.
|
|
278
|
+
Definitions do not carry it. Failures point at `data`, keep their `RangeError` type, and
|
|
259
279
|
carry `code`, `limit`, and `actual`. Every render starts with fresh counters.
|
|
260
280
|
|
|
261
281
|
## Writing a render target
|
|
262
282
|
|
|
263
283
|
Read the stream through the public API. Do not reach for engine internals. A complete, tested
|
|
264
|
-
Markdown target lives in the repository at `example/markdown.js` in
|
|
265
|
-
targets
|
|
284
|
+
Markdown target lives in the repository at `example/markdown.js` in under 60 lines of code. The built-in
|
|
285
|
+
targets use the same public API.
|
|
266
286
|
|
|
267
287
|
Two rules a target owes its users: escape or neutralize every `value` token at your own edge, and
|
|
268
288
|
map the [style vocabulary](https://getquario.com/docs/reference/style-declarations/) to your own
|
|
@@ -291,10 +311,10 @@ the [engine reference](https://getquario.com/docs/reference/quario/) is this pac
|
|
|
291
311
|
|
|
292
312
|
## License
|
|
293
313
|
|
|
294
|
-
Commercial software with readable source.
|
|
295
|
-
licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
|
|
314
|
+
Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
|
|
315
|
+
Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
|
|
296
316
|
|
|
297
|
-
Pass your license key in the options
|
|
317
|
+
Pass your license key in the options. quario verifies it offline:
|
|
298
318
|
|
|
299
319
|
```js
|
|
300
320
|
const q = quario({ license: "quario_..." });
|
package/lib/band-height.js
CHANGED
|
@@ -12,10 +12,8 @@
|
|
|
12
12
|
* node a fault names.
|
|
13
13
|
*
|
|
14
14
|
* The traversal half is a guard the descent carries rather than a second walk
|
|
15
|
-
* of the schema: `plan.js` offers it each band as it compiles it,
|
|
16
|
-
*
|
|
17
|
-
* every other problem and the group chain is read once
|
|
18
|
-
* (docs/agents/semantics.md, "One traversal, read two ways").
|
|
15
|
+
* of the schema: `plan.js` offers it each band as it compiles it, so a fault
|
|
16
|
+
* lands in the documented key order beside every other problem.
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import { isExpr, positivePts } from "./style.js";
|
|
@@ -74,19 +72,16 @@ let firstOccupying = (list, path) => {
|
|
|
74
72
|
* The guard the traversal carries while it descends.
|
|
75
73
|
*
|
|
76
74
|
* Which band sits under the pinned box is partly the document's answer and
|
|
77
|
-
* partly the data's. With rows, every group level opens
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* own. Both outcomes are reachable for any report declaring both, and the
|
|
84
|
-
* author declared each, so a lead on either is refused.
|
|
75
|
+
* partly the data's. With rows, every group level opens before any detail
|
|
76
|
+
* row, so the first group header carrying an occupying item is that band —
|
|
77
|
+
* the order `bandOf` compiles them in, so `body` takes the first band that
|
|
78
|
+
* says anything. With no rows the `empty` band replaces all of it, judged on
|
|
79
|
+
* its own by `instead`. Both outcomes are reachable for any report declaring
|
|
80
|
+
* both, so a lead on either is refused.
|
|
85
81
|
*
|
|
86
|
-
* A table `detail` reaches neither
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* there is no lead a table could carry.
|
|
82
|
+
* A table `detail` reaches neither and needs no case: `table-start` and `row`
|
|
83
|
+
* are not occupying events, and the style vocabulary refuses `spaceBefore` on
|
|
84
|
+
* a table, a row and a cell alike.
|
|
90
85
|
*
|
|
91
86
|
* @param {(path: string, message: string) => void} bad The traversal's collector.
|
|
92
87
|
* @returns {{ arm: (height: number | null) => void,
|
package/lib/format.js
CHANGED
|
@@ -11,11 +11,9 @@
|
|
|
11
11
|
* renders the same bytes on every machine (docs/adr/0041, docs/adr/0025).
|
|
12
12
|
*
|
|
13
13
|
* The kind, its digit count, and a date's form all come off the one resolved
|
|
14
|
-
* declaration `formatOf` answers — the same object the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* carries no count presents nothing here and the caller falls back to
|
|
18
|
-
* `display()`.
|
|
14
|
+
* declaration `formatOf` answers — the same object the XLSX target builds its
|
|
15
|
+
* number formats from — so a cell shows the same digits wherever it is
|
|
16
|
+
* rendered (docs/adr/0054, docs/adr/0056).
|
|
19
17
|
*/
|
|
20
18
|
import { boundedMemo } from "./memo.js";
|
|
21
19
|
import { currencyOf, formatOf } from "./style.js";
|
|
@@ -29,29 +27,23 @@ let zoneOf = (options) => options?.timeZone || "UTC";
|
|
|
29
27
|
// carries the rules a bounded memo has; what is here is what only this call
|
|
30
28
|
// site knows -- its key, and its cap.
|
|
31
29
|
//
|
|
32
|
-
// **The key is everything the formatter is made from**,
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// pair must differ in **only** the piece it is there for: EUR against JPY
|
|
40
|
-
// would prove nothing about the code, because their digit counts already
|
|
41
|
-
// differ and that piece would separate them on its own.
|
|
30
|
+
// **The key is everything the formatter is made from**, by value rather than
|
|
31
|
+
// object identity, because no identity survives both paths: a literal style
|
|
32
|
+
// block folds to one frozen declaration every cell shares, while a block
|
|
33
|
+
// holding an `=` resolves a fresh one per cell. Get a piece wrong and one
|
|
34
|
+
// cell presents under another's formatter — a USD row reading as EUR — which
|
|
35
|
+
// `test/semantics.test.js` pins a pair for, one piece at a time. Each pair
|
|
36
|
+
// must differ in **only** the piece it is there for.
|
|
42
37
|
//
|
|
43
38
|
// **The cap is the part to read**, because the key space is not the host's to
|
|
44
39
|
// bound. A currency code reaches this from author data through
|
|
45
40
|
// `currency: "=@.ccy"`, gated only to three uppercase letters, and `digits`
|
|
46
|
-
// can be an `=` result too: 17,576 codes times 21 counts is 369,000 keys for
|
|
47
|
-
//
|
|
48
|
-
// held for the life of the process on data nobody vetted.
|
|
41
|
+
// can be an `=` result too: 17,576 codes times 21 counts is 369,000 keys for
|
|
42
|
+
// one locale, about 90 MB at 244 bytes retained per entry.
|
|
49
43
|
//
|
|
50
|
-
// 2048
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// costs bytes and lowering it costs rebuilt formatters; neither is a
|
|
54
|
-
// correctness knob.
|
|
44
|
+
// 2048 is about 500 KB at the cap, against a real report reaching a small
|
|
45
|
+
// multiple of the ISO codes in circulation. Neither bound is a correctness
|
|
46
|
+
// knob.
|
|
55
47
|
//
|
|
56
48
|
// Memoising a fact that cannot change within an ICU version, like
|
|
57
49
|
// `fractionDigits`' own minor-units map (docs/adr/0025).
|
package/lib/host.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host got wrong in a target factory's options, named. A target
|
|
3
|
+
* refuses what it does not understand at the factory call, so a typo costs
|
|
4
|
+
* one stack trace at the line the host wrote rather than a wrong document
|
|
5
|
+
* nothing reports (`docs/adr/0072`).
|
|
6
|
+
*
|
|
7
|
+
* Here rather than in a target for the reason `imageError` carries beside it:
|
|
8
|
+
* every target needs it, no two of them may depend on each other, and the
|
|
9
|
+
* engine is what they all already have. Nothing an author writes reaches this
|
|
10
|
+
* module -- these are host API, and no located diagnostic is involved.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {(msg: string) => never} */
|
|
14
|
+
let fail = (msg) => {
|
|
15
|
+
throw TypeError(msg);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An object a host may have written by hand: not an array, not a function, and
|
|
20
|
+
* not `null`, each of which is a different mistake wearing `typeof "object"`.
|
|
21
|
+
*
|
|
22
|
+
* @type {(value: unknown) => boolean}
|
|
23
|
+
*/
|
|
24
|
+
let plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
|
|
26
|
+
// Own keys only: an inherited enumerable property is the prototype's business,
|
|
27
|
+
// and naming it back at the host would name something they never wrote.
|
|
28
|
+
/** @type {(value: object, keys: readonly string[], at: string) => void} */
|
|
29
|
+
let closed = (value, keys, at) => {
|
|
30
|
+
for (let key of Object.keys(value))
|
|
31
|
+
if (!keys.includes(key)) fail(at + ': unknown option "' + key + '"');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A target's own options, with its key set closed. Nullish is absence, which
|
|
36
|
+
* is what the engine does with every value a host omits, so a host writing
|
|
37
|
+
* `meta: config.meta ?? null` is not a host making a mistake.
|
|
38
|
+
*
|
|
39
|
+
* @template T
|
|
40
|
+
* @param {T} value The options object, or a nested one.
|
|
41
|
+
* @param {readonly string[]} keys Every key this object may carry.
|
|
42
|
+
* @param {string} at The option path, which prefixes the failure.
|
|
43
|
+
* @returns {T} `value`, unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export let hostOptions = (value, keys, at) => {
|
|
46
|
+
if (value == null) return value;
|
|
47
|
+
if (!plain(value)) fail(at + ": expected an object");
|
|
48
|
+
closed(value, keys, at);
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The three document properties a host may write. */
|
|
53
|
+
let PROPERTIES = ["title", "author", "subject"];
|
|
54
|
+
|
|
55
|
+
// Nullish is absence here too, one level down: a host writing
|
|
56
|
+
// `{ title: config.title }` over a config that carries no title has made no
|
|
57
|
+
// mistake, and a target skips the property the same way it skips a key that
|
|
58
|
+
// was never written.
|
|
59
|
+
/** @type {(value: any, at: string) => void} */
|
|
60
|
+
let everyString = (value, at) => {
|
|
61
|
+
for (let key of Object.keys(value))
|
|
62
|
+
if (value[key] != null && typeof value[key] !== "string")
|
|
63
|
+
fail(at + "." + key + ": expected a string");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The `meta` option every document target takes, validated whole: those three
|
|
68
|
+
* properties, each a string, and never a date. What a host cannot write is
|
|
69
|
+
* what keeps a render reproducible -- each of these formats has one part with
|
|
70
|
+
* a slot for a clock, and none of them has a way to fill it.
|
|
71
|
+
*
|
|
72
|
+
* The contract is one thing, so it is checked in one place. What stays a
|
|
73
|
+
* target's own is the name each property carries in its format.
|
|
74
|
+
*
|
|
75
|
+
* Returns a **copy**, so the properties are read once, here. A host that
|
|
76
|
+
* mutates its options object between renders cannot make one configured
|
|
77
|
+
* target write two different documents, and no target has to remember to
|
|
78
|
+
* copy for itself.
|
|
79
|
+
*
|
|
80
|
+
* @param {any} value The `meta` option.
|
|
81
|
+
* @param {string} at The option path, which prefixes the failure.
|
|
82
|
+
* @returns {any} A copy of `value`, or undefined when the host wrote none.
|
|
83
|
+
*/
|
|
84
|
+
export let hostMeta = (value, at) => {
|
|
85
|
+
hostOptions(value, PROPERTIES, at);
|
|
86
|
+
if (value == null) return undefined;
|
|
87
|
+
everyString(value, at);
|
|
88
|
+
return { ...value };
|
|
89
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -367,11 +367,15 @@ export interface TableDetail {
|
|
|
367
367
|
export interface Group {
|
|
368
368
|
name: string;
|
|
369
369
|
by: `=${string}`;
|
|
370
|
-
/** Start every instance of this group on a new page (paginated targets). */
|
|
371
|
-
break?: "page";
|
|
372
370
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
371
|
+
* Where this group turns a page (paginated targets). Every boundary between
|
|
372
|
+
* consecutive instances turns; `before` adds the run's leading edge, `after`
|
|
373
|
+
* its trailing edge, and `around` both.
|
|
374
|
+
*/
|
|
375
|
+
break?: "before" | "between" | "after" | "around";
|
|
376
|
+
/**
|
|
377
|
+
* Restart `page.number` / `page.total` at every instance. It turns no page
|
|
378
|
+
* of its own, so it requires `break` to be `"before"` or `"around"`.
|
|
375
379
|
*/
|
|
376
380
|
reset?: "page";
|
|
377
381
|
/**
|
|
@@ -664,6 +668,8 @@ export interface ImageEvent {
|
|
|
664
668
|
export interface SplitStartEvent {
|
|
665
669
|
type: "split-start";
|
|
666
670
|
role: ItemRole;
|
|
671
|
+
/** The split definition's schema path. */
|
|
672
|
+
path: string;
|
|
667
673
|
/** One entry per slot, in order; `width` is absent on a width-less slot. */
|
|
668
674
|
slots: { width?: number }[];
|
|
669
675
|
style?: Record<string, unknown>;
|
|
@@ -673,6 +679,15 @@ export interface SplitEndEvent {
|
|
|
673
679
|
type: "split-end";
|
|
674
680
|
}
|
|
675
681
|
|
|
682
|
+
/**
|
|
683
|
+
* A split bracket folded whole by `splits()`: the opening event's fields, and
|
|
684
|
+
* the slot events in slot order under `items`. No stream emits it.
|
|
685
|
+
*/
|
|
686
|
+
export interface SplitEvent extends Omit<SplitStartEvent, "type"> {
|
|
687
|
+
type: "split";
|
|
688
|
+
items: (ItemEvent | ImageEvent)[];
|
|
689
|
+
}
|
|
690
|
+
|
|
676
691
|
export interface GroupStartEvent {
|
|
677
692
|
type: "group-start";
|
|
678
693
|
name: string;
|
|
@@ -681,7 +696,10 @@ export interface GroupStartEvent {
|
|
|
681
696
|
depth: number;
|
|
682
697
|
key: unknown;
|
|
683
698
|
aggregates: Record<string, unknown>;
|
|
699
|
+
/** A page turns before this instance, resolved from the group's `break`. */
|
|
684
700
|
break?: "page";
|
|
701
|
+
/** A page turns after this instance, resolved from the group's `break`. */
|
|
702
|
+
breakAfter?: "page";
|
|
685
703
|
/** Restart `page.number` / `page.total` at every instance (paginated targets). */
|
|
686
704
|
reset?: "page";
|
|
687
705
|
/** The declared page column count, present when the group declares one. */
|
|
@@ -692,6 +710,8 @@ export interface GroupEndEvent {
|
|
|
692
710
|
type: "group-end";
|
|
693
711
|
name: string;
|
|
694
712
|
depth: number;
|
|
713
|
+
/** Present when the instance is hollow: nothing but group brackets arrived between its ends. */
|
|
714
|
+
hollow?: true;
|
|
695
715
|
}
|
|
696
716
|
|
|
697
717
|
export interface TableStartEvent {
|
|
@@ -766,9 +786,21 @@ export interface ReportEventStream {
|
|
|
766
786
|
* ignores that event.
|
|
767
787
|
*/
|
|
768
788
|
export type WalkHandlers = {
|
|
769
|
-
[K in
|
|
789
|
+
[K in WalkEvent["type"]]?: (event: Extract<WalkEvent, { type: K }>) => void;
|
|
770
790
|
};
|
|
771
791
|
|
|
792
|
+
/** What the walk driver dispatches: a stream event, or a split `splits()` folded. */
|
|
793
|
+
export type WalkEvent = ReportEvent | SplitEvent;
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The split bracket folded: every `split-start`, its slot events and its
|
|
797
|
+
* `split-end` become one `split` event, and every other event passes through
|
|
798
|
+
* untouched, lazily. Read `splits(stream)` where a split is wanted whole rather
|
|
799
|
+
* than keeping a bracket of your own; register a `split` handler and no
|
|
800
|
+
* `split-start` or `split-end` one.
|
|
801
|
+
*/
|
|
802
|
+
export function splits(events: Iterable<ReportEvent>): Generator<WalkEvent, void>;
|
|
803
|
+
|
|
772
804
|
/**
|
|
773
805
|
* The walk driver: dispatch one render's event stream to per-event handlers,
|
|
774
806
|
* in stream order and exactly once each, pulling lazily and handing the loop
|
|
@@ -778,7 +810,7 @@ export type WalkHandlers = {
|
|
|
778
810
|
* than pulling the stream itself; no opening event is required, and a stream
|
|
779
811
|
* that starts part-way through walks like any other.
|
|
780
812
|
*/
|
|
781
|
-
export function walk(events: Iterable<
|
|
813
|
+
export function walk(events: Iterable<WalkEvent>, handlers: WalkHandlers): Promise<void>;
|
|
782
814
|
|
|
783
815
|
/**
|
|
784
816
|
* Hand the event loop back to the host, resolving once it has had its turn.
|
|
@@ -920,6 +952,33 @@ export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
|
|
|
920
952
|
*/
|
|
921
953
|
export function imageError(path: string | undefined, said: string, cause?: unknown): Error;
|
|
922
954
|
|
|
955
|
+
/** Optional document information; never includes dates, so output stays deterministic. */
|
|
956
|
+
export interface QuarioMeta {
|
|
957
|
+
title?: string;
|
|
958
|
+
author?: string;
|
|
959
|
+
subject?: string;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Close a **target factory's** option key set, for the target itself to call.
|
|
964
|
+
* A target refuses what it does not understand at the factory call, so a
|
|
965
|
+
* host's typo costs one stack trace where they wrote it rather than an option
|
|
966
|
+
* silently lost. Throws a `TypeError` prefixed with `at`.
|
|
967
|
+
*
|
|
968
|
+
* Nullish is absence. Own enumerable keys only, so an inherited property is
|
|
969
|
+
* not reported back as an option the host wrote.
|
|
970
|
+
*/
|
|
971
|
+
export function hostOptions<T>(value: T, keys: readonly string[], at: string): T;
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Validate a target's `meta` option whole: `title`, `author` and `subject`,
|
|
975
|
+
* each a string, and nothing else. Every document target takes the same three,
|
|
976
|
+
* so the contract is checked once; the name each property carries inside a
|
|
977
|
+
* given format stays that target's own. Throws a `TypeError` prefixed with
|
|
978
|
+
* `at`.
|
|
979
|
+
*/
|
|
980
|
+
export function hostMeta<T extends QuarioMeta | null | undefined>(value: T, at: string): T;
|
|
981
|
+
|
|
923
982
|
/**
|
|
924
983
|
* Join a token stream to display text: literals verbatim, values through
|
|
925
984
|
* `display()`. Re-exported from sjabloon.
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { opt } from "./stream.js";
|
|
|
24
24
|
// The event stream's public seam, single-sourced in ./stream.js, and the
|
|
25
25
|
// diagnostic predicate in ./locate.js. Re-exported here because a consumer
|
|
26
26
|
// imports them from the package, not from a file inside it.
|
|
27
|
-
export { breathe, display, isReportBand, styledRuns, text, typed, walk } from "./stream.js";
|
|
27
|
+
export { breathe, display, isReportBand, splits, styledRuns, text, typed, walk } from "./stream.js";
|
|
28
28
|
export { format } from "./format.js";
|
|
29
29
|
export {
|
|
30
30
|
currencyOf,
|
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
STYLE_NAMES,
|
|
35
35
|
} from "./style.js";
|
|
36
36
|
export { imageError, isDiagnostic } from "./locate.js";
|
|
37
|
+
export { hostMeta, hostOptions } from "./host.js";
|
|
37
38
|
|
|
38
39
|
/** @typedef {import("./scope.js").Scope} Scope */
|
|
39
40
|
|