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/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. One traversal, read two ways: a schema key
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, names,
17
- * functions, and the stubs a failed compile stands in
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
- let NO_CELL_LOOK = { styles: NIL, box: /** @type {string[]} */ ([]) };
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
- // The authored shares worth summing, or null when there is nothing to decide.
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
- /** @type {(parts: { width?: any }[]) => number[] | null} */
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
- return !shares.length || shares.some(Number.isNaN) ? null : shares;
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 shares = sharesOf(parts);
173
- if (!shares) return;
174
- let total = shares.reduce((all, width) => all + width, 0);
175
- let sized = shares.length === parts.length;
176
- if (total > (sized ? 100 + WIDTH_EPS : 100 - WIDTH_EPS)) bad(path, shareMsg(sized, noun));
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 styleFn = (entries) => (scope) => {
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 resolveFormat(style, options);
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 folded = (entries) => {
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 = styleFn(entries);
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
- // 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;
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
- // A cell value: one sjabloon template. Emphasis is the cell's own `style`;
754
- // markup in literal text is meaningful only to the HTML target.
755
- /** @type {(value: any, path: string) => (scope: Scope, opts?: any) => any} */
756
- let cellValue = (value, path) => {
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
- cellLook,
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 = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, stylesOf, cellLook }) => {
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 {(tpl: any, visible: Eval | null, styles: (scope: Scope) => any)
1175
+ * @type {(styled: StyledRun[], visible: Eval | null, look: Look)
894
1176
  * => (scope: Scope) => any}
895
1177
  */
896
- let cellShape = (tpl, visible, styles) => (scope) =>
897
- opt({ tokens: hidden(visible, scope) ? [] : tpl(scope) }, { style: styles(scope) });
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 tpl = cellValue(def.value, valuePath);
906
- let visible = visibleOf(def, path);
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, NIL), span: 1, box: NO_CELL_LOOK.box };
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(EMPTY, null, NIL), span: 1, box: NO_CELL_LOOK.box };
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 tpl = cellValue(def.value, path + ".value");
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(tpl, visible, look.styles),
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 tpl = cellValue(def.value, path + ".value");
1008
- let visible = visibleOf(def, path);
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 ? cellValue(def.alt, path + ".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 styles = stylesOf(def, path, imageLook(inSlot));
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(EMPTY, null, styles);
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/0056).
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