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/reducers.js CHANGED
@@ -150,6 +150,53 @@ export let namesFor = (role) =>
150
150
  */
151
151
  export let runnerFor = (fn) => REGISTRY[fn].run();
152
152
 
153
+ // Nullish never reaches here, so what is left is a type name -- and only the
154
+ // article in front of it needs a decision.
155
+ /** @type {(value: any) => string} */
156
+ let typeOf = (value) => (typeof value === "object" ? "an object" : "a " + typeof value);
157
+ // One demand, stated twice: what the argument must be, and the sentence that
158
+ // says so.
159
+ /** @type {(ok: (value: any) => boolean, says: string) => (fn: string, value: any) => void} */
160
+ let demands = (ok, says) => (fn, value) => {
161
+ if (value != null && !ok(value)) throw new TypeError(fn + says + typeOf(value));
162
+ };
163
+ let anArray = demands(Array.isArray, " is a reducer over an array, not a scalar function; got ");
164
+ let aLambda = demands(
165
+ (value) => typeof value === "function",
166
+ " takes a lambda as its second argument; got ",
167
+ );
168
+
169
+ // What an author actually wrote, in the words the spec uses for it. A reducer
170
+ // reached an expression as an ordinary registry function, so a call the
171
+ // registry cannot honour used to fold anyway and fail inside: `min(1, 2)`
172
+ // arrived as "(rows || []).map is not a function", the fold's own internals
173
+ // located at the cell but naming neither the reducer nor the mistake
174
+ // (quario-4lys). Two of them did not even fail: `count` and `avg` reach for a
175
+ // `length` before they fold, so `count('ab')` answered 2 -- a string's own
176
+ // length, counted as rows -- and `count(1)` and `avg(1, 2)` answered with
177
+ // nothing at all.
178
+ //
179
+ // The guard wraps the table's own entries because this is the one place the
180
+ // reducer's name is in hand to say. Only one of the two consumers can carry an
181
+ // authored mistake -- `plan.js` spreads this record into an expression's
182
+ // function scope, where the arguments are whatever the author wrote -- while
183
+ // `scope.js` folds rows the engine grouped through a compiled lambda, so
184
+ // neither demand can fire there. That dead check is the price of one door
185
+ // instead of two, and it is a comparison per fold, not per row. Nullish passes
186
+ // both: an absent read is null, so `sum(@.lines)` on a row with no lines folds
187
+ // the empty array, which is the answer the spec's six give.
188
+ //
189
+ // It carries no engine code: the mistake is quario's own verdict on an
190
+ // authored call, like `round`'s bad `n`, and SCHEMA.md's "Validation" tells a
191
+ // host to recognise a located error with `isDiagnostic` rather than by probing
192
+ // a `code` anything could fake.
193
+ /** @type {(fn: string, fold: Reducer) => Reducer} */
194
+ let guarded = (fn, fold) => (rows, of) => {
195
+ anArray(fn, rows);
196
+ aLambda(fn, of);
197
+ return fold(rows, of);
198
+ };
199
+
153
200
  /**
154
201
  * The reducers as a plain name→function record: what an expression's function
155
202
  * scope spreads in, and what a declared aggregate evaluates through. It is the
@@ -161,5 +208,5 @@ export let REDUCERS = Object.fromEntries(
161
208
  // The cast is the derivation's own guarantee: the aggregate role is exactly
162
209
  // the entries carrying a fold, which is why this asks for that role rather
163
210
  // than testing for the fold a second time.
164
- namesFor("aggregate").map((fn) => [fn, /** @type {Reducer} */ (REGISTRY[fn].fold)]),
211
+ namesFor("aggregate").map((fn) => [fn, guarded(fn, /** @type {Reducer} */ (REGISTRY[fn].fold))]),
165
212
  );
package/lib/stream.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // the scalar `display()` rule — re-exported together so event consumers
13
13
  // derive display text from a cell's tokens without hand-rolling the join.
14
14
  export { display, text } from "sjabloon";
15
+ import { record } from "./names.js";
15
16
  import { kindOf } from "./style.js";
16
17
 
17
18
  /** @type {(value: any) => boolean} */
@@ -104,12 +105,83 @@ export let reviveDate = (value) => {
104
105
  * @returns {number | boolean | Date | undefined} The native value, if any.
105
106
  */
106
107
  export function typed(tokens, decl) {
107
- let [token] = tokens;
108
- if (tokens.length !== 1 || !("value" in token)) return undefined;
108
+ if (!oneValue(tokens)) return undefined;
109
109
  // `??` and not `||`: a boolean cell's own `false` is a typed value.
110
+ let [token] = tokens;
110
111
  return asTyped(token.value) ?? asDeclared(token.value, decl);
111
112
  }
112
113
 
114
+ /**
115
+ * Whether a token list renders exactly one value token -- the seam `typed()`
116
+ * gates on, and the same one `format` is read under. A run holding a literal
117
+ * and an interpolation, or two interpolations, presents no single value, so a
118
+ * declaration that presents a value has nothing to speak about (ADR 0061).
119
+ *
120
+ * Internal to the package: `typed()` is the public reading of it, and the
121
+ * traversal asks it of the tokens each style block governs.
122
+ *
123
+ * @type {(tokens: any[]) => boolean}
124
+ */
125
+ export let oneValue = (tokens) => tokens.length === 1 && "value" in tokens[0];
126
+
127
+ // Two objects holding the same own names, each pair judged by `alike`. The
128
+ // one shape both levels of style equality are written in.
129
+ /** @type {(a: any, b: any, alike: (x: any, y: any) => boolean) => boolean} */
130
+ let sameRecord = (a, b, alike) =>
131
+ Object.keys(a).length === Object.keys(b).length &&
132
+ Object.keys(a).every((name) => Object.hasOwn(b, name) && alike(a[name], b[name]));
133
+
134
+ /** @type {(a: any, b: any) => boolean} */
135
+ let strictly = (a, b) => a === b;
136
+
137
+ // Value equality one level deep, which is exhaustive rather than approximate
138
+ // for a resolved style block: `format` is the only object-valued declaration
139
+ // and is itself flat (SCHEMA.md, "Event stream"). Identity short-circuits,
140
+ // which is the common case -- the engine shares one resolved object across a
141
+ // run's tokens.
142
+ /** @type {(a: any, b: any) => boolean} */
143
+ let sameValue = (a, b) => a === b || (record(a) && record(b) ? sameRecord(a, b, strictly) : false);
144
+
145
+ /**
146
+ * Whether two token styles resolve identically. `null` is the absence of one,
147
+ * so an unstyled token and one carrying an empty block are not two cases.
148
+ *
149
+ * @type {(a: any, b: any) => boolean}
150
+ */
151
+ export let sameStyle = (a, b) => a === b || (a && b ? sameRecord(a, b, sameValue) : false);
152
+
153
+ // Whether a token wearing `style` belongs to the run being built.
154
+ /** @type {(last: any, style: any) => boolean} */
155
+ let joins = (last, style) => !!last && sameStyle(last.style, style);
156
+
157
+ /**
158
+ * Group a cell's tokens into its styled runs, in order. Consecutive tokens
159
+ * with equal styles are one run — a lossless rule, because equal styles render
160
+ * identically, so grouping survives a JSON round trip and a target that
161
+ * reserialized the stream reads the same runs back.
162
+ *
163
+ * Shipped so the built-in targets and a custom one cannot drift: the stream
164
+ * carries a cell's runs flattened into one `tokens` array with an optional
165
+ * per-token `style`, and this is the whole rule for reading them back
166
+ * (SCHEMA.md, "Event stream"; ADR 0061).
167
+ *
168
+ * @param {any[]} tokens A cell's tokens.
169
+ * @returns {{ style: any, tokens: any[] }[]} The runs, each with the style its
170
+ * tokens carry — `null` where they carry none, and the cell's own `style`
171
+ * applies.
172
+ */
173
+ export function styledRuns(tokens) {
174
+ /** @type {{ style: any, tokens: any[] }[]} */
175
+ let out = [];
176
+ for (let token of tokens) {
177
+ let style = token.style ?? null;
178
+ let last = out[out.length - 1];
179
+ if (joins(last, style)) last.tokens.push(token);
180
+ else out.push({ style, tokens: [token] });
181
+ }
182
+ return out;
183
+ }
184
+
113
185
  // The report's own bands, by the role their items wear (CONTEXT.md, "Band").
114
186
  // The page ones are in it although no walk emits them: the question is whose
115
187
  // band a role names, not what a walk hands over (ADR 0028).
package/lib/style.js CHANGED
@@ -207,6 +207,14 @@ let checkFormat = (value) => {
207
207
  // in. They share every site, so the places that refuse one refuse both from
208
208
  // this list rather than from two lists that have to be compared (ADR 0041).
209
209
  let VALUE_NAMES = ["format", "currency"];
210
+ /**
211
+ * Whether a name is one of the two. The counterpart of `isBoxName`, and the
212
+ * one place the question is answered: the traversal withdraws both together
213
+ * from a style whose tokens are not a single value token (ADR 0061).
214
+ *
215
+ * @type {(name: string) => boolean}
216
+ */
217
+ export let isValueName = (name) => VALUE_NAMES.includes(name);
210
218
  let POINTS = spec(
211
219
  "expected a non-negative number of points",
212
220
  (value) => finite(value) && value >= 0,
@@ -326,6 +334,44 @@ let withoutNames = (names, check, subject) => (name, value) =>
326
334
  let withoutFlow = (check, subject) => withoutNames(FLOW_NAMES, check, subject);
327
335
 
328
336
  export let checkCellStyle = withoutFlow(checkStyle, "a table cell");
337
+ // The inline half of the vocabulary: what a stretch of text inside a cell can
338
+ // wear. The box and flow spacing describe a block and `align` describes a
339
+ // line, and neither means anything on part of one -- so a styled run is
340
+ // narrowed from the other end than an image item is, which keeps only what a
341
+ // picture can wear (SCHEMA.md, "Style declarations").
342
+ let RUN_STYLES = [
343
+ "family",
344
+ "size",
345
+ "bold",
346
+ "italic",
347
+ "underline",
348
+ "strikethrough",
349
+ "color",
350
+ "background",
351
+ "uppercase",
352
+ "format",
353
+ "currency",
354
+ ];
355
+ /**
356
+ * The inline half as a list, in the vocabulary's own order — the same bargain
357
+ * `STYLE_NAMES` is: a tool that offers what a styled run may wear (the
358
+ * editor's style rail) imports this rather than keeping a copy that has to be
359
+ * told about every change here.
360
+ */
361
+ /** @type {readonly string[]} */
362
+ export let RUN_STYLE_NAMES = Object.freeze(STYLE_NAMES.filter((name) => RUN_STYLES.includes(name)));
363
+ let RUN = new Set(RUN_STYLES);
364
+ /**
365
+ * Whether a name is one a styled run may wear. The counterpart of
366
+ * `isBoxName`, and what the traversal composes a run's block through: a cell's
367
+ * declarations reach its runs, but only the ones a run could have declared
368
+ * itself, so a cell's padding or `align` never rides down onto a stretch of
369
+ * its text (ADR 0061).
370
+ *
371
+ * @type {(name: string) => boolean}
372
+ */
373
+ export let isRunName = (name) => RUN.has(name);
374
+ export let checkRunStyle = narrowed(RUN_STYLES, "a styled run");
329
375
  export let checkSlotStyle = withoutFlow(checkStyle, "a split slot");
330
376
  // `valign` is legal exactly where a box has height it did not ask for
331
377
  // (CONTEXT.md "Box": slack). A slot's box is the split's height, so a slot --
@@ -384,7 +430,7 @@ export let SKIP = Symbol("skip");
384
430
  * them to a value no target can read. It is the rule for the box too: side
385
431
  * ownership is settled over the names a block declares, before any of this
386
432
  * resolves, so dropping a rejected `borderTopColor` cannot hand the cell its
387
- * row's other two (docs/adr/0056).
433
+ * row's other two (docs/adr/0062).
388
434
  *
389
435
  * **Voided** is `currency` alone. Its fallback is not the layer below but the
390
436
  * instance's own code, so omitting would present one denomination's money
@@ -549,7 +595,7 @@ let BORDER_NAMES = Object.fromEntries(
549
595
  * name it declares whatever the values resolve to. Composing the totals block
550
596
  * into its rows uses it at compile time, so no third style level survives to
551
597
  * render (docs/adr/0049); fanning a row's box onto its cells uses it for the
552
- * same reason (docs/adr/0056).
598
+ * same reason (docs/adr/0062).
553
599
  *
554
600
  * @type {(names: Iterable<string>) => Set<string>}
555
601
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quario",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A tiny, runtime-neutral report engine — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -51,7 +51,7 @@
51
51
  "sjabloon",
52
52
  "padvinder"
53
53
  ],
54
- "limit": "13 kB"
54
+ "limit": "14 kB"
55
55
  }
56
56
  ],
57
57
  "engines": {