quario 0.9.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 +43 -0
- package/README.md +22 -2
- package/lib/band-height.js +11 -16
- package/lib/format.js +15 -23
- package/lib/index.d.ts +38 -6
- package/lib/index.js +1 -1
- package/lib/license.js +1 -1
- 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,48 @@
|
|
|
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
|
+
|
|
3
46
|
## 0.9.0
|
|
4
47
|
|
|
5
48
|
### 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
|
|
@@ -261,7 +281,7 @@ carry `code`, `limit`, and `actual`. Every render starts with fresh counters.
|
|
|
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
|
|
284
|
+
Markdown target lives in the repository at `example/markdown.js` in under 60 lines of code. The built-in
|
|
265
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
|
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/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.
|
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,
|
package/lib/license.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// though nothing reassigns them. A key is valid for every version released inside its window
|
|
12
12
|
// (LICENSE section 7), so validity compares ISO date strings and never reads
|
|
13
13
|
// a clock — accepted output stays accepted.
|
|
14
|
-
let RELEASE = "2026-09-
|
|
14
|
+
let RELEASE = "2026-09-19"; // release-date
|
|
15
15
|
// The verifying half of the signing pair: the 65-byte uncompressed P-256
|
|
16
16
|
// point, base64url. The private half never enters the repo;
|
|
17
17
|
// scripts/license/sign.mjs mints keys against it.
|
package/lib/locate.js
CHANGED
|
@@ -41,14 +41,12 @@ export let fault = (path, msg) => {
|
|
|
41
41
|
* An image failure a **target** raises, named on the item that asked for the
|
|
42
42
|
* bytes. The engine vouches for an image's magic numbers and no further
|
|
43
43
|
* (SCHEMA.md, "Image item"), so every failure past that point belongs to
|
|
44
|
-
* whichever target was sizing or embedding the file
|
|
45
|
-
*
|
|
46
|
-
* pdf-lib's bare "Invalid typed array length: 0" (quario-e0bz).
|
|
44
|
+
* whichever target was sizing or embedding the file, and one wording serves
|
|
45
|
+
* all of them.
|
|
47
46
|
*
|
|
48
|
-
* Here rather than in a target because three of them raise it and no two
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* it from a `catch` or a rejection in whichever style its own code is.
|
|
47
|
+
* Here rather than in a target because three of them raise it and no two may
|
|
48
|
+
* depend on each other. Minted rather than thrown, unlike `fault` beside it,
|
|
49
|
+
* so a caller can raise it from a `catch` or a rejection.
|
|
52
50
|
*
|
|
53
51
|
* The shape is the one every render error has: the path prefixes the message
|
|
54
52
|
* and the failing class survives behind it. It carries no `LOCATION` and is no
|
package/lib/math.js
CHANGED
|
@@ -7,37 +7,28 @@
|
|
|
7
7
|
* error.
|
|
8
8
|
*
|
|
9
9
|
* `round`, `floor` and `ceil` round the DECIMAL the author wrote, not the
|
|
10
|
-
* binary double it is stored as, and `Intl.NumberFormat` is how they reach
|
|
11
|
-
* its rounding starts from the shortest decimal that round-trips, so
|
|
12
|
-
* `round(2.675, 2)` is 2.68 where `Math.round(2.675 * 100) / 100`
|
|
13
|
-
* `Number((2.675).toFixed(2))`
|
|
14
|
-
*
|
|
15
|
-
* table, is in docs/adr/0050. The locale is hardcoded `en-US` rather than the
|
|
16
|
-
* instance's: this is arithmetic, not presentation, and a locale with
|
|
10
|
+
* binary double it is stored as, and `Intl.NumberFormat` is how they reach
|
|
11
|
+
* it: its rounding starts from the shortest decimal that round-trips, so
|
|
12
|
+
* `round(2.675, 2)` is 2.68 where both `Math.round(2.675 * 100) / 100` and
|
|
13
|
+
* `Number((2.675).toFixed(2))` are 2.67 (docs/adr/0050). The locale is
|
|
14
|
+
* hardcoded `en-US`: this is arithmetic, not presentation, and a locale with
|
|
17
15
|
* non-ASCII digits or its own grouping would not parse back through `Number`.
|
|
18
16
|
*/
|
|
19
17
|
import { boundedMemo } from "./memo.js";
|
|
20
18
|
|
|
21
19
|
// The formatters are memoised behind `./memo.js`, the same seam `format()`
|
|
22
|
-
// uses
|
|
23
|
-
// cache and records why the decline expired; what is here is what only this
|
|
24
|
-
// call site knows, its key and its cap.
|
|
20
|
+
// uses — one bounded store each, not one between them (docs/adr/0050).
|
|
25
21
|
//
|
|
26
22
|
// **The key is the mode and the count**, which is all that varies: the locale
|
|
27
|
-
// and `useGrouping` are constants here
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// same way. Drop either piece and a `floor` presents a `round`'s answer, or a
|
|
31
|
-
// three-digit call presents two -- pinned a piece at a time in
|
|
23
|
+
// and `useGrouping` are constants here, precisely so the answer parses back
|
|
24
|
+
// through `Number`. Drop either piece and a `floor` presents a `round`'s
|
|
25
|
+
// answer, or a three-digit call presents two — pinned a piece at a time in
|
|
32
26
|
// `test/frozen.test.js`.
|
|
33
27
|
//
|
|
34
28
|
// **The cap is unreachable, and that is the point of stating it.** `places`
|
|
35
29
|
// admits 0 to 100 and there are three modes, so this store holds at most 303
|
|
36
|
-
// entries however hostile the data is
|
|
37
|
-
//
|
|
38
|
-
// threshold: if the admitted range ever widened, the store would empty and
|
|
39
|
-
// rebuild rather than grow without limit, which is the same degradation
|
|
40
|
-
// `format()`'s cap buys and no correctness knob either way.
|
|
30
|
+
// entries however hostile the data is. 512 is headroom, so a widened range
|
|
31
|
+
// would empty and rebuild the store rather than grow it without limit.
|
|
41
32
|
let memo = boundedMemo(512);
|
|
42
33
|
|
|
43
34
|
/** @type {(x: number, n: number, mode: string) => number} */
|
package/lib/plan.js
CHANGED
|
@@ -154,6 +154,23 @@ let TEXT_SLOT_KEYS = [...TEXT_KEYS, "width"];
|
|
|
154
154
|
let IMAGE_KEYS = ["type", "source", "fit", "alt", "visible", "style"];
|
|
155
155
|
let IMAGE_SLOT_KEYS = [...IMAGE_KEYS, "width"];
|
|
156
156
|
|
|
157
|
+
// A group's two structural literals. `break` names a position rather than a
|
|
158
|
+
// unit (ADR 0077), and this table is the whole of what its four values mean:
|
|
159
|
+
// every boundary `between` consecutive instances turns a page, `lead` adds the
|
|
160
|
+
// run's own leading edge, and `trail` its trailing one. One row says everything
|
|
161
|
+
// about one value, so a reader checks a value by reading it rather than by
|
|
162
|
+
// intersecting sets. `reset` names a page-number sequence and turns nothing.
|
|
163
|
+
let EDGES = new Map([
|
|
164
|
+
["before", { between: "page", lead: "page", trail: null }],
|
|
165
|
+
["between", { between: "page", lead: null, trail: null }],
|
|
166
|
+
["after", { between: "page", lead: null, trail: "page" }],
|
|
167
|
+
["around", { between: "page", lead: "page", trail: "page" }],
|
|
168
|
+
]);
|
|
169
|
+
// The `break` values that leave the run's leading edge alone, and so cannot
|
|
170
|
+
// carry a `reset`. Read off the table, so it cannot drift from it.
|
|
171
|
+
let UNLED = new Set([...EDGES].filter(([, edges]) => !edges.lead).map(([value]) => value));
|
|
172
|
+
let RESETS = new Set(["page"]);
|
|
173
|
+
|
|
157
174
|
// Which detail events carry the row's running values: the ones holding a cell.
|
|
158
175
|
// A split's bracket holds none, so run rides the item and image events inside
|
|
159
176
|
// it rather than the bracket around them.
|
|
@@ -236,21 +253,15 @@ let formatsMoney = (block) => {
|
|
|
236
253
|
};
|
|
237
254
|
|
|
238
255
|
// One pass over a report's constants: it checks each value, copies it, and
|
|
239
|
-
// freezes the copy. Copying
|
|
240
|
-
//
|
|
241
|
-
//
|
|
256
|
+
// freezes the copy. Copying keeps a consumer from reaching back into the
|
|
257
|
+
// compiled schema through a nested object; freezing stops them mutating what
|
|
258
|
+
// they were handed.
|
|
242
259
|
//
|
|
243
|
-
// The vocabulary is JSON's, not structured clone's, because
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// only ever serve a schema built in JS -- and it is what forced the caveat
|
|
247
|
-
// about the values freezing cannot lock. Rejecting them makes the freeze
|
|
248
|
-
// total. NaN and Infinity go with them: JSON cannot hold either, and
|
|
260
|
+
// The vocabulary is JSON's, not structured clone's, because a report document
|
|
261
|
+
// is JSON (SCHEMA.md, "Deliberate asymmetries"): rejecting a Date, a Map or a
|
|
262
|
+
// typed array makes the freeze total. NaN and Infinity go with them —
|
|
249
263
|
// coercing them to null would turn a definition error into a wrong value
|
|
250
|
-
// three bands away.
|
|
251
|
-
//
|
|
252
|
-
// `seen` rejects cycles rather than surviving them, for the same reason: a
|
|
253
|
-
// JSON document has no way to write one.
|
|
264
|
+
// three bands away — and `seen` rejects cycles, which JSON cannot write.
|
|
254
265
|
/** @type {(value: any) => boolean} */
|
|
255
266
|
let isJsonAtom = (value) =>
|
|
256
267
|
value === null || typeof value === "boolean" || typeof value === "string";
|
|
@@ -485,17 +496,15 @@ let runProp = (expression, path, source) => (scope) => {
|
|
|
485
496
|
};
|
|
486
497
|
// The [live field](../../../CONTEXT.md#live-field): which page value a token
|
|
487
498
|
// *is*, for a target whose own format numbers pages. sjabloon offers each
|
|
488
|
-
// interpolation's expression source once
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// ruled out because xprsn's `==` is strict: it would break `=page.number == 1`.
|
|
499
|
+
// interpolation's expression source once while compiling, the only place the
|
|
500
|
+
// author's words still exist. Reading the source here instead is what hard
|
|
501
|
+
// constraint 2 forbids, and a sentinel bound to `page.number` was ruled out
|
|
502
|
+
// because xprsn's `==` is strict: it would break `=page.number == 1`.
|
|
493
503
|
//
|
|
494
|
-
// One table for the whole compile: the answer depends on the expression
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// frozen because every cell that says it shares the one object.
|
|
504
|
+
// One table for the whole compile: the answer depends on the expression
|
|
505
|
+
// alone. A Map rather than a lookup object, because the key is author source
|
|
506
|
+
// and nothing reached through `Object.prototype` answers this. Frozen,
|
|
507
|
+
// because every cell that says a name shares the one object.
|
|
499
508
|
const PAGE_FIELDS = /** @type {Map<string, { field: import("./index.js").PageField }>} */ (
|
|
500
509
|
new Map(["page.number", "page.total"].map((field) => [field, Object.freeze({ field })]))
|
|
501
510
|
);
|
|
@@ -558,13 +567,10 @@ let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
|
558
567
|
// rendered over quario's own scope chain, which already binds the anchors:
|
|
559
568
|
// `$` at the render base, `@` on the detail row, neither anywhere else — so
|
|
560
569
|
// a group or report band leaves `@` unbound and a stray `@.field` throws.
|
|
561
|
-
// sjabloon's `bound` excludes its loop vars
|
|
562
|
-
// names
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
// located SJABLOON_RAW_TAG definition error — so escaping is entirely a
|
|
566
|
-
// render-target concern at the markup edge, with no exceptions to carry
|
|
567
|
-
// through the stream.
|
|
570
|
+
// sjabloon's `bound` excludes its loop vars, `$`, `@` and quario's engine
|
|
571
|
+
// names from the reported `names`. Templates have no raw form — a
|
|
572
|
+
// `{{{ }}}` tag is a located SJABLOON_RAW_TAG definition error — so
|
|
573
|
+
// escaping is entirely a render target's concern at the markup edge.
|
|
568
574
|
/** @type {(str: any, path: string) => (scope: Scope) => any} */
|
|
569
575
|
let cell = (source, path) => {
|
|
570
576
|
let tokens = compileCell(asText(source), path, source, FNS, BND);
|
|
@@ -692,12 +698,10 @@ let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any
|
|
|
692
698
|
// warning rather than a definition error because the kind is a *later* edit
|
|
693
699
|
// away and an editor session must never refuse a document mid-edit.
|
|
694
700
|
//
|
|
695
|
-
// The `currency` is read off `entries
|
|
696
|
-
// vocabulary already refused earns no second entry
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
// not have either -- the code is unread all the same, and saying so is not a
|
|
700
|
-
// second entry about the same key.
|
|
701
|
+
// The `currency` is read off `entries`, exactly what survived `check`, so
|
|
702
|
+
// one the vocabulary already refused earns no second entry. The `format`
|
|
703
|
+
// beside it is read as written: a refused `format` is a kind the engine
|
|
704
|
+
// will not have either, so the code is unread all the same.
|
|
701
705
|
/** @type {(block: any, path: string, entries: StyleEntry[]) => void} */
|
|
702
706
|
let unreadCurrency = (block, path, entries) => {
|
|
703
707
|
if (!entries.some(([name]) => name === "currency")) return;
|
|
@@ -739,13 +743,11 @@ let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any
|
|
|
739
743
|
// a guarded name that answers is written where it was declared rather than
|
|
740
744
|
// after the ones that never needed guarding.
|
|
741
745
|
//
|
|
742
|
-
// `format` resolves after the loop
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
// block. The instance's options come with it, for the cell that declares no
|
|
748
|
-
// code of its own and takes the instance's (docs/adr/0056).
|
|
746
|
+
// `format` resolves after the loop, not inside it: a `currency` kind takes
|
|
747
|
+
// its digit count from the code the cell wears, so the code must have been
|
|
748
|
+
// written — or voided by `guarded` — first. Per entry would make one
|
|
749
|
+
// declaration depend on where another sits in the block. The instance's
|
|
750
|
+
// options come with it, for a cell declaring no code (docs/adr/0056).
|
|
749
751
|
/** @type {(entries: StyleEntry[]) => (scope: Scope) => Scope} */
|
|
750
752
|
let blockFn = (entries) => (scope) => {
|
|
751
753
|
/** @type {Scope} */
|
|
@@ -912,16 +914,13 @@ let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any
|
|
|
912
914
|
// settle it. That is the whole of why the warning below is incomplete by
|
|
913
915
|
// construction (SCHEMA.md, "Validation").
|
|
914
916
|
//
|
|
915
|
-
// The two delimiters are the one place quario names sjabloon's syntax
|
|
916
|
-
// than asking for it
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// standalone tag -- so a wrong answer costs a missing warning, never a
|
|
923
|
-
// warning on a correct document. A value that is not one template is not this
|
|
924
|
-
// question at all: a run array's runs each answer it for themselves.
|
|
917
|
+
// The two delimiters are the one place quario names sjabloon's syntax
|
|
918
|
+
// rather than asking for it (hard constraint 2). It is a *shape* test, not
|
|
919
|
+
// a parse: nothing is tokenised or evaluated, and the answer decides an
|
|
920
|
+
// advisory alone. It has no false positives — text outside every tag cannot
|
|
921
|
+
// be conditional, a block opening with `{{#`, and sjabloon strips no
|
|
922
|
+
// whitespace around a standalone tag — so a wrong answer costs a missing
|
|
923
|
+
// warning, never a warning on a correct document.
|
|
925
924
|
/** @type {(src: any) => boolean} */
|
|
926
925
|
let neverPresents = (src) =>
|
|
927
926
|
typeof src === "string" && !(src.startsWith("{{") && src.endsWith("}}"));
|
|
@@ -1582,17 +1581,16 @@ let bands = (
|
|
|
1582
1581
|
keys(value.header, ["style"], "detail.header");
|
|
1583
1582
|
return rowStylesOf(value.header, "detail.header");
|
|
1584
1583
|
};
|
|
1585
|
-
// Which slot each column's header sits in
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
// column and gets its own diagnostic when the column is parsed.
|
|
1584
|
+
// Which slot each column's header sits in, walked positionally rather than
|
|
1585
|
+
// summed because the diagnostic has to name the column it broke at. Read
|
|
1586
|
+
// off the raw definitions, before any column is parsed, so `columnOf` can
|
|
1587
|
+
// report its verdict in the column's own turn. An unreadable span covers a
|
|
1588
|
+
// single column and gets its own diagnostic there.
|
|
1591
1589
|
//
|
|
1592
|
-
// `own` fills its own slot, `covered` is filled by a neighbour's span,
|
|
1593
|
-
// reaches past the last column, and everything after an `over` is
|
|
1594
|
-
// `unreached`
|
|
1595
|
-
//
|
|
1590
|
+
// `own` fills its own slot, `covered` is filled by a neighbour's span,
|
|
1591
|
+
// `over` reaches past the last column, and everything after an `over` is
|
|
1592
|
+
// `unreached` — one broken span should not report every column behind it as
|
|
1593
|
+
// headerless.
|
|
1596
1594
|
// How many columns a definition's header claims, read leniently: a malformed
|
|
1597
1595
|
// column, a missing header and an unreadable span all claim one here, and
|
|
1598
1596
|
// each gets its own diagnostic when the column is parsed.
|
|
@@ -1799,11 +1797,25 @@ let bands = (
|
|
|
1799
1797
|
if (groupNames.has(name)) bad(path, 'duplicate group name "' + name + '"');
|
|
1800
1798
|
else groupNames.add(name);
|
|
1801
1799
|
};
|
|
1802
|
-
/** @type {(value: any, path: string, kind: string
|
|
1803
|
-
|
|
1804
|
-
|
|
1800
|
+
/** @type {(value: any, path: string, kind: string,
|
|
1801
|
+
* known: { has: (value: string) => boolean }) => void} */
|
|
1802
|
+
let checkHint = (value, path, kind, known) => {
|
|
1803
|
+
if (value == null || known.has(value)) return;
|
|
1805
1804
|
bad(path, "unknown " + kind + ' "' + value + '"');
|
|
1806
1805
|
};
|
|
1806
|
+
// A group's two structural literals, and the one rule tying them together.
|
|
1807
|
+
// A page-number sequence owns whole pages, so it needs a leading boundary
|
|
1808
|
+
// `break` may not supply (ADR 0077). That is read off the pair, and only for
|
|
1809
|
+
// the values this compile understood: an unknown `break` already has its own
|
|
1810
|
+
// problem, and a second about what it fails to supply would be that one
|
|
1811
|
+
// twice over.
|
|
1812
|
+
/** @type {(def: any, path: string) => void} */
|
|
1813
|
+
let checkHints = (def, path) => {
|
|
1814
|
+
checkHint(def.break, path + ".break", "break", EDGES);
|
|
1815
|
+
checkHint(def.reset, path + ".reset", "reset", RESETS);
|
|
1816
|
+
if (def.reset === "page" && (def.break == null || UNLED.has(def.break)))
|
|
1817
|
+
bad(path + ".reset", 'reset needs break "before" or "around"');
|
|
1818
|
+
};
|
|
1807
1819
|
/** @type {(aggs: [string, Fold][], rows: any[], scope: Scope, handle: Scope) => Scope} */
|
|
1808
1820
|
let applyAggs = (aggs, rows, scope, handle) => {
|
|
1809
1821
|
/** @type {Scope} */
|
|
@@ -1840,10 +1852,38 @@ let bands = (
|
|
|
1840
1852
|
rows,
|
|
1841
1853
|
};
|
|
1842
1854
|
};
|
|
1855
|
+
// One instance's bands, in order, as the events they yield.
|
|
1856
|
+
/** @type {(ctx: any, groupScope: any, ordered: any[], runners: any) => Generator<any>} */
|
|
1857
|
+
let instanceBands = function* (ctx, groupScope, ordered, runners) {
|
|
1858
|
+
yield* ctx.header(groupScope);
|
|
1859
|
+
yield* ctx.inner(ordered, groupScope, runners.extend(ctx.running));
|
|
1860
|
+
yield* ctx.footer(groupScope);
|
|
1861
|
+
};
|
|
1862
|
+
// Pass an instance's events through, and answer whether any of them
|
|
1863
|
+
// occupied the document. Only a group bracket can arrive without something
|
|
1864
|
+
// being there, so an instance holding nothing but hollow instances is hollow
|
|
1865
|
+
// itself, and one holding a child that drew is not.
|
|
1866
|
+
/** @type {(events: Iterable<any>) => Generator<any, boolean>} */
|
|
1867
|
+
let occupied = function* (events) {
|
|
1868
|
+
let drew = false;
|
|
1869
|
+
for (let event of events) {
|
|
1870
|
+
drew ||= event.type !== "group-start" && event.type !== "group-end";
|
|
1871
|
+
yield event;
|
|
1872
|
+
}
|
|
1873
|
+
return drew;
|
|
1874
|
+
};
|
|
1843
1875
|
/** @type {(ctx: any) => Band} */
|
|
1844
1876
|
let groupWalk = (ctx) =>
|
|
1845
1877
|
function* (rows, scope, runners) {
|
|
1846
1878
|
let parts = Map.groupBy(rows, (row) => ctx.by(withRow(scope, row)));
|
|
1879
|
+
// The four `break` values resolved to `ctx.edges` at compile, so a
|
|
1880
|
+
// target reads boundaries rather than positions: every boundary between
|
|
1881
|
+
// consecutive instances is the later instance's leading edge, and the
|
|
1882
|
+
// run's own two edges fall on its first and its last. The run is this
|
|
1883
|
+
// call's partition, so a nested group's edges belong to the parent
|
|
1884
|
+
// instance holding it rather than to the document.
|
|
1885
|
+
let last = parts.size - 1;
|
|
1886
|
+
let index = 0;
|
|
1847
1887
|
for (let [key, groupRows] of parts) {
|
|
1848
1888
|
// A group band has no current row, so `@` stays unbound here.
|
|
1849
1889
|
let { groupScope, aggregates, rows: ordered } = openInstance(ctx, key, groupRows, scope);
|
|
@@ -1856,12 +1896,18 @@ let bands = (
|
|
|
1856
1896
|
aggregates,
|
|
1857
1897
|
path: ctx.path,
|
|
1858
1898
|
},
|
|
1859
|
-
{
|
|
1899
|
+
{
|
|
1900
|
+
break: index > 0 ? ctx.edges.between : ctx.edges.lead,
|
|
1901
|
+
breakAfter: index === last ? ctx.edges.trail : null,
|
|
1902
|
+
reset: ctx.reset,
|
|
1903
|
+
columns: ctx.columns,
|
|
1904
|
+
},
|
|
1860
1905
|
);
|
|
1861
|
-
yield* ctx
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
yield { type: "group-end", name: ctx.name, depth: ctx.index };
|
|
1906
|
+
let drew = yield* occupied(instanceBands(ctx, groupScope, ordered, runners));
|
|
1907
|
+
// The closing bracket says whether the instance is hollow, so no
|
|
1908
|
+
// target rediscovers it from the stream (docs/adr/0060, as amended).
|
|
1909
|
+
yield opt({ type: "group-end", name: ctx.name, depth: ctx.index }, { hollow: !drew });
|
|
1910
|
+
index++;
|
|
1865
1911
|
}
|
|
1866
1912
|
};
|
|
1867
1913
|
/** @type {(def: any, path: string, defs: any[], index: number, detail: any, columned?: boolean) => Band} */
|
|
@@ -1886,8 +1932,7 @@ let bands = (
|
|
|
1886
1932
|
named(def.name, path + ".name", RESERVED);
|
|
1887
1933
|
noteGroup(def.name, path + ".name");
|
|
1888
1934
|
let by = expression(def.by, path + ".by");
|
|
1889
|
-
|
|
1890
|
-
checkHint(def.reset, path + ".reset", "reset");
|
|
1935
|
+
checkHints(def, path);
|
|
1891
1936
|
let columns = columnsOf(def.columns, path + ".columns", columned);
|
|
1892
1937
|
return groupWalk({
|
|
1893
1938
|
by,
|
|
@@ -1900,7 +1945,7 @@ let bands = (
|
|
|
1900
1945
|
inner: bandOf(defs, index + 1, detail, columned || columns != null),
|
|
1901
1946
|
name: def.name,
|
|
1902
1947
|
path,
|
|
1903
|
-
|
|
1948
|
+
edges: EDGES.get(def.break) ?? NONE,
|
|
1904
1949
|
reset: def.reset,
|
|
1905
1950
|
columns,
|
|
1906
1951
|
index,
|
package/lib/reducers.js
CHANGED
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
* form `sum(@.lines, l => l.total)` and a declared aggregate alike — a step
|
|
6
6
|
* folded across rows in render order for a declared running value, and a
|
|
7
7
|
* spelling rule, since `count` is the one declared form that takes no
|
|
8
|
-
* expression.
|
|
9
|
-
* derived name set, and a reducer named in the validator, so adding a reducer
|
|
10
|
-
* was three edits and forgetting the runner passed validation and threw a raw
|
|
11
|
-
* TypeError at render. One entry per reducer; every role derives from it.
|
|
8
|
+
* expression. One entry per reducer; every role derives from it.
|
|
12
9
|
*
|
|
13
10
|
* `prev` is the entry with no fold. There is no whole-array "previous", so it
|
|
14
|
-
* is a running value only
|
|
15
|
-
*
|
|
11
|
+
* is a running value only, and that missing half is what keeps it out of an
|
|
12
|
+
* `aggregates` block.
|
|
16
13
|
*
|
|
17
14
|
* Coercion is quiet: only a finite number contributes, everything else folds
|
|
18
15
|
* to 0, as the spec promises.
|
|
@@ -32,12 +29,22 @@ let num = (value) => {
|
|
|
32
29
|
// why `countDistinct` reads its lambda this way despite the shared prefix.
|
|
33
30
|
/** @type {(rows: any[] | null | undefined, of?: ((row: any) => any) | null) => any[]} */
|
|
34
31
|
let project = (rows, of) => (rows || []).map((row) => (of ? of(row) : row));
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
// The shape every reducer with no zero shares: project through the lambda,
|
|
33
|
+
// then answer from a nonempty array. An empty one has no answer, so it is
|
|
34
|
+
// `null` -- and an inline caller can tell, because a call is not the member
|
|
35
|
+
// read that normalizes an absent value.
|
|
36
|
+
/** @type {(from: (values: any[]) => any) => Reducer} */
|
|
37
|
+
let folded = (from) => (rows, of) => {
|
|
38
38
|
let values = project(rows, of);
|
|
39
|
-
return values.length ? values
|
|
39
|
+
return values.length ? from(values) : null;
|
|
40
40
|
};
|
|
41
|
+
// `min`/`max` pick a winner out of the projection. `first`/`last` take an end
|
|
42
|
+
// of it instead, which makes them the only folds that read the array's order
|
|
43
|
+
// rather than only its contents -- so the order a scope hands them is
|
|
44
|
+
// author-visible where no other reducer's is. SCHEMA.md, "Aggregates", says
|
|
45
|
+
// which order each scope hands.
|
|
46
|
+
/** @type {(pick: (a: any, b: any) => any) => Reducer} */
|
|
47
|
+
let extreme = (pick) => folded((values) => values.reduce(pick));
|
|
41
48
|
/** @type {Reducer} */
|
|
42
49
|
let sum = (rows, of) => (rows || []).reduce((total, row) => total + num(of ? of(row) : row), 0);
|
|
43
50
|
|
|
@@ -45,9 +52,10 @@ let sum = (rows, of) => (rows || []).reduce((total, row) => total + num(of ? of(
|
|
|
45
52
|
* The table. Each entry is one reducer entire: how it folds an array, how it
|
|
46
53
|
* steps a row, and whether its declared form carries `:=<expression>`.
|
|
47
54
|
*
|
|
48
|
-
* The running halves fold in render order. `min
|
|
49
|
-
* seed to the first row's value — the
|
|
50
|
-
* xprsn reads absent values as null
|
|
55
|
+
* The running halves fold in render order. `min`, `max` and `first` have no
|
|
56
|
+
* zero, so they seed to the first row's value — the deliberate `undefined`s
|
|
57
|
+
* here, since xprsn reads absent values as null and a runner's step only ever
|
|
58
|
+
* sees what an xprsn expression returned.
|
|
51
59
|
* @type {Record<string, Entry>}
|
|
52
60
|
*/
|
|
53
61
|
let REGISTRY = {
|
|
@@ -104,6 +112,23 @@ let REGISTRY = {
|
|
|
104
112
|
},
|
|
105
113
|
expr: true,
|
|
106
114
|
},
|
|
115
|
+
first: {
|
|
116
|
+
fold: folded((values) => values[0]),
|
|
117
|
+
run: () => {
|
|
118
|
+
let opened = /** @type {any} */ (undefined);
|
|
119
|
+
return (value) => (opened = opened === undefined ? value : opened);
|
|
120
|
+
},
|
|
121
|
+
expr: true,
|
|
122
|
+
},
|
|
123
|
+
last: {
|
|
124
|
+
// The runner is the identity on purpose: the last row seen *is* this row,
|
|
125
|
+
// so `run.last` says what `@` already says. It exists because the run
|
|
126
|
+
// vocabulary is the reducer vocabulary, and SCHEMA.md states it rather
|
|
127
|
+
// than leaving the derivation to surprise an author (ADR 0006).
|
|
128
|
+
fold: folded((values) => values.at(-1)),
|
|
129
|
+
run: () => (value) => value,
|
|
130
|
+
expr: true,
|
|
131
|
+
},
|
|
107
132
|
prev: {
|
|
108
133
|
fold: null,
|
|
109
134
|
run: () => {
|
|
@@ -177,19 +202,16 @@ let aLambda = demands(
|
|
|
177
202
|
// nothing at all.
|
|
178
203
|
//
|
|
179
204
|
// The guard wraps the table's own entries because this is the one place the
|
|
180
|
-
// reducer's name is in hand to say. Only
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// both: an absent read is null, so `sum(@.lines)` on a row with no lines folds
|
|
187
|
-
// the empty array, which is the answer the spec's six give.
|
|
205
|
+
// reducer's name is in hand to say. Only `plan.js` can carry an authored
|
|
206
|
+
// mistake here, since `scope.js` folds rows the engine grouped through a
|
|
207
|
+
// compiled lambda; that dead check is the price of one door instead of two,
|
|
208
|
+
// and it costs a comparison per fold, not per row. Nullish passes both: an
|
|
209
|
+
// absent read is null, so `sum(@.lines)` on a row with no lines folds the
|
|
210
|
+
// empty array.
|
|
188
211
|
//
|
|
189
212
|
// It carries no engine code: the mistake is quario's own verdict on an
|
|
190
|
-
// authored call,
|
|
191
|
-
//
|
|
192
|
-
// a `code` anything could fake.
|
|
213
|
+
// authored call, and SCHEMA.md's "Validation" tells a host to recognise a
|
|
214
|
+
// located error with `isDiagnostic` rather than by probing a `code`.
|
|
193
215
|
/** @type {(fn: string, fold: Reducer) => Reducer} */
|
|
194
216
|
let guarded = (fn, fold) => (rows, of) => {
|
|
195
217
|
anArray(fn, rows);
|
package/lib/stream.js
CHANGED
|
@@ -93,11 +93,9 @@ export let reviveDate = (value) => {
|
|
|
93
93
|
*
|
|
94
94
|
* Passing the cell's resolved `format` declaration opts into the one coercion
|
|
95
95
|
* the seam performs: under the `date` kind, a string in either accepted RFC
|
|
96
|
-
* 3339 form revives to the `Date` it names.
|
|
97
|
-
* the
|
|
98
|
-
*
|
|
99
|
-
* to revive (ADR 0041). Callers that read no style omit the argument and see
|
|
100
|
-
* the seam exactly as it was.
|
|
96
|
+
* 3339 form revives to the `Date` it names. `date` alone, because a date is
|
|
97
|
+
* the one type a JSON document cannot carry (ADR 0041). Callers that read no
|
|
98
|
+
* style omit the argument.
|
|
101
99
|
*
|
|
102
100
|
* @param {any[]} tokens A cell's tokens.
|
|
103
101
|
* @param {unknown} [decl] The cell's resolved `format` declaration — the
|
|
@@ -112,13 +110,11 @@ export function typed(tokens, decl) {
|
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
/**
|
|
115
|
-
* Whether a token list renders exactly one value token
|
|
116
|
-
* gates on, and the same one `format` is read under. A run holding
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* Internal to the package: `typed()` is the public reading of it, and the
|
|
121
|
-
* traversal asks it of the tokens each style block governs.
|
|
113
|
+
* Whether a token list renders exactly one value token — the seam `typed()`
|
|
114
|
+
* gates on, and the same one `format` is read under. A run holding two
|
|
115
|
+
* interpolations presents no single value, so a declaration that presents a
|
|
116
|
+
* value has nothing to speak about (ADR 0061). Internal: `typed()` is the
|
|
117
|
+
* public reading of it.
|
|
122
118
|
*
|
|
123
119
|
* @type {(tokens: any[]) => boolean}
|
|
124
120
|
*/
|
|
@@ -156,13 +152,9 @@ let joins = (last, style) => !!last && sameStyle(last.style, style);
|
|
|
156
152
|
|
|
157
153
|
/**
|
|
158
154
|
* Group a cell's tokens into its styled runs, in order. Consecutive tokens
|
|
159
|
-
* with equal styles are one run —
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* Shipped so the built-in targets and a custom one cannot drift: the stream
|
|
164
|
-
* carries a cell's runs flattened into one `tokens` array with an optional
|
|
165
|
-
* per-token `style`, and this is the whole rule for reading them back
|
|
155
|
+
* with equal styles are one run — lossless, so grouping survives a JSON round
|
|
156
|
+
* trip. Shipped so the built-in targets and a custom one cannot drift: this
|
|
157
|
+
* is the whole rule for reading a cell's flattened `tokens` back
|
|
166
158
|
* (SCHEMA.md, "Event stream"; ADR 0061).
|
|
167
159
|
*
|
|
168
160
|
* @param {any[]} tokens A cell's tokens.
|
|
@@ -194,11 +186,10 @@ let REPORT_BANDS = new Set([
|
|
|
194
186
|
]);
|
|
195
187
|
|
|
196
188
|
/**
|
|
197
|
-
* Whether a band item's `role` names one of the report's own bands rather
|
|
198
|
-
* a group instance's. This says whose band it is, never how to lay one
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* than carrying a verdict on the events themselves.
|
|
189
|
+
* Whether a band item's `role` names one of the report's own bands rather
|
|
190
|
+
* than a group instance's. This says whose band it is, never how to lay one
|
|
191
|
+
* out. ADR 0028 says why the engine classifies rather than carrying a verdict
|
|
192
|
+
* on the events themselves.
|
|
202
193
|
*
|
|
203
194
|
* @param {string} role An item or image event's `role`.
|
|
204
195
|
* @returns {boolean} True for the report's own bands.
|
|
@@ -207,22 +198,18 @@ export function isReportBand(role) {
|
|
|
207
198
|
return REPORT_BANDS.has(role);
|
|
208
199
|
}
|
|
209
200
|
|
|
210
|
-
// The cooperative hand-back's plumbing. A timer is the slowest
|
|
211
|
-
// hand
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// the loop back is for. The timer stays as the last rung for a runtime that
|
|
219
|
-
// offers neither of the others; no runtime quario supports reaches it.
|
|
201
|
+
// The cooperative hand-back's plumbing. A timer is the slowest rung: ~1.20ms
|
|
202
|
+
// per hand-back against ~0.015ms for setImmediate, so a 10k-row render spent
|
|
203
|
+
// about 30% of its wall-clock on timers. Browsers are worse — past a
|
|
204
|
+
// nested-timer depth of five the HTML spec clamps a zero timer to 4ms.
|
|
205
|
+
// Deliberately not scheduler.yield(): Chrome-only, and its continuation is
|
|
206
|
+
// scheduled ahead of other host tasks, the opposite of what handing the loop
|
|
207
|
+
// back is for. The timer stays as the last rung, which no runtime quario
|
|
208
|
+
// supports reaches.
|
|
220
209
|
//
|
|
221
|
-
// setImmediate is Node's alone, so reaching it is a feature detection
|
|
222
|
-
// an import: this module runs in browsers
|
|
223
|
-
//
|
|
224
|
-
// types are deliberately DOM-only (only @quario/xlsx pulls Node's in) — a
|
|
225
|
-
// typed global would mean a dependency for one name.
|
|
210
|
+
// setImmediate is Node's alone, so reaching it is a feature detection rather
|
|
211
|
+
// than an import: this module runs in browsers, where node:timers would not
|
|
212
|
+
// resolve. Through globalThis because the package's types are DOM-only.
|
|
226
213
|
let immediate = /** @type {any} */ (globalThis).setImmediate;
|
|
227
214
|
/** @type {MessageChannel | undefined} */
|
|
228
215
|
let channel;
|
|
@@ -265,13 +252,10 @@ export async function breathe() {
|
|
|
265
252
|
* so a target writes no event loop of its own — in stream order, exactly once
|
|
266
253
|
* each, pulled lazily, handing the loop back every 1000 events. A missing
|
|
267
254
|
* handler ignores that event, a handler that throws rejects without draining
|
|
268
|
-
* the rest, and handlers are synchronous
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* itself. No opening event is required: the driver reads `event.type` and
|
|
273
|
-
* nothing else, so a stream that starts part-way through walks like any other.
|
|
274
|
-
* See SCHEMA.md ("The walk driver"), which is normative.
|
|
255
|
+
* the rest, and handlers are synchronous. The first event reaches its handler
|
|
256
|
+
* before a second is pulled, so a target settles what it needs from
|
|
257
|
+
* `report-start` there. No opening event is required: the driver reads
|
|
258
|
+
* `event.type` and nothing else. SCHEMA.md ("The walk driver") is normative.
|
|
275
259
|
*
|
|
276
260
|
* @param {Iterable<any>} events One render's event stream.
|
|
277
261
|
* @param {Record<string, (event: any) => void>} handlers Per-event handlers.
|
|
@@ -285,6 +269,48 @@ export async function walk(events, handlers) {
|
|
|
285
269
|
}
|
|
286
270
|
}
|
|
287
271
|
|
|
272
|
+
// What each event does to the bracket being folded, and what it yields, if
|
|
273
|
+
// anything. Outside a bracket an event passes straight through; inside, it is
|
|
274
|
+
// a slot. A stray `split-end` with no opening -- a stream taken up part-way --
|
|
275
|
+
// yields nothing.
|
|
276
|
+
/** @type {Record<string, (fold: { bracket: any }, event: any) => any>} */
|
|
277
|
+
let FOLD = {
|
|
278
|
+
"split-start": (fold, event) => {
|
|
279
|
+
fold.bracket = { ...event, type: "split", items: [] };
|
|
280
|
+
return null;
|
|
281
|
+
},
|
|
282
|
+
"split-end": (fold) => {
|
|
283
|
+
let out = fold.bracket;
|
|
284
|
+
fold.bracket = null;
|
|
285
|
+
return out;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
/** @type {(fold: { bracket: any }, event: any) => any} */
|
|
289
|
+
let plain = (fold, event) => {
|
|
290
|
+
if (!fold.bracket) return event;
|
|
291
|
+
fold.bracket.items.push(event);
|
|
292
|
+
return null;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The split bracket, folded: `split-start`, the slot events between, and
|
|
297
|
+
* `split-end` become one `split` event wearing the opening's `role`, `path`,
|
|
298
|
+
* `slots` and `style`, with the slot events in order under `items`. Every
|
|
299
|
+
* other event passes through untouched, lazily, so a consumer that wants a
|
|
300
|
+
* split whole reads `splits(stream)` instead of keeping a bracket of its own.
|
|
301
|
+
* SCHEMA.md ("The walk driver") is normative.
|
|
302
|
+
*
|
|
303
|
+
* @param {Iterable<any>} events One render's event stream.
|
|
304
|
+
* @returns {Generator<any, void>} The same stream, each bracket one event.
|
|
305
|
+
*/
|
|
306
|
+
export function* splits(events) {
|
|
307
|
+
let fold = { bracket: null };
|
|
308
|
+
for (let event of events) {
|
|
309
|
+
let out = (FOLD[event.type] ?? plain)(fold, event);
|
|
310
|
+
if (out) yield out;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
288
314
|
// Optional public event fields are present only when truthy — the one rule
|
|
289
315
|
// for optional event data, owned here. What rides it is therefore any value
|
|
290
316
|
// whose falsy form means "undeclared"; a field whose zero or empty string is
|
package/lib/style.js
CHANGED
|
@@ -9,13 +9,8 @@
|
|
|
9
9
|
* The **resolutions** are what a declaration means before any target sees it.
|
|
10
10
|
* A row's box is the one there is (SCHEMA.md, "Style declarations"): it says
|
|
11
11
|
* what the document means rather than what a surface paints, so every target
|
|
12
|
-
* is handed the answer
|
|
13
|
-
*
|
|
14
|
-
* which are restated per target on purpose because a coercion is each target's
|
|
15
|
-
* own edge.
|
|
16
|
-
*
|
|
17
|
-
* Past those, each target maps these names to its own formatting model, and
|
|
18
|
-
* nothing here knows about any of them.
|
|
12
|
+
* is handed the answer. `finite`/`HEX` sit on the other side of that line and
|
|
13
|
+
* are restated per target, a coercion being each target's own edge.
|
|
19
14
|
*/
|
|
20
15
|
import { record } from "./names.js";
|
|
21
16
|
import { defaultDigits } from "./precision.js";
|
|
@@ -147,10 +142,9 @@ export let FORMAT_VOCABULARY = Object.freeze({
|
|
|
147
142
|
* the guard every name from a document gets: a `kind` resolved off
|
|
148
143
|
* `Object.prototype` is not a kind.
|
|
149
144
|
*
|
|
150
|
-
* Exported because
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* cell for nothing (`./stream.js`).
|
|
145
|
+
* Exported because `typed()` asks the same question and only that question:
|
|
146
|
+
* resolving a whole declaration to read one field would allocate per cell
|
|
147
|
+
* for nothing (`./stream.js`).
|
|
154
148
|
*
|
|
155
149
|
* @type {(value: any) => string | undefined}
|
|
156
150
|
*/
|
|
@@ -398,19 +392,15 @@ export let checkRowStyle = withoutNames(ROW_NAMES, checkStyle, "a table row");
|
|
|
398
392
|
// --- resolution --------------------------------------------------------------
|
|
399
393
|
|
|
400
394
|
// A resolved `=` result the vocabulary rejects, judged once where every style
|
|
401
|
-
// block resolves
|
|
402
|
-
//
|
|
403
|
-
// partitions the vocabulary three ways -- omitted, kept, voided. The argument
|
|
404
|
-
// for each is docs/adr/0055; what is restated here is only which is which, and
|
|
405
|
-
// why the two exceptions cannot be folded into the general rule.
|
|
395
|
+
// block resolves. What the declaration falls back *to* partitions the
|
|
396
|
+
// vocabulary three ways — omitted, kept, voided (docs/adr/0055).
|
|
406
397
|
//
|
|
407
|
-
// A literal never reaches here: `checkStyle`
|
|
408
|
-
//
|
|
398
|
+
// A literal never reaches here: `checkStyle` refused it at compile, so only
|
|
399
|
+
// an `=` entry is ever guarded.
|
|
409
400
|
//
|
|
410
|
-
// `format` then takes one more step
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// count or its form already on it (docs/adr/0056).
|
|
401
|
+
// `format` then takes one more step below: whatever survives is **resolved**,
|
|
402
|
+
// the shorthand widened and the kind's own answer filled in, so what crosses
|
|
403
|
+
// the stream carries its digit count or its form (docs/adr/0056).
|
|
414
404
|
|
|
415
405
|
// The sentinel a failed result becomes when its name is one the engine omits.
|
|
416
406
|
// A symbol, so nothing an expression can evaluate to is ever equal to it --
|
|
@@ -424,20 +414,16 @@ export let SKIP = Symbol("skip");
|
|
|
424
414
|
* Wrap a style entry's compiled `=` result in the rule its name answers to.
|
|
425
415
|
*
|
|
426
416
|
* **Omitted** is the general rule and needs no branch: the key is never
|
|
427
|
-
* written, so the layer below stands
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* `
|
|
431
|
-
*
|
|
432
|
-
* ownership is settled over the names a block declares, before any of this
|
|
433
|
-
* resolves, so dropping a rejected `borderTopColor` cannot hand the cell its
|
|
434
|
-
* row's other two (docs/adr/0062).
|
|
417
|
+
* written, so the layer below stands — "contributes nothing, same as omit"
|
|
418
|
+
* (SCHEMA.md). It is the rule for the box too: side ownership is settled over
|
|
419
|
+
* the names a block declares, before any of this resolves, so dropping a
|
|
420
|
+
* rejected `borderTopColor` cannot hand the cell its row's other two
|
|
421
|
+
* (docs/adr/0062).
|
|
435
422
|
*
|
|
436
|
-
* **Voided** is `currency` alone. Its fallback is
|
|
437
|
-
*
|
|
438
|
-
* under another's name
|
|
439
|
-
*
|
|
440
|
-
* to `display()` as a bogus `format` kind does (docs/adr/0041).
|
|
423
|
+
* **Voided** is `currency` alone. Its fallback is the instance's own code,
|
|
424
|
+
* not the layer below, so omitting would present one denomination's money
|
|
425
|
+
* under another's name. `null` keeps an answer nothing can present, so the
|
|
426
|
+
* cell drops to `display()` (docs/adr/0041).
|
|
441
427
|
*
|
|
442
428
|
* @type {(name: string, resolve: (scope: any) => any) => (scope: any) => any}
|
|
443
429
|
*/
|
|
@@ -506,10 +492,8 @@ let presentedNumber = (kind, own, style, options) => {
|
|
|
506
492
|
* own answer and no consumer computes one (docs/adr/0053, docs/adr/0056).
|
|
507
493
|
*
|
|
508
494
|
* A `currency` whose code Intl will not read resolves to a kind with **no**
|
|
509
|
-
* `digits
|
|
510
|
-
* format. A declared `digits` still crosses
|
|
511
|
-
* terms, and making it depend on a sibling declaration would be a special
|
|
512
|
-
* case bought for nothing.
|
|
495
|
+
* `digits`, the withdrawal that leaves the worksheet writing no number
|
|
496
|
+
* format. A declared `digits` still crosses: it resolved on its own terms.
|
|
513
497
|
*
|
|
514
498
|
* @param {{ format?: unknown, currency?: unknown } | null} [style] A cell's style.
|
|
515
499
|
* @param {{ currency?: string } | null} [options] The instance's options.
|
|
@@ -529,15 +513,10 @@ export let formatOf = (style, options) => {
|
|
|
529
513
|
* past `guarded` that a name needs here, and the last thing a style block has
|
|
530
514
|
* done to it before a target sees it (docs/adr/0056).
|
|
531
515
|
*
|
|
532
|
-
* By the time this runs the declaration is
|
|
533
|
-
* `checkStyle` at compile,
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* to a kind that reads and still presents nothing, which today is only a
|
|
537
|
-
* `currency` whose code Intl will not take.
|
|
538
|
-
*
|
|
539
|
-
* The block is returned rather than mutated silently, because it is the value
|
|
540
|
-
* `styleFn` hands on.
|
|
516
|
+
* By the time this runs the declaration is sound: a literal passed
|
|
517
|
+
* `checkStyle` at compile, an `=` result passed `guarded`. So the `delete`
|
|
518
|
+
* below is not the omit rule but the answer to a kind that reads and still
|
|
519
|
+
* presents nothing — today only a `currency` whose code Intl will not take.
|
|
541
520
|
*
|
|
542
521
|
* @type {(style: any, options?: any) => any}
|
|
543
522
|
*/
|
|
@@ -556,9 +535,8 @@ export let resolveFormat = (style, options) => {
|
|
|
556
535
|
* rather than once per helper.
|
|
557
536
|
*
|
|
558
537
|
* A resolved style already carries the answer, and a target reads it off the
|
|
559
|
-
* declaration rather than calling this.
|
|
560
|
-
*
|
|
561
|
-
* either may be asked.
|
|
538
|
+
* declaration rather than calling this. This is for a host holding a style
|
|
539
|
+
* the stream has not resolved: the shorthand is widened here first.
|
|
562
540
|
*
|
|
563
541
|
* @param {{ format?: unknown, currency?: unknown } | null} [style] A cell's style.
|
|
564
542
|
* @param {{ currency?: string } | null} [options] The instance's currency code.
|
|
@@ -570,14 +548,12 @@ export let fractionDigits = (style, options) => formatOf(style, options)?.digits
|
|
|
570
548
|
|
|
571
549
|
// A row's `style` resolves onto its cells (SCHEMA.md, "Style declarations").
|
|
572
550
|
// The box is the half that cannot arrive any other way: a table row is not a
|
|
573
|
-
// box on any surface
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
// layering each target already does, which is why only the box moves.
|
|
551
|
+
// box on any surface, so it is resolved here once and no target is left with
|
|
552
|
+
// an opinion. Every other declaration reaches the cells by the layering each
|
|
553
|
+
// target already does, which is why only the box moves.
|
|
577
554
|
//
|
|
578
|
-
// Which names are the box
|
|
579
|
-
//
|
|
580
|
-
// So is which of them reaches which cell -- see `boxReaching`. What is left
|
|
555
|
+
// Which names are the box, and which reach which cell, are decided where the
|
|
556
|
+
// block is compiled — once per table rather than once per row. What is left
|
|
581
557
|
// for render is writing the answer.
|
|
582
558
|
|
|
583
559
|
// The three names of each border side, built once, which is what lets
|
|
@@ -591,12 +567,10 @@ let BORDER_NAMES = Object.fromEntries(
|
|
|
591
567
|
* The names an under-box loses once an over-box has spoken: every name the
|
|
592
568
|
* over-box states, plus the other two of any border side it states one of.
|
|
593
569
|
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* render (docs/adr/0049); fanning a row's box onto its cells uses it for the
|
|
599
|
-
* same reason (docs/adr/0062).
|
|
570
|
+
* The whole-side rule, over declaration names rather than resolved objects: a
|
|
571
|
+
* compiled block writes every name it declares whatever the values resolve
|
|
572
|
+
* to. Used composing the totals block into its rows (docs/adr/0049) and
|
|
573
|
+
* fanning a row's box onto its cells (docs/adr/0062).
|
|
600
574
|
*
|
|
601
575
|
* @type {(names: Iterable<string>) => Set<string>}
|
|
602
576
|
*/
|
|
@@ -612,12 +586,10 @@ export let coveredNames = (names) => {
|
|
|
612
586
|
* Which of a row box's names reach one cell: the row's, less everything the
|
|
613
587
|
* cell's own declarations cover. A border side is won whole by the cell that
|
|
614
588
|
* names any of its three parts, so a cell that failed-soft on one name does
|
|
615
|
-
* not pick up the row's other two
|
|
616
|
-
* question settled here, once per table, rather than per cell per row.
|
|
589
|
+
* not pick up the row's other two. Settled once per table.
|
|
617
590
|
*
|
|
618
|
-
* `null` is the
|
|
619
|
-
*
|
|
620
|
-
* the object over rather than copying it name by name.
|
|
591
|
+
* `null` is the common case, a cell declaring no box name: the row's box is
|
|
592
|
+
* that cell's entire, so `fanBox` hands the object over rather than copying.
|
|
621
593
|
*
|
|
622
594
|
* @type {(rowNames: readonly string[], cellNames: readonly string[]) => string[] | null}
|
|
623
595
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quario",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Tiny, CSP-safe report engine for JavaScript. One JSON document renders to HTML, PDF, XLSX, CSV and Word.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"csp",
|
|
7
|
+
"json",
|
|
8
|
+
"pdf",
|
|
9
|
+
"report",
|
|
10
|
+
"reporting",
|
|
11
|
+
"xlsx"
|
|
12
|
+
],
|
|
5
13
|
"homepage": "https://getquario.com",
|
|
6
14
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
15
|
"repository": {
|
|
@@ -51,7 +59,7 @@
|
|
|
51
59
|
"sjabloon",
|
|
52
60
|
"padvinder"
|
|
53
61
|
],
|
|
54
|
-
"limit": "14 kB"
|
|
62
|
+
"limit": "14.2 kB"
|
|
55
63
|
}
|
|
56
64
|
],
|
|
57
65
|
"engines": {
|