quario 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +435 -0
- package/README.md +49 -25
- package/lib/band-height.js +123 -0
- package/lib/format.js +140 -32
- package/lib/index.d.ts +209 -21
- package/lib/index.js +13 -8
- package/lib/license.js +1 -1
- package/lib/locate.js +32 -0
- package/lib/math.js +71 -0
- package/lib/plan.js +485 -184
- package/lib/precision.js +75 -0
- package/lib/reducers.js +165 -0
- package/lib/scope.js +7 -86
- package/lib/stream.js +20 -15
- package/lib/style.js +485 -20
- package/package.json +2 -2
package/lib/plan.js
CHANGED
|
@@ -43,9 +43,12 @@ import {
|
|
|
43
43
|
relocate as relocateTemplate,
|
|
44
44
|
template,
|
|
45
45
|
} from "sjabloon";
|
|
46
|
+
import { pinGuard } from "./band-height.js";
|
|
46
47
|
import { LOCATION, fault, isDiagnostic, locate } from "./locate.js";
|
|
47
48
|
import { ANCHORS, BLOCKED, NAME, RESERVED, record } from "./names.js";
|
|
48
|
-
import {
|
|
49
|
+
import { MATH } from "./math.js";
|
|
50
|
+
import { REDUCERS, reducerFor } from "./reducers.js";
|
|
51
|
+
import { aggregateValue, withRow } from "./scope.js";
|
|
49
52
|
import { opt, sniff } from "./stream.js";
|
|
50
53
|
import {
|
|
51
54
|
checkBorderSides,
|
|
@@ -54,8 +57,18 @@ import {
|
|
|
54
57
|
checkReportStyle,
|
|
55
58
|
checkRowStyle,
|
|
56
59
|
checkSlotImageStyle,
|
|
60
|
+
checkItemStyle,
|
|
57
61
|
checkSlotStyle,
|
|
58
62
|
checkStyle,
|
|
63
|
+
boxReaching,
|
|
64
|
+
coveredNames,
|
|
65
|
+
fanBox,
|
|
66
|
+
guarded,
|
|
67
|
+
isBoxName,
|
|
68
|
+
isExpr,
|
|
69
|
+
positivePts,
|
|
70
|
+
resolveFormat,
|
|
71
|
+
SKIP,
|
|
59
72
|
} from "./style.js";
|
|
60
73
|
|
|
61
74
|
// Tolerance on the column-width sum. Widths are literal numbers an author may
|
|
@@ -67,13 +80,46 @@ let WIDTH_EPS = 1e-9;
|
|
|
67
80
|
// plan rather than riding the state a plan accumulates.
|
|
68
81
|
/** @type {any} */
|
|
69
82
|
let NIL = () => null;
|
|
83
|
+
// The two halves a table row's `style` compiles to, and the pair a row that
|
|
84
|
+
// declares none wears.
|
|
85
|
+
/** @typedef {{ box: (scope: Scope) => any, names: string[], rest: (scope: Scope) => any }} RowStyles */
|
|
86
|
+
/** @typedef {{ styles: RowStyles, visible: any, cells: any[], fan: (string[] | null)[] }} TotalRow */
|
|
87
|
+
/** @typedef {{ visible: any, rows: readonly TotalRow[] }} TotalBlock */
|
|
88
|
+
// One declaration compiled: its name, what it resolves to, and whether that
|
|
89
|
+
// defers to render. The third is what lets two blocks compose without the
|
|
90
|
+
// documents they came from -- see `underRow`.
|
|
91
|
+
/** @typedef {[string, Eval, boolean]} StyleEntry */
|
|
92
|
+
/** @typedef {{ box: StyleEntry[], rest: StyleEntry[] }} RowEntries */
|
|
93
|
+
/** @type {RowEntries} */
|
|
94
|
+
let NO_ROW_ENTRIES = { box: [], rest: [] };
|
|
95
|
+
/** @type {RowStyles} */
|
|
96
|
+
let NO_ROW_STYLE = { box: NIL, names: [], rest: NIL };
|
|
97
|
+
// A cell that declares no readable style block: nothing to resolve, and no
|
|
98
|
+
// box name to keep any of its row's box off it.
|
|
99
|
+
let NO_CELL_LOOK = { styles: NIL, box: /** @type {string[]} */ ([]) };
|
|
100
|
+
// An absent `total` is the block with no rows. `rows: []` is a definition
|
|
101
|
+
// error, so an empty one can only have come from absence and nothing has to
|
|
102
|
+
// tell the two apart downstream.
|
|
103
|
+
/** @type {TotalBlock} */
|
|
104
|
+
let NO_TOTALS = Object.freeze({ visible: null, rows: Object.freeze([]) });
|
|
70
105
|
/** @type {any} */
|
|
71
106
|
let EMPTY = () => [];
|
|
72
107
|
// What `$.params` reads when a report declares none.
|
|
73
108
|
let NONE = Object.freeze({});
|
|
74
109
|
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
// How many table columns one cell covers. A literal, for the same reason a
|
|
111
|
+
// column `width` is: the column geometry is allocated before any row is laid
|
|
112
|
+
// out, so it cannot depend on one. Absent is 1, and an unreadable span reads
|
|
113
|
+
// as 1 so the coverage walk above it reports where the author actually went
|
|
114
|
+
// wrong instead of cascading. `spanned` is the emit side: a span of 1 is the
|
|
115
|
+
// absence of one, and never reaches the stream.
|
|
116
|
+
/** @type {(span: any) => number} */
|
|
117
|
+
let spanValue = (span) => (Number.isInteger(span) && span >= 1 ? span : 1);
|
|
118
|
+
/** @type {(span: any) => boolean} */
|
|
119
|
+
let validSpan = (span) => span == null || spanValue(span) === span;
|
|
120
|
+
/** @type {(span: number) => number} */
|
|
121
|
+
let spanned = (span) => (span > 1 ? span : 0);
|
|
122
|
+
|
|
77
123
|
/** @type {(from: Iterable<string>, into: Set<string>) => void} */
|
|
78
124
|
let collect = (from, into) => {
|
|
79
125
|
for (let name of from) into.add(name);
|
|
@@ -204,6 +250,7 @@ let jsonCopy = (value, path, bad, seen) => {
|
|
|
204
250
|
/** @typedef {import("./scope.js").Scope} Scope */
|
|
205
251
|
/** @typedef {import("./scope.js").Eval} Eval */
|
|
206
252
|
/** @typedef {import("./scope.js").Fold} Fold */
|
|
253
|
+
/** @typedef {import("./reducers.js").Role} Role */
|
|
207
254
|
/** @typedef {import("./scope.js").RunnerSet} RunnerSet */
|
|
208
255
|
/** @typedef {(rows: any[], scope: Scope, runners: RunnerSet) => Generator<any>} Band */
|
|
209
256
|
/** @typedef {ReturnType<typeof planState>} State */
|
|
@@ -240,7 +287,10 @@ let bindHandles = (schema, bound) => {
|
|
|
240
287
|
*/
|
|
241
288
|
let planState = (schema, funcs) => {
|
|
242
289
|
/** @type {Record<string, (...args: any[]) => any>} */
|
|
243
|
-
|
|
290
|
+
// Built-in functions first, the host's registry last: a `funcs` entry of the
|
|
291
|
+
// same name shadows a built-in, which is the rule SCHEMA.md states for the
|
|
292
|
+
// reducers and which the scalar built-ins join rather than complicate.
|
|
293
|
+
let FNS = /** @type {any} */ ({ ...REDUCERS, ...MATH, ...funcs });
|
|
244
294
|
/** @type {Set<string>} */
|
|
245
295
|
let names = new Set();
|
|
246
296
|
/** @type {Set<string>} */
|
|
@@ -333,6 +383,10 @@ let planState = (schema, funcs) => {
|
|
|
333
383
|
add,
|
|
334
384
|
bad,
|
|
335
385
|
attempt,
|
|
386
|
+
// The band-height rule, carried by the descent rather than walked
|
|
387
|
+
// separately: `traverse` arms it from the report header and offers it each
|
|
388
|
+
// band below (./band-height.js).
|
|
389
|
+
pin: pinGuard(bad),
|
|
336
390
|
diagnostic: () => firstDiag,
|
|
337
391
|
};
|
|
338
392
|
};
|
|
@@ -382,10 +436,14 @@ let splitFold = (spec) => {
|
|
|
382
436
|
if (colon < 0) return { fn: spec, src: "", colon };
|
|
383
437
|
return { fn: spec.slice(0, colon), src: spec.slice(colon + 1), colon };
|
|
384
438
|
};
|
|
385
|
-
|
|
386
|
-
|
|
439
|
+
// The two spellings a reducer's declared form takes, one per `expr` flag on its
|
|
440
|
+
// registry entry — `checkBareFold` for a false one, `checkExprFold` for a true.
|
|
441
|
+
// Neither names a reducer: `count` is only the entry that happens to carry
|
|
442
|
+
// `expr: false` today, and another would be refused the same way.
|
|
443
|
+
/** @type {(colon: number, fn: string, path: string) => void} */
|
|
444
|
+
let checkBareFold = (colon, fn, path) => {
|
|
387
445
|
// oxlint-disable-next-line no-unused-expressions
|
|
388
|
-
colon < 0 || fault(path, "
|
|
446
|
+
colon < 0 || fault(path, fn + " does not take an expression");
|
|
389
447
|
};
|
|
390
448
|
/** @type {(src: string, fn: string, path: string) => void} */
|
|
391
449
|
let checkExprFold = (src, fn, path) => {
|
|
@@ -427,15 +485,17 @@ let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
|
427
485
|
return runCell(tokens, path, source);
|
|
428
486
|
};
|
|
429
487
|
|
|
430
|
-
// Parse "reducer:=expr" into a reducer name and a compiled per-row expression
|
|
431
|
-
//
|
|
432
|
-
/** @type {(spec: string, path: string,
|
|
433
|
-
let parseFold = (spec, path,
|
|
488
|
+
// Parse "reducer:=expr" into a reducer name and a compiled per-row expression,
|
|
489
|
+
// null where the entry's `expr` flag says the form carries none.
|
|
490
|
+
/** @type {(spec: string, path: string, role: Role) => Fold} */
|
|
491
|
+
let parseFold = (spec, path, role) => {
|
|
434
492
|
let { fn, src, colon } = splitFold(spec);
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
493
|
+
// The role decides the vocabulary: a reducer with no whole-array fold is a
|
|
494
|
+
// running value only, so `prev` is an unknown reducer under `aggregates`.
|
|
495
|
+
let reducer = reducerFor(role, fn);
|
|
496
|
+
if (!reducer) fault(path, 'unknown reducer "' + fn + '"');
|
|
497
|
+
if (reducer.expr) checkExprFold(src, fn, path);
|
|
498
|
+
else checkBareFold(colon, fn, path);
|
|
439
499
|
return { fn, per: src ? prop(src, path) : null };
|
|
440
500
|
};
|
|
441
501
|
|
|
@@ -451,7 +511,7 @@ let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
|
451
511
|
* @param {State} state
|
|
452
512
|
* @param {Primitives} primitives
|
|
453
513
|
*/
|
|
454
|
-
let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
514
|
+
let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ options) => {
|
|
455
515
|
/** @type {(value: any, path: string) => boolean} */
|
|
456
516
|
let obj = (value, path) => {
|
|
457
517
|
if (record(value)) return true;
|
|
@@ -544,51 +604,152 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
544
604
|
// CSS). An all-literal block folds to one frozen object at compile time.
|
|
545
605
|
/**
|
|
546
606
|
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
547
|
-
* => [
|
|
607
|
+
* => StyleEntry[]}
|
|
548
608
|
*/
|
|
549
609
|
let readStyles = (block, path, check) => {
|
|
550
|
-
/** @type {[
|
|
610
|
+
/** @type {StyleEntry[]} */
|
|
551
611
|
let entries = [];
|
|
552
612
|
for (let [name, value] of Object.entries(block)) {
|
|
553
613
|
let stylePath = path + "." + name;
|
|
554
614
|
let problem = check(name, value);
|
|
555
615
|
if (problem) bad(stylePath, problem);
|
|
556
|
-
else
|
|
616
|
+
else {
|
|
617
|
+
// A literal has already passed `check` above and can never fail at
|
|
618
|
+
// render, so only an `=` result is wrapped in the rule its name
|
|
619
|
+
// answers to. `style.js` owns which rule that is (docs/adr/0055).
|
|
620
|
+
let resolve = attempt(() => prop(value, stylePath), NIL);
|
|
621
|
+
let expr = isExpr(value);
|
|
622
|
+
entries.push([name, expr ? guarded(name, resolve) : resolve, expr]);
|
|
623
|
+
}
|
|
557
624
|
}
|
|
558
625
|
return entries;
|
|
559
626
|
};
|
|
560
|
-
|
|
627
|
+
// The names are written in declaration order, which the stream promises, so
|
|
628
|
+
// a guarded name that answers is written where it was declared rather than
|
|
629
|
+
// after the ones that never needed guarding.
|
|
630
|
+
//
|
|
631
|
+
// `format` resolves after the loop rather than inside it, and the order is
|
|
632
|
+
// what makes that necessary: a `currency` kind takes its digit count from
|
|
633
|
+
// the code the cell wears, so the code has to have been written -- or
|
|
634
|
+
// voided by `guarded` -- before the kind can be resolved over it. Doing it
|
|
635
|
+
// per entry would make one declaration depend on where another sits in the
|
|
636
|
+
// block. The instance's options come with it, for the cell that declares no
|
|
637
|
+
// code of its own and takes the instance's (docs/adr/0056).
|
|
638
|
+
/** @type {(entries: StyleEntry[]) => (scope: Scope) => Scope} */
|
|
561
639
|
let styleFn = (entries) => (scope) => {
|
|
562
640
|
/** @type {Scope} */
|
|
563
641
|
let style = {};
|
|
564
|
-
for (let [name, value] of entries)
|
|
565
|
-
|
|
642
|
+
for (let [name, value] of entries) {
|
|
643
|
+
let resolved = value(scope);
|
|
644
|
+
if (resolved !== SKIP) style[name] = resolved;
|
|
645
|
+
}
|
|
646
|
+
return resolveFormat(style, options);
|
|
566
647
|
};
|
|
567
|
-
/** @type {(block: any) => boolean} */
|
|
568
|
-
let isComputed = (block) => Object.values(block).some(isExpr);
|
|
569
648
|
/**
|
|
570
649
|
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
571
650
|
* => (scope: Scope) => any}
|
|
572
651
|
*/
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
652
|
+
// A compiled block, constant-folded when nothing in it defers to render, so
|
|
653
|
+
// a literal style is resolved once and every node that wears it is handed
|
|
654
|
+
// the same frozen object.
|
|
655
|
+
/** @type {(entries: StyleEntry[]) => (scope: Scope) => any} */
|
|
656
|
+
let folded = (entries) => {
|
|
576
657
|
if (!entries.length) return NIL;
|
|
577
658
|
let resolve = styleFn(entries);
|
|
578
|
-
if (
|
|
659
|
+
if (entries.some((entry) => entry[2])) return resolve;
|
|
579
660
|
let constant = Object.freeze(resolve({}));
|
|
580
661
|
return () => constant;
|
|
581
662
|
};
|
|
582
663
|
/**
|
|
583
|
-
* @type {(
|
|
664
|
+
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
665
|
+
* => StyleEntry[]}
|
|
666
|
+
*/
|
|
667
|
+
let styleEntries = (block, path, check) => {
|
|
668
|
+
let entries = readStyles(block, path, check);
|
|
669
|
+
for (let [name, msg] of checkBorderSides(block, check)) bad(path + "." + name, msg);
|
|
670
|
+
return entries;
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
584
674
|
* => (scope: Scope) => any}
|
|
585
675
|
*/
|
|
586
|
-
|
|
676
|
+
// A table row's block is split where it is compiled rather than at every
|
|
677
|
+
// row: its box half resolves onto the row's cells and the rest stays on the
|
|
678
|
+
// row (SCHEMA.md, "Style declarations"). Both halves fold like any other
|
|
679
|
+
// block, so a literal row style is partitioned once per table.
|
|
680
|
+
/** @type {(def: any, path: string) => RowEntries} */
|
|
681
|
+
let rowEntriesOf = (def, path) => {
|
|
682
|
+
let block = def.style;
|
|
683
|
+
let stylePath = pathTo(path, "style");
|
|
684
|
+
if (block == null || !obj(block, stylePath)) return NO_ROW_ENTRIES;
|
|
685
|
+
let entries = styleEntries(block, stylePath, checkRowStyle);
|
|
686
|
+
return {
|
|
687
|
+
box: entries.filter(([name]) => isBoxName(name)),
|
|
688
|
+
rest: entries.filter(([name]) => !isBoxName(name)),
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* One row block under another: the totals block's under a total row's. The
|
|
693
|
+
* row's names win, and its box takes any border side it speaks for whole --
|
|
694
|
+
* the same rule a cell applies over a row, settled here on the names rather
|
|
695
|
+
* than at render on the values, so the block leaves no third level behind
|
|
696
|
+
* (docs/adr/0049). A row that composes to all-literal folds to one frozen
|
|
697
|
+
* object like any other block.
|
|
698
|
+
*
|
|
699
|
+
* @type {(under: RowEntries, over: RowEntries) => RowEntries}
|
|
700
|
+
*/
|
|
701
|
+
let underRow = (under, over) => {
|
|
702
|
+
if (!under.box.length && !under.rest.length) return over;
|
|
703
|
+
let box = coveredNames(over.box.map(([name]) => name));
|
|
704
|
+
let rest = new Set(over.rest.map(([name]) => name));
|
|
705
|
+
return {
|
|
706
|
+
box: [...under.box.filter(([name]) => !box.has(name)), ...over.box],
|
|
707
|
+
rest: [...under.rest.filter(([name]) => !rest.has(name)), ...over.rest],
|
|
708
|
+
};
|
|
709
|
+
};
|
|
710
|
+
/** @type {(entries: RowEntries) => RowStyles} */
|
|
711
|
+
let foldRow = ({ box, rest }) => ({
|
|
712
|
+
box: folded(box),
|
|
713
|
+
names: box.map(([name]) => name),
|
|
714
|
+
rest: folded(rest),
|
|
715
|
+
});
|
|
716
|
+
// The entries a node's `style` compiles to, or none when it declares no
|
|
717
|
+
// readable block. Every caller folds them; a table cell keeps their box
|
|
718
|
+
// names first.
|
|
719
|
+
/**
|
|
720
|
+
* @type {(def: any, path: string, check: (name: string, value: any) => string | null)
|
|
721
|
+
* => StyleEntry[] | null}
|
|
722
|
+
*/
|
|
723
|
+
let entriesOf = (def, path, check) => {
|
|
587
724
|
let block = def.style;
|
|
588
725
|
let stylePath = pathTo(path, "style");
|
|
589
|
-
if (block == null || !obj(block, stylePath)) return
|
|
590
|
-
return
|
|
726
|
+
if (block == null || !obj(block, stylePath)) return null;
|
|
727
|
+
return styleEntries(block, stylePath, check);
|
|
591
728
|
};
|
|
729
|
+
/**
|
|
730
|
+
* @type {(def: any, path: string, check?: (name: string, value: any) => string | null)
|
|
731
|
+
* => (scope: Scope) => any}
|
|
732
|
+
*/
|
|
733
|
+
let stylesOf = (def, path, check = checkStyle) => {
|
|
734
|
+
let entries = entriesOf(def, path, check);
|
|
735
|
+
return entries ? folded(entries) : NIL;
|
|
736
|
+
};
|
|
737
|
+
// A cell's compiled style with the box names it declared kept beside it.
|
|
738
|
+
// Those names are what settles which of its row's box reaches it -- the
|
|
739
|
+
// question `layer` used to ask of the resolved object at render, asked once
|
|
740
|
+
// here instead (docs/adr/0056).
|
|
741
|
+
/** @type {(def: any, path: string) => { styles: (scope: Scope) => any, box: string[] }} */
|
|
742
|
+
let cellLook = (def, path) => {
|
|
743
|
+
let entries = entriesOf(def, path, checkCellStyle);
|
|
744
|
+
if (!entries) return NO_CELL_LOOK;
|
|
745
|
+
return {
|
|
746
|
+
styles: folded(entries),
|
|
747
|
+
box: entries.filter(([name]) => isBoxName(name)).map(([name]) => name),
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
/** The same, for the blocks that sit on a table row. */
|
|
751
|
+
/** @type {(def: any, path: string) => RowStyles} */
|
|
752
|
+
let rowStylesOf = (def, path) => foldRow(rowEntriesOf(def, path));
|
|
592
753
|
// A cell value: one sjabloon template. Emphasis is the cell's own `style`;
|
|
593
754
|
// markup in literal text is meaningful only to the HTML target.
|
|
594
755
|
/** @type {(value: any, path: string) => (scope: Scope, opts?: any) => any} */
|
|
@@ -603,43 +764,43 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
603
764
|
if (seen.has(name)) bad(foldPath, 'duplicate run name "' + name + '"');
|
|
604
765
|
else seen.add(name);
|
|
605
766
|
};
|
|
606
|
-
/** @type {(spec: any, foldPath: string,
|
|
607
|
-
let readFold = (spec, foldPath,
|
|
767
|
+
/** @type {(spec: any, foldPath: string, role: Role) => Fold | null} */
|
|
768
|
+
let readFold = (spec, foldPath, role) => {
|
|
608
769
|
if (typeof spec !== "string") {
|
|
609
770
|
bad(foldPath, "expected a reducer specification");
|
|
610
771
|
return null;
|
|
611
772
|
}
|
|
612
|
-
return attempt(() => parseFold(spec, foldPath,
|
|
773
|
+
return attempt(() => parseFold(spec, foldPath, role), null);
|
|
613
774
|
};
|
|
614
|
-
/** @type {(name: string, spec: any, path: string,
|
|
615
|
-
let oneFold = (name, spec, path,
|
|
775
|
+
/** @type {(name: string, spec: any, path: string, role: Role, reserved: Set<string> | null, seen?: Set<string>) => [string, Fold] | null} */
|
|
776
|
+
let oneFold = (name, spec, path, role, reserved, seen) => {
|
|
616
777
|
let foldPath = path + "." + name;
|
|
617
778
|
named(name, foldPath, reserved);
|
|
618
779
|
noteRun(seen, name, foldPath);
|
|
619
|
-
let fold = readFold(spec, foldPath,
|
|
780
|
+
let fold = readFold(spec, foldPath, role);
|
|
620
781
|
return fold && [name, fold];
|
|
621
782
|
};
|
|
622
783
|
/**
|
|
623
|
-
* @type {(value: any, path: string,
|
|
784
|
+
* @type {(value: any, path: string, role: Role,
|
|
624
785
|
* reserved: Set<string> | null, seen?: Set<string>) => [string, Fold][]}
|
|
625
786
|
*/
|
|
626
|
-
let readFolds = (value, path,
|
|
787
|
+
let readFolds = (value, path, role, reserved, seen) => {
|
|
627
788
|
/** @type {[string, Fold][]} */
|
|
628
789
|
let out = [];
|
|
629
790
|
for (let [name, spec] of Object.entries(value)) {
|
|
630
|
-
let fold = oneFold(name, spec, path,
|
|
791
|
+
let fold = oneFold(name, spec, path, role, reserved, seen);
|
|
631
792
|
if (fold) out.push(fold);
|
|
632
793
|
}
|
|
633
794
|
return out;
|
|
634
795
|
};
|
|
635
796
|
// Declared aggregates and running values, both spelled "reducer:=expr".
|
|
636
797
|
/**
|
|
637
|
-
* @type {(value: any, path: string,
|
|
798
|
+
* @type {(value: any, path: string, role: Role,
|
|
638
799
|
* reserved: Set<string> | null, seen?: Set<string>) => [string, Fold][]}
|
|
639
800
|
*/
|
|
640
|
-
let foldsOf = (value, path,
|
|
801
|
+
let foldsOf = (value, path, role, reserved, seen) => {
|
|
641
802
|
if (value == null || !obj(value, path)) return [];
|
|
642
|
-
return readFolds(value, path,
|
|
803
|
+
return readFolds(value, path, role, reserved, seen);
|
|
643
804
|
};
|
|
644
805
|
/** @type {(dir: any, path: string) => void} */
|
|
645
806
|
let checkDir = (dir, path) => {
|
|
@@ -703,6 +864,11 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
703
864
|
columnsOf,
|
|
704
865
|
takeOf,
|
|
705
866
|
stylesOf,
|
|
867
|
+
cellLook,
|
|
868
|
+
rowStylesOf,
|
|
869
|
+
rowEntriesOf,
|
|
870
|
+
underRow,
|
|
871
|
+
foldRow,
|
|
706
872
|
cellValue,
|
|
707
873
|
foldsOf,
|
|
708
874
|
sortOf,
|
|
@@ -718,7 +884,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
718
884
|
* @param {State} state
|
|
719
885
|
* @param {Readers} readers
|
|
720
886
|
*/
|
|
721
|
-
let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, stylesOf }) => {
|
|
887
|
+
let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, stylesOf, cellLook }) => {
|
|
722
888
|
// `tokens` plus its truthy `style`. A hidden cell keeps its slot with no
|
|
723
889
|
// tokens so a column keeps its alignment; items pass `visible` null and drop
|
|
724
890
|
// themselves instead, having no slot to keep. The anchor story lives on
|
|
@@ -731,24 +897,65 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
731
897
|
opt({ tokens: hidden(visible, scope) ? [] : tpl(scope) }, { style: styles(scope) });
|
|
732
898
|
|
|
733
899
|
// A table cell: the shape of a total cell and of an object column header.
|
|
734
|
-
/**
|
|
900
|
+
/**
|
|
901
|
+
* @type {(def: any, path: string, valuePath?: string) =>
|
|
902
|
+
* { cell: (scope: Scope) => any, box: string[] }}
|
|
903
|
+
*/
|
|
735
904
|
let cellOf = (def, path, valuePath = path + ".value") => {
|
|
736
905
|
let tpl = cellValue(def.value, valuePath);
|
|
737
906
|
let visible = visibleOf(def, path);
|
|
738
|
-
|
|
907
|
+
let look = cellLook(def, path);
|
|
908
|
+
return { cell: cellShape(tpl, visible, look.styles), box: look.box };
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
/** @type {(def: any, path: string) => number} */
|
|
912
|
+
let spanOf = (def, path) => {
|
|
913
|
+
if (validSpan(def.span)) return spanValue(def.span);
|
|
914
|
+
bad(path + ".span", "expected a positive integer (span >= 1)");
|
|
915
|
+
return 1;
|
|
739
916
|
};
|
|
740
917
|
|
|
741
918
|
// A cell without a row, so `@` stays unbound. A bare string is shorthand for
|
|
742
919
|
// `{ value }` and keeps its authored path, so problems locate where written.
|
|
743
|
-
|
|
920
|
+
// A span needs the object form -- a bare string has nowhere to carry one.
|
|
921
|
+
// Absent is a problem here: a column that legitimately declares no header is
|
|
922
|
+
// one a neighbour's span covers, and `headerSlot` answers that before this
|
|
923
|
+
// is reached.
|
|
924
|
+
/**
|
|
925
|
+
* @type {(def: any, path: string) =>
|
|
926
|
+
* { cell: (scope: Scope) => any, span: number, box: string[] }}
|
|
927
|
+
*/
|
|
744
928
|
let headerOf = (def, path) => {
|
|
745
|
-
if (typeof def === "string")
|
|
929
|
+
if (typeof def === "string")
|
|
930
|
+
return { cell: cellShape(cellValue(def, path), null, NIL), span: 1, box: NO_CELL_LOOK.box };
|
|
746
931
|
if (!record(def)) {
|
|
747
932
|
bad(path, "expected a template string or object");
|
|
748
|
-
return cellShape(EMPTY, null, NIL);
|
|
933
|
+
return { cell: cellShape(EMPTY, null, NIL), span: 1, box: NO_CELL_LOOK.box };
|
|
749
934
|
}
|
|
750
|
-
keys(def, ["value", "style"], path);
|
|
751
|
-
return cellOf(def, path);
|
|
935
|
+
keys(def, ["value", "style", "span"], path);
|
|
936
|
+
return { ...cellOf(def, path), span: spanOf(def, path) };
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// A column a neighbour's span covers declares no header of its own. One that
|
|
940
|
+
// declares one anyway is the overlap half of the coverage rule.
|
|
941
|
+
/** @type {(def: any, path: string) => null} */
|
|
942
|
+
let covered = (def, path) => {
|
|
943
|
+
if (def != null) bad(path, "covered by an earlier header's span");
|
|
944
|
+
return null;
|
|
945
|
+
};
|
|
946
|
+
// The per-column half of the coverage rule. `headerCover` decides which slot
|
|
947
|
+
// a column is in; this reports it, in the column's own turn, so header
|
|
948
|
+
// problems still come out in the documented key order.
|
|
949
|
+
/**
|
|
950
|
+
* @type {(def: any, path: string, slot: string) =>
|
|
951
|
+
* { cell: (scope: Scope) => any, span: number, box: string[] } | null}
|
|
952
|
+
*/
|
|
953
|
+
let headerSlot = (def, path, slot) => {
|
|
954
|
+
if (slot === "unreached") return null;
|
|
955
|
+
if (slot === "covered") return covered(def, path);
|
|
956
|
+
let header = headerOf(def, path);
|
|
957
|
+
if (slot === "over") bad(path + ".span", "reaches past the last column");
|
|
958
|
+
return header;
|
|
752
959
|
};
|
|
753
960
|
|
|
754
961
|
/** @type {(width: any) => boolean} */
|
|
@@ -759,25 +966,34 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
759
966
|
bad(path + ".width", "expected a percentage number (0 < width <= 100)");
|
|
760
967
|
return NaN;
|
|
761
968
|
};
|
|
762
|
-
/** @type {(def: any, path: string) => any} */
|
|
763
|
-
let columnOf = (def, path) => {
|
|
969
|
+
/** @type {(def: any, path: string, slot: string) => any} */
|
|
970
|
+
let columnOf = (def, path, slot) => {
|
|
764
971
|
if (!obj(def, path)) return null;
|
|
765
972
|
keys(def, ["header", "value", "visible", "style", "width"], path);
|
|
766
|
-
let header =
|
|
973
|
+
let header = headerSlot(def.header, path + ".header", slot);
|
|
767
974
|
let tpl = cellValue(def.value, path + ".value");
|
|
768
975
|
let visible = visibleOf(def, path);
|
|
769
|
-
let
|
|
976
|
+
let look = cellLook(def, path);
|
|
770
977
|
// A width is a percentage of the table width, resolved by every target.
|
|
771
978
|
// The range verdict is owned here: an unreadable width comes back as NaN,
|
|
772
979
|
// so the total check below reads the verdict instead of re-deciding it.
|
|
773
|
-
return {
|
|
980
|
+
return {
|
|
981
|
+
header,
|
|
982
|
+
cell: cellShape(tpl, visible, look.styles),
|
|
983
|
+
box: look.box,
|
|
984
|
+
width: widthOf(def.width, path),
|
|
985
|
+
path,
|
|
986
|
+
};
|
|
774
987
|
};
|
|
775
988
|
|
|
776
|
-
/**
|
|
989
|
+
/**
|
|
990
|
+
* @type {(def: any, path: string) =>
|
|
991
|
+
* { cell: (scope: Scope) => any, span: number, box: string[] } | null}
|
|
992
|
+
*/
|
|
777
993
|
let totalOf = (def, path) => {
|
|
778
994
|
if (!obj(def, path)) return null;
|
|
779
|
-
keys(def, ["value", "visible", "style"], path);
|
|
780
|
-
return cellOf(def, path);
|
|
995
|
+
keys(def, ["value", "visible", "style", "span"], path);
|
|
996
|
+
return { ...cellOf(def, path), span: spanOf(def, path) };
|
|
781
997
|
};
|
|
782
998
|
|
|
783
999
|
// A text item: a cell with no slot to keep, so a hidden one drops itself
|
|
@@ -793,7 +1009,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
793
1009
|
let shape = cellShape(
|
|
794
1010
|
tpl,
|
|
795
1011
|
inSlot ? visible : null,
|
|
796
|
-
stylesOf(def, path, inSlot ? checkSlotStyle :
|
|
1012
|
+
stylesOf(def, path, inSlot ? checkSlotStyle : checkItemStyle),
|
|
797
1013
|
);
|
|
798
1014
|
// The cell shape first, then the item's own fields, so a cell reads
|
|
799
1015
|
// the same wherever it appears in the stream.
|
|
@@ -843,7 +1059,14 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
843
1059
|
TypeError("expected a Uint8Array of PNG or JPEG bytes"),
|
|
844
1060
|
);
|
|
845
1061
|
return opt(
|
|
846
|
-
{
|
|
1062
|
+
{
|
|
1063
|
+
type: "image",
|
|
1064
|
+
role: fields.role,
|
|
1065
|
+
bytes,
|
|
1066
|
+
format,
|
|
1067
|
+
fit: fields.fit,
|
|
1068
|
+
path: fields.path,
|
|
1069
|
+
},
|
|
847
1070
|
{ alt: imageAlt(fields.alt, scope), style: fields.styles(scope) },
|
|
848
1071
|
);
|
|
849
1072
|
};
|
|
@@ -941,7 +1164,9 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
941
1164
|
);
|
|
942
1165
|
return (scope) => {
|
|
943
1166
|
if (hidden(visible, scope)) return null;
|
|
944
|
-
let out = [
|
|
1167
|
+
let out = [
|
|
1168
|
+
opt({ type: "split-start", role, path, slots: geometry }, { style: styles(scope) }),
|
|
1169
|
+
];
|
|
945
1170
|
for (let slot of slots) out.push(slot.render(scope));
|
|
946
1171
|
out.push({ type: "split-end" });
|
|
947
1172
|
return out;
|
|
@@ -997,8 +1222,23 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
997
1222
|
* @param {Nodes} nodes
|
|
998
1223
|
*/
|
|
999
1224
|
let bands = (
|
|
1000
|
-
{ bad, runNames },
|
|
1001
|
-
{
|
|
1225
|
+
{ bad, pin, runNames },
|
|
1226
|
+
{
|
|
1227
|
+
arr,
|
|
1228
|
+
obj,
|
|
1229
|
+
keys,
|
|
1230
|
+
named,
|
|
1231
|
+
expression,
|
|
1232
|
+
visibleOf,
|
|
1233
|
+
columnsOf,
|
|
1234
|
+
takeOf,
|
|
1235
|
+
rowStylesOf,
|
|
1236
|
+
rowEntriesOf,
|
|
1237
|
+
underRow,
|
|
1238
|
+
foldRow,
|
|
1239
|
+
foldsOf,
|
|
1240
|
+
sortOf,
|
|
1241
|
+
},
|
|
1002
1242
|
{ columnOf, itemsOf, totalOf },
|
|
1003
1243
|
) => {
|
|
1004
1244
|
// Stacked items, one pass per row.
|
|
@@ -1014,30 +1254,71 @@ let bands = (
|
|
|
1014
1254
|
}
|
|
1015
1255
|
};
|
|
1016
1256
|
|
|
1017
|
-
/** @type {(value: any) => { rowVisible: Eval | null, rowStyles:
|
|
1257
|
+
/** @type {(value: any) => { rowVisible: Eval | null, rowStyles: RowStyles }} */
|
|
1018
1258
|
let rowLook = (value) => {
|
|
1019
1259
|
if (value.row == null || !obj(value.row, "detail.row"))
|
|
1020
|
-
return { rowVisible: null, rowStyles:
|
|
1260
|
+
return { rowVisible: null, rowStyles: NO_ROW_STYLE };
|
|
1021
1261
|
keys(value.row, ["visible", "style"], "detail.row");
|
|
1022
1262
|
return {
|
|
1023
1263
|
rowVisible: visibleOf(value.row, "detail.row"),
|
|
1024
|
-
rowStyles:
|
|
1264
|
+
rowStyles: rowStylesOf(value.row, "detail.row"),
|
|
1025
1265
|
};
|
|
1026
1266
|
};
|
|
1027
1267
|
// The header-row box: `style` only, a different path from each column's
|
|
1028
1268
|
// `header` cell. Absent when the table declares none.
|
|
1029
|
-
/** @type {(value: any) =>
|
|
1269
|
+
/** @type {(value: any) => RowStyles} */
|
|
1030
1270
|
let headerLook = (value) => {
|
|
1031
|
-
if (value.header == null || !obj(value.header, "detail.header")) return
|
|
1271
|
+
if (value.header == null || !obj(value.header, "detail.header")) return NO_ROW_STYLE;
|
|
1032
1272
|
keys(value.header, ["style"], "detail.header");
|
|
1033
|
-
return
|
|
1273
|
+
return rowStylesOf(value.header, "detail.header");
|
|
1274
|
+
};
|
|
1275
|
+
// Which slot each column's header sits in -- the same invariant a total row's
|
|
1276
|
+
// cells carry, walked positionally rather than summed because the diagnostic
|
|
1277
|
+
// has to name the column it broke at. Read off the raw definitions, before
|
|
1278
|
+
// any column is parsed, so `columnOf` can report its verdict in the column's
|
|
1279
|
+
// own turn. Spans are read leniently: an unreadable one covers a single
|
|
1280
|
+
// column and gets its own diagnostic when the column is parsed.
|
|
1281
|
+
//
|
|
1282
|
+
// `own` fills its own slot, `covered` is filled by a neighbour's span, `over`
|
|
1283
|
+
// reaches past the last column, and everything after an `over` is
|
|
1284
|
+
// `unreached` -- one broken span should not also report every column behind
|
|
1285
|
+
// it as headerless.
|
|
1286
|
+
// How many columns a definition's header claims, read leniently: a malformed
|
|
1287
|
+
// column, a missing header and an unreadable span all claim one here, and
|
|
1288
|
+
// each gets its own diagnostic when the column is parsed.
|
|
1289
|
+
/** @type {(def: any) => number} */
|
|
1290
|
+
let headerSpan = (def) => {
|
|
1291
|
+
let header = record(def) ? def.header : null;
|
|
1292
|
+
return record(header) ? spanValue(header.span) : 1;
|
|
1293
|
+
};
|
|
1294
|
+
/** @type {(slots: string[], from: number, to: number, slot: string) => void} */
|
|
1295
|
+
let fill = (slots, from, to, slot) => {
|
|
1296
|
+
for (let at = from; at < to; at++) slots[at] = slot;
|
|
1297
|
+
};
|
|
1298
|
+
/** @type {(defs: any[]) => string[]} */
|
|
1299
|
+
let headerCover = (defs) => {
|
|
1300
|
+
let slots = defs.map(() => "own");
|
|
1301
|
+
let at = 0;
|
|
1302
|
+
while (at < defs.length) {
|
|
1303
|
+
let span = headerSpan(defs[at]);
|
|
1304
|
+
if (at + span > defs.length) {
|
|
1305
|
+
slots[at] = "over";
|
|
1306
|
+
fill(slots, at + 1, defs.length, "unreached");
|
|
1307
|
+
return slots;
|
|
1308
|
+
}
|
|
1309
|
+
fill(slots, at + 1, at + span, "covered");
|
|
1310
|
+
at += span;
|
|
1311
|
+
}
|
|
1312
|
+
return slots;
|
|
1034
1313
|
};
|
|
1035
1314
|
/** @type {(value: any) => { defs: any[] | null, columns: any[] }} */
|
|
1036
1315
|
let tableColumns = (value) => {
|
|
1037
1316
|
let defs = arr(value.columns, "detail.columns", true);
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1317
|
+
let list = defs || [];
|
|
1318
|
+
if (defs && !list.length) bad("detail.columns", "expected at least one column");
|
|
1319
|
+
let slots = headerCover(list);
|
|
1320
|
+
let columns = list
|
|
1321
|
+
.map((def, i) => columnOf(def, "detail.columns[" + i + "]", slots[i]))
|
|
1041
1322
|
.filter((column) => column != null);
|
|
1042
1323
|
checkShares(bad, columns, "detail.columns", "column");
|
|
1043
1324
|
return { defs, columns };
|
|
@@ -1047,100 +1328,134 @@ let bands = (
|
|
|
1047
1328
|
totals
|
|
1048
1329
|
.map((def, i) => {
|
|
1049
1330
|
let path = prefix + "[" + i + "]";
|
|
1050
|
-
let
|
|
1051
|
-
return
|
|
1331
|
+
let total = totalOf(def, path);
|
|
1332
|
+
return total && { cell: total.cell, span: total.span, box: total.box, path };
|
|
1052
1333
|
})
|
|
1053
1334
|
.filter((entry) => entry != null);
|
|
1054
1335
|
/** @type {(rows: any[] | null) => any[]} */
|
|
1055
1336
|
let nonempty = (rows) => {
|
|
1056
|
-
if (rows && !rows.length) bad("detail.total", "expected at least one total row");
|
|
1337
|
+
if (rows && !rows.length) bad("detail.total.rows", "expected at least one total row");
|
|
1057
1338
|
return rows || [];
|
|
1058
1339
|
};
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1340
|
+
// The totals block is the one place a table's rows are authored as a list,
|
|
1341
|
+
// so it is also the one that needs a level above them to say what belongs to
|
|
1342
|
+
// the block rather than to a row: its `style` and `visible` are the header
|
|
1343
|
+
// row's, one level out (docs/adr/0049).
|
|
1344
|
+
/** @type {(value: any, defs: any[] | null) => TotalBlock} */
|
|
1345
|
+
let tableTotals = (value, defs) => {
|
|
1346
|
+
if (value.total == null || !obj(value.total, "detail.total")) return NO_TOTALS;
|
|
1347
|
+
keys(value.total, ["style", "visible", "rows"], "detail.total");
|
|
1348
|
+
let block = rowEntriesOf(value.total, "detail.total");
|
|
1349
|
+
return {
|
|
1350
|
+
visible: visibleOf(value.total, "detail.total"),
|
|
1351
|
+
rows: nonempty(arr(value.total.rows, "detail.total.rows", true)).map((def, r) =>
|
|
1352
|
+
totalRowOf(def, "detail.total.rows[" + r + "]", defs, block),
|
|
1353
|
+
),
|
|
1354
|
+
};
|
|
1355
|
+
};
|
|
1356
|
+
// Read leniently: a cell whose span is unreadable, or which is not an object
|
|
1357
|
+
// at all, covers one column here, and its own diagnostic comes from parsing
|
|
1358
|
+
// it. Otherwise one mistake would report twice.
|
|
1069
1359
|
/** @type {(cells: any[], defs: any[] | null) => boolean} */
|
|
1070
|
-
let
|
|
1360
|
+
let covering = (cells, defs) =>
|
|
1361
|
+
!defs ||
|
|
1362
|
+
cells.reduce((all, cell) => all + spanValue(record(cell) ? cell.span : null), 0) ===
|
|
1363
|
+
defs.length;
|
|
1071
1364
|
/** @type {(cells: any[] | null, defs: any[] | null, path: string) => any[]} */
|
|
1072
1365
|
let counted = (cells, defs, path) => {
|
|
1073
1366
|
if (!cells) return [];
|
|
1074
|
-
if (!
|
|
1367
|
+
if (!covering(cells, defs)) bad(path + ".cells", "expected cells covering every column");
|
|
1075
1368
|
return cells;
|
|
1076
1369
|
};
|
|
1077
|
-
/**
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
*/
|
|
1081
|
-
let totalRowOf = (def, path, defs) => {
|
|
1082
|
-
if (!obj(def, path)) return { styles: NIL, visible: null, cells: [] };
|
|
1370
|
+
/** @type {(def: any, path: string, defs: any[] | null, block: RowEntries) => TotalRow} */
|
|
1371
|
+
let totalRowOf = (def, path, defs, block) => {
|
|
1372
|
+
if (!obj(def, path)) return { styles: foldRow(block), visible: null, cells: [], fan: [] };
|
|
1083
1373
|
keys(def, ["cells", "style", "visible"], path);
|
|
1084
1374
|
let cells = counted(arr(def.cells, path + ".cells", true), defs, path);
|
|
1375
|
+
let styles = foldRow(underRow(block, rowEntriesOf(def, path)));
|
|
1376
|
+
let mapped = mapTotals(cells, path + ".cells");
|
|
1085
1377
|
return {
|
|
1086
|
-
styles
|
|
1378
|
+
styles,
|
|
1087
1379
|
visible: visibleOf(def, path),
|
|
1088
|
-
cells:
|
|
1380
|
+
cells: mapped,
|
|
1381
|
+
fan: mapped.map((entry) => boxReaching(styles.names, entry.box)),
|
|
1089
1382
|
};
|
|
1090
1383
|
};
|
|
1091
1384
|
/**
|
|
1092
1385
|
* @type {(rows: any[], scope: Scope, runners: RunnerSet, columns: any[],
|
|
1093
|
-
* rowVisible: Eval | null, rowStyles:
|
|
1386
|
+
* rowVisible: Eval | null, rowStyles: RowStyles,
|
|
1387
|
+
* fan: (string[] | null)[]) => Generator<any>}
|
|
1094
1388
|
*/
|
|
1095
|
-
function* tableRows(rows, scope, runners, columns, rowVisible, rowStyles) {
|
|
1389
|
+
function* tableRows(rows, scope, runners, columns, rowVisible, rowStyles, fan) {
|
|
1096
1390
|
for (let row of rows) {
|
|
1097
1391
|
let scopeOfRow = runners.bind(scope, row);
|
|
1098
|
-
if (
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
),
|
|
1105
|
-
},
|
|
1106
|
-
{ style: rowStyles(scopeOfRow), run: scopeOfRow.run },
|
|
1107
|
-
);
|
|
1392
|
+
if (hidden(rowVisible, scopeOfRow)) continue;
|
|
1393
|
+
let cells = columns.map((column) =>
|
|
1394
|
+
Object.assign(column.cell(scopeOfRow), { path: column.path }),
|
|
1395
|
+
);
|
|
1396
|
+
fanBox(rowStyles.box(scopeOfRow), cells, fan);
|
|
1397
|
+
yield opt({ type: "row", cells }, { style: rowStyles.rest(scopeOfRow), run: scopeOfRow.run });
|
|
1108
1398
|
}
|
|
1109
1399
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1400
|
+
// One cell that may cover several columns: its value against this scope, the
|
|
1401
|
+
// path naming the definition behind it, and a `span` only when it covers more
|
|
1402
|
+
// than one. The header row and the total rows are both authored as cells like
|
|
1403
|
+
// this; a data row's are the columns' own and cannot span (SCHEMA.md,
|
|
1404
|
+
// "Span"), which is why `tableRows` builds its own without a span at all.
|
|
1405
|
+
/** @type {(cell: any, span: any, path: string, scope: Scope) => any} */
|
|
1406
|
+
let spanCell = (cell, span, path, scope) =>
|
|
1407
|
+
opt(Object.assign(cell(scope), { path }), { span: spanned(span) });
|
|
1408
|
+
// One emitted total row. Its `styles` already carry the totals block's,
|
|
1409
|
+
// composed in at compile time, so this is a row like any other.
|
|
1410
|
+
/** @type {(row: TotalRow, scope: Scope) => any} */
|
|
1411
|
+
let totalRow = (row, scope) => {
|
|
1412
|
+
let cells = row.cells.map(({ cell, span, path }) => spanCell(cell, span, path, scope));
|
|
1413
|
+
fanBox(row.styles.box(scope), cells, row.fan);
|
|
1414
|
+
return opt({ type: "total-row", cells }, { style: row.styles.rest(scope) });
|
|
1415
|
+
};
|
|
1416
|
+
// A hidden block is the whole thing gone -- its rows are never reached, so
|
|
1417
|
+
// nothing is evaluated and nothing votes on a column width, exactly as an
|
|
1418
|
+
// absent `total` behaves.
|
|
1419
|
+
/** @type {(total: TotalBlock, scope: Scope) => Generator<any>} */
|
|
1114
1420
|
function* emitTotals(total, scope) {
|
|
1115
|
-
if (
|
|
1116
|
-
for (let row of total)
|
|
1117
|
-
if (hidden(row.visible, scope)) continue;
|
|
1118
|
-
yield opt(
|
|
1119
|
-
{
|
|
1120
|
-
type: "total-row",
|
|
1121
|
-
cells: row.cells.map(({ cell, path }) => Object.assign(cell(scope), { path })),
|
|
1122
|
-
},
|
|
1123
|
-
{ style: row.styles(scope) },
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1421
|
+
if (hidden(total.visible, scope)) return;
|
|
1422
|
+
for (let row of total.rows) if (!hidden(row.visible, scope)) yield totalRow(row, scope);
|
|
1126
1423
|
}
|
|
1127
|
-
/** @type {(columns: any[], total:
|
|
1128
|
-
let tableBand = (columns, total, rowVisible, rowStyles, headerStyles) =>
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1424
|
+
/** @type {(columns: any[], total: TotalBlock, rowVisible: Eval | null, rowStyles: RowStyles, headerStyles: RowStyles) => Band} */
|
|
1425
|
+
let tableBand = (columns, total, rowVisible, rowStyles, headerStyles) => {
|
|
1426
|
+
// Which of each row box reaches which cell, settled once per table over
|
|
1427
|
+
// the names both sides declared -- so nothing at render reads a resolved
|
|
1428
|
+
// object to find out who owns a border side (docs/adr/0056).
|
|
1429
|
+
let rowFan = columns.map((column) => boxReaching(rowStyles.names, column.box));
|
|
1430
|
+
let headers = columns.filter((column) => column.header);
|
|
1431
|
+
let headerFan = headers.map((column) => boxReaching(headerStyles.names, column.header.box));
|
|
1432
|
+
return function* (rows, scope, runners) {
|
|
1433
|
+
// The header row, built once here rather than reconstructed by every
|
|
1434
|
+
// consumer from a per-column `header` (docs/adr/0053). A covered column
|
|
1435
|
+
// declares none, so this is fewer cells than columns whenever one spans
|
|
1436
|
+
// -- and a header cell carries its column's `path`, as a row cell does,
|
|
1437
|
+
// so every rendered cell can name the definition behind it.
|
|
1438
|
+
let cells = headers.map((column) =>
|
|
1439
|
+
spanCell(column.header.cell, column.header.span, column.path, scope),
|
|
1139
1440
|
);
|
|
1140
|
-
|
|
1441
|
+
// The header row's box lands on its cells, exactly as a data row's does,
|
|
1442
|
+
// which is why what is left of the block rides beside them.
|
|
1443
|
+
fanBox(headerStyles.box(scope), cells, headerFan);
|
|
1444
|
+
// `columns` is what is left once the header row is its own: the
|
|
1445
|
+
// geometry, one entry per column, carrying a `width` share when the
|
|
1446
|
+
// document authored one.
|
|
1447
|
+
let cols = columns.map((column) => opt({}, { width: column.width }));
|
|
1448
|
+
yield {
|
|
1449
|
+
type: "table-start",
|
|
1450
|
+
path: "detail",
|
|
1451
|
+
header: opt({ cells }, { style: headerStyles.rest(scope) }),
|
|
1452
|
+
columns: cols,
|
|
1453
|
+
};
|
|
1454
|
+
yield* tableRows(rows, scope, runners, columns, rowVisible, rowStyles, rowFan);
|
|
1141
1455
|
yield* emitTotals(total, scope);
|
|
1142
1456
|
yield { type: "table-end" };
|
|
1143
1457
|
};
|
|
1458
|
+
};
|
|
1144
1459
|
/** @type {(value: any) => Band} */
|
|
1145
1460
|
let tableOf = (value) => {
|
|
1146
1461
|
keys(value, ["header", "row", "columns", "total"], "detail");
|
|
@@ -1154,6 +1469,7 @@ let bands = (
|
|
|
1154
1469
|
// `{ columns, total }` object is a real table.
|
|
1155
1470
|
/** @type {(value: any) => Band} */
|
|
1156
1471
|
let detailOf = (value) => {
|
|
1472
|
+
pin.body(value, "detail");
|
|
1157
1473
|
if (Array.isArray(value) || value == null) return itemsBand(itemsOf(value, "detail", "detail"));
|
|
1158
1474
|
if (!record(value)) {
|
|
1159
1475
|
bad("detail", "expected an array of items or a table object");
|
|
@@ -1207,7 +1523,11 @@ let bands = (
|
|
|
1207
1523
|
}
|
|
1208
1524
|
groupScope[ctx.name] = handle;
|
|
1209
1525
|
let rows = (ctx.sort ? ctx.sort(groupRows, groupScope) : groupRows).slice(0, ctx.take);
|
|
1210
|
-
return {
|
|
1526
|
+
return {
|
|
1527
|
+
groupScope,
|
|
1528
|
+
aggregates: applyAggs(ctx.aggs, rows, groupScope, handle),
|
|
1529
|
+
rows,
|
|
1530
|
+
};
|
|
1211
1531
|
};
|
|
1212
1532
|
/** @type {(ctx: any) => Band} */
|
|
1213
1533
|
let groupWalk = (ctx) =>
|
|
@@ -1262,9 +1582,9 @@ let bands = (
|
|
|
1262
1582
|
by,
|
|
1263
1583
|
sort: sortOf(def.sort, path + ".sort"),
|
|
1264
1584
|
take: takeOf(def.take, path + ".take"),
|
|
1265
|
-
aggs: foldsOf(def.aggregates, path + ".aggregates",
|
|
1266
|
-
running: foldsOf(def.run, path + ".run",
|
|
1267
|
-
header:
|
|
1585
|
+
aggs: foldsOf(def.aggregates, path + ".aggregates", "aggregate", new Set(["key"])),
|
|
1586
|
+
running: foldsOf(def.run, path + ".run", "run", null, runNames),
|
|
1587
|
+
header: pinBand(def.header, path + ".header"),
|
|
1268
1588
|
footer: itemsOf(def.footer, path + ".footer", "group-footer"),
|
|
1269
1589
|
inner: bandOf(defs, index + 1, detail, columned || columns != null),
|
|
1270
1590
|
name: def.name,
|
|
@@ -1276,6 +1596,15 @@ let bands = (
|
|
|
1276
1596
|
});
|
|
1277
1597
|
};
|
|
1278
1598
|
|
|
1599
|
+
// A group's header, offered to the band-height guard on the way past: the
|
|
1600
|
+
// chain compiles outermost first, so the first one that occupies is the band
|
|
1601
|
+
// under a pinned box (./band-height.js).
|
|
1602
|
+
/** @type {(value: any, path: string) => (scope: Scope) => Generator<any>} */
|
|
1603
|
+
let pinBand = (value, path) => {
|
|
1604
|
+
pin.body(value, path);
|
|
1605
|
+
return itemsOf(value, path, "group-header");
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1279
1608
|
// The band chain, outermost group first: each group partitions its rows and
|
|
1280
1609
|
// recurses; the detail band sits at the leaf.
|
|
1281
1610
|
/** @type {(defs: any[], index: number, detail: any, columned?: boolean) => Band} */
|
|
@@ -1319,25 +1648,22 @@ let selectOf = (schema, bad, attempt, options) => {
|
|
|
1319
1648
|
};
|
|
1320
1649
|
/** @type {(value: any, path: string, expression: any) => any} */
|
|
1321
1650
|
let maybeExpr = (value, path, expression) => (value != null ? expression(value, path) : null);
|
|
1322
|
-
|
|
1323
|
-
let posPts = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
1651
|
+
|
|
1324
1652
|
/** @type {(n: any) => boolean} */
|
|
1325
1653
|
let padPts = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
1326
1654
|
/** @type {(height: any, bad: any) => number | null} */
|
|
1327
1655
|
let headerHeightOf = (height, bad) => {
|
|
1328
1656
|
if (height == null) bad("header.height", "required");
|
|
1329
|
-
else if (!
|
|
1330
|
-
return
|
|
1657
|
+
else if (!positivePts(height)) bad("header.height", "expected a positive number of points");
|
|
1658
|
+
return positivePts(height) ? height : null;
|
|
1331
1659
|
};
|
|
1332
|
-
/** @type {(itemsOf: any) => { items: any, height: number | null }} */
|
|
1333
|
-
let emptyHeader = (itemsOf) => ({ items: itemsOf(null, "header", "report-header"), height: null });
|
|
1334
1660
|
/** @type {(value: any, keys: any, itemsOf: any, bad: any) => { items: any, height: number | null }} */
|
|
1335
1661
|
let reportHeaderOf = (value, keys, itemsOf, bad) => {
|
|
1336
1662
|
if (value == null || Array.isArray(value))
|
|
1337
1663
|
return { items: itemsOf(value, "header", "report-header"), height: null };
|
|
1338
1664
|
if (!record(value)) {
|
|
1339
1665
|
bad("header", "expected an array of items or { height, items }");
|
|
1340
|
-
return
|
|
1666
|
+
return { items: itemsOf(null, "header", "report-header"), height: null };
|
|
1341
1667
|
}
|
|
1342
1668
|
keys(value, ["height", "items"], "header");
|
|
1343
1669
|
return {
|
|
@@ -1364,36 +1690,6 @@ let pageBands = (schema, obj, keys, itemsOf, bad) => {
|
|
|
1364
1690
|
};
|
|
1365
1691
|
};
|
|
1366
1692
|
|
|
1367
|
-
/** @type {(def: any) => boolean} */
|
|
1368
|
-
let shown = (def) => !(def && def.visible === false);
|
|
1369
|
-
/** @type {(list: any, path: string) => [any, string] | null} */
|
|
1370
|
-
let firstOccupying = (list, path) => {
|
|
1371
|
-
if (!Array.isArray(list)) return null;
|
|
1372
|
-
let i = list.findIndex(shown);
|
|
1373
|
-
return i < 0 ? null : [list[i], path + "[" + i + "]"];
|
|
1374
|
-
};
|
|
1375
|
-
/** @type {(schema: any) => any} */
|
|
1376
|
-
let firstGroupHeader = (schema) => {
|
|
1377
|
-
let groups = schema.groups;
|
|
1378
|
-
return Array.isArray(groups) && groups[0] ? groups[0].header : null;
|
|
1379
|
-
};
|
|
1380
|
-
/** @type {(schema: any) => [any, string] | null} */
|
|
1381
|
-
let nextAfterHeader = (schema) =>
|
|
1382
|
-
firstOccupying(firstGroupHeader(schema), "groups[0].header") ||
|
|
1383
|
-
firstOccupying(schema.detail, "detail");
|
|
1384
|
-
/** @type {(n: any) => boolean} */
|
|
1385
|
-
let nonzeroLead = (n) => typeof n === "number" && n !== 0;
|
|
1386
|
-
/** @type {(next: [any, string], bad: any) => void} */
|
|
1387
|
-
let refuseLiteralLead = (next, bad) => {
|
|
1388
|
-
if (nonzeroLead(next[0].style?.spaceBefore))
|
|
1389
|
-
bad(next[1] + ".style.spaceBefore", "must be 0 after a height-declared header");
|
|
1390
|
-
};
|
|
1391
|
-
/** @type {(schema: any, headerHeight: number | null, bad: any) => void} */
|
|
1392
|
-
let refusePinnedLead = (schema, headerHeight, bad) => {
|
|
1393
|
-
if (headerHeight == null) return;
|
|
1394
|
-
let next = nextAfterHeader(schema);
|
|
1395
|
-
if (next) refuseLiteralLead(next, bad);
|
|
1396
|
-
};
|
|
1397
1693
|
/** @type {(headerHeight: number | null, schema: any, bad: any) => void} */
|
|
1398
1694
|
let needPinnedMargin = (headerHeight, schema, bad) => {
|
|
1399
1695
|
if (headerHeight != null && schema.page?.margin == null)
|
|
@@ -1416,7 +1712,7 @@ let needPinnedMargin = (headerHeight, schema, bad) => {
|
|
|
1416
1712
|
* render may traverse, never what this descent reports.
|
|
1417
1713
|
*/
|
|
1418
1714
|
let traverse = (
|
|
1419
|
-
{ add, bad, attempt, runNames },
|
|
1715
|
+
{ add, bad, attempt, pin, runNames },
|
|
1420
1716
|
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf, stylesOf },
|
|
1421
1717
|
{ itemsOf },
|
|
1422
1718
|
{ bandOf },
|
|
@@ -1456,8 +1752,8 @@ let traverse = (
|
|
|
1456
1752
|
let where = maybeExpr(schema.where, "where", expression);
|
|
1457
1753
|
let sort = sortOf(schema.sort, "sort");
|
|
1458
1754
|
let take = takeOf(schema.take, "take");
|
|
1459
|
-
let aggs = foldsOf(schema.aggregates, "aggregates",
|
|
1460
|
-
let running = foldsOf(schema.run, "run",
|
|
1755
|
+
let aggs = foldsOf(schema.aggregates, "aggregates", "aggregate", new Set(["params", "input"]));
|
|
1756
|
+
let running = foldsOf(schema.run, "run", "run", null, runNames);
|
|
1461
1757
|
// The report default: one style block for the whole document, narrowed to
|
|
1462
1758
|
// the declarations a typeface is made of. It resolves once per render in
|
|
1463
1759
|
// report scope rather than per node, which is why it crosses the seam as a
|
|
@@ -1465,9 +1761,14 @@ let traverse = (
|
|
|
1465
1761
|
// nothing here allocates per cell (docs/adr/0033).
|
|
1466
1762
|
let style = stylesOf(schema, "", checkReportStyle);
|
|
1467
1763
|
let { items: header, height: headerHeight } = reportHeaderOf(schema.header, keys, itemsOf, bad);
|
|
1468
|
-
|
|
1764
|
+
// A declared height arms the band-height guard, which every band below is
|
|
1765
|
+
// offered as the descent reaches it (./band-height.js).
|
|
1766
|
+
pin.arm(headerHeight);
|
|
1469
1767
|
// Null when undeclared, so the render decides on the compiled band rather
|
|
1470
|
-
// than reading `schema.empty` a second time from outside the traversal.
|
|
1768
|
+
// than reading `schema.empty` a second time from outside the traversal. The
|
|
1769
|
+
// guard is offered it here, where the traversal already holds it: `empty`
|
|
1770
|
+
// replaces the body rather than following it, so it is judged on its own.
|
|
1771
|
+
pin.instead(schema.empty, "empty");
|
|
1471
1772
|
let empty = maybeItems(schema.empty, "empty", "empty", itemsOf);
|
|
1472
1773
|
let columns = columnsOf(schema.columns, "columns");
|
|
1473
1774
|
let band = bandOf(arr(schema.groups, "groups") || [], 0, schema.detail, columns != null);
|
|
@@ -1510,7 +1811,7 @@ let traverse = (
|
|
|
1510
1811
|
export let plan = (schema, funcs, options) => {
|
|
1511
1812
|
let state = planState(schema, funcs);
|
|
1512
1813
|
let prim = primitives(state);
|
|
1513
|
-
let read = readers(state, prim);
|
|
1814
|
+
let read = readers(state, prim, options);
|
|
1514
1815
|
let node = nodes(state, read);
|
|
1515
1816
|
let band = bands(state, read, node);
|
|
1516
1817
|
return {
|