quario 0.6.0 → 0.7.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 +123 -0
- package/README.md +39 -15
- package/lib/format.js +5 -28
- package/lib/index.d.ts +106 -11
- package/lib/index.js +17 -8
- package/lib/math.js +64 -17
- package/lib/memo.js +49 -0
- package/lib/plan.js +345 -59
- package/lib/reducers.js +48 -1
- package/lib/stream.js +74 -2
- package/lib/style.js +48 -2
- package/package.json +2 -2
package/lib/plan.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* each node against its closed key set — in the documented key order, which is
|
|
4
4
|
* the order the allowed-key list spells out — and compiles the closures that
|
|
5
5
|
* render it, collecting problems instead of throwing. `validate()` reads the
|
|
6
|
-
* problems, `events()` the closures
|
|
6
|
+
* problems, `events()` the closures, and `plan()` hands over both plus the
|
|
7
|
+
* advisory warnings the same descent noticed. One traversal, read two ways: a schema key
|
|
7
8
|
* is checked once, beside the code that compiles it, and never in two walkers.
|
|
8
9
|
* (The safety suite greps every source file for the forbidden string-to-code
|
|
9
10
|
* constructs, so they must not appear even in a comment.)
|
|
@@ -13,8 +14,9 @@
|
|
|
13
14
|
* returns and taking the layers it builds on, composed by `plan()` at the foot
|
|
14
15
|
* of the file:
|
|
15
16
|
*
|
|
16
|
-
* 1. `planState` — what the whole descent accumulates: problems,
|
|
17
|
-
* functions, and the stubs a failed compile
|
|
17
|
+
* 1. `planState` — what the whole descent accumulates: problems, warnings,
|
|
18
|
+
* names, functions, and the stubs a failed compile
|
|
19
|
+
* stands in
|
|
18
20
|
* 2. `primitives` — authored source to located evaluator; the only layer that
|
|
19
21
|
* throws, which is why every reader wraps it in `attempt`
|
|
20
22
|
* 3. `readers` — one per kind of declared value: each checks, compiles and
|
|
@@ -49,7 +51,7 @@ import { ANCHORS, BLOCKED, NAME, RESERVED, record } from "./names.js";
|
|
|
49
51
|
import { MATH } from "./math.js";
|
|
50
52
|
import { REDUCERS, reducerFor } from "./reducers.js";
|
|
51
53
|
import { aggregateValue, withRow } from "./scope.js";
|
|
52
|
-
import { opt, sniff } from "./stream.js";
|
|
54
|
+
import { oneValue, opt, sameStyle, sniff } from "./stream.js";
|
|
53
55
|
import {
|
|
54
56
|
checkBorderSides,
|
|
55
57
|
checkCellStyle,
|
|
@@ -64,8 +66,12 @@ import {
|
|
|
64
66
|
coveredNames,
|
|
65
67
|
fanBox,
|
|
66
68
|
guarded,
|
|
69
|
+
checkRunStyle,
|
|
67
70
|
isBoxName,
|
|
68
71
|
isExpr,
|
|
72
|
+
isRunName,
|
|
73
|
+
isValueName,
|
|
74
|
+
kindOf,
|
|
69
75
|
positivePts,
|
|
70
76
|
resolveFormat,
|
|
71
77
|
SKIP,
|
|
@@ -96,7 +102,9 @@ let NO_ROW_ENTRIES = { box: [], rest: [] };
|
|
|
96
102
|
let NO_ROW_STYLE = { box: NIL, names: [], rest: NIL };
|
|
97
103
|
// A cell that declares no readable style block: nothing to resolve, and no
|
|
98
104
|
// box name to keep any of its row's box off it.
|
|
99
|
-
|
|
105
|
+
// What a node that declares no readable `style` block wears: no resolved
|
|
106
|
+
// object, no unresolved one for a run to layer over, and no box names.
|
|
107
|
+
let NO_LOOK = { styles: NIL, raw: NIL, box: /** @type {string[]} */ ([]) };
|
|
100
108
|
// An absent `total` is the block with no rows. `rows: []` is a definition
|
|
101
109
|
// error, so an empty one can only have come from absence and nothing has to
|
|
102
110
|
// tell the two apart downstream.
|
|
@@ -104,6 +112,17 @@ let NO_CELL_LOOK = { styles: NIL, box: /** @type {string[]} */ ([]) };
|
|
|
104
112
|
let NO_TOTALS = Object.freeze({ visible: null, rows: Object.freeze([]) });
|
|
105
113
|
/** @type {any} */
|
|
106
114
|
let EMPTY = () => [];
|
|
115
|
+
// One styled run of a cell's value: its compiled template, and the unresolved
|
|
116
|
+
// style block it layers over the cell's -- null where it declares none, which
|
|
117
|
+
// is the whole of a cell written as one template string.
|
|
118
|
+
/** @typedef {{ tpl: (scope: Scope) => any, raw: ((scope: Scope) => any) | null }} StyledRun */
|
|
119
|
+
// A node's compiled style: the resolved block a target reads, the unresolved
|
|
120
|
+
// one a styled run of its value layers over, and the box names it declared.
|
|
121
|
+
/** @typedef {{ styles: (scope: Scope) => any, raw: (scope: Scope) => any, box: string[] }} Look */
|
|
122
|
+
/** @type {StyledRun} */
|
|
123
|
+
let NO_STYLED_RUN = { tpl: EMPTY, raw: null };
|
|
124
|
+
/** @type {StyledRun[]} */
|
|
125
|
+
let NO_STYLED_RUNS = [NO_STYLED_RUN];
|
|
107
126
|
// What `$.params` reads when a report declares none.
|
|
108
127
|
let NONE = Object.freeze({});
|
|
109
128
|
|
|
@@ -155,25 +174,64 @@ let shareMsg = (sized, noun) =>
|
|
|
155
174
|
" widths to total under 100, leaving room for the " +
|
|
156
175
|
noun +
|
|
157
176
|
"s without one";
|
|
158
|
-
//
|
|
177
|
+
// What the authored shares come to, or null when there is nothing to decide.
|
|
159
178
|
// Nothing authored is nothing to check; a width out of range (a NaN, per the
|
|
160
179
|
// width verdict) already carries its own problem, and one nobody can read
|
|
161
180
|
// leaves it undecidable whether its part is one of those needing room.
|
|
162
|
-
|
|
181
|
+
// `sized` is whether every part authored one, which is the question both
|
|
182
|
+
// halves of the rule turn on. The one arithmetic, so the error below and the
|
|
183
|
+
// warning beside it can never drift into two rules wearing one name.
|
|
184
|
+
/** @type {(parts: { width?: any }[]) => { total: number, sized: boolean } | null} */
|
|
163
185
|
let sharesOf = (parts) => {
|
|
164
186
|
let shares = parts.map((part) => part.width).filter((width) => width != null);
|
|
165
|
-
|
|
187
|
+
if (!shares.length || shares.some(Number.isNaN)) return null;
|
|
188
|
+
let total = shares.reduce((all, width) => all + width, 0);
|
|
189
|
+
return { total, sized: shares.length === parts.length };
|
|
166
190
|
};
|
|
167
191
|
// Authored shares must leave room for the parts without one. Widths are
|
|
168
192
|
// literal, so the sum is known here; a tolerance keeps three at 33.3 from
|
|
169
193
|
// summing to 100.00000000000001 and failing on IEEE 754 alone.
|
|
170
194
|
/** @type {(bad: any, parts: { width?: any }[], path: string, noun: string) => void} */
|
|
171
195
|
let checkShares = (bad, parts, path, noun) => {
|
|
172
|
-
let
|
|
173
|
-
if (!
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
let sum = sharesOf(parts);
|
|
197
|
+
if (!sum) return;
|
|
198
|
+
if (sum.total > (sum.sized ? 100 + WIDTH_EPS : 100 - WIDTH_EPS))
|
|
199
|
+
bad(path, shareMsg(sum.sized, noun));
|
|
200
|
+
};
|
|
201
|
+
// A sum written back to an author: the tolerance above admits thirds, so the
|
|
202
|
+
// wording must too rather than reporting the 99.89999999999999 nobody typed.
|
|
203
|
+
// Rounding, not the tolerance -- the tolerance decides, this only says.
|
|
204
|
+
/** @type {(value: number) => number} */
|
|
205
|
+
let rounded = (value) => +value.toFixed(4);
|
|
206
|
+
// The other side of the same rule, and the only side that is not a definition
|
|
207
|
+
// error: every column sized and the sum short leaves the trailing width to
|
|
208
|
+
// nobody, because there is no width-less column to divide it (SCHEMA.md,
|
|
209
|
+
// "Table detail"). A table only. A split's slots carry the same percentages
|
|
210
|
+
// under the same arithmetic, but the spec states this consequence for the
|
|
211
|
+
// table alone, and a warning is only worth as much as the sentence it can
|
|
212
|
+
// point at.
|
|
213
|
+
/** @type {(warn: any, columns: { width?: any }[], path: string) => void} */
|
|
214
|
+
let underCommitted = (warn, columns, path) => {
|
|
215
|
+
let sum = sharesOf(columns);
|
|
216
|
+
if (!sum || !sum.sized || sum.total >= 100 - WIDTH_EPS) return;
|
|
217
|
+
warn(
|
|
218
|
+
path,
|
|
219
|
+
"every column is sized and the widths total " +
|
|
220
|
+
rounded(sum.total) +
|
|
221
|
+
", leaving the trailing " +
|
|
222
|
+
rounded(100 - sum.total) +
|
|
223
|
+
" of the table width unused",
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Whether a style block settles on the money kind from the document alone --
|
|
228
|
+
// the question `currency` is read under (SCHEMA.md, "Style declarations"). An
|
|
229
|
+
// `=` format resolves at render, so a block that defers its kind has not named
|
|
230
|
+
// another one, and silence is the only honest answer for it.
|
|
231
|
+
/** @type {(block: any) => boolean} */
|
|
232
|
+
let formatsMoney = (block) => {
|
|
233
|
+
let format = Object.hasOwn(block, "format") ? block.format : undefined;
|
|
234
|
+
return isExpr(format) || kindOf(format) === "currency";
|
|
177
235
|
};
|
|
178
236
|
|
|
179
237
|
// One pass over a report's constants: it checks each value, copies it, and
|
|
@@ -338,6 +396,16 @@ let planState = (schema, funcs) => {
|
|
|
338
396
|
};
|
|
339
397
|
/** @type {(path: string, msg: string) => void} */
|
|
340
398
|
let bad = (path, msg) => problems.push({ path, message: path + ": " + msg });
|
|
399
|
+
// The advisory half of the same descent: the document declares something
|
|
400
|
+
// nothing will read. Not a problem at another severity -- a `Problem` carries
|
|
401
|
+
// the engine's located `diagnostic` when one authenticated the fault, and
|
|
402
|
+
// nothing raises here, so the type says what arrives (SCHEMA.md,
|
|
403
|
+
// "Validation"). Kept in its own list, which is what leaves `problems`'
|
|
404
|
+
// fatal contract untouched.
|
|
405
|
+
/** @type {{ path: string, source?: string, message: string }[]} */
|
|
406
|
+
let warnings = [];
|
|
407
|
+
/** @type {(path: string, msg: string) => void} */
|
|
408
|
+
let warn = (path, msg) => warnings.push({ path, message: path + ": " + msg });
|
|
341
409
|
// Record a throwing compile step and stand in a stub, so the traversal reports
|
|
342
410
|
// the whole definition rather than only its first fault.
|
|
343
411
|
/** @type {<T>(fn: () => T, fallback: T) => T} */
|
|
@@ -377,11 +445,13 @@ let planState = (schema, funcs) => {
|
|
|
377
445
|
functions,
|
|
378
446
|
BND,
|
|
379
447
|
problems,
|
|
448
|
+
warnings,
|
|
380
449
|
runNames,
|
|
381
450
|
anchors,
|
|
382
451
|
anchorsOf,
|
|
383
452
|
add,
|
|
384
453
|
bad,
|
|
454
|
+
warn,
|
|
385
455
|
attempt,
|
|
386
456
|
// The band-height rule, carried by the descent rather than walked
|
|
387
457
|
// separately: `traverse` arms it from the report header and offers it each
|
|
@@ -511,7 +581,7 @@ let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
|
511
581
|
* @param {State} state
|
|
512
582
|
* @param {Primitives} primitives
|
|
513
583
|
*/
|
|
514
|
-
let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ options) => {
|
|
584
|
+
let readers = ({ bad, warn, attempt }, { prop, cell, parseFold }, /** @type {any} */ options) => {
|
|
515
585
|
/** @type {(value: any, path: string) => boolean} */
|
|
516
586
|
let obj = (value, path) => {
|
|
517
587
|
if (record(value)) return true;
|
|
@@ -598,6 +668,27 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
598
668
|
}
|
|
599
669
|
return value;
|
|
600
670
|
};
|
|
671
|
+
// `currency` is read only under `format: "currency"`; declared without that
|
|
672
|
+
// kind it contributes nothing (SCHEMA.md, "Style declarations"), which is a
|
|
673
|
+
// warning rather than a definition error because the kind is a *later* edit
|
|
674
|
+
// away and an editor session must never refuse a document mid-edit.
|
|
675
|
+
//
|
|
676
|
+
// The `currency` is read off `entries` rather than off the block, so one the
|
|
677
|
+
// vocabulary already refused earns no second entry about that declaration:
|
|
678
|
+
// `entries` is exactly what survived `check`. The `format` beside it is read
|
|
679
|
+
// as written, because a `format` that was refused is a kind the engine will
|
|
680
|
+
// not have either -- the code is unread all the same, and saying so is not a
|
|
681
|
+
// second entry about the same key.
|
|
682
|
+
/** @type {(block: any, path: string, entries: StyleEntry[]) => void} */
|
|
683
|
+
let unreadCurrency = (block, path, entries) => {
|
|
684
|
+
if (!entries.some(([name]) => name === "currency")) return;
|
|
685
|
+
if (formatsMoney(block)) return;
|
|
686
|
+
warn(
|
|
687
|
+
path + ".currency",
|
|
688
|
+
'currency is read only under format: "currency", so this declaration' +
|
|
689
|
+
" contributes nothing; declare that kind or drop the code",
|
|
690
|
+
);
|
|
691
|
+
};
|
|
601
692
|
// A declared style block resolves to plain data — the closed vocabulary's
|
|
602
693
|
// names in declaration order, null when empty — and stays data; a render
|
|
603
694
|
// target maps it to its own formatting model (the HTML target to inline
|
|
@@ -622,6 +713,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
622
713
|
entries.push([name, expr ? guarded(name, resolve) : resolve, expr]);
|
|
623
714
|
}
|
|
624
715
|
}
|
|
716
|
+
unreadCurrency(block, path, entries);
|
|
625
717
|
return entries;
|
|
626
718
|
};
|
|
627
719
|
// The names are written in declaration order, which the stream promises, so
|
|
@@ -636,14 +728,19 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
636
728
|
// block. The instance's options come with it, for the cell that declares no
|
|
637
729
|
// code of its own and takes the instance's (docs/adr/0056).
|
|
638
730
|
/** @type {(entries: StyleEntry[]) => (scope: Scope) => Scope} */
|
|
639
|
-
let
|
|
731
|
+
let blockFn = (entries) => (scope) => {
|
|
640
732
|
/** @type {Scope} */
|
|
641
733
|
let style = {};
|
|
642
734
|
for (let [name, value] of entries) {
|
|
643
735
|
let resolved = value(scope);
|
|
644
736
|
if (resolved !== SKIP) style[name] = resolved;
|
|
645
737
|
}
|
|
646
|
-
return
|
|
738
|
+
return style;
|
|
739
|
+
};
|
|
740
|
+
/** @type {(entries: StyleEntry[]) => (scope: Scope) => Scope} */
|
|
741
|
+
let styleFn = (entries) => {
|
|
742
|
+
let block = blockFn(entries);
|
|
743
|
+
return (scope) => resolveFormat(block(scope), options);
|
|
647
744
|
};
|
|
648
745
|
/**
|
|
649
746
|
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
@@ -652,14 +749,23 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
652
749
|
// A compiled block, constant-folded when nothing in it defers to render, so
|
|
653
750
|
// a literal style is resolved once and every node that wears it is handed
|
|
654
751
|
// the same frozen object.
|
|
655
|
-
/** @type {(entries: StyleEntry[]) => (scope: Scope) => any} */
|
|
656
|
-
let
|
|
752
|
+
/** @type {(entries: StyleEntry[], fn: (entries: StyleEntry[]) => (scope: Scope) => any) => (scope: Scope) => any} */
|
|
753
|
+
let foldWith = (entries, fn) => {
|
|
657
754
|
if (!entries.length) return NIL;
|
|
658
|
-
let resolve =
|
|
755
|
+
let resolve = fn(entries);
|
|
659
756
|
if (entries.some((entry) => entry[2])) return resolve;
|
|
660
757
|
let constant = Object.freeze(resolve({}));
|
|
661
758
|
return () => constant;
|
|
662
759
|
};
|
|
760
|
+
/** @type {(entries: StyleEntry[]) => (scope: Scope) => any} */
|
|
761
|
+
let folded = (entries) => foldWith(entries, styleFn);
|
|
762
|
+
// The same block with `format` left as the author wrote it. A styled run
|
|
763
|
+
// layers over the cell's declarations and *then* resolves, so a run naming a
|
|
764
|
+
// `currency` under an inherited kind takes its digit count from the code it
|
|
765
|
+
// named rather than from the one the cell wore (docs/adr/0056, ADR 0061).
|
|
766
|
+
// Nothing outside that composition reads it.
|
|
767
|
+
/** @type {(entries: StyleEntry[]) => (scope: Scope) => any} */
|
|
768
|
+
let foldedRaw = (entries) => foldWith(entries, blockFn);
|
|
663
769
|
/**
|
|
664
770
|
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
665
771
|
* => StyleEntry[]}
|
|
@@ -734,30 +840,127 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
734
840
|
let entries = entriesOf(def, path, check);
|
|
735
841
|
return entries ? folded(entries) : NIL;
|
|
736
842
|
};
|
|
737
|
-
// A cell's compiled style with the box names it declared kept beside it
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
// here instead
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
843
|
+
// A cell's compiled style with the box names it declared kept beside it, and
|
|
844
|
+
// the unresolved block a styled run of its value layers over. Those names are
|
|
845
|
+
// what settles which of its row's box reaches it -- the question `layer` used
|
|
846
|
+
// to ask of the resolved object at render, asked once here instead
|
|
847
|
+
// (docs/adr/0062).
|
|
848
|
+
/** @type {(def: any, path: string, check: (name: string, value: any) => string | null) => Look} */
|
|
849
|
+
let lookOf = (def, path, check) => {
|
|
850
|
+
let entries = entriesOf(def, path, check);
|
|
851
|
+
if (!entries) return NO_LOOK;
|
|
745
852
|
return {
|
|
746
853
|
styles: folded(entries),
|
|
854
|
+
// Only the inline half is offered to a styled run: a run carries that
|
|
855
|
+
// half and nothing else, so a cell's padding, border or `align` must not
|
|
856
|
+
// ride down onto a stretch of its text (SCHEMA.md, "Style
|
|
857
|
+
// declarations"). Narrowed here, at compile, rather than per render.
|
|
858
|
+
raw: foldedRaw(entries.filter(([name]) => isRunName(name))),
|
|
747
859
|
box: entries.filter(([name]) => isBoxName(name)).map(([name]) => name),
|
|
748
860
|
};
|
|
749
861
|
};
|
|
862
|
+
// Every cell-bearing node reads the same three keys in the same documented
|
|
863
|
+
// order -- value, visible, style -- and each reader reports as it reads, so
|
|
864
|
+
// that order is the order problems and warnings come out in. Stated once, so
|
|
865
|
+
// a fourth cell site cannot quietly report them in another.
|
|
866
|
+
/**
|
|
867
|
+
* @type {(def: any, path: string, check: (name: string, value: any) => string | null,
|
|
868
|
+
* valuePath?: string) => { styled: StyledRun[], visible: Eval | null, look: Look }}
|
|
869
|
+
*/
|
|
870
|
+
let cellParts = (def, path, check, valuePath = path + ".value") => {
|
|
871
|
+
let styled = cellValue(def.value, valuePath);
|
|
872
|
+
let visible = visibleOf(def, path);
|
|
873
|
+
unreadFormat(def.value, def.style, path);
|
|
874
|
+
return { styled, visible, look: lookOf(def, path, check) };
|
|
875
|
+
};
|
|
750
876
|
/** The same, for the blocks that sit on a table row. */
|
|
751
877
|
/** @type {(def: any, path: string) => RowStyles} */
|
|
752
878
|
let rowStylesOf = (def, path) => foldRow(rowEntriesOf(def, path));
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
879
|
+
// One sjabloon template, and nothing else: an image item's `alt` is a textual
|
|
880
|
+
// stand-in rather than a rendered line, so styling a fragment of one means
|
|
881
|
+
// nothing in any target that has a place for it (SCHEMA.md, "Cell values").
|
|
882
|
+
/** @type {(value: any, path: string) => (scope: Scope) => any} */
|
|
883
|
+
let textValue = (value, path) => {
|
|
757
884
|
if (typeof value === "string") return attempt(() => cell(value, path), EMPTY);
|
|
758
885
|
bad(path, "expected a template string");
|
|
759
886
|
return EMPTY;
|
|
760
887
|
};
|
|
888
|
+
// Whether a template can *never* render as a single value token, which is the
|
|
889
|
+
// only answer this channel is certain of without data. Text before the first
|
|
890
|
+
// `{{` or after the last `}}` renders on every row whatever the blocks
|
|
891
|
+
// between them do, so a source that does not open and close on a tag can
|
|
892
|
+
// never be one value; one that does may be, and `plan()` has no data to
|
|
893
|
+
// settle it. That is the whole of why the warning below is incomplete by
|
|
894
|
+
// construction (SCHEMA.md, "Validation").
|
|
895
|
+
//
|
|
896
|
+
// The two delimiters are the one place quario names sjabloon's syntax rather
|
|
897
|
+
// than asking for it, and hard constraint 2 is why that is written down here
|
|
898
|
+
// instead of passing unremarked. It is a *shape* test and not a parse:
|
|
899
|
+
// nothing is tokenised, extracted or evaluated, the compile above stays the
|
|
900
|
+
// only thing that reads the source, and the answer decides an advisory alone.
|
|
901
|
+
// It has no false positives -- text outside every tag cannot be conditional,
|
|
902
|
+
// since a block opens with `{{#`, and sjabloon strips no whitespace around a
|
|
903
|
+
// standalone tag -- so a wrong answer costs a missing warning, never a
|
|
904
|
+
// warning on a correct document. A value that is not one template is not this
|
|
905
|
+
// question at all: a run array's runs each answer it for themselves.
|
|
906
|
+
/** @type {(src: any) => boolean} */
|
|
907
|
+
let neverPresents = (src) =>
|
|
908
|
+
typeof src === "string" && !(src.startsWith("{{") && src.endsWith("}}"));
|
|
909
|
+
// A `format` on a run that can never render one value token is unread by the
|
|
910
|
+
// rule that declaration states, so it contributes nothing. It warns where it
|
|
911
|
+
// is *declared*, like every other entry on this channel -- not at the value
|
|
912
|
+
// whose token count caused it (quario-xdap.10).
|
|
913
|
+
/** @type {(src: any, block: any, path: string) => void} */
|
|
914
|
+
let unreadFormat = (src, block, path) => {
|
|
915
|
+
if (!record(block) || !Object.hasOwn(block, "format") || !neverPresents(src)) return;
|
|
916
|
+
warn(
|
|
917
|
+
path + ".style.format",
|
|
918
|
+
"format is read only where its run renders a single value token, so this" +
|
|
919
|
+
" declaration contributes nothing; split the value into styled runs, or" +
|
|
920
|
+
" drop the declaration",
|
|
921
|
+
);
|
|
922
|
+
};
|
|
923
|
+
// One styled run: a template and the inline half of the vocabulary. A closed
|
|
924
|
+
// two-key object -- `{{#if}}` already conditions a run's own text, so there
|
|
925
|
+
// is no `visible` on one (SCHEMA.md, "Cell values").
|
|
926
|
+
/** @type {(def: any, path: string) => StyledRun} */
|
|
927
|
+
let styledRunOf = (def, path) => {
|
|
928
|
+
if (!record(def)) {
|
|
929
|
+
bad(path, "expected an object");
|
|
930
|
+
return NO_STYLED_RUN;
|
|
931
|
+
}
|
|
932
|
+
keys(def, ["value", "style"], path);
|
|
933
|
+
let tpl = textValue(def.value, path + ".value");
|
|
934
|
+
unreadFormat(def.value, def.style, path);
|
|
935
|
+
let entries = entriesOf(def, path, checkRunStyle);
|
|
936
|
+
return { tpl, raw: entries ? foldedRaw(entries) : null };
|
|
937
|
+
};
|
|
938
|
+
// A cell value: one sjabloon template, or a non-empty array of styled runs.
|
|
939
|
+
// A cell written as one template string is one implicit run, so every rule
|
|
940
|
+
// about a run reads the same for both spellings (ADR 0061). Markup in literal
|
|
941
|
+
// text is meaningful only to the HTML target; a run is not.
|
|
942
|
+
/** @type {(value: any, path: string) => StyledRun[]} */
|
|
943
|
+
let cellValue = (value, path) => {
|
|
944
|
+
if (typeof value === "string")
|
|
945
|
+
return [{ tpl: attempt(() => cell(value, path), EMPTY), raw: null }];
|
|
946
|
+
if (Array.isArray(value)) {
|
|
947
|
+
if (!value.length) {
|
|
948
|
+
bad(path, "expected at least one styled run");
|
|
949
|
+
return NO_STYLED_RUNS;
|
|
950
|
+
}
|
|
951
|
+
return value.map((styled, i) => styledRunOf(styled, path + "[" + i + "]"));
|
|
952
|
+
}
|
|
953
|
+
bad(path, "expected a template string or an array of styled runs");
|
|
954
|
+
return NO_STYLED_RUNS;
|
|
955
|
+
};
|
|
956
|
+
// A styled run's resolved style: the cell's own declarations with the run's
|
|
957
|
+
// over them, composed while `format` is still as written so the kind resolves
|
|
958
|
+
// once, over the pair. A run that declares no block of its own wears the
|
|
959
|
+
// cell's answer unchanged, which is what makes a run-less cell's stream
|
|
960
|
+
// byte-identical to what it always was.
|
|
961
|
+
/** @type {(own: any, ownRaw: any, styled: StyledRun, scope: Scope) => any} */
|
|
962
|
+
let styledRunLook = (own, ownRaw, styled, scope) =>
|
|
963
|
+
styled.raw ? resolveFormat({ ...ownRaw, ...styled.raw(scope) }, options) : own;
|
|
761
964
|
/** @type {(seen: Set<string> | undefined, name: string, foldPath: string) => void} */
|
|
762
965
|
let noteRun = (seen, name, foldPath) => {
|
|
763
966
|
if (!seen) return;
|
|
@@ -864,12 +1067,15 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
864
1067
|
columnsOf,
|
|
865
1068
|
takeOf,
|
|
866
1069
|
stylesOf,
|
|
867
|
-
|
|
1070
|
+
lookOf,
|
|
1071
|
+
cellParts,
|
|
1072
|
+
styledRunLook,
|
|
868
1073
|
rowStylesOf,
|
|
869
1074
|
rowEntriesOf,
|
|
870
1075
|
underRow,
|
|
871
1076
|
foldRow,
|
|
872
1077
|
cellValue,
|
|
1078
|
+
textValue,
|
|
873
1079
|
foldsOf,
|
|
874
1080
|
sortOf,
|
|
875
1081
|
};
|
|
@@ -884,17 +1090,104 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }, /** @type {any} */ o
|
|
|
884
1090
|
* @param {State} state
|
|
885
1091
|
* @param {Readers} readers
|
|
886
1092
|
*/
|
|
887
|
-
let nodes = (
|
|
1093
|
+
let nodes = (
|
|
1094
|
+
{ bad },
|
|
1095
|
+
{
|
|
1096
|
+
arr,
|
|
1097
|
+
obj,
|
|
1098
|
+
keys,
|
|
1099
|
+
expression,
|
|
1100
|
+
cellValue,
|
|
1101
|
+
cellParts,
|
|
1102
|
+
textValue,
|
|
1103
|
+
visibleOf,
|
|
1104
|
+
stylesOf,
|
|
1105
|
+
lookOf,
|
|
1106
|
+
styledRunLook,
|
|
1107
|
+
},
|
|
1108
|
+
) => {
|
|
888
1109
|
// `tokens` plus its truthy `style`. A hidden cell keeps its slot with no
|
|
889
1110
|
// tokens so a column keeps its alignment; items pass `visible` null and drop
|
|
890
1111
|
// themselves instead, having no slot to keep. The anchor story lives on
|
|
891
1112
|
// `cell` in primitives: the scope chain itself carries `$` and `@`.
|
|
1113
|
+
// The declaration presents a value, so it needs one value to speak about: a
|
|
1114
|
+
// style governing tokens that are not exactly one value token is read without
|
|
1115
|
+
// its `format`, and without the `currency` that rides only under that kind.
|
|
1116
|
+
// The engine settles it per render and omits the pair from the stream, so no
|
|
1117
|
+
// target decides it and none can disagree with another (ADR 0061).
|
|
1118
|
+
/** @type {(style: any) => any} */
|
|
1119
|
+
let withoutValueNames = (style) => {
|
|
1120
|
+
/** @type {any} */
|
|
1121
|
+
let out = {};
|
|
1122
|
+
for (let name of Object.keys(style)) if (!isValueName(name)) out[name] = style[name];
|
|
1123
|
+
return Object.keys(out).length ? Object.freeze(out) : null;
|
|
1124
|
+
};
|
|
1125
|
+
/** @type {(style: any, one: boolean) => any} */
|
|
1126
|
+
let presented = (style, one) => {
|
|
1127
|
+
if (!style || one) return style;
|
|
1128
|
+
return Object.keys(style).some(isValueName) ? withoutValueNames(style) : style;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// A cell's styled runs, flattened into the one `tokens` array the stream
|
|
1132
|
+
// carries. A token gains a `style` only where its run resolves differently
|
|
1133
|
+
// from the cell's own, so a consumer that has never heard of a run reads
|
|
1134
|
+
// `cell.style`, joins the tokens and is correct rather than merely unbroken
|
|
1135
|
+
// (ADR 0061).
|
|
1136
|
+
// What the cell's own style governs is the flattened token list, so the cell
|
|
1137
|
+
// is its own implicit run for the rule above -- counted rather than
|
|
1138
|
+
// concatenated, since only "exactly one, and a value" is being asked.
|
|
1139
|
+
/** @type {(rendered: { tokens: any[] }[]) => boolean} */
|
|
1140
|
+
let flatOneValue = (rendered) => {
|
|
1141
|
+
let count = 0;
|
|
1142
|
+
/** @type {any} */
|
|
1143
|
+
let sole = null;
|
|
1144
|
+
for (let { tokens } of rendered) {
|
|
1145
|
+
count += tokens.length;
|
|
1146
|
+
if (tokens.length === 1) sole = tokens[0];
|
|
1147
|
+
}
|
|
1148
|
+
return count === 1 && "value" in sole;
|
|
1149
|
+
};
|
|
1150
|
+
// One run's tokens, each wearing that run's style where it resolves
|
|
1151
|
+
// differently from the cell's.
|
|
1152
|
+
/** @type {(out: any[], worn: { tokens: any[], style: any }, style: any) => void} */
|
|
1153
|
+
let wearing = (out, worn, style) => {
|
|
1154
|
+
let look = presented(worn.style, oneValue(worn.tokens));
|
|
1155
|
+
let wears = !sameStyle(look, style);
|
|
1156
|
+
for (let token of worn.tokens) out.push(wears ? { ...token, style: look } : token);
|
|
1157
|
+
};
|
|
1158
|
+
/** @type {(styled: StyledRun[], scope: Scope, own: any, ownRaw: (scope: Scope) => any) => any} */
|
|
1159
|
+
let composed = (styled, scope, own, ownRaw) => {
|
|
1160
|
+
// The inline half of the cell's own block, unresolved, read once for every
|
|
1161
|
+
// run to layer over.
|
|
1162
|
+
let raw = ownRaw(scope);
|
|
1163
|
+
let rendered = styled.map((one) => ({
|
|
1164
|
+
tokens: one.tpl(scope),
|
|
1165
|
+
style: styledRunLook(own, raw, one, scope),
|
|
1166
|
+
}));
|
|
1167
|
+
let style = presented(own, flatOneValue(rendered));
|
|
1168
|
+
/** @type {any[]} */
|
|
1169
|
+
let tokens = [];
|
|
1170
|
+
for (let worn of rendered) wearing(tokens, worn, style);
|
|
1171
|
+
return opt({ tokens }, { style });
|
|
1172
|
+
};
|
|
1173
|
+
|
|
892
1174
|
/**
|
|
893
|
-
* @type {(
|
|
1175
|
+
* @type {(styled: StyledRun[], visible: Eval | null, look: Look)
|
|
894
1176
|
* => (scope: Scope) => any}
|
|
895
1177
|
*/
|
|
896
|
-
let cellShape = (
|
|
897
|
-
|
|
1178
|
+
let cellShape = (styled, visible, look) => (scope) => {
|
|
1179
|
+
let own = look.styles(scope);
|
|
1180
|
+
// A hidden cell keeps its slot with no tokens, and no tokens is no value
|
|
1181
|
+
// for a declaration to present.
|
|
1182
|
+
if (hidden(visible, scope)) return opt({ tokens: [] }, { style: presented(own, false) });
|
|
1183
|
+
// A cell written as one template string is one implicit run wearing the
|
|
1184
|
+
// cell's own style, so it never composes and never carries a token style.
|
|
1185
|
+
if (styled.length === 1 && !styled[0].raw) {
|
|
1186
|
+
let tokens = styled[0].tpl(scope);
|
|
1187
|
+
return opt({ tokens }, { style: presented(own, oneValue(tokens)) });
|
|
1188
|
+
}
|
|
1189
|
+
return composed(styled, scope, own, look.raw);
|
|
1190
|
+
};
|
|
898
1191
|
|
|
899
1192
|
// A table cell: the shape of a total cell and of an object column header.
|
|
900
1193
|
/**
|
|
@@ -902,10 +1195,8 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
902
1195
|
* { cell: (scope: Scope) => any, box: string[] }}
|
|
903
1196
|
*/
|
|
904
1197
|
let cellOf = (def, path, valuePath = path + ".value") => {
|
|
905
|
-
let
|
|
906
|
-
|
|
907
|
-
let look = cellLook(def, path);
|
|
908
|
-
return { cell: cellShape(tpl, visible, look.styles), box: look.box };
|
|
1198
|
+
let { styled, visible, look } = cellParts(def, path, checkCellStyle, valuePath);
|
|
1199
|
+
return { cell: cellShape(styled, visible, look), box: look.box };
|
|
909
1200
|
};
|
|
910
1201
|
|
|
911
1202
|
/** @type {(def: any, path: string) => number} */
|
|
@@ -927,10 +1218,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
927
1218
|
*/
|
|
928
1219
|
let headerOf = (def, path) => {
|
|
929
1220
|
if (typeof def === "string")
|
|
930
|
-
return { cell: cellShape(cellValue(def, path), null,
|
|
1221
|
+
return { cell: cellShape(cellValue(def, path), null, NO_LOOK), span: 1, box: NO_LOOK.box };
|
|
931
1222
|
if (!record(def)) {
|
|
932
1223
|
bad(path, "expected a template string or object");
|
|
933
|
-
return { cell: cellShape(
|
|
1224
|
+
return { cell: cellShape(NO_STYLED_RUNS, null, NO_LOOK), span: 1, box: NO_LOOK.box };
|
|
934
1225
|
}
|
|
935
1226
|
keys(def, ["value", "style", "span"], path);
|
|
936
1227
|
return { ...cellOf(def, path), span: spanOf(def, path) };
|
|
@@ -971,15 +1262,13 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
971
1262
|
if (!obj(def, path)) return null;
|
|
972
1263
|
keys(def, ["header", "value", "visible", "style", "width"], path);
|
|
973
1264
|
let header = headerSlot(def.header, path + ".header", slot);
|
|
974
|
-
let
|
|
975
|
-
let visible = visibleOf(def, path);
|
|
976
|
-
let look = cellLook(def, path);
|
|
1265
|
+
let { styled, visible, look } = cellParts(def, path, checkCellStyle);
|
|
977
1266
|
// A width is a percentage of the table width, resolved by every target.
|
|
978
1267
|
// The range verdict is owned here: an unreadable width comes back as NaN,
|
|
979
1268
|
// so the total check below reads the verdict instead of re-deciding it.
|
|
980
1269
|
return {
|
|
981
1270
|
header,
|
|
982
|
-
cell: cellShape(
|
|
1271
|
+
cell: cellShape(styled, visible, look),
|
|
983
1272
|
box: look.box,
|
|
984
1273
|
width: widthOf(def.width, path),
|
|
985
1274
|
path,
|
|
@@ -1004,13 +1293,8 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
1004
1293
|
let textOf = (def, path, role, inSlot = false) => {
|
|
1005
1294
|
keys(def, inSlot ? TEXT_SLOT_KEYS : TEXT_KEYS, path);
|
|
1006
1295
|
checkTextType(def.type, path);
|
|
1007
|
-
let
|
|
1008
|
-
let
|
|
1009
|
-
let shape = cellShape(
|
|
1010
|
-
tpl,
|
|
1011
|
-
inSlot ? visible : null,
|
|
1012
|
-
stylesOf(def, path, inSlot ? checkSlotStyle : checkItemStyle),
|
|
1013
|
-
);
|
|
1296
|
+
let { styled, visible, look } = cellParts(def, path, inSlot ? checkSlotStyle : checkItemStyle);
|
|
1297
|
+
let shape = cellShape(styled, inSlot ? visible : null, look);
|
|
1014
1298
|
// The cell shape first, then the item's own fields, so a cell reads
|
|
1015
1299
|
// the same wherever it appears in the stream.
|
|
1016
1300
|
return (scope) => {
|
|
@@ -1039,7 +1323,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
1039
1323
|
bad(path, 'unknown fit "' + fit + '"');
|
|
1040
1324
|
};
|
|
1041
1325
|
/** @type {(def: any, path: string) => any} */
|
|
1042
|
-
let altOf = (def, path) => (def.alt != null ?
|
|
1326
|
+
let altOf = (def, path) => (def.alt != null ? textValue(def.alt, path + ".alt") : null);
|
|
1043
1327
|
/** @type {(alt: any, scope: Scope) => any} */
|
|
1044
1328
|
let imageAlt = (alt, scope) => alt && alt(scope);
|
|
1045
1329
|
/** @type {(fields: any, scope: Scope, bytes: any) => any} */
|
|
@@ -1094,13 +1378,13 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
1094
1378
|
// problems twice, and reading it early would report them out of order.
|
|
1095
1379
|
let alt = altOf(def, path);
|
|
1096
1380
|
let visible = visibleOf(def, path);
|
|
1097
|
-
let
|
|
1381
|
+
let look = lookOf(def, path, imageLook(inSlot));
|
|
1098
1382
|
let render = renderImage({
|
|
1099
1383
|
source,
|
|
1100
1384
|
fit: def.fit ?? "natural",
|
|
1101
1385
|
alt,
|
|
1102
1386
|
visible,
|
|
1103
|
-
styles,
|
|
1387
|
+
styles: look.styles,
|
|
1104
1388
|
path,
|
|
1105
1389
|
sourceSpec: def.source,
|
|
1106
1390
|
role,
|
|
@@ -1110,7 +1394,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
1110
1394
|
// nullish source, leaves the same empty-token placeholder a hidden text
|
|
1111
1395
|
// slot leaves, so the widths either side of it do not move. `cellShape`
|
|
1112
1396
|
// with no template is that placeholder, so the two cannot drift.
|
|
1113
|
-
let empty = cellShape(
|
|
1397
|
+
let empty = cellShape(NO_STYLED_RUNS, null, look);
|
|
1114
1398
|
return (scope) => render(scope) ?? { ...empty(scope), type: "item", role };
|
|
1115
1399
|
};
|
|
1116
1400
|
|
|
@@ -1222,7 +1506,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
1222
1506
|
* @param {Nodes} nodes
|
|
1223
1507
|
*/
|
|
1224
1508
|
let bands = (
|
|
1225
|
-
{ bad, pin, runNames },
|
|
1509
|
+
{ bad, warn, pin, runNames },
|
|
1226
1510
|
{
|
|
1227
1511
|
arr,
|
|
1228
1512
|
obj,
|
|
@@ -1321,6 +1605,7 @@ let bands = (
|
|
|
1321
1605
|
.map((def, i) => columnOf(def, "detail.columns[" + i + "]", slots[i]))
|
|
1322
1606
|
.filter((column) => column != null);
|
|
1323
1607
|
checkShares(bad, columns, "detail.columns", "column");
|
|
1608
|
+
underCommitted(warn, columns, "detail.columns");
|
|
1324
1609
|
return { defs, columns };
|
|
1325
1610
|
};
|
|
1326
1611
|
/** @type {(totals: any[], prefix: string) => any[]} */
|
|
@@ -1425,7 +1710,7 @@ let bands = (
|
|
|
1425
1710
|
let tableBand = (columns, total, rowVisible, rowStyles, headerStyles) => {
|
|
1426
1711
|
// Which of each row box reaches which cell, settled once per table over
|
|
1427
1712
|
// the names both sides declared -- so nothing at render reads a resolved
|
|
1428
|
-
// object to find out who owns a border side (docs/adr/
|
|
1713
|
+
// object to find out who owns a border side (docs/adr/0062).
|
|
1429
1714
|
let rowFan = columns.map((column) => boxReaching(rowStyles.names, column.box));
|
|
1430
1715
|
let headers = columns.filter((column) => column.header);
|
|
1431
1716
|
let headerFan = headers.map((column) => boxReaching(headerStyles.names, column.header.box));
|
|
@@ -1817,6 +2102,7 @@ export let plan = (schema, funcs, options) => {
|
|
|
1817
2102
|
return {
|
|
1818
2103
|
compiled: traverse(state, read, node, band, schema, options),
|
|
1819
2104
|
problems: state.problems,
|
|
2105
|
+
warnings: state.warnings,
|
|
1820
2106
|
anchors: state.anchors,
|
|
1821
2107
|
names: state.names,
|
|
1822
2108
|
// Registry metadata comes from xprsn's one signature convention, described
|