quario 0.11.0 → 0.12.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 +36 -0
- package/lib/index.d.ts +114 -8
- package/lib/index.js +117 -12
- package/lib/plan.js +175 -74
- package/lib/scope.js +7 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# quario
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- A registered function may be asynchronous. The engine resolves every such call before the banded walk, in one wave, and the walk itself stays synchronous.
|
|
8
|
+
|
|
9
|
+
Write the call as the whole value of a cell in a detail row, and take its arguments from the row:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{ "data": "$.orders[*]", "detail": [{ "type": "text", "value": "{{ customerName(@.customerId) }}" }] }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The engine calls the function once for every row it renders, starts every call together, and waits for all of them before the first event reaches a target. A row that `where` or `take` removed is never fetched. Deduplication and any limit on how many calls run at once belong to the function itself: a report over ten thousand rows starts ten thousand calls.
|
|
16
|
+
|
|
17
|
+
Three rules hold. The call is the whole value of a cell, so `{{ lookup(@.id).name }}` and `{{ "#" + lookup(@.id) }}` are errors that name the function and the cell. The arguments come from the row, so a cell that reads no row cannot hold one. And `render()` resolves, while `stream()` reports the call it cannot wait for.
|
|
18
|
+
|
|
19
|
+
A function that rejects fails the render with an error that names the function and the cell, and keeps the class the function threw.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- **The declarations now refuse what the engine refuses.** Eight style declarations type-checked at nodes the engine rejects them on, so a report that compiled could still fail to render: `valign` on a stacked item, flow spacing on a table cell or a total cell, and `format`, `currency`, `href` and flow spacing on a row box. Each site now states the narrowing the engine has always applied, through four new exported types — `StackedStyleDeclarations`, `CellStyleDeclarations`, `RowStyleDeclarations`, and the `SlotStyleDeclarations` that was already there.
|
|
24
|
+
|
|
25
|
+
No document that the engine accepts has stopped type-checking. The narrowing removes only names the engine was already refusing.
|
|
26
|
+
|
|
27
|
+
**`SCHEMA.md` now states two keys it always admitted.** The document-shape block never listed `required`, and the group-definition block never listed `collapsed` — the latter while three target sections linked to that section for it, and while the section said a group is collapsible without a field for it. Both are stated now, `collapsed` with the paragraph it never had.
|
|
28
|
+
|
|
29
|
+
- **The declarations now carry `required`, and a group's `label` and `collapsed`.** `ReportSchema` states `required`, and `Group` states both `label` and `collapsed`. The engine has admitted all three and `SCHEMA.md` specifies them, so a TypeScript author marking a declaration required, naming an instance for navigation, or opening a group shut was told it was an error and had to cast.
|
|
30
|
+
|
|
31
|
+
`required` is keyed by a new exported type, `RequirableName`, which spells the same names `REQUIRABLE` carries. A marking no target could answer for is therefore an error where an author writes it, rather than a `plan` problem after the fact — `{ "negative": true }` names a modifier inside `format` rather than a declaration a target answers for, and the compiler now says so. `collapsed` reads `boolean`, because the engine accepts `false` for it as plainly as `true`.
|
|
32
|
+
|
|
33
|
+
## 0.11.1
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- **The declarations now carry `href` and `schemes`.** `StyleDeclarations` states `href`, `RunStyleDeclarations` picks it up as the twelfth inline name, and `QuarioOptions` states the `schemes` allowlist beside the three names that present `format`. The engine has admitted both since 0.11.0 and `SCHEMA.md` specifies them; the published types said otherwise, so a TypeScript consumer writing a link, or the allowlist that admits one, was told it was an error. The `format` declaration's own comment said a kind carries one modifier, which stopped being true when a number kind took three. `test/types.check.ts` exercises the allowlist and both spellings of a link, since a hand-written surface drifts exactly where nothing reads it.
|
|
38
|
+
|
|
3
39
|
## 0.11.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/lib/index.d.ts
CHANGED
|
@@ -95,8 +95,9 @@ export interface StyleDeclarations extends BoxDeclarations {
|
|
|
95
95
|
valign?: ExpressionValue<VAlign>;
|
|
96
96
|
/**
|
|
97
97
|
* How a number or date is presented: a kind, or an object naming one and
|
|
98
|
-
* carrying the
|
|
99
|
-
* three number kinds, `form` for `date`. Locale stays on the
|
|
98
|
+
* carrying the modifiers that kind understands — `digits`, `negative` and
|
|
99
|
+
* `zero` for the three number kinds, `form` for `date`. Locale stays on the
|
|
100
|
+
* instance.
|
|
100
101
|
*/
|
|
101
102
|
format?: ExpressionValue<FormatDeclaration>;
|
|
102
103
|
/**
|
|
@@ -107,6 +108,14 @@ export interface StyleDeclarations extends BoxDeclarations {
|
|
|
107
108
|
* than falling back to the instance's currency. Unread without the kind.
|
|
108
109
|
*/
|
|
109
110
|
currency?: ExpressionValue<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Where this cell's text points: a URL, or `#` and a group's `label` for a
|
|
113
|
+
* destination inside the document. The one declaration the engine judges
|
|
114
|
+
* rather than passes on — a URL is admitted against the instance's `schemes`
|
|
115
|
+
* allowlist before it crosses, and a refused value is omitted, so the text
|
|
116
|
+
* renders and the link does not. A table row refuses it.
|
|
117
|
+
*/
|
|
118
|
+
href?: ExpressionValue<string>;
|
|
110
119
|
/** Blank space before this item, in points. */
|
|
111
120
|
spaceBefore?: ExpressionValue<number>;
|
|
112
121
|
/** Blank space after this item, in points. */
|
|
@@ -147,6 +156,36 @@ export const ZERO_DASH: string;
|
|
|
147
156
|
*/
|
|
148
157
|
export const REQUIRABLE: readonly string[];
|
|
149
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The same names as a type, so a marking no target could answer for is an
|
|
161
|
+
* error where an author writes it rather than a
|
|
162
|
+
* [`plan`](../../../SCHEMA.md#validation) problem after the fact. This
|
|
163
|
+
* package's suite holds the two to each other in both directions, so neither
|
|
164
|
+
* can gain a name the other lacks.
|
|
165
|
+
*/
|
|
166
|
+
export type RequirableName =
|
|
167
|
+
| "page.header"
|
|
168
|
+
| "page.footer"
|
|
169
|
+
| "break"
|
|
170
|
+
| "reset"
|
|
171
|
+
| "page.margin"
|
|
172
|
+
| "header.height"
|
|
173
|
+
| "groups"
|
|
174
|
+
| "label"
|
|
175
|
+
| "collapsed"
|
|
176
|
+
| "column.width"
|
|
177
|
+
| "slot.width"
|
|
178
|
+
| "span"
|
|
179
|
+
| "uppercase"
|
|
180
|
+
| "href"
|
|
181
|
+
| "spaceBefore"
|
|
182
|
+
| "spaceAfter"
|
|
183
|
+
| "format"
|
|
184
|
+
| "family"
|
|
185
|
+
| "valign"
|
|
186
|
+
| "runs"
|
|
187
|
+
| "alt";
|
|
188
|
+
|
|
150
189
|
/**
|
|
151
190
|
* The declarations a styled run accepts: the inline half of the vocabulary,
|
|
152
191
|
* and nothing else. The box and flow spacing describe a block and `align`
|
|
@@ -167,6 +206,7 @@ export type RunStyleDeclarations = Pick<
|
|
|
167
206
|
| "uppercase"
|
|
168
207
|
| "format"
|
|
169
208
|
| "currency"
|
|
209
|
+
| "href"
|
|
170
210
|
>;
|
|
171
211
|
|
|
172
212
|
/**
|
|
@@ -240,8 +280,10 @@ export interface Cell {
|
|
|
240
280
|
style?: StyleDeclarations;
|
|
241
281
|
}
|
|
242
282
|
|
|
243
|
-
export interface TextItem extends Cell {
|
|
283
|
+
export interface TextItem extends Omit<Cell, "style"> {
|
|
244
284
|
type: "text";
|
|
285
|
+
/** Literal only — never an expression. */
|
|
286
|
+
style?: StackedStyleDeclarations;
|
|
245
287
|
}
|
|
246
288
|
|
|
247
289
|
/** How an image is sized: at its intrinsic size, or scaled to the content width. */
|
|
@@ -292,6 +334,32 @@ export interface ImageItem {
|
|
|
292
334
|
*/
|
|
293
335
|
export type SlotStyleDeclarations = Omit<StyleDeclarations, "spaceBefore" | "spaceAfter">;
|
|
294
336
|
|
|
337
|
+
/**
|
|
338
|
+
* What a stacked item declares: the vocabulary less `valign`. A stacked item
|
|
339
|
+
* sits in a band's flow and its box is its own height, so there is nothing for
|
|
340
|
+
* it to align against — where a slot's box is the split's height, which is why
|
|
341
|
+
* a slot keeps the name.
|
|
342
|
+
*/
|
|
343
|
+
export type StackedStyleDeclarations = Omit<StyleDeclarations, "valign">;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* What a table cell declares: the vocabulary less flow spacing. The same
|
|
347
|
+
* narrowing a slot takes, under its own name because the engine refuses the
|
|
348
|
+
* two in their own words and a reader follows the wording to the node.
|
|
349
|
+
*/
|
|
350
|
+
export type CellStyleDeclarations = Omit<StyleDeclarations, "spaceBefore" | "spaceAfter">;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* What a row box declares: a cell's names less the three that present a value.
|
|
354
|
+
* A box wraps cells and holds no value of its own, so `format`, `currency` and
|
|
355
|
+
* `href` have nothing to act on. Worn by the detail row, the header row's box
|
|
356
|
+
* and both halves of the totals block.
|
|
357
|
+
*/
|
|
358
|
+
export type RowStyleDeclarations = Omit<
|
|
359
|
+
StyleDeclarations,
|
|
360
|
+
"format" | "currency" | "href" | "spaceBefore" | "spaceAfter"
|
|
361
|
+
>;
|
|
362
|
+
|
|
295
363
|
/**
|
|
296
364
|
* What an image slot declares: an image item's names less flow spacing, plus
|
|
297
365
|
* `valign` — the one name an image accepts in a slot and nowhere else, because
|
|
@@ -343,7 +411,9 @@ export interface TableHeader {
|
|
|
343
411
|
span?: number;
|
|
344
412
|
}
|
|
345
413
|
|
|
346
|
-
export interface TableColumn extends Cell {
|
|
414
|
+
export interface TableColumn extends Omit<Cell, "style"> {
|
|
415
|
+
/** Literal only — never an expression. */
|
|
416
|
+
style?: CellStyleDeclarations;
|
|
347
417
|
/**
|
|
348
418
|
* Absent exactly when a neighbour's header `span` covers this column: every
|
|
349
419
|
* column's header slot is filled once, by its own or by a span.
|
|
@@ -355,14 +425,16 @@ export interface TableColumn extends Cell {
|
|
|
355
425
|
|
|
356
426
|
export interface TableRow {
|
|
357
427
|
visible?: ExpressionValue<boolean>;
|
|
358
|
-
style?:
|
|
428
|
+
style?: RowStyleDeclarations;
|
|
359
429
|
}
|
|
360
430
|
|
|
361
431
|
/**
|
|
362
432
|
* One cell of a total row: an ordinary cell, plus the columns it covers. The
|
|
363
433
|
* spans in a row must sum to the column count exactly.
|
|
364
434
|
*/
|
|
365
|
-
export type TotalCell = Cell & {
|
|
435
|
+
export type TotalCell = Omit<Cell, "style"> & {
|
|
436
|
+
/** Literal only — never an expression. */
|
|
437
|
+
style?: CellStyleDeclarations;
|
|
366
438
|
/**
|
|
367
439
|
* How many columns this cell covers — a literal integer >= 1, never an
|
|
368
440
|
* expression; absent is 1.
|
|
@@ -373,7 +445,7 @@ export type TotalCell = Cell & {
|
|
|
373
445
|
export interface TotalRow {
|
|
374
446
|
cells: [TotalCell, ...TotalCell[]];
|
|
375
447
|
visible?: ExpressionValue<boolean>;
|
|
376
|
-
style?:
|
|
448
|
+
style?: RowStyleDeclarations;
|
|
377
449
|
}
|
|
378
450
|
|
|
379
451
|
/**
|
|
@@ -392,7 +464,7 @@ export interface TotalBlock extends TableRow {
|
|
|
392
464
|
* header cells on every page continuation.
|
|
393
465
|
*/
|
|
394
466
|
export interface TableHeaderBox {
|
|
395
|
-
style?:
|
|
467
|
+
style?: RowStyleDeclarations;
|
|
396
468
|
}
|
|
397
469
|
|
|
398
470
|
export interface TableDetail {
|
|
@@ -405,6 +477,23 @@ export interface TableDetail {
|
|
|
405
477
|
export interface Group {
|
|
406
478
|
name: string;
|
|
407
479
|
by: `=${string}`;
|
|
480
|
+
/**
|
|
481
|
+
* This instance's name for **navigation** rather than for the page: one
|
|
482
|
+
* template, resolved once per instance in group scope, where the handle and
|
|
483
|
+
* `$` are bound and `@` is unbound. A target with somewhere to put a
|
|
484
|
+
* navigation name spends it there and the rest withdraw it, so it never
|
|
485
|
+
* changes what the page shows.
|
|
486
|
+
*/
|
|
487
|
+
label?: string;
|
|
488
|
+
/**
|
|
489
|
+
* Open every instance of this group shut: its own header and footer show,
|
|
490
|
+
* and the content between them does not. A target with a way to hide a row
|
|
491
|
+
* and keep it -- the worksheet -- hides those rows, where a reader opens the
|
|
492
|
+
* group again. The rest drop them, and the
|
|
493
|
+
* [matrix](../../../SCHEMA.md#what-each-target-makes-of-a-declaration) says
|
|
494
|
+
* which does which.
|
|
495
|
+
*/
|
|
496
|
+
collapsed?: boolean;
|
|
408
497
|
/**
|
|
409
498
|
* Where this group turns a page (paginated targets). Every boundary between
|
|
410
499
|
* consecutive instances turns; `before` adds the run's leading edge, `after`
|
|
@@ -448,6 +537,15 @@ export interface PageBands {
|
|
|
448
537
|
|
|
449
538
|
export interface ReportSchema {
|
|
450
539
|
params?: Record<string, JsonValue>;
|
|
540
|
+
/**
|
|
541
|
+
* The declarations this document cannot do without, each marked `true`.
|
|
542
|
+
* Only [`plan`](../../../SCHEMA.md#validation) reads it, against the target
|
|
543
|
+
* descriptors its caller names: every required declaration one of them
|
|
544
|
+
* withdraws or leaves unread becomes a problem of its own at
|
|
545
|
+
* `required.<name>`, which nulls the report as any other problem does. No
|
|
546
|
+
* target sees the marking, and no render changes because of it.
|
|
547
|
+
*/
|
|
548
|
+
required?: Partial<Record<RequirableName, true>>;
|
|
451
549
|
data: string;
|
|
452
550
|
where?: `=${string}`;
|
|
453
551
|
sort?: SortKey[];
|
|
@@ -516,6 +614,14 @@ export interface QuarioOptions {
|
|
|
516
614
|
currency?: string;
|
|
517
615
|
/** Timezone for `format: "date"`. Defaults to `"UTC"`. */
|
|
518
616
|
timeZone?: string;
|
|
617
|
+
/**
|
|
618
|
+
* The URL schemes an `href` may use, each lower-case and ending in a colon.
|
|
619
|
+
* Defaults to `https:` and `mailto:`. The allowlist is the host's, never the
|
|
620
|
+
* document's, and a value this cannot read throws at construction rather
|
|
621
|
+
* than falling back — an allowlist quietly replaced is a host believing it
|
|
622
|
+
* restricted something it had not.
|
|
623
|
+
*/
|
|
624
|
+
schemes?: readonly string[];
|
|
519
625
|
}
|
|
520
626
|
|
|
521
627
|
/** The settled result of an instance's license key verification. */
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { MARKING, verify } from "./license.js";
|
|
|
18
18
|
import { err, fault, locate } from "./locate.js";
|
|
19
19
|
import { BLOCKED, NAME, record } from "./names.js";
|
|
20
20
|
import { plan } from "./plan.js";
|
|
21
|
-
import { aggregateValue, startRunners, withPage, withRow } from "./scope.js";
|
|
21
|
+
import { aggregateValue, RESOLVED, startRunners, withPage, withRow } from "./scope.js";
|
|
22
22
|
import { opt } from "./stream.js";
|
|
23
23
|
|
|
24
24
|
// The event stream's public seam, single-sourced in ./stream.js, and the
|
|
@@ -199,7 +199,8 @@ let freezeAnchors = (anchors) =>
|
|
|
199
199
|
* @param {() => string | false} marking This render's unlicensed marking — its
|
|
200
200
|
* wording, or false when the render is covered. A thunk, so what it answers
|
|
201
201
|
* is this render's answer and not the compile's.
|
|
202
|
-
* @returns {{(data?: any): Generator<object, void, undefined>,
|
|
202
|
+
* @returns {{(data?: any, pre?: any): Generator<object, void, undefined>,
|
|
203
|
+
* names: string[],
|
|
203
204
|
* functions: { name: string, arity: number, doc?: string }[],
|
|
204
205
|
* paths: readonly any[]}} Event stream factory.
|
|
205
206
|
*/
|
|
@@ -239,6 +240,12 @@ function* guardPin(events) {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
// How `render` reaches the resolve wave without publishing it. A host drives a
|
|
244
|
+
// compiled report through `render()` or `stream()` and never settles one
|
|
245
|
+
// itself, so this is the engine talking to itself: a symbol, so the compiled
|
|
246
|
+
// report's public shape stays exactly what `lib/index.d.ts` declares.
|
|
247
|
+
let SETTLE = Symbol("quario.settle");
|
|
248
|
+
|
|
242
249
|
/**
|
|
243
250
|
* Build the event stream factory from a settled traversal — the half of the
|
|
244
251
|
* one compile that runs only when the plan carried no problems, shared by
|
|
@@ -250,7 +257,8 @@ function* guardPin(events) {
|
|
|
250
257
|
* @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number},
|
|
251
258
|
* locale?: string, currency?: string, timeZone?: string} | undefined} options
|
|
252
259
|
* Host controls closed over this compile.
|
|
253
|
-
* @returns {{(data?: any): Generator<object, void, undefined>,
|
|
260
|
+
* @returns {{(data?: any, pre?: any): Generator<object, void, undefined>,
|
|
261
|
+
* names: string[],
|
|
254
262
|
* functions: { name: string, arity: number, doc?: string }[],
|
|
255
263
|
* paths: readonly any[]}} Event stream factory.
|
|
256
264
|
*/
|
|
@@ -276,6 +284,7 @@ function assemble(planned, schema, marking, options) {
|
|
|
276
284
|
pageFooter,
|
|
277
285
|
headerHeight,
|
|
278
286
|
margin,
|
|
287
|
+
slotted,
|
|
279
288
|
} = /** @type {NonNullable<typeof planned.compiled>} */ (planned.compiled);
|
|
280
289
|
|
|
281
290
|
// The runner's own faults are located per render: a budget is spent while
|
|
@@ -289,23 +298,99 @@ function assemble(planned, schema, marking, options) {
|
|
|
289
298
|
}
|
|
290
299
|
};
|
|
291
300
|
|
|
292
|
-
|
|
293
|
-
|
|
301
|
+
// The eager pre-pass: select → filter → report sort → report take →
|
|
302
|
+
// aggregates (over the rows that remain). It cannot be lazy because the
|
|
303
|
+
// report header may interpolate report aggregates; only the banded walk
|
|
304
|
+
// itself streams. Lifted out of `emit` so a render that has to settle async
|
|
305
|
+
// calls first runs it once and hands the result on: a second run would
|
|
306
|
+
// re-spend the data query's budget and could fail a render that works.
|
|
307
|
+
/** @type {(data: any) => any} */
|
|
308
|
+
let prepare = (data) => {
|
|
294
309
|
let root = Object.create(null);
|
|
295
310
|
root.params = params;
|
|
296
311
|
root.input = inputOf(data);
|
|
297
312
|
let base = Object.create(null);
|
|
298
313
|
base["$"] = root;
|
|
299
|
-
// The eager pre-pass: select → filter → report sort → report take →
|
|
300
|
-
// aggregates (over the rows that remain). It cannot be lazy because the
|
|
301
|
-
// report header may interpolate report aggregates; only the banded walk
|
|
302
|
-
// itself streams.
|
|
303
314
|
let rows = runSelect(root.input);
|
|
304
315
|
if (where) rows = rows.filter((row) => where(withRow(base, row)));
|
|
305
316
|
if (sort) rows = sort(rows, base);
|
|
306
317
|
if (take != null) rows = rows.slice(0, take);
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
return {
|
|
319
|
+
root,
|
|
320
|
+
base,
|
|
321
|
+
rows,
|
|
322
|
+
aggregates: foldAggs(aggs, rows, base, root),
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// The wave settles what it can and leaves the rest to the walk. A slot it
|
|
327
|
+
// cannot evaluate against the row alone -- a synchronous host call reading
|
|
328
|
+
// `run.x` or a group handle, which is ordinary and stays ordinary -- goes
|
|
329
|
+
// back unsupplied, so the walk evaluates it with the scope it always had.
|
|
330
|
+
// Nothing is swallowed: a fault that is real throws again there, at the same
|
|
331
|
+
// cell path, and an async call whose arguments were not row-local reaches
|
|
332
|
+
// `runCell` as a promise nothing resolved and is refused by name.
|
|
333
|
+
let UNSETTLED = Symbol("quario.unsettled");
|
|
334
|
+
/** @type {(slot: any, scope: any) => any} */
|
|
335
|
+
let attempt = (slot, scope) => {
|
|
336
|
+
try {
|
|
337
|
+
return slot(scope);
|
|
338
|
+
} catch {
|
|
339
|
+
return UNSETTLED;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// The resolve wave (docs/adr/0088): every async call the surviving rows make,
|
|
344
|
+
// issued together and settled once, before the walk sees a single row. The
|
|
345
|
+
// walk then reads the answers off the render base and stays synchronous.
|
|
346
|
+
// A row the pre-pass dropped is never fetched.
|
|
347
|
+
/** @type {(data: any) => Promise<any>} */
|
|
348
|
+
let resolve = async (data) => {
|
|
349
|
+
let pre = prepare(data),
|
|
350
|
+
held = new Map(),
|
|
351
|
+
waiting = [];
|
|
352
|
+
// The map is parked whether or not anything filled it: its presence is how
|
|
353
|
+
// a cell tells a render from a caller draining `stream()`, and the two owe
|
|
354
|
+
// the author different sentences.
|
|
355
|
+
pre.base[RESOLVED] = held;
|
|
356
|
+
if (!slotted.length) return pre;
|
|
357
|
+
for (let row of pre.rows) {
|
|
358
|
+
let scope = withRow(pre.base, row),
|
|
359
|
+
forRow = Object.create(null);
|
|
360
|
+
held.set(row, forRow);
|
|
361
|
+
for (let { path, source, live } of slotted) {
|
|
362
|
+
/** @type {any[]} */
|
|
363
|
+
let supply = [];
|
|
364
|
+
forRow[path] = supply;
|
|
365
|
+
// A slot's own scope is the row and nothing else: no runners, no group
|
|
366
|
+
// handle. That is the rule, and it is what makes one call per row
|
|
367
|
+
// answerable without the walk having run.
|
|
368
|
+
for (let [at, slot] of live) {
|
|
369
|
+
let answer = attempt(slot, scope);
|
|
370
|
+
if (answer === UNSETTLED) continue;
|
|
371
|
+
supply[at] = answer;
|
|
372
|
+
if (typeof answer?.then === "function")
|
|
373
|
+
waiting.push(
|
|
374
|
+
Promise.resolve(answer).then(
|
|
375
|
+
(value) => (supply[at] = value),
|
|
376
|
+
// No relocation: the template finished evaluating before this
|
|
377
|
+
// promise settled, so what rejected is host code and keeps
|
|
378
|
+
// its own class behind the cell path (docs/adr/0057).
|
|
379
|
+
(error) => locate(path, source, error),
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await Promise.all(waiting);
|
|
386
|
+
return pre;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** @type {(data: any, pre?: any) => Generator<any>} */
|
|
390
|
+
let emit = (data, pre = prepare(data)) => {
|
|
391
|
+
let { root, base, rows, aggregates } = pre;
|
|
392
|
+
// The report's runners belong to this drain, not to the pre-pass: a target
|
|
393
|
+
// that drains the same stream twice counts from one again both times.
|
|
309
394
|
let runners = startRunners(running);
|
|
310
395
|
// Per-render page band closures: a paginated target calls them once per
|
|
311
396
|
// page with `{ number, total }` (1-based) and receives resolved item
|
|
@@ -347,6 +432,7 @@ function assemble(planned, schema, marking, options) {
|
|
|
347
432
|
return walked();
|
|
348
433
|
};
|
|
349
434
|
return Object.assign(emit, {
|
|
435
|
+
[SETTLE]: resolve,
|
|
350
436
|
names: Array.from(planned.names),
|
|
351
437
|
// Already signature objects — plan() paired each called name with its
|
|
352
438
|
// registry metadata through xprsn's signatures().
|
|
@@ -412,7 +498,26 @@ export function quario(options) {
|
|
|
412
498
|
// render is always a promise, even over a synchronous renderer.
|
|
413
499
|
render: (/** @type {any} */ target, /** @type {any} */ data) => {
|
|
414
500
|
checkTarget(target);
|
|
415
|
-
|
|
501
|
+
// The resolve wave runs before the target compiles, so the stream the
|
|
502
|
+
// target drains is the one carrying its answers (docs/adr/0088). The
|
|
503
|
+
// bound stream keeps the compiled report's own metadata: a target reads
|
|
504
|
+
// it off the function it was handed.
|
|
505
|
+
return settled
|
|
506
|
+
.then(() => /** @type {any} */ (stream)[SETTLE](data))
|
|
507
|
+
.then((pre) =>
|
|
508
|
+
runTarget(
|
|
509
|
+
target.compile,
|
|
510
|
+
// Only the render's own document reads the settled wave. A target
|
|
511
|
+
// that drains the stream over something else gets an ordinary
|
|
512
|
+
// stream, because nothing resolved that document's rows.
|
|
513
|
+
Object.assign((/** @type {any} */ d) => (d === data ? stream(d, pre) : stream(d)), {
|
|
514
|
+
names: stream.names,
|
|
515
|
+
functions: stream.functions,
|
|
516
|
+
paths: stream.paths,
|
|
517
|
+
}),
|
|
518
|
+
data,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
416
521
|
},
|
|
417
522
|
});
|
|
418
523
|
return {
|
package/lib/plan.js
CHANGED
|
@@ -51,7 +51,7 @@ import { LOCATION, fault, isDiagnostic, locate } from "./locate.js";
|
|
|
51
51
|
import { ANCHORS, BLOCKED, NAME, RESERVED, isRequirable, record } from "./names.js";
|
|
52
52
|
import { MATH } from "./math.js";
|
|
53
53
|
import { REDUCERS, reducerFor } from "./reducers.js";
|
|
54
|
-
import { aggregateValue, withRow } from "./scope.js";
|
|
54
|
+
import { aggregateValue, RESOLVED, withRow } from "./scope.js";
|
|
55
55
|
import { dimensions, sniff } from "./image.js";
|
|
56
56
|
import { oneValue, opt, sameStyle } from "./stream.js";
|
|
57
57
|
import {
|
|
@@ -149,12 +149,68 @@ let collect = (from, into) => {
|
|
|
149
149
|
/** @type {(visible: any, scope: any) => any} */
|
|
150
150
|
let hidden = (visible, scope) => visible && visible(scope) === false;
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
// Every closed key set on the authoring surface, named in one place. The
|
|
153
|
+
// readers below are what enforce them, and the gates that hold the other two
|
|
154
|
+
// descriptions of the surface to this one -- the declarations and SCHEMA.md --
|
|
155
|
+
// read the same object, so no gate keeps a list of its own (ADR 0086). A set
|
|
156
|
+
// that lives only at its call site is one nothing can check.
|
|
157
|
+
//
|
|
158
|
+
// A slot's set is its item's plus the width share, derived rather than
|
|
159
|
+
// restated, so the two cannot part.
|
|
160
|
+
let TEXT = ["type", "value", "visible", "style"];
|
|
161
|
+
let IMAGE = ["type", "source", "fit", "alt", "visible", "style"];
|
|
162
|
+
export let KEY_SETS = Object.freeze({
|
|
163
|
+
report: [
|
|
164
|
+
"params",
|
|
165
|
+
"required",
|
|
166
|
+
"data",
|
|
167
|
+
"where",
|
|
168
|
+
"sort",
|
|
169
|
+
"take",
|
|
170
|
+
"aggregates",
|
|
171
|
+
"run",
|
|
172
|
+
"style",
|
|
173
|
+
"header",
|
|
174
|
+
"empty",
|
|
175
|
+
"columns",
|
|
176
|
+
"groups",
|
|
177
|
+
"detail",
|
|
178
|
+
"footer",
|
|
179
|
+
"page",
|
|
180
|
+
],
|
|
181
|
+
reportHeader: ["height", "items"],
|
|
182
|
+
page: ["header", "footer", "margin"],
|
|
183
|
+
group: [
|
|
184
|
+
"name",
|
|
185
|
+
"by",
|
|
186
|
+
"label",
|
|
187
|
+
"collapsed",
|
|
188
|
+
"break",
|
|
189
|
+
"reset",
|
|
190
|
+
"columns",
|
|
191
|
+
"sort",
|
|
192
|
+
"take",
|
|
193
|
+
"aggregates",
|
|
194
|
+
"run",
|
|
195
|
+
"header",
|
|
196
|
+
"footer",
|
|
197
|
+
],
|
|
198
|
+
sortKey: ["by", "dir"],
|
|
199
|
+
text: TEXT,
|
|
200
|
+
textSlot: [...TEXT, "width"],
|
|
201
|
+
image: IMAGE,
|
|
202
|
+
imageSlot: [...IMAGE, "width"],
|
|
203
|
+
split: ["type", "slots", "visible", "style"],
|
|
204
|
+
styledRun: ["value", "style"],
|
|
205
|
+
detail: ["header", "row", "columns", "total"],
|
|
206
|
+
detailHeader: ["style"],
|
|
207
|
+
detailRow: ["visible", "style"],
|
|
208
|
+
column: ["header", "value", "visible", "style", "width"],
|
|
209
|
+
columnHeader: ["value", "style", "span"],
|
|
210
|
+
totalBlock: ["style", "visible", "rows"],
|
|
211
|
+
totalRow: ["cells", "style", "visible"],
|
|
212
|
+
totalCell: ["value", "visible", "style", "span"],
|
|
213
|
+
});
|
|
158
214
|
|
|
159
215
|
// A group's two structural literals. `break` names a position rather than a
|
|
160
216
|
// unit (ADR 0077), and this table is the whole of what its four values mean:
|
|
@@ -446,6 +502,10 @@ let planState = (schema, funcs) => {
|
|
|
446
502
|
// keeps the anchor-and-handle subset a consumer placing a node needs.
|
|
447
503
|
/** @type {Map<string, Set<string>>} */
|
|
448
504
|
let anchors = new Map();
|
|
505
|
+
/** @type {{ path: string, source: string,
|
|
506
|
+
* live: [number, (scope: any) => any][] }[]} */
|
|
507
|
+
let slotted = [];
|
|
508
|
+
|
|
449
509
|
/** @type {(path: string, reads: { name: string }[]) => void} */
|
|
450
510
|
let anchorsOf = (path, reads) => {
|
|
451
511
|
for (let { name } of reads) {
|
|
@@ -458,11 +518,19 @@ let planState = (schema, funcs) => {
|
|
|
458
518
|
|
|
459
519
|
return {
|
|
460
520
|
FNS,
|
|
521
|
+
// The host's own registry keys, which is what separates a call that may
|
|
522
|
+
// hand back a promise from one of the engine's synchronous built-ins. A
|
|
523
|
+
// host that shadows a built-in owns that name, and its key says so.
|
|
524
|
+
HOSTS: new Set(Object.keys(funcs || {})),
|
|
461
525
|
names,
|
|
462
526
|
functions,
|
|
463
527
|
BND,
|
|
464
528
|
problems,
|
|
465
529
|
warnings,
|
|
530
|
+
// Every cell the resolve wave has to settle before the walk, in compile
|
|
531
|
+
// order (docs/adr/0088). Empty for a document that calls nothing async,
|
|
532
|
+
// which is what lets a render skip the wave entirely.
|
|
533
|
+
slotted,
|
|
466
534
|
runNames,
|
|
467
535
|
anchors,
|
|
468
536
|
anchorsOf,
|
|
@@ -492,13 +560,27 @@ let compileProp = (source, path, FNS, BND) => {
|
|
|
492
560
|
locate(path, source, error, isXprsnDiagnostic(error) && relocateXprsn, 1);
|
|
493
561
|
}
|
|
494
562
|
};
|
|
563
|
+
// A promise nothing settled, refused where it stands. It is worse than a wrong
|
|
564
|
+
// value, because it is truthy: an unrefused one keeps every row a `where`
|
|
565
|
+
// should drop and shows every item a `visible` should hide. One helper, so the
|
|
566
|
+
// three places that can meet one say it the same way (docs/adr/0088).
|
|
567
|
+
/** @type {(value: any, path: string, source: any, said: string) => any} */
|
|
568
|
+
let refuseThenable = (value, path, source, said) => {
|
|
569
|
+
if (typeof value?.then === "function") locate(path, source, TypeError(said));
|
|
570
|
+
return value;
|
|
571
|
+
};
|
|
572
|
+
let ONLY_A_CELL = "an async function is read from a cell's value and nowhere else";
|
|
495
573
|
/** @type {(expression: any, path: string, source: any) => Eval} */
|
|
496
574
|
let runProp = (expression, path, source) => (scope) => {
|
|
575
|
+
let out;
|
|
497
576
|
try {
|
|
498
|
-
|
|
577
|
+
out = expression(scope);
|
|
499
578
|
} catch (error) {
|
|
500
579
|
locate(path, source, error, expression.isDiagnostic(error) && relocateXprsn, 1);
|
|
501
580
|
}
|
|
581
|
+
// The resolve wave settles a cell's value and only that, so nothing ever
|
|
582
|
+
// settles a computed property.
|
|
583
|
+
return refuseThenable(out, path, source, ONLY_A_CELL);
|
|
502
584
|
};
|
|
503
585
|
// The [live field](../../../CONTEXT.md#live-field): which page value a token
|
|
504
586
|
// *is*, for a target whose own format numbers pages. sjabloon offers each
|
|
@@ -524,13 +606,52 @@ let compileCell = (src, path, FNS, BND) => {
|
|
|
524
606
|
locate(path, src, error, isTemplateDiagnostic(error) && relocateTemplate);
|
|
525
607
|
}
|
|
526
608
|
};
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
609
|
+
// A promise that reached a token is one nothing resolved: either the caller
|
|
610
|
+
// drained `stream()`, which cannot wait, or the cell sits where no row does.
|
|
611
|
+
// xprsn already refuses one an operator consumed, so what arrives here is a
|
|
612
|
+
// whole-token call, and saying so is the only thing left to do about it.
|
|
613
|
+
/** @type {(out: any, path: string, source: any, resolving: boolean) => any} */
|
|
614
|
+
let refusePending = (out, path, source, resolving) => {
|
|
615
|
+
let said = resolving
|
|
616
|
+
? "an async function is read from a detail row's cell and nowhere else"
|
|
617
|
+
: "an async function is resolved by render(), and stream() cannot wait for one";
|
|
618
|
+
for (let token of out) refuseThenable(token.value, path, source, said);
|
|
619
|
+
return out;
|
|
620
|
+
};
|
|
621
|
+
// This cell's settled values for the row in scope, or nothing when the wave
|
|
622
|
+
// did not reach it. `held` is the render's whole map; a caller that already
|
|
623
|
+
// read it passes it in, because its presence is also how a cell tells a render
|
|
624
|
+
// from a caller draining `stream()`.
|
|
625
|
+
/** @type {(held: any, scope: Scope, path: string) => any} */
|
|
626
|
+
let settledFor = (held, scope, path) => {
|
|
627
|
+
let forRow = held && held.get(scope["@"]);
|
|
628
|
+
return forRow && forRow[path];
|
|
629
|
+
};
|
|
630
|
+
/** @type {(tokens: any, path: string, source: any, live: any[]) => (scope: Scope) => any} */
|
|
631
|
+
let runCell = (tokens, path, source, live) => {
|
|
632
|
+
// The branch is the compile's, not the render's: a cell that calls no host
|
|
633
|
+
// function is the overwhelming majority and reads nothing off the chain.
|
|
634
|
+
if (!live.length)
|
|
635
|
+
return (scope) => {
|
|
636
|
+
try {
|
|
637
|
+
return tokens.scoped(scope);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
locate(path, source, error, tokens.isDiagnostic(error) && relocateTemplate);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
return (scope) => {
|
|
643
|
+
let held = /** @type {any} */ (scope)[RESOLVED],
|
|
644
|
+
supply = settledFor(held, scope, path);
|
|
645
|
+
let out;
|
|
646
|
+
try {
|
|
647
|
+
out = tokens.scoped(scope, supply);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
locate(path, source, error, tokens.isDiagnostic(error) && relocateTemplate);
|
|
650
|
+
}
|
|
651
|
+
// Outside the catch: the refusal locates itself, and locating it twice
|
|
652
|
+
// would print the cell path twice.
|
|
653
|
+
return supply ? out : refusePending(out, path, source, !!held);
|
|
654
|
+
};
|
|
534
655
|
};
|
|
535
656
|
/** @type {(spec: string) => { fn: string, src: string, colon: number }} */
|
|
536
657
|
let splitFold = (spec) => {
|
|
@@ -554,7 +675,7 @@ let checkExprFold = (src, fn, path) => {
|
|
|
554
675
|
};
|
|
555
676
|
|
|
556
677
|
/** @param {State} state */
|
|
557
|
-
let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
678
|
+
let primitives = ({ FNS, HOSTS, BND, names, functions, anchorsOf, slotted }) => {
|
|
558
679
|
// A computed property: a literal, or an `=` xprsn expression. xprsn's
|
|
559
680
|
// `bound` keeps engine-provided anchors/handles out of the reported `names`.
|
|
560
681
|
/** @type {(spec: any, path: string) => Eval} */
|
|
@@ -583,7 +704,25 @@ let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
|
583
704
|
collect(tokens.names, names);
|
|
584
705
|
collect(tokens.functions, functions);
|
|
585
706
|
anchorsOf(path, tokens.reads);
|
|
586
|
-
|
|
707
|
+
// A slot is any registry call, and most of them are the engine's own
|
|
708
|
+
// reducers and rounders, which are synchronous by construction. Only a
|
|
709
|
+
// call into the host's registry can hand back a promise, so only those
|
|
710
|
+
// reach the resolve wave; every index stays sjabloon's, because the
|
|
711
|
+
// supply array is indexed by slot.
|
|
712
|
+
/** @type {[number, any][]} */
|
|
713
|
+
let live = tokens.slots
|
|
714
|
+
.map((/** @type {any} */ slot, /** @type {number} */ at) => [at, slot])
|
|
715
|
+
.filter((/** @type {any} */ pair) =>
|
|
716
|
+
pair[1].functions.some((/** @type {string} */ n) => HOSTS.has(n)),
|
|
717
|
+
);
|
|
718
|
+
// A host call's arguments come from the row (docs/adr/0088), so only a cell
|
|
719
|
+
// that reads `@` can be projected. The rest are not refused here: most
|
|
720
|
+
// host functions are synchronous, a footer calling one is ordinary, and
|
|
721
|
+
// whether a call is async is not a fact a compile holds. A promise that
|
|
722
|
+
// reaches a token unresolved is what says so, and `runCell` refuses it.
|
|
723
|
+
if (live.length && tokens.reads.some((/** @type {any} */ read) => read.name === "@"))
|
|
724
|
+
slotted.push({ path, source, live });
|
|
725
|
+
return runCell(tokens, path, source, live);
|
|
587
726
|
};
|
|
588
727
|
|
|
589
728
|
// Parse "reducer:=expr" into a reducer name and a compiled per-row expression,
|
|
@@ -958,7 +1097,7 @@ let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any
|
|
|
958
1097
|
bad(path, "expected an object");
|
|
959
1098
|
return NO_STYLED_RUN;
|
|
960
1099
|
}
|
|
961
|
-
keys(def,
|
|
1100
|
+
keys(def, KEY_SETS.styledRun, path);
|
|
962
1101
|
let tpl = textValue(def.value, path + ".value");
|
|
963
1102
|
unreadFormat(def.value, def.style, path);
|
|
964
1103
|
let entries = entriesOf(def, path, checkRunStyle);
|
|
@@ -1043,7 +1182,7 @@ let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any
|
|
|
1043
1182
|
let sortKey = (spec, i, path) => {
|
|
1044
1183
|
let keyPath = path + "[" + i + "]";
|
|
1045
1184
|
if (!obj(spec, keyPath)) return [];
|
|
1046
|
-
keys(spec,
|
|
1185
|
+
keys(spec, KEY_SETS.sortKey, keyPath);
|
|
1047
1186
|
let by = expression(spec.by, keyPath + ".by");
|
|
1048
1187
|
checkDir(spec.dir, keyPath + ".dir");
|
|
1049
1188
|
return [{ by, sign: spec.dir === "desc" ? -1 : 1 }];
|
|
@@ -1261,7 +1400,7 @@ let nodes = (
|
|
|
1261
1400
|
bad(path, "expected a template string or object");
|
|
1262
1401
|
return { cell: cellShape(NO_STYLED_RUNS, null, NO_LOOK), span: 1, box: NO_LOOK.box };
|
|
1263
1402
|
}
|
|
1264
|
-
keys(def,
|
|
1403
|
+
keys(def, KEY_SETS.columnHeader, path);
|
|
1265
1404
|
return { ...cellOf(def, path), span: spanOf(def, path) };
|
|
1266
1405
|
};
|
|
1267
1406
|
|
|
@@ -1298,7 +1437,7 @@ let nodes = (
|
|
|
1298
1437
|
/** @type {(def: any, path: string, slot: string) => any} */
|
|
1299
1438
|
let columnOf = (def, path, slot) => {
|
|
1300
1439
|
if (!obj(def, path)) return null;
|
|
1301
|
-
keys(def,
|
|
1440
|
+
keys(def, KEY_SETS.column, path);
|
|
1302
1441
|
let header = headerSlot(def.header, path + ".header", slot);
|
|
1303
1442
|
let { styled, visible, look } = cellParts(def, path, checkCellStyle);
|
|
1304
1443
|
// A width is a percentage of the table width, resolved by every target.
|
|
@@ -1319,7 +1458,7 @@ let nodes = (
|
|
|
1319
1458
|
*/
|
|
1320
1459
|
let totalOf = (def, path) => {
|
|
1321
1460
|
if (!obj(def, path)) return null;
|
|
1322
|
-
keys(def,
|
|
1461
|
+
keys(def, KEY_SETS.totalCell, path);
|
|
1323
1462
|
return { ...cellOf(def, path), span: spanOf(def, path) };
|
|
1324
1463
|
};
|
|
1325
1464
|
|
|
@@ -1329,7 +1468,7 @@ let nodes = (
|
|
|
1329
1468
|
// the line's geometry does not move with the data (SCHEMA.md, "Split item").
|
|
1330
1469
|
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
1331
1470
|
let textOf = (def, path, role, inSlot = false) => {
|
|
1332
|
-
keys(def, inSlot ?
|
|
1471
|
+
keys(def, inSlot ? KEY_SETS.textSlot : KEY_SETS.text, path);
|
|
1333
1472
|
checkTextType(def.type, path);
|
|
1334
1473
|
let { styled, visible, look } = cellParts(def, path, inSlot ? checkSlotStyle : checkItemStyle);
|
|
1335
1474
|
let shape = cellShape(styled, inSlot ? visible : null, look);
|
|
@@ -1415,7 +1554,7 @@ let nodes = (
|
|
|
1415
1554
|
let imageLook = (inSlot) => (inSlot ? checkSlotImageStyle : checkImageStyle);
|
|
1416
1555
|
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
1417
1556
|
let imageOf = (def, path, role, inSlot = false) => {
|
|
1418
|
-
keys(def, inSlot ?
|
|
1557
|
+
keys(def, inSlot ? KEY_SETS.imageSlot : KEY_SETS.image, path);
|
|
1419
1558
|
let source = expression(def.source, path + ".source");
|
|
1420
1559
|
checkFit(def.fit, path + ".fit");
|
|
1421
1560
|
// Read in the documented key order, and each read once: `stylesOf` reports
|
|
@@ -1469,7 +1608,7 @@ let nodes = (
|
|
|
1469
1608
|
// every slot crosses as the ordinary item or image event it already is.
|
|
1470
1609
|
/** @type {(def: any, path: string, role: string) => (scope: Scope) => any} */
|
|
1471
1610
|
let splitOf = (def, path, role) => {
|
|
1472
|
-
keys(def,
|
|
1611
|
+
keys(def, KEY_SETS.split, path);
|
|
1473
1612
|
let list = arr(def.slots, path + ".slots", true);
|
|
1474
1613
|
let defs = list || [];
|
|
1475
1614
|
// Two is the floor: a one-slot split is a text item wearing more syntax,
|
|
@@ -1589,7 +1728,7 @@ let bands = (
|
|
|
1589
1728
|
let rowLook = (value) => {
|
|
1590
1729
|
if (value.row == null || !obj(value.row, "detail.row"))
|
|
1591
1730
|
return { rowVisible: null, rowStyles: NO_ROW_STYLE };
|
|
1592
|
-
keys(value.row,
|
|
1731
|
+
keys(value.row, KEY_SETS.detailRow, "detail.row");
|
|
1593
1732
|
return {
|
|
1594
1733
|
rowVisible: visibleOf(value.row, "detail.row"),
|
|
1595
1734
|
rowStyles: rowStylesOf(value.row, "detail.row"),
|
|
@@ -1600,7 +1739,7 @@ let bands = (
|
|
|
1600
1739
|
/** @type {(value: any) => RowStyles} */
|
|
1601
1740
|
let headerLook = (value) => {
|
|
1602
1741
|
if (value.header == null || !obj(value.header, "detail.header")) return NO_ROW_STYLE;
|
|
1603
|
-
keys(value.header,
|
|
1742
|
+
keys(value.header, KEY_SETS.detailHeader, "detail.header");
|
|
1604
1743
|
return rowStylesOf(value.header, "detail.header");
|
|
1605
1744
|
};
|
|
1606
1745
|
// Which slot each column's header sits in, walked positionally rather than
|
|
@@ -1675,7 +1814,7 @@ let bands = (
|
|
|
1675
1814
|
/** @type {(value: any, defs: any[] | null) => TotalBlock} */
|
|
1676
1815
|
let tableTotals = (value, defs) => {
|
|
1677
1816
|
if (value.total == null || !obj(value.total, "detail.total")) return NO_TOTALS;
|
|
1678
|
-
keys(value.total,
|
|
1817
|
+
keys(value.total, KEY_SETS.totalBlock, "detail.total");
|
|
1679
1818
|
let block = rowEntriesOf(value.total, "detail.total");
|
|
1680
1819
|
return {
|
|
1681
1820
|
visible: visibleOf(value.total, "detail.total"),
|
|
@@ -1701,7 +1840,7 @@ let bands = (
|
|
|
1701
1840
|
/** @type {(def: any, path: string, defs: any[] | null, block: RowEntries) => TotalRow} */
|
|
1702
1841
|
let totalRowOf = (def, path, defs, block) => {
|
|
1703
1842
|
if (!obj(def, path)) return { styles: foldRow(block), visible: null, cells: [], fan: [] };
|
|
1704
|
-
keys(def,
|
|
1843
|
+
keys(def, KEY_SETS.totalRow, path);
|
|
1705
1844
|
let cells = counted(arr(def.cells, path + ".cells", true), defs, path);
|
|
1706
1845
|
let styles = foldRow(underRow(block, rowEntriesOf(def, path)));
|
|
1707
1846
|
let mapped = mapTotals(cells, path + ".cells");
|
|
@@ -1789,7 +1928,7 @@ let bands = (
|
|
|
1789
1928
|
};
|
|
1790
1929
|
/** @type {(value: any) => Band} */
|
|
1791
1930
|
let tableOf = (value) => {
|
|
1792
|
-
keys(value,
|
|
1931
|
+
keys(value, KEY_SETS.detail, "detail");
|
|
1793
1932
|
let headerStyles = headerLook(value);
|
|
1794
1933
|
let { rowVisible, rowStyles } = rowLook(value);
|
|
1795
1934
|
let { defs, columns } = tableColumns(value);
|
|
@@ -1950,25 +2089,7 @@ let bands = (
|
|
|
1950
2089
|
};
|
|
1951
2090
|
/** @type {(def: any, path: string, defs: any[], index: number, detail: any, columned?: boolean) => Band} */
|
|
1952
2091
|
let compileGroup = (def, path, defs, index, detail, columned) => {
|
|
1953
|
-
keys(
|
|
1954
|
-
def,
|
|
1955
|
-
[
|
|
1956
|
-
"name",
|
|
1957
|
-
"by",
|
|
1958
|
-
"label",
|
|
1959
|
-
"collapsed",
|
|
1960
|
-
"break",
|
|
1961
|
-
"reset",
|
|
1962
|
-
"columns",
|
|
1963
|
-
"sort",
|
|
1964
|
-
"take",
|
|
1965
|
-
"aggregates",
|
|
1966
|
-
"run",
|
|
1967
|
-
"header",
|
|
1968
|
-
"footer",
|
|
1969
|
-
],
|
|
1970
|
-
path,
|
|
1971
|
-
);
|
|
2092
|
+
keys(def, KEY_SETS.group, path);
|
|
1972
2093
|
named(def.name, path + ".name", RESERVED);
|
|
1973
2094
|
noteGroup(def.name, path + ".name");
|
|
1974
2095
|
let by = expression(def.by, path + ".by");
|
|
@@ -2101,7 +2222,7 @@ let reportHeaderOf = (value, keys, itemsOf, bad) => {
|
|
|
2101
2222
|
bad("header", "expected an array of items or { height, items }");
|
|
2102
2223
|
return { items: itemsOf(null, "header", "report-header"), height: null };
|
|
2103
2224
|
}
|
|
2104
|
-
keys(value,
|
|
2225
|
+
keys(value, KEY_SETS.reportHeader, "header");
|
|
2105
2226
|
return {
|
|
2106
2227
|
items: itemsOf(value.items, "header.items", "report-header"),
|
|
2107
2228
|
height: headerHeightOf(value.height, bad),
|
|
@@ -2118,7 +2239,7 @@ let pageMarginOf = (margin, bad) => {
|
|
|
2118
2239
|
let pageBands = (schema, obj, keys, itemsOf, bad) => {
|
|
2119
2240
|
if (schema.page == null || !obj(schema.page, "page"))
|
|
2120
2241
|
return { pageHeader: null, pageFooter: null, margin: null };
|
|
2121
|
-
keys(schema.page,
|
|
2242
|
+
keys(schema.page, KEY_SETS.page, "page");
|
|
2122
2243
|
return {
|
|
2123
2244
|
pageHeader: maybeItems(schema.page.header, "page.header", "page-header", itemsOf),
|
|
2124
2245
|
pageFooter: maybeItems(schema.page.footer, "page.footer", "page-footer", itemsOf),
|
|
@@ -2148,7 +2269,7 @@ let needPinnedMargin = (headerHeight, schema, bad) => {
|
|
|
2148
2269
|
* render may traverse, never what this descent reports.
|
|
2149
2270
|
*/
|
|
2150
2271
|
let traverse = (
|
|
2151
|
-
{ add, bad, attempt, pin, runNames },
|
|
2272
|
+
{ add, bad, attempt, pin, runNames, slotted },
|
|
2152
2273
|
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf, stylesOf },
|
|
2153
2274
|
{ itemsOf },
|
|
2154
2275
|
{ bandOf },
|
|
@@ -2159,28 +2280,7 @@ let traverse = (
|
|
|
2159
2280
|
add("schema: expected an object");
|
|
2160
2281
|
return null;
|
|
2161
2282
|
}
|
|
2162
|
-
keys(
|
|
2163
|
-
schema,
|
|
2164
|
-
[
|
|
2165
|
-
"params",
|
|
2166
|
-
"required",
|
|
2167
|
-
"data",
|
|
2168
|
-
"where",
|
|
2169
|
-
"sort",
|
|
2170
|
-
"take",
|
|
2171
|
-
"aggregates",
|
|
2172
|
-
"run",
|
|
2173
|
-
"style",
|
|
2174
|
-
"header",
|
|
2175
|
-
"empty",
|
|
2176
|
-
"columns",
|
|
2177
|
-
"groups",
|
|
2178
|
-
"detail",
|
|
2179
|
-
"footer",
|
|
2180
|
-
"page",
|
|
2181
|
-
],
|
|
2182
|
-
"",
|
|
2183
|
-
);
|
|
2283
|
+
keys(schema, KEY_SETS.report, "");
|
|
2184
2284
|
// Frozen either way, so `$.params` reads the same whether or not a report
|
|
2185
2285
|
// declares any. A count here columns every group inside it, so the band
|
|
2186
2286
|
// chain below carries the verdict down as `enclosed`.
|
|
@@ -2234,6 +2334,7 @@ let traverse = (
|
|
|
2234
2334
|
pageFooter,
|
|
2235
2335
|
margin,
|
|
2236
2336
|
required,
|
|
2337
|
+
slotted,
|
|
2237
2338
|
};
|
|
2238
2339
|
};
|
|
2239
2340
|
|
package/lib/scope.js
CHANGED
|
@@ -17,6 +17,13 @@ import { REDUCERS, runnerFor } from "./reducers.js";
|
|
|
17
17
|
* bind: (scope: Scope, row: any) => Scope }} RunnerSet
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
// Where a render parks the values its resolve wave settled: a Map from row to
|
|
21
|
+
// that row's supplies, keyed by cell path (docs/adr/0088). It sits on the
|
|
22
|
+
// render base, so every scope under it inherits the one object and a cell that
|
|
23
|
+
// holds no slot never looks. A symbol, so it cannot collide with an author's
|
|
24
|
+
// name and never reaches an expression.
|
|
25
|
+
export let RESOLVED = Symbol("quario.resolved");
|
|
26
|
+
|
|
20
27
|
// Bind a row as `@` on a child of `scope`, ready for an xprsn per-row read.
|
|
21
28
|
/** @type {(scope: Scope, row: any) => Scope} */
|
|
22
29
|
export let withRow = (scope, row) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quario",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Tiny, CSP-safe report engine for JavaScript. One JSON document renders to HTML, PDF, XLSX, CSV and Word.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"padvinder": "^0.9.0",
|
|
46
|
-
"sjabloon": "^0.
|
|
47
|
-
"xprsn": "^0.
|
|
46
|
+
"sjabloon": "^0.14.0",
|
|
47
|
+
"xprsn": "^0.12.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
51
|
-
"@size-limit/preset-small-lib": "^13.
|
|
51
|
+
"@size-limit/preset-small-lib": "^13.1.1",
|
|
52
52
|
"size-limit": "^13.0.3",
|
|
53
53
|
"typescript": "^7.0.2"
|
|
54
54
|
},
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"sjabloon",
|
|
61
61
|
"padvinder"
|
|
62
62
|
],
|
|
63
|
-
"limit": "
|
|
63
|
+
"limit": "16.2 kB"
|
|
64
64
|
}
|
|
65
65
|
],
|
|
66
66
|
"engines": {
|