quario 0.5.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 +386 -0
- package/README.md +49 -25
- package/lib/band-height.js +123 -0
- package/lib/format.js +140 -32
- package/lib/index.d.ts +167 -20
- package/lib/index.js +13 -9
- package/lib/license.js +1 -1
- package/lib/locate.js +32 -0
- package/lib/math.js +71 -0
- package/lib/plan.js +331 -188
- package/lib/precision.js +75 -0
- package/lib/reducers.js +165 -0
- package/lib/scope.js +7 -86
- package/lib/stream.js +19 -12
- package/lib/style.js +402 -41
- package/package.json +2 -2
package/lib/format.js
CHANGED
|
@@ -1,66 +1,174 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Present a cell's raw token as `number` / `currency` / `percent` / `date`.
|
|
3
|
-
* Targets import this instead of each other: the stream keeps the
|
|
4
|
-
* `style` and the token raw, and HTML/PDF stringify at the
|
|
5
|
-
* the wrong type, or on null, contributes nothing — the
|
|
6
|
-
* `display()`. Invalid locale or currency codes fail the
|
|
3
|
+
* Targets import this instead of each other: the stream keeps the resolved
|
|
4
|
+
* declaration on `style` and the token raw, and HTML/PDF stringify at the
|
|
5
|
+
* edge. A kind on the wrong type, or on null, contributes nothing — the
|
|
6
|
+
* caller falls back to `display()`. Invalid locale or currency codes fail the
|
|
7
|
+
* same way. Under the `currency` kind a cell's own `style.currency` names the
|
|
8
|
+
* denomination and beats the instance's default code.
|
|
7
9
|
*
|
|
8
10
|
* Defaults (`en-US`, UTC) are pinned so a PDF without a host locale still
|
|
9
11
|
* renders the same bytes on every machine (docs/adr/0041, docs/adr/0025).
|
|
12
|
+
*
|
|
13
|
+
* The kind, its digit count, and a date's form all come off the one resolved
|
|
14
|
+
* declaration `formatOf` answers — the same object the stream carries and the
|
|
15
|
+
* XLSX target builds its number formats from — so a cell shows the same
|
|
16
|
+
* digits wherever it is rendered (docs/adr/0054, docs/adr/0056). A kind that
|
|
17
|
+
* carries no count presents nothing here and the caller falls back to
|
|
18
|
+
* `display()`.
|
|
10
19
|
*/
|
|
20
|
+
import { currencyOf, formatOf } from "./style.js";
|
|
11
21
|
import { finiteDate, finiteNum, reviveDate } from "./stream.js";
|
|
12
22
|
/** @type {(options: any) => string} */
|
|
13
23
|
let localeOf = (options) => options?.locale || "en-US";
|
|
14
24
|
/** @type {(options: any) => string} */
|
|
15
25
|
let zoneOf = (options) => options?.timeZone || "UTC";
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// Building an `Intl` formatter costs about 22 µs; using one costs 0.3 µs. A
|
|
28
|
+
// report presents cell after cell from a handful of distinct formats, so
|
|
29
|
+
// constructing per token spent 3.2 s on a hundred thousand presented cells
|
|
30
|
+
// where 0.2 s does (a report of mixed currencies rendered to HTML), and the
|
|
31
|
+
// formatters are memoised.
|
|
32
|
+
//
|
|
33
|
+
// **The key is everything the formatter is made from**, and it is values
|
|
34
|
+
// rather than an object identity because no identity survives both paths: a
|
|
35
|
+
// literal style block folds to one frozen declaration every cell shares, while
|
|
36
|
+
// a block holding an `=` expression resolves a fresh one per cell (`plan.js`).
|
|
37
|
+
// Keying on values hits in both. Get a piece of it wrong and one cell presents
|
|
38
|
+
// under another's formatter -- a USD row reading as EUR -- which is the
|
|
39
|
+
// failure `test/semantics.test.js` pins a pair for, one piece at a time. Each
|
|
40
|
+
// pair must differ in **only** the piece it is there for: EUR against JPY
|
|
41
|
+
// would prove nothing about the code, because their digit counts already
|
|
42
|
+
// differ and that piece would separate them on its own.
|
|
43
|
+
//
|
|
44
|
+
// **The cap is the part to read**, because the key space is not the host's to
|
|
45
|
+
// bound. A currency code reaches this from author data through
|
|
46
|
+
// `currency: "=@.ccy"`, gated only to three uppercase letters, and `digits`
|
|
47
|
+
// can be an `=` result too: 17,576 codes times 21 counts is 369,000 keys for a
|
|
48
|
+
// single locale. Measured at 244 bytes retained per entry, that is about 90 MB
|
|
49
|
+
// held for the life of the process on data nobody vetted.
|
|
50
|
+
//
|
|
51
|
+
// So the map is bounded, and past the bound it is emptied rather than grown.
|
|
52
|
+
// A workload presenting more distinct formats than the cap pays what it paid
|
|
53
|
+
// before any of this and never more, which is the one cost that needs no
|
|
54
|
+
// argument -- and it can never hand back a formatter built for another cell,
|
|
55
|
+
// because clearing forgets rather than reuses.
|
|
56
|
+
//
|
|
57
|
+
// 2048 leaves the margin a threshold like this wants on both sides: about
|
|
58
|
+
// 500 KB at the cap, against a real report reaching a small multiple of the
|
|
59
|
+
// ISO codes actually in circulation, which is under two hundred. Raising it
|
|
60
|
+
// costs bytes and lowering it costs rebuilt formatters; neither is a
|
|
61
|
+
// correctness knob.
|
|
62
|
+
//
|
|
63
|
+
// Memoising a fact that cannot change within an ICU version, like
|
|
64
|
+
// `fractionDigits`' own minor-units map (docs/adr/0025).
|
|
65
|
+
let CAP = 2048;
|
|
66
|
+
/** @type {Map<string, any>} */
|
|
67
|
+
let formatters = new Map();
|
|
68
|
+
// A construction that throws -- an unreadable locale, zone, or currency code
|
|
69
|
+
// -- caches nothing: it reaches `format()`'s own catch, and a later cell asks
|
|
70
|
+
// again rather than finding nothing sitting in the map under a good key.
|
|
71
|
+
// Missing on `undefined` rather than `has()`, which is what `precision.js`
|
|
72
|
+
// needs because *it* caches an absent answer; a formatter is never one.
|
|
73
|
+
/** @type {(key: string, make: () => any) => any} */
|
|
74
|
+
let memo = (key, make) => {
|
|
75
|
+
let formatter = formatters.get(key);
|
|
76
|
+
if (formatter !== undefined) return formatter;
|
|
77
|
+
formatter = make();
|
|
78
|
+
if (formatters.size >= CAP) formatters.clear();
|
|
79
|
+
formatters.set(key, formatter);
|
|
80
|
+
return formatter;
|
|
27
81
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
|
|
83
|
+
// The kind leads, so a number's key can never read as a date's; the currency
|
|
84
|
+
// is empty for the two kinds that wear none.
|
|
85
|
+
/** @type {(locale: string, decl: any, rest?: any) => string} */
|
|
86
|
+
let numberKey = (locale, decl, rest) =>
|
|
87
|
+
decl.kind + "|" + locale + "|" + decl.digits + "|" + (rest?.currency || "");
|
|
88
|
+
|
|
89
|
+
/** @type {(value: any, locale: string, decl: any, rest?: any) => string | undefined} */
|
|
90
|
+
let asFixed = (value, locale, decl, rest) => {
|
|
91
|
+
if (!finiteNum(value) || decl.digits === undefined) return;
|
|
92
|
+
return memo(
|
|
93
|
+
numberKey(locale, decl, rest),
|
|
94
|
+
() =>
|
|
95
|
+
new Intl.NumberFormat(locale, {
|
|
96
|
+
...rest,
|
|
97
|
+
minimumFractionDigits: decl.digits,
|
|
98
|
+
maximumFractionDigits: decl.digits,
|
|
99
|
+
}),
|
|
100
|
+
).format(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** @type {(value: any, locale: string, decl: any) => string | undefined} */
|
|
104
|
+
let asNumber = (value, locale, decl) => asFixed(value, locale, decl);
|
|
105
|
+
/** @type {(value: any, locale: string, decl: any) => string | undefined} */
|
|
106
|
+
let asPercent = (value, locale, decl) =>
|
|
107
|
+
// `style: "percent"` multiplies by 100 and the digits count the *displayed*
|
|
108
|
+
// places, which is what Excel's `0.00%` already means too — so the count
|
|
109
|
+
// crosses to the sheet with no translation (docs/adr/0056).
|
|
110
|
+
asFixed(value, locale, decl, { style: "percent" });
|
|
111
|
+
// The code the cell actually wears is what the digit count was asked about, not
|
|
112
|
+
// the instance's: minor units are a property of the currency, so a row that
|
|
113
|
+
// names JPY presents no decimals beside one naming EUR that presents two
|
|
114
|
+
// (docs/adr/0041, docs/adr/0054).
|
|
115
|
+
/** @type {(value: any, locale: string, decl: any, options: any, style: any) => string | undefined} */
|
|
116
|
+
let asMoney = (value, locale, decl, options, style) => {
|
|
117
|
+
let currency = currencyOf(style, options);
|
|
118
|
+
return currency ? asFixed(value, locale, decl, { style: "currency", currency }) : undefined;
|
|
33
119
|
};
|
|
34
120
|
// A string in an accepted RFC 3339 form revives to the Date a host would have
|
|
35
121
|
// injected, then presents like any other Date — including in the instance's
|
|
36
122
|
// timezone, so a calendar date under a western zone presents as the day
|
|
37
123
|
// before, exactly as an injected `new Date("2026-08-14")` does today. The two
|
|
38
124
|
// spellings never disagree, which is the point (ADR 0041).
|
|
39
|
-
|
|
40
|
-
|
|
125
|
+
//
|
|
126
|
+
// The form is Intl's own `dateStyle`, and it is always present: a declaration
|
|
127
|
+
// that named none resolved to `medium` (docs/adr/0056). This is the half that
|
|
128
|
+
// does not converge — the worksheet approximates it (CONTEXT.md, "Form").
|
|
129
|
+
// `date` leads for the same reason the number kinds do, so the two key shapes
|
|
130
|
+
// live beside each other and "one piece at a time" is auditable in one place.
|
|
131
|
+
/** @type {(locale: string, decl: any, zone: string) => string} */
|
|
132
|
+
let dateKey = (locale, decl, zone) => "date|" + locale + "|" + decl.form + "|" + zone;
|
|
133
|
+
|
|
134
|
+
/** @type {(value: any, locale: string, decl: any, options: any) => string | undefined} */
|
|
135
|
+
let asDate = (value, locale, decl, options) => {
|
|
41
136
|
let date = finiteDate(value) ? value : reviveDate(value);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
137
|
+
if (!date) return;
|
|
138
|
+
let zone = zoneOf(options);
|
|
139
|
+
return memo(
|
|
140
|
+
dateKey(locale, decl, zone),
|
|
141
|
+
() => new Intl.DateTimeFormat(locale, { timeZone: zone, dateStyle: decl.form }),
|
|
142
|
+
).format(date);
|
|
45
143
|
};
|
|
46
144
|
|
|
47
|
-
/** @type {Record<string, (value: any, locale: string, options: any) => string | undefined>} */
|
|
145
|
+
/** @type {Record<string, (value: any, locale: string, decl: any, options: any, style: any) => string | undefined>} */
|
|
48
146
|
let KINDS = { number: asNumber, currency: asMoney, percent: asPercent, date: asDate };
|
|
49
147
|
|
|
50
148
|
/**
|
|
149
|
+
* The helper takes the whole resolved `style`, not just the declaration, so
|
|
150
|
+
* the question "which declarations does presentation read?" is answered here
|
|
151
|
+
* once rather than once per target (docs/adr/0014).
|
|
152
|
+
*
|
|
51
153
|
* @param {unknown} value The interpolation's pre-stringify token.
|
|
52
|
-
* @param {unknown}
|
|
154
|
+
* @param {{ format?: unknown, currency?: unknown } | null} [style] The cell's
|
|
155
|
+
* resolved style block: its `format` declaration, and its own `currency`
|
|
156
|
+
* code when it declares one.
|
|
53
157
|
* @param {{ locale?: string, currency?: string, timeZone?: string } | null} [options]
|
|
54
|
-
* The instance's locale, currency code, and timezone.
|
|
158
|
+
* The instance's locale, default currency code, and timezone.
|
|
55
159
|
* @returns {string | undefined} The presented text, or nothing when this
|
|
56
160
|
* kind does not apply.
|
|
57
161
|
*/
|
|
58
|
-
export function format(value,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
162
|
+
export function format(value, style, options) {
|
|
163
|
+
// A style the stream resolved already carries its declaration whole; one a
|
|
164
|
+
// host built by hand may still hold the shorthand, so it is widened here
|
|
165
|
+
// rather than refused. No own-key guard on the lookup: `formatOf` answers a
|
|
166
|
+
// kind only from the closed vocabulary, which is exactly `KINDS`' own keys,
|
|
167
|
+
// so nothing an `=` expression resolved to can reach this table.
|
|
168
|
+
let decl = formatOf(style, options);
|
|
169
|
+
if (!decl) return;
|
|
62
170
|
try {
|
|
63
|
-
return
|
|
171
|
+
return KINDS[decl.kind](value, localeOf(options), decl, options, style);
|
|
64
172
|
} catch {
|
|
65
173
|
return;
|
|
66
174
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -93,8 +93,20 @@ export interface StyleDeclarations extends BoxDeclarations {
|
|
|
93
93
|
* and on a band image, whose boxes have no such slack.
|
|
94
94
|
*/
|
|
95
95
|
valign?: ExpressionValue<VAlign>;
|
|
96
|
-
/**
|
|
97
|
-
|
|
96
|
+
/**
|
|
97
|
+
* How a number or date is presented: a kind, or an object naming one and
|
|
98
|
+
* carrying the single modifier that kind understands — `digits` for the
|
|
99
|
+
* three number kinds, `form` for `date`. Locale stays on the instance.
|
|
100
|
+
*/
|
|
101
|
+
format?: ExpressionValue<FormatDeclaration>;
|
|
102
|
+
/**
|
|
103
|
+
* What this cell's money is denominated in, under `format: "currency"`:
|
|
104
|
+
* a three-letter ISO 4217 code, beating the instance's default. A literal
|
|
105
|
+
* that is not one is a definition error; an `=` result that is not one is
|
|
106
|
+
* the cell's own answer still, so the cell presents as display text rather
|
|
107
|
+
* than falling back to the instance's currency. Unread without the kind.
|
|
108
|
+
*/
|
|
109
|
+
currency?: ExpressionValue<string>;
|
|
98
110
|
/** Blank space before this item, in points. */
|
|
99
111
|
spaceBefore?: ExpressionValue<number>;
|
|
100
112
|
/** Blank space after this item, in points. */
|
|
@@ -113,6 +125,32 @@ export type Align = "left" | "center" | "right";
|
|
|
113
125
|
export type VAlign = "top" | "middle" | "bottom";
|
|
114
126
|
export type FormatKind = "number" | "currency" | "percent" | "date";
|
|
115
127
|
|
|
128
|
+
/** The four named date forms, Intl's own `dateStyle`. A `date` that names
|
|
129
|
+
* none is `"medium"`. */
|
|
130
|
+
export type DateForm = "short" | "medium" | "long" | "full";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* How an author writes `format`: the kind alone, or the kind carrying its one
|
|
134
|
+
* modifier. `digits` is a whole number from 0 to 20 — the range both a
|
|
135
|
+
* presenting target and a worksheet number format can honour — and it beats
|
|
136
|
+
* the count the kind would otherwise present, a currency's minor units
|
|
137
|
+
* included. A modifier on a kind that does not take it is a definition error.
|
|
138
|
+
*/
|
|
139
|
+
export type FormatDeclaration =
|
|
140
|
+
| FormatKind
|
|
141
|
+
| { kind: "number" | "currency" | "percent"; digits?: number }
|
|
142
|
+
| { kind: "date"; form?: DateForm };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What `format` resolves to on the event stream: never the shorthand, always
|
|
146
|
+
* the kind with the answer it presents. A number kind carries `digits` unless
|
|
147
|
+
* it has no count at all — a `currency` whose code cannot be read — and a
|
|
148
|
+
* `date` always carries its `form`.
|
|
149
|
+
*/
|
|
150
|
+
export type ResolvedFormat =
|
|
151
|
+
| { kind: "number" | "currency" | "percent"; digits?: number }
|
|
152
|
+
| { kind: "date"; form: DateForm };
|
|
153
|
+
|
|
116
154
|
export interface Cell {
|
|
117
155
|
value: CellValue;
|
|
118
156
|
visible?: ExpressionValue<boolean>;
|
|
@@ -162,12 +200,32 @@ export interface ImageItem {
|
|
|
162
200
|
style?: ImageStyleDeclarations;
|
|
163
201
|
}
|
|
164
202
|
|
|
203
|
+
/**
|
|
204
|
+
* What a text slot declares: the vocabulary less flow spacing, which is a
|
|
205
|
+
* band-item pad and does not apply inside a split.
|
|
206
|
+
*/
|
|
207
|
+
export type SlotStyleDeclarations = Omit<StyleDeclarations, "spaceBefore" | "spaceAfter">;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* What an image slot declares: an image item's names less flow spacing, plus
|
|
211
|
+
* `valign` — the one name an image accepts in a slot and nowhere else, because
|
|
212
|
+
* a slot's box is the split's height rather than the picture's own.
|
|
213
|
+
*/
|
|
214
|
+
export type SlotImageStyleDeclarations = Omit<
|
|
215
|
+
ImageStyleDeclarations,
|
|
216
|
+
"spaceBefore" | "spaceAfter"
|
|
217
|
+
> &
|
|
218
|
+
Pick<StyleDeclarations, "valign">;
|
|
219
|
+
|
|
165
220
|
/**
|
|
166
221
|
* One slot of a split: an ordinary item, plus the width share that is a slot
|
|
167
222
|
* property rather than an item one. A split may not stand in a slot —
|
|
168
223
|
* placement inside placement is the coordinate system quario does not have.
|
|
169
224
|
*/
|
|
170
|
-
export type SplitSlot = (
|
|
225
|
+
export type SplitSlot = (
|
|
226
|
+
| (Omit<TextItem, "style"> & { style?: SlotStyleDeclarations })
|
|
227
|
+
| (Omit<ImageItem, "style"> & { style?: SlotImageStyleDeclarations })
|
|
228
|
+
) & {
|
|
171
229
|
/** Slot width as a percentage of the content width (0 < width <= 100). */
|
|
172
230
|
width?: number;
|
|
173
231
|
};
|
|
@@ -232,6 +290,16 @@ export interface TotalRow {
|
|
|
232
290
|
style?: StyleDeclarations;
|
|
233
291
|
}
|
|
234
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The table's summary block: the rows, plus what belongs to the block rather
|
|
295
|
+
* than to any one of them. Its `style` is the layer below each row's — box
|
|
296
|
+
* under box, everything else under everything else — and exact `false` on its
|
|
297
|
+
* `visible` omits the block whole, its rows never evaluated.
|
|
298
|
+
*/
|
|
299
|
+
export interface TotalBlock extends TableRow {
|
|
300
|
+
rows: [TotalRow, ...TotalRow[]];
|
|
301
|
+
}
|
|
302
|
+
|
|
235
303
|
/**
|
|
236
304
|
* The header row's own box — distinct from `TableHeader`, which is one
|
|
237
305
|
* column's header cell. This is the box the row wears, replayed with the
|
|
@@ -245,7 +313,7 @@ export interface TableDetail {
|
|
|
245
313
|
header?: TableHeaderBox;
|
|
246
314
|
row?: TableRow;
|
|
247
315
|
columns: [TableColumn, ...TableColumn[]];
|
|
248
|
-
total?:
|
|
316
|
+
total?: TotalBlock;
|
|
249
317
|
}
|
|
250
318
|
|
|
251
319
|
export interface Group {
|
|
@@ -351,7 +419,10 @@ export interface QuarioOptions {
|
|
|
351
419
|
license?: string;
|
|
352
420
|
/** Locale for `format`. Defaults to `"en-US"` when a kind is presented. */
|
|
353
421
|
locale?: string;
|
|
354
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* Default currency code for `format: "currency"`, overridden by a cell's own
|
|
424
|
+
* `style.currency`. Absent from both, that kind contributes nothing.
|
|
425
|
+
*/
|
|
355
426
|
currency?: string;
|
|
356
427
|
/** Timezone for `format: "date"`. Defaults to `"UTC"`. */
|
|
357
428
|
timeZone?: string;
|
|
@@ -393,7 +464,7 @@ export interface EventCell {
|
|
|
393
464
|
/**
|
|
394
465
|
* The definition's schema path, on cells that belong to one: a `row` cell
|
|
395
466
|
* carries its column's (`detail.columns[i]`), a `total-row` cell its
|
|
396
|
-
* `detail.total[r].cells[i]` entry. Absent on page-band cells reached by closure.
|
|
467
|
+
* `detail.total.rows[r].cells[i]` entry. Absent on page-band cells reached by closure.
|
|
397
468
|
*/
|
|
398
469
|
path?: string;
|
|
399
470
|
}
|
|
@@ -406,14 +477,21 @@ export interface PageInfo {
|
|
|
406
477
|
total: number;
|
|
407
478
|
}
|
|
408
479
|
|
|
480
|
+
/**
|
|
481
|
+
* What a page band closure hands back: the band's items as the walk would
|
|
482
|
+
* emit them, wearing the `page-header`/`page-footer` role — a text item, an
|
|
483
|
+
* image, or a split's bracket with one item per slot between it.
|
|
484
|
+
*/
|
|
485
|
+
export type PageBandEvent = ItemEvent | ImageEvent | SplitStartEvent | SplitEndEvent;
|
|
486
|
+
|
|
409
487
|
/**
|
|
410
488
|
* Per-render page band closures on `report-start` (present only when the
|
|
411
489
|
* schema declares page bands). A paginated target calls them once per page;
|
|
412
|
-
* each call returns
|
|
490
|
+
* each call returns that page's resolved band events.
|
|
413
491
|
*/
|
|
414
492
|
export interface PageBandRenderers {
|
|
415
|
-
header?: (page: PageInfo) =>
|
|
416
|
-
footer?: (page: PageInfo) =>
|
|
493
|
+
header?: (page: PageInfo) => PageBandEvent[];
|
|
494
|
+
footer?: (page: PageInfo) => PageBandEvent[];
|
|
417
495
|
}
|
|
418
496
|
|
|
419
497
|
export interface ReportStartEvent {
|
|
@@ -523,10 +601,15 @@ export interface TableStartEvent {
|
|
|
523
601
|
type: "table-start";
|
|
524
602
|
/** Always `detail` — the table definition's schema path. */
|
|
525
603
|
path: string;
|
|
526
|
-
/**
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
604
|
+
/**
|
|
605
|
+
* The header row: one cell per column a neighbour's span does not cover,
|
|
606
|
+
* so fewer cells than columns whenever one spans. Shaped like a `row`
|
|
607
|
+
* payload, `style` being what is left of `detail.header`'s block once its
|
|
608
|
+
* box has landed on these cells.
|
|
609
|
+
*/
|
|
610
|
+
header: { cells: EventCell[]; style?: Record<string, unknown> };
|
|
611
|
+
/** The column geometry, one entry per column. */
|
|
612
|
+
columns: { width?: number }[];
|
|
530
613
|
}
|
|
531
614
|
|
|
532
615
|
export interface RowEvent {
|
|
@@ -706,6 +789,22 @@ export function validate(schema: unknown, functions?: FunctionRegistry): string[
|
|
|
706
789
|
*/
|
|
707
790
|
export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
|
|
708
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Mint an image failure named on the item that asked for the bytes — for a
|
|
794
|
+
* **render target**, which is where every image failure past the magic
|
|
795
|
+
* numbers happens: the engine vouches for those and no further.
|
|
796
|
+
*
|
|
797
|
+
* `path` is the image item's, from a stream event or a display-list op;
|
|
798
|
+
* `.source` is appended, that being the key an author would change. `said` is
|
|
799
|
+
* what failed in your own words, and a library's own message belongs after it
|
|
800
|
+
* rather than instead of it. The result is `cause`'s class, so an embedder's
|
|
801
|
+
* `RangeError` stays a `RangeError`, with `cause` set.
|
|
802
|
+
*
|
|
803
|
+
* Not a diagnostic: `isDiagnostic` does not answer for it, a decoder that
|
|
804
|
+
* refused a file being nobody's delegated engine.
|
|
805
|
+
*/
|
|
806
|
+
export function imageError(path: string | undefined, said: string, cause?: unknown): Error;
|
|
807
|
+
|
|
709
808
|
/**
|
|
710
809
|
* Join a token stream to display text: literals verbatim, values through
|
|
711
810
|
* `display()`. Re-exported from sjabloon.
|
|
@@ -722,29 +821,62 @@ export function text(tokens: readonly Token[]): string;
|
|
|
722
821
|
export function display(value: unknown): string;
|
|
723
822
|
|
|
724
823
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
824
|
+
* Which currency code a cell wears under `format: "currency"`: its own
|
|
825
|
+
* `style.currency` when it declares one — the gate's `null` included, which is
|
|
826
|
+
* why an unusable code presents nothing rather than the instance's currency —
|
|
827
|
+
* else the instance default. Exported for a target that needs the code itself
|
|
828
|
+
* rather than the presented text, as the XLSX one does.
|
|
829
|
+
*/
|
|
830
|
+
export function currencyOf(
|
|
831
|
+
style?: { currency?: unknown } | null,
|
|
832
|
+
options?: { currency?: string } | null,
|
|
833
|
+
): string | null;
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Present a token for a cell's resolved `style`. Reads the `format` kind and,
|
|
837
|
+
* under `"currency"`, the cell's own `currency` code; returns nothing when the
|
|
838
|
+
* kind does not apply, so the caller falls back to `display()`. Locale
|
|
839
|
+
* defaults to `"en-US"`, dates to UTC.
|
|
728
840
|
*/
|
|
729
841
|
export function format(
|
|
730
842
|
value: unknown,
|
|
731
|
-
|
|
843
|
+
style?: { format?: unknown; currency?: unknown } | null,
|
|
732
844
|
options?: { locale?: string; currency?: string; timeZone?: string } | null,
|
|
733
845
|
): string | undefined;
|
|
734
846
|
|
|
847
|
+
/**
|
|
848
|
+
* How many fraction digits a cell presents. Takes the whole style, as
|
|
849
|
+
* `format()` and `currencyOf()` do. `number` and `percent` present a fixed
|
|
850
|
+
* count, `currency` its currency's minor units, and a declared `digits` beats
|
|
851
|
+
* either — so the digits converge wherever the cell is rendered.
|
|
852
|
+
*
|
|
853
|
+
* A resolved style carries the answer on `style.format.digits` and a target
|
|
854
|
+
* reads it there; this is for a host holding a style the stream has not
|
|
855
|
+
* resolved, and the shorthand is widened here first.
|
|
856
|
+
*
|
|
857
|
+
* Returns nothing when the cell presents no count: `date`, no `format`, an
|
|
858
|
+
* unknown kind, and a `currency` with no code or one Intl will not read. A
|
|
859
|
+
* caller that gets nothing presents some other way — `format()` falls back to
|
|
860
|
+
* `display()`, and a worksheet writes no number format.
|
|
861
|
+
*/
|
|
862
|
+
export function fractionDigits(
|
|
863
|
+
style?: { format?: unknown; currency?: unknown } | null,
|
|
864
|
+
options?: { currency?: string } | null,
|
|
865
|
+
): number | undefined;
|
|
866
|
+
|
|
735
867
|
/**
|
|
736
868
|
* The typed-cell seam: exactly one value token holding a finite number, a
|
|
737
869
|
* boolean, or a valid Date keeps its pre-stringify value; anything else —
|
|
738
870
|
* including a lone null — reports `undefined` and joins to display text.
|
|
739
871
|
*
|
|
740
|
-
* Passing the cell's resolved `format`
|
|
741
|
-
* coercion: under `
|
|
872
|
+
* Passing the cell's resolved `format` declaration opts into the seam's one
|
|
873
|
+
* coercion: under the `date` kind, a string in either accepted RFC 3339 form —
|
|
742
874
|
* `YYYY-MM-DD`, or a timestamp naming its offset — revives to the Date it
|
|
743
875
|
* names. Omitting the argument leaves the seam as it was.
|
|
744
876
|
*/
|
|
745
877
|
export function typed(
|
|
746
878
|
tokens: readonly Token[],
|
|
747
|
-
|
|
879
|
+
decl?: ResolvedFormat | FormatKind,
|
|
748
880
|
): number | boolean | Date | undefined;
|
|
749
881
|
|
|
750
882
|
/**
|
|
@@ -762,3 +894,18 @@ export function isReportBand(role: string | undefined): boolean;
|
|
|
762
894
|
* keeping a copy.
|
|
763
895
|
*/
|
|
764
896
|
export const STYLE_NAMES: readonly string[];
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The `format` vocabulary as data, for a tool that offers it rather than
|
|
900
|
+
* checks it — a style rail's kind picker and its modifier control — so no
|
|
901
|
+
* surface keeps a copy that has to be told about a change.
|
|
902
|
+
*/
|
|
903
|
+
export const FORMAT_VOCABULARY: {
|
|
904
|
+
readonly kinds: readonly FormatKind[];
|
|
905
|
+
readonly forms: readonly DateForm[];
|
|
906
|
+
/** Which modifier each kind takes, so a surface offering the pair need not
|
|
907
|
+
* know that three kinds share `digits` and `date` does not. */
|
|
908
|
+
readonly modifiers: Readonly<Record<FormatKind, "digits" | "form">>;
|
|
909
|
+
readonly defaultForm: DateForm;
|
|
910
|
+
readonly maxDigits: number;
|
|
911
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* siblings beside it, one seam each. See SCHEMA.md for the document shape.
|
|
14
14
|
*/
|
|
15
15
|
import { relocate as relocateQuery } from "padvinder";
|
|
16
|
+
import { LEAD_REFUSED, isLead, leadPath } from "./band-height.js";
|
|
16
17
|
import { MARKING, verify } from "./license.js";
|
|
17
18
|
import { err, fault, locate } from "./locate.js";
|
|
18
19
|
import { BLOCKED, NAME, record } from "./names.js";
|
|
@@ -25,8 +26,8 @@ import { opt } from "./stream.js";
|
|
|
25
26
|
// imports them from the package, not from a file inside it.
|
|
26
27
|
export { breathe, display, isReportBand, text, typed, walk } from "./stream.js";
|
|
27
28
|
export { format } from "./format.js";
|
|
28
|
-
export { STYLE_NAMES } from "./style.js";
|
|
29
|
-
export { isDiagnostic } from "./locate.js";
|
|
29
|
+
export { currencyOf, FORMAT_VOCABULARY, fractionDigits, STYLE_NAMES } from "./style.js";
|
|
30
|
+
export { imageError, isDiagnostic } from "./locate.js";
|
|
30
31
|
|
|
31
32
|
/** @typedef {import("./scope.js").Scope} Scope */
|
|
32
33
|
|
|
@@ -134,16 +135,19 @@ function events(schema, funcs, options, marking) {
|
|
|
134
135
|
/** @type {(event: any) => boolean} */
|
|
135
136
|
let occupying = (event) =>
|
|
136
137
|
event.type === "item" || event.type === "image" || event.type === "split-start";
|
|
137
|
-
/** @type {(n: any) => boolean} */
|
|
138
|
-
let nonzeroLead = (n) => Number.isFinite(n) && n !== 0;
|
|
139
|
-
/** @type {(event: any) => string} */
|
|
140
|
-
let eventPath = (event) => event.path || "header";
|
|
141
138
|
/** @type {(event: any) => void} */
|
|
142
139
|
let refuseLead = (event) => {
|
|
143
|
-
if (
|
|
144
|
-
fault(eventPath(event) + ".style.spaceBefore", "must be 0 after a height-declared header");
|
|
140
|
+
if (isLead(event.style?.spaceBefore)) fault(leadPath(event.path), LEAD_REFUSED);
|
|
145
141
|
};
|
|
146
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Refuse the lead the document could not settle. `plan()` has already refused
|
|
144
|
+
* every candidate the document itself decides (./band-height.js); what reaches
|
|
145
|
+
* here is the rest — a lead that is an expression, or an item whose occupancy
|
|
146
|
+
* only the data knows — so the first occupying event of the band under the
|
|
147
|
+
* pinned box is the one this sees, whichever band that turned out to be.
|
|
148
|
+
*
|
|
149
|
+
* @type {(events: Iterable<any>) => Generator<any>}
|
|
150
|
+
*/
|
|
147
151
|
function* guardPin(events) {
|
|
148
152
|
let pending = true;
|
|
149
153
|
for (let event of events) {
|
package/lib/license.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// reassigns them. A key is valid for every version released inside its window
|
|
12
12
|
// (LICENSE section 7), so validity compares ISO date strings and never reads
|
|
13
13
|
// a clock — accepted output stays accepted.
|
|
14
|
-
let RELEASE = "2026-09-
|
|
14
|
+
let RELEASE = "2026-09-07"; // x-release-please-date
|
|
15
15
|
// The verifying half of the signing pair: the 65-byte uncompressed P-256
|
|
16
16
|
// point, base64url. The private half never enters the repo;
|
|
17
17
|
// scripts/license/sign.mjs mints keys against it.
|
package/lib/locate.js
CHANGED
|
@@ -37,6 +37,38 @@ export let fault = (path, msg) => {
|
|
|
37
37
|
throw Object.assign(Error(path + ": " + msg), { [LOCATION]: { path } });
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* An image failure a **target** raises, named on the item that asked for the
|
|
42
|
+
* bytes. The engine vouches for an image's magic numbers and no further
|
|
43
|
+
* (SCHEMA.md, "Image item"), so every failure past that point belongs to
|
|
44
|
+
* whichever target was sizing or embedding the file — and each of them was
|
|
45
|
+
* inventing its own wording, down to a corrupt PNG reaching a host as
|
|
46
|
+
* pdf-lib's bare "Invalid typed array length: 0" (quario-e0bz).
|
|
47
|
+
*
|
|
48
|
+
* Here rather than in a target because three of them raise it and no two of
|
|
49
|
+
* them may depend on each other; the engine is what they all already have.
|
|
50
|
+
* Minted rather than thrown, unlike `fault` beside it, so a caller can raise
|
|
51
|
+
* it from a `catch` or a rejection in whichever style its own code is.
|
|
52
|
+
*
|
|
53
|
+
* The shape is the one every render error has: the path prefixes the message
|
|
54
|
+
* and the failing class survives behind it. It carries no `LOCATION` and is no
|
|
55
|
+
* `isDiagnostic` diagnostic — the traversal never sees it, and a decoder that
|
|
56
|
+
* refused a file is not a delegated engine.
|
|
57
|
+
*
|
|
58
|
+
* @type {(path: string | undefined, said: string, cause?: unknown) => Error}
|
|
59
|
+
*/
|
|
60
|
+
export let imageError = (path, said, cause) => {
|
|
61
|
+
let error = TypeError((path ? path + ".source: " : "") + said, { cause });
|
|
62
|
+
// Wear the failing class rather than construct it. Keeping
|
|
63
|
+
// `e instanceof RangeError` true matters — a host may already be catching
|
|
64
|
+
// one — but the class is a library's and its constructor need not take
|
|
65
|
+
// `(message, options)` at all: one taking other arguments would mangle the
|
|
66
|
+
// message and swallow `cause`, turning a located failure back into the
|
|
67
|
+
// stray throw this exists to replace. Reparenting asks the class for
|
|
68
|
+
// nothing, so there is no call to check and nothing it can refuse.
|
|
69
|
+
if (cause instanceof Error) Object.setPrototypeOf(error, Object.getPrototypeOf(cause));
|
|
70
|
+
return error;
|
|
71
|
+
};
|
|
40
72
|
/**
|
|
41
73
|
* Re-throw `error` located at `path`, with `src` — the offending author
|
|
42
74
|
* source — in the message.
|