quario 0.10.0 → 0.11.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 +21 -0
- package/README.md +18 -1
- package/lib/band-height.js +13 -19
- package/lib/format.js +31 -2
- package/lib/index.d.ts +102 -11
- package/lib/index.js +107 -7
- package/lib/license.js +1 -1
- package/lib/locate.js +10 -2
- package/lib/names.js +43 -0
- package/lib/plan.js +108 -25
- package/lib/precision.js +4 -4
- package/lib/stream.js +76 -4
- package/lib/style.js +157 -32
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# quario
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **`isBoxName(name)` is exported.** It answers whether a style name is part of the box — a padding or a border part — which is the partition the spec defines a table row's box by. A target that strips the box off a style, or a tool that lists the box names, imports it beside `STYLE_NAMES` rather than matching the two prefixes itself.
|
|
8
|
+
- **A number kind reads `negative` and `zero`.** `format` gains two modifiers on `number`, `currency` and `percent`: `negative` is `"minus"` or `"parens"`, and `zero` is `"number"` or `"dash"`. Under `parens` a negative reads as its magnitude in parentheses, and under `dash` a zero reads as `-`. Both defaults are what every kind presented before, so no existing report moves. They compose with `digits`, with the percent kind's scaling and with a currency's code. A value outside either pair is a located definition error, and `date` takes neither. The resolved declaration a cell carries now states all three of a number kind's modifiers, so no consumer defaults one for itself.
|
|
9
|
+
- **A group may declare `collapsed`.** One optional literal, `true` or `false`, asking that the instance open **shut**: its header and footer are shown and the content between them is not. Aggregates are untouched — the rows stay in the partition and still count, so a footer totals what nobody is shown. The `group-start` event carries it, and the new `collapse(stream)` helper is what a target with nowhere to hide a row reads the stream through: it drops a collapsed instance's content, nested instances whole, and keeps its brackets and its own bands. An expression is a located definition error.
|
|
10
|
+
- **A group may declare a `label`.** One optional template beside `name` and `by`, resolved once per instance in group scope — the instance's own handle and `$` are bound, `@` is unbound as in any group band — and carried on `group-start` as the resolved text. It names the instance for navigation rather than for the page: a target with somewhere to put a navigation name spends it there, and the others withdraw it, so no page changes. A value that is not a template string is a located definition error, and a hollow instance stays unlisted whether it declares one or not.
|
|
11
|
+
- **A run may carry an `href`.** The twelfth name in the styled run's inline subset, and the one declaration this engine judges rather than passes on: a URL names a location, and `javascript:` is a string-to-code path that escaping at the markup edge cannot answer. A URL is admitted before it crosses, against a host allowlist — `quario({ schemes })`, defaulting to `https:` and `mailto:` — so a target receives a URL already admitted or none at all. A refused value is omitted, so the text renders and the link does not; a URL stating no scheme is refused with the rest, because a scheme list cannot say "no scheme" without admitting `//host/path`. An `href` beginning `#` names a group's `label` and needs no allowlist. A `schemes` option the engine cannot read throws at construction rather than falling back to the default, because an allowlist quietly replaced is a host believing it restricted something it had not. A table row refuses `href`, as it refuses every declaration that presents a value.
|
|
12
|
+
- **`REPORT_STYLE_NAMES`.** The two names a report default may declare, `family` and `size`, in the vocabulary's own order — the same export `RUN_STYLE_NAMES` already is, so a tool that offers them reads the engine rather than keeping a copy.
|
|
13
|
+
- **A document may mark a declaration `required`, and `plan` checks it against the targets you name.** `"required": { "uppercase": true }` at the report root says which declarations this document cannot do without; `plan(schema, funcs, { targets })` takes the `capabilities` descriptors of the targets it is meant for, and every required declaration one of them withdraws or leaves unread becomes a problem of its own at `required.<name>`, which nulls the report as any other problem does. An approximated declaration is not one. Every withdrawal was silent by design before this, so this is how an author says that one of them matters. Additive throughout: no target sees the marking, no render changes because of it, and passing no descriptors checks nothing. A name outside the set the support matrix answers for is a definition error, because a marking nothing can check would go quiet exactly when it was needed. `REQUIRABLE` exports that set.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- **The declarations now state every `format` modifier the engine settles.** `FormatDeclaration` and `ResolvedFormat` carry `negative` and `zero` beside `digits` and `form`, and `FORMAT_VOCABULARY` offers the two readings and their defaults — `negatives`, `zeroes`, `defaultNegative`, `defaultZero` — with `modifiers` naming every modifier a kind takes rather than one of them. The engine has always read and resolved those declarations; the types said otherwise, so a TypeScript consumer writing one was told it was an error, and a tool reading the vocabulary to offer them found only the count.
|
|
18
|
+
- **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.
|
|
19
|
+
|
|
20
|
+
The viewer's and the editor's own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout's own account of the value behind it.
|
|
21
|
+
|
|
22
|
+
- **An all-authored split whose slot widths sum under 100 warns, as a table's columns already did.** A split's slots carry the same percentage shares under the same arithmetic, so the plan now reports the trailing width they leave unused, on the split's `slots` path. The report still renders; the warning is advisory, like every other.
|
|
23
|
+
|
|
3
24
|
## 0.10.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -193,6 +193,8 @@ cell the same way:
|
|
|
193
193
|
group's.
|
|
194
194
|
- `STYLE_NAMES` — the closed style vocabulary, in the spec's order, and
|
|
195
195
|
`RUN_STYLE_NAMES` — the inline half of it, which is what a styled run may wear.
|
|
196
|
+
- `isBoxName(name)` — whether a style name is part of the box, a padding or a border part. The
|
|
197
|
+
spec defines a row's box by this partition, and this function is its one home.
|
|
196
198
|
|
|
197
199
|
### `validate(schema, functions?)`
|
|
198
200
|
|
|
@@ -207,7 +209,7 @@ validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
|
|
|
207
209
|
`report()` throws on the first problem instead. Validation and compilation share one traversal,
|
|
208
210
|
so `validate()` can never disagree with what `report()` accepts.
|
|
209
211
|
|
|
210
|
-
### `quario().plan(schema, functions?)`
|
|
212
|
+
### `quario().plan(schema, functions?, { targets }?)`
|
|
211
213
|
|
|
212
214
|
The one traversal, whole — for hosts that validate and render in a loop, like an editor. Returns
|
|
213
215
|
`{ report, problems, anchors, warnings }`: the compiled report (`null` while the document has
|
|
@@ -226,6 +228,21 @@ a cell whose `format` is not `"currency"`, and a table where the author sized ev
|
|
|
226
228
|
total under 100 — and the list is advisory and deliberately incomplete, so a quiet one is not a
|
|
227
229
|
promise that every declaration will be read. `validate()` returns problems only.
|
|
228
230
|
|
|
231
|
+
**`targets` checks the document against where it is going.** A document may mark declarations it
|
|
232
|
+
cannot do without — `"required": { "uppercase": true }` at its root — and hand `plan` the
|
|
233
|
+
`capabilities` descriptors of the targets it is meant for. Each required declaration one of them
|
|
234
|
+
withdraws, or leaves unread as the page bands are, becomes a problem of its own at
|
|
235
|
+
`required.<name>`, which nulls the report as any other problem does. An approximated one does not:
|
|
236
|
+
the target rendered the intent as closely as it can. Pass no descriptors and nothing is checked,
|
|
237
|
+
which is what keeps the marking additive. It is never a render-time failure.
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
import { capabilities as csv } from "@quario/csv";
|
|
241
|
+
|
|
242
|
+
const { problems } = quario().plan(schema, undefined, { targets: [csv] });
|
|
243
|
+
// required.uppercase: the csv target withdraws "uppercase"
|
|
244
|
+
```
|
|
245
|
+
|
|
229
246
|
```js
|
|
230
247
|
const { report, problems, anchors, warnings } = quario().plan(schema);
|
|
231
248
|
for (const warning of warnings) console.warn(warning.message);
|
package/lib/band-height.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* lands in the documented key order beside every other problem.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import { positivePts } from "./style.js";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* A lead: authored flow spacing before an item that a target would actually
|
|
@@ -43,29 +43,20 @@ export let LEAD_REFUSED = "must be 0 after a height-declared header";
|
|
|
43
43
|
*/
|
|
44
44
|
export let leadPath = (path) => path + ".style.spaceBefore";
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* An occupancy the document leaves to the data: a `visible` written as an
|
|
48
|
-
* expression, or an image, whose bytes are expression-only and whose event is
|
|
49
|
-
* absent when its `source` yields nothing (SCHEMA.md, "Event stream"). Neither
|
|
50
|
-
* is a defect — they are how an author writes an optional item — but they mean
|
|
51
|
-
* the traversal cannot say which item comes first, so it says nothing rather
|
|
52
|
-
* than guessing (docs/adr/0052).
|
|
53
|
-
*
|
|
54
|
-
* @type {(def: any) => boolean}
|
|
55
|
-
*/
|
|
56
|
-
let dataDecides = (def) => isExpr(def?.visible) || def?.type === "image";
|
|
57
|
-
|
|
58
46
|
/**
|
|
59
47
|
* The first item of a band the document settles as occupying, `undefined` when
|
|
60
48
|
* only the render settles it, and null for a band settled to occupy nothing.
|
|
49
|
+
* Whether an item's occupancy is the data's to decide is `optional`'s answer,
|
|
50
|
+
* which the item compilers own: this file knows what a lead is and which node
|
|
51
|
+
* a fault names, not which item kinds can decline to render.
|
|
61
52
|
*
|
|
62
|
-
* @type {(list: any, path: string) => { def: any, path: string } | undefined | null}
|
|
53
|
+
* @type {(optional: (def: any) => boolean, list: any, path: string) => { def: any, path: string } | undefined | null}
|
|
63
54
|
*/
|
|
64
|
-
let firstOccupying = (list, path) => {
|
|
55
|
+
let firstOccupying = (optional, list, path) => {
|
|
65
56
|
if (!Array.isArray(list)) return null;
|
|
66
57
|
let i = list.findIndex((def) => def?.visible !== false);
|
|
67
58
|
if (i < 0) return null;
|
|
68
|
-
return
|
|
59
|
+
return optional(list[i]) ? undefined : { def: list[i], path: path + "[" + i + "]" };
|
|
69
60
|
};
|
|
70
61
|
|
|
71
62
|
/**
|
|
@@ -84,11 +75,14 @@ let firstOccupying = (list, path) => {
|
|
|
84
75
|
* a table, a row and a cell alike.
|
|
85
76
|
*
|
|
86
77
|
* @param {(path: string, message: string) => void} bad The traversal's collector.
|
|
78
|
+
* @param {(def: any) => boolean} optional Whether an item definition may
|
|
79
|
+
* decline to occupy at render — the item compilers' verdict, so a new item
|
|
80
|
+
* kind that can states it where it is compiled rather than here.
|
|
87
81
|
* @returns {{ arm: (height: number | null) => void,
|
|
88
82
|
* instead: (list: any, path: string) => void,
|
|
89
83
|
* body: (list: any, path: string) => void }} The guard.
|
|
90
84
|
*/
|
|
91
|
-
export let pinGuard = (bad) => {
|
|
85
|
+
export let pinGuard = (bad, optional) => {
|
|
92
86
|
let pinned = false;
|
|
93
87
|
let settled = true;
|
|
94
88
|
/** @type {(found: { def: any, path: string } | undefined | null) => void} */
|
|
@@ -101,11 +95,11 @@ export let pinGuard = (bad) => {
|
|
|
101
95
|
settled = !pinned;
|
|
102
96
|
},
|
|
103
97
|
instead: (/** @type {any} */ list, /** @type {string} */ path) => {
|
|
104
|
-
if (pinned) refuse(firstOccupying(list, path));
|
|
98
|
+
if (pinned) refuse(firstOccupying(optional, list, path));
|
|
105
99
|
},
|
|
106
100
|
body: (/** @type {any} */ list, /** @type {string} */ path) => {
|
|
107
101
|
if (settled) return;
|
|
108
|
-
let found = firstOccupying(list, path);
|
|
102
|
+
let found = firstOccupying(optional, list, path);
|
|
109
103
|
// A band settled to occupy nothing is not the one under the box, so the
|
|
110
104
|
// descent keeps looking; one only the render settles ends the search
|
|
111
105
|
// without a verdict, because whether the band below is next is exactly
|
package/lib/format.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* rendered (docs/adr/0054, docs/adr/0056).
|
|
17
17
|
*/
|
|
18
18
|
import { boundedMemo } from "./memo.js";
|
|
19
|
-
import { currencyOf, formatOf } from "./style.js";
|
|
19
|
+
import { ZERO_DASH, currencyOf, formatOf } from "./style.js";
|
|
20
20
|
import { finiteDate, finiteNum, reviveDate } from "./stream.js";
|
|
21
21
|
/** @type {(options: any) => string} */
|
|
22
22
|
let localeOf = (options) => options?.locale || "en-US";
|
|
@@ -114,6 +114,34 @@ let asDate = (value, locale, decl, options) => {
|
|
|
114
114
|
/** @type {Record<string, (value: any, locale: string, decl: any, options: any, style: any) => string | undefined>} */
|
|
115
115
|
let KINDS = { number: asNumber, currency: asMoney, percent: asPercent, date: asDate };
|
|
116
116
|
|
|
117
|
+
// The two presentation modifiers a number kind carries, applied around
|
|
118
|
+
// whatever its own formatter produced. A `date` carries neither, and answers
|
|
119
|
+
// `undefined` to both reads below.
|
|
120
|
+
//
|
|
121
|
+
// Parentheses are spelled here rather than taken from Intl's accounting sign:
|
|
122
|
+
// the worksheet writes the same negative section into its number format, and
|
|
123
|
+
// the two edges have to agree on one wording (docs/adr/0056, as amended).
|
|
124
|
+
// Which is also why the number is formatted from its own magnitude — the sign
|
|
125
|
+
// a locale would have written is exactly what the parentheses replace.
|
|
126
|
+
/** @type {(value: any, decl: any) => any} */
|
|
127
|
+
let magnitude = (value, decl) =>
|
|
128
|
+
decl.negative === "parens" && finiteNum(value) && value < 0 ? -value : value;
|
|
129
|
+
// What each sign reads as, given the modifiers its kind carries. A table
|
|
130
|
+
// rather than a chain, so the three cases are three entries and each one is
|
|
131
|
+
// the whole answer for its sign.
|
|
132
|
+
/** @type {Record<string, (text: string, decl: any) => string>} */
|
|
133
|
+
let SIGNED = {
|
|
134
|
+
zero: (text, decl) => (decl.zero === "dash" ? ZERO_DASH : text),
|
|
135
|
+
negative: (text, decl) => (decl.negative === "parens" ? "(" + text + ")" : text),
|
|
136
|
+
positive: (text) => text,
|
|
137
|
+
};
|
|
138
|
+
// `value === 0` is true of a negative zero, which is a zero.
|
|
139
|
+
/** @type {(value: number) => string} */
|
|
140
|
+
let signOf = (value) => (value === 0 ? "zero" : value < 0 ? "negative" : "positive");
|
|
141
|
+
/** @type {(value: any, text: string | undefined, decl: any) => string | undefined} */
|
|
142
|
+
let presented = (value, text, decl) =>
|
|
143
|
+
text === undefined || !finiteNum(value) ? text : SIGNED[signOf(value)](text, decl);
|
|
144
|
+
|
|
117
145
|
/**
|
|
118
146
|
* The helper takes the whole resolved `style`, not just the declaration, so
|
|
119
147
|
* the question "which declarations does presentation read?" is answered here
|
|
@@ -137,7 +165,8 @@ export function format(value, style, options) {
|
|
|
137
165
|
let decl = formatOf(style, options);
|
|
138
166
|
if (!decl) return;
|
|
139
167
|
try {
|
|
140
|
-
|
|
168
|
+
let text = KINDS[decl.kind](magnitude(value, decl), localeOf(options), decl, options, style);
|
|
169
|
+
return presented(value, text, decl);
|
|
141
170
|
} catch {
|
|
142
171
|
return;
|
|
143
172
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -125,6 +125,28 @@ export interface SortKey {
|
|
|
125
125
|
*/
|
|
126
126
|
export const RUN_STYLE_NAMES: readonly string[];
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The names a [report default](../../../SCHEMA.md#style-declarations) may
|
|
130
|
+
* declare, in the vocabulary's own order. A tool that offers them — the
|
|
131
|
+
* editor's Document panel — reads this rather than keeping a copy.
|
|
132
|
+
*/
|
|
133
|
+
export const REPORT_STYLE_NAMES: readonly string[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What a cell reads as under `format`'s `zero: "dash"`. Stated once, because
|
|
137
|
+
* the XLSX target writes the same character into its sectioned number format
|
|
138
|
+
* and the two edges must agree.
|
|
139
|
+
*/
|
|
140
|
+
export const ZERO_DASH: string;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The declarations a document may mark [`required`](../../../SCHEMA.md#validation),
|
|
144
|
+
* in the order the support matrix states them. Every published target's
|
|
145
|
+
* `capabilities` descriptor answers for exactly these, which is what makes a
|
|
146
|
+
* marking checkable.
|
|
147
|
+
*/
|
|
148
|
+
export const REQUIRABLE: readonly string[];
|
|
149
|
+
|
|
128
150
|
/**
|
|
129
151
|
* The declarations a styled run accepts: the inline half of the vocabulary,
|
|
130
152
|
* and nothing else. The box and flow spacing describe a block and `align`
|
|
@@ -173,26 +195,42 @@ export type FormatKind = "number" | "currency" | "percent" | "date";
|
|
|
173
195
|
* none is `"medium"`. */
|
|
174
196
|
export type DateForm = "short" | "medium" | "long" | "full";
|
|
175
197
|
|
|
198
|
+
/** How a negative amount reads. A number kind that names none is `"minus"`. */
|
|
199
|
+
export type NegativeReading = "minus" | "parens";
|
|
200
|
+
|
|
201
|
+
/** How a zero reads. A number kind that names none is `"number"`. */
|
|
202
|
+
export type ZeroReading = "number" | "dash";
|
|
203
|
+
|
|
176
204
|
/**
|
|
177
|
-
* How an author writes `format`: the kind alone, or the kind carrying
|
|
178
|
-
*
|
|
179
|
-
* presenting target and a worksheet number format can honour — and it beats
|
|
205
|
+
* How an author writes `format`: the kind alone, or the kind carrying the
|
|
206
|
+
* modifiers it takes. `digits` is a whole number from 0 to 20 — the range both
|
|
207
|
+
* a presenting target and a worksheet number format can honour — and it beats
|
|
180
208
|
* the count the kind would otherwise present, a currency's minor units
|
|
181
209
|
* included. A modifier on a kind that does not take it is a definition error.
|
|
182
210
|
*/
|
|
183
211
|
export type FormatDeclaration =
|
|
184
212
|
| FormatKind
|
|
185
|
-
| {
|
|
213
|
+
| {
|
|
214
|
+
kind: "number" | "currency" | "percent";
|
|
215
|
+
digits?: number;
|
|
216
|
+
negative?: NegativeReading;
|
|
217
|
+
zero?: ZeroReading;
|
|
218
|
+
}
|
|
186
219
|
| { kind: "date"; form?: DateForm };
|
|
187
220
|
|
|
188
221
|
/**
|
|
189
222
|
* What `format` resolves to on the event stream: never the shorthand, always
|
|
190
223
|
* the kind with the answer it presents. A number kind carries `digits` unless
|
|
191
|
-
* it has no count at all — a `currency` whose code cannot be read — and
|
|
192
|
-
* `date` always carries its `form`.
|
|
224
|
+
* it has no count at all — a `currency` whose code cannot be read — and both
|
|
225
|
+
* of its readings, and a `date` always carries its `form`.
|
|
193
226
|
*/
|
|
194
227
|
export type ResolvedFormat =
|
|
195
|
-
| {
|
|
228
|
+
| {
|
|
229
|
+
kind: "number" | "currency" | "percent";
|
|
230
|
+
digits?: number;
|
|
231
|
+
negative: NegativeReading;
|
|
232
|
+
zero: ZeroReading;
|
|
233
|
+
}
|
|
196
234
|
| { kind: "date"; form: DateForm };
|
|
197
235
|
|
|
198
236
|
export interface Cell {
|
|
@@ -696,6 +734,10 @@ export interface GroupStartEvent {
|
|
|
696
734
|
depth: number;
|
|
697
735
|
key: unknown;
|
|
698
736
|
aggregates: Record<string, unknown>;
|
|
737
|
+
/** The instance's navigation name, resolved from the group's `label` and
|
|
738
|
+
* joined to text. Absent where the group declares none, so a target with a
|
|
739
|
+
* name it derives keeps deriving it. */
|
|
740
|
+
label?: string;
|
|
699
741
|
/** A page turns before this instance, resolved from the group's `break`. */
|
|
700
742
|
break?: "page";
|
|
701
743
|
/** A page turns after this instance, resolved from the group's `break`. */
|
|
@@ -704,6 +746,9 @@ export interface GroupStartEvent {
|
|
|
704
746
|
reset?: "page";
|
|
705
747
|
/** The declared page column count, present when the group declares one. */
|
|
706
748
|
columns?: number;
|
|
749
|
+
/** The instance opens shut, present only when the group declares it: its
|
|
750
|
+
* bands are shown and the content between them is not. */
|
|
751
|
+
collapsed?: true;
|
|
707
752
|
}
|
|
708
753
|
|
|
709
754
|
export interface GroupEndEvent {
|
|
@@ -801,6 +846,18 @@ export type WalkEvent = ReportEvent | SplitEvent;
|
|
|
801
846
|
*/
|
|
802
847
|
export function splits(events: Iterable<ReportEvent>): Generator<WalkEvent, void>;
|
|
803
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Each [collapsed](../../../SCHEMA.md#group-definition) instance shut: its
|
|
851
|
+
* brackets and its own header and footer items survive, and everything
|
|
852
|
+
* between them is dropped, nested instances whole.
|
|
853
|
+
*
|
|
854
|
+
* For a target with no way to hide a row and keep it. One that has — the
|
|
855
|
+
* worksheet — reads the stream as it is. Aggregates are untouched either
|
|
856
|
+
* way. Read it over `splits(stream)`, so a split inside a header arrives
|
|
857
|
+
* folded and wearing the role this reads.
|
|
858
|
+
*/
|
|
859
|
+
export function collapse(events: Iterable<WalkEvent>): Generator<WalkEvent, void>;
|
|
860
|
+
|
|
804
861
|
/**
|
|
805
862
|
* The walk driver: dispatch one render's event stream to per-event handlers,
|
|
806
863
|
* in stream order and exactly once each, pulling lazily and handing the loop
|
|
@@ -897,6 +954,27 @@ export interface Warning {
|
|
|
897
954
|
* complement of `names`, which excludes them. `problems` is fatal and
|
|
898
955
|
* `warnings` is advisory, both in document order from the one descent.
|
|
899
956
|
*/
|
|
957
|
+
/** A target's `capabilities` descriptor, as each published target exports it. */
|
|
958
|
+
export interface TargetCapabilities {
|
|
959
|
+
readonly target: string;
|
|
960
|
+
readonly declarations: Readonly<Record<string, string>>;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** What a caller asks `plan` to check beyond the document itself. */
|
|
964
|
+
export interface PlanOptions {
|
|
965
|
+
/**
|
|
966
|
+
* The `capabilities` descriptors of the targets this document is meant for.
|
|
967
|
+
* Every declaration the document marks `required` that one of them
|
|
968
|
+
* withdraws or leaves unread becomes a problem of its own, which nulls the
|
|
969
|
+
* report exactly as any other problem does. Omit them and nothing is
|
|
970
|
+
* checked, which is what makes the marking additive.
|
|
971
|
+
*
|
|
972
|
+
* Descriptors rather than targets: the engine knows about no target, and
|
|
973
|
+
* what it reads here is the data a target ships about itself.
|
|
974
|
+
*/
|
|
975
|
+
readonly targets?: readonly TargetCapabilities[];
|
|
976
|
+
}
|
|
977
|
+
|
|
900
978
|
export interface Plan {
|
|
901
979
|
readonly report: CompiledReport | null;
|
|
902
980
|
readonly problems: readonly Problem[];
|
|
@@ -917,7 +995,7 @@ export interface Quario {
|
|
|
917
995
|
* problems, advisory warnings, and per-node anchor sets — one descent per
|
|
918
996
|
* edit for a host that validates and renders in a loop.
|
|
919
997
|
*/
|
|
920
|
-
plan(schema: unknown, functions?: FunctionRegistry): Plan;
|
|
998
|
+
plan(schema: unknown, functions?: FunctionRegistry, options?: PlanOptions): Plan;
|
|
921
999
|
}
|
|
922
1000
|
|
|
923
1001
|
/**
|
|
@@ -1069,6 +1147,14 @@ export function isReportBand(role: string | undefined): boolean;
|
|
|
1069
1147
|
*/
|
|
1070
1148
|
export const STYLE_NAMES: readonly string[];
|
|
1071
1149
|
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether a style name belongs to the box: a padding or a border part. The
|
|
1152
|
+
* partition is the spec's (a row's box is defined by it), and this is its one
|
|
1153
|
+
* home. A target that strips the box off a style, or a tool that lists the box
|
|
1154
|
+
* names, asks this rather than matching the prefixes itself.
|
|
1155
|
+
*/
|
|
1156
|
+
export function isBoxName(name: string): boolean;
|
|
1157
|
+
|
|
1072
1158
|
/**
|
|
1073
1159
|
* The `format` vocabulary as data, for a tool that offers it rather than
|
|
1074
1160
|
* checks it — a style rail's kind picker and its modifier control — so no
|
|
@@ -1077,9 +1163,14 @@ export const STYLE_NAMES: readonly string[];
|
|
|
1077
1163
|
export const FORMAT_VOCABULARY: {
|
|
1078
1164
|
readonly kinds: readonly FormatKind[];
|
|
1079
1165
|
readonly forms: readonly DateForm[];
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1166
|
+
readonly negatives: readonly NegativeReading[];
|
|
1167
|
+
readonly zeroes: readonly ZeroReading[];
|
|
1168
|
+
/** Which modifiers each kind takes, in the order a surface offers them, so
|
|
1169
|
+
* one offering them need not know that three kinds share theirs and `date`
|
|
1170
|
+
* does not. */
|
|
1171
|
+
readonly modifiers: Readonly<Record<FormatKind, readonly string[]>>;
|
|
1083
1172
|
readonly defaultForm: DateForm;
|
|
1173
|
+
readonly defaultNegative: NegativeReading;
|
|
1174
|
+
readonly defaultZero: ZeroReading;
|
|
1084
1175
|
readonly maxDigits: number;
|
|
1085
1176
|
};
|
package/lib/index.js
CHANGED
|
@@ -24,15 +24,29 @@ import { opt } from "./stream.js";
|
|
|
24
24
|
// The event stream's public seam, single-sourced in ./stream.js, and the
|
|
25
25
|
// diagnostic predicate in ./locate.js. Re-exported here because a consumer
|
|
26
26
|
// imports them from the package, not from a file inside it.
|
|
27
|
-
export {
|
|
27
|
+
export {
|
|
28
|
+
breathe,
|
|
29
|
+
collapse,
|
|
30
|
+
display,
|
|
31
|
+
isReportBand,
|
|
32
|
+
splits,
|
|
33
|
+
styledRuns,
|
|
34
|
+
text,
|
|
35
|
+
typed,
|
|
36
|
+
walk,
|
|
37
|
+
} from "./stream.js";
|
|
28
38
|
export { format } from "./format.js";
|
|
29
39
|
export {
|
|
30
40
|
currencyOf,
|
|
31
41
|
FORMAT_VOCABULARY,
|
|
32
42
|
fractionDigits,
|
|
43
|
+
isBoxName,
|
|
44
|
+
REPORT_STYLE_NAMES,
|
|
45
|
+
ZERO_DASH,
|
|
33
46
|
RUN_STYLE_NAMES,
|
|
34
47
|
STYLE_NAMES,
|
|
35
48
|
} from "./style.js";
|
|
49
|
+
export { REQUIRABLE } from "./names.js";
|
|
36
50
|
export { imageError, isDiagnostic } from "./locate.js";
|
|
37
51
|
export { hostMeta, hostOptions } from "./host.js";
|
|
38
52
|
|
|
@@ -104,6 +118,63 @@ export function validate(schema, funcs) {
|
|
|
104
118
|
/** @type {(entries: any[]) => readonly any[]} */
|
|
105
119
|
let freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
106
120
|
|
|
121
|
+
// The fates that lose the declaration, and how each one reads in a message.
|
|
122
|
+
// `approximated` is not among them: the target rendered the intent as closely
|
|
123
|
+
// as it can, which is what the author asked for (docs/adr/0008). An own-key
|
|
124
|
+
// lookup, because a fate reaches this from a caller's own descriptor.
|
|
125
|
+
/** @type {Record<string, (name: string) => string>} */
|
|
126
|
+
let LOST = Object.assign(Object.create(null), {
|
|
127
|
+
withdrawn: (/** @type {string} */ name) => 'withdraws "' + name + '"',
|
|
128
|
+
unread: (/** @type {string} */ name) => 'leaves "' + name + '" unread',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// What a target makes of one name, from the descriptor it ships. An `unknown`
|
|
132
|
+
// descriptor answers nothing rather than guessing: a caller may hold one from
|
|
133
|
+
// a package older than the name.
|
|
134
|
+
/** @type {(target: any, name: string) => string | undefined} */
|
|
135
|
+
let fateOf = (target, name) => {
|
|
136
|
+
let declarations = target?.declarations;
|
|
137
|
+
return record(declarations) && Object.hasOwn(declarations, name) ? declarations[name] : undefined;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// A target's own name, for the message. A descriptor that names none is still
|
|
141
|
+
// readable, so this never throws: `plan` reports on a document, and a caller's
|
|
142
|
+
// malformed descriptor is not the document's fault.
|
|
143
|
+
/** @type {(target: any) => string} */
|
|
144
|
+
let calling = (target) => (typeof target?.target === "string" ? target.target : "a target");
|
|
145
|
+
|
|
146
|
+
// Every required declaration a passed target loses, one problem each, in the
|
|
147
|
+
// document's own order and then the caller's. Plan-time only: this never
|
|
148
|
+
// reaches a render, which is what keeps whose-fault-is-a-render out of it
|
|
149
|
+
// (docs/adr/0057).
|
|
150
|
+
// The descriptors a caller named, or none — which is what makes the whole
|
|
151
|
+
// marking additive: nothing is checked against a target nobody named.
|
|
152
|
+
/** @type {readonly any[]} */
|
|
153
|
+
let NO_TARGETS = Object.freeze([]);
|
|
154
|
+
/** @type {(opts: any) => readonly any[]} */
|
|
155
|
+
let named = (opts) => (Array.isArray(opts?.targets) ? opts.targets : NO_TARGETS);
|
|
156
|
+
|
|
157
|
+
// What a caller asked to check beyond the document: the required declarations
|
|
158
|
+
// its targets lose.
|
|
159
|
+
/** @type {(planned: any, opts: any) => any[]} */
|
|
160
|
+
let checked = (planned, opts) => unmet(planned.compiled?.required ?? NO_TARGETS, named(opts));
|
|
161
|
+
|
|
162
|
+
// What one target loses of one name, or null where it keeps it.
|
|
163
|
+
/** @type {(target: any, name: string) => any} */
|
|
164
|
+
let loss = (target, name) => {
|
|
165
|
+
let fate = fateOf(target, name);
|
|
166
|
+
let said = fate === undefined ? undefined : LOST[fate];
|
|
167
|
+
if (!said) return null;
|
|
168
|
+
return {
|
|
169
|
+
path: "required." + name,
|
|
170
|
+
message: "required." + name + ": the " + calling(target) + " target " + said(name),
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** @type {(required: readonly string[], targets: readonly any[]) => any[]} */
|
|
175
|
+
let unmet = (required, targets) =>
|
|
176
|
+
required.flatMap((name) => targets.map((target) => loss(target, name)).filter(Boolean));
|
|
177
|
+
|
|
107
178
|
// The per-node anchor sets, as frozen host data (CONTEXT.md, "Freeze"): which
|
|
108
179
|
// anchors and group handles each compiled source reads, keyed by schema path.
|
|
109
180
|
/** @type {(anchors: Map<string, Set<string>>) => any} */
|
|
@@ -286,6 +357,21 @@ function assemble(planned, schema, marking, options) {
|
|
|
286
357
|
|
|
287
358
|
/** @typedef {import("./license.js").LicenseInfo} LicenseInfo */
|
|
288
359
|
|
|
360
|
+
// The `href` allowlist, checked once at construction rather than per cell.
|
|
361
|
+
// It **throws** where every other host option is lenient: an allowlist is a
|
|
362
|
+
// security control, and one the engine quietly replaced with its own default
|
|
363
|
+
// would be a host believing it had restricted something it had not
|
|
364
|
+
// (docs/adr/0085).
|
|
365
|
+
/** @type {(schemes: unknown) => void} */
|
|
366
|
+
let checkSchemes = (schemes) => {
|
|
367
|
+
if (schemes === undefined) return;
|
|
368
|
+
let listed =
|
|
369
|
+
Array.isArray(schemes) &&
|
|
370
|
+
schemes.every((scheme) => typeof scheme === "string" && /^[a-z][a-z0-9+.-]*:$/.test(scheme));
|
|
371
|
+
if (!listed)
|
|
372
|
+
throw Error("schemes: expected an array of lower-case URL schemes, each ending in a colon");
|
|
373
|
+
};
|
|
374
|
+
|
|
289
375
|
/**
|
|
290
376
|
* Create a configured quario instance: the host-level options — license key,
|
|
291
377
|
* query budgets. The key is verified once, here, and `license` settles with
|
|
@@ -295,14 +381,17 @@ function assemble(planned, schema, marking, options) {
|
|
|
295
381
|
*
|
|
296
382
|
* @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number},
|
|
297
383
|
* license?: string, trust?: {publicKey?: string, release?: string},
|
|
298
|
-
* locale?: string, currency?: string, timeZone?: string
|
|
384
|
+
* locale?: string, currency?: string, timeZone?: string,
|
|
385
|
+
* schemes?: readonly string[]}} [options]
|
|
299
386
|
* Host configuration. `trust` is internal, not API — see ./license.js.
|
|
300
|
-
* `locale` / `currency` / `timeZone` present `format` (docs/adr/0041)
|
|
387
|
+
* `locale` / `currency` / `timeZone` present `format` (docs/adr/0041), and
|
|
388
|
+
* `schemes` is the `href` allowlist (docs/adr/0085).
|
|
301
389
|
* @returns {{license: Promise<LicenseInfo>,
|
|
302
390
|
* report: (schema: any, funcs?: Record<string, Function>) => any,
|
|
303
391
|
* plan: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
|
|
304
392
|
*/
|
|
305
393
|
export function quario(options) {
|
|
394
|
+
checkSchemes(options?.schemes);
|
|
306
395
|
let { licensed, settled } = verify(options?.license, options?.trust);
|
|
307
396
|
// The wording is the instance's, where the key is; the stream is what
|
|
308
397
|
// states it, so nothing wraps a compiled stream to reach its first event.
|
|
@@ -337,12 +426,23 @@ export function quario(options) {
|
|
|
337
426
|
// hands over the whole thing — the compiled report (null while the
|
|
338
427
|
// document has problems), every problem structurally, and the per-node
|
|
339
428
|
// anchor sets — so an editing host pays one traversal per edit, not two.
|
|
340
|
-
|
|
341
|
-
|
|
429
|
+
//
|
|
430
|
+
// `targets` is the opt-in half: hand it the `capabilities` descriptors of
|
|
431
|
+
// the targets this document is meant for, and every declaration it marks
|
|
432
|
+
// `required` that one of them withdraws or leaves unread becomes a
|
|
433
|
+
// problem of its own. Descriptors rather than targets, because the engine
|
|
434
|
+
// knows about no target (docs/adr/0014) — what it reads is the data a
|
|
435
|
+
// target ships about itself. Without them nothing checks, which is what
|
|
436
|
+
// makes the whole marking additive.
|
|
437
|
+
/** @type {(schema: any, funcs?: Record<string, Function>, opts?: any) => any} */
|
|
438
|
+
plan(schema, funcs, opts) {
|
|
342
439
|
let planned = plan(schema, funcs, options);
|
|
440
|
+
// After the document's own, so a problem list still opens with what is
|
|
441
|
+
// wrong with the report rather than with where it is going.
|
|
442
|
+
let problems = planned.problems.concat(checked(planned, opts));
|
|
343
443
|
return {
|
|
344
|
-
report:
|
|
345
|
-
problems: freezeEntries(
|
|
444
|
+
report: problems.length ? null : wrap(assemble(planned, schema, marking, options)),
|
|
445
|
+
problems: freezeEntries(problems),
|
|
346
446
|
anchors: freezeAnchors(planned.anchors),
|
|
347
447
|
warnings: freezeEntries(planned.warnings),
|
|
348
448
|
};
|
package/lib/license.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// though nothing 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-21"; // release-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
|
@@ -83,15 +83,23 @@ export let imageError = (path, said, cause) => {
|
|
|
83
83
|
* @type {(path: string, src: any, error: any, relocate?: false | ((e: any, o: { prefix: string, offset: number }) => any), offset?: number) => never}
|
|
84
84
|
*/
|
|
85
85
|
export let locate = (path, src, error, relocate, offset = 0) => {
|
|
86
|
-
|
|
86
|
+
// Every caller has the author source in hand -- a query, a template, an
|
|
87
|
+
// expression, an image's own `source` -- so the message always names it.
|
|
88
|
+
let prefix = path + " [" + String(src) + "]: ";
|
|
87
89
|
let where = { [LOCATION]: { path, source: src } };
|
|
88
90
|
if (relocate) throw Object.assign(relocate(error, { prefix, offset }), where);
|
|
89
91
|
// A host throw keeps its own class — the located error is the same kind of
|
|
90
|
-
// failure, named where it happened — and gains no diagnostic metadata.
|
|
92
|
+
// failure, named where it happened — and gains no diagnostic metadata. The
|
|
93
|
+
// original rides along as `cause`: a replacement is a claim about what went
|
|
94
|
+
// wrong, and the only thing that can contradict it is the error it replaced
|
|
95
|
+
// (docs/adr/0084). Assigned rather than passed to the constructor, for the
|
|
96
|
+
// reason `imageError` states above — a foreign class need not accept an
|
|
97
|
+
// options bag.
|
|
91
98
|
throw Object.assign(
|
|
92
99
|
error instanceof Error
|
|
93
100
|
? new /** @type {any} */ (error.constructor)(prefix + error.message)
|
|
94
101
|
: Error(prefix + String(error)),
|
|
95
102
|
where,
|
|
103
|
+
{ cause: error },
|
|
96
104
|
);
|
|
97
105
|
};
|
package/lib/names.js
CHANGED
|
@@ -17,3 +17,46 @@ export let ANCHORS = ["@", "$", "run", "loop", "page"];
|
|
|
17
17
|
// aggregate and run names stay the author's, `count` and `index` included.
|
|
18
18
|
export let RESERVED = new Set([...ANCHORS, "report", "table", "data", "meta"]);
|
|
19
19
|
export let BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The declarations a document may mark [`required`](../../../SCHEMA.md#validation),
|
|
23
|
+
* in the order the support matrix states them.
|
|
24
|
+
*
|
|
25
|
+
* These are the names every official target answers for, which is what makes
|
|
26
|
+
* a marking checkable: `plan` reads a passed target's `capabilities`
|
|
27
|
+
* descriptor, and a name no descriptor carries could never be reported on.
|
|
28
|
+
* The descriptors are keyed by exactly this set, and the repository's
|
|
29
|
+
* `test/capabilities.test.js` holds the three in agreement — this list, the
|
|
30
|
+
* five descriptors, and the matrix all say the same names.
|
|
31
|
+
*
|
|
32
|
+
* Two of them are qualified: `width` names a column's share and a slot's, the
|
|
33
|
+
* targets do not answer alike for the two, and one key could only answer for
|
|
34
|
+
* one of them.
|
|
35
|
+
*/
|
|
36
|
+
export let REQUIRABLE = Object.freeze([
|
|
37
|
+
"page.header",
|
|
38
|
+
"page.footer",
|
|
39
|
+
"break",
|
|
40
|
+
"reset",
|
|
41
|
+
"page.margin",
|
|
42
|
+
"header.height",
|
|
43
|
+
"groups",
|
|
44
|
+
"label",
|
|
45
|
+
"collapsed",
|
|
46
|
+
"column.width",
|
|
47
|
+
"slot.width",
|
|
48
|
+
"span",
|
|
49
|
+
"uppercase",
|
|
50
|
+
"href",
|
|
51
|
+
"spaceBefore",
|
|
52
|
+
"spaceAfter",
|
|
53
|
+
"format",
|
|
54
|
+
"family",
|
|
55
|
+
"valign",
|
|
56
|
+
"runs",
|
|
57
|
+
"alt",
|
|
58
|
+
]);
|
|
59
|
+
let REQUIRABLE_SET = new Set(REQUIRABLE);
|
|
60
|
+
/** Whether a name is one a document may mark required. */
|
|
61
|
+
/** @type {(name: string) => boolean} */
|
|
62
|
+
export let isRequirable = (name) => REQUIRABLE_SET.has(name);
|