quario 0.1.0 → 0.3.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 +191 -0
- package/README.md +19 -4
- package/lib/format.js +60 -0
- package/lib/index.d.ts +210 -13
- package/lib/index.js +157 -69
- package/lib/license.js +1 -1
- package/lib/locate.js +43 -56
- package/lib/plan.js +498 -164
- package/lib/scope.js +4 -10
- package/lib/stream.js +8 -8
- package/lib/style.js +105 -13
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,197 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`format` is a closed style name.** `number` / `currency` / `percent` /
|
|
15
|
+
`date` on text items, column cells, headers, and totals. Not images, not
|
|
16
|
+
`row.style`, not the report default. Locale, currency code, and timezone
|
|
17
|
+
live on `quario({ locale, currency, timeZone })`. The public `format()`
|
|
18
|
+
helper is how targets present a kind; a kind on the wrong type contributes
|
|
19
|
+
nothing (`docs/adr/0041`).
|
|
20
|
+
|
|
21
|
+
- **The report header may pin a `height` from the page top.** Dual-shaped
|
|
22
|
+
like `detail`: an item array, or `{ height, items }`. `page.margin` is a
|
|
23
|
+
document field (one number, all four sides), required with `height` and
|
|
24
|
+
legal without. Authored `spaceBefore` on the first occupying item of the
|
|
25
|
+
next band is refused (`docs/adr/0038`).
|
|
26
|
+
|
|
27
|
+
- **`spaceBefore` / `spaceAfter` return as item flow spacing.** Blank space
|
|
28
|
+
before or after a band item, in points, including band images and splits as
|
|
29
|
+
band items. Adjacent gaps add. Table cells, `row.style`, headers, totals,
|
|
30
|
+
and split slots refuse the names. `spaceBefore` drops at a fresh body page
|
|
31
|
+
or strip top; page-band items keep it. Leading and inset stay cut
|
|
32
|
+
(`docs/adr/0037`).
|
|
33
|
+
|
|
34
|
+
- **Per-side padding and border on the closed style vocabulary.**
|
|
35
|
+
`paddingTop` / `Right` / `Bottom` / `Left` (points, ≥ 0) and, per side,
|
|
36
|
+
`border*Width`, `border*Style` (`solid` | `dashed` | `dotted`),
|
|
37
|
+
`border*Color` (`#rgb` / `#rrggbb`). A border side is all three names or
|
|
38
|
+
none; width `0` is no stroke; an incomplete literal is a definition error,
|
|
39
|
+
and an incomplete expression result at render contributes nothing rather
|
|
40
|
+
than a solid black stroke. The names are legal wherever `background` is,
|
|
41
|
+
including images, plus `row.style`. The report default still takes only
|
|
42
|
+
`family` and `size`. Column `%` widths are border-box.
|
|
43
|
+
|
|
44
|
+
- **`detail.header` is the header-row box.** `{ style }` only, a different
|
|
45
|
+
path from `columns[i].header`. It crosses the seam as `table-start.style`.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **A table total is N rows.** Before, `total` was one row: a flat cell
|
|
50
|
+
array or `{ style, cells }`. After, it is absent or a non-empty array of
|
|
51
|
+
`{ cells, style?, visible? }`. A one-row total is `[{ cells: [...] }]`.
|
|
52
|
+
`total: []` is a definition error. Paths are `detail.total[r].cells[i]`.
|
|
53
|
+
The stream yields one `total-row` per emitted row (`docs/adr/0042`).
|
|
54
|
+
|
|
55
|
+
- **A visible text item occupies a line at its own `size`.** Empty display
|
|
56
|
+
used to take the report default's leading in PDF and collapse in HTML;
|
|
57
|
+
`"a\n\nb"` broke in PDF and collapsed to a space in HTML. A visible item
|
|
58
|
+
now occupies at least one line set in that item's `size`, and a literal
|
|
59
|
+
newline is a line break, on every target that can show a line. Empty
|
|
60
|
+
table cells stay contentless for height. This is not a spacing primitive;
|
|
61
|
+
`visible: false` is still how an item leaves the layout.
|
|
62
|
+
|
|
63
|
+
## [0.2.0] - 2026-09-01
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **A report default: one `style` block for the whole document.** A top-level
|
|
68
|
+
`style` beside `header`/`detail`/`footer` states the typeface a report is set
|
|
69
|
+
in, so a document with one face names it once instead of on every item, every
|
|
70
|
+
table column, every header and every total cell. It takes `family` and
|
|
71
|
+
`size`; any other declaration there is a located definition error, exactly as
|
|
72
|
+
a text declaration on an image item is. Values are literals or `=`
|
|
73
|
+
expressions like any other style block's, resolved once per render in report
|
|
74
|
+
scope.
|
|
75
|
+
|
|
76
|
+
- **`report-start` carries the resolved report default as `style`.** A
|
|
77
|
+
report-level fact, never merged into an item's own `style`: an event's
|
|
78
|
+
`style` stays what the author wrote on that node, and a consumer composes the
|
|
79
|
+
default itself, once — under its own band-role defaults and under every
|
|
80
|
+
event's own style. Absent when the report declares none, so a consumer
|
|
81
|
+
written before this field renders in its own baseline exactly as it did.
|
|
82
|
+
|
|
83
|
+
- **An `uppercase` style declaration.** A boolean beside `bold` and `italic`,
|
|
84
|
+
literal or an `=` expression, for the capitalised column labels business
|
|
85
|
+
forms are usually set with. It is capitals, not small caps — real small caps
|
|
86
|
+
need a font feature the PDF target's built-in faces cannot supply, so the
|
|
87
|
+
declaration promises only what every target can draw. Image items keep
|
|
88
|
+
refusing it, as they refuse every text declaration.
|
|
89
|
+
|
|
90
|
+
- **A `split` item places values across the line instead of down the band.**
|
|
91
|
+
The invoice header's "seller left, customer right", which a band could not
|
|
92
|
+
say before. `{ "type": "split", "slots": [...] }` takes two or more slots,
|
|
93
|
+
each an ordinary `text` or `image` item plus an optional `width` percentage
|
|
94
|
+
under the same rules a table column's obeys; width-less slots divide what
|
|
95
|
+
the sized ones leave, evenly. A split is always the full content width and
|
|
96
|
+
never nests — it says what sits beside what, never where anything is — and
|
|
97
|
+
a slot that renders nothing keeps its width, so a line's geometry never
|
|
98
|
+
moves with the data. Splits may appear in every item array except table and
|
|
99
|
+
total cells.
|
|
100
|
+
|
|
101
|
+
- **`split-start` / `split-end` bracket a split's slots on the event stream.**
|
|
102
|
+
`split-start` carries the slot geometry, then one ordinary `item` or `image`
|
|
103
|
+
event per slot in order, then `split-end`. Existing consumers need no
|
|
104
|
+
change: the walk driver's missing-handler rule means a target that ignores
|
|
105
|
+
the bracket still receives the slot items and renders them stacked.
|
|
106
|
+
|
|
107
|
+
- **`quario().plan(schema, funcs?)` hands the whole traversal over at once.**
|
|
108
|
+
Returns `{ report, problems, anchors }` from one descent: the compiled
|
|
109
|
+
report (`null` while the document has problems), every problem as
|
|
110
|
+
`{ path, source?, message, diagnostic? }`, and a map from each compiled
|
|
111
|
+
source's schema path to the anchors and group handles it reads. A host that
|
|
112
|
+
validates and renders per edit — an editor — pays one traversal instead of
|
|
113
|
+
two, can point at a problem in its own UI without parsing a message apart,
|
|
114
|
+
and can tell where a node may safely move. `validate()` is unchanged: its
|
|
115
|
+
strings are those problems' `message` fields.
|
|
116
|
+
- **Every problem keeps its own located diagnostic.** `validate()` retained
|
|
117
|
+
`start`/`end` offsets on at most one problem per document; the structured
|
|
118
|
+
list carries each problem's own, so a consumer can underline the offending
|
|
119
|
+
character in every faulty expression rather than only the first.
|
|
120
|
+
- **Events name the definition behind them.** `item`, `image` and
|
|
121
|
+
`group-start` events carry `path`, the schema path of the node that
|
|
122
|
+
produced them; `table-start` carries `detail` and its columns their own,
|
|
123
|
+
and every table cell carries its column definition's path (a total cell its
|
|
124
|
+
`detail.total[i]` entry). Rows carry no identity — a row is data, not
|
|
125
|
+
schema. Additive: a consumer that ignores the field is unaffected.
|
|
126
|
+
- **`display(value)` joins one value the way `text()` does.** The scalar
|
|
127
|
+
display rule behind the token join, re-exported beside `text()` so a stream
|
|
128
|
+
consumer that stringifies token values itself renders exactly what the
|
|
129
|
+
official targets render — Dates included.
|
|
130
|
+
|
|
131
|
+
- **`maxDepth: Infinity` opts a query budget out.** The data query's traversal
|
|
132
|
+
budgets accept an explicit `Infinity` per key for "this budget, unbounded".
|
|
133
|
+
The 500-deep default is unchanged — it is now padvinder's own, applied for
|
|
134
|
+
every consumer rather than added by quario at the seam.
|
|
135
|
+
|
|
136
|
+
- **Located data-query errors carry padvinder's code and span.** A `data`
|
|
137
|
+
query that does not parse now surfaces with padvinder's `code` and
|
|
138
|
+
`start`/`end` offsets into the query you wrote — filter faults included —
|
|
139
|
+
alongside the band/item path quario already attached. The code names the
|
|
140
|
+
category of mistake: `PADVINDER_MISSING_ROOT`, `PADVINDER_BAD_SELECTOR`,
|
|
141
|
+
`PADVINDER_UNCLOSED_BRACKET`, `PADVINDER_BAD_STRING`,
|
|
142
|
+
`PADVINDER_UNKNOWN_FUNCTION`, or `PADVINDER_SYNTAX` for a path character or
|
|
143
|
+
filter body that is open-endedly not a query. Traversal budgets exceeded at
|
|
144
|
+
render time keep `limit`/`actual` and carry no span.
|
|
145
|
+
|
|
146
|
+
### Changed
|
|
147
|
+
|
|
148
|
+
- **Cells render straight over the engine scope chain.** A text cell no longer
|
|
149
|
+
allocates a wrapper scope and an anchor pair per cell per row — quario's
|
|
150
|
+
chain already binds `$` at the render base and `@` on the detail row, and
|
|
151
|
+
sjabloon now renders over it as-is. A 4-column stream over a million rows
|
|
152
|
+
went from 1.7s to 0.5s. No report changes what it renders. Requires
|
|
153
|
+
sjabloon 0.11.
|
|
154
|
+
|
|
155
|
+
- **A compiled report's `functions` carry signatures.** Each entry is now
|
|
156
|
+
`{ name, arity, doc? }` instead of a bare name — `arity` from the function's
|
|
157
|
+
declared parameter count (or its own numeric `arity` where rest parameters
|
|
158
|
+
mislead `length`), `doc` from an own `doc` string when it carries one — in
|
|
159
|
+
the same call-first-seen order. `names` is unchanged. Requires xprsn 0.11
|
|
160
|
+
and sjabloon 0.11.
|
|
161
|
+
|
|
162
|
+
- **A bare `Date` renders as ISO 8601 UTC, the same on every machine.**
|
|
163
|
+
Display text for a `Date` value was `String(date)`, which bakes the
|
|
164
|
+
rendering host's timezone and locale into the output — so one report
|
|
165
|
+
produced different bytes on different machines. Every target now renders a
|
|
166
|
+
valid `Date` as its `toISOString()` form through the one shared display
|
|
167
|
+
rule; an invalid `Date` keeps its deterministic `Invalid Date` text.
|
|
168
|
+
Reports that want a formatted date keep using a registered function,
|
|
169
|
+
exactly as before.
|
|
170
|
+
|
|
171
|
+
- **A bad literal pattern in the data query is a definition error.** A typo'd
|
|
172
|
+
I-Regexp written as a string literal in `match()`/`search()` used to
|
|
173
|
+
produce a plausible empty report with no signal; `report()` and
|
|
174
|
+
`validate()` now surface it at compile time as a located error, with
|
|
175
|
+
offsets at the pattern literal in the query. The class and `code` are
|
|
176
|
+
treffer's — a malformed pattern is a `SyntaxError` with `TREFFER_SYNTAX`,
|
|
177
|
+
one past treffer's limits a `RangeError` with a `TREFFER_MAX_*` code and
|
|
178
|
+
`limit`/`actual` — on the same rule that already gives xprsn's codes to a
|
|
179
|
+
fault in an expression: the engine that decided the fault names it. A
|
|
180
|
+
pattern that arrives from render data keeps RFC 9535 semantics and still
|
|
181
|
+
matches nothing at render time. Requires padvinder 0.8.
|
|
182
|
+
|
|
183
|
+
- **Each engine relocates its own diagnostic.** A located error is now a copy
|
|
184
|
+
made by the engine that raised it (xprsn, sjabloon, or padvinder), so it
|
|
185
|
+
carries every field that engine puts on a diagnostic — nothing is lost in
|
|
186
|
+
quario's re-throw, today or when an engine adds a field — and it still
|
|
187
|
+
passes that engine's own `isDiagnostic`. Host errors are wrapped as plain
|
|
188
|
+
errors with no diagnostic metadata, exactly as before. Requires xprsn 0.10,
|
|
189
|
+
sjabloon 0.9, and padvinder 0.5. No report changes what it renders.
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- **A host error cannot pose as the report's diagnostic.** `report()` rethrows
|
|
194
|
+
the first definition problem's located engine error; it chose that error by
|
|
195
|
+
probing for a `code` property, so a host error class that stamps `code` on
|
|
196
|
+
itself in its constructor was rethrown as if the engines had raised it. The
|
|
197
|
+
choice now goes through the same identity-based authentication as
|
|
198
|
+
`isDiagnostic`; everything else falls back to a plain `SyntaxError` naming
|
|
199
|
+
the first problem, as before.
|
|
200
|
+
|
|
10
201
|
## [0.1.0] - 2026-08-27
|
|
11
202
|
|
|
12
203
|
### Added
|
package/README.md
CHANGED
|
@@ -84,11 +84,11 @@ row, an optional `total-row`, and `table-end`.
|
|
|
84
84
|
| Event | Carries |
|
|
85
85
|
| -------------- | ------------------------------------------------------------------------------------------- |
|
|
86
86
|
| `report-start` | `params`, resolved report `aggregates`, optional `page` band closures, `columns`, `marking` |
|
|
87
|
-
| `item` | `role`, `tokens`, optional `style`, `run`
|
|
88
|
-
| `image` | `role`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run`
|
|
89
|
-
| `group-start` | `name`, `depth`, `key`, `aggregates`, optional `break`, `reset`, `columns`
|
|
87
|
+
| `item` | `role`, `path`, `tokens`, optional `style`, `run` |
|
|
88
|
+
| `image` | `role`, `path`, `bytes`, `format` (`png` \| `jpeg`), `fit`, optional `alt`, `style`, `run` |
|
|
89
|
+
| `group-start` | `name`, `depth`, `key`, `aggregates`, `path`, optional `break`, `reset`, `columns` |
|
|
90
90
|
| `group-end` | `name`, `depth` |
|
|
91
|
-
| `table-start` | `columns` (each `header`, optional `width`)
|
|
91
|
+
| `table-start` | `path`, `columns` (each `header`, `path`, optional `width`) |
|
|
92
92
|
| `row` | `cells`, optional `style`, `run` |
|
|
93
93
|
| `total-row` | `cells` |
|
|
94
94
|
| `table-end` | - |
|
|
@@ -145,6 +145,21 @@ validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
|
|
|
145
145
|
`report()` throws on the first problem instead. Validation and compilation share one traversal,
|
|
146
146
|
so `validate()` can never disagree with what `report()` accepts.
|
|
147
147
|
|
|
148
|
+
### `quario().plan(schema, functions?)`
|
|
149
|
+
|
|
150
|
+
The one traversal, whole — for hosts that validate and render in a loop, like an editor. Returns
|
|
151
|
+
`{ report, problems, anchors }`: the compiled report (`null` while the document has problems),
|
|
152
|
+
every problem structurally as `{ path, source?, message, diagnostic? }` (the `message` is exactly
|
|
153
|
+
`validate()`'s string, and every problem keeps its own located diagnostic with `start`/`end`
|
|
154
|
+
offsets, not only the first), and `anchors`, mapping each compiled source's schema path to the
|
|
155
|
+
anchors and group handles it reads — the unfiltered complement of `names`.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const { report, problems, anchors } = quario().plan(schema);
|
|
159
|
+
if (report) await report.render(html(), data);
|
|
160
|
+
else console.error(problems[0].path, problems[0].message);
|
|
161
|
+
```
|
|
162
|
+
|
|
148
163
|
### `isDiagnostic(error)`
|
|
149
164
|
|
|
150
165
|
True when a caught value is one of the stack's located errors: quario's own, or one thrown
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Present a cell's raw token as `number` / `currency` / `percent` / `date`.
|
|
3
|
+
* Targets import this instead of each other: the stream keeps the kind on
|
|
4
|
+
* `style` and the token raw, and HTML/PDF stringify at the edge. A kind on
|
|
5
|
+
* the wrong type, or on null, contributes nothing — the caller falls back to
|
|
6
|
+
* `display()`. Invalid locale or currency codes fail the same way.
|
|
7
|
+
*
|
|
8
|
+
* Defaults (`en-US`, UTC) are pinned so a PDF without a host locale still
|
|
9
|
+
* renders the same bytes on every machine (docs/adr/0041, docs/adr/0025).
|
|
10
|
+
*/
|
|
11
|
+
import { finiteDate, finiteNum } from "./stream.js";
|
|
12
|
+
/** @type {(options: any) => string} */
|
|
13
|
+
let localeOf = (options) => options?.locale || "en-US";
|
|
14
|
+
/** @type {(options: any) => string} */
|
|
15
|
+
let zoneOf = (options) => options?.timeZone || "UTC";
|
|
16
|
+
|
|
17
|
+
/** @type {(value: any, locale: string) => string | undefined} */
|
|
18
|
+
let asNumber = (value, locale) =>
|
|
19
|
+
finiteNum(value) ? new Intl.NumberFormat(locale).format(value) : undefined;
|
|
20
|
+
/** @type {(value: any, locale: string) => string | undefined} */
|
|
21
|
+
let asPercent = (value, locale) =>
|
|
22
|
+
finiteNum(value) ? new Intl.NumberFormat(locale, { style: "percent" }).format(value) : undefined;
|
|
23
|
+
/** @type {(options: any) => string | null} */
|
|
24
|
+
let currencyOf = (options) => {
|
|
25
|
+
let currency = options?.currency;
|
|
26
|
+
return typeof currency === "string" && currency ? currency : null;
|
|
27
|
+
};
|
|
28
|
+
/** @type {(value: any, locale: string, options: any) => string | undefined} */
|
|
29
|
+
let asMoney = (value, locale, options) => {
|
|
30
|
+
let currency = currencyOf(options);
|
|
31
|
+
if (!finiteNum(value) || !currency) return;
|
|
32
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
|
|
33
|
+
};
|
|
34
|
+
/** @type {(value: any, locale: string, options: any) => string | undefined} */
|
|
35
|
+
let asDate = (value, locale, options) =>
|
|
36
|
+
finiteDate(value)
|
|
37
|
+
? new Intl.DateTimeFormat(locale, { timeZone: zoneOf(options) }).format(value)
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
/** @type {Record<string, (value: any, locale: string, options: any) => string | undefined>} */
|
|
41
|
+
let KINDS = { number: asNumber, currency: asMoney, percent: asPercent, date: asDate };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} value The interpolation's pre-stringify token.
|
|
45
|
+
* @param {unknown} kind A resolved `format` declaration.
|
|
46
|
+
* @param {{ locale?: string, currency?: string, timeZone?: string } | null} [options]
|
|
47
|
+
* The instance's locale, currency code, and timezone.
|
|
48
|
+
* @returns {string | undefined} The presented text, or nothing when this
|
|
49
|
+
* kind does not apply.
|
|
50
|
+
*/
|
|
51
|
+
export function format(value, kind, options) {
|
|
52
|
+
if (typeof kind !== "string") return;
|
|
53
|
+
let present = KINDS[kind];
|
|
54
|
+
if (!present) return;
|
|
55
|
+
try {
|
|
56
|
+
return present(value, localeOf(options), options);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|