quario 0.9.0 → 0.11.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 +64 -0
- package/README.md +40 -3
- package/lib/band-height.js +24 -35
- package/lib/format.js +46 -25
- package/lib/index.d.ts +140 -17
- package/lib/index.js +107 -7
- package/lib/license.js +1 -1
- package/lib/locate.js +15 -9
- package/lib/math.js +11 -20
- package/lib/names.js +43 -0
- package/lib/plan.js +227 -99
- package/lib/precision.js +4 -4
- package/lib/reducers.js +46 -24
- package/lib/stream.js +147 -49
- package/lib/style.js +198 -101
- package/package.json +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# quario
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **`isBoxName(name)` is exported.** It answers whether a style name is part of the box — a padding or a border part — which is the partition the spec defines a table row's box by. A target that strips the box off a style, or a tool that lists the box names, imports it beside `STYLE_NAMES` rather than matching the two prefixes itself.
|
|
8
|
+
- **A number kind reads `negative` and `zero`.** `format` gains two modifiers on `number`, `currency` and `percent`: `negative` is `"minus"` or `"parens"`, and `zero` is `"number"` or `"dash"`. Under `parens` a negative reads as its magnitude in parentheses, and under `dash` a zero reads as `-`. Both defaults are what every kind presented before, so no existing report moves. They compose with `digits`, with the percent kind's scaling and with a currency's code. A value outside either pair is a located definition error, and `date` takes neither. The resolved declaration a cell carries now states all three of a number kind's modifiers, so no consumer defaults one for itself.
|
|
9
|
+
- **A group may declare `collapsed`.** One optional literal, `true` or `false`, asking that the instance open **shut**: its header and footer are shown and the content between them is not. Aggregates are untouched — the rows stay in the partition and still count, so a footer totals what nobody is shown. The `group-start` event carries it, and the new `collapse(stream)` helper is what a target with nowhere to hide a row reads the stream through: it drops a collapsed instance's content, nested instances whole, and keeps its brackets and its own bands. An expression is a located definition error.
|
|
10
|
+
- **A group may declare a `label`.** One optional template beside `name` and `by`, resolved once per instance in group scope — the instance's own handle and `$` are bound, `@` is unbound as in any group band — and carried on `group-start` as the resolved text. It names the instance for navigation rather than for the page: a target with somewhere to put a navigation name spends it there, and the others withdraw it, so no page changes. A value that is not a template string is a located definition error, and a hollow instance stays unlisted whether it declares one or not.
|
|
11
|
+
- **A run may carry an `href`.** The twelfth name in the styled run's inline subset, and the one declaration this engine judges rather than passes on: a URL names a location, and `javascript:` is a string-to-code path that escaping at the markup edge cannot answer. A URL is admitted before it crosses, against a host allowlist — `quario({ schemes })`, defaulting to `https:` and `mailto:` — so a target receives a URL already admitted or none at all. A refused value is omitted, so the text renders and the link does not; a URL stating no scheme is refused with the rest, because a scheme list cannot say "no scheme" without admitting `//host/path`. An `href` beginning `#` names a group's `label` and needs no allowlist. A `schemes` option the engine cannot read throws at construction rather than falling back to the default, because an allowlist quietly replaced is a host believing it restricted something it had not. A table row refuses `href`, as it refuses every declaration that presents a value.
|
|
12
|
+
- **`REPORT_STYLE_NAMES`.** The two names a report default may declare, `family` and `size`, in the vocabulary's own order — the same export `RUN_STYLE_NAMES` already is, so a tool that offers them reads the engine rather than keeping a copy.
|
|
13
|
+
- **A document may mark a declaration `required`, and `plan` checks it against the targets you name.** `"required": { "uppercase": true }` at the report root says which declarations this document cannot do without; `plan(schema, funcs, { targets })` takes the `capabilities` descriptors of the targets it is meant for, and every required declaration one of them withdraws or leaves unread becomes a problem of its own at `required.<name>`, which nulls the report as any other problem does. An approximated declaration is not one. Every withdrawal was silent by design before this, so this is how an author says that one of them matters. Additive throughout: no target sees the marking, no render changes because of it, and passing no descriptors checks nothing. A name outside the set the support matrix answers for is a definition error, because a marking nothing can check would go quiet exactly when it was needed. `REQUIRABLE` exports that set.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- **The declarations now state every `format` modifier the engine settles.** `FormatDeclaration` and `ResolvedFormat` carry `negative` and `zero` beside `digits` and `form`, and `FORMAT_VOCABULARY` offers the two readings and their defaults — `negatives`, `zeroes`, `defaultNegative`, `defaultZero` — with `modifiers` naming every modifier a kind takes rather than one of them. The engine has always read and resolved those declarations; the types said otherwise, so a TypeScript consumer writing one was told it was an error, and a tool reading the vocabulary to offer them found only the count.
|
|
18
|
+
- **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.
|
|
19
|
+
|
|
20
|
+
The viewer's and the editor's own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout's own account of the value behind it.
|
|
21
|
+
|
|
22
|
+
- **An all-authored split whose slot widths sum under 100 warns, as a table's columns already did.** A split's slots carry the same percentage shares under the same arithmetic, so the plan now reports the trailing width they leave unused, on the split's `slots` path. The report still renders; the warning is advisory, like every other.
|
|
23
|
+
|
|
24
|
+
## 0.10.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
**`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.
|
|
33
|
+
|
|
34
|
+
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`.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
- **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.
|
|
41
|
+
|
|
42
|
+
```jsonc
|
|
43
|
+
// the customer a region's rows open and close on
|
|
44
|
+
{ "aggregates": { "opened": "first:=@.customer", "closed": "last:=@.customer" } }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
{{ first(@.lines, l => l.sku) }} the first line's article number
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
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`.
|
|
54
|
+
|
|
55
|
+
Reports that already render are unaffected. A document that spelled `first:=...` before was a definition error, and it now compiles.
|
|
56
|
+
|
|
57
|
+
- **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.
|
|
58
|
+
|
|
59
|
+
`@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.
|
|
60
|
+
|
|
61
|
+
- **`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.
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
Every built-in target reads its splits this way now, so a custom target can too.
|
|
66
|
+
|
|
3
67
|
## 0.9.0
|
|
4
68
|
|
|
5
69
|
### 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
|
|
@@ -90,7 +102,7 @@ row, one `total-row` per emitted total row, and `table-end`.
|
|
|
90
102
|
| `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
|
|
91
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` |
|
|
@@ -155,6 +167,14 @@ itself.
|
|
|
155
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
|
|
@@ -173,6 +193,8 @@ cell the same way:
|
|
|
173
193
|
group's.
|
|
174
194
|
- `STYLE_NAMES` — the closed style vocabulary, in the spec's order, and
|
|
175
195
|
`RUN_STYLE_NAMES` — the inline half of it, which is what a styled run may wear.
|
|
196
|
+
- `isBoxName(name)` — whether a style name is part of the box, a padding or a border part. The
|
|
197
|
+
spec defines a row's box by this partition, and this function is its one home.
|
|
176
198
|
|
|
177
199
|
### `validate(schema, functions?)`
|
|
178
200
|
|
|
@@ -187,7 +209,7 @@ validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
|
|
|
187
209
|
`report()` throws on the first problem instead. Validation and compilation share one traversal,
|
|
188
210
|
so `validate()` can never disagree with what `report()` accepts.
|
|
189
211
|
|
|
190
|
-
### `quario().plan(schema, functions?)`
|
|
212
|
+
### `quario().plan(schema, functions?, { targets }?)`
|
|
191
213
|
|
|
192
214
|
The one traversal, whole — for hosts that validate and render in a loop, like an editor. Returns
|
|
193
215
|
`{ report, problems, anchors, warnings }`: the compiled report (`null` while the document has
|
|
@@ -206,6 +228,21 @@ a cell whose `format` is not `"currency"`, and a table where the author sized ev
|
|
|
206
228
|
total under 100 — and the list is advisory and deliberately incomplete, so a quiet one is not a
|
|
207
229
|
promise that every declaration will be read. `validate()` returns problems only.
|
|
208
230
|
|
|
231
|
+
**`targets` checks the document against where it is going.** A document may mark declarations it
|
|
232
|
+
cannot do without — `"required": { "uppercase": true }` at its root — and hand `plan` the
|
|
233
|
+
`capabilities` descriptors of the targets it is meant for. Each required declaration one of them
|
|
234
|
+
withdraws, or leaves unread as the page bands are, becomes a problem of its own at
|
|
235
|
+
`required.<name>`, which nulls the report as any other problem does. An approximated one does not:
|
|
236
|
+
the target rendered the intent as closely as it can. Pass no descriptors and nothing is checked,
|
|
237
|
+
which is what keeps the marking additive. It is never a render-time failure.
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
import { capabilities as csv } from "@quario/csv";
|
|
241
|
+
|
|
242
|
+
const { problems } = quario().plan(schema, undefined, { targets: [csv] });
|
|
243
|
+
// required.uppercase: the csv target withdraws "uppercase"
|
|
244
|
+
```
|
|
245
|
+
|
|
209
246
|
```js
|
|
210
247
|
const { report, problems, anchors, warnings } = quario().plan(schema);
|
|
211
248
|
for (const warning of warnings) console.warn(warning.message);
|
|
@@ -261,7 +298,7 @@ carry `code`, `limit`, and `actual`. Every render starts with fresh counters.
|
|
|
261
298
|
## Writing a render target
|
|
262
299
|
|
|
263
300
|
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
|
|
301
|
+
Markdown target lives in the repository at `example/markdown.js` in under 60 lines of code. The built-in
|
|
265
302
|
targets use the same public API.
|
|
266
303
|
|
|
267
304
|
Two rules a target owes its users: escape or neutralize every `value` token at your own edge, and
|
package/lib/band-height.js
CHANGED
|
@@ -12,13 +12,11 @@
|
|
|
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
|
-
import {
|
|
19
|
+
import { positivePts } from "./style.js";
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
22
|
* A lead: authored flow spacing before an item that a target would actually
|
|
@@ -45,55 +43,46 @@ export let LEAD_REFUSED = "must be 0 after a height-declared header";
|
|
|
45
43
|
*/
|
|
46
44
|
export let leadPath = (path) => path + ".style.spaceBefore";
|
|
47
45
|
|
|
48
|
-
/**
|
|
49
|
-
* An occupancy the document leaves to the data: a `visible` written as an
|
|
50
|
-
* expression, or an image, whose bytes are expression-only and whose event is
|
|
51
|
-
* absent when its `source` yields nothing (SCHEMA.md, "Event stream"). Neither
|
|
52
|
-
* is a defect — they are how an author writes an optional item — but they mean
|
|
53
|
-
* the traversal cannot say which item comes first, so it says nothing rather
|
|
54
|
-
* than guessing (docs/adr/0052).
|
|
55
|
-
*
|
|
56
|
-
* @type {(def: any) => boolean}
|
|
57
|
-
*/
|
|
58
|
-
let dataDecides = (def) => isExpr(def?.visible) || def?.type === "image";
|
|
59
|
-
|
|
60
46
|
/**
|
|
61
47
|
* The first item of a band the document settles as occupying, `undefined` when
|
|
62
48
|
* only the render settles it, and null for a band settled to occupy nothing.
|
|
49
|
+
* Whether an item's occupancy is the data's to decide is `optional`'s answer,
|
|
50
|
+
* which the item compilers own: this file knows what a lead is and which node
|
|
51
|
+
* a fault names, not which item kinds can decline to render.
|
|
63
52
|
*
|
|
64
|
-
* @type {(list: any, path: string) => { def: any, path: string } | undefined | null}
|
|
53
|
+
* @type {(optional: (def: any) => boolean, list: any, path: string) => { def: any, path: string } | undefined | null}
|
|
65
54
|
*/
|
|
66
|
-
let firstOccupying = (list, path) => {
|
|
55
|
+
let firstOccupying = (optional, list, path) => {
|
|
67
56
|
if (!Array.isArray(list)) return null;
|
|
68
57
|
let i = list.findIndex((def) => def?.visible !== false);
|
|
69
58
|
if (i < 0) return null;
|
|
70
|
-
return
|
|
59
|
+
return optional(list[i]) ? undefined : { def: list[i], path: path + "[" + i + "]" };
|
|
71
60
|
};
|
|
72
61
|
|
|
73
62
|
/**
|
|
74
63
|
* The guard the traversal carries while it descends.
|
|
75
64
|
*
|
|
76
65
|
* 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.
|
|
66
|
+
* partly the data's. With rows, every group level opens before any detail
|
|
67
|
+
* row, so the first group header carrying an occupying item is that band —
|
|
68
|
+
* the order `bandOf` compiles them in, so `body` takes the first band that
|
|
69
|
+
* says anything. With no rows the `empty` band replaces all of it, judged on
|
|
70
|
+
* its own by `instead`. Both outcomes are reachable for any report declaring
|
|
71
|
+
* both, so a lead on either is refused.
|
|
85
72
|
*
|
|
86
|
-
* A table `detail` reaches neither
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* there is no lead a table could carry.
|
|
73
|
+
* A table `detail` reaches neither and needs no case: `table-start` and `row`
|
|
74
|
+
* are not occupying events, and the style vocabulary refuses `spaceBefore` on
|
|
75
|
+
* a table, a row and a cell alike.
|
|
90
76
|
*
|
|
91
77
|
* @param {(path: string, message: string) => void} bad The traversal's collector.
|
|
78
|
+
* @param {(def: any) => boolean} optional Whether an item definition may
|
|
79
|
+
* decline to occupy at render — the item compilers' verdict, so a new item
|
|
80
|
+
* kind that can states it where it is compiled rather than here.
|
|
92
81
|
* @returns {{ arm: (height: number | null) => void,
|
|
93
82
|
* instead: (list: any, path: string) => void,
|
|
94
83
|
* body: (list: any, path: string) => void }} The guard.
|
|
95
84
|
*/
|
|
96
|
-
export let pinGuard = (bad) => {
|
|
85
|
+
export let pinGuard = (bad, optional) => {
|
|
97
86
|
let pinned = false;
|
|
98
87
|
let settled = true;
|
|
99
88
|
/** @type {(found: { def: any, path: string } | undefined | null) => void} */
|
|
@@ -106,11 +95,11 @@ export let pinGuard = (bad) => {
|
|
|
106
95
|
settled = !pinned;
|
|
107
96
|
},
|
|
108
97
|
instead: (/** @type {any} */ list, /** @type {string} */ path) => {
|
|
109
|
-
if (pinned) refuse(firstOccupying(list, path));
|
|
98
|
+
if (pinned) refuse(firstOccupying(optional, list, path));
|
|
110
99
|
},
|
|
111
100
|
body: (/** @type {any} */ list, /** @type {string} */ path) => {
|
|
112
101
|
if (settled) return;
|
|
113
|
-
let found = firstOccupying(list, path);
|
|
102
|
+
let found = firstOccupying(optional, list, path);
|
|
114
103
|
// A band settled to occupy nothing is not the one under the box, so the
|
|
115
104
|
// descent keeps looking; one only the render settles ends the search
|
|
116
105
|
// without a verdict, because whether the band below is next is exactly
|
package/lib/format.js
CHANGED
|
@@ -11,14 +11,12 @@
|
|
|
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
|
-
import { currencyOf, formatOf } from "./style.js";
|
|
19
|
+
import { ZERO_DASH, currencyOf, formatOf } from "./style.js";
|
|
22
20
|
import { finiteDate, finiteNum, reviveDate } from "./stream.js";
|
|
23
21
|
/** @type {(options: any) => string} */
|
|
24
22
|
let localeOf = (options) => options?.locale || "en-US";
|
|
@@ -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).
|
|
@@ -122,6 +114,34 @@ let asDate = (value, locale, decl, options) => {
|
|
|
122
114
|
/** @type {Record<string, (value: any, locale: string, decl: any, options: any, style: any) => string | undefined>} */
|
|
123
115
|
let KINDS = { number: asNumber, currency: asMoney, percent: asPercent, date: asDate };
|
|
124
116
|
|
|
117
|
+
// The two presentation modifiers a number kind carries, applied around
|
|
118
|
+
// whatever its own formatter produced. A `date` carries neither, and answers
|
|
119
|
+
// `undefined` to both reads below.
|
|
120
|
+
//
|
|
121
|
+
// Parentheses are spelled here rather than taken from Intl's accounting sign:
|
|
122
|
+
// the worksheet writes the same negative section into its number format, and
|
|
123
|
+
// the two edges have to agree on one wording (docs/adr/0056, as amended).
|
|
124
|
+
// Which is also why the number is formatted from its own magnitude — the sign
|
|
125
|
+
// a locale would have written is exactly what the parentheses replace.
|
|
126
|
+
/** @type {(value: any, decl: any) => any} */
|
|
127
|
+
let magnitude = (value, decl) =>
|
|
128
|
+
decl.negative === "parens" && finiteNum(value) && value < 0 ? -value : value;
|
|
129
|
+
// What each sign reads as, given the modifiers its kind carries. A table
|
|
130
|
+
// rather than a chain, so the three cases are three entries and each one is
|
|
131
|
+
// the whole answer for its sign.
|
|
132
|
+
/** @type {Record<string, (text: string, decl: any) => string>} */
|
|
133
|
+
let SIGNED = {
|
|
134
|
+
zero: (text, decl) => (decl.zero === "dash" ? ZERO_DASH : text),
|
|
135
|
+
negative: (text, decl) => (decl.negative === "parens" ? "(" + text + ")" : text),
|
|
136
|
+
positive: (text) => text,
|
|
137
|
+
};
|
|
138
|
+
// `value === 0` is true of a negative zero, which is a zero.
|
|
139
|
+
/** @type {(value: number) => string} */
|
|
140
|
+
let signOf = (value) => (value === 0 ? "zero" : value < 0 ? "negative" : "positive");
|
|
141
|
+
/** @type {(value: any, text: string | undefined, decl: any) => string | undefined} */
|
|
142
|
+
let presented = (value, text, decl) =>
|
|
143
|
+
text === undefined || !finiteNum(value) ? text : SIGNED[signOf(value)](text, decl);
|
|
144
|
+
|
|
125
145
|
/**
|
|
126
146
|
* The helper takes the whole resolved `style`, not just the declaration, so
|
|
127
147
|
* the question "which declarations does presentation read?" is answered here
|
|
@@ -145,7 +165,8 @@ export function format(value, style, options) {
|
|
|
145
165
|
let decl = formatOf(style, options);
|
|
146
166
|
if (!decl) return;
|
|
147
167
|
try {
|
|
148
|
-
|
|
168
|
+
let text = KINDS[decl.kind](magnitude(value, decl), localeOf(options), decl, options, style);
|
|
169
|
+
return presented(value, text, decl);
|
|
149
170
|
} catch {
|
|
150
171
|
return;
|
|
151
172
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -125,6 +125,28 @@ export interface SortKey {
|
|
|
125
125
|
*/
|
|
126
126
|
export const RUN_STYLE_NAMES: readonly string[];
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The names a [report default](../../../SCHEMA.md#style-declarations) may
|
|
130
|
+
* declare, in the vocabulary's own order. A tool that offers them — the
|
|
131
|
+
* editor's Document panel — reads this rather than keeping a copy.
|
|
132
|
+
*/
|
|
133
|
+
export const REPORT_STYLE_NAMES: readonly string[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What a cell reads as under `format`'s `zero: "dash"`. Stated once, because
|
|
137
|
+
* the XLSX target writes the same character into its sectioned number format
|
|
138
|
+
* and the two edges must agree.
|
|
139
|
+
*/
|
|
140
|
+
export const ZERO_DASH: string;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The declarations a document may mark [`required`](../../../SCHEMA.md#validation),
|
|
144
|
+
* in the order the support matrix states them. Every published target's
|
|
145
|
+
* `capabilities` descriptor answers for exactly these, which is what makes a
|
|
146
|
+
* marking checkable.
|
|
147
|
+
*/
|
|
148
|
+
export const REQUIRABLE: readonly string[];
|
|
149
|
+
|
|
128
150
|
/**
|
|
129
151
|
* The declarations a styled run accepts: the inline half of the vocabulary,
|
|
130
152
|
* and nothing else. The box and flow spacing describe a block and `align`
|
|
@@ -173,26 +195,42 @@ export type FormatKind = "number" | "currency" | "percent" | "date";
|
|
|
173
195
|
* none is `"medium"`. */
|
|
174
196
|
export type DateForm = "short" | "medium" | "long" | "full";
|
|
175
197
|
|
|
198
|
+
/** How a negative amount reads. A number kind that names none is `"minus"`. */
|
|
199
|
+
export type NegativeReading = "minus" | "parens";
|
|
200
|
+
|
|
201
|
+
/** How a zero reads. A number kind that names none is `"number"`. */
|
|
202
|
+
export type ZeroReading = "number" | "dash";
|
|
203
|
+
|
|
176
204
|
/**
|
|
177
|
-
* How an author writes `format`: the kind alone, or the kind carrying
|
|
178
|
-
*
|
|
179
|
-
* presenting target and a worksheet number format can honour — and it beats
|
|
205
|
+
* How an author writes `format`: the kind alone, or the kind carrying the
|
|
206
|
+
* modifiers it takes. `digits` is a whole number from 0 to 20 — the range both
|
|
207
|
+
* a presenting target and a worksheet number format can honour — and it beats
|
|
180
208
|
* the count the kind would otherwise present, a currency's minor units
|
|
181
209
|
* included. A modifier on a kind that does not take it is a definition error.
|
|
182
210
|
*/
|
|
183
211
|
export type FormatDeclaration =
|
|
184
212
|
| FormatKind
|
|
185
|
-
| {
|
|
213
|
+
| {
|
|
214
|
+
kind: "number" | "currency" | "percent";
|
|
215
|
+
digits?: number;
|
|
216
|
+
negative?: NegativeReading;
|
|
217
|
+
zero?: ZeroReading;
|
|
218
|
+
}
|
|
186
219
|
| { kind: "date"; form?: DateForm };
|
|
187
220
|
|
|
188
221
|
/**
|
|
189
222
|
* What `format` resolves to on the event stream: never the shorthand, always
|
|
190
223
|
* the kind with the answer it presents. A number kind carries `digits` unless
|
|
191
|
-
* it has no count at all — a `currency` whose code cannot be read — and
|
|
192
|
-
* `date` always carries its `form`.
|
|
224
|
+
* it has no count at all — a `currency` whose code cannot be read — and both
|
|
225
|
+
* of its readings, and a `date` always carries its `form`.
|
|
193
226
|
*/
|
|
194
227
|
export type ResolvedFormat =
|
|
195
|
-
| {
|
|
228
|
+
| {
|
|
229
|
+
kind: "number" | "currency" | "percent";
|
|
230
|
+
digits?: number;
|
|
231
|
+
negative: NegativeReading;
|
|
232
|
+
zero: ZeroReading;
|
|
233
|
+
}
|
|
196
234
|
| { kind: "date"; form: DateForm };
|
|
197
235
|
|
|
198
236
|
export interface Cell {
|
|
@@ -367,11 +405,15 @@ export interface TableDetail {
|
|
|
367
405
|
export interface Group {
|
|
368
406
|
name: string;
|
|
369
407
|
by: `=${string}`;
|
|
370
|
-
/** Start every instance of this group on a new page (paginated targets). */
|
|
371
|
-
break?: "page";
|
|
372
408
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
409
|
+
* Where this group turns a page (paginated targets). Every boundary between
|
|
410
|
+
* consecutive instances turns; `before` adds the run's leading edge, `after`
|
|
411
|
+
* its trailing edge, and `around` both.
|
|
412
|
+
*/
|
|
413
|
+
break?: "before" | "between" | "after" | "around";
|
|
414
|
+
/**
|
|
415
|
+
* Restart `page.number` / `page.total` at every instance. It turns no page
|
|
416
|
+
* of its own, so it requires `break` to be `"before"` or `"around"`.
|
|
375
417
|
*/
|
|
376
418
|
reset?: "page";
|
|
377
419
|
/**
|
|
@@ -664,6 +706,8 @@ export interface ImageEvent {
|
|
|
664
706
|
export interface SplitStartEvent {
|
|
665
707
|
type: "split-start";
|
|
666
708
|
role: ItemRole;
|
|
709
|
+
/** The split definition's schema path. */
|
|
710
|
+
path: string;
|
|
667
711
|
/** One entry per slot, in order; `width` is absent on a width-less slot. */
|
|
668
712
|
slots: { width?: number }[];
|
|
669
713
|
style?: Record<string, unknown>;
|
|
@@ -673,6 +717,15 @@ export interface SplitEndEvent {
|
|
|
673
717
|
type: "split-end";
|
|
674
718
|
}
|
|
675
719
|
|
|
720
|
+
/**
|
|
721
|
+
* A split bracket folded whole by `splits()`: the opening event's fields, and
|
|
722
|
+
* the slot events in slot order under `items`. No stream emits it.
|
|
723
|
+
*/
|
|
724
|
+
export interface SplitEvent extends Omit<SplitStartEvent, "type"> {
|
|
725
|
+
type: "split";
|
|
726
|
+
items: (ItemEvent | ImageEvent)[];
|
|
727
|
+
}
|
|
728
|
+
|
|
676
729
|
export interface GroupStartEvent {
|
|
677
730
|
type: "group-start";
|
|
678
731
|
name: string;
|
|
@@ -681,17 +734,29 @@ export interface GroupStartEvent {
|
|
|
681
734
|
depth: number;
|
|
682
735
|
key: unknown;
|
|
683
736
|
aggregates: Record<string, unknown>;
|
|
737
|
+
/** The instance's navigation name, resolved from the group's `label` and
|
|
738
|
+
* joined to text. Absent where the group declares none, so a target with a
|
|
739
|
+
* name it derives keeps deriving it. */
|
|
740
|
+
label?: string;
|
|
741
|
+
/** A page turns before this instance, resolved from the group's `break`. */
|
|
684
742
|
break?: "page";
|
|
743
|
+
/** A page turns after this instance, resolved from the group's `break`. */
|
|
744
|
+
breakAfter?: "page";
|
|
685
745
|
/** Restart `page.number` / `page.total` at every instance (paginated targets). */
|
|
686
746
|
reset?: "page";
|
|
687
747
|
/** The declared page column count, present when the group declares one. */
|
|
688
748
|
columns?: number;
|
|
749
|
+
/** The instance opens shut, present only when the group declares it: its
|
|
750
|
+
* bands are shown and the content between them is not. */
|
|
751
|
+
collapsed?: true;
|
|
689
752
|
}
|
|
690
753
|
|
|
691
754
|
export interface GroupEndEvent {
|
|
692
755
|
type: "group-end";
|
|
693
756
|
name: string;
|
|
694
757
|
depth: number;
|
|
758
|
+
/** Present when the instance is hollow: nothing but group brackets arrived between its ends. */
|
|
759
|
+
hollow?: true;
|
|
695
760
|
}
|
|
696
761
|
|
|
697
762
|
export interface TableStartEvent {
|
|
@@ -766,9 +831,33 @@ export interface ReportEventStream {
|
|
|
766
831
|
* ignores that event.
|
|
767
832
|
*/
|
|
768
833
|
export type WalkHandlers = {
|
|
769
|
-
[K in
|
|
834
|
+
[K in WalkEvent["type"]]?: (event: Extract<WalkEvent, { type: K }>) => void;
|
|
770
835
|
};
|
|
771
836
|
|
|
837
|
+
/** What the walk driver dispatches: a stream event, or a split `splits()` folded. */
|
|
838
|
+
export type WalkEvent = ReportEvent | SplitEvent;
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* The split bracket folded: every `split-start`, its slot events and its
|
|
842
|
+
* `split-end` become one `split` event, and every other event passes through
|
|
843
|
+
* untouched, lazily. Read `splits(stream)` where a split is wanted whole rather
|
|
844
|
+
* than keeping a bracket of your own; register a `split` handler and no
|
|
845
|
+
* `split-start` or `split-end` one.
|
|
846
|
+
*/
|
|
847
|
+
export function splits(events: Iterable<ReportEvent>): Generator<WalkEvent, void>;
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Each [collapsed](../../../SCHEMA.md#group-definition) instance shut: its
|
|
851
|
+
* brackets and its own header and footer items survive, and everything
|
|
852
|
+
* between them is dropped, nested instances whole.
|
|
853
|
+
*
|
|
854
|
+
* For a target with no way to hide a row and keep it. One that has — the
|
|
855
|
+
* worksheet — reads the stream as it is. Aggregates are untouched either
|
|
856
|
+
* way. Read it over `splits(stream)`, so a split inside a header arrives
|
|
857
|
+
* folded and wearing the role this reads.
|
|
858
|
+
*/
|
|
859
|
+
export function collapse(events: Iterable<WalkEvent>): Generator<WalkEvent, void>;
|
|
860
|
+
|
|
772
861
|
/**
|
|
773
862
|
* The walk driver: dispatch one render's event stream to per-event handlers,
|
|
774
863
|
* in stream order and exactly once each, pulling lazily and handing the loop
|
|
@@ -778,7 +867,7 @@ export type WalkHandlers = {
|
|
|
778
867
|
* than pulling the stream itself; no opening event is required, and a stream
|
|
779
868
|
* that starts part-way through walks like any other.
|
|
780
869
|
*/
|
|
781
|
-
export function walk(events: Iterable<
|
|
870
|
+
export function walk(events: Iterable<WalkEvent>, handlers: WalkHandlers): Promise<void>;
|
|
782
871
|
|
|
783
872
|
/**
|
|
784
873
|
* Hand the event loop back to the host, resolving once it has had its turn.
|
|
@@ -865,6 +954,27 @@ export interface Warning {
|
|
|
865
954
|
* complement of `names`, which excludes them. `problems` is fatal and
|
|
866
955
|
* `warnings` is advisory, both in document order from the one descent.
|
|
867
956
|
*/
|
|
957
|
+
/** A target's `capabilities` descriptor, as each published target exports it. */
|
|
958
|
+
export interface TargetCapabilities {
|
|
959
|
+
readonly target: string;
|
|
960
|
+
readonly declarations: Readonly<Record<string, string>>;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** What a caller asks `plan` to check beyond the document itself. */
|
|
964
|
+
export interface PlanOptions {
|
|
965
|
+
/**
|
|
966
|
+
* The `capabilities` descriptors of the targets this document is meant for.
|
|
967
|
+
* Every declaration the document marks `required` that one of them
|
|
968
|
+
* withdraws or leaves unread becomes a problem of its own, which nulls the
|
|
969
|
+
* report exactly as any other problem does. Omit them and nothing is
|
|
970
|
+
* checked, which is what makes the marking additive.
|
|
971
|
+
*
|
|
972
|
+
* Descriptors rather than targets: the engine knows about no target, and
|
|
973
|
+
* what it reads here is the data a target ships about itself.
|
|
974
|
+
*/
|
|
975
|
+
readonly targets?: readonly TargetCapabilities[];
|
|
976
|
+
}
|
|
977
|
+
|
|
868
978
|
export interface Plan {
|
|
869
979
|
readonly report: CompiledReport | null;
|
|
870
980
|
readonly problems: readonly Problem[];
|
|
@@ -885,7 +995,7 @@ export interface Quario {
|
|
|
885
995
|
* problems, advisory warnings, and per-node anchor sets — one descent per
|
|
886
996
|
* edit for a host that validates and renders in a loop.
|
|
887
997
|
*/
|
|
888
|
-
plan(schema: unknown, functions?: FunctionRegistry): Plan;
|
|
998
|
+
plan(schema: unknown, functions?: FunctionRegistry, options?: PlanOptions): Plan;
|
|
889
999
|
}
|
|
890
1000
|
|
|
891
1001
|
/**
|
|
@@ -1037,6 +1147,14 @@ export function isReportBand(role: string | undefined): boolean;
|
|
|
1037
1147
|
*/
|
|
1038
1148
|
export const STYLE_NAMES: readonly string[];
|
|
1039
1149
|
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether a style name belongs to the box: a padding or a border part. The
|
|
1152
|
+
* partition is the spec's (a row's box is defined by it), and this is its one
|
|
1153
|
+
* home. A target that strips the box off a style, or a tool that lists the box
|
|
1154
|
+
* names, asks this rather than matching the prefixes itself.
|
|
1155
|
+
*/
|
|
1156
|
+
export function isBoxName(name: string): boolean;
|
|
1157
|
+
|
|
1040
1158
|
/**
|
|
1041
1159
|
* The `format` vocabulary as data, for a tool that offers it rather than
|
|
1042
1160
|
* checks it — a style rail's kind picker and its modifier control — so no
|
|
@@ -1045,9 +1163,14 @@ export const STYLE_NAMES: readonly string[];
|
|
|
1045
1163
|
export const FORMAT_VOCABULARY: {
|
|
1046
1164
|
readonly kinds: readonly FormatKind[];
|
|
1047
1165
|
readonly forms: readonly DateForm[];
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1166
|
+
readonly negatives: readonly NegativeReading[];
|
|
1167
|
+
readonly zeroes: readonly ZeroReading[];
|
|
1168
|
+
/** Which modifiers each kind takes, in the order a surface offers them, so
|
|
1169
|
+
* one offering them need not know that three kinds share theirs and `date`
|
|
1170
|
+
* does not. */
|
|
1171
|
+
readonly modifiers: Readonly<Record<FormatKind, readonly string[]>>;
|
|
1051
1172
|
readonly defaultForm: DateForm;
|
|
1173
|
+
readonly defaultNegative: NegativeReading;
|
|
1174
|
+
readonly defaultZero: ZeroReading;
|
|
1052
1175
|
readonly maxDigits: number;
|
|
1053
1176
|
};
|