quario 0.1.0 → 0.2.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 +138 -0
- package/README.md +19 -4
- package/lib/index.d.ts +154 -10
- package/lib/index.js +109 -67
- package/lib/license.js +1 -1
- package/lib/locate.js +43 -56
- package/lib/plan.js +328 -136
- package/lib/scope.js +4 -10
- package/lib/stream.js +6 -6
- package/lib/style.js +23 -8
- package/package.json +5 -5
package/lib/plan.js
CHANGED
|
@@ -31,21 +31,23 @@
|
|
|
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 { checkImageStyle, checkStyle } from "./style.js";
|
|
42
|
-
|
|
43
|
-
// Default traversal budget for the data query. Render input is untrusted, so
|
|
44
|
-
// without a bound a deep document exhausts the native stack as an opaque
|
|
45
|
-
// RangeError; with it, padvinder throws a typed PADVINDER_MAX_DEPTH diagnostic
|
|
46
|
-
// long before that. Hosts override (or effectively disable) it per key through
|
|
47
|
-
// `options.query`. 500 is ~4x inside the stack limit and far beyond report data.
|
|
48
|
-
let QUERY = { maxDepth: 500 };
|
|
50
|
+
import { checkImageStyle, checkReportStyle, checkStyle } from "./style.js";
|
|
49
51
|
|
|
50
52
|
// Tolerance on the column-width sum. Widths are literal numbers an author may
|
|
51
53
|
// write as thirds, so an exact comparison rejects 33.3 x 3 for a rounding error
|
|
@@ -67,19 +69,58 @@ let isExpr = (value) => typeof value === "string" && value[0] === "=";
|
|
|
67
69
|
let collect = (from, into) => {
|
|
68
70
|
for (let name of from) into.add(name);
|
|
69
71
|
};
|
|
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
72
|
/** @type {(visible: any, scope: any) => any} */
|
|
78
73
|
let hidden = (visible, scope) => visible && visible(scope) === false;
|
|
74
|
+
|
|
75
|
+
// A text item's closed key set, and the same set inside a split, where the
|
|
76
|
+
// slot's width share joins it.
|
|
77
|
+
let TEXT_KEYS = ["type", "value", "visible", "style"];
|
|
78
|
+
let TEXT_SLOT_KEYS = [...TEXT_KEYS, "width"];
|
|
79
|
+
let IMAGE_KEYS = ["type", "source", "fit", "alt", "visible", "style"];
|
|
80
|
+
let IMAGE_SLOT_KEYS = [...IMAGE_KEYS, "width"];
|
|
81
|
+
|
|
82
|
+
// Which detail events carry the row's running values: the ones holding a cell.
|
|
83
|
+
// A split's bracket holds none, so run rides the item and image events inside
|
|
84
|
+
// it rather than the bracket around them.
|
|
85
|
+
/** @type {(event: any) => boolean} */
|
|
86
|
+
let carriesRun = (event) => event.type === "item" || event.type === "image";
|
|
79
87
|
/** @type {(value: any, path: string, role: string, itemsOf: any) => any} */
|
|
80
88
|
let maybeItems = (value, path, role, itemsOf) =>
|
|
81
89
|
value != null ? itemsOf(value, path, role) : null;
|
|
82
90
|
|
|
91
|
+
// The width-share rule, shared by a table's columns and a split's slots: the
|
|
92
|
+
// two carry the same percentages under the same arithmetic, and one reader for
|
|
93
|
+
// both is what keeps them from drifting into two rules wearing one name.
|
|
94
|
+
/** @type {(sized: boolean, noun: string) => string} */
|
|
95
|
+
let shareMsg = (sized, noun) =>
|
|
96
|
+
sized
|
|
97
|
+
? "expected " + noun + " widths to total at most 100"
|
|
98
|
+
: "expected " +
|
|
99
|
+
noun +
|
|
100
|
+
" widths to total under 100, leaving room for the " +
|
|
101
|
+
noun +
|
|
102
|
+
"s without one";
|
|
103
|
+
// The authored shares worth summing, or null when there is nothing to decide.
|
|
104
|
+
// Nothing authored is nothing to check; a width out of range (a NaN, per the
|
|
105
|
+
// width verdict) already carries its own problem, and one nobody can read
|
|
106
|
+
// leaves it undecidable whether its part is one of those needing room.
|
|
107
|
+
/** @type {(parts: { width?: any }[]) => number[] | null} */
|
|
108
|
+
let sharesOf = (parts) => {
|
|
109
|
+
let shares = parts.map((part) => part.width).filter((width) => width != null);
|
|
110
|
+
return !shares.length || shares.some(Number.isNaN) ? null : shares;
|
|
111
|
+
};
|
|
112
|
+
// Authored shares must leave room for the parts without one. Widths are
|
|
113
|
+
// literal, so the sum is known here; a tolerance keeps three at 33.3 from
|
|
114
|
+
// summing to 100.00000000000001 and failing on IEEE 754 alone.
|
|
115
|
+
/** @type {(bad: any, parts: { width?: any }[], path: string, noun: string) => void} */
|
|
116
|
+
let checkShares = (bad, parts, path, noun) => {
|
|
117
|
+
let shares = sharesOf(parts);
|
|
118
|
+
if (!shares) return;
|
|
119
|
+
let total = shares.reduce((all, width) => all + width, 0);
|
|
120
|
+
let sized = shares.length === parts.length;
|
|
121
|
+
if (total > (sized ? 100 + WIDTH_EPS : 100 - WIDTH_EPS)) bad(path, shareMsg(sized, noun));
|
|
122
|
+
};
|
|
123
|
+
|
|
83
124
|
// One pass over a report's constants: it checks each value, copies it, and
|
|
84
125
|
// freezes the copy. Copying is what keeps a consumer from reaching back into
|
|
85
126
|
// the compiled schema through a nested object; freezing is what stops them
|
|
@@ -202,18 +243,42 @@ let planState = (schema, funcs) => {
|
|
|
202
243
|
let BND = new Set(ANCHORS);
|
|
203
244
|
bindHandles(schema, BND);
|
|
204
245
|
|
|
205
|
-
|
|
246
|
+
// Structured problems: the path was a live value at every site that reports
|
|
247
|
+
// one, so it is kept as a field rather than recovered from the message. The
|
|
248
|
+
// message stays the whole located string — `validate()`'s view is
|
|
249
|
+
// `problems.map((p) => p.message)`, unchanged.
|
|
250
|
+
/** @type {{ path: string, source?: string, message: string, diagnostic?: any }[]} */
|
|
206
251
|
let problems = [];
|
|
207
252
|
// Kept so `events()` rethrows the engine's located error, not a bare one.
|
|
208
253
|
/** @type {any} */
|
|
209
254
|
let firstDiag = null;
|
|
255
|
+
// Where a caught error says it happened: `locate()` stamps that on the copy
|
|
256
|
+
// it throws, so nothing here parses a message back apart.
|
|
257
|
+
/** @type {(error: any) => { path: string, source?: any }} */
|
|
258
|
+
let where = (error) => (error && error[LOCATION]) || { path: "" };
|
|
259
|
+
// A caught error carries its location as data (locate.js), so the problem
|
|
260
|
+
// keeps the path and source structurally; the diagnostic rides along per
|
|
261
|
+
// problem — every problem keeps its offsets, not only the first.
|
|
262
|
+
/** @type {(msg: string, error?: any) => any} */
|
|
263
|
+
let located = (msg, error) => {
|
|
264
|
+
let at = where(error);
|
|
265
|
+
/** @type {any} */
|
|
266
|
+
let problem = { path: at.path, message: msg };
|
|
267
|
+
if (at.source !== undefined) problem.source = String(at.source);
|
|
268
|
+
if (isDiagnostic(error)) problem.diagnostic = error;
|
|
269
|
+
return problem;
|
|
270
|
+
};
|
|
210
271
|
/** @type {(msg: string, error?: any) => void} */
|
|
211
272
|
let add = (msg, error) => {
|
|
212
|
-
|
|
213
|
-
|
|
273
|
+
// Identity, not shape: locate() copies an untrusted host throw with
|
|
274
|
+
// `new error.constructor(...)`, so a host class that stamps `code` on
|
|
275
|
+
// itself produces a copy shaped like a diagnostic. Only an error the
|
|
276
|
+
// engines authenticated is worth rethrowing as one (quario-lkz).
|
|
277
|
+
if (!problems.length && isDiagnostic(error)) firstDiag = error;
|
|
278
|
+
problems.push(located(msg, error));
|
|
214
279
|
};
|
|
215
280
|
/** @type {(path: string, msg: string) => void} */
|
|
216
|
-
let bad = (path, msg) =>
|
|
281
|
+
let bad = (path, msg) => problems.push({ path, message: path + ": " + msg });
|
|
217
282
|
// Record a throwing compile step and stand in a stub, so the traversal reports
|
|
218
283
|
// the whole definition rather than only its first fault.
|
|
219
284
|
/** @type {<T>(fn: () => T, fallback: T) => T} */
|
|
@@ -231,6 +296,22 @@ let planState = (schema, funcs) => {
|
|
|
231
296
|
/** @type {Set<string>} */
|
|
232
297
|
let runNames = new Set();
|
|
233
298
|
|
|
299
|
+
// Which anchors each compiled source actually reads, keyed by its schema
|
|
300
|
+
// path. Retention, not computation: the engines report every root-name read
|
|
301
|
+
// in `.reads` — the unfiltered view `bound` keeps out of `.names` — and this
|
|
302
|
+
// keeps the anchor-and-handle subset a consumer placing a node needs.
|
|
303
|
+
/** @type {Map<string, Set<string>>} */
|
|
304
|
+
let anchors = new Map();
|
|
305
|
+
/** @type {(path: string, reads: { name: string }[]) => void} */
|
|
306
|
+
let anchorsOf = (path, reads) => {
|
|
307
|
+
for (let { name } of reads) {
|
|
308
|
+
if (!BND.has(name)) continue;
|
|
309
|
+
let set = anchors.get(path);
|
|
310
|
+
if (!set) anchors.set(path, (set = new Set()));
|
|
311
|
+
set.add(name);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
234
315
|
return {
|
|
235
316
|
FNS,
|
|
236
317
|
names,
|
|
@@ -238,6 +319,8 @@ let planState = (schema, funcs) => {
|
|
|
238
319
|
BND,
|
|
239
320
|
problems,
|
|
240
321
|
runNames,
|
|
322
|
+
anchors,
|
|
323
|
+
anchorsOf,
|
|
241
324
|
add,
|
|
242
325
|
bad,
|
|
243
326
|
attempt,
|
|
@@ -255,7 +338,7 @@ let compileProp = (source, path, FNS, BND) => {
|
|
|
255
338
|
try {
|
|
256
339
|
return compile(source.slice(1), FNS, { bound: BND });
|
|
257
340
|
} catch (error) {
|
|
258
|
-
locate(path, source, error,
|
|
341
|
+
locate(path, source, error, isXprsnDiagnostic(error) && relocateXprsn, 1);
|
|
259
342
|
}
|
|
260
343
|
};
|
|
261
344
|
/** @type {(expression: any, path: string, source: any) => Eval} */
|
|
@@ -263,23 +346,23 @@ let runProp = (expression, path, source) => (scope) => {
|
|
|
263
346
|
try {
|
|
264
347
|
return expression(scope);
|
|
265
348
|
} catch (error) {
|
|
266
|
-
locate(path, source, error,
|
|
349
|
+
locate(path, source, error, expression.isDiagnostic(error) && relocateXprsn, 1);
|
|
267
350
|
}
|
|
268
351
|
};
|
|
269
|
-
/** @type {(src: string, path: string, source: any, FNS: any) => any} */
|
|
270
|
-
let compileCell = (src, path, source, FNS) => {
|
|
352
|
+
/** @type {(src: string, path: string, source: any, FNS: any, BND: Set<string>) => any} */
|
|
353
|
+
let compileCell = (src, path, source, FNS, BND) => {
|
|
271
354
|
try {
|
|
272
|
-
return template(src, FNS);
|
|
355
|
+
return template(src, FNS, { bound: BND });
|
|
273
356
|
} catch (error) {
|
|
274
|
-
locate(path, source, error,
|
|
357
|
+
locate(path, source, error, isTemplateDiagnostic(error) && relocateTemplate);
|
|
275
358
|
}
|
|
276
359
|
};
|
|
277
|
-
/** @type {(tokens: any, path: string, source: any) => (scope: Scope
|
|
278
|
-
let runCell = (tokens, path, source) => (scope
|
|
360
|
+
/** @type {(tokens: any, path: string, source: any) => (scope: Scope) => any} */
|
|
361
|
+
let runCell = (tokens, path, source) => (scope) => {
|
|
279
362
|
try {
|
|
280
|
-
return tokens(scope
|
|
363
|
+
return tokens.scoped(scope);
|
|
281
364
|
} catch (error) {
|
|
282
|
-
locate(path, source, error,
|
|
365
|
+
locate(path, source, error, tokens.isDiagnostic(error) && relocateTemplate);
|
|
283
366
|
}
|
|
284
367
|
};
|
|
285
368
|
/** @type {(source: any) => string} */
|
|
@@ -293,16 +376,16 @@ let splitFold = (spec) => {
|
|
|
293
376
|
/** @type {(colon: number, path: string) => void} */
|
|
294
377
|
let checkCount = (colon, path) => {
|
|
295
378
|
// oxlint-disable-next-line no-unused-expressions
|
|
296
|
-
colon < 0 ||
|
|
379
|
+
colon < 0 || fault(path, "count does not take an expression");
|
|
297
380
|
};
|
|
298
381
|
/** @type {(src: string, fn: string, path: string) => void} */
|
|
299
382
|
let checkExprFold = (src, fn, path) => {
|
|
300
383
|
// oxlint-disable-next-line no-unused-expressions
|
|
301
|
-
(src.startsWith("=") && src.length > 1) ||
|
|
384
|
+
(src.startsWith("=") && src.length > 1) || fault(path, 'expected "' + fn + ':=<expression>"');
|
|
302
385
|
};
|
|
303
386
|
|
|
304
387
|
/** @param {State} state */
|
|
305
|
-
let primitives = ({ FNS, BND, names, functions }) => {
|
|
388
|
+
let primitives = ({ FNS, BND, names, functions, anchorsOf }) => {
|
|
306
389
|
// A computed property: a literal, or an `=` xprsn expression. xprsn's
|
|
307
390
|
// `bound` keeps engine-provided anchors/handles out of the reported `names`.
|
|
308
391
|
/** @type {(spec: any, path: string) => Eval} */
|
|
@@ -311,23 +394,27 @@ let primitives = ({ FNS, BND, names, functions }) => {
|
|
|
311
394
|
let expression = compileProp(source, path, FNS, BND);
|
|
312
395
|
collect(expression.names, names);
|
|
313
396
|
collect(expression.functions, functions);
|
|
397
|
+
anchorsOf(path, expression.reads);
|
|
314
398
|
return runProp(expression, path, source);
|
|
315
399
|
};
|
|
316
400
|
|
|
317
|
-
// A text cell: a sjabloon template, compiled with the token edition
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// `
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
401
|
+
// A text cell: a sjabloon template, compiled with the token edition and
|
|
402
|
+
// rendered over quario's own scope chain, which already binds the anchors:
|
|
403
|
+
// `$` at the render base, `@` on the detail row, neither anywhere else — so
|
|
404
|
+
// a group or report band leaves `@` unbound and a stray `@.field` throws.
|
|
405
|
+
// sjabloon's `bound` excludes its loop vars / `$` / `@` and quario's engine
|
|
406
|
+
// names alike from the reported `names`. A render yields the cell's public
|
|
407
|
+
// token stream: literal runs verbatim plus each interpolation's
|
|
408
|
+
// pre-stringify value. Templates have no raw form — a `{{{ }}}` tag is a
|
|
409
|
+
// located SJABLOON_RAW_TAG definition error — so escaping is entirely a
|
|
410
|
+
// render-target concern at the markup edge, with no exceptions to carry
|
|
411
|
+
// through the stream.
|
|
412
|
+
/** @type {(str: any, path: string) => (scope: Scope) => any} */
|
|
327
413
|
let cell = (source, path) => {
|
|
328
|
-
let tokens = compileCell(asText(source), path, source, FNS);
|
|
329
|
-
|
|
414
|
+
let tokens = compileCell(asText(source), path, source, FNS, BND);
|
|
415
|
+
collect(tokens.names, names);
|
|
330
416
|
collect(tokens.functions, functions);
|
|
417
|
+
anchorsOf(path, tokens.reads);
|
|
331
418
|
return runCell(tokens, path, source);
|
|
332
419
|
};
|
|
333
420
|
|
|
@@ -337,7 +424,7 @@ let primitives = ({ FNS, BND, names, functions }) => {
|
|
|
337
424
|
let parseFold = (spec, path, allowed) => {
|
|
338
425
|
let { fn, src, colon } = splitFold(spec);
|
|
339
426
|
// oxlint-disable-next-line no-unused-expressions
|
|
340
|
-
allowed.has(fn) ||
|
|
427
|
+
allowed.has(fn) || fault(path, 'unknown reducer "' + fn + '"');
|
|
341
428
|
if (fn === "count") checkCount(colon, path);
|
|
342
429
|
else checkExprFold(src, fn, path);
|
|
343
430
|
return { fn, per: src ? prop(src, path) : null };
|
|
@@ -371,10 +458,16 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
371
458
|
};
|
|
372
459
|
// Every schema object is a closed set, so typos and misplaced fields
|
|
373
460
|
// (page.size, sort on a table detail) fail loud instead of doing nothing.
|
|
461
|
+
// One schema path from a parent's and a key. The document root's path is the
|
|
462
|
+
// empty string, so the dot is conditional -- `style.family`, never
|
|
463
|
+
// `.style.family`. Every reader that can be reached with the root joins
|
|
464
|
+
// through here rather than restating the rule.
|
|
465
|
+
/** @type {(path: string, key: string) => string} */
|
|
466
|
+
let pathTo = (path, key) => (path ? path + "." + key : key);
|
|
374
467
|
/** @type {(value: any, allowed: string[], path: string) => void} */
|
|
375
468
|
let keys = (value, allowed, path) => {
|
|
376
469
|
for (let key of Object.keys(value))
|
|
377
|
-
if (!allowed.includes(key)) bad(path
|
|
470
|
+
if (!allowed.includes(key)) bad(pathTo(path, key), 'unknown key "' + key + '"');
|
|
378
471
|
};
|
|
379
472
|
/** @type {(name: string, path: string, reserved?: Set<string> | null) => void} */
|
|
380
473
|
let checkName = (name, path, reserved) => {
|
|
@@ -448,7 +541,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
448
541
|
/** @type {[string, Eval][]} */
|
|
449
542
|
let entries = [];
|
|
450
543
|
for (let [name, value] of Object.entries(block)) {
|
|
451
|
-
let stylePath = path + ".
|
|
544
|
+
let stylePath = path + "." + name;
|
|
452
545
|
let problem = check(name, value);
|
|
453
546
|
if (problem) bad(stylePath, problem);
|
|
454
547
|
else entries.push([name, attempt(() => prop(value, stylePath), NIL)]);
|
|
@@ -482,8 +575,9 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
482
575
|
*/
|
|
483
576
|
let stylesOf = (def, path, check = checkStyle) => {
|
|
484
577
|
let block = def.style;
|
|
485
|
-
|
|
486
|
-
|
|
578
|
+
let stylePath = pathTo(path, "style");
|
|
579
|
+
if (block == null || !obj(block, stylePath)) return NIL;
|
|
580
|
+
return compileStyles(block, stylePath, check);
|
|
487
581
|
};
|
|
488
582
|
// A cell value: one sjabloon template. Emphasis is the cell's own `style`;
|
|
489
583
|
// markup in literal text is meaningful only to the HTML target.
|
|
@@ -617,19 +711,17 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
617
711
|
let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, stylesOf }) => {
|
|
618
712
|
// `tokens` plus its truthy `style`. A hidden cell keeps its slot with no
|
|
619
713
|
// tokens so a column keeps its alignment; items pass `visible` null and drop
|
|
620
|
-
// themselves instead, having no slot to keep.
|
|
714
|
+
// themselves instead, having no slot to keep. The anchor story lives on
|
|
715
|
+
// `cell` in primitives: the scope chain itself carries `$` and `@`.
|
|
621
716
|
/**
|
|
622
717
|
* @type {(tpl: any, visible: Eval | null, styles: (scope: Scope) => any)
|
|
623
|
-
* => (scope: Scope
|
|
718
|
+
* => (scope: Scope) => any}
|
|
624
719
|
*/
|
|
625
|
-
let cellShape = (tpl, visible, styles) => (scope
|
|
626
|
-
opt(
|
|
627
|
-
{ tokens: hidden(visible, scope) ? [] : tpl(scope, { root: scope["$"], item }) },
|
|
628
|
-
{ style: styles(scope) },
|
|
629
|
-
);
|
|
720
|
+
let cellShape = (tpl, visible, styles) => (scope) =>
|
|
721
|
+
opt({ tokens: hidden(visible, scope) ? [] : tpl(scope) }, { style: styles(scope) });
|
|
630
722
|
|
|
631
723
|
// 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
|
|
724
|
+
/** @type {(def: any, path: string, valuePath?: string) => (scope: Scope) => any} */
|
|
633
725
|
let cellOf = (def, path, valuePath = path + ".value") => {
|
|
634
726
|
let tpl = cellValue(def.value, valuePath);
|
|
635
727
|
let visible = visibleOf(def, path);
|
|
@@ -638,7 +730,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
638
730
|
|
|
639
731
|
// A cell without a row, so `@` stays unbound. A bare string is shorthand for
|
|
640
732
|
// `{ value }` and keeps its authored path, so problems locate where written.
|
|
641
|
-
/** @type {(def: any, path: string) => (scope: Scope
|
|
733
|
+
/** @type {(def: any, path: string) => (scope: Scope) => any} */
|
|
642
734
|
let headerOf = (def, path) => {
|
|
643
735
|
if (typeof def === "string") return cellShape(cellValue(def, path), null, NIL);
|
|
644
736
|
if (!record(def)) {
|
|
@@ -668,10 +760,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
668
760
|
// A width is a percentage of the table width, resolved by every target.
|
|
669
761
|
// The range verdict is owned here: an unreadable width comes back as NaN,
|
|
670
762
|
// 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) };
|
|
763
|
+
return { header, cell: cellShape(tpl, visible, styles), width: widthOf(def.width, path), path };
|
|
672
764
|
};
|
|
673
765
|
|
|
674
|
-
/** @type {(def: any, path: string) => ((scope: Scope
|
|
766
|
+
/** @type {(def: any, path: string) => ((scope: Scope) => any) | null} */
|
|
675
767
|
let totalOf = (def, path) => {
|
|
676
768
|
if (!obj(def, path)) return null;
|
|
677
769
|
keys(def, ["value", "visible", "style"], path);
|
|
@@ -679,29 +771,38 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
679
771
|
};
|
|
680
772
|
|
|
681
773
|
// 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 + '"');
|
|
774
|
+
// instead of keeping empty tokens the way a table cell does. Inside a split
|
|
775
|
+
// it is the other way round -- a slot holds its place with empty tokens, so
|
|
776
|
+
// the line's geometry does not move with the data (SCHEMA.md, "Split item").
|
|
777
|
+
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
778
|
+
let textOf = (def, path, role, inSlot = false) => {
|
|
779
|
+
keys(def, inSlot ? TEXT_SLOT_KEYS : TEXT_KEYS, path);
|
|
780
|
+
checkTextType(def.type, path);
|
|
691
781
|
let tpl = cellValue(def.value, path + ".value");
|
|
692
782
|
let visible = visibleOf(def, path);
|
|
693
|
-
let shape = cellShape(tpl, null, stylesOf(def, path));
|
|
783
|
+
let shape = cellShape(tpl, inSlot ? visible : null, stylesOf(def, path));
|
|
694
784
|
// The cell shape first, then the item's own fields, so a cell reads
|
|
695
785
|
// the same wherever it appears in the stream.
|
|
696
|
-
return (scope
|
|
697
|
-
if (hidden(visible, scope)) return null;
|
|
698
|
-
let event = shape(scope
|
|
786
|
+
return (scope) => {
|
|
787
|
+
if (!inSlot && hidden(visible, scope)) return null;
|
|
788
|
+
let event = shape(scope);
|
|
699
789
|
event.type = "item";
|
|
700
790
|
event.role = role;
|
|
791
|
+
event.path = path;
|
|
701
792
|
return event;
|
|
702
793
|
};
|
|
703
794
|
};
|
|
704
795
|
|
|
796
|
+
// The fallback shape's verdict on a `type` it does not name, reported after
|
|
797
|
+
// this node's key set and in the documented key order -- and then read as
|
|
798
|
+
// text, so the rest of its keys are checked rather than left behind the one
|
|
799
|
+
// problem.
|
|
800
|
+
/** @type {(type: any, path: string) => void} */
|
|
801
|
+
let checkTextType = (type, path) => {
|
|
802
|
+
if (type === "text") return;
|
|
803
|
+
bad(path + ".type", type == null ? "required" : 'unknown item type "' + type + '"');
|
|
804
|
+
};
|
|
805
|
+
|
|
705
806
|
/** @type {(fit: any, path: string) => void} */
|
|
706
807
|
let checkFit = (fit, path) => {
|
|
707
808
|
if (fit == null || fit === "natural" || fit === "width") return;
|
|
@@ -709,10 +810,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
709
810
|
};
|
|
710
811
|
/** @type {(def: any, path: string) => any} */
|
|
711
812
|
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,
|
|
813
|
+
/** @type {(alt: any, scope: Scope) => any} */
|
|
814
|
+
let imageAlt = (alt, scope) => alt && alt(scope);
|
|
815
|
+
/** @type {(fields: any, scope: Scope, bytes: any) => any} */
|
|
816
|
+
let imageEvent = (fields, scope, bytes) => {
|
|
716
817
|
// The bytes stay bytes. Sniffing reads their magic numbers -- it never
|
|
717
818
|
// decodes the image, and nothing here turns them into a string -- and
|
|
718
819
|
// the answer rides on the event so no target sniffs a second time.
|
|
@@ -728,52 +829,125 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
728
829
|
TypeError("expected a Uint8Array of PNG or JPEG bytes"),
|
|
729
830
|
);
|
|
730
831
|
return opt(
|
|
731
|
-
{ type: "image", role: fields.role, bytes, format, fit: fields.fit },
|
|
732
|
-
{ alt: imageAlt(fields.alt, scope
|
|
832
|
+
{ type: "image", role: fields.role, bytes, format, fit: fields.fit, path: fields.path },
|
|
833
|
+
{ alt: imageAlt(fields.alt, scope), style: fields.styles(scope) },
|
|
733
834
|
);
|
|
734
835
|
};
|
|
735
|
-
/** @type {(fields: any) => (scope: Scope
|
|
736
|
-
let renderImage = (fields) => (scope
|
|
836
|
+
/** @type {(fields: any) => (scope: Scope) => any} */
|
|
837
|
+
let renderImage = (fields) => (scope) => {
|
|
737
838
|
if (hidden(fields.visible, scope)) return null;
|
|
738
839
|
let bytes = fields.source(scope);
|
|
739
840
|
// Nullish is the missing-data twin of `visible: false`: no event and no
|
|
740
841
|
// error, so a report whose input sometimes lacks a logo needs no guard.
|
|
741
842
|
if (bytes == null) return null;
|
|
742
|
-
return imageEvent(fields, scope,
|
|
843
|
+
return imageEvent(fields, scope, bytes);
|
|
743
844
|
};
|
|
744
845
|
// An image item: bytes an `=` expression yields, sized by `fit`, with an
|
|
745
846
|
// optional textual stand-in. JSON has no way to write bytes, so `source` is
|
|
746
847
|
// expression-only; the vocabulary an image accepts is `style.js`'s to narrow.
|
|
747
|
-
/** @type {(def: any, path: string, role: string) => (scope: Scope
|
|
748
|
-
let imageOf = (def, path, role) => {
|
|
749
|
-
keys(def,
|
|
848
|
+
/** @type {(def: any, path: string, role: string, inSlot?: boolean) => (scope: Scope) => any} */
|
|
849
|
+
let imageOf = (def, path, role, inSlot = false) => {
|
|
850
|
+
keys(def, inSlot ? IMAGE_SLOT_KEYS : IMAGE_KEYS, path);
|
|
750
851
|
let source = expression(def.source, path + ".source");
|
|
751
852
|
checkFit(def.fit, path + ".fit");
|
|
752
|
-
|
|
853
|
+
// Read in the documented key order, and each read once: `stylesOf` reports
|
|
854
|
+
// as it reads, so compiling it twice would report this node's style
|
|
855
|
+
// problems twice, and reading it early would report them out of order.
|
|
856
|
+
let alt = altOf(def, path);
|
|
857
|
+
let visible = visibleOf(def, path);
|
|
858
|
+
let styles = stylesOf(def, path, checkImageStyle);
|
|
859
|
+
let render = renderImage({
|
|
753
860
|
source,
|
|
754
861
|
fit: def.fit ?? "natural",
|
|
755
|
-
alt
|
|
756
|
-
visible
|
|
757
|
-
styles
|
|
862
|
+
alt,
|
|
863
|
+
visible,
|
|
864
|
+
styles,
|
|
758
865
|
path,
|
|
759
866
|
sourceSpec: def.source,
|
|
760
867
|
role,
|
|
761
868
|
});
|
|
869
|
+
if (!inSlot) return render;
|
|
870
|
+
// A slot keeps its place whatever the data does: a hidden image, or a
|
|
871
|
+
// nullish source, leaves the same empty-token placeholder a hidden text
|
|
872
|
+
// slot leaves, so the widths either side of it do not move. `cellShape`
|
|
873
|
+
// with no template is that placeholder, so the two cannot drift.
|
|
874
|
+
let empty = cellShape(EMPTY, null, styles);
|
|
875
|
+
return (scope) => render(scope) ?? { ...empty(scope), type: "item", role };
|
|
762
876
|
};
|
|
763
877
|
|
|
764
|
-
|
|
878
|
+
// One slot of a split: an ordinary item, plus the width share that is a slot
|
|
879
|
+
// property rather than an item one -- which is why `width` reaches an item's
|
|
880
|
+
// key set only here (docs/adr/0029).
|
|
881
|
+
/**
|
|
882
|
+
* @type {(def: any, path: string, role: string)
|
|
883
|
+
* => { render: (scope: Scope) => any, width: any } | null}
|
|
884
|
+
*/
|
|
885
|
+
let slotOf = (def, path, role) => {
|
|
886
|
+
if (!obj(def, path)) return null;
|
|
887
|
+
// Placement inside placement is the coordinate system quario does not
|
|
888
|
+
// have, so the nesting verdict is named rather than left to read as an
|
|
889
|
+
// unknown item type.
|
|
890
|
+
if (def.type === "split") {
|
|
891
|
+
bad(path + ".type", "a split cannot nest");
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
render: (def.type === "image" ? imageOf : textOf)(def, path, role, true),
|
|
896
|
+
width: widthOf(def.width, path),
|
|
897
|
+
};
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// A split: slots placed across the content width, bracketed in the stream so
|
|
901
|
+
// every slot crosses as the ordinary item or image event it already is.
|
|
902
|
+
/** @type {(def: any, path: string, role: string) => (scope: Scope) => any} */
|
|
903
|
+
let splitOf = (def, path, role) => {
|
|
904
|
+
keys(def, ["type", "slots", "visible", "style"], path);
|
|
905
|
+
let list = arr(def.slots, path + ".slots", true);
|
|
906
|
+
let defs = list || [];
|
|
907
|
+
// Two is the floor: a one-slot split is a text item wearing more syntax,
|
|
908
|
+
// and permitting it would be a second way to say one thing. Read off the
|
|
909
|
+
// array `arr` accepted, so a missing `slots` reports "required" once
|
|
910
|
+
// rather than that and this.
|
|
911
|
+
if (list && defs.length < 2) bad(path + ".slots", "expected at least two slots");
|
|
912
|
+
let slots = defs
|
|
913
|
+
.map((slot, i) => slotOf(slot, path + ".slots[" + i + "]", role))
|
|
914
|
+
.filter((slot) => slot != null);
|
|
915
|
+
checkShares(bad, slots, path + ".slots", "slot");
|
|
916
|
+
let visible = visibleOf(def, path);
|
|
917
|
+
let styles = stylesOf(def, path);
|
|
918
|
+
// Every width is a literal the traversal already resolved, so the geometry
|
|
919
|
+
// the bracket carries is the same array on every render -- built here
|
|
920
|
+
// rather than rebuilt per row in a detail band. Frozen because one array
|
|
921
|
+
// now reaches every render: a consumer must not be able to reach back
|
|
922
|
+
// through an event and change what a later render sees.
|
|
923
|
+
let geometry = Object.freeze(
|
|
924
|
+
slots.map((slot) => Object.freeze(opt({}, { width: slot.width }))),
|
|
925
|
+
);
|
|
926
|
+
return (scope) => {
|
|
927
|
+
if (hidden(visible, scope)) return null;
|
|
928
|
+
let out = [opt({ type: "split-start", role, slots: geometry }, { style: styles(scope) })];
|
|
929
|
+
for (let slot of slots) out.push(slot.render(scope));
|
|
930
|
+
out.push({ type: "split-end" });
|
|
931
|
+
return out;
|
|
932
|
+
};
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/** @type {(def: any, itemPath: string, role: string) => ((scope: Scope) => any) | null} */
|
|
765
936
|
let itemOf = (def, itemPath, role) => {
|
|
766
937
|
if (!obj(def, itemPath)) return null;
|
|
767
938
|
// `type` picks the item's shape; `textOf` owns the verdict on the ones
|
|
768
939
|
// it does not name, having the closed key set that reports first.
|
|
940
|
+
if (def.type === "split") return splitOf(def, itemPath, role);
|
|
769
941
|
return (def.type === "image" ? imageOf : textOf)(def, itemPath, role);
|
|
770
942
|
};
|
|
771
|
-
/** @type {(list: ((scope: Scope
|
|
943
|
+
/** @type {(list: ((scope: Scope) => any)[]) => (scope: Scope) => Generator<any>} */
|
|
772
944
|
let yieldItems = (list) =>
|
|
773
|
-
function* (scope
|
|
945
|
+
function* (scope) {
|
|
774
946
|
for (let render of list) {
|
|
775
|
-
let event = render(scope
|
|
776
|
-
|
|
947
|
+
let event = render(scope);
|
|
948
|
+
// A split renders to its whole bracket; every other item to one event.
|
|
949
|
+
if (Array.isArray(event)) yield* event;
|
|
950
|
+
else if (event) yield event;
|
|
777
951
|
}
|
|
778
952
|
};
|
|
779
953
|
|
|
@@ -781,10 +955,10 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
781
955
|
// `q-<role>` class, which print CSS targets for styling and breaks.
|
|
782
956
|
/**
|
|
783
957
|
* @type {(value: any, path: string, role: string)
|
|
784
|
-
* => (scope: Scope
|
|
958
|
+
* => (scope: Scope) => Generator<any>}
|
|
785
959
|
*/
|
|
786
960
|
let itemsOf = (value, path, role) => {
|
|
787
|
-
/** @type {((scope: Scope
|
|
961
|
+
/** @type {((scope: Scope) => any)[]} */
|
|
788
962
|
let list = [];
|
|
789
963
|
for (let [i, def] of (arr(value, path) || []).entries()) {
|
|
790
964
|
let render = itemOf(def, path + "[" + i + "]", role);
|
|
@@ -812,14 +986,15 @@ let bands = (
|
|
|
812
986
|
{ columnOf, itemsOf, totalOf },
|
|
813
987
|
) => {
|
|
814
988
|
// Stacked items, one pass per row.
|
|
815
|
-
/** @type {(items: (scope: Scope
|
|
989
|
+
/** @type {(items: (scope: Scope) => Generator<any>) => Band} */
|
|
816
990
|
let itemsBand = (items) =>
|
|
817
991
|
function* (rows, scope, runners) {
|
|
818
992
|
for (let row of rows) {
|
|
819
993
|
let scopeOfRow = runners.bind(scope, row);
|
|
820
994
|
// Detail items carry the row's run values as data; group and report
|
|
821
995
|
// band items never do (they have no current row).
|
|
822
|
-
for (let event of items(scopeOfRow
|
|
996
|
+
for (let event of items(scopeOfRow))
|
|
997
|
+
yield carriesRun(event) ? opt(event, { run: scopeOfRow.run }) : event;
|
|
823
998
|
}
|
|
824
999
|
};
|
|
825
1000
|
|
|
@@ -833,30 +1008,6 @@ let bands = (
|
|
|
833
1008
|
rowStyles: stylesOf(value.row, "detail.row"),
|
|
834
1009
|
};
|
|
835
1010
|
};
|
|
836
|
-
/** @type {(sized: boolean) => string} */
|
|
837
|
-
let shareMsg = (sized) =>
|
|
838
|
-
sized
|
|
839
|
-
? "expected column widths to total at most 100"
|
|
840
|
-
: "expected column widths to total under 100, leaving room for the columns without one";
|
|
841
|
-
/** @type {(columns: any[], shares: number[]) => void} */
|
|
842
|
-
let warnShares = (columns, shares) => {
|
|
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);
|
|
859
|
-
};
|
|
860
1011
|
/** @type {(value: any) => { defs: any[] | null, columns: any[] }} */
|
|
861
1012
|
let tableColumns = (value) => {
|
|
862
1013
|
let defs = arr(value.columns, "detail.columns", true);
|
|
@@ -864,7 +1015,7 @@ let bands = (
|
|
|
864
1015
|
let columns = (defs || [])
|
|
865
1016
|
.map((def, i) => columnOf(def, "detail.columns[" + i + "]"))
|
|
866
1017
|
.filter((column) => column != null);
|
|
867
|
-
checkShares(columns);
|
|
1018
|
+
checkShares(bad, columns, "detail.columns", "column");
|
|
868
1019
|
return { defs, columns };
|
|
869
1020
|
};
|
|
870
1021
|
/** @type {(totals: any[] | null, defs: any[] | null) => void} */
|
|
@@ -874,7 +1025,12 @@ let bands = (
|
|
|
874
1025
|
};
|
|
875
1026
|
/** @type {(totals: any[]) => any[]} */
|
|
876
1027
|
let mapTotals = (totals) =>
|
|
877
|
-
totals
|
|
1028
|
+
totals
|
|
1029
|
+
.map((def, i) => {
|
|
1030
|
+
let cell = totalOf(def, "detail.total[" + i + "]");
|
|
1031
|
+
return cell && { cell, path: "detail.total[" + i + "]" };
|
|
1032
|
+
})
|
|
1033
|
+
.filter((entry) => entry != null);
|
|
878
1034
|
/** @type {(value: any, defs: any[] | null) => any[] | null} */
|
|
879
1035
|
let tableTotals = (value, defs) => {
|
|
880
1036
|
let totals = arr(value.total, "detail.total");
|
|
@@ -890,7 +1046,12 @@ let bands = (
|
|
|
890
1046
|
let scopeOfRow = runners.bind(scope, row);
|
|
891
1047
|
if (!hidden(rowVisible, scopeOfRow))
|
|
892
1048
|
yield opt(
|
|
893
|
-
{
|
|
1049
|
+
{
|
|
1050
|
+
type: "row",
|
|
1051
|
+
cells: columns.map((column) =>
|
|
1052
|
+
Object.assign(column.cell(scopeOfRow), { path: column.path }),
|
|
1053
|
+
),
|
|
1054
|
+
},
|
|
894
1055
|
{ style: rowStyles(scopeOfRow), run: scopeOfRow.run },
|
|
895
1056
|
);
|
|
896
1057
|
}
|
|
@@ -900,12 +1061,17 @@ let bands = (
|
|
|
900
1061
|
function* (rows, scope, runners) {
|
|
901
1062
|
yield {
|
|
902
1063
|
type: "table-start",
|
|
1064
|
+
path: "detail",
|
|
903
1065
|
columns: columns.map((column) =>
|
|
904
|
-
opt({ header: column.header(scope) }, { width: column.width }),
|
|
1066
|
+
opt({ header: column.header(scope), path: column.path }, { width: column.width }),
|
|
905
1067
|
),
|
|
906
1068
|
};
|
|
907
1069
|
yield* tableRows(rows, scope, runners, columns, rowVisible, rowStyles);
|
|
908
|
-
if (total)
|
|
1070
|
+
if (total)
|
|
1071
|
+
yield {
|
|
1072
|
+
type: "total-row",
|
|
1073
|
+
cells: total.map(({ cell, path }) => Object.assign(cell(scope), { path })),
|
|
1074
|
+
};
|
|
909
1075
|
yield { type: "table-end" };
|
|
910
1076
|
};
|
|
911
1077
|
/** @type {(value: any) => Band} */
|
|
@@ -983,7 +1149,14 @@ let bands = (
|
|
|
983
1149
|
// A group band has no current row, so `@` stays unbound here.
|
|
984
1150
|
let { groupScope, aggregates, rows: ordered } = openInstance(ctx, key, groupRows, scope);
|
|
985
1151
|
yield opt(
|
|
986
|
-
{
|
|
1152
|
+
{
|
|
1153
|
+
type: "group-start",
|
|
1154
|
+
name: ctx.name,
|
|
1155
|
+
depth: ctx.index,
|
|
1156
|
+
key,
|
|
1157
|
+
aggregates,
|
|
1158
|
+
path: ctx.path,
|
|
1159
|
+
},
|
|
987
1160
|
{ break: ctx.pageBreak, reset: ctx.reset, columns: ctx.columns },
|
|
988
1161
|
);
|
|
989
1162
|
yield* ctx.header(groupScope);
|
|
@@ -1027,6 +1200,7 @@ let bands = (
|
|
|
1027
1200
|
footer: itemsOf(def.footer, path + ".footer", "group-footer"),
|
|
1028
1201
|
inner: bandOf(defs, index + 1, detail, columned || columns != null),
|
|
1029
1202
|
name: def.name,
|
|
1203
|
+
path,
|
|
1030
1204
|
pageBreak: def.break,
|
|
1031
1205
|
reset: def.reset,
|
|
1032
1206
|
columns,
|
|
@@ -1053,9 +1227,12 @@ let bands = (
|
|
|
1053
1227
|
/** @type {(source: string, options: any) => any} */
|
|
1054
1228
|
let compileQuery = (source, options) => {
|
|
1055
1229
|
try {
|
|
1056
|
-
|
|
1230
|
+
// padvinder itself defaults maxDepth to 500 (safety by construction since
|
|
1231
|
+
// 0.6.0), so untrusted deep documents throw typed with no quario-side
|
|
1232
|
+
// budget; hosts override per key — Infinity opts a budget out.
|
|
1233
|
+
return query(source, undefined, options && options.query);
|
|
1057
1234
|
} catch (error) {
|
|
1058
|
-
locate("data", source, error,
|
|
1235
|
+
locate("data", source, error, isQueryDiagnostic(error) && relocateQuery);
|
|
1059
1236
|
}
|
|
1060
1237
|
};
|
|
1061
1238
|
/** @type {(schema: any, obj: any, named: any, bad: any) => any} */
|
|
@@ -1102,7 +1279,7 @@ let pageBands = (schema, obj, keys, itemsOf) => {
|
|
|
1102
1279
|
*/
|
|
1103
1280
|
let traverse = (
|
|
1104
1281
|
{ add, bad, attempt, runNames },
|
|
1105
|
-
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf },
|
|
1282
|
+
{ arr, obj, keys, named, expression, columnsOf, takeOf, foldsOf, sortOf, stylesOf },
|
|
1106
1283
|
{ itemsOf },
|
|
1107
1284
|
{ bandOf },
|
|
1108
1285
|
schema,
|
|
@@ -1122,6 +1299,7 @@ let traverse = (
|
|
|
1122
1299
|
"take",
|
|
1123
1300
|
"aggregates",
|
|
1124
1301
|
"run",
|
|
1302
|
+
"style",
|
|
1125
1303
|
"header",
|
|
1126
1304
|
"empty",
|
|
1127
1305
|
"columns",
|
|
@@ -1142,6 +1320,12 @@ let traverse = (
|
|
|
1142
1320
|
let take = takeOf(schema.take, "take");
|
|
1143
1321
|
let aggs = foldsOf(schema.aggregates, "aggregates", AGG, new Set(["params", "input"]));
|
|
1144
1322
|
let running = foldsOf(schema.run, "run", RUN, null, runNames);
|
|
1323
|
+
// The report default: one style block for the whole document, narrowed to
|
|
1324
|
+
// the declarations a typeface is made of. It resolves once per render in
|
|
1325
|
+
// report scope rather than per node, which is why it crosses the seam as a
|
|
1326
|
+
// `report-start` field instead of being merged into every item's style --
|
|
1327
|
+
// nothing here allocates per cell (docs/adr/0033).
|
|
1328
|
+
let style = stylesOf(schema, "", checkReportStyle);
|
|
1145
1329
|
let header = itemsOf(schema.header, "header", "report-header");
|
|
1146
1330
|
// Null when undeclared, so the render decides on the compiled band rather
|
|
1147
1331
|
// than reading `schema.empty` a second time from outside the traversal.
|
|
@@ -1160,6 +1344,7 @@ let traverse = (
|
|
|
1160
1344
|
take,
|
|
1161
1345
|
aggs,
|
|
1162
1346
|
running,
|
|
1347
|
+
style,
|
|
1163
1348
|
header,
|
|
1164
1349
|
empty,
|
|
1165
1350
|
columns,
|
|
@@ -1189,8 +1374,15 @@ export let plan = (schema, funcs, options) => {
|
|
|
1189
1374
|
return {
|
|
1190
1375
|
compiled: traverse(state, read, node, band, schema, options),
|
|
1191
1376
|
problems: state.problems,
|
|
1377
|
+
anchors: state.anchors,
|
|
1192
1378
|
names: state.names,
|
|
1193
|
-
|
|
1379
|
+
// Registry metadata comes from xprsn's one signature convention, described
|
|
1380
|
+
// over just the called subset — every called name resolved from the
|
|
1381
|
+
// registry at compile time, and the rebuilt object keeps the flat name
|
|
1382
|
+
// set's call-first-seen order.
|
|
1383
|
+
functions: signatures(
|
|
1384
|
+
Object.fromEntries(Array.from(state.functions, (name) => [name, state.FNS[name]])),
|
|
1385
|
+
),
|
|
1194
1386
|
diagnostic: state.diagnostic,
|
|
1195
1387
|
};
|
|
1196
1388
|
};
|