quario 0.7.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 +114 -77
- package/README.md +44 -27
- package/lib/host.js +89 -0
- package/lib/image.js +92 -0
- package/lib/index.d.ts +49 -2
- package/lib/index.js +1 -0
- package/lib/license.js +6 -6
- package/lib/plan.js +41 -15
- package/lib/stream.js +0 -23
- package/lib/style.js +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,105 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
# quario
|
|
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
|
+
|
|
47
|
+
## 0.8.0
|
|
48
|
+
|
|
49
|
+
### Minor Changes
|
|
50
|
+
|
|
51
|
+
- **BREAKING: an image whose header does not state its size now fails on every
|
|
52
|
+
target.** A PNG or JPEG truncated before its dimensions used to render on the
|
|
53
|
+
HTML and CSV targets — the browser coped, and CSV has no picture to place —
|
|
54
|
+
while failing on the PDF and XLSX targets, which have to know how big a
|
|
55
|
+
picture is before they can put it anywhere. One file, two verdicts, depending
|
|
56
|
+
on where the report was sent.
|
|
57
|
+
|
|
58
|
+
It is now a single render error, raised where every other verdict on image
|
|
59
|
+
bytes is raised and naming the item's `source` the same way — for example
|
|
60
|
+
`header[0].source [=$.input.logo]: could not read the image's size from its bytes`.
|
|
61
|
+
A report that renders as a PDF today is unaffected: those bytes already
|
|
62
|
+
failed there. What changes is a report that renders as HTML today on bytes no other target
|
|
63
|
+
would accept: it now fails, and the file it names is one no target could ever
|
|
64
|
+
have drawn correctly.
|
|
65
|
+
|
|
66
|
+
Nothing further has changed about what the engine promises: a file whose size
|
|
67
|
+
reads but whose pixel data is corrupt is still nobody's guarantee, and each
|
|
68
|
+
target does whatever its own machinery does with it.
|
|
69
|
+
|
|
70
|
+
- **The image event carries the picture's size.** `width` and `height`, in
|
|
71
|
+
pixels, read from the same header the format is sniffed from. A target — the
|
|
72
|
+
built-in ones, or your own — places a picture from two fields instead of
|
|
73
|
+
parsing a PNG's IHDR and a JPEG's frame header for itself.
|
|
74
|
+
- **A page-number token says which page value it is.** A value token whose
|
|
75
|
+
interpolation is exactly `{{ page.number }}` or `{{ page.total }}` now carries
|
|
76
|
+
`field` on the event stream, holding that same string. The `value` is
|
|
77
|
+
unchanged — the number the page band was called with — so nothing renders
|
|
78
|
+
differently; `field` tells a target whose own document format numbers pages
|
|
79
|
+
that it may write its own live field there instead of the number this render
|
|
80
|
+
saw. Anything computed from them (`{{ page.number + 1 }}`, `{{ pad(page.total)
|
|
81
|
+
}}`) is an ordinary value and carries no `field`, so write the bare token
|
|
82
|
+
wherever a live number matters.
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- **A PNG wider or taller than 65535 pixels is no longer misplaced.** Its
|
|
87
|
+
dimensions were read from the low half of each of IHDR's four-byte fields, so
|
|
88
|
+
a 70000-pixel panorama measured 4464 pixels and was laid out at that size,
|
|
89
|
+
confidently and silently.
|
|
90
|
+
- **Every published README says where the documentation is.** Each package now
|
|
91
|
+
carries a Documentation section pointing at the reference, at the report schema
|
|
92
|
+
that normatively specifies what a report may declare, and at the package's own
|
|
93
|
+
API. The paragraphs that used to end on an unstated contract — the event
|
|
94
|
+
stream's field semantics, the style vocabulary, page columns, the Content
|
|
95
|
+
Security Policy a fragment with images needs, the formula mangling, and each
|
|
96
|
+
target's own contract — link the page that states it. Every link is an absolute
|
|
97
|
+
URL, so it resolves from the npm package page as readily as from an installed
|
|
98
|
+
copy.
|
|
99
|
+
|
|
100
|
+
## 0.7.0
|
|
101
|
+
|
|
102
|
+
### Minor Changes
|
|
13
103
|
|
|
14
104
|
- **Styled runs.** Bold a word, colour a phrase, or format one value inside a
|
|
15
105
|
sentence, and have it survive into the PDF and the spreadsheet — not only the
|
|
@@ -47,8 +137,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
47
137
|
seen in the definition alone, without data, so a quiet `warnings` list is not
|
|
48
138
|
a promise that every declaration will be read.
|
|
49
139
|
|
|
50
|
-
### Changed
|
|
51
|
-
|
|
52
140
|
- **A `format` declaration now needs one value to speak about, and a cell can
|
|
53
141
|
give it one.** Previously a `format` on a cell reached every interpolation in
|
|
54
142
|
it and skipped the literal text between them, which meant
|
|
@@ -78,8 +166,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
78
166
|
twice. `plan()` reports the cells this affects wherever it can see them
|
|
79
167
|
without data.
|
|
80
168
|
|
|
81
|
-
### Fixed
|
|
82
|
-
|
|
83
169
|
- **`round`, `floor` and `ceil` reuse their formatters instead of rebuilding
|
|
84
170
|
one per call.** The three reach the decimal you wrote through `Intl`, and
|
|
85
171
|
each call built a fresh formatter to do it — so a detail column of
|
|
@@ -88,7 +174,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
88
174
|
Every answer is the value it was before: the rounding mode and the digit
|
|
89
175
|
count are both part of what a reused formatter is found by, so no call can
|
|
90
176
|
be handed one built for another.
|
|
91
|
-
|
|
92
177
|
- **`round`, `floor` and `ceil` name themselves when they refuse an `n`.** All
|
|
93
178
|
three threw `n must be a number from 0 to 100`, so a cell calling more than
|
|
94
179
|
one was told how it failed and never which call; the message now opens with
|
|
@@ -112,7 +197,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
112
197
|
the same and are not diagnostics, so a host following that example rethrew
|
|
113
198
|
report faults as its own. Nothing about the guard changes; it never behaved
|
|
114
199
|
the way the page described.
|
|
115
|
-
|
|
116
200
|
- **A misused inline reducer says what it is, instead of how it folds.**
|
|
117
201
|
`{{ min(1, 2) }}` — the two-argument scalar `min` quario does not have —
|
|
118
202
|
failed at the cell with `(rows || []).map is not a function`, the fold's own
|
|
@@ -130,9 +214,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
130
214
|
mistake: `sum(@.lines)` on a row carrying no lines is `0`, exactly as an
|
|
131
215
|
empty array gives.
|
|
132
216
|
|
|
133
|
-
##
|
|
217
|
+
## 0.6.0
|
|
134
218
|
|
|
135
|
-
###
|
|
219
|
+
### Minor Changes
|
|
136
220
|
|
|
137
221
|
- **`imageError(path, said, cause)`**, the mint a render target raises an image
|
|
138
222
|
failure through. The engine vouches for an image's magic numbers and no
|
|
@@ -141,7 +225,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
141
225
|
that asked for the bytes and keeps the failing class and message behind it,
|
|
142
226
|
so one report reads the same however it is rendered. It is not a diagnostic:
|
|
143
227
|
`isDiagnostic` does not answer for one.
|
|
144
|
-
|
|
145
228
|
- **Formatted cells render dramatically faster.** The engine built a fresh
|
|
146
229
|
`Intl` formatter for every presented cell and now keeps them, keyed on
|
|
147
230
|
everything a formatter is built from — the kind, the locale, the digit count,
|
|
@@ -197,14 +280,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
197
280
|
|
|
198
281
|
- **`sort` is stable**: rows whose keys compare equal keep the order they
|
|
199
282
|
arrived in. The engine has always sorted this way; the promise is new.
|
|
200
|
-
|
|
201
283
|
- `currencyOf(style, options)` joins the package's exports beside `format()`.
|
|
202
284
|
It answers which currency code a cell wears — its own when it declares one,
|
|
203
285
|
else the instance default — for a target that needs the code itself rather
|
|
204
286
|
than presented text. A cell that declared a code the engine could not accept
|
|
205
287
|
carries `style.currency` as `null`, so `style.currency ?? yourDefault` is the
|
|
206
288
|
wrong spelling and this helper is the right one.
|
|
207
|
-
|
|
208
289
|
- **A cell may name its own currency.** `currency` is a new style declaration
|
|
209
290
|
beside `format` — a three-letter ISO 4217 code saying what a money cell is
|
|
210
291
|
denominated in.
|
|
@@ -257,14 +338,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
257
338
|
- `split-start` events carry `path`, the split definition's schema path, as
|
|
258
339
|
`item` and `image` events already do. A consumer can now name the definition
|
|
259
340
|
behind a split without tracking position.
|
|
260
|
-
|
|
261
341
|
- `fractionDigits(kind, options)` is exported beside `format()`: how many
|
|
262
342
|
fraction digits a kind presents, or nothing for a kind that carries no
|
|
263
343
|
count. It is the table the official targets read, and it is public so a
|
|
264
344
|
consumer writing its own target presents the same digits they do.
|
|
265
|
-
|
|
266
|
-
### Changed
|
|
267
|
-
|
|
268
345
|
- **A `date` cell with no `form` now presents `medium`, not the runtime's own
|
|
269
346
|
default.** A `date` that names no form used to render whatever the platform's
|
|
270
347
|
default date format was — under `en-US`, `8/14/2026` — which matched none of
|
|
@@ -285,18 +362,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
285
362
|
to write no number format. Read `style.format.kind` where you read
|
|
286
363
|
`style.format`, and take the digit count off the declaration rather than
|
|
287
364
|
computing one.
|
|
288
|
-
|
|
289
365
|
- **`fractionDigits` takes the whole style.** Its first argument was the kind
|
|
290
366
|
and is now the style, matching `format()` and `currencyOf()`.
|
|
291
367
|
`fractionDigits("number")` becomes
|
|
292
368
|
`fractionDigits({ format: "number" })`. Targets no longer need it at all —
|
|
293
369
|
the count rides on the declaration the stream carries.
|
|
294
|
-
|
|
295
370
|
- **`typed()`'s second argument is the resolved declaration.** Pass
|
|
296
371
|
`style.format` as it arrives on the stream; it reads the kind from the
|
|
297
372
|
object. The shorthand string is still accepted, and omitting the argument is
|
|
298
373
|
unchanged.
|
|
299
|
-
|
|
300
374
|
- **A rejected box value is now dropped from the resolved style rather than
|
|
301
375
|
carried on it.** A `border*` or `padding*` declaration written as an `=`
|
|
302
376
|
expression used to cross the render stream with whatever the expression
|
|
@@ -435,23 +509,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
435
509
|
- A `currency` code that is not a readable currency now presents nothing in
|
|
436
510
|
every target, rather than the worksheet inventing a number format from it
|
|
437
511
|
while the other targets fell back to the unformatted value.
|
|
438
|
-
|
|
439
|
-
### Fixed
|
|
440
|
-
|
|
441
512
|
- **A split slot is typed with the declarations the traversal accepts.**
|
|
442
513
|
`SplitSlot` reused the band items' style types, so TypeScript refused
|
|
443
514
|
`valign` on an image slot — legal there, and only there — and admitted
|
|
444
515
|
`spaceBefore`/`spaceAfter` on a slot, which the engine rejects. Two new
|
|
445
516
|
exported types, `SlotStyleDeclarations` and `SlotImageStyleDeclarations`,
|
|
446
517
|
now say what a slot may declare.
|
|
447
|
-
|
|
448
518
|
- **A page band closure is typed as what it returns.** `PageBandRenderers`
|
|
449
519
|
declared `header`/`footer` as returning item and image events only, while a
|
|
450
520
|
split in a page band has always come back as its bracket — `split-start`,
|
|
451
521
|
one event per slot, `split-end`. The closures now return `PageBandEvent[]`,
|
|
452
522
|
a new exported union naming all four, so a TypeScript host reading a page
|
|
453
523
|
band no longer types `role` onto a `split-end` that carries none.
|
|
454
|
-
|
|
455
524
|
- **A computed style value the schema does not accept now leaves the layer
|
|
456
525
|
below in place**, which is what the schema has always said it does —
|
|
457
526
|
"contributes nothing, same as omit". The name is dropped from the resolved
|
|
@@ -486,7 +555,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
486
555
|
the cell falls back to display text, as the documented rule says any
|
|
487
556
|
unrecognised kind does. Before, such a cell could render `[object Undefined]`.
|
|
488
557
|
A literal was never affected: it is checked against the four kinds.
|
|
489
|
-
|
|
490
558
|
- **A pinned report header now refuses a lead on every band that can sit under
|
|
491
559
|
its box, and only where the document settles which one that is.** When
|
|
492
560
|
`header` declares a `height`, an authored `spaceBefore` on the first item of
|
|
@@ -516,9 +584,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
516
584
|
than reporting against the report header, which is a band this rule never
|
|
517
585
|
covers.
|
|
518
586
|
|
|
519
|
-
##
|
|
587
|
+
## 0.5.0
|
|
520
588
|
|
|
521
|
-
###
|
|
589
|
+
### Minor Changes
|
|
522
590
|
|
|
523
591
|
- **`STYLE_NAMES`**, every name in the style vocabulary as a read-only list,
|
|
524
592
|
in the order the specification's table lists them. A tool that offers the
|
|
@@ -540,9 +608,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
540
608
|
on a band image, whose boxes have no such slack. A row's or a split's layers
|
|
541
609
|
under its cells' or slots' own. Undeclared is not a declaration: each target
|
|
542
610
|
keeps its own default.
|
|
543
|
-
|
|
544
|
-
### Changed
|
|
545
|
-
|
|
546
611
|
- **A table row's `style` now resolves onto that row's cells**, the box
|
|
547
612
|
included, instead of meaning something different on every output. Before,
|
|
548
613
|
padding and border on `detail.header`, `detail.row` or a total row's `style`
|
|
@@ -565,9 +630,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
565
630
|
carries only what layers by ordinary means, and a row whose whole block was
|
|
566
631
|
box carries no `style` at all.
|
|
567
632
|
|
|
568
|
-
##
|
|
633
|
+
## 0.4.0
|
|
569
634
|
|
|
570
|
-
###
|
|
635
|
+
### Minor Changes
|
|
571
636
|
|
|
572
637
|
- **`format: "date"` now reads a date string, not only a `Date`.** A JSON
|
|
573
638
|
document has no date type, so the kind could not be reached from parsed
|
|
@@ -591,9 +656,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
591
656
|
- **`typed(tokens, kind?)` takes the cell's `format` kind.** Passing `"date"`
|
|
592
657
|
opts into the same revival, for consumers that write typed cells. The
|
|
593
658
|
one-argument call is unchanged.
|
|
594
|
-
|
|
595
|
-
### Fixed
|
|
596
|
-
|
|
597
659
|
- **The TypeScript declarations accept the box.** The per-side `padding*` and
|
|
598
660
|
`border*` names, and the table's `detail.header`, were validated by the
|
|
599
661
|
engine from 0.3.0 but were missing from the shipped declarations, so a
|
|
@@ -608,9 +670,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
608
670
|
about rendering changes: a report that compiled through a cast produces the
|
|
609
671
|
same output without one.
|
|
610
672
|
|
|
611
|
-
##
|
|
673
|
+
## 0.3.0
|
|
612
674
|
|
|
613
|
-
###
|
|
675
|
+
### Minor Changes
|
|
614
676
|
|
|
615
677
|
- **`format` is a closed style name.** `number` / `currency` / `percent` /
|
|
616
678
|
`date` on text items, column cells, headers, and totals. Not images, not
|
|
@@ -618,19 +680,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
618
680
|
live on `quario({ locale, currency, timeZone })`. The public `format()`
|
|
619
681
|
helper is how targets present a kind; a kind on the wrong type contributes
|
|
620
682
|
nothing.
|
|
621
|
-
|
|
622
683
|
- **The report header may pin a `height` from the page top.** Dual-shaped
|
|
623
684
|
like `detail`: an item array, or `{ height, items }`. `page.margin` is a
|
|
624
685
|
document field (one number, all four sides), required with `height` and
|
|
625
686
|
legal without. Authored `spaceBefore` on the first occupying item of the
|
|
626
687
|
next band is refused.
|
|
627
|
-
|
|
628
688
|
- **`spaceBefore` / `spaceAfter` return as item flow spacing.** Blank space
|
|
629
689
|
before or after a band item, in points, including band images and splits as
|
|
630
690
|
band items. Adjacent gaps add. Table cells, `row.style`, headers, totals,
|
|
631
691
|
and split slots refuse the names. `spaceBefore` drops at a fresh body page
|
|
632
692
|
or strip top; page-band items keep it. Leading and inset stay cut.
|
|
633
|
-
|
|
634
693
|
- **Per-side padding and border on the closed style vocabulary.**
|
|
635
694
|
`paddingTop` / `Right` / `Bottom` / `Left` (points, ≥ 0) and, per side,
|
|
636
695
|
`border*Width`, `border*Style` (`solid` | `dashed` | `dotted`),
|
|
@@ -640,18 +699,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
640
699
|
than a solid black stroke. The names are legal wherever `background` is,
|
|
641
700
|
including images, plus `row.style`. The report default still takes only
|
|
642
701
|
`family` and `size`. Column `%` widths are border-box.
|
|
643
|
-
|
|
644
702
|
- **`detail.header` is the header-row box.** `{ style }` only, a different
|
|
645
703
|
path from `columns[i].header`. It crosses the seam as `table-start.style`.
|
|
646
|
-
|
|
647
|
-
### Changed
|
|
648
|
-
|
|
649
704
|
- **A table total is N rows.** Before, `total` was one row: a flat cell
|
|
650
705
|
array or `{ style, cells }`. After, it is absent or a non-empty array of
|
|
651
706
|
`{ cells, style?, visible? }`. A one-row total is `[{ cells: [...] }]`.
|
|
652
707
|
`total: []` is a definition error. Paths are `detail.total[r].cells[i]`.
|
|
653
708
|
The stream yields one `total-row` per emitted row.
|
|
654
|
-
|
|
655
709
|
- **A visible text item occupies a line at its own `size`.** Empty display
|
|
656
710
|
used to take the report default's leading in PDF and collapse in HTML;
|
|
657
711
|
`"a\n\nb"` broke in PDF and collapsed to a space in HTML. A visible item
|
|
@@ -660,9 +714,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
660
714
|
table cells stay contentless for height. This is not a spacing primitive;
|
|
661
715
|
`visible: false` is still how an item leaves the layout.
|
|
662
716
|
|
|
663
|
-
##
|
|
717
|
+
## 0.2.0
|
|
664
718
|
|
|
665
|
-
###
|
|
719
|
+
### Minor Changes
|
|
666
720
|
|
|
667
721
|
- **A report default: one `style` block for the whole document.** A top-level
|
|
668
722
|
`style` beside `header`/`detail`/`footer` states the typeface a report is set
|
|
@@ -672,21 +726,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
672
726
|
a text declaration on an image item is. Values are literals or `=`
|
|
673
727
|
expressions like any other style block's, resolved once per render in report
|
|
674
728
|
scope.
|
|
675
|
-
|
|
676
729
|
- **`report-start` carries the resolved report default as `style`.** A
|
|
677
730
|
report-level fact, never merged into an item's own `style`: an event's
|
|
678
731
|
`style` stays what the author wrote on that node, and a consumer composes the
|
|
679
732
|
default itself, once — under its own band-role defaults and under every
|
|
680
733
|
event's own style. Absent when the report declares none, so a consumer
|
|
681
734
|
written before this field renders in its own baseline exactly as it did.
|
|
682
|
-
|
|
683
735
|
- **An `uppercase` style declaration.** A boolean beside `bold` and `italic`,
|
|
684
736
|
literal or an `=` expression, for the capitalised column labels business
|
|
685
737
|
forms are usually set with. It is capitals, not small caps — real small caps
|
|
686
738
|
need a font feature the PDF target's built-in faces cannot supply, so the
|
|
687
739
|
declaration promises only what every target can draw. Image items keep
|
|
688
740
|
refusing it, as they refuse every text declaration.
|
|
689
|
-
|
|
690
741
|
- **A `split` item places values across the line instead of down the band.**
|
|
691
742
|
The invoice header's "seller left, customer right", which a band could not
|
|
692
743
|
say before. `{ "type": "split", "slots": [...] }` takes two or more slots,
|
|
@@ -697,13 +748,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
697
748
|
a slot that renders nothing keeps its width, so a line's geometry never
|
|
698
749
|
moves with the data. Splits may appear in every item array except table and
|
|
699
750
|
total cells.
|
|
700
|
-
|
|
701
751
|
- **`split-start` / `split-end` bracket a split's slots on the event stream.**
|
|
702
752
|
`split-start` carries the slot geometry, then one ordinary `item` or `image`
|
|
703
753
|
event per slot in order, then `split-end`. Existing consumers need no
|
|
704
754
|
change: the walk driver's missing-handler rule means a target that ignores
|
|
705
755
|
the bracket still receives the slot items and renders them stacked.
|
|
706
|
-
|
|
707
756
|
- **`quario().plan(schema, funcs?)` hands the whole traversal over at once.**
|
|
708
757
|
Returns `{ report, problems, anchors }` from one descent: the compiled
|
|
709
758
|
report (`null` while the document has problems), every problem as
|
|
@@ -727,12 +776,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
727
776
|
display rule behind the token join, re-exported beside `text()` so a stream
|
|
728
777
|
consumer that stringifies token values itself renders exactly what the
|
|
729
778
|
official targets render — Dates included.
|
|
730
|
-
|
|
731
779
|
- **`maxDepth: Infinity` opts a query budget out.** The data query's traversal
|
|
732
780
|
budgets accept an explicit `Infinity` per key for "this budget, unbounded".
|
|
733
781
|
The 500-deep default is unchanged — it is now padvinder's own, applied for
|
|
734
782
|
every consumer rather than added by quario at the seam.
|
|
735
|
-
|
|
736
783
|
- **Located data-query errors carry padvinder's code and span.** A `data`
|
|
737
784
|
query that does not parse now surfaces with padvinder's `code` and
|
|
738
785
|
`start`/`end` offsets into the query you wrote — filter faults included —
|
|
@@ -742,23 +789,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
742
789
|
`PADVINDER_UNKNOWN_FUNCTION`, or `PADVINDER_SYNTAX` for a path character or
|
|
743
790
|
filter body that is open-endedly not a query. Traversal budgets exceeded at
|
|
744
791
|
render time keep `limit`/`actual` and carry no span.
|
|
745
|
-
|
|
746
|
-
### Changed
|
|
747
|
-
|
|
748
792
|
- **Cells render straight over the engine scope chain.** A text cell no longer
|
|
749
793
|
allocates a wrapper scope and an anchor pair per cell per row — quario's
|
|
750
794
|
chain already binds `$` at the render base and `@` on the detail row, and
|
|
751
795
|
sjabloon now renders over it as-is. A 4-column stream over a million rows
|
|
752
796
|
went from 1.7s to 0.5s. No report changes what it renders. Requires
|
|
753
797
|
sjabloon 0.11.
|
|
754
|
-
|
|
755
798
|
- **A compiled report's `functions` carry signatures.** Each entry is now
|
|
756
799
|
`{ name, arity, doc? }` instead of a bare name — `arity` from the function's
|
|
757
800
|
declared parameter count (or its own numeric `arity` where rest parameters
|
|
758
801
|
mislead `length`), `doc` from an own `doc` string when it carries one — in
|
|
759
802
|
the same call-first-seen order. `names` is unchanged. Requires xprsn 0.11
|
|
760
803
|
and sjabloon 0.11.
|
|
761
|
-
|
|
762
804
|
- **A bare `Date` renders as ISO 8601 UTC, the same on every machine.**
|
|
763
805
|
Display text for a `Date` value was `String(date)`, which bakes the
|
|
764
806
|
rendering host's timezone and locale into the output — so one report
|
|
@@ -767,7 +809,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
767
809
|
rule; an invalid `Date` keeps its deterministic `Invalid Date` text.
|
|
768
810
|
Reports that want a formatted date keep using a registered function,
|
|
769
811
|
exactly as before.
|
|
770
|
-
|
|
771
812
|
- **A bad literal pattern in the data query is a definition error.** A typo'd
|
|
772
813
|
I-Regexp written as a string literal in `match()`/`search()` used to
|
|
773
814
|
produce a plausible empty report with no signal; `report()` and
|
|
@@ -779,7 +820,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
779
820
|
fault in an expression: the engine that decided the fault names it. A
|
|
780
821
|
pattern that arrives from render data keeps RFC 9535 semantics and still
|
|
781
822
|
matches nothing at render time. Requires padvinder 0.8.
|
|
782
|
-
|
|
783
823
|
- **Each engine relocates its own diagnostic.** A located error is now a copy
|
|
784
824
|
made by the engine that raised it (xprsn, sjabloon, or padvinder), so it
|
|
785
825
|
carries every field that engine puts on a diagnostic — nothing is lost in
|
|
@@ -787,9 +827,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
787
827
|
passes that engine's own `isDiagnostic`. Host errors are wrapped as plain
|
|
788
828
|
errors with no diagnostic metadata, exactly as before. Requires xprsn 0.10,
|
|
789
829
|
sjabloon 0.9, and padvinder 0.5. No report changes what it renders.
|
|
790
|
-
|
|
791
|
-
### Fixed
|
|
792
|
-
|
|
793
830
|
- **A host error cannot pose as the report's diagnostic.** `report()` rethrows
|
|
794
831
|
the first definition problem's located engine error; it chose that error by
|
|
795
832
|
probing for a `code` property, so a host error class that stamps `code` on
|
|
@@ -798,9 +835,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
798
835
|
`isDiagnostic`; everything else falls back to a plain `SyntaxError` naming
|
|
799
836
|
the first problem, as before.
|
|
800
837
|
|
|
801
|
-
##
|
|
838
|
+
## 0.1.0
|
|
802
839
|
|
|
803
|
-
###
|
|
840
|
+
### Minor Changes
|
|
804
841
|
|
|
805
842
|
- **A compile-once report engine.** You hand it a JSON report definition and
|
|
806
843
|
data; it plans once and streams render events that every target consumes.
|
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
|
|
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
|
|
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
|
|
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` |
|
|
@@ -98,19 +98,28 @@ row, one `total-row` per emitted total row, and `table-end`.
|
|
|
98
98
|
| `table-end` | - |
|
|
99
99
|
| `report-end` | - |
|
|
100
100
|
|
|
101
|
+
The full field semantics are in the [event stream reference](https://getquario.com/docs/reference/events/).
|
|
102
|
+
|
|
101
103
|
**Cells carry tokens.** A cell is `{ tokens, style? }`. Each token is either `{ literal }`
|
|
102
104
|
(static template text, verbatim) or `{ value }` (one interpolation's _pre-format_ value, the
|
|
103
105
|
expression result before any stringification). This is the typed seam: a cell whose template is `{{ @.amount }}` holds one value token with the
|
|
104
106
|
number itself, so a spreadsheet consumer writes a real numeric cell. `Total: {{ @.amount }}` mixes
|
|
105
107
|
a literal and a value, so the cell is text only.
|
|
106
108
|
|
|
109
|
+
**A page-number token names itself.** A value token whose interpolation is exactly
|
|
110
|
+
`{{ page.number }}` or `{{ page.total }}` also carries `field`, holding that same string. The
|
|
111
|
+
value is the number the render used for this page. `field` says which page value the token stands
|
|
112
|
+
for, so a target whose own document format numbers pages can write its own live field there
|
|
113
|
+
instead. Anything computed from them carries no `field`.
|
|
114
|
+
|
|
107
115
|
**Escaping is the consumer's job.** A target that embeds values in markup must escape them at its
|
|
108
116
|
own edge.
|
|
109
117
|
|
|
110
|
-
**`report-start.marking`** carries the evaluation wording when the render
|
|
111
|
-
verification is still settling. Licensed streams omit it. Targets place the marking
|
|
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
|
|
112
120
|
author its wording. **`columns`** on `report-start` / `group-start` is the declared
|
|
113
|
-
page column
|
|
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.
|
|
114
123
|
xlsx never will.
|
|
115
124
|
|
|
116
125
|
### `text(tokens)`
|
|
@@ -124,7 +133,7 @@ Exactly one value token holding a finite number, a boolean, or a valid `Date` ke
|
|
|
124
133
|
pre-stringify value. Anything else, including a lone null, reports `undefined` and joins to
|
|
125
134
|
display text. Passing the cell's resolved `format` kind opts into the seam's one coercion: under
|
|
126
135
|
`"date"`, an RFC 3339 string revives to the `Date` it names. Spreadsheet consumers use this for
|
|
127
|
-
real numeric cells
|
|
136
|
+
real numeric cells.
|
|
128
137
|
[`@quario/csv`](https://www.npmjs.com/package/@quario/csv) is the short form.
|
|
129
138
|
|
|
130
139
|
### `styledRuns(tokens)`
|
|
@@ -138,12 +147,12 @@ target cannot drift from them.
|
|
|
138
147
|
### `walk(events, handlers)` / `breathe()`
|
|
139
148
|
|
|
140
149
|
`walk` is the delivery driver every official target uses. Pass one render's event iterable and
|
|
141
|
-
per-event handlers keyed by type
|
|
142
|
-
the loop
|
|
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
|
|
143
152
|
target can settle `report-start` (page bands, marking) there instead of draining the stream
|
|
144
153
|
itself.
|
|
145
154
|
|
|
146
|
-
`breathe()` is that
|
|
155
|
+
`breathe()` is that return alone. Await it between batches of a loop you own. `walk` already
|
|
147
156
|
calls it for you.
|
|
148
157
|
|
|
149
158
|
### Presentation helpers
|
|
@@ -154,11 +163,11 @@ cell the same way:
|
|
|
154
163
|
- `display(value)` — the scalar rule `text()` joins with: a `Date` as ISO 8601 UTC, nullish as
|
|
155
164
|
the empty string, everything else `String(value)`.
|
|
156
165
|
- `format(value, style?, options?)` — presents a token under the cell's resolved `format`
|
|
157
|
-
declaration (its kind and modifier, and for `currency` the cell's own code)
|
|
158
|
-
`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()`.
|
|
159
168
|
- `fractionDigits(style?, options?)` — the digit count a resolved `format` declaration presents:
|
|
160
169
|
the kind's own (two for `number` and `percent`, a currency's minor units for `currency`) unless
|
|
161
|
-
the declaration's `digits` overrides it
|
|
170
|
+
the declaration's `digits` overrides it. It answers `undefined` where there is no count.
|
|
162
171
|
- `currencyOf(style?, options?)` — which code a money cell wears: its own, else the instance's.
|
|
163
172
|
- `isReportBand(role)` — whether a role names one of the report's own bands rather than a
|
|
164
173
|
group's.
|
|
@@ -167,8 +176,8 @@ cell the same way:
|
|
|
167
176
|
|
|
168
177
|
### `validate(schema, functions?)`
|
|
169
178
|
|
|
170
|
-
|
|
171
|
-
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.
|
|
172
181
|
|
|
173
182
|
```js
|
|
174
183
|
validate({ data: "$.o[*]", sort: [{ by: "=@.a", dir: "up" }] });
|
|
@@ -193,7 +202,7 @@ is not a problem at a lower severity, which is why it has no `diagnostic` — no
|
|
|
193
202
|
engine decided. Neither warning below locates into an authored source, so none carries a `source`
|
|
194
203
|
today. **A warning is never fatal**: a document carrying only warnings compiles and
|
|
195
204
|
renders, so `report` is `null` on `problems` alone. Two declarations warn today — a `currency` on
|
|
196
|
-
a cell whose `format` is not `"currency"`, and a table where every column
|
|
205
|
+
a cell whose `format` is not `"currency"`, and a table where the author sized every column and the widths
|
|
197
206
|
total under 100 — and the list is advisory and deliberately incomplete, so a quiet one is not a
|
|
198
207
|
promise that every declaration will be read. `validate()` returns problems only.
|
|
199
208
|
|
|
@@ -208,10 +217,10 @@ else console.error(problems[0].path, problems[0].message);
|
|
|
208
217
|
|
|
209
218
|
True when a caught value is a **located diagnostic**: an error xprsn, sjabloon or padvinder
|
|
210
219
|
minted — thrown by that engine, or re-thrown by quario with the engine original behind it.
|
|
211
|
-
Authentication
|
|
220
|
+
Authentication tests identity. An error that only matches the shape does not pass.
|
|
212
221
|
|
|
213
|
-
|
|
214
|
-
function's own throw
|
|
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
|
|
215
224
|
name the path they failed at — and the offending source, where there is one — while keeping their
|
|
216
225
|
original type (`SyntaxError`, `TypeError`, `RangeError`). What a diagnostic adds on top is
|
|
217
226
|
metadata an engine vouches for: `code`, `start`/`end` offsets, and, for a query budget in place
|
|
@@ -237,7 +246,7 @@ try {
|
|
|
237
246
|
| `run.<name>` | Running accumulator values on the current detail row |
|
|
238
247
|
|
|
239
248
|
Each anchor is a distinct object. Absent reads are `null`, so `x == null` holds for a missing
|
|
240
|
-
field
|
|
249
|
+
field. Reading _through_ a null base still throws, so use `?.`.
|
|
241
250
|
|
|
242
251
|
## Options
|
|
243
252
|
|
|
@@ -245,18 +254,19 @@ field; reading _through_ a null base still throws, so use `?.`.
|
|
|
245
254
|
quario({ query: { maxNodes: 10_000, maxDepth: 64, maxResults: 1_000 } }).report(schema, functions);
|
|
246
255
|
```
|
|
247
256
|
|
|
248
|
-
`query` bounds the JSONPath data selection. Hosts set budgets through this API argument
|
|
249
|
-
|
|
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
|
|
250
259
|
carry `code`, `limit`, and `actual`. Every render starts with fresh counters.
|
|
251
260
|
|
|
252
261
|
## Writing a render target
|
|
253
262
|
|
|
254
263
|
Read the stream through the public API. Do not reach for engine internals. A complete, tested
|
|
255
264
|
Markdown target lives in the repository at `example/markdown.js` in about 70 lines. The built-in
|
|
256
|
-
targets
|
|
265
|
+
targets use the same public API.
|
|
257
266
|
|
|
258
267
|
Two rules a target owes its users: escape or neutralize every `value` token at your own edge, and
|
|
259
|
-
map the style vocabulary to your own
|
|
268
|
+
map the [style vocabulary](https://getquario.com/docs/reference/style-declarations/) to your own
|
|
269
|
+
formatting model rather than expecting CSS.
|
|
260
270
|
|
|
261
271
|
The stream is additive. The walk driver skips any event you register no handler for, so a target
|
|
262
272
|
that ignores a newer event (for example, `example/markdown.js` has none for `image`) keeps rendering
|
|
@@ -272,12 +282,19 @@ package, so it runs under a script policy that omits `unsafe-eval`. The test sui
|
|
|
272
282
|
under Node's `--disallow-code-generation-from-strings` flag, a source scan, and a Playwright
|
|
273
283
|
harness that loads the published files under a strict CSP.
|
|
274
284
|
|
|
285
|
+
## Documentation
|
|
286
|
+
|
|
287
|
+
[The quario documentation](https://getquario.com/docs/) is the reference.
|
|
288
|
+
The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
|
|
289
|
+
specification of what a report may declare, and
|
|
290
|
+
the [engine reference](https://getquario.com/docs/reference/quario/) is this package's own API.
|
|
291
|
+
|
|
275
292
|
## License
|
|
276
293
|
|
|
277
|
-
Commercial software with readable source.
|
|
278
|
-
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.
|
|
279
296
|
|
|
280
|
-
Pass your license key in the options
|
|
297
|
+
Pass your license key in the options. quario verifies it offline:
|
|
281
298
|
|
|
282
299
|
```js
|
|
283
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/image.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the engine reads out of an image item's bytes: which format they are,
|
|
3
|
+
* and how big the picture is. Read, never decoded -- a magic number, a PNG's
|
|
4
|
+
* IHDR and a JPEG's frame header, and nothing here looks at a pixel.
|
|
5
|
+
*
|
|
6
|
+
* Both answers ride on the image event, so no target reads these headers
|
|
7
|
+
* again. That is the whole of `docs/adr/0067`: two targets used to keep a
|
|
8
|
+
* parser each, in different units and with the same bug, and the engine was
|
|
9
|
+
* already in these bytes to name the format.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// The two raster formats an image item may carry, as the bytes announce
|
|
13
|
+
// themselves: PNG's 8-byte signature and JPEG's start-of-image marker. Read,
|
|
14
|
+
// never decoded -- the engine answers which format the bytes are, not what
|
|
15
|
+
// they depict.
|
|
16
|
+
let PNG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
17
|
+
let JPEG = [0xff, 0xd8, 0xff];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The image seam: name the format a `source` expression's bytes hold, or null
|
|
21
|
+
* when they are not image bytes at all -- including when they are not bytes.
|
|
22
|
+
* The traversal sniffs once per image event and the answer rides on the event,
|
|
23
|
+
* so no consumer repeats it.
|
|
24
|
+
*
|
|
25
|
+
* @param {any} bytes The value a `source` expression yielded.
|
|
26
|
+
* @returns {"png" | "jpeg" | null} The format, or null when unreadable.
|
|
27
|
+
*/
|
|
28
|
+
export function sniff(bytes) {
|
|
29
|
+
if (!(bytes instanceof Uint8Array)) return null;
|
|
30
|
+
if (PNG.every((byte, i) => bytes[i] === byte)) return "png";
|
|
31
|
+
if (JPEG.every((byte, i) => bytes[i] === byte)) return "jpeg";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @type {(bytes: Uint8Array, at: number) => number} */
|
|
36
|
+
let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
|
|
37
|
+
|
|
38
|
+
// A PNG states each dimension in four bytes, so two words make one number.
|
|
39
|
+
/** @type {(bytes: Uint8Array, at: number) => number} */
|
|
40
|
+
let long = (bytes, at) => word(bytes, at) * 0x10000 + word(bytes, at + 2);
|
|
41
|
+
|
|
42
|
+
// The three codes in C0..CF that are not frame headers: DHT, the JPG
|
|
43
|
+
// extension, and DAC. Named as a set so the frame test is one range and one
|
|
44
|
+
// lookup rather than a chain of exclusions.
|
|
45
|
+
let NOT_FRAME = new Set([0xc4, 0xc8, 0xcc]);
|
|
46
|
+
|
|
47
|
+
// The dimensions live in the frame header, which is the first SOFn marker.
|
|
48
|
+
// SOF2 is a frame like SOF0, so a progressive file is read like a baseline one.
|
|
49
|
+
/** @type {(code: number) => boolean} */
|
|
50
|
+
let isFrame = (code) => code >= 0xc0 && code <= 0xcf && !NOT_FRAME.has(code);
|
|
51
|
+
|
|
52
|
+
// A PNG carries the two numbers in its IHDR at a fixed offset.
|
|
53
|
+
/** @type {(bytes: Uint8Array) => { width: number, height: number } } */
|
|
54
|
+
let pngSize = (bytes) => ({ width: long(bytes, 16), height: long(bytes, 20) });
|
|
55
|
+
|
|
56
|
+
/** @type {(bytes: Uint8Array) => { width: number, height: number } } */
|
|
57
|
+
let jpegSize = (bytes) => {
|
|
58
|
+
for (let at = 2; at + 9 < bytes.length; at += 2 + word(bytes, at + 2)) {
|
|
59
|
+
if (bytes[at] !== 0xff) break;
|
|
60
|
+
if (isFrame(bytes[at + 1])) return { width: word(bytes, at + 7), height: word(bytes, at + 5) };
|
|
61
|
+
}
|
|
62
|
+
return { width: 0, height: 0 };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// One reader per format `sniff` names.
|
|
66
|
+
/** @type {Record<string, (bytes: Uint8Array) => { width: number, height: number }>} */
|
|
67
|
+
let READERS = { png: pngSize, jpeg: jpegSize };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The image's own size in pixels, or null when the header does not carry one
|
|
71
|
+
* -- bytes whose magic numbers `sniff` vouched for, truncated before the
|
|
72
|
+
* dimensions. Read, never decoded: a PNG's IHDR and a JPEG's frame header
|
|
73
|
+
* carry the two numbers, and nothing here looks at a pixel.
|
|
74
|
+
*
|
|
75
|
+
* The size rides on the image event beside the format, so every target places
|
|
76
|
+
* a picture from a field rather than parsing the same two headers again, and a
|
|
77
|
+
* file too short to state its size fails once, in the engine, for every target
|
|
78
|
+
* alike (`docs/adr/0067`).
|
|
79
|
+
*
|
|
80
|
+
* @param {Uint8Array} bytes The image file.
|
|
81
|
+
* @param {"png" | "jpeg"} format The format `sniff` named.
|
|
82
|
+
* @returns {{ width: number, height: number } | null} The size, if readable.
|
|
83
|
+
*/
|
|
84
|
+
export function dimensions(bytes, format) {
|
|
85
|
+
// Keyed rather than a ternary so the format list lives in one place: a
|
|
86
|
+
// format `sniff` learns later and this table does not would throw here
|
|
87
|
+
// rather than be read silently as the one on the ternary's else branch.
|
|
88
|
+
let size = READERS[format](bytes);
|
|
89
|
+
// A zero is how either reader says "the header stopped before this" -- an
|
|
90
|
+
// absent byte reads as zero, and no image is zero wide.
|
|
91
|
+
return size.width > 0 && size.height > 0 ? size : null;
|
|
92
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -506,8 +506,20 @@ export interface TokenStyle {
|
|
|
506
506
|
/** One static text run of a cell's template, verbatim (sjabloon's literal token). */
|
|
507
507
|
export type LiteralToken = SjabloonLiteralToken & TokenStyle;
|
|
508
508
|
|
|
509
|
-
/**
|
|
510
|
-
export type
|
|
509
|
+
/** Which page value a token *is*, on the two whose interpolation says so. */
|
|
510
|
+
export type PageField = "page.number" | "page.total";
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* One interpolation's pre-stringify value (sjabloon's value token).
|
|
514
|
+
*
|
|
515
|
+
* `field` is present where the interpolation is exactly `{{ page.number }}` or
|
|
516
|
+
* `{{ page.total }}`. The value is unchanged -- the number this render saw --
|
|
517
|
+
* and `field` says which page value the token stands for, so a target whose
|
|
518
|
+
* own document format numbers pages writes a live field there instead
|
|
519
|
+
* (SCHEMA.md, "Page bands"). Anything computed from a page value carries no
|
|
520
|
+
* `field`, and a target that paginates itself, or not at all, ignores the key.
|
|
521
|
+
*/
|
|
522
|
+
export type ValueToken = SjabloonValueToken & TokenStyle & { field?: PageField };
|
|
511
523
|
|
|
512
524
|
export type Token = LiteralToken | ValueToken;
|
|
513
525
|
|
|
@@ -629,6 +641,14 @@ export interface ImageEvent {
|
|
|
629
641
|
bytes: Uint8Array;
|
|
630
642
|
/** Sniffed from the bytes' magic numbers, so no consumer repeats it. */
|
|
631
643
|
format: ImageFormat;
|
|
644
|
+
/**
|
|
645
|
+
* The image's own width in pixels, read from the same header. Bytes too
|
|
646
|
+
* short to state a size never reach a target: that is a render error on
|
|
647
|
+
* every target alike.
|
|
648
|
+
*/
|
|
649
|
+
width: number;
|
|
650
|
+
/** The image's own height in pixels, read from the same header. */
|
|
651
|
+
height: number;
|
|
632
652
|
fit: ImageFit;
|
|
633
653
|
/** The rendered `alt` template, present when the item declares one. */
|
|
634
654
|
alt?: Token[];
|
|
@@ -900,6 +920,33 @@ export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
|
|
|
900
920
|
*/
|
|
901
921
|
export function imageError(path: string | undefined, said: string, cause?: unknown): Error;
|
|
902
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
|
+
|
|
903
950
|
/**
|
|
904
951
|
* Join a token stream to display text: literals verbatim, values through
|
|
905
952
|
* `display()`. Re-exported from sjabloon.
|
package/lib/index.js
CHANGED
package/lib/license.js
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
// Verification is entirely offline, and asynchronous only because WebCrypto
|
|
6
6
|
// is.
|
|
7
7
|
|
|
8
|
-
// Release date of this version, written into the
|
|
9
|
-
// release-
|
|
10
|
-
// spelling that `scripts/license/keygen.mjs` matches on,
|
|
11
|
-
// reassigns them. A key is valid for every version released inside its window
|
|
8
|
+
// Release date of this version, written into the Version PR's commit by
|
|
9
|
+
// `scripts/stamp-release-date.mjs` (the `release-date` mark). Both constants
|
|
10
|
+
// keep the `let` spelling that `scripts/license/keygen.mjs` matches on,
|
|
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-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
|
-
"
|
|
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/lib/plan.js
CHANGED
|
@@ -51,7 +51,8 @@ import { ANCHORS, BLOCKED, NAME, RESERVED, record } from "./names.js";
|
|
|
51
51
|
import { MATH } from "./math.js";
|
|
52
52
|
import { REDUCERS, reducerFor } from "./reducers.js";
|
|
53
53
|
import { aggregateValue, withRow } from "./scope.js";
|
|
54
|
-
import {
|
|
54
|
+
import { dimensions, sniff } from "./image.js";
|
|
55
|
+
import { oneValue, opt, sameStyle } from "./stream.js";
|
|
55
56
|
import {
|
|
56
57
|
checkBorderSides,
|
|
57
58
|
checkCellStyle,
|
|
@@ -482,10 +483,28 @@ let runProp = (expression, path, source) => (scope) => {
|
|
|
482
483
|
locate(path, source, error, expression.isDiagnostic(error) && relocateXprsn, 1);
|
|
483
484
|
}
|
|
484
485
|
};
|
|
486
|
+
// The [live field](../../../CONTEXT.md#live-field): which page value a token
|
|
487
|
+
// *is*, for a target whose own format numbers pages. sjabloon offers each
|
|
488
|
+
// interpolation's expression source once, while compiling, which is the only
|
|
489
|
+
// place the words the author wrote still exist -- a rendered token carries the
|
|
490
|
+
// number and nothing about where it came from. Reading the source here instead
|
|
491
|
+
// is what hard constraint 2 forbids, and a sentinel bound to `page.number` was
|
|
492
|
+
// ruled out because xprsn's `==` is strict: it would break `=page.number == 1`.
|
|
493
|
+
//
|
|
494
|
+
// One table for the whole compile: the answer depends on the expression alone,
|
|
495
|
+
// never on where the cell sits. A Map rather than a lookup object, because the
|
|
496
|
+
// key is author source and nothing reached through `Object.prototype` is an
|
|
497
|
+
// answer to this question; each name is written once and is its own answer,
|
|
498
|
+
// frozen because every cell that says it shares the one object.
|
|
499
|
+
const PAGE_FIELDS = /** @type {Map<string, { field: import("./index.js").PageField }>} */ (
|
|
500
|
+
new Map(["page.number", "page.total"].map((field) => [field, Object.freeze({ field })]))
|
|
501
|
+
);
|
|
502
|
+
/** @type {(expr: string) => { field: import("./index.js").PageField } | undefined} */
|
|
503
|
+
let pageField = (expr) => PAGE_FIELDS.get(expr);
|
|
485
504
|
/** @type {(src: string, path: string, source: any, FNS: any, BND: Set<string>) => any} */
|
|
486
505
|
let compileCell = (src, path, source, FNS, BND) => {
|
|
487
506
|
try {
|
|
488
|
-
return template(src, FNS, { bound: BND });
|
|
507
|
+
return template(src, FNS, { bound: BND, tag: pageField });
|
|
489
508
|
} catch (error) {
|
|
490
509
|
locate(path, source, error, isTemplateDiagnostic(error) && relocateTemplate);
|
|
491
510
|
}
|
|
@@ -1326,28 +1345,35 @@ let nodes = (
|
|
|
1326
1345
|
let altOf = (def, path) => (def.alt != null ? textValue(def.alt, path + ".alt") : null);
|
|
1327
1346
|
/** @type {(alt: any, scope: Scope) => any} */
|
|
1328
1347
|
let imageAlt = (alt, scope) => alt && alt(scope);
|
|
1348
|
+
// The two places `locate` originates an error instead of re-throwing a
|
|
1349
|
+
// caught one: each fault is quario's own verdict on the bytes, so it carries
|
|
1350
|
+
// the location every other render error does and, having no engine
|
|
1351
|
+
// diagnostic behind it, stays out of `isDiagnostic`.
|
|
1352
|
+
/** @type {(fields: any, message: string) => never} */
|
|
1353
|
+
let badImage = (fields, message) =>
|
|
1354
|
+
locate(fields.path + ".source", fields.sourceSpec, TypeError(message));
|
|
1329
1355
|
/** @type {(fields: any, scope: Scope, bytes: any) => any} */
|
|
1330
1356
|
let imageEvent = (fields, scope, bytes) => {
|
|
1331
|
-
// The bytes stay bytes.
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1357
|
+
// The bytes stay bytes. Reading them touches the magic numbers and the
|
|
1358
|
+
// header the size lives in -- never a pixel, and nothing here turns them
|
|
1359
|
+
// into a string -- and both answers ride on the event so no target reads
|
|
1360
|
+
// the same header a second time.
|
|
1334
1361
|
let format = sniff(bytes);
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
fields.sourceSpec,
|
|
1343
|
-
TypeError("expected a Uint8Array of PNG or JPEG bytes"),
|
|
1344
|
-
);
|
|
1362
|
+
if (!format) badImage(fields, "expected a Uint8Array of PNG or JPEG bytes");
|
|
1363
|
+
let size = dimensions(bytes, format);
|
|
1364
|
+
// The one thing the engine reads past the magic numbers, and so the one
|
|
1365
|
+
// header failure every target shares: a file that cannot state its size
|
|
1366
|
+
// can be placed by none of them (`docs/adr/0067`). Past the size the
|
|
1367
|
+
// engine still vouches for nothing.
|
|
1368
|
+
if (!size) badImage(fields, "could not read the image's size from its bytes");
|
|
1345
1369
|
return opt(
|
|
1346
1370
|
{
|
|
1347
1371
|
type: "image",
|
|
1348
1372
|
role: fields.role,
|
|
1349
1373
|
bytes,
|
|
1350
1374
|
format,
|
|
1375
|
+
width: size.width,
|
|
1376
|
+
height: size.height,
|
|
1351
1377
|
fit: fields.fit,
|
|
1352
1378
|
path: fields.path,
|
|
1353
1379
|
},
|
package/lib/stream.js
CHANGED
|
@@ -207,29 +207,6 @@ export function isReportBand(role) {
|
|
|
207
207
|
return REPORT_BANDS.has(role);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
// The two raster formats an image item may carry, as the bytes announce
|
|
211
|
-
// themselves: PNG's 8-byte signature and JPEG's start-of-image marker. Read,
|
|
212
|
-
// never decoded -- the engine answers which format the bytes are, not what
|
|
213
|
-
// they depict.
|
|
214
|
-
let PNG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
215
|
-
let JPEG = [0xff, 0xd8, 0xff];
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* The image seam: name the format a `source` expression's bytes hold, or null
|
|
219
|
-
* when they are not image bytes at all -- including when they are not bytes.
|
|
220
|
-
* The traversal sniffs once per image event and the answer rides on the event,
|
|
221
|
-
* so no consumer repeats it.
|
|
222
|
-
*
|
|
223
|
-
* @param {any} bytes The value a `source` expression yielded.
|
|
224
|
-
* @returns {"png" | "jpeg" | null} The format, or null when unreadable.
|
|
225
|
-
*/
|
|
226
|
-
export function sniff(bytes) {
|
|
227
|
-
if (!(bytes instanceof Uint8Array)) return null;
|
|
228
|
-
if (PNG.every((byte, i) => bytes[i] === byte)) return "png";
|
|
229
|
-
if (JPEG.every((byte, i) => bytes[i] === byte)) return "jpeg";
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
210
|
// The cooperative hand-back's plumbing. A timer is the slowest way a runtime will
|
|
234
211
|
// hand the loop back and, until this landed, the only one quario used: measured
|
|
235
212
|
// on an M-series Mac it costs ~1.20ms per hand-back against ~0.015ms for
|
package/lib/style.js
CHANGED
|
@@ -375,7 +375,8 @@ export let checkRunStyle = narrowed(RUN_STYLES, "a styled run");
|
|
|
375
375
|
export let checkSlotStyle = withoutFlow(checkStyle, "a split slot");
|
|
376
376
|
// `valign` is legal exactly where a box has height it did not ask for
|
|
377
377
|
// (CONTEXT.md "Box": slack). A slot's box is the split's height, so a slot --
|
|
378
|
-
// text or image -- reads it; a band image's box is its
|
|
378
|
+
// text or image -- reads it; a band image's box is exactly its picture's
|
|
379
|
+
// height, so it does not.
|
|
379
380
|
// That is the one name an image accepts in a slot and nowhere else, the same
|
|
380
381
|
// shape of reason `width` reaches an item only there (docs/adr/0029).
|
|
381
382
|
let SLOT_IMAGE_STYLES = [...IMAGE_STYLES, "valign"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quario",
|
|
3
|
-
"version": "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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"padvinder": "^0.9.0",
|
|
37
|
-
"sjabloon": "^0.
|
|
37
|
+
"sjabloon": "^0.13.0",
|
|
38
38
|
"xprsn": "^0.11.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|