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/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
|
-
|
|
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/
|
|
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/
|
|
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.
|
|
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": "
|
|
54
|
+
"limit": "14 kB"
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
57
|
"engines": {
|