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/index.d.ts
CHANGED
|
@@ -87,8 +87,26 @@ export interface StyleDeclarations extends BoxDeclarations {
|
|
|
87
87
|
background?: ExpressionValue<string>;
|
|
88
88
|
/** Horizontal alignment within the cell. */
|
|
89
89
|
align?: ExpressionValue<Align>;
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Vertical alignment within a box taller than its content: a table cell or
|
|
92
|
+
* row, a split slot (text or image). A definition error on a stacked item
|
|
93
|
+
* and on a band image, whose boxes have no such slack.
|
|
94
|
+
*/
|
|
95
|
+
valign?: ExpressionValue<VAlign>;
|
|
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>;
|
|
92
110
|
/** Blank space before this item, in points. */
|
|
93
111
|
spaceBefore?: ExpressionValue<number>;
|
|
94
112
|
/** Blank space after this item, in points. */
|
|
@@ -104,8 +122,35 @@ export interface SortKey {
|
|
|
104
122
|
export type CellValue = string;
|
|
105
123
|
|
|
106
124
|
export type Align = "left" | "center" | "right";
|
|
125
|
+
export type VAlign = "top" | "middle" | "bottom";
|
|
107
126
|
export type FormatKind = "number" | "currency" | "percent" | "date";
|
|
108
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
|
+
|
|
109
154
|
export interface Cell {
|
|
110
155
|
value: CellValue;
|
|
111
156
|
visible?: ExpressionValue<boolean>;
|
|
@@ -155,12 +200,32 @@ export interface ImageItem {
|
|
|
155
200
|
style?: ImageStyleDeclarations;
|
|
156
201
|
}
|
|
157
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
|
+
|
|
158
220
|
/**
|
|
159
221
|
* One slot of a split: an ordinary item, plus the width share that is a slot
|
|
160
222
|
* property rather than an item one. A split may not stand in a slot —
|
|
161
223
|
* placement inside placement is the coordinate system quario does not have.
|
|
162
224
|
*/
|
|
163
|
-
export type SplitSlot = (
|
|
225
|
+
export type SplitSlot = (
|
|
226
|
+
| (Omit<TextItem, "style"> & { style?: SlotStyleDeclarations })
|
|
227
|
+
| (Omit<ImageItem, "style"> & { style?: SlotImageStyleDeclarations })
|
|
228
|
+
) & {
|
|
164
229
|
/** Slot width as a percentage of the content width (0 < width <= 100). */
|
|
165
230
|
width?: number;
|
|
166
231
|
};
|
|
@@ -185,10 +250,19 @@ export interface TableHeader {
|
|
|
185
250
|
value: CellValue;
|
|
186
251
|
/** Literal only — never an expression. */
|
|
187
252
|
style?: StyleDeclarations;
|
|
253
|
+
/**
|
|
254
|
+
* How many columns this header covers — a literal integer >= 1, never an
|
|
255
|
+
* expression; absent is 1. The columns it covers omit their own `header`.
|
|
256
|
+
*/
|
|
257
|
+
span?: number;
|
|
188
258
|
}
|
|
189
259
|
|
|
190
260
|
export interface TableColumn extends Cell {
|
|
191
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Absent exactly when a neighbour's header `span` covers this column: every
|
|
263
|
+
* column's header slot is filled once, by its own or by a span.
|
|
264
|
+
*/
|
|
265
|
+
header?: string | TableHeader;
|
|
192
266
|
/** Column width as a percentage of the table width (0 < width <= 100). */
|
|
193
267
|
width?: number;
|
|
194
268
|
}
|
|
@@ -198,12 +272,34 @@ export interface TableRow {
|
|
|
198
272
|
style?: StyleDeclarations;
|
|
199
273
|
}
|
|
200
274
|
|
|
275
|
+
/**
|
|
276
|
+
* One cell of a total row: an ordinary cell, plus the columns it covers. The
|
|
277
|
+
* spans in a row must sum to the column count exactly.
|
|
278
|
+
*/
|
|
279
|
+
export type TotalCell = Cell & {
|
|
280
|
+
/**
|
|
281
|
+
* How many columns this cell covers — a literal integer >= 1, never an
|
|
282
|
+
* expression; absent is 1.
|
|
283
|
+
*/
|
|
284
|
+
span?: number;
|
|
285
|
+
};
|
|
286
|
+
|
|
201
287
|
export interface TotalRow {
|
|
202
|
-
cells: [
|
|
288
|
+
cells: [TotalCell, ...TotalCell[]];
|
|
203
289
|
visible?: ExpressionValue<boolean>;
|
|
204
290
|
style?: StyleDeclarations;
|
|
205
291
|
}
|
|
206
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
|
+
|
|
207
303
|
/**
|
|
208
304
|
* The header row's own box — distinct from `TableHeader`, which is one
|
|
209
305
|
* column's header cell. This is the box the row wears, replayed with the
|
|
@@ -217,7 +313,7 @@ export interface TableDetail {
|
|
|
217
313
|
header?: TableHeaderBox;
|
|
218
314
|
row?: TableRow;
|
|
219
315
|
columns: [TableColumn, ...TableColumn[]];
|
|
220
|
-
total?:
|
|
316
|
+
total?: TotalBlock;
|
|
221
317
|
}
|
|
222
318
|
|
|
223
319
|
export interface Group {
|
|
@@ -323,7 +419,10 @@ export interface QuarioOptions {
|
|
|
323
419
|
license?: string;
|
|
324
420
|
/** Locale for `format`. Defaults to `"en-US"` when a kind is presented. */
|
|
325
421
|
locale?: string;
|
|
326
|
-
/**
|
|
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
|
+
*/
|
|
327
426
|
currency?: string;
|
|
328
427
|
/** Timezone for `format: "date"`. Defaults to `"UTC"`. */
|
|
329
428
|
timeZone?: string;
|
|
@@ -357,10 +456,15 @@ export type Token = LiteralToken | ValueToken;
|
|
|
357
456
|
export interface EventCell {
|
|
358
457
|
tokens: Token[];
|
|
359
458
|
style?: Record<string, unknown>;
|
|
459
|
+
/**
|
|
460
|
+
* How many columns this cell covers, present only when more than one: a
|
|
461
|
+
* span of 1 is the absence of one and never reaches the stream.
|
|
462
|
+
*/
|
|
463
|
+
span?: number;
|
|
360
464
|
/**
|
|
361
465
|
* The definition's schema path, on cells that belong to one: a `row` cell
|
|
362
466
|
* carries its column's (`detail.columns[i]`), a `total-row` cell its
|
|
363
|
-
* `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.
|
|
364
468
|
*/
|
|
365
469
|
path?: string;
|
|
366
470
|
}
|
|
@@ -373,14 +477,21 @@ export interface PageInfo {
|
|
|
373
477
|
total: number;
|
|
374
478
|
}
|
|
375
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
|
+
|
|
376
487
|
/**
|
|
377
488
|
* Per-render page band closures on `report-start` (present only when the
|
|
378
489
|
* schema declares page bands). A paginated target calls them once per page;
|
|
379
|
-
* each call returns
|
|
490
|
+
* each call returns that page's resolved band events.
|
|
380
491
|
*/
|
|
381
492
|
export interface PageBandRenderers {
|
|
382
|
-
header?: (page: PageInfo) =>
|
|
383
|
-
footer?: (page: PageInfo) =>
|
|
493
|
+
header?: (page: PageInfo) => PageBandEvent[];
|
|
494
|
+
footer?: (page: PageInfo) => PageBandEvent[];
|
|
384
495
|
}
|
|
385
496
|
|
|
386
497
|
export interface ReportStartEvent {
|
|
@@ -490,9 +601,15 @@ export interface TableStartEvent {
|
|
|
490
601
|
type: "table-start";
|
|
491
602
|
/** Always `detail` — the table definition's schema path. */
|
|
492
603
|
path: string;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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 }[];
|
|
496
613
|
}
|
|
497
614
|
|
|
498
615
|
export interface RowEvent {
|
|
@@ -672,6 +789,22 @@ export function validate(schema: unknown, functions?: FunctionRegistry): string[
|
|
|
672
789
|
*/
|
|
673
790
|
export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
|
|
674
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
|
+
|
|
675
808
|
/**
|
|
676
809
|
* Join a token stream to display text: literals verbatim, values through
|
|
677
810
|
* `display()`. Re-exported from sjabloon.
|
|
@@ -688,29 +821,62 @@ export function text(tokens: readonly Token[]): string;
|
|
|
688
821
|
export function display(value: unknown): string;
|
|
689
822
|
|
|
690
823
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
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.
|
|
694
840
|
*/
|
|
695
841
|
export function format(
|
|
696
842
|
value: unknown,
|
|
697
|
-
|
|
843
|
+
style?: { format?: unknown; currency?: unknown } | null,
|
|
698
844
|
options?: { locale?: string; currency?: string; timeZone?: string } | null,
|
|
699
845
|
): string | undefined;
|
|
700
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
|
+
|
|
701
867
|
/**
|
|
702
868
|
* The typed-cell seam: exactly one value token holding a finite number, a
|
|
703
869
|
* boolean, or a valid Date keeps its pre-stringify value; anything else —
|
|
704
870
|
* including a lone null — reports `undefined` and joins to display text.
|
|
705
871
|
*
|
|
706
|
-
* Passing the cell's resolved `format`
|
|
707
|
-
* 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 —
|
|
708
874
|
* `YYYY-MM-DD`, or a timestamp naming its offset — revives to the Date it
|
|
709
875
|
* names. Omitting the argument leaves the seam as it was.
|
|
710
876
|
*/
|
|
711
877
|
export function typed(
|
|
712
878
|
tokens: readonly Token[],
|
|
713
|
-
|
|
879
|
+
decl?: ResolvedFormat | FormatKind,
|
|
714
880
|
): number | boolean | Date | undefined;
|
|
715
881
|
|
|
716
882
|
/**
|
|
@@ -721,3 +887,25 @@ export function typed(
|
|
|
721
887
|
* one: the question is whose band a role names, not what a walk can hand over.
|
|
722
888
|
*/
|
|
723
889
|
export function isReportBand(role: string | undefined): boolean;
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Every name in the closed style vocabulary, in the order SCHEMA.md's table
|
|
893
|
+
* lists them. A tool that offers the vocabulary imports this rather than
|
|
894
|
+
* keeping a copy.
|
|
895
|
+
*/
|
|
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,7 +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 {
|
|
29
|
+
export { currencyOf, FORMAT_VOCABULARY, fractionDigits, STYLE_NAMES } from "./style.js";
|
|
30
|
+
export { imageError, isDiagnostic } from "./locate.js";
|
|
29
31
|
|
|
30
32
|
/** @typedef {import("./scope.js").Scope} Scope */
|
|
31
33
|
|
|
@@ -133,16 +135,19 @@ function events(schema, funcs, options, marking) {
|
|
|
133
135
|
/** @type {(event: any) => boolean} */
|
|
134
136
|
let occupying = (event) =>
|
|
135
137
|
event.type === "item" || event.type === "image" || event.type === "split-start";
|
|
136
|
-
/** @type {(n: any) => boolean} */
|
|
137
|
-
let nonzeroLead = (n) => Number.isFinite(n) && n !== 0;
|
|
138
|
-
/** @type {(event: any) => string} */
|
|
139
|
-
let eventPath = (event) => event.path || "header";
|
|
140
138
|
/** @type {(event: any) => void} */
|
|
141
139
|
let refuseLead = (event) => {
|
|
142
|
-
if (
|
|
143
|
-
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);
|
|
144
141
|
};
|
|
145
|
-
/**
|
|
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
|
+
*/
|
|
146
151
|
function* guardPin(events) {
|
|
147
152
|
let pending = true;
|
|
148
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.
|
package/lib/math.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scalar built-ins: quario's half of an expression's function scope that
|
|
3
|
+
* folds nothing. They live beside the reducers in SCHEMA.md ("Built-in
|
|
4
|
+
* functions") and apart from them here, because the reducers are also the
|
|
5
|
+
* declared-aggregate vocabulary and these can never be one -- `reducers.js`
|
|
6
|
+
* names no function in this file, so `"total": "round:=..."` stays a definition
|
|
7
|
+
* error.
|
|
8
|
+
*
|
|
9
|
+
* `round`, `floor` and `ceil` round the DECIMAL the author wrote, not the
|
|
10
|
+
* binary double it is stored as, and `Intl.NumberFormat` is how they reach it:
|
|
11
|
+
* its rounding starts from the shortest decimal that round-trips, so
|
|
12
|
+
* `round(2.675, 2)` is 2.68 where `Math.round(2.675 * 100) / 100` is 2.67 and
|
|
13
|
+
* `Number((2.675).toFixed(2))` is also 2.67. Those two disagree with each other
|
|
14
|
+
* as well as with us, on different inputs -- the whole argument, with the
|
|
15
|
+
* table, is in docs/adr/0050. The locale is hardcoded `en-US` rather than the
|
|
16
|
+
* instance's: this is arithmetic, not presentation, and a locale with
|
|
17
|
+
* non-ASCII digits or its own grouping would not parse back through `Number`.
|
|
18
|
+
* Formatters are built per call, deliberately -- see the ADR.
|
|
19
|
+
*/
|
|
20
|
+
/** @type {(x: number, n: number, mode: string) => number} */
|
|
21
|
+
let decimal = (x, n, mode) =>
|
|
22
|
+
Number(
|
|
23
|
+
new Intl.NumberFormat("en-US", {
|
|
24
|
+
minimumFractionDigits: n,
|
|
25
|
+
maximumFractionDigits: n,
|
|
26
|
+
useGrouping: false,
|
|
27
|
+
roundingMode: /** @type {any} */ (mode),
|
|
28
|
+
}).format(x),
|
|
29
|
+
);
|
|
30
|
+
// The one test both halves make of `x`. Named because the rule it stands for
|
|
31
|
+
// is the interesting part: rounding a value that is not a finite number would
|
|
32
|
+
// have to invent one, so it passes through instead. The reducers coerce
|
|
33
|
+
// quietly because a fold must produce a total; a scalar has no such
|
|
34
|
+
// obligation, and coercing here would turn a data error into a confident 0
|
|
35
|
+
// where passing through leaves the cell as display text -- a failure the
|
|
36
|
+
// author can see.
|
|
37
|
+
/** @type {(x: any) => boolean} */
|
|
38
|
+
let finite = (x) => typeof x === "number" && Number.isFinite(x);
|
|
39
|
+
|
|
40
|
+
// Split from `places` rather than inlined: the three tests plus the coercion
|
|
41
|
+
// and the throw put one function at cyclomatic 5, and the health gate's CRAP
|
|
42
|
+
// ceiling lands at 4 (docs/agents/commands.md).
|
|
43
|
+
/** @type {(digits: number) => boolean} */
|
|
44
|
+
let honourable = (digits) => Number.isFinite(digits) && digits >= 0 && digits <= 100;
|
|
45
|
+
// `n` is authored, so a bad one is a mistake worth failing on -- unlike `x`,
|
|
46
|
+
// which is data. Intl would throw its own RangeError naming
|
|
47
|
+
// `maximumFractionDigits`, an option the author never wrote, so the message is
|
|
48
|
+
// ours. Fractional `n` truncates rather than throwing, which is what Intl does
|
|
49
|
+
// with it and is not worth a second rule for an author to hold.
|
|
50
|
+
/** @type {(n: any) => number} */
|
|
51
|
+
let places = (n) => {
|
|
52
|
+
let digits = Math.trunc(+(n ?? 0));
|
|
53
|
+
if (!honourable(digits)) throw new RangeError("n must be a number from 0 to 100");
|
|
54
|
+
return digits;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** @type {(mode: string) => (x: any, n: any) => any} */
|
|
58
|
+
let rounder = (mode) => (x, n) => {
|
|
59
|
+
let digits = places(n);
|
|
60
|
+
return finite(x) ? decimal(x, digits, mode) : x;
|
|
61
|
+
};
|
|
62
|
+
/** @type {Record<string, (...args: any[]) => any>} */
|
|
63
|
+
export let MATH = {
|
|
64
|
+
// Written `(x, n)` rather than `(x, n = 0)` on purpose: xprsn's signatures()
|
|
65
|
+
// reports arity from `fn.length`, and a default parameter would describe
|
|
66
|
+
// these to an editor as taking one argument.
|
|
67
|
+
round: rounder("halfExpand"),
|
|
68
|
+
floor: rounder("floor"),
|
|
69
|
+
ceil: rounder("ceil"),
|
|
70
|
+
abs: (x) => (finite(x) ? Math.abs(x) : x),
|
|
71
|
+
};
|