quario 0.8.0 → 0.9.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 CHANGED
@@ -1,5 +1,49 @@
1
1
  # quario
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Every target factory now refuses an option it does not understand.** An unknown key, a key with
8
+ a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
9
+ factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
10
+ already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
11
+ the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
12
+ signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
13
+ one.
14
+
15
+ The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
16
+ `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
17
+ fourth document property is one edit rather than three.
18
+
19
+ **What this changes for you.** One options object spread across several targets stops working if
20
+ any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
21
+ `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
22
+ share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
23
+ TypeScript does not warn about this: excess-property checking fires on an object literal and not
24
+ on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
25
+
26
+ Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
27
+ `{ meta: { title: config.title } }` over a config that carries neither are both fine.
28
+ `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
29
+ instead of turning path stamping on. A `fonts` mapping given as an array is refused by
30
+ `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
31
+
32
+ Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
33
+ `instanceof Error` is unaffected. One that compares the constructor is not.
34
+
35
+ `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
36
+ and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
37
+ is a property you did not write rather than one of the wrong type, and an inherited enumerable key
38
+ is no longer reported as an option you wrote.
39
+
40
+ `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
41
+ object you passed no longer changes what a configured target writes.
42
+
43
+ ### Patch Changes
44
+
45
+ - The engine verifies a license key against a new signing key.
46
+
3
47
  ## 0.8.0
4
48
 
5
49
  ### Minor Changes
package/README.md CHANGED
@@ -63,7 +63,7 @@ Compiles a report and returns the compiled report. `stream(data)` is the raw eve
63
63
  (e.g. `html()` from `@quario/html`) or your own.
64
64
 
65
65
  A render is async: quario awaits key verification before the target sees its first event. A
66
- malformed target throws synchronously from `render`, before the first event; definition problems
66
+ malformed target throws synchronously from `render`, before the first event. Definition problems
67
67
  throw earlier, at `report()`.
68
68
 
69
69
  The data pre-pass (select, filter, sort, aggregate) runs when you call the renderer, because a
@@ -80,7 +80,7 @@ report.paths; // padvinder's deeply frozen dependency topology for the `data` qu
80
80
 
81
81
  Events arrive in render order: `report-start`, report `header` items, then either the `empty`
82
82
  items or the group/detail walk, then `footer` items, `report-end`. Group instances bracket their
83
- content with `group-start`/`group-end`; a table detail yields `table-start`, one `row` per visible
83
+ content with `group-start`/`group-end`. A table detail yields `table-start`, one `row` per visible
84
84
  row, one `total-row` per emitted total row, and `table-end`.
85
85
 
86
86
  | Event | Carries |
@@ -88,7 +88,7 @@ row, one `total-row` per emitted total row, and `table-end`.
88
88
  | `report-start` | `params`, resolved report `aggregates`, optional `page` band closures, `columns`, `style` (the report default), `marking`, `margin`, `headerHeight`, and the instance's `locale` / `currency` / `timeZone` when set |
89
89
  | `item` | `role`, `path`, `tokens`, optional `style`, `run` |
90
90
  | `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
91
- | `split-start` | `role`, `slots` (each an optional `width`), optional `style`; one `item` or `image` per slot follows, then `split-end` |
91
+ | `split-start` | `role`, `slots` (each an optional `width`), optional `style`. One `item` or `image` per slot follows, then `split-end` |
92
92
  | `split-end` | - |
93
93
  | `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `reset`, `columns` |
94
94
  | `group-end` | `name`, `depth` |
@@ -108,18 +108,18 @@ a literal and a value, so the cell is text only.
108
108
 
109
109
  **A page-number token names itself.** A value token whose interpolation is exactly
110
110
  `{{ page.number }}` or `{{ page.total }}` also carries `field`, holding that same string. The
111
- value is the number this page was rendered with; `field` says which page value the token stands
111
+ value is the number the render used for this page. `field` says which page value the token stands
112
112
  for, so a target whose own document format numbers pages can write its own live field there
113
113
  instead. Anything computed from them carries no `field`.
114
114
 
115
115
  **Escaping is the consumer's job.** A target that embeds values in markup must escape them at its
116
116
  own edge.
117
117
 
118
- **`report-start.marking`** carries the evaluation wording when the render is unlicensed, or while
119
- verification is still settling. Licensed streams omit it. Targets place the marking; they do not
118
+ **`report-start.marking`** carries the evaluation wording when no license covers the render, or while
119
+ verification is still settling. Licensed streams omit it. Targets place the marking. They do not
120
120
  author its wording. **`columns`** on `report-start` / `group-start` is the declared
121
121
  [page column](https://getquario.com/docs/diving-deeper/bands/#flowing-in-columns) count when
122
- present. `@quario/pdf` and `@quario/html` lay page columns out;
122
+ present. `@quario/pdf` and `@quario/html` lay page columns out.
123
123
  xlsx never will.
124
124
 
125
125
  ### `text(tokens)`
@@ -133,7 +133,7 @@ Exactly one value token holding a finite number, a boolean, or a valid `Date` ke
133
133
  pre-stringify value. Anything else, including a lone null, reports `undefined` and joins to
134
134
  display text. Passing the cell's resolved `format` kind opts into the seam's one coercion: under
135
135
  `"date"`, an RFC 3339 string revives to the `Date` it names. Spreadsheet consumers use this for
136
- real numeric cells;
136
+ real numeric cells.
137
137
  [`@quario/csv`](https://www.npmjs.com/package/@quario/csv) is the short form.
138
138
 
139
139
  ### `styledRuns(tokens)`
@@ -147,12 +147,12 @@ target cannot drift from them.
147
147
  ### `walk(events, handlers)` / `breathe()`
148
148
 
149
149
  `walk` is the delivery driver every official target uses. Pass one render's event iterable and
150
- per-event handlers keyed by type; a missing handler ignores that event. It pulls on demand, hands
151
- the loop back between batches, and delivers the opening event before pulling a second, so a
150
+ per-event handlers keyed by type. A missing handler ignores that event. It pulls on demand,
151
+ returns the loop between batches, and delivers the opening event before pulling a second, so a
152
152
  target can settle `report-start` (page bands, marking) there instead of draining the stream
153
153
  itself.
154
154
 
155
- `breathe()` is that hand-back alone. Await it between batches of a loop you own; `walk` already
155
+ `breathe()` is that return alone. Await it between batches of a loop you own. `walk` already
156
156
  calls it for you.
157
157
 
158
158
  ### Presentation helpers
@@ -163,11 +163,11 @@ cell the same way:
163
163
  - `display(value)` — the scalar rule `text()` joins with: a `Date` as ISO 8601 UTC, nullish as
164
164
  the empty string, everything else `String(value)`.
165
165
  - `format(value, style?, options?)` — presents a token under the cell's resolved `format`
166
- declaration (its kind and modifier, and for `currency` the cell's own code); `undefined` when the kind does not apply, so the caller falls back to
167
- `display()`.
166
+ declaration (its kind and modifier, and for `currency` the cell's own code). It answers
167
+ `undefined` when the kind does not apply, so the caller falls back to `display()`.
168
168
  - `fractionDigits(style?, options?)` — the digit count a resolved `format` declaration presents:
169
169
  the kind's own (two for `number` and `percent`, a currency's minor units for `currency`) unless
170
- the declaration's `digits` overrides it; `undefined` where there is no count.
170
+ the declaration's `digits` overrides it. It answers `undefined` where there is no count.
171
171
  - `currencyOf(style?, options?)` — which code a money cell wears: its own, else the instance's.
172
172
  - `isReportBand(role)` — whether a role names one of the report's own bands rather than a
173
173
  group's.
@@ -176,8 +176,8 @@ cell the same way:
176
176
 
177
177
  ### `validate(schema, functions?)`
178
178
 
179
- Checks a definition without rendering it. Returns every problem as a path-prefixed string; an
180
- empty array means valid.
179
+ Validates a definition without rendering it. It returns every problem as a path-prefixed string.
180
+ An empty array means valid.
181
181
 
182
182
  ```js
183
183
  validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
@@ -202,7 +202,7 @@ is not a problem at a lower severity, which is why it has no `diagnostic` — no
202
202
  engine decided. Neither warning below locates into an authored source, so none carries a `source`
203
203
  today. **A warning is never fatal**: a document carrying only warnings compiles and
204
204
  renders, so `report` is `null` on `problems` alone. Two declarations warn today — a `currency` on
205
- a cell whose `format` is not `"currency"`, and a table where every column is sized and the widths
205
+ a cell whose `format` is not `"currency"`, and a table where the author sized every column and the widths
206
206
  total under 100 — and the list is advisory and deliberately incomplete, so a quiet one is not a
207
207
  promise that every declaration will be read. `validate()` returns problems only.
208
208
 
@@ -217,10 +217,10 @@ else console.error(problems[0].path, problems[0].message);
217
217
 
218
218
  True when a caught value is a **located diagnostic**: an error xprsn, sjabloon or padvinder
219
219
  minted — thrown by that engine, or re-thrown by quario with the engine original behind it.
220
- Authentication checks identity. An error that only matches the shape does not pass.
220
+ Authentication tests identity. An error that only matches the shape does not pass.
221
221
 
222
- Being located is not what the guard reads: quario's own verdicts on a document and a registered
223
- function's own throw are located too, and neither is a diagnostic. Errors a report throws
222
+ Location is not what the guard reads: quario's own verdicts on a document and a registered
223
+ function's own throw carry a location too, and neither is a diagnostic. Errors a report throws
224
224
  name the path they failed at — and the offending source, where there is one — while keeping their
225
225
  original type (`SyntaxError`, `TypeError`, `RangeError`). What a diagnostic adds on top is
226
226
  metadata an engine vouches for: `code`, `start`/`end` offsets, and, for a query budget in place
@@ -246,7 +246,7 @@ try {
246
246
  | `run.<name>` | Running accumulator values on the current detail row |
247
247
 
248
248
  Each anchor is a distinct object. Absent reads are `null`, so `x == null` holds for a missing
249
- field; reading _through_ a null base still throws, so use `?.`.
249
+ field. Reading _through_ a null base still throws, so use `?.`.
250
250
 
251
251
  ## Options
252
252
 
@@ -254,15 +254,15 @@ field; reading _through_ a null base still throws, so use `?.`.
254
254
  quario({ query: { maxNodes: 10_000, maxDepth: 64, maxResults: 1_000 } }).report(schema, functions);
255
255
  ```
256
256
 
257
- `query` bounds the JSONPath data selection. Hosts set budgets through this API argument;
258
- definitions do not carry it. Failures are located at `data`, keep their `RangeError` type, and
257
+ `query` bounds the JSONPath data selection. Hosts set budgets through this API argument.
258
+ Definitions do not carry it. Failures point at `data`, keep their `RangeError` type, and
259
259
  carry `code`, `limit`, and `actual`. Every render starts with fresh counters.
260
260
 
261
261
  ## Writing a render target
262
262
 
263
263
  Read the stream through the public API. Do not reach for engine internals. A complete, tested
264
264
  Markdown target lives in the repository at `example/markdown.js` in about 70 lines. The built-in
265
- targets are written against the same public API.
265
+ targets use the same public API.
266
266
 
267
267
  Two rules a target owes its users: escape or neutralize every `value` token at your own edge, and
268
268
  map the [style vocabulary](https://getquario.com/docs/reference/style-declarations/) to your own
@@ -291,10 +291,10 @@ the [engine reference](https://getquario.com/docs/reference/quario/) is this pac
291
291
 
292
292
  ## License
293
293
 
294
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
295
- licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
294
+ Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
295
+ Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
296
296
 
297
- Pass your license key in the options; it is verified offline:
297
+ Pass your license key in the options. quario verifies it offline:
298
298
 
299
299
  ```js
300
300
  const q = quario({ license: "quario_..." });
package/lib/host.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * What a host got wrong in a target factory's options, named. A target
3
+ * refuses what it does not understand at the factory call, so a typo costs
4
+ * one stack trace at the line the host wrote rather than a wrong document
5
+ * nothing reports (`docs/adr/0072`).
6
+ *
7
+ * Here rather than in a target for the reason `imageError` carries beside it:
8
+ * every target needs it, no two of them may depend on each other, and the
9
+ * engine is what they all already have. Nothing an author writes reaches this
10
+ * module -- these are host API, and no located diagnostic is involved.
11
+ */
12
+
13
+ /** @type {(msg: string) => never} */
14
+ let fail = (msg) => {
15
+ throw TypeError(msg);
16
+ };
17
+
18
+ /**
19
+ * An object a host may have written by hand: not an array, not a function, and
20
+ * not `null`, each of which is a different mistake wearing `typeof "object"`.
21
+ *
22
+ * @type {(value: unknown) => boolean}
23
+ */
24
+ let plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25
+
26
+ // Own keys only: an inherited enumerable property is the prototype's business,
27
+ // and naming it back at the host would name something they never wrote.
28
+ /** @type {(value: object, keys: readonly string[], at: string) => void} */
29
+ let closed = (value, keys, at) => {
30
+ for (let key of Object.keys(value))
31
+ if (!keys.includes(key)) fail(at + ': unknown option "' + key + '"');
32
+ };
33
+
34
+ /**
35
+ * A target's own options, with its key set closed. Nullish is absence, which
36
+ * is what the engine does with every value a host omits, so a host writing
37
+ * `meta: config.meta ?? null` is not a host making a mistake.
38
+ *
39
+ * @template T
40
+ * @param {T} value The options object, or a nested one.
41
+ * @param {readonly string[]} keys Every key this object may carry.
42
+ * @param {string} at The option path, which prefixes the failure.
43
+ * @returns {T} `value`, unchanged.
44
+ */
45
+ export let hostOptions = (value, keys, at) => {
46
+ if (value == null) return value;
47
+ if (!plain(value)) fail(at + ": expected an object");
48
+ closed(value, keys, at);
49
+ return value;
50
+ };
51
+
52
+ /** The three document properties a host may write. */
53
+ let PROPERTIES = ["title", "author", "subject"];
54
+
55
+ // Nullish is absence here too, one level down: a host writing
56
+ // `{ title: config.title }` over a config that carries no title has made no
57
+ // mistake, and a target skips the property the same way it skips a key that
58
+ // was never written.
59
+ /** @type {(value: any, at: string) => void} */
60
+ let everyString = (value, at) => {
61
+ for (let key of Object.keys(value))
62
+ if (value[key] != null && typeof value[key] !== "string")
63
+ fail(at + "." + key + ": expected a string");
64
+ };
65
+
66
+ /**
67
+ * The `meta` option every document target takes, validated whole: those three
68
+ * properties, each a string, and never a date. What a host cannot write is
69
+ * what keeps a render reproducible -- each of these formats has one part with
70
+ * a slot for a clock, and none of them has a way to fill it.
71
+ *
72
+ * The contract is one thing, so it is checked in one place. What stays a
73
+ * target's own is the name each property carries in its format.
74
+ *
75
+ * Returns a **copy**, so the properties are read once, here. A host that
76
+ * mutates its options object between renders cannot make one configured
77
+ * target write two different documents, and no target has to remember to
78
+ * copy for itself.
79
+ *
80
+ * @param {any} value The `meta` option.
81
+ * @param {string} at The option path, which prefixes the failure.
82
+ * @returns {any} A copy of `value`, or undefined when the host wrote none.
83
+ */
84
+ export let hostMeta = (value, at) => {
85
+ hostOptions(value, PROPERTIES, at);
86
+ if (value == null) return undefined;
87
+ everyString(value, at);
88
+ return { ...value };
89
+ };
package/lib/index.d.ts CHANGED
@@ -920,6 +920,33 @@ export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
920
920
  */
921
921
  export function imageError(path: string | undefined, said: string, cause?: unknown): Error;
922
922
 
923
+ /** Optional document information; never includes dates, so output stays deterministic. */
924
+ export interface QuarioMeta {
925
+ title?: string;
926
+ author?: string;
927
+ subject?: string;
928
+ }
929
+
930
+ /**
931
+ * Close a **target factory's** option key set, for the target itself to call.
932
+ * A target refuses what it does not understand at the factory call, so a
933
+ * host's typo costs one stack trace where they wrote it rather than an option
934
+ * silently lost. Throws a `TypeError` prefixed with `at`.
935
+ *
936
+ * Nullish is absence. Own enumerable keys only, so an inherited property is
937
+ * not reported back as an option the host wrote.
938
+ */
939
+ export function hostOptions<T>(value: T, keys: readonly string[], at: string): T;
940
+
941
+ /**
942
+ * Validate a target's `meta` option whole: `title`, `author` and `subject`,
943
+ * each a string, and nothing else. Every document target takes the same three,
944
+ * so the contract is checked once; the name each property carries inside a
945
+ * given format stays that target's own. Throws a `TypeError` prefixed with
946
+ * `at`.
947
+ */
948
+ export function hostMeta<T extends QuarioMeta | null | undefined>(value: T, at: string): T;
949
+
923
950
  /**
924
951
  * Join a token stream to display text: literals verbatim, values through
925
952
  * `display()`. Re-exported from sjabloon.
package/lib/index.js CHANGED
@@ -34,6 +34,7 @@ export {
34
34
  STYLE_NAMES,
35
35
  } from "./style.js";
36
36
  export { imageError, isDiagnostic } from "./locate.js";
37
+ export { hostMeta, hostOptions } from "./host.js";
37
38
 
38
39
  /** @typedef {import("./scope.js").Scope} Scope */
39
40
 
package/lib/license.js CHANGED
@@ -11,12 +11,12 @@
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"; // release-date
14
+ let RELEASE = "2026-09-15"; // 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.
18
18
  let PUBKEY =
19
- "BEyXC0sH4pDoL-gGxLTqHguWi38WK2v7OrAfNvK8_4Ou8yLhcKgX5dzCoj1ojj6ez0B1cwMDPfejthqEQ4wZGjM";
19
+ "BLT0Rp5dYp7U6qiaBO5GfEx5lOntJZAbCrbmEfE824dHhdEzfylChU7xUEDj5jnArxSjXaTu8jnBPD8MX5FbNik";
20
20
 
21
21
  // The marking wording, stated once for the whole engine and carried to every
22
22
  // target on `report-start` (SCHEMA.md, "License keys"): the targets own where
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quario",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A tiny, runtime-neutral report engine — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",