quario 0.4.0 → 0.6.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 +435 -0
- package/README.md +49 -25
- package/lib/band-height.js +123 -0
- package/lib/format.js +140 -32
- package/lib/index.d.ts +209 -21
- package/lib/index.js +13 -8
- package/lib/license.js +1 -1
- package/lib/locate.js +32 -0
- package/lib/math.js +71 -0
- package/lib/plan.js +485 -184
- package/lib/precision.js +75 -0
- package/lib/reducers.js +165 -0
- package/lib/scope.js +7 -86
- package/lib/stream.js +20 -15
- package/lib/style.js +485 -20
- package/package.json +2 -2
package/lib/style.js
CHANGED
|
@@ -1,24 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The closed style vocabulary
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* its own formatting model, and nothing here knows about any of them.
|
|
2
|
+
* The closed style vocabulary — target-neutral names with typed literal values —
|
|
3
|
+
* and the resolutions the event stream is defined in terms of. Two things, and
|
|
4
|
+
* the split between them is the file's organising claim.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The **checks** are what the traversal runs: unknown names and mistyped
|
|
7
|
+
* literals are definition errors; `=` results stay lenient at render.
|
|
8
|
+
*
|
|
9
|
+
* The **resolutions** are what a declaration means before any target sees it.
|
|
10
|
+
* A row's box is the one there is (SCHEMA.md, "Style declarations"): it says
|
|
11
|
+
* what the document means rather than what a surface paints, so every target
|
|
12
|
+
* is handed the answer instead of reaching its own. That is the same line
|
|
13
|
+
* `./stream.js` sits on, and the opposite side of it from `finite`/`HEX`,
|
|
14
|
+
* which are restated per target on purpose because a coercion is each target's
|
|
15
|
+
* own edge.
|
|
16
|
+
*
|
|
17
|
+
* Past those, each target maps these names to its own formatting model, and
|
|
18
|
+
* nothing here knows about any of them.
|
|
10
19
|
*/
|
|
20
|
+
import { record } from "./names.js";
|
|
21
|
+
import { defaultDigits } from "./precision.js";
|
|
11
22
|
/** @type {(value: any) => boolean} */
|
|
12
23
|
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
24
|
+
/**
|
|
25
|
+
* A positive number of points: `POINTS` below, less the zero it admits. The
|
|
26
|
+
* vocabulary's own shape rather than a second numeric test, so a future
|
|
27
|
+
* loosening of what a measurement may be reaches every reader of it.
|
|
28
|
+
*
|
|
29
|
+
* @type {(value: any) => boolean}
|
|
30
|
+
*/
|
|
31
|
+
export let positivePts = (value) => finite(value) && value > 0;
|
|
13
32
|
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
14
33
|
/** @type {(value: any) => boolean} */
|
|
15
34
|
let isHex = (value) => typeof value === "string" && HEX.test(value);
|
|
16
|
-
|
|
35
|
+
// A currency code's *shape*, not the ISO 4217 registry: `Intl` owns the real
|
|
36
|
+
// list, so a well-formed but unknown code fails soft at presentation like any
|
|
37
|
+
// other kind that does not apply. Shipping the registry would buy one
|
|
38
|
+
// diagnostic at the price of a data file that goes stale (docs/adr/0041).
|
|
39
|
+
let CODE = /^[A-Z]{3}$/;
|
|
40
|
+
/** @type {(value: any) => boolean} */
|
|
41
|
+
let isCode = (value) => typeof value === "string" && CODE.test(value);
|
|
42
|
+
// A declaration checks to a message or to null, and `spec` is the sugar for
|
|
43
|
+
// the ordinary case: one predicate, one wording. `format` is the one entry
|
|
44
|
+
// written out longhand, because a value with structure of its own has four
|
|
45
|
+
// distinct mistakes -- unknown kind, unknown option, an option the kind does
|
|
46
|
+
// not take, a count out of range -- and they cannot be one wording. Same
|
|
47
|
+
// bargain a border side already gets a checker of its own for, rather than
|
|
48
|
+
// bending the table around it.
|
|
49
|
+
/** @typedef {{ check: (value: any) => string | null }} StyleSpec */
|
|
17
50
|
/** @type {(msg: string, ok: (value: any) => boolean) => StyleSpec} */
|
|
18
|
-
let spec = (msg, ok) => ({ ok
|
|
51
|
+
let spec = (msg, ok) => ({ check: (value) => (ok(value) ? null : msg) });
|
|
19
52
|
let COLOR = spec("expected a #rgb or #rrggbb color", isHex);
|
|
20
|
-
|
|
53
|
+
/** @type {(value: any) => boolean} */
|
|
54
|
+
let isFlag = (value) => typeof value === "boolean";
|
|
55
|
+
let FLAG = spec("expected a boolean", isFlag);
|
|
21
56
|
let ALIGNMENTS = ["left", "center", "right"];
|
|
57
|
+
// "middle", not "center": that word is already the horizontal one.
|
|
58
|
+
let VALIGNMENTS = ["top", "middle", "bottom"];
|
|
22
59
|
let LINES = ["solid", "dashed", "dotted"];
|
|
23
60
|
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
24
61
|
let BORDER_PARTS = ["Width", "Style", "Color"];
|
|
@@ -40,7 +77,7 @@ let STYLES = {
|
|
|
40
77
|
"expected a font family name",
|
|
41
78
|
(value) => typeof value === "string" && value.length > 0,
|
|
42
79
|
),
|
|
43
|
-
size: spec("expected a positive number of points",
|
|
80
|
+
size: spec("expected a positive number of points", positivePts),
|
|
44
81
|
bold: FLAG,
|
|
45
82
|
italic: FLAG,
|
|
46
83
|
underline: FLAG,
|
|
@@ -49,9 +86,127 @@ let STYLES = {
|
|
|
49
86
|
color: COLOR,
|
|
50
87
|
background: COLOR,
|
|
51
88
|
align: spec("expected left, center, or right", (value) => ALIGNMENTS.includes(value)),
|
|
52
|
-
|
|
89
|
+
valign: spec("expected top, middle, or bottom", (value) => VALIGNMENTS.includes(value)),
|
|
90
|
+
format: { check: (value) => checkFormat(value) },
|
|
91
|
+
currency: spec("expected a three-letter ISO 4217 code", isCode),
|
|
53
92
|
};
|
|
54
93
|
let FORMATS = ["number", "currency", "percent", "date"];
|
|
94
|
+
|
|
95
|
+
// --- the format declaration ---------------------------------------------------
|
|
96
|
+
//
|
|
97
|
+
// `format` is a kind, optionally carrying one modifier that kind understands
|
|
98
|
+
// (docs/adr/0056). The kinds that present a number take a digit count; `date`
|
|
99
|
+
// takes a form. That is the whole rule, and the two halves below -- the check
|
|
100
|
+
// and the resolution -- are the only places it is spelled out.
|
|
101
|
+
|
|
102
|
+
// The four names are Intl's `dateStyle`, and CONTEXT.md's "Form" is the term.
|
|
103
|
+
// A bare `date` means `medium`: the fifth shape Intl's no-option default used
|
|
104
|
+
// to produce was nameable by nothing, so an author held one behaviour more
|
|
105
|
+
// than the vocabulary described (docs/adr/0056).
|
|
106
|
+
let FORMS = ["short", "medium", "long", "full"];
|
|
107
|
+
let DEFAULT_FORM = "medium";
|
|
108
|
+
// Both edges honour every count in this range: Intl guarantees it and an
|
|
109
|
+
// Excel number format can spell it. A wider one would let an author name a
|
|
110
|
+
// precision only one target could present, which is the divergence
|
|
111
|
+
// docs/adr/0054 closed.
|
|
112
|
+
let MAX_DIGITS = 20;
|
|
113
|
+
// Which modifier each kind takes, and the whole answer to "does this kind
|
|
114
|
+
// understand this option?". The three number kinds share one, so the table is
|
|
115
|
+
// keyed by kind rather than by option -- a kind is what an author names first.
|
|
116
|
+
/** @type {Record<string, string>} */
|
|
117
|
+
let MODIFIERS = { number: "digits", percent: "digits", currency: "digits", date: "form" };
|
|
118
|
+
let KINDS_MSG = "expected number, currency, percent, or date";
|
|
119
|
+
/** @type {(value: any) => boolean} */
|
|
120
|
+
let isDigits = (value) =>
|
|
121
|
+
Number.isInteger(value) && value >= 0 && /** @type {number} */ (value) <= MAX_DIGITS;
|
|
122
|
+
/** @type {Record<string, (value: any) => string | null>} */
|
|
123
|
+
let MODIFIER_CHECKS = {
|
|
124
|
+
digits: (value) =>
|
|
125
|
+
isDigits(value) ? null : "expected a whole number of digits from 0 to " + MAX_DIGITS,
|
|
126
|
+
form: (value) => (FORMS.includes(value) ? null : "expected short, medium, long, or full"),
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The `format` vocabulary as data, for a tool that offers it rather than
|
|
130
|
+
* checks it -- the editor's style rail -- so no surface keeps a copy of the
|
|
131
|
+
* kinds, the forms, the default form or the digit ceiling that has to be told
|
|
132
|
+
* about a change here. `STYLE_NAMES` is the same bargain one level up.
|
|
133
|
+
*/
|
|
134
|
+
export let FORMAT_VOCABULARY = Object.freeze({
|
|
135
|
+
kinds: Object.freeze([...FORMATS]),
|
|
136
|
+
forms: Object.freeze([...FORMS]),
|
|
137
|
+
// Which modifier each kind takes, so a surface offering the pair does not
|
|
138
|
+
// have to know that three kinds share `digits` and `date` does not.
|
|
139
|
+
modifiers: Object.freeze({ ...MODIFIERS }),
|
|
140
|
+
defaultForm: DEFAULT_FORM,
|
|
141
|
+
maxDigits: MAX_DIGITS,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The kind a declaration names -- the shorthand string, or an object's `kind`
|
|
146
|
+
* -- or nothing when it names none this vocabulary knows. An own-key read,
|
|
147
|
+
* the guard every name from a document gets: a `kind` resolved off
|
|
148
|
+
* `Object.prototype` is not a kind.
|
|
149
|
+
*
|
|
150
|
+
* Exported because the typed-cell seam asks the same question and only that
|
|
151
|
+
* question: `typed()` gates its one coercion on the kind being `date`, and
|
|
152
|
+
* resolving a whole declaration to read one field off it would allocate per
|
|
153
|
+
* cell for nothing (`./stream.js`).
|
|
154
|
+
*
|
|
155
|
+
* @type {(value: any) => string | undefined}
|
|
156
|
+
*/
|
|
157
|
+
export let kindOf = (value) => {
|
|
158
|
+
let named = record(value) && Object.hasOwn(value, "kind") ? value.kind : value;
|
|
159
|
+
return FORMATS.includes(named) ? named : undefined;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Which of the three refusals a value that named no kind earns. An object got
|
|
163
|
+
// as far as being read and failed on one key; a string named a kind that is
|
|
164
|
+
// not one; anything else is not a declaration at all.
|
|
165
|
+
/** @type {(value: any) => string} */
|
|
166
|
+
let noKind = (value) => {
|
|
167
|
+
if (record(value)) return KINDS_MSG + ' on the object\'s "kind"';
|
|
168
|
+
return typeof value === "string" ? KINDS_MSG : KINDS_MSG + ", or an object naming one";
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// The two refusals are worded differently on purpose: a name no kind takes is
|
|
172
|
+
// a typo, while a name another kind takes is a mismatch, and naming the kind
|
|
173
|
+
// that refused it is what turns the diagnostic into the fix.
|
|
174
|
+
/** @type {(name: string, kind: string) => string} */
|
|
175
|
+
let refuseOption = (name, kind) =>
|
|
176
|
+
Object.hasOwn(MODIFIER_CHECKS, name)
|
|
177
|
+
? 'format option "' + name + '" does not apply to ' + kind
|
|
178
|
+
: 'unknown format option "' + name + '"';
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Every key past `kind`, against the one option this kind understands.
|
|
182
|
+
*
|
|
183
|
+
* @type {(value: any, kind: string) => string | null}
|
|
184
|
+
*/
|
|
185
|
+
let checkOptions = (value, kind) => {
|
|
186
|
+
let modifier = MODIFIERS[kind];
|
|
187
|
+
let stray = Object.keys(value).find((name) => name !== "kind" && name !== modifier);
|
|
188
|
+
if (stray !== undefined) return refuseOption(stray, kind);
|
|
189
|
+
return Object.hasOwn(value, modifier) ? MODIFIER_CHECKS[modifier](value[modifier]) : null;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A `format` declaration's own check: a message naming which of the four
|
|
194
|
+
* mistakes this is, or null. The kind is settled first, because every other
|
|
195
|
+
* answer depends on it -- an option is only wrong *for a kind*.
|
|
196
|
+
*
|
|
197
|
+
* @type {(value: any) => string | null}
|
|
198
|
+
*/
|
|
199
|
+
let checkFormat = (value) => {
|
|
200
|
+
let kind = kindOf(value);
|
|
201
|
+
if (kind === undefined) return noKind(value);
|
|
202
|
+
return record(value) ? checkOptions(value, kind) : null;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// The two declarations that present a *value* rather than describe a surface:
|
|
206
|
+
// the kind, and -- under the `currency` kind -- what the number is denominated
|
|
207
|
+
// in. They share every site, so the places that refuse one refuse both from
|
|
208
|
+
// this list rather than from two lists that have to be compared (ADR 0041).
|
|
209
|
+
let VALUE_NAMES = ["format", "currency"];
|
|
55
210
|
let POINTS = spec(
|
|
56
211
|
"expected a non-negative number of points",
|
|
57
212
|
(value) => finite(value) && value >= 0,
|
|
@@ -73,19 +228,39 @@ for (let name of FLOW) {
|
|
|
73
228
|
STYLES[name] = POINTS;
|
|
74
229
|
IMAGE_STYLES.push(name);
|
|
75
230
|
}
|
|
231
|
+
// The vocabulary as a list, in the order SCHEMA.md's table reads it: the text
|
|
232
|
+
// names, the flow, then the box one part at a time across the four sides. A
|
|
233
|
+
// tool that offers the vocabulary (the editor's style rail) imports this
|
|
234
|
+
// rather than keeping a copy that has to be told about every new name.
|
|
235
|
+
/** @type {readonly string[]} */
|
|
236
|
+
// The box half of the vocabulary, generated once from the same two tables the
|
|
237
|
+
// checks read. `isBoxName` is the only place the question "does this name
|
|
238
|
+
// belong to the box?" is answered, so a name that does not start with
|
|
239
|
+
// `padding` or `border` could join without every asker being told separately.
|
|
240
|
+
let BOX_NAMES = [
|
|
241
|
+
...SIDES.map((side) => "padding" + side),
|
|
242
|
+
...BORDER_PARTS.flatMap((part) => SIDES.map((side) => "border" + side + part)),
|
|
243
|
+
];
|
|
244
|
+
let BOX = new Set(BOX_NAMES);
|
|
245
|
+
/** @type {(name: string) => boolean} */
|
|
246
|
+
export let isBoxName = (name) => BOX.has(name);
|
|
247
|
+
export let STYLE_NAMES = Object.freeze([
|
|
248
|
+
...Object.keys(STYLES).filter((name) => !FLOW.includes(name) && !isBoxName(name)),
|
|
249
|
+
...FLOW,
|
|
250
|
+
...BOX_NAMES,
|
|
251
|
+
]);
|
|
76
252
|
// One check for both the validating traversal and the compile path: an error
|
|
77
253
|
// message for a declaration, or null when it is acceptable (expressions defer
|
|
78
254
|
// to render).
|
|
79
255
|
/** @type {(value: any) => boolean} */
|
|
80
|
-
let isExpr = (value) => typeof value === "string" && value[0] === "=";
|
|
256
|
+
export let isExpr = (value) => typeof value === "string" && value[0] === "=";
|
|
81
257
|
/** @type {(name: string, value: any) => string | null} */
|
|
82
258
|
export let checkStyle = (name, value) => {
|
|
83
259
|
// Own-key lookup: a declaration named after an Object.prototype member
|
|
84
260
|
// (`toString`, `constructor`) is unknown, not an inherited function.
|
|
85
261
|
if (!Object.hasOwn(STYLES, name)) return 'unknown style "' + name + '"';
|
|
86
262
|
if (isExpr(value)) return null;
|
|
87
|
-
|
|
88
|
-
return rule.ok(value) ? null : rule.msg;
|
|
263
|
+
return STYLES[name].check(value);
|
|
89
264
|
};
|
|
90
265
|
|
|
91
266
|
// A border side is width, style, and colour together, or none. Incomplete
|
|
@@ -132,8 +307,10 @@ export let checkReportStyle = narrowed(REPORT_STYLES, "a report default");
|
|
|
132
307
|
// Flow spacing is a band-item pad. A table cell, a row box, and a split slot
|
|
133
308
|
// have no flow, so the names are refused there the way a text declaration is
|
|
134
309
|
// on an image (docs/adr/0037).
|
|
310
|
+
/** @type {(names: string[]) => Record<string, true>} */
|
|
311
|
+
let nameSet = (names) => Object.fromEntries(names.map((name) => [name, true]));
|
|
135
312
|
/** @type {Record<string, true>} */
|
|
136
|
-
let FLOW_NAMES =
|
|
313
|
+
let FLOW_NAMES = nameSet(FLOW);
|
|
137
314
|
/**
|
|
138
315
|
* @type {(names: Record<string, true>, check: (name: string, value: any) => string | null, subject: string) =>
|
|
139
316
|
* (name: string, value: any) => string | null}
|
|
@@ -150,9 +327,297 @@ let withoutFlow = (check, subject) => withoutNames(FLOW_NAMES, check, subject);
|
|
|
150
327
|
|
|
151
328
|
export let checkCellStyle = withoutFlow(checkStyle, "a table cell");
|
|
152
329
|
export let checkSlotStyle = withoutFlow(checkStyle, "a split slot");
|
|
153
|
-
|
|
154
|
-
// A
|
|
155
|
-
//
|
|
330
|
+
// `valign` is legal exactly where a box has height it did not ask for
|
|
331
|
+
// (CONTEXT.md "Box": slack). A slot's box is the split's height, so a slot --
|
|
332
|
+
// text or image -- reads it; a band image's box is its own, so it does not.
|
|
333
|
+
// That is the one name an image accepts in a slot and nowhere else, the same
|
|
334
|
+
// shape of reason `width` reaches an item only there (docs/adr/0029).
|
|
335
|
+
let SLOT_IMAGE_STYLES = [...IMAGE_STYLES, "valign"];
|
|
336
|
+
export let checkSlotImageStyle = withoutFlow(
|
|
337
|
+
narrowed(SLOT_IMAGE_STYLES, "an image"),
|
|
338
|
+
"a split slot",
|
|
339
|
+
);
|
|
340
|
+
// A stacked text item's box is exactly its content and padding: no slack, so
|
|
341
|
+
// `valign` is refused rather than left unread.
|
|
342
|
+
/** @type {Record<string, true>} */
|
|
343
|
+
let STACKED_NAMES = { valign: true };
|
|
344
|
+
export let checkItemStyle = withoutNames(STACKED_NAMES, checkStyle, "a stacked item");
|
|
345
|
+
// A row box has no cell value, so the value names are refused there the way
|
|
346
|
+
// flow spacing is — they are a presentation of a value, not of a band of cells.
|
|
156
347
|
/** @type {Record<string, true>} */
|
|
157
|
-
let ROW_NAMES =
|
|
348
|
+
let ROW_NAMES = nameSet([...FLOW, ...VALUE_NAMES]);
|
|
158
349
|
export let checkRowStyle = withoutNames(ROW_NAMES, checkStyle, "a table row");
|
|
350
|
+
|
|
351
|
+
// --- resolution --------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
// A resolved `=` result the vocabulary rejects, judged once where every style
|
|
354
|
+
// block resolves rather than at each surface that would read it. What the
|
|
355
|
+
// declaration falls back *to* decides what becomes of it, and that question
|
|
356
|
+
// partitions the vocabulary three ways -- omitted, kept, voided. The argument
|
|
357
|
+
// for each is docs/adr/0055; what is restated here is only which is which, and
|
|
358
|
+
// why the two exceptions cannot be folded into the general rule.
|
|
359
|
+
//
|
|
360
|
+
// A literal never reaches here: `checkStyle` already refused it at compile, so
|
|
361
|
+
// only an `=` entry is ever guarded.
|
|
362
|
+
//
|
|
363
|
+
// `format` then takes one more step that no other name needs, below: whatever
|
|
364
|
+
// survives is **resolved**, the shorthand widened and the kind's own answer
|
|
365
|
+
// filled in, because what crosses the stream is the declaration with its digit
|
|
366
|
+
// count or its form already on it (docs/adr/0056).
|
|
367
|
+
|
|
368
|
+
// The sentinel a failed result becomes when its name is one the engine omits.
|
|
369
|
+
// A symbol, so nothing an expression can evaluate to is ever equal to it --
|
|
370
|
+
// every value that reaches here came from author data or a `funcs` return, and
|
|
371
|
+
// this came from neither. It is the one piece of this file the compiler has to
|
|
372
|
+
// know, because writing the resolved block is its side of the seam; which name
|
|
373
|
+
// gets which rule stays here.
|
|
374
|
+
export let SKIP = Symbol("skip");
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Wrap a style entry's compiled `=` result in the rule its name answers to.
|
|
378
|
+
*
|
|
379
|
+
* **Omitted** is the general rule and needs no branch: the key is never
|
|
380
|
+
* written, so the layer below stands. That is what the spec means by
|
|
381
|
+
* "contributes nothing, same as omit" (SCHEMA.md, "Style declarations") -- a
|
|
382
|
+
* report-header item whose `bold` returns `1`, or whose `size` returns
|
|
383
|
+
* `"big"`, keeps its band-role bold and its band-role size rather than losing
|
|
384
|
+
* them to a value no target can read. It is the rule for the box too: side
|
|
385
|
+
* ownership is settled over the names a block declares, before any of this
|
|
386
|
+
* resolves, so dropping a rejected `borderTopColor` cannot hand the cell its
|
|
387
|
+
* row's other two (docs/adr/0056).
|
|
388
|
+
*
|
|
389
|
+
* **Voided** is `currency` alone. Its fallback is not the layer below but the
|
|
390
|
+
* instance's own code, so omitting would present one denomination's money
|
|
391
|
+
* under another's name -- the one failure louder than no formatting at all.
|
|
392
|
+
* `null` keeps the cell's answer, which nothing can present, so the cell drops
|
|
393
|
+
* to `display()` as a bogus `format` kind does (docs/adr/0041).
|
|
394
|
+
*
|
|
395
|
+
* @type {(name: string, resolve: (scope: any) => any) => (scope: any) => any}
|
|
396
|
+
*/
|
|
397
|
+
export let guarded = (name, resolve) => {
|
|
398
|
+
// The spec's own checker, not `checkStyle`: this judges a *resolved* value,
|
|
399
|
+
// where `checkStyle` short-circuits on an `=` source it would still be
|
|
400
|
+
// looking at. Reading the message rather than a predicate is what lets
|
|
401
|
+
// `format`, whose value has a shape, answer here like every scalar name.
|
|
402
|
+
let { check } = STYLES[name];
|
|
403
|
+
let fail = name === "currency" ? null : SKIP;
|
|
404
|
+
return (scope) => {
|
|
405
|
+
let value = resolve(scope);
|
|
406
|
+
return check(value) === null ? value : fail;
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/** @type {(code: any) => string | null} */
|
|
411
|
+
let codeOf = (code) => (typeof code === "string" && code ? code : null);
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Which currency code a cell wears: its own when it declares one, else the
|
|
415
|
+
* instance's default. A resolution rather than a presentation rule, which is
|
|
416
|
+
* why it sits here: the xlsx target needs the code itself — it splices it into
|
|
417
|
+
* a number format — where the display targets need the presented text
|
|
418
|
+
* `format()` returns, and one report's PDF and worksheet must not disagree
|
|
419
|
+
* about a denomination (docs/adr/0041, docs/adr/0014).
|
|
420
|
+
*
|
|
421
|
+
* @param {{ currency?: unknown } | null} [style] A cell's resolved style.
|
|
422
|
+
* @param {{ currency?: string } | null} [options] The instance's options.
|
|
423
|
+
* @returns {string | null} The code to present in, or nothing.
|
|
424
|
+
*/
|
|
425
|
+
export let currencyOf = (style, options) =>
|
|
426
|
+
// Declared at all — even as the `null` the engine's gate leaves behind — is
|
|
427
|
+
// the cell's own answer, and the instance's default does not override it.
|
|
428
|
+
style && Object.hasOwn(style, "currency") ? codeOf(style.currency) : codeOf(options?.currency);
|
|
429
|
+
|
|
430
|
+
// The modifier the declaration names, or nothing. One that did not resolve to
|
|
431
|
+
// a usable value is dropped rather than honoured, so the kind's own default
|
|
432
|
+
// stands: the layer below a failed modifier is the kind's default count, not
|
|
433
|
+
// nothing at all -- the same call `guarded` makes one level up, and for the
|
|
434
|
+
// same reason (docs/adr/0055, docs/adr/0056).
|
|
435
|
+
/** @type {(declared: any, kind: string) => any} */
|
|
436
|
+
let modifierOf = (declared, kind) => {
|
|
437
|
+
let name = MODIFIERS[kind];
|
|
438
|
+
let own = Object.hasOwn(declared, name) ? declared[name] : undefined;
|
|
439
|
+
return own !== undefined && MODIFIER_CHECKS[name](own) ? undefined : own;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/** @type {(own: any) => any} */
|
|
443
|
+
let presentedDate = (own) => Object.freeze({ kind: "date", form: own ?? DEFAULT_FORM });
|
|
444
|
+
|
|
445
|
+
// A kind with no count at all -- an unreadable currency -- carries no
|
|
446
|
+
// `digits` key rather than an explicit `undefined`, so a consumer's own
|
|
447
|
+
// `Object.hasOwn` reads the same answer the engine reached.
|
|
448
|
+
/** @type {(kind: string, own: any, style: any, options: any) => any} */
|
|
449
|
+
let presentedNumber = (kind, own, style, options) => {
|
|
450
|
+
let digits = own ?? defaultDigits(kind, currencyOf(style, options));
|
|
451
|
+
return Object.freeze(digits === undefined ? { kind } : { kind, digits });
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A `format` declaration resolved: the shorthand widened to its object, and
|
|
456
|
+
* the modifier the author did not name filled in from the kind's own default.
|
|
457
|
+
* `"number"` resolves to `{ kind: "number", digits: 2 }` and `"date"` to
|
|
458
|
+
* `{ kind: "date", form: "medium" }`, so what crosses the stream carries its
|
|
459
|
+
* own answer and no consumer computes one (docs/adr/0053, docs/adr/0056).
|
|
460
|
+
*
|
|
461
|
+
* A `currency` whose code Intl will not read resolves to a kind with **no**
|
|
462
|
+
* `digits` — the withdrawal that leaves the worksheet writing no number
|
|
463
|
+
* format. A declared `digits` still crosses there: it resolved on its own
|
|
464
|
+
* terms, and making it depend on a sibling declaration would be a special
|
|
465
|
+
* case bought for nothing.
|
|
466
|
+
*
|
|
467
|
+
* @param {{ format?: unknown, currency?: unknown } | null} [style] A cell's style.
|
|
468
|
+
* @param {{ currency?: string } | null} [options] The instance's options.
|
|
469
|
+
* @returns {{ kind: string, digits?: number, form?: string } | undefined} The
|
|
470
|
+
* resolved declaration, or nothing when this style names no readable kind.
|
|
471
|
+
*/
|
|
472
|
+
export let formatOf = (style, options) => {
|
|
473
|
+
let declared = style?.format;
|
|
474
|
+
let kind = kindOf(declared);
|
|
475
|
+
if (kind === undefined) return undefined;
|
|
476
|
+
let own = modifierOf(declared, kind);
|
|
477
|
+
return kind === "date" ? presentedDate(own) : presentedNumber(kind, own, style, options);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Replace a block's `format` with what it resolves to, in place. The one step
|
|
482
|
+
* past `guarded` that a name needs here, and the last thing a style block has
|
|
483
|
+
* done to it before a target sees it (docs/adr/0056).
|
|
484
|
+
*
|
|
485
|
+
* By the time this runs the declaration is already sound: a literal passed
|
|
486
|
+
* `checkStyle` at compile, and an `=` result passed `guarded`, which omits a
|
|
487
|
+
* kind it cannot read the way it omits any other rejected value. So the
|
|
488
|
+
* `delete` below is not the omit rule -- that is `guarded`'s -- but the answer
|
|
489
|
+
* to a kind that reads and still presents nothing, which today is only a
|
|
490
|
+
* `currency` whose code Intl will not take.
|
|
491
|
+
*
|
|
492
|
+
* The block is returned rather than mutated silently, because it is the value
|
|
493
|
+
* `styleFn` hands on.
|
|
494
|
+
*
|
|
495
|
+
* @type {(style: any, options?: any) => any}
|
|
496
|
+
*/
|
|
497
|
+
export let resolveFormat = (style, options) => {
|
|
498
|
+
if (!Object.hasOwn(style, "format")) return style;
|
|
499
|
+
let resolved = formatOf(style, options);
|
|
500
|
+
if (resolved) style.format = resolved;
|
|
501
|
+
else delete style.format;
|
|
502
|
+
return style;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The fraction digits a cell presents. Public beside `format()` and
|
|
507
|
+
* `currencyOf()`, and taking the same argument they do — the whole style — so
|
|
508
|
+
* "which declarations does presentation read?" is answered in one place
|
|
509
|
+
* rather than once per helper.
|
|
510
|
+
*
|
|
511
|
+
* A resolved style already carries the answer, and a target reads it off the
|
|
512
|
+
* declaration rather than calling this. What this is for is a host holding a
|
|
513
|
+
* style the stream has not resolved: the shorthand is widened here first, so
|
|
514
|
+
* either may be asked.
|
|
515
|
+
*
|
|
516
|
+
* @param {{ format?: unknown, currency?: unknown } | null} [style] A cell's style.
|
|
517
|
+
* @param {{ currency?: string } | null} [options] The instance's currency code.
|
|
518
|
+
* @returns {number | undefined} The digits, or nothing when this cell presents
|
|
519
|
+
* no count — `date`, no `format`, an unreadable kind, and a `currency` whose
|
|
520
|
+
* code Intl will not read.
|
|
521
|
+
*/
|
|
522
|
+
export let fractionDigits = (style, options) => formatOf(style, options)?.digits;
|
|
523
|
+
|
|
524
|
+
// A row's `style` resolves onto its cells (SCHEMA.md, "Style declarations").
|
|
525
|
+
// The box is the half that cannot arrive any other way: a table row is not a
|
|
526
|
+
// box on any surface -- not a `<tr>`, not a worksheet row, not a band of the
|
|
527
|
+
// PDF's own -- so it is resolved here, once, and no target is left to have an
|
|
528
|
+
// opinion about it. Every other declaration on a row reaches its cells by the
|
|
529
|
+
// layering each target already does, which is why only the box moves.
|
|
530
|
+
//
|
|
531
|
+
// Which names are the box is decided where the block is compiled, not here: a
|
|
532
|
+
// literal row style is partitioned once per table rather than once per row.
|
|
533
|
+
// So is which of them reaches which cell -- see `boxReaching`. What is left
|
|
534
|
+
// for render is writing the answer.
|
|
535
|
+
|
|
536
|
+
// The three names of each border side, built once, which is what lets
|
|
537
|
+
// `coveredNames` complete a side from any one of its parts.
|
|
538
|
+
/** @type {Record<string, string[]>} */
|
|
539
|
+
let BORDER_NAMES = Object.fromEntries(
|
|
540
|
+
SIDES.map((side) => [side, BORDER_PARTS.map((part) => "border" + side + part)]),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* The names an under-box loses once an over-box has spoken: every name the
|
|
545
|
+
* over-box states, plus the other two of any border side it states one of.
|
|
546
|
+
*
|
|
547
|
+
* This is the whole-side rule, over declaration names rather than resolved
|
|
548
|
+
* objects -- which is all it ever needed, since a compiled block writes every
|
|
549
|
+
* name it declares whatever the values resolve to. Composing the totals block
|
|
550
|
+
* into its rows uses it at compile time, so no third style level survives to
|
|
551
|
+
* render (docs/adr/0049); fanning a row's box onto its cells uses it for the
|
|
552
|
+
* same reason (docs/adr/0056).
|
|
553
|
+
*
|
|
554
|
+
* @type {(names: Iterable<string>) => Set<string>}
|
|
555
|
+
*/
|
|
556
|
+
export let coveredNames = (names) => {
|
|
557
|
+
let out = new Set(names);
|
|
558
|
+
for (let side of SIDES)
|
|
559
|
+
if (BORDER_NAMES[side].some((name) => out.has(name)))
|
|
560
|
+
for (let name of BORDER_NAMES[side]) out.add(name);
|
|
561
|
+
return out;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Which of a row box's names reach one cell: the row's, less everything the
|
|
566
|
+
* cell's own declarations cover. A border side is won whole by the cell that
|
|
567
|
+
* names any of its three parts, so a cell that failed-soft on one name does
|
|
568
|
+
* not pick up the row's other two and become a stroke nobody declared -- the
|
|
569
|
+
* question settled here, once per table, rather than per cell per row.
|
|
570
|
+
*
|
|
571
|
+
* `null` is the answer for a cell that declares no box name at all, which is
|
|
572
|
+
* the common case: the row's box is that cell's box entire, so `fanBox` hands
|
|
573
|
+
* the object over rather than copying it name by name.
|
|
574
|
+
*
|
|
575
|
+
* @type {(rowNames: readonly string[], cellNames: readonly string[]) => string[] | null}
|
|
576
|
+
*/
|
|
577
|
+
export let boxReaching = (rowNames, cellNames) => {
|
|
578
|
+
if (!cellNames.length) return null;
|
|
579
|
+
let covered = coveredNames(cellNames);
|
|
580
|
+
return rowNames.filter((name) => !covered.has(name));
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// One cell's share of the row's box: the names settled for it, written in
|
|
584
|
+
// the row's own order, under whatever the cell declared itself. A name the
|
|
585
|
+
// row declared may still be absent -- a row's box is guarded like any other
|
|
586
|
+
// block, so one that resolved badly was never written.
|
|
587
|
+
/** @type {(box: any, style: any, names: readonly string[]) => any} */
|
|
588
|
+
let underBox = (box, style, names) => {
|
|
589
|
+
/** @type {any} */
|
|
590
|
+
let out = {};
|
|
591
|
+
for (let name of names) if (Object.hasOwn(box, name)) out[name] = box[name];
|
|
592
|
+
return Object.assign(out, style);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// What one cell's style becomes under the row's box. `null` names the common
|
|
596
|
+
// case -- the cell claimed none of the box, so the row's is the cell's own
|
|
597
|
+
// and the resolved object is shared rather than copied. An empty list is the
|
|
598
|
+
// opposite end: the cell covered all of it, and its style comes back
|
|
599
|
+
// unchanged, so `fanBox` can tell there is nothing to write.
|
|
600
|
+
/** @type {(box: any, style: any, names: string[] | null) => any} */
|
|
601
|
+
let fanned = (box, style, names) => {
|
|
602
|
+
if (names === null) return style ? { ...box, ...style } : box;
|
|
603
|
+
return names.length ? underBox(box, style, names) : style;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Resolve a row's box onto the row's cells, each taking the names settled for
|
|
608
|
+
* it at compile time (`boxReaching`). The cells are written in place: both
|
|
609
|
+
* they and the box were built for this one event and nothing else holds them
|
|
610
|
+
* yet. A row that declares no box is the common case and costs one test.
|
|
611
|
+
*
|
|
612
|
+
* @type {(box: any, cells: any[], reaching: (string[] | null)[]) => void}
|
|
613
|
+
*/
|
|
614
|
+
export let fanBox = (box, cells, reaching) => {
|
|
615
|
+
if (!box) return;
|
|
616
|
+
for (let i = 0; i < cells.length; i++) {
|
|
617
|
+
let cell = cells[i];
|
|
618
|
+
let style = fanned(box, cell.style, reaching[i]);
|
|
619
|
+
// A cell the box does not reach keeps the style object it already had, so
|
|
620
|
+
// one that carried none never gains the key.
|
|
621
|
+
if (style !== cell.style) cell.style = style;
|
|
622
|
+
}
|
|
623
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quario",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A tiny, runtime-neutral report engine — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"sjabloon",
|
|
52
52
|
"padvinder"
|
|
53
53
|
],
|
|
54
|
-
"limit": "
|
|
54
|
+
"limit": "13 kB"
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
57
|
"engines": {
|