quario 0.1.0 → 0.3.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 +191 -0
- package/README.md +19 -4
- package/lib/format.js +60 -0
- package/lib/index.d.ts +210 -13
- package/lib/index.js +157 -69
- package/lib/license.js +1 -1
- package/lib/locate.js +43 -56
- package/lib/plan.js +498 -164
- package/lib/scope.js +4 -10
- package/lib/stream.js +8 -8
- package/lib/style.js +105 -13
- package/package.json +5 -5
package/lib/plan.js
CHANGED
|
@@ -31,21 +31,32 @@
|
|
|
31
31
|
// Each engine's own predicate, not the union `locate.js` exports: every call
|
|
32
32
|
// site knows which engine it just called, and asking that one is what stops a
|
|
33
33
|
// sjabloon-shaped error being trusted where an xprsn error was expected.
|
|
34
|
-
import { isDiagnostic as isQueryDiagnostic, query } from "padvinder";
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
import { isDiagnostic as isQueryDiagnostic, query, relocate as relocateQuery } from "padvinder";
|
|
35
|
+
import {
|
|
36
|
+
compile,
|
|
37
|
+
isDiagnostic as isXprsnDiagnostic,
|
|
38
|
+
relocate as relocateXprsn,
|
|
39
|
+
signatures,
|
|
40
|
+
} from "xprsn";
|
|
41
|
+
import {
|
|
42
|
+
isDiagnostic as isTemplateDiagnostic,
|
|
43
|
+
relocate as relocateTemplate,
|
|
44
|
+
template,
|
|
45
|
+
} from "sjabloon";
|
|
46
|
+
import { LOCATION, fault, isDiagnostic, locate } from "./locate.js";
|
|
38
47
|
import { ANCHORS, BLOCKED, NAME, RESERVED, record } from "./names.js";
|
|
39
48
|
import { AGG, REDUCERS, RUN, aggregateValue, withRow } from "./scope.js";
|
|
40
49
|
import { opt, sniff } from "./stream.js";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
import {
|
|
51
|
+
checkBorderSides,
|
|
52
|
+
checkCellStyle,
|
|
53
|
+
checkImageStyle,
|
|
54
|
+
checkReportStyle,
|
|
55
|
+
checkRowStyle,
|
|
56
|
+
checkSlotImageStyle,
|
|
57
|
+
checkSlotStyle,
|
|
58
|
+
checkStyle,
|
|
59
|
+
} from "./style.js";
|
|
49
60
|
|
|
50
61
|
// Tolerance on the column-width sum. Widths are literal numbers an author may
|
|
51
62
|
// write as thirds, so an exact comparison rejects 33.3 x 3 for a rounding error
|
|
@@ -67,19 +78,58 @@ let isExpr = (value) => typeof value === "string" && value[0] === "=";
|
|
|
67
78
|
let collect = (from, into) => {
|
|
68
79
|
for (let name of from) into.add(name);
|
|
69
80
|
};
|
|
70
|
-
/** @type {(from: Iterable<string>, bound: Set<string>, into: Set<string>) => void} */
|
|
71
|
-
let collectFree = (from, bound, into) => {
|
|
72
|
-
for (let name of from) {
|
|
73
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
74
|
-
bound.has(name) || into.add(name);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
81
|
/** @type {(visible: any, scope: any) => any} */
|
|
78
82
|
let hidden = (visible, scope) => visible && visible(scope) === false;
|
|
83
|
+
|
|
84
|
+
// A text item's closed key set, and the same set inside a split, where the
|
|
85
|
+
// slot's width share joins it.
|
|
86
|
+
let TEXT_KEYS = ["type", "value", "visible", "style"];
|
|
87
|
+
let TEXT_SLOT_KEYS = [...TEXT_KEYS, "width"];
|
|
88
|
+
let IMAGE_KEYS = ["type", "source", "fit", "alt", "visible", "style"];
|
|
89
|
+
let IMAGE_SLOT_KEYS = [...IMAGE_KEYS, "width"];
|
|
90
|
+
|
|
91
|
+
// Which detail events carry the row's running values: the ones holding a cell.
|
|
92
|
+
// A split's bracket holds none, so run rides the item and image events inside
|
|
93
|
+
// it rather than the bracket around them.
|
|
94
|
+
/** @type {(event: any) => boolean} */
|
|
95
|
+
let carriesRun = (event) => event.type === "item" || event.type === "image";
|
|
79
96
|
/** @type {(value: any, path: string, role: string, itemsOf: any) => any} */
|
|
80
97
|
let maybeItems = (value, path, role, itemsOf) =>
|
|
81
98
|
value != null ? itemsOf(value, path, role) : null;
|
|
82
99
|
|
|
100
|
+
// The width-share rule, shared by a table's columns and a split's slots: the
|
|
101
|
+
// two carry the same percentages under the same arithmetic, and one reader for
|
|
102
|
+
// both is what keeps them from drifting into two rules wearing one name.
|
|
103
|
+
/** @type {(sized: boolean, noun: string) => string} */
|
|
104
|
+
let shareMsg = (sized, noun) =>
|
|
105
|
+
sized
|
|
106
|
+
? "expected " + noun + " widths to total at most 100"
|
|
107
|
+
: "expected " +
|
|
108
|
+
noun +
|
|
109
|
+
" widths to total under 100, leaving room for the " +
|
|
110
|
+
noun +
|
|
111
|
+
"s without one";
|
|
112
|
+
// The authored shares worth summing, or null when there is nothing to decide.
|
|
113
|
+
// Nothing authored is nothing to check; a width out of range (a NaN, per the
|
|
114
|
+
// width verdict) already carries its own problem, and one nobody can read
|
|
115
|
+
// leaves it undecidable whether its part is one of those needing room.
|
|
116
|
+
/** @type {(parts: { width?: any }[]) => number[] | null} */
|
|
117
|
+
let sharesOf = (parts) => {
|
|
118
|
+
let shares = parts.map((part) => part.width).filter((width) => width != null);
|
|
119
|
+
return !shares.length || shares.some(Number.isNaN) ? null : shares;
|
|
120
|
+
};
|
|
121
|
+
// Authored shares must leave room for the parts without one. Widths are
|
|
122
|
+
// literal, so the sum is known here; a tolerance keeps three at 33.3 from
|
|
123
|
+
// summing to 100.00000000000001 and failing on IEEE 754 alone.
|
|
124
|
+
/** @type {(bad: any, parts: { width?: any }[], path: string, noun: string) => void} */
|
|
125
|
+
let checkShares = (bad, parts, path, noun) => {
|
|
126
|
+
let shares = sharesOf(parts);
|
|
127
|
+
if (!shares) return;
|
|
128
|
+
let total = shares.reduce((all, width) => all + width, 0);
|
|
129
|
+
let sized = shares.length === parts.length;
|
|
130
|
+
if (total > (sized ? 100 + WIDTH_EPS : 100 - WIDTH_EPS)) bad(path, shareMsg(sized, noun));
|
|
131
|
+
};
|
|
132
|
+
|
|
83
133
|
// One pass over a report's constants: it checks each value, copies it, and
|
|
84
134
|
// freezes the copy. Copying is what keeps a consumer from reaching back into
|
|
85
135
|
// the compiled schema through a nested object; freezing is what stops them
|
|
@@ -202,18 +252,42 @@ let planState = (schema, funcs) => {
|
|
|
202
252
|
let BND = new Set(ANCHORS);
|
|
203
253
|
bindHandles(schema, BND);
|
|
204
254
|
|
|
205
|
-
|
|
255
|
+
// Structured problems: the path was a live value at every site that reports
|
|
256
|
+
// one, so it is kept as a field rather than recovered from the message. The
|
|
257
|
+
// message stays the whole located string — `validate()`'s view is
|
|
258
|
+
// `problems.map((p) => p.message)`, unchanged.
|
|
259
|
+
/** @type {{ path: string, source?: string, message: string, diagnostic?: any }[]} */
|
|
206
260
|
let problems = [];
|
|
207
261
|
// Kept so `events()` rethrows the engine's located error, not a bare one.
|
|
208
262
|
/** @type {any} */
|
|
209
263
|
let firstDiag = null;
|
|
264
|
+
// Where a caught error says it happened: `locate()` stamps that on the copy
|
|
265
|
+
// it throws, so nothing here parses a message back apart.
|
|
266
|
+
/** @type {(error: any) => { path: string, source?: any }} */
|
|
267
|
+
let where = (error) => (error && error[LOCATION]) || { path: "" };
|
|
268
|
+
// A caught error carries its location as data (locate.js), so the problem
|
|
269
|
+
// keeps the path and source structurally; the diagnostic rides along per
|
|
270
|
+
// problem — every problem keeps its offsets, not only the first.
|
|
271
|
+
/** @type {(msg: string, error?: any) => any} */
|
|
272
|
+
let located = (msg, error) => {
|
|
273
|
+
let at = where(error);
|
|
274
|
+
/** @type {any} */
|
|
275
|
+
let problem = { path: at.path, message: msg };
|
|
276
|
+
if (at.source !== undefined) problem.source = String(at.source);
|
|
277
|
+
if (isDiagnostic(error)) problem.diagnostic = error;
|
|
278
|
+
return problem;
|
|
279
|
+
};
|
|
210
280
|
/** @type {(msg: string, error?: any) => void} */
|
|
211
281
|
let add = (msg, error) => {
|
|
212
|
-
|
|
213
|
-
|
|
282
|
+
// Identity, not shape: locate() copies an untrusted host throw with
|
|
283
|
+
// `new error.constructor(...)`, so a host class that stamps `code` on
|
|
284
|
+
// itself produces a copy shaped like a diagnostic. Only an error the
|
|
285
|
+
// engines authenticated is worth rethrowing as one (quario-lkz).
|
|
286
|
+
if (!problems.length && isDiagnostic(error)) firstDiag = error;
|
|
287
|
+
problems.push(located(msg, error));
|
|
214
288
|
};
|
|
215
289
|
/** @type {(path: string, msg: string) => void} */
|
|
216
|
-
let bad = (path, msg) =>
|
|
290
|
+
let bad = (path, msg) => problems.push({ path, message: path + ": " + msg });
|
|
217
291
|
// Record a throwing compile step and stand in a stub, so the traversal reports
|
|
218
292
|
// the whole definition rather than only its first fault.
|
|
219
293
|
/** @type {<T>(fn: () => T, fallback: T) => T} */
|
|
@@ -231,6 +305,22 @@ let planState = (schema, funcs) => {
|
|
|
231
305
|
/** @type {Set<string>} */
|
|
232
306
|
let runNames = new Set();
|
|
233
307
|
|
|
308
|
+
// Which anchors each compiled source actually reads, keyed by its schema
|
|
309
|
+
// path. Retention, not computation: the engines report every root-name read
|
|
310
|
+
// in `.reads` — the unfiltered view `bound` keeps out of `.names` — and this
|
|
311
|
+
// keeps the anchor-and-handle subset a consumer placing a node needs.
|
|
312
|
+
/** @type {Map<string, Set<string>>} */
|
|
313
|
+
let anchors = new Map();
|
|
314
|
+
/** @type {(path: string, reads: { name: string }[]) => void} */
|
|
315
|
+
let anchorsOf = (path, reads) => {
|
|
316
|
+
for (let { name } of reads) {
|
|
317
|
+
if (!BND.has(name)) continue;
|
|
318
|
+
let set = anchors.get(path);
|
|
319
|
+
if (!set) anchors.set(path, (set = new Set()));
|
|
320
|
+
set.add(name);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
234
324
|
return {
|
|
235
325
|
FNS,
|
|
236
326
|
names,
|
|
@@ -238,6 +328,8 @@ let planState = (schema, funcs) => {
|
|
|
238
328
|
BND,
|
|
239
329
|
problems,
|
|
240
330
|
runNames,
|
|
331
|
+
anchors,
|
|
332
|
+
anchorsOf,
|
|
241
333
|
add,
|
|
242
334
|
bad,
|
|
243
335
|
attempt,
|
|
@@ -255,7 +347,7 @@ let compileProp = (source, path, FNS, BND) => {
|
|
|
255
347
|
try {
|
|
256
348
|
return compile(source.slice(1), FNS, { bound: BND });
|
|
257
349
|
} catch (error) {
|
|
258
|
-
locate(path, source, error,
|
|
350
|
+
locate(path, source, error, isXprsnDiagnostic(error) && relocateXprsn, 1);
|
|
259
351
|
}
|
|
260
352
|
};
|
|
261
353
|
/** @type {(expression: any, path: string, source: any) => Eval} */
|
|
@@ -263,23 +355,23 @@ let runProp = (expression, path, source) => (scope) => {
|
|
|
263
355
|
try {
|
|
264
356
|
return expression(scope);
|
|
265
357
|
} catch (error) {
|
|
266
|
-
locate(path, source, error,
|
|
358
|
+
locate(path, source, error, expression.isDiagnostic(error) && relocateXprsn, 1);
|
|
267
359
|
}
|
|
268
360
|
};
|
|
269
|
-
/** @type {(src: string, path: string, source: any, FNS: any) => any} */
|
|
270
|
-
let compileCell = (src, path, source, FNS) => {
|
|
361
|
+
/** @type {(src: string, path: string, source: any, FNS: any, BND: Set<string>) => any} */
|
|
362
|
+
let compileCell = (src, path, source, FNS, BND) => {
|
|
271
363
|
try {
|
|
272
|
-
return template(src, FNS);
|
|
364
|
+
return template(src, FNS, { bound: BND });
|
|
273
365
|
} catch (error) {
|
|
274
|
-
locate(path, source, error,
|
|
366
|
+
locate(path, source, error, isTemplateDiagnostic(error) && relocateTemplate);
|
|
275
367
|
}
|
|
276
368
|
};
|
|
277
|
-
/** @type {(tokens: any, path: string, source: any) => (scope: Scope
|
|
278
|
-
let runCell = (tokens, path, source) => (scope
|
|
369
|
+
/** @type {(tokens: any, path: string, source: any) => (scope: Scope) => any} */
|
|
370
|
+
let runCell = (tokens, path, source) => (scope) => {
|
|
279
371
|
try {
|
|
280
|
-
return tokens(scope
|
|
372
|
+
return tokens.scoped(scope);
|
|
281
373
|
} catch (error) {
|
|
282
|
-
locate(path, source, error,
|
|
374
|
+
locate(path, source, error, tokens.isDiagnostic(error) && relocateTemplate);
|
|
283
375
|
}
|
|
284
376
|
};
|
|
285
377
|
/** @type {(source: any) => string} */
|
|
@@ -293,16 +385,16 @@ let splitFold = (spec) => {
|
|
|
293
385
|
/** @type {(colon: number, path: string) => void} */
|
|
294
386
|
let checkCount = (colon, path) => {
|
|
295
387
|
// oxlint-disable-next-line no-unused-expressions
|
|
296
|
-
colon < 0 ||
|
|
388
|
+
colon < 0 || fault(path, "count does not take an expression");
|
|
297
389
|
};
|
|
298
390
|
/** @type {(src: string, fn: string, path: string) => void} */
|
|
299
391
|
let checkExprFold = (src, fn, path) => {
|
|
300
392
|
// oxlint-disable-next-line no-unused-expressions
|
|
301
|
-
(src.startsWith("=") && src.length > 1) ||
|
|
393
|
+
(src.startsWith("=") && src.length > 1) || fault(path, 'expected "' + fn + ':=<expression>"');
|
|
302
394
|
};
|
|
303
395
|
|
|
304
396
|
/** @param {State} state */
|
|
305
|
-
let primitives = ({ FNS, BND, names, functions }) => {
|
|
397
|
+
let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
306
398
|
// A computed property: a literal, or an `=` xprsn expression. xprsn's
|
|
307
399
|
// `bound` keeps engine-provided anchors/handles out of the reported `names`.
|
|
308
400
|
/** @type {(spec: any, path: string) => Eval} */
|
|
@@ -311,23 +403,27 @@ let primitives = ({ FNS, BND, names, functions }) => {
|
|
|
311
403
|
let expression = compileProp(source, path, FNS, BND);
|
|
312
404
|
collect(expression.names, names);
|
|
313
405
|
collect(expression.functions, functions);
|
|
406
|
+
anchorsOf(path, expression.reads);
|
|
314
407
|
return runProp(expression, path, source);
|
|
315
408
|
};
|
|
316
409
|
|
|
317
|
-
// A text cell: a sjabloon template, compiled with the token edition
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// `
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
410
|
+
// A text cell: a sjabloon template, compiled with the token edition and
|
|
411
|
+
// rendered over quario's own scope chain, which already binds the anchors:
|
|
412
|
+
// `$` at the render base, `@` on the detail row, neither anywhere else — so
|
|
413
|
+
// a group or report band leaves `@` unbound and a stray `@.field` throws.
|
|
414
|
+
// sjabloon's `bound` excludes its loop vars / `$` / `@` and quario's engine
|
|
415
|
+
// names alike from the reported `names`. A render yields the cell's public
|
|
416
|
+
// token stream: literal runs verbatim plus each interpolation's
|
|
417
|
+
// pre-stringify value. Templates have no raw form — a `{{{ }}}` tag is a
|
|
418
|
+
// located SJABLOON_RAW_TAG definition error — so escaping is entirely a
|
|
419
|
+
// render-target concern at the markup edge, with no exceptions to carry
|
|
420
|
+
// through the stream.
|
|
421
|
+
/** @type {(str: any, path: string) => (scope: Scope) => any} */
|
|
327
422
|
let cell = (source, path) => {
|
|
328
|
-
let tokens = compileCell(asText(source), path, source, FNS);
|
|
329
|
-
|
|
423
|
+
let tokens = compileCell(asText(source), path, source, FNS, BND);
|
|
424
|
+
collect(tokens.names, names);
|
|
330
425
|
collect(tokens.functions, functions);
|
|
426
|
+
anchorsOf(path, tokens.reads);
|
|
331
427
|
return runCell(tokens, path, source);
|
|
332
428
|
};
|
|
333
429
|
|
|
@@ -337,7 +433,7 @@ let primitives = ({ FNS, BND, names, functions }) => {
|
|
|
337
433
|
let parseFold = (spec, path, allowed) => {
|
|
338
434
|
let { fn, src, colon } = splitFold(spec);
|
|
339
435
|
// oxlint-disable-next-line no-unused-expressions
|
|
340
|
-
allowed.has(fn) ||
|
|
436
|
+
allowed.has(fn) || fault(path, 'unknown reducer "' + fn + '"');
|
|
341
437
|
if (fn === "count") checkCount(colon, path);
|
|
342
438
|
else checkExprFold(src, fn, path);
|
|
343
439
|
return { fn, per: src ? prop(src, path) : null };
|
|
@@ -371,10 +467,16 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
371
467
|
};
|
|
372
468
|
// Every schema object is a closed set, so typos and misplaced fields
|
|
373
469
|
// (page.size, sort on a table detail) fail loud instead of doing nothing.
|
|
470
|
+
// One schema path from a parent's and a key. The document root's path is the
|
|
471
|
+
// empty string, so the dot is conditional -- `style.family`, never
|
|
472
|
+
// `.style.family`. Every reader that can be reached with the root joins
|
|
473
|
+
// through here rather than restating the rule.
|
|
474
|
+
/** @type {(path: string, key: string) => string} */
|
|
475
|
+
let pathTo = (path, key) => (path ? path + "." + key : key);
|
|
374
476
|
/** @type {(value: any, allowed: string[], path: string) => void} */
|
|
375
477
|
let keys = (value, allowed, path) => {
|
|
376
478
|
for (let key of Object.keys(value))
|
|
377
|
-
if (!allowed.includes(key)) bad(path
|
|
479
|
+
if (!allowed.includes(key)) bad(pathTo(path, key), 'unknown key "' + key + '"');
|
|
378
480
|
};
|
|
379
481
|
/** @type {(name: string, path: string, reserved?: Set<string> | null) => void} */
|
|
380
482
|
let checkName = (name, path, reserved) => {
|
|
@@ -448,7 +550,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
448
550
|
/** @type {[string, Eval][]} */
|
|
449
551
|
let entries = [];
|
|
450
552
|
for (let [name, value] of Object.entries(block)) {
|
|
451
|
-
let stylePath = path + ".
|
|
553
|
+
let stylePath = path + "." + name;
|
|
452
554
|
let problem = check(name, value);
|
|
453
555
|
if (problem) bad(stylePath, problem);
|
|
454
556
|
else entries.push([name, attempt(() => prop(value, stylePath), NIL)]);
|
|
@@ -470,6 +572,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
470
572
|
*/
|
|
471
573
|
let compileStyles = (block, path, check) => {
|
|
472
574
|
let entries = readStyles(block, path, check);
|
|
575
|
+
for (let [name, msg] of checkBorderSides(block, check)) bad(path + "." + name, msg);
|
|
473
576
|
if (!entries.length) return NIL;
|
|
474
577
|
let resolve = styleFn(entries);
|
|
475
578
|
if (isComputed(block)) return resolve;
|
|
@@ -482,8 +585,9 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
482
585
|
*/
|
|
483
586
|
let stylesOf = (def, path, check = checkStyle) => {
|
|
484
587
|
let block = def.style;
|
|
485
|
-
|
|
486
|
-
|
|
588
|
+
let stylePath = pathTo(path, "style");
|
|
589
|
+
if (block == null || !obj(block, stylePath)) return NIL;
|
|
590
|
+
return compileStyles(block, stylePath, check);
|
|
487
591
|
};
|
|
488
592
|
// A cell value: one sjabloon template. Emphasis is the cell's own `style`;
|
|
489
593
|
// markup in literal text is meaningful only to the HTML target.
|
|
@@ -617,28 +721,26 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
617
721
|
let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, stylesOf }) => {
|
|
618
722
|
// `tokens` plus its truthy `style`. A hidden cell keeps its slot with no
|
|
619
723
|
// tokens so a column keeps its alignment; items pass `visible` null and drop
|
|
620
|
-
// themselves instead, having no slot to keep.
|
|
724
|
+
// themselves instead, having no slot to keep. The anchor story lives on
|
|
725
|
+
// `cell` in primitives: the scope chain itself carries `$` and `@`.
|
|
621
726
|
/**
|
|
622
727
|
* @type {(tpl: any, visible: Eval | null, styles: (scope: Scope) => any)
|
|
623
|
-
* => (scope: Scope
|
|
728
|
+
* => (scope: Scope) => any}
|
|
624
729
|
*/
|
|
625
|
-
let cellShape = (tpl, visible, styles) => (scope
|
|
626
|
-
opt(
|
|
627
|
-
{ tokens: hidden(visible, scope) ? [] : tpl(scope, { root: scope["$"], item }) },
|
|
628
|
-
{ style: styles(scope) },
|
|
629
|
-
);
|
|
730
|
+
let cellShape = (tpl, visible, styles) => (scope) =>
|
|
731
|
+
opt({ tokens: hidden(visible, scope) ? [] : tpl(scope) }, { style: styles(scope) });
|
|
630
732
|
|
|
631
733
|
// A table cell: the shape of a total cell and of an object column header.
|
|
632
|
-
/** @type {(def: any, path: string, valuePath?: string) => (scope: Scope
|
|
734
|
+
/** @type {(def: any, path: string, valuePath?: string) => (scope: Scope) => any} */
|
|
633
735
|
let cellOf = (def, path, valuePath = path + ".value") => {
|
|
634
736
|
let tpl = cellValue(def.value, valuePath);
|
|
635
737
|
let visible = visibleOf(def, path);
|
|
636
|
-
return cellShape(tpl, visible, stylesOf(def, path));
|
|
738
|
+
return cellShape(tpl, visible, stylesOf(def, path, checkCellStyle));
|
|
637
739
|
};
|
|
638
740
|
|
|
639
741
|
// A cell without a row, so `@` stays unbound. A bare string is shorthand for
|
|
640
742
|
// `{ value }` and keeps its authored path, so problems locate where written.
|
|
641
|
-
/** @type {(def: any, path: string) => (scope: Scope
|
|
743
|
+
/** @type {(def: any, path: string) => (scope: Scope) => any} */
|
|
642
744
|
let headerOf = (def, path) => {
|
|
643
745
|
if (typeof def === "string") return cellShape(cellValue(def, path), null, NIL);
|
|
644
746
|
if (!record(def)) {
|
|
@@ -664,14 +766,14 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
664
766
|
let header = headerOf(def.header, path + ".header");
|
|
665
767
|
let tpl = cellValue(def.value, path + ".value");
|
|
666
768
|
let visible = visibleOf(def, path);
|
|
667
|
-
let styles = stylesOf(def, path);
|
|
769
|
+
let styles = stylesOf(def, path, checkCellStyle);
|
|
668
770
|
// A width is a percentage of the table width, resolved by every target.
|
|
669
771
|
// The range verdict is owned here: an unreadable width comes back as NaN,
|
|
670
772
|
// so the total check below reads the verdict instead of re-deciding it.
|
|
671
|
-
return { header, cell: cellShape(tpl, visible, styles), width: widthOf(def.width, path) };
|
|
773
|
+
return { header, cell: cellShape(tpl, visible, styles), width: widthOf(def.width, path), path };
|
|
672
774
|
};
|
|
673
775
|
|
|
674
|
-
/** @type {(def: any, path: string) => ((scope: Scope
|
|
776
|
+
/** @type {(def: any, path: string) => ((scope: Scope) => any) | null} */
|
|
675
777
|
let totalOf = (def, path) => {
|
|
676
778
|
if (!obj(def, path)) return null;
|
|
677
779
|
keys(def, ["value", "visible", "style"], path);
|
|
@@ -679,29 +781,42 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
679
781
|
};
|
|
680
782
|
|
|
681
783
|
// A text item: a cell with no slot to keep, so a hidden one drops itself
|
|
682
|
-
// instead of keeping empty tokens the way a table cell does.
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (def.type !== "text")
|
|
690
|
-
bad(path + ".type", def.type == null ? "required" : 'unknown item type "' + def.type + '"');
|
|
784
|
+
// instead of keeping empty tokens the way a table cell does. Inside a split
|
|
785
|
+
// it is the other way round -- a slot holds its place with empty tokens, so
|
|
786
|
+
// the line's geometry does not move with the data (SCHEMA.md, "Split item").
|
|
787
|
+
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
788
|
+
let textOf = (def, path, role, inSlot = false) => {
|
|
789
|
+
keys(def, inSlot ? TEXT_SLOT_KEYS : TEXT_KEYS, path);
|
|
790
|
+
checkTextType(def.type, path);
|
|
691
791
|
let tpl = cellValue(def.value, path + ".value");
|
|
692
792
|
let visible = visibleOf(def, path);
|
|
693
|
-
let shape = cellShape(
|
|
793
|
+
let shape = cellShape(
|
|
794
|
+
tpl,
|
|
795
|
+
inSlot ? visible : null,
|
|
796
|
+
stylesOf(def, path, inSlot ? checkSlotStyle : checkStyle),
|
|
797
|
+
);
|
|
694
798
|
// The cell shape first, then the item's own fields, so a cell reads
|
|
695
799
|
// the same wherever it appears in the stream.
|
|
696
|
-
return (scope
|
|
697
|
-
if (hidden(visible, scope)) return null;
|
|
698
|
-
let event = shape(scope
|
|
800
|
+
return (scope) => {
|
|
801
|
+
if (!inSlot && hidden(visible, scope)) return null;
|
|
802
|
+
let event = shape(scope);
|
|
699
803
|
event.type = "item";
|
|
700
804
|
event.role = role;
|
|
805
|
+
event.path = path;
|
|
701
806
|
return event;
|
|
702
807
|
};
|
|
703
808
|
};
|
|
704
809
|
|
|
810
|
+
// The fallback shape's verdict on a `type` it does not name, reported after
|
|
811
|
+
// this node's key set and in the documented key order -- and then read as
|
|
812
|
+
// text, so the rest of its keys are checked rather than left behind the one
|
|
813
|
+
// problem.
|
|
814
|
+
/** @type {(type: any, path: string) => void} */
|
|
815
|
+
let checkTextType = (type, path) => {
|
|
816
|
+
if (type === "text") return;
|
|
817
|
+
bad(path + ".type", type == null ? "required" : 'unknown item type "' + type + '"');
|
|
818
|
+
};
|
|
819
|
+
|
|
705
820
|
/** @type {(fit: any, path: string) => void} */
|
|
706
821
|
let checkFit = (fit, path) => {
|
|
707
822
|
if (fit == null || fit === "natural" || fit === "width") return;
|
|
@@ -709,10 +824,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
709
824
|
};
|
|
710
825
|
/** @type {(def: any, path: string) => any} */
|
|
711
826
|
let altOf = (def, path) => (def.alt != null ? cellValue(def.alt, path + ".alt") : null);
|
|
712
|
-
/** @type {(alt: any, scope: Scope
|
|
713
|
-
let imageAlt = (alt, scope
|
|
714
|
-
/** @type {(fields: any, scope: Scope,
|
|
715
|
-
let imageEvent = (fields, scope,
|
|
827
|
+
/** @type {(alt: any, scope: Scope) => any} */
|
|
828
|
+
let imageAlt = (alt, scope) => alt && alt(scope);
|
|
829
|
+
/** @type {(fields: any, scope: Scope, bytes: any) => any} */
|
|
830
|
+
let imageEvent = (fields, scope, bytes) => {
|
|
716
831
|
// The bytes stay bytes. Sniffing reads their magic numbers -- it never
|
|
717
832
|
// decodes the image, and nothing here turns them into a string -- and
|
|
718
833
|
// the answer rides on the event so no target sniffs a second time.
|
|
@@ -728,52 +843,127 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
728
843
|
TypeError("expected a Uint8Array of PNG or JPEG bytes"),
|
|
729
844
|
);
|
|
730
845
|
return opt(
|
|
731
|
-
{ type: "image", role: fields.role, bytes, format, fit: fields.fit },
|
|
732
|
-
{ alt: imageAlt(fields.alt, scope
|
|
846
|
+
{ type: "image", role: fields.role, bytes, format, fit: fields.fit, path: fields.path },
|
|
847
|
+
{ alt: imageAlt(fields.alt, scope), style: fields.styles(scope) },
|
|
733
848
|
);
|
|
734
849
|
};
|
|
735
|
-
/** @type {(fields: any) => (scope: Scope
|
|
736
|
-
let renderImage = (fields) => (scope
|
|
850
|
+
/** @type {(fields: any) => (scope: Scope) => any} */
|
|
851
|
+
let renderImage = (fields) => (scope) => {
|
|
737
852
|
if (hidden(fields.visible, scope)) return null;
|
|
738
853
|
let bytes = fields.source(scope);
|
|
739
854
|
// Nullish is the missing-data twin of `visible: false`: no event and no
|
|
740
855
|
// error, so a report whose input sometimes lacks a logo needs no guard.
|
|
741
856
|
if (bytes == null) return null;
|
|
742
|
-
return imageEvent(fields, scope,
|
|
857
|
+
return imageEvent(fields, scope, bytes);
|
|
743
858
|
};
|
|
744
859
|
// An image item: bytes an `=` expression yields, sized by `fit`, with an
|
|
745
860
|
// optional textual stand-in. JSON has no way to write bytes, so `source` is
|
|
746
861
|
// expression-only; the vocabulary an image accepts is `style.js`'s to narrow.
|
|
747
|
-
/** @type {(
|
|
748
|
-
let
|
|
749
|
-
|
|
862
|
+
/** @type {(inSlot: boolean) => (name: string, value: any) => string | null} */
|
|
863
|
+
let imageLook = (inSlot) => (inSlot ? checkSlotImageStyle : checkImageStyle);
|
|
864
|
+
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
865
|
+
let imageOf = (def, path, role, inSlot = false) => {
|
|
866
|
+
keys(def, inSlot ? IMAGE_SLOT_KEYS : IMAGE_KEYS, path);
|
|
750
867
|
let source = expression(def.source, path + ".source");
|
|
751
868
|
checkFit(def.fit, path + ".fit");
|
|
752
|
-
|
|
869
|
+
// Read in the documented key order, and each read once: `stylesOf` reports
|
|
870
|
+
// as it reads, so compiling it twice would report this node's style
|
|
871
|
+
// problems twice, and reading it early would report them out of order.
|
|
872
|
+
let alt = altOf(def, path);
|
|
873
|
+
let visible = visibleOf(def, path);
|
|
874
|
+
let styles = stylesOf(def, path, imageLook(inSlot));
|
|
875
|
+
let render = renderImage({
|
|
753
876
|
source,
|
|
754
877
|
fit: def.fit ?? "natural",
|
|
755
|
-
alt
|
|
756
|
-
visible
|
|
757
|
-
styles
|
|
878
|
+
alt,
|
|
879
|
+
visible,
|
|
880
|
+
styles,
|
|
758
881
|
path,
|
|
759
882
|
sourceSpec: def.source,
|
|
760
883
|
role,
|
|
761
884
|
});
|
|
885
|
+
if (!inSlot) return render;
|
|
886
|
+
// A slot keeps its place whatever the data does: a hidden image, or a
|
|
887
|
+
// nullish source, leaves the same empty-token placeholder a hidden text
|
|
888
|
+
// slot leaves, so the widths either side of it do not move. `cellShape`
|
|
889
|
+
// with no template is that placeholder, so the two cannot drift.
|
|
890
|
+
let empty = cellShape(EMPTY, null, styles);
|
|
891
|
+
return (scope) => render(scope) ?? { ...empty(scope), type: "item", role };
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// One slot of a split: an ordinary item, plus the width share that is a slot
|
|
895
|
+
// property rather than an item one -- which is why `width` reaches an item's
|
|
896
|
+
// key set only here (docs/adr/0029).
|
|
897
|
+
/**
|
|
898
|
+
* @type {(def: any, path: string, role: string)
|
|
899
|
+
* => { render: (scope: Scope) => any, width: any } | null}
|
|
900
|
+
*/
|
|
901
|
+
let slotOf = (def, path, role) => {
|
|
902
|
+
if (!obj(def, path)) return null;
|
|
903
|
+
// Placement inside placement is the coordinate system quario does not
|
|
904
|
+
// have, so the nesting verdict is named rather than left to read as an
|
|
905
|
+
// unknown item type.
|
|
906
|
+
if (def.type === "split") {
|
|
907
|
+
bad(path + ".type", "a split cannot nest");
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
render: (def.type === "image" ? imageOf : textOf)(def, path, role, true),
|
|
912
|
+
width: widthOf(def.width, path),
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// A split: slots placed across the content width, bracketed in the stream so
|
|
917
|
+
// every slot crosses as the ordinary item or image event it already is.
|
|
918
|
+
/** @type {(def: any, path: string, role: string) => (scope: Scope) => any} */
|
|
919
|
+
let splitOf = (def, path, role) => {
|
|
920
|
+
keys(def, ["type", "slots", "visible", "style"], path);
|
|
921
|
+
let list = arr(def.slots, path + ".slots", true);
|
|
922
|
+
let defs = list || [];
|
|
923
|
+
// Two is the floor: a one-slot split is a text item wearing more syntax,
|
|
924
|
+
// and permitting it would be a second way to say one thing. Read off the
|
|
925
|
+
// array `arr` accepted, so a missing `slots` reports "required" once
|
|
926
|
+
// rather than that and this.
|
|
927
|
+
if (list && defs.length < 2) bad(path + ".slots", "expected at least two slots");
|
|
928
|
+
let slots = defs
|
|
929
|
+
.map((slot, i) => slotOf(slot, path + ".slots[" + i + "]", role))
|
|
930
|
+
.filter((slot) => slot != null);
|
|
931
|
+
checkShares(bad, slots, path + ".slots", "slot");
|
|
932
|
+
let visible = visibleOf(def, path);
|
|
933
|
+
let styles = stylesOf(def, path);
|
|
934
|
+
// Every width is a literal the traversal already resolved, so the geometry
|
|
935
|
+
// the bracket carries is the same array on every render -- built here
|
|
936
|
+
// rather than rebuilt per row in a detail band. Frozen because one array
|
|
937
|
+
// now reaches every render: a consumer must not be able to reach back
|
|
938
|
+
// through an event and change what a later render sees.
|
|
939
|
+
let geometry = Object.freeze(
|
|
940
|
+
slots.map((slot) => Object.freeze(opt({}, { width: slot.width }))),
|
|
941
|
+
);
|
|
942
|
+
return (scope) => {
|
|
943
|
+
if (hidden(visible, scope)) return null;
|
|
944
|
+
let out = [opt({ type: "split-start", role, slots: geometry }, { style: styles(scope) })];
|
|
945
|
+
for (let slot of slots) out.push(slot.render(scope));
|
|
946
|
+
out.push({ type: "split-end" });
|
|
947
|
+
return out;
|
|
948
|
+
};
|
|
762
949
|
};
|
|
763
950
|
|
|
764
|
-
/** @type {(def: any, itemPath: string, role: string) => ((scope: Scope
|
|
951
|
+
/** @type {(def: any, itemPath: string, role: string) => ((scope: Scope) => any) | null} */
|
|
765
952
|
let itemOf = (def, itemPath, role) => {
|
|
766
953
|
if (!obj(def, itemPath)) return null;
|
|
767
954
|
// `type` picks the item's shape; `textOf` owns the verdict on the ones
|
|
768
955
|
// it does not name, having the closed key set that reports first.
|
|
956
|
+
if (def.type === "split") return splitOf(def, itemPath, role);
|
|
769
957
|
return (def.type === "image" ? imageOf : textOf)(def, itemPath, role);
|
|
770
958
|
};
|
|
771
|
-
/** @type {(list: ((scope: Scope
|
|
959
|
+
/** @type {(list: ((scope: Scope) => any)[]) => (scope: Scope) => Generator<any>} */
|
|
772
960
|
let yieldItems = (list) =>
|
|
773
|
-
function* (scope
|
|
961
|
+
function* (scope) {
|
|
774
962
|
for (let render of list) {
|
|
775
|
-
let event = render(scope
|
|
776
|
-
|
|
963
|
+
let event = render(scope);
|
|
964
|
+
// A split renders to its whole bracket; every other item to one event.
|
|
965
|
+
if (Array.isArray(event)) yield* event;
|
|
966
|
+
else if (event) yield event;
|
|
777
967
|
}
|
|
778
968
|
};
|
|
779
969
|
|
|
@@ -781,10 +971,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
781
971
|
// `q-<role>` class, which print CSS targets for styling and breaks.
|
|
782
972
|
/**
|
|
783
973
|
* @type {(value: any, path: string, role: string)
|
|
784
|
-
* => (scope: Scope
|
|
974
|
+
* => (scope: Scope) => Generator<any>}
|
|
785
975
|
*/
|
|
786
976
|
let itemsOf = (value, path, role) => {
|
|
787
|
-
/** @type {((scope: Scope
|
|
977
|
+
/** @type {((scope: Scope) => any)[]} */
|
|
788
978
|
let list = [];
|
|
789
979
|
for (let [i, def] of (arr(value, path) || []).entries()) {
|
|
790
980
|
let render = itemOf(def, path + "[" + i + "]", role);
|
|
@@ -812,14 +1002,15 @@ let bands = (
|
|
|
812
1002
|
{ columnOf, itemsOf, totalOf },
|
|
813
1003
|
) => {
|
|
814
1004
|
// Stacked items, one pass per row.
|
|
815
|
-
/** @type {(items: (scope: Scope
|
|
1005
|
+
/** @type {(items: (scope: Scope) => Generator<any>) => Band} */
|
|
816
1006
|
let itemsBand = (items) =>
|
|
817
1007
|
function* (rows, scope, runners) {
|
|
818
1008
|
for (let row of rows) {
|
|
819
1009
|
let scopeOfRow = runners.bind(scope, row);
|
|
820
1010
|
// Detail items carry the row's run values as data; group and report
|
|
821
1011
|
// band items never do (they have no current row).
|
|
822
|
-
for (let event of items(scopeOfRow
|
|
1012
|
+
for (let event of items(scopeOfRow))
|
|
1013
|
+
yield carriesRun(event) ? opt(event, { run: scopeOfRow.run }) : event;
|
|
823
1014
|
}
|
|
824
1015
|
};
|
|
825
1016
|
|
|
@@ -830,32 +1021,16 @@ let bands = (
|
|
|
830
1021
|
keys(value.row, ["visible", "style"], "detail.row");
|
|
831
1022
|
return {
|
|
832
1023
|
rowVisible: visibleOf(value.row, "detail.row"),
|
|
833
|
-
rowStyles: stylesOf(value.row, "detail.row"),
|
|
1024
|
+
rowStyles: stylesOf(value.row, "detail.row", checkRowStyle),
|
|
834
1025
|
};
|
|
835
1026
|
};
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
let total = shares.reduce((all, width) => all + width, 0);
|
|
844
|
-
let sized = shares.length === columns.length;
|
|
845
|
-
if (total > (sized ? 100 + WIDTH_EPS : 100 - WIDTH_EPS)) bad("detail.columns", shareMsg(sized));
|
|
846
|
-
};
|
|
847
|
-
/** @type {(columns: any[]) => void} */
|
|
848
|
-
let checkShares = (columns) => {
|
|
849
|
-
// Authored shares must leave room for the columns without one. Widths are
|
|
850
|
-
// literal, so the sum is known here; a tolerance keeps three columns at
|
|
851
|
-
// 33.3 from summing to 100.00000000000001 and failing on IEEE 754 alone.
|
|
852
|
-
// Skipped outright when any authored width is out of range (a NaN, per
|
|
853
|
-
// columnOf's verdict): the document already carries that problem, and a
|
|
854
|
-
// width nobody can read leaves it undecidable whether its column is one
|
|
855
|
-
// of the ones needing room.
|
|
856
|
-
let shares = columns.map((column) => column.width).filter((width) => width != null);
|
|
857
|
-
if (!shares.length || shares.some(Number.isNaN)) return;
|
|
858
|
-
warnShares(columns, shares);
|
|
1027
|
+
// The header-row box: `style` only, a different path from each column's
|
|
1028
|
+
// `header` cell. Absent when the table declares none.
|
|
1029
|
+
/** @type {(value: any) => (scope: Scope) => any} */
|
|
1030
|
+
let headerLook = (value) => {
|
|
1031
|
+
if (value.header == null || !obj(value.header, "detail.header")) return NIL;
|
|
1032
|
+
keys(value.header, ["style"], "detail.header");
|
|
1033
|
+
return stylesOf(value.header, "detail.header", checkRowStyle);
|
|
859
1034
|
};
|
|
860
1035
|
/** @type {(value: any) => { defs: any[] | null, columns: any[] }} */
|
|
861
1036
|
let tableColumns = (value) => {
|
|
@@ -864,22 +1039,54 @@ let bands = (
|
|
|
864
1039
|
let columns = (defs || [])
|
|
865
1040
|
.map((def, i) => columnOf(def, "detail.columns[" + i + "]"))
|
|
866
1041
|
.filter((column) => column != null);
|
|
867
|
-
checkShares(columns);
|
|
1042
|
+
checkShares(bad, columns, "detail.columns", "column");
|
|
868
1043
|
return { defs, columns };
|
|
869
1044
|
};
|
|
870
|
-
/** @type {(totals: any[]
|
|
871
|
-
let
|
|
872
|
-
|
|
873
|
-
|
|
1045
|
+
/** @type {(totals: any[], prefix: string) => any[]} */
|
|
1046
|
+
let mapTotals = (totals, prefix) =>
|
|
1047
|
+
totals
|
|
1048
|
+
.map((def, i) => {
|
|
1049
|
+
let path = prefix + "[" + i + "]";
|
|
1050
|
+
let cell = totalOf(def, path);
|
|
1051
|
+
return cell && { cell, path };
|
|
1052
|
+
})
|
|
1053
|
+
.filter((entry) => entry != null);
|
|
1054
|
+
/** @type {(rows: any[] | null) => any[]} */
|
|
1055
|
+
let nonempty = (rows) => {
|
|
1056
|
+
if (rows && !rows.length) bad("detail.total", "expected at least one total row");
|
|
1057
|
+
return rows || [];
|
|
874
1058
|
};
|
|
875
|
-
/**
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
let tableTotals = (value, defs) =>
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* @type {(value: any, defs: any[] | null) =>
|
|
1061
|
+
* { styles: (scope: Scope) => any, visible: any, cells: any[] }[] | null}
|
|
1062
|
+
*/
|
|
1063
|
+
let tableTotals = (value, defs) =>
|
|
1064
|
+
value.total == null
|
|
1065
|
+
? null
|
|
1066
|
+
: nonempty(arr(value.total, "detail.total", true)).map((def, r) =>
|
|
1067
|
+
totalRowOf(def, "detail.total[" + r + "]", defs),
|
|
1068
|
+
);
|
|
1069
|
+
/** @type {(cells: any[], defs: any[] | null) => boolean} */
|
|
1070
|
+
let aligned = (cells, defs) => !defs || cells.length === defs.length;
|
|
1071
|
+
/** @type {(cells: any[] | null, defs: any[] | null, path: string) => any[]} */
|
|
1072
|
+
let counted = (cells, defs, path) => {
|
|
1073
|
+
if (!cells) return [];
|
|
1074
|
+
if (!aligned(cells, defs)) bad(path + ".cells", "expected one cell per column");
|
|
1075
|
+
return cells;
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* @type {(def: any, path: string, defs: any[] | null) =>
|
|
1079
|
+
* { styles: (scope: Scope) => any, visible: any, cells: any[] }}
|
|
1080
|
+
*/
|
|
1081
|
+
let totalRowOf = (def, path, defs) => {
|
|
1082
|
+
if (!obj(def, path)) return { styles: NIL, visible: null, cells: [] };
|
|
1083
|
+
keys(def, ["cells", "style", "visible"], path);
|
|
1084
|
+
let cells = counted(arr(def.cells, path + ".cells", true), defs, path);
|
|
1085
|
+
return {
|
|
1086
|
+
styles: stylesOf(def, path, checkRowStyle),
|
|
1087
|
+
visible: visibleOf(def, path),
|
|
1088
|
+
cells: mapTotals(cells, path + ".cells"),
|
|
1089
|
+
};
|
|
883
1090
|
};
|
|
884
1091
|
/**
|
|
885
1092
|
* @type {(rows: any[], scope: Scope, runners: RunnerSet, columns: any[],
|
|
@@ -890,30 +1097,57 @@ let bands = (
|
|
|
890
1097
|
let scopeOfRow = runners.bind(scope, row);
|
|
891
1098
|
if (!hidden(rowVisible, scopeOfRow))
|
|
892
1099
|
yield opt(
|
|
893
|
-
{
|
|
1100
|
+
{
|
|
1101
|
+
type: "row",
|
|
1102
|
+
cells: columns.map((column) =>
|
|
1103
|
+
Object.assign(column.cell(scopeOfRow), { path: column.path }),
|
|
1104
|
+
),
|
|
1105
|
+
},
|
|
894
1106
|
{ style: rowStyles(scopeOfRow), run: scopeOfRow.run },
|
|
895
1107
|
);
|
|
896
1108
|
}
|
|
897
1109
|
}
|
|
898
|
-
/**
|
|
899
|
-
|
|
1110
|
+
/**
|
|
1111
|
+
* @type {(total: { styles: (scope: Scope) => any, visible: any, cells: any[] }[] | null,
|
|
1112
|
+
* scope: Scope) => Generator<any>}
|
|
1113
|
+
*/
|
|
1114
|
+
function* emitTotals(total, scope) {
|
|
1115
|
+
if (!total) return;
|
|
1116
|
+
for (let row of total) {
|
|
1117
|
+
if (hidden(row.visible, scope)) continue;
|
|
1118
|
+
yield opt(
|
|
1119
|
+
{
|
|
1120
|
+
type: "total-row",
|
|
1121
|
+
cells: row.cells.map(({ cell, path }) => Object.assign(cell(scope), { path })),
|
|
1122
|
+
},
|
|
1123
|
+
{ style: row.styles(scope) },
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/** @type {(columns: any[], total: { styles: (scope: Scope) => any, visible: any, cells: any[] }[] | null, rowVisible: Eval | null, rowStyles: (scope: Scope) => any, headerStyles: (scope: Scope) => any) => Band} */
|
|
1128
|
+
let tableBand = (columns, total, rowVisible, rowStyles, headerStyles) =>
|
|
900
1129
|
function* (rows, scope, runners) {
|
|
901
|
-
yield
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1130
|
+
yield opt(
|
|
1131
|
+
{
|
|
1132
|
+
type: "table-start",
|
|
1133
|
+
path: "detail",
|
|
1134
|
+
columns: columns.map((column) =>
|
|
1135
|
+
opt({ header: column.header(scope), path: column.path }, { width: column.width }),
|
|
1136
|
+
),
|
|
1137
|
+
},
|
|
1138
|
+
{ style: headerStyles(scope) },
|
|
1139
|
+
);
|
|
907
1140
|
yield* tableRows(rows, scope, runners, columns, rowVisible, rowStyles);
|
|
908
|
-
|
|
1141
|
+
yield* emitTotals(total, scope);
|
|
909
1142
|
yield { type: "table-end" };
|
|
910
1143
|
};
|
|
911
1144
|
/** @type {(value: any) => Band} */
|
|
912
1145
|
let tableOf = (value) => {
|
|
913
|
-
keys(value, ["row", "columns", "total"], "detail");
|
|
1146
|
+
keys(value, ["header", "row", "columns", "total"], "detail");
|
|
1147
|
+
let headerStyles = headerLook(value);
|
|
914
1148
|
let { rowVisible, rowStyles } = rowLook(value);
|
|
915
1149
|
let { defs, columns } = tableColumns(value);
|
|
916
|
-
return tableBand(columns, tableTotals(value, defs), rowVisible, rowStyles);
|
|
1150
|
+
return tableBand(columns, tableTotals(value, defs), rowVisible, rowStyles, headerStyles);
|
|
917
1151
|
};
|
|
918
1152
|
|
|
919
1153
|
// The detail band is polymorphic: an array of items stacks them per row, a
|
|
@@ -983,7 +1217,14 @@ let bands = (
|
|
|
983
1217
|
// A group band has no current row, so `@` stays unbound here.
|
|
984
1218
|
let { groupScope, aggregates, rows: ordered } = openInstance(ctx, key, groupRows, scope);
|
|
985
1219
|
yield opt(
|
|
986
|
-
{
|
|
1220
|
+
{
|
|
1221
|
+
type: "group-start",
|
|
1222
|
+
name: ctx.name,
|
|
1223
|
+
depth: ctx.index,
|
|
1224
|
+
key,
|
|
1225
|
+
aggregates,
|
|
1226
|
+
path: ctx.path,
|
|
1227
|
+
},
|
|
987
1228
|
{ break: ctx.pageBreak, reset: ctx.reset, columns: ctx.columns },
|
|
988
1229
|
);
|
|
989
1230
|
yield* ctx.header(groupScope);
|
|
@@ -1027,6 +1268,7 @@ let bands = (
|
|
|
1027
1268
|
footer: itemsOf(def.footer, path + ".footer", "group-footer"),
|
|
1028
1269
|
inner: bandOf(defs, index + 1, detail, columned || columns != null),
|
|
1029
1270
|
name: def.name,
|
|
1271
|
+
path,
|
|
1030
1272
|
pageBreak: def.break,
|
|
1031
1273
|
reset: def.reset,
|
|
1032
1274
|
columns,
|
|
@@ -1053,9 +1295,12 @@ let bands = (
|
|
|
1053
1295
|
/** @type {(source: string, options: any) => any} */
|
|
1054
1296
|
let compileQuery = (source, options) => {
|
|
1055
1297
|
try {
|
|
1056
|
-
|
|
1298
|
+
// padvinder itself defaults maxDepth to 500 (safety by construction since
|
|
1299
|
+
// 0.6.0), so untrusted deep documents throw typed with no quario-side
|
|
1300
|
+
// budget; hosts override per key — Infinity opts a budget out.
|
|
1301
|
+
return query(source, undefined, options && options.query);
|
|
1057
1302
|
} catch (error) {
|
|
1058
|
-
locate("data", source, error,
|
|
1303
|
+
locate("data", source, error, isQueryDiagnostic(error) && relocateQuery);
|
|
1059
1304
|
}
|
|
1060
1305
|
};
|
|
1061
1306
|
/** @type {(schema: any, obj: any, named: any, bad: any) => any} */
|
|
@@ -1074,17 +1319,87 @@ let selectOf = (schema, bad, attempt, options) => {
|
|
|
1074
1319
|
};
|
|
1075
1320
|
/** @type {(value: any, path: string, expression: any) => any} */
|
|
1076
1321
|
let maybeExpr = (value, path, expression) => (value != null ? expression(value, path) : null);
|
|
1077
|
-
/** @type {(
|
|
1078
|
-
let
|
|
1322
|
+
/** @type {(n: any) => boolean} */
|
|
1323
|
+
let posPts = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
1324
|
+
/** @type {(n: any) => boolean} */
|
|
1325
|
+
let padPts = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
1326
|
+
/** @type {(height: any, bad: any) => number | null} */
|
|
1327
|
+
let headerHeightOf = (height, bad) => {
|
|
1328
|
+
if (height == null) bad("header.height", "required");
|
|
1329
|
+
else if (!posPts(height)) bad("header.height", "expected a positive number of points");
|
|
1330
|
+
return posPts(height) ? height : null;
|
|
1331
|
+
};
|
|
1332
|
+
/** @type {(itemsOf: any) => { items: any, height: number | null }} */
|
|
1333
|
+
let emptyHeader = (itemsOf) => ({ items: itemsOf(null, "header", "report-header"), height: null });
|
|
1334
|
+
/** @type {(value: any, keys: any, itemsOf: any, bad: any) => { items: any, height: number | null }} */
|
|
1335
|
+
let reportHeaderOf = (value, keys, itemsOf, bad) => {
|
|
1336
|
+
if (value == null || Array.isArray(value))
|
|
1337
|
+
return { items: itemsOf(value, "header", "report-header"), height: null };
|
|
1338
|
+
if (!record(value)) {
|
|
1339
|
+
bad("header", "expected an array of items or { height, items }");
|
|
1340
|
+
return emptyHeader(itemsOf);
|
|
1341
|
+
}
|
|
1342
|
+
keys(value, ["height", "items"], "header");
|
|
1343
|
+
return {
|
|
1344
|
+
items: itemsOf(value.items, "header.items", "report-header"),
|
|
1345
|
+
height: headerHeightOf(value.height, bad),
|
|
1346
|
+
};
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
/** @type {(margin: any, bad: any) => number | null} */
|
|
1350
|
+
let pageMarginOf = (margin, bad) => {
|
|
1351
|
+
if (margin != null && !padPts(margin))
|
|
1352
|
+
bad("page.margin", "expected a non-negative number of points");
|
|
1353
|
+
return padPts(margin) ? margin : null;
|
|
1354
|
+
};
|
|
1355
|
+
/** @type {(schema: any, obj: any, keys: any, itemsOf: any, bad: any) => { pageHeader: any, pageFooter: any, margin: number | null }} */
|
|
1356
|
+
let pageBands = (schema, obj, keys, itemsOf, bad) => {
|
|
1079
1357
|
if (schema.page == null || !obj(schema.page, "page"))
|
|
1080
|
-
return { pageHeader: null, pageFooter: null };
|
|
1081
|
-
keys(schema.page, ["header", "footer"], "page");
|
|
1358
|
+
return { pageHeader: null, pageFooter: null, margin: null };
|
|
1359
|
+
keys(schema.page, ["header", "footer", "margin"], "page");
|
|
1082
1360
|
return {
|
|
1083
1361
|
pageHeader: maybeItems(schema.page.header, "page.header", "page-header", itemsOf),
|
|
1084
1362
|
pageFooter: maybeItems(schema.page.footer, "page.footer", "page-footer", itemsOf),
|
|
1363
|
+
margin: pageMarginOf(schema.page.margin, bad),
|
|
1085
1364
|
};
|
|
1086
1365
|
};
|
|
1087
1366
|
|
|
1367
|
+
/** @type {(def: any) => boolean} */
|
|
1368
|
+
let shown = (def) => !(def && def.visible === false);
|
|
1369
|
+
/** @type {(list: any, path: string) => [any, string] | null} */
|
|
1370
|
+
let firstOccupying = (list, path) => {
|
|
1371
|
+
if (!Array.isArray(list)) return null;
|
|
1372
|
+
let i = list.findIndex(shown);
|
|
1373
|
+
return i < 0 ? null : [list[i], path + "[" + i + "]"];
|
|
1374
|
+
};
|
|
1375
|
+
/** @type {(schema: any) => any} */
|
|
1376
|
+
let firstGroupHeader = (schema) => {
|
|
1377
|
+
let groups = schema.groups;
|
|
1378
|
+
return Array.isArray(groups) && groups[0] ? groups[0].header : null;
|
|
1379
|
+
};
|
|
1380
|
+
/** @type {(schema: any) => [any, string] | null} */
|
|
1381
|
+
let nextAfterHeader = (schema) =>
|
|
1382
|
+
firstOccupying(firstGroupHeader(schema), "groups[0].header") ||
|
|
1383
|
+
firstOccupying(schema.detail, "detail");
|
|
1384
|
+
/** @type {(n: any) => boolean} */
|
|
1385
|
+
let nonzeroLead = (n) => typeof n === "number" && n !== 0;
|
|
1386
|
+
/** @type {(next: [any, string], bad: any) => void} */
|
|
1387
|
+
let refuseLiteralLead = (next, bad) => {
|
|
1388
|
+
if (nonzeroLead(next[0].style?.spaceBefore))
|
|
1389
|
+
bad(next[1] + ".style.spaceBefore", "must be 0 after a height-declared header");
|
|
1390
|
+
};
|
|
1391
|
+
/** @type {(schema: any, headerHeight: number | null, bad: any) => void} */
|
|
1392
|
+
let refusePinnedLead = (schema, headerHeight, bad) => {
|
|
1393
|
+
if (headerHeight == null) return;
|
|
1394
|
+
let next = nextAfterHeader(schema);
|
|
1395
|
+
if (next) refuseLiteralLead(next, bad);
|
|
1396
|
+
};
|
|
1397
|
+
/** @type {(headerHeight: number | null, schema: any, bad: any) => void} */
|
|
1398
|
+
let needPinnedMargin = (headerHeight, schema, bad) => {
|
|
1399
|
+
if (headerHeight != null && schema.page?.margin == null)
|
|
1400
|
+
bad("page.margin", "required when header declares height");
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1088
1403
|
/**
|
|
1089
1404
|
* The root descent, in the schema's documented key order. `params` and `data`
|
|
1090
1405
|
* are checked here the way every other key is — by compiling what they declare
|
|
@@ -1102,7 +1417,7 @@ let pageBands = (schema, obj, keys, itemsOf) => {
|
|
|
1102
1417
|
*/
|
|
1103
1418
|
let traverse = (
|
|
1104
1419
|
{ add, bad, attempt, runNames },
|
|
1105
|
-
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf },
|
|
1420
|
+
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf, stylesOf },
|
|
1106
1421
|
{ itemsOf },
|
|
1107
1422
|
{ bandOf },
|
|
1108
1423
|
schema,
|
|
@@ -1122,6 +1437,7 @@ let traverse = (
|
|
|
1122
1437
|
"take",
|
|
1123
1438
|
"aggregates",
|
|
1124
1439
|
"run",
|
|
1440
|
+
"style",
|
|
1125
1441
|
"header",
|
|
1126
1442
|
"empty",
|
|
1127
1443
|
"columns",
|
|
@@ -1142,7 +1458,14 @@ let traverse = (
|
|
|
1142
1458
|
let take = takeOf(schema.take, "take");
|
|
1143
1459
|
let aggs = foldsOf(schema.aggregates, "aggregates", AGG, new Set(["params", "input"]));
|
|
1144
1460
|
let running = foldsOf(schema.run, "run", RUN, null, runNames);
|
|
1145
|
-
|
|
1461
|
+
// The report default: one style block for the whole document, narrowed to
|
|
1462
|
+
// the declarations a typeface is made of. It resolves once per render in
|
|
1463
|
+
// report scope rather than per node, which is why it crosses the seam as a
|
|
1464
|
+
// `report-start` field instead of being merged into every item's style --
|
|
1465
|
+
// nothing here allocates per cell (docs/adr/0033).
|
|
1466
|
+
let style = stylesOf(schema, "", checkReportStyle);
|
|
1467
|
+
let { items: header, height: headerHeight } = reportHeaderOf(schema.header, keys, itemsOf, bad);
|
|
1468
|
+
refusePinnedLead(schema, headerHeight, bad);
|
|
1146
1469
|
// Null when undeclared, so the render decides on the compiled band rather
|
|
1147
1470
|
// than reading `schema.empty` a second time from outside the traversal.
|
|
1148
1471
|
let empty = maybeItems(schema.empty, "empty", "empty", itemsOf);
|
|
@@ -1151,7 +1474,8 @@ let traverse = (
|
|
|
1151
1474
|
let footer = itemsOf(schema.footer, "footer", "report-footer");
|
|
1152
1475
|
// Page bands compile like any other band, but render per page, so the
|
|
1153
1476
|
// stream hands paginated targets closures instead of events.
|
|
1154
|
-
let { pageHeader, pageFooter } = pageBands(schema, obj, keys, itemsOf);
|
|
1477
|
+
let { pageHeader, pageFooter, margin } = pageBands(schema, obj, keys, itemsOf, bad);
|
|
1478
|
+
needPinnedMargin(headerHeight, schema, bad);
|
|
1155
1479
|
return {
|
|
1156
1480
|
select,
|
|
1157
1481
|
params,
|
|
@@ -1160,13 +1484,16 @@ let traverse = (
|
|
|
1160
1484
|
take,
|
|
1161
1485
|
aggs,
|
|
1162
1486
|
running,
|
|
1487
|
+
style,
|
|
1163
1488
|
header,
|
|
1489
|
+
headerHeight,
|
|
1164
1490
|
empty,
|
|
1165
1491
|
columns,
|
|
1166
1492
|
footer,
|
|
1167
1493
|
band,
|
|
1168
1494
|
pageHeader,
|
|
1169
1495
|
pageFooter,
|
|
1496
|
+
margin,
|
|
1170
1497
|
};
|
|
1171
1498
|
};
|
|
1172
1499
|
|
|
@@ -1189,8 +1516,15 @@ export let plan = (schema, funcs, options) => {
|
|
|
1189
1516
|
return {
|
|
1190
1517
|
compiled: traverse(state, read, node, band, schema, options),
|
|
1191
1518
|
problems: state.problems,
|
|
1519
|
+
anchors: state.anchors,
|
|
1192
1520
|
names: state.names,
|
|
1193
|
-
|
|
1521
|
+
// Registry metadata comes from xprsn's one signature convention, described
|
|
1522
|
+
// over just the called subset — every called name resolved from the
|
|
1523
|
+
// registry at compile time, and the rebuilt object keeps the flat name
|
|
1524
|
+
// set's call-first-seen order.
|
|
1525
|
+
functions: signatures(
|
|
1526
|
+
Object.fromEntries(Array.from(state.functions, (name) => [name, state.FNS[name]])),
|
|
1527
|
+
),
|
|
1194
1528
|
diagnostic: state.diagnostic,
|
|
1195
1529
|
};
|
|
1196
1530
|
};
|