@quario/xlsx 0.7.0 → 0.8.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,15 +1,74 @@
1
- # Changelog
1
+ # @quario/xlsx
2
2
 
3
- All notable changes to @quario/xlsx are documented in this file.
3
+ ## 0.8.0
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ ### Minor Changes
7
6
 
8
- ## [Unreleased]
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.
9
14
 
10
- ## [0.7.0] - 2026-09-07
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.
11
18
 
12
- ### Added
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
+ - **`meta` is now read once, at the factory call.** It was read inside the render, so a host that
44
+ mutated its options object between renders got a different workbook from the same configured
45
+ target. The documented behaviour was always the factory call. If you were mutating an options
46
+ object to change the document properties, build a new target instead.
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies
51
+ - quario@0.9.0
52
+
53
+ ## 0.7.1
54
+
55
+ ### Patch Changes
56
+
57
+ - **Every published README says where the documentation is.** Each package now
58
+ carries a Documentation section pointing at the reference, at the report schema
59
+ that normatively specifies what a report may declare, and at the package's own
60
+ API. The paragraphs that used to end on an unstated contract — the event
61
+ stream's field semantics, the style vocabulary, page columns, the Content
62
+ Security Policy a fragment with images needs, the formula mangling, and each
63
+ target's own contract — link the page that states it. Every link is an absolute
64
+ URL, so it resolves from the npm package page as readily as from an installed
65
+ copy.
66
+ - Updated dependencies
67
+ - quario@0.8.0
68
+
69
+ ## 0.7.0
70
+
71
+ ### Minor Changes
13
72
 
14
73
  - **Styled runs become rich text.** A cell of more than one styled run is written
15
74
  as rich text, each run's font merged over the cell's — so bold, italic,
@@ -18,14 +77,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
77
  only, and filling the cell would colour text nobody asked to colour. A cell of
19
78
  exactly one run stays a typed cell, so a single styled amount is still a
20
79
  number.
21
-
22
80
  - **A run inside a sentence presents its value.** A run holding one
23
81
  interpolation under a `format` shows the same decimals here as on the page,
24
82
  where a mixed cell used to show the bare value.
25
83
 
26
- ## [0.6.0] - 2026-09-07
84
+ ## 0.6.0
27
85
 
28
- ### Added
86
+ ### Minor Changes
29
87
 
30
88
  - **A date cell's number format follows the `form` its report declares.** The
31
89
  four forms map to `dd/mm/yy`, `dd mmm yyyy`, `dd mmmm yyyy` and
@@ -33,26 +91,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
91
  weekday name is spelled in stays the reader's, because that is what a
34
92
  spreadsheet application supplies and pinning it would stop the file reading
35
93
  naturally for whoever opens it.
36
-
37
94
  - **A declared digit count builds the pattern.** A `number` cell asking for
38
95
  three digits writes `#,##0.000`, and a `currency` cell asking for none writes
39
96
  `"EUR"#,##0` — the same count the page presents, taken off the declaration
40
97
  the engine resolved rather than computed here.
41
-
42
98
  - **A cell's own `currency` code picks the number format**, ahead of the
43
99
  instance's default: `"JPY"#,##0` where the row says `JPY`, since the code
44
100
  decides the digits too. The cell stays a number either way. A code the engine
45
101
  could not accept writes no number format at all, rather than labelling the
46
102
  cell in the instance's currency.
47
-
48
- ### Changed
49
-
50
103
  - **A `date` cell with no declared form now writes `dd mmm yyyy`, not
51
104
  `yyyy-mm-dd`.** A bare `date` means the `medium` form throughout, so the grid
52
105
  agrees with the page. A report that wants the ISO shape has no form for it;
53
106
  the nearest is `{ "kind": "date", "form": "short" }`, which writes
54
107
  `dd/mm/yy`. The cell is still a real typed date either way.
55
-
56
108
  - **A cell's number format now comes from the engine's fraction-digit count
57
109
  rather than from four patterns this target held of its own.** The visible
58
110
  change is `currency`: the pattern followed the currency's own minor units,
@@ -61,7 +113,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
113
  (`#,##0.00`), `percent` (`0.00%`) and `date` (`yyyy-mm-dd`) are unchanged in
62
114
  a worksheet; what changed for those is that HTML and PDF now show the same
63
115
  decimals the grid always did, instead of their own.
64
-
65
116
  - A `currency` code that is not a readable currency no longer produces a number
66
117
  format at all. The cell keeps its value; before, the code was pasted straight
67
118
  into a pattern while every other target fell back to the unformatted value.
@@ -70,39 +121,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
121
  position still come from the application that opens the file, which is what
71
122
  lets one workbook read naturally wherever it is opened.
72
123
 
73
- ### Fixed
74
-
75
124
  - A `format` kind reaching this target from an `=` expression is now looked up
76
125
  as an own key. Before, a kind resolving to the name of a built-in object
77
126
  member threw out of the render (`valueOf`, `hasOwnProperty`) or wrote
78
127
  `[object Undefined]` into the cell's number format (`toString`). A literal
79
128
  was never affected: it is checked against the four kinds.
80
-
81
- ### Fixed
82
-
83
129
  - **An image whose size cannot be read now names the item that asked for it.**
84
130
  The failure said only that the size could not be read from the bytes, so a
85
131
  report with two pictures gave no way to tell which one was bad. The message
86
132
  now begins with the item's `source` path, as every other render error does
87
133
  and as it does from every other target.
88
134
 
89
- ## [0.5.0] - 2026-09-05
135
+ ## 0.5.0
90
136
 
91
- ### Added
137
+ ### Minor Changes
92
138
 
93
139
  - **Chrome merges, data does not.** A cell spanning several columns in the
94
140
  header row or a total row is written into the first column it covers, the
95
141
  rest of the range is written empty, and the range is merged. Nothing in the
96
142
  data region merges, so sort, filter and column selection keep working over
97
143
  it.
98
-
99
144
  - **`valign`** writes the cell's vertical alignment for table cells and split
100
145
  slots. Undeclared writes nothing, so the spreadsheet application keeps its
101
146
  own default.
102
147
 
103
- ## [0.4.0] - 2026-09-03
148
+ ## 0.4.0
104
149
 
105
- ### Changed
150
+ ### Minor Changes
106
151
 
107
152
  - **A date string under `format: "date"` now reaches the grid as a date.**
108
153
  The cell carries a real date value under the `yyyy-mm-dd` number format
@@ -110,83 +155,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
110
155
  cell declaring the kind is affected; a string in a form the engine does not
111
156
  read stays text. See the `quario` changelog for the forms.
112
157
 
113
- ## [0.3.0] - 2026-09-02
158
+ ## 0.3.0
114
159
 
115
- ### Added
160
+ ### Minor Changes
116
161
 
117
162
  - **`format` maps to a number format; the cell stays typed.** `number` is
118
163
  `#,##0.00`, `percent` `0.00%`, `date` `yyyy-mm-dd`, `currency` the
119
164
  instance currency code as `"USD"#,##0.00`. A kind on the wrong type
120
165
  contributes nothing.
121
-
122
166
  - **Cell borders map; padding and flow spacing do not.** `solid` is exceljs
123
167
  `thin`; `dashed` and `dotted` keep their names. A header-row, row, or
124
168
  total-row box fans onto that row's cells; a cell that named any of a
125
169
  side's three keys owns that side whole. Padding is unread: a worksheet
126
170
  cell has no inset. Flow spacing is unread: a grid has no flow.
127
-
128
171
  - **`page.margin` and report-header `height` are unread.** A grid has no
129
172
  page top to pin from.
130
-
131
- ### Changed
132
-
133
173
  - **A table total emits N rows.** Each `total-row` is one worksheet row, as
134
174
  each data row is.
135
-
136
175
  - **A cell whose display contains a newline wraps.** `wrapText` is this
137
176
  target's mapping of a literal newline as a line break, so `"one\ntwo"` is
138
177
  two lines in the grid. Single-line cells are unchanged.
139
178
 
140
- ## [0.2.0] - 2026-09-01
179
+ ## 0.2.0
141
180
 
142
- ### Added
181
+ ### Minor Changes
143
182
 
144
183
  - **The report default reaches every cell.** A report's top-level `style`
145
184
  replaces this target's baseline, so a document declaring a face and size gets
146
185
  them in cells that declare nothing of their own. It is the layer under the
147
186
  band-role defaults — a report declaring `size: 12` still writes its report
148
187
  header at 14 — and under each cell's own style.
149
-
150
188
  - **`uppercase` is accepted and deliberately not read.** A spreadsheet font
151
189
  has no text-transform, and writing capitals into the cell instead would turn
152
190
  presentation into data — the cell would stop round-tripping and would sort
153
191
  differently. The cell keeps the text you wrote; every other declaration on
154
192
  it still applies. Same posture as the column widths this target withdrew.
155
-
156
193
  - **A split is one row, its slots the cells across it.** The grid's own
157
194
  reading of values placed beside each other. Slot `width` shares go unread,
158
195
  on exactly the ground the table's column widths do — a worksheet's columns
159
196
  are global to the sheet. The split's own style is what its slots sit under,
160
197
  layering the way a table row's does, and a slot that renders nothing writes
161
198
  an empty cell so the cells either side keep their columns.
162
-
163
- ### Changed
164
-
165
199
  - **Every cell is written at 10 points unless something declares otherwise.**
166
200
  The size was previously left to the writer's own default of 11. Now that a
167
201
  report can declare its own size, this target and the PDF carrying different
168
202
  numbers for one declaration is exactly what the targets' agreement rule
169
203
  forbids. A worksheet's row height follows its font size, so existing unstyled
170
204
  sheets come out slightly tighter.
171
-
172
205
  - **Display-text `Date`s join as ISO 8601 UTC.** A `Date` inside a mixed
173
206
  cell (literal text plus interpolation) joined through `String(date)`, which
174
207
  bakes the host's timezone and locale into the worksheet. It now joins
175
208
  through the engine's shared display rule as `toISOString()` text. Typed
176
209
  cells are untouched: a cell that is one bare `Date` interpolation still
177
210
  writes a native date.
178
-
179
- ### Fixed
180
-
181
211
  - **Cells carry their font face explicitly.** Text declaring no `family` used
182
212
  to inherit whatever the workbook writer defaulted to, which agreed with the
183
213
  other targets only by coincidence. It now writes the same face that
184
214
  `family: "sans"` resolves to, so the baseline is a rule rather than a
185
215
  property of the library underneath.
186
216
 
187
- ## [0.1.0] - 2026-08-27
217
+ ## 0.1.0
188
218
 
189
- ### Added
219
+ ### Minor Changes
190
220
 
191
221
  - **A workbook from the same compiled report.** Bands become rows; a bare
192
222
  interpolation that is a number, boolean, or Date stays that type. The first
package/README.md CHANGED
@@ -13,12 +13,12 @@ can keep working in, not a grid of strings.
13
13
  npm install quario @quario/xlsx
14
14
  ```
15
15
 
16
- [exceljs](https://github.com/exceljs/exceljs) comes along as a dependency; the engine is a peer, installed beside it. ESM-only,
16
+ [exceljs](https://github.com/exceljs/exceljs) arrives as a dependency. The engine is a peer, installed beside it. ESM-only,
17
17
  Node 22+, and browser-ready through any standards-based ESM bundler. The renderer returns bytes,
18
18
  so you decide where they go.
19
19
 
20
- > The writer is an implementation detail: exceljs is confined to a single internal module, so it
21
- > can be swapped behind this exact API without a breaking change.
20
+ > The writer is an implementation detail. This package confines exceljs to a single internal
21
+ > module, so a later version can swap it behind this exact API without a breaking change.
22
22
 
23
23
  ## Quick start
24
24
 
@@ -33,11 +33,11 @@ const bytes = await report.render(xlsx({ meta: { title: "Sales 2026" } }), data)
33
33
  writeFileSync("sales.xlsx", bytes);
34
34
  ```
35
35
 
36
- Any schema the other targets render works here unchanged. A spreadsheet has no pages, so page bands
37
- are ignored.
36
+ Any schema the other targets render works here unchanged. A spreadsheet has no pages, so this
37
+ target ignores page bands.
38
38
 
39
39
  One tip that changes the output a lot: **interpolate the bare value, not a formatter.** A cell
40
- written `{{ @.amount }}` becomes a real numeric cell; `{{ currency(@.amount) }}` becomes text,
40
+ written `{{ @.amount }}` becomes a real numeric cell. `{{ currency(@.amount) }}` becomes text,
41
41
  because the formatter already turned the number into a string. Let the spreadsheet do the
42
42
  formatting it is good at.
43
43
 
@@ -45,8 +45,9 @@ formatting it is good at.
45
45
 
46
46
  ### `xlsx(options?)`
47
47
 
48
- The target factory: it takes this target's host options and returns the target you pass to
49
- `render`. `report()` compiles once and `report.render(xlsx(options), data)` resolves the
48
+ The target factory takes this target's host options, validates them at the call, and returns the
49
+ target you pass to `render`. The factory refuses an option it does not know. It throws a `TypeError` at the call for an unknown key. It throws one also for a key with a value of the wrong type. It also reads `meta` at the call. One configured target
50
+ therefore always writes the same document. `report()` compiles once and `report.render(xlsx(options), data)` resolves the
50
51
  workbook bytes. Compile at startup, render per request. Definition problems throw at
51
52
  `report()`, at compile time.
52
53
 
@@ -59,7 +60,7 @@ The compiled report carries `stream` (the raw event generator), `names`, `functi
59
60
  `paths`, like every quario report. Engine-level options (`query` budgets, the license key)
60
61
  live on the instance, and `q.license` settles with the verification result.
61
62
 
62
- Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
63
+ Rendering is asynchronous and returns the loop between batches, so a large report never blocks the
63
64
  host. Render-time failures reject with located errors.
64
65
 
65
66
  ### Options
@@ -74,12 +75,12 @@ host. Render-time failures reject with located errors.
74
75
 
75
76
  One worksheet, named `Report`, with the banded walk flattened onto it as rows in render order:
76
77
  report-header items, then per group instance its header items, nested content, and footer items,
77
- then the table's header row, data rows, and total rows, then report-footer items. Grouping shows up
78
- as that order alone. Group boundaries add no rows of their own, and `break: "page"` and
79
- `reset: "page"` are ignored, as are `page.margin` and a report header's `height`.
78
+ then the table's header row, data rows, and total rows, then report-footer items. Grouping exists
79
+ as that order alone. Group boundaries add no rows of their own. This target ignores `break: "page"`,
80
+ `reset: "page"`, `page.margin`, and a report header's `height`.
80
81
 
81
- Each item takes one row with its cell in the first column, unmerged; a split takes one row with its
82
- slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell), and a
82
+ Each item takes one row with its cell in the first column, unmerged. A split takes one row with its
83
+ slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell). A
83
84
  spanning header or total cell is a merged range. Two band roles carry an
84
85
  omakase default that the author's own style always overrides: report-header items render bold at
85
86
  size 14, group-header items render bold.
@@ -99,9 +100,9 @@ A hidden cell keeps its column slot as an empty cell that retains its style.
99
100
 
100
101
  `format` writes a number format so the grid presents what the other targets do. `date` builds
101
102
  its pattern from the cell's `form` (`dd mmm yyyy` undeclared), with month and weekday names
102
- spelled by the reader's application; the number kinds build theirs from the declaration's
103
- fraction-digit count an undeclared `number` is `#,##0.00`, `percent` is `0.00%`,
104
- `{ "kind": "number", "digits": 3 }` is `#,##0.000` — and `currency` is the code the cell wears
103
+ spelled by the reader's application. The number kinds build theirs from the declaration's
104
+ fraction-digit count: an undeclared `number` is `#,##0.00`, `percent` is `0.00%`, and
105
+ `{ "kind": "number", "digits": 3 }` is `#,##0.000`. `currency` is the code the cell wears
105
106
  — its own `currency` when it declares one, else the instance's — over that currency's minor
106
107
  units unless `digits` says otherwise: `"USD"#,##0.00`, `"JPY"#,##0`, `"BHD"#,##0.000`. A kind
107
108
  on the wrong type, or a code the engine cannot read, writes no format.
@@ -110,25 +111,27 @@ on the wrong type, or a code the engine cannot read, writes no format.
110
111
 
111
112
  An image item is not a cell: it takes one worksheet row as its anchor and floats over the sheet as a
112
113
  drawing at its natural pixel size. Placement here is best-effort. `fit`, `alt`, and `style` are
113
- unused, and all of it may change without a breaking change. Bytes too short to carry a size reject
114
- the render rather than vanish.
114
+ unused, and all of it may change without a breaking change (see the
115
+ [support matrix](https://getquario.com/docs/reference/support-matrix/#output)). Bytes too short to
116
+ carry a size never reach this target: the engine reads every image's size and rejects them for
117
+ every target alike.
115
118
 
116
119
  ### Styles
117
120
 
118
121
  | Declaration | Maps to |
119
122
  | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
120
- | `family` | `sans` → Calibri, `serif` → Times New Roman, `mono` → Courier New; any other name passes through for the host application to resolve |
123
+ | `family` | `sans` → Calibri, `serif` → Times New Roman, `mono` → Courier New. Any other name passes through for the host application to resolve |
121
124
  | `size`, `bold`, `italic`, `underline`, `strikethrough` | Font (`strikethrough` → writer's `strike`) |
122
125
  | `color`, `background` | Font colour, solid fill |
123
126
  | `align` | Horizontal alignment |
124
- | `valign` | Vertical alignment; undeclared writes nothing, so the spreadsheet application keeps its own default |
125
- | `border*` | Cell borders (`solid` → `thin`, `dashed`, `dotted`; width approximated); a row's borders arrive on its cells already |
127
+ | `valign` | Vertical alignment. Undeclared writes nothing, so the spreadsheet application keeps its own default |
128
+ | `border*` | Cell borders (`solid` → `thin`, `dashed`, `dotted`, with width approximated). A row's borders arrive on its cells already |
126
129
  | `format`, `currency` | A number format — see [Number formats](#number-formats) |
127
130
  | `uppercase`, `padding*`, `spaceBefore`, `spaceAfter` | Not read: a grid has no text-transform, no cell inset of its own, and no flow |
128
131
 
129
132
  Style blocks layer row under cell, as everywhere in quario, with the report default outermost.
130
- Every cell is written at 10 points unless something declares otherwise, and a cell whose display
131
- contains a newline wraps.
133
+ This target writes every cell at 10 points unless something declares otherwise, and a cell whose
134
+ display contains a newline wraps.
132
135
 
133
136
  ### Freeze
134
137
 
@@ -145,28 +148,40 @@ table for a share to be a share of.
145
148
 
146
149
  ### Page columns are withdrawn
147
150
 
148
- A page column count sizes and splits nothing here, and will not change, as `break: "page"` is
149
- ignored. Page columns are strips of a page, and this target has no page to strip.
151
+ A [page column](https://getquario.com/docs/reference/support-matrix/#page-and-pagination) count
152
+ sizes and splits nothing here, and will not change, as this target ignores `break: "page"`. Page columns
153
+ are strips of a page, and this target has no page to strip.
150
154
 
151
155
  ## Unlicensed marking
152
156
 
153
157
  An unlicensed render writes the wording from `report-start.marking` twice: as a styled banner in
154
158
  row 1 (so the whole report, frozen view included, sits one row lower than a licensed render), and
155
- as the workbook's description property. Both are normative; banner styling is best-effort. A
159
+ as the workbook's description property. Both are normative. Banner styling is best-effort. A
156
160
  licensed render has no banner and no description marking.
157
161
 
158
162
  ## Determinism
159
163
 
160
- The document model is deterministic: the workbook's dates are pinned rather than stamped, and
161
- `meta` is opt-in. The **bytes** are not. The writer stamps archive entries with the packing time.
164
+ The document model is deterministic. This target pins the workbook's dates rather than stamping
165
+ them, and `meta` is opt-in. The **bytes** are not. The writer stamps archive entries with the packing time.
162
166
  Compare extracted content, not digests.
163
167
 
168
+ The full contract is [The XLSX target](https://getquario.com/docs/diving-deeper/xlsx-target/),
169
+ with each declaration's fate in the
170
+ [support matrix](https://getquario.com/docs/reference/support-matrix/).
171
+
172
+ ## Documentation
173
+
174
+ [The quario documentation](https://getquario.com/docs/) is the reference.
175
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
176
+ specification of what a report may declare, and
177
+ [`@quario/xlsx`](https://getquario.com/docs/reference/xlsx/) is this package's own API.
178
+
164
179
  ## License
165
180
 
166
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
167
- licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
181
+ Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
182
+ Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
168
183
 
169
- Pass your license key once, on the instance; it is verified offline:
184
+ Pass your license key once, on the instance. quario verifies it offline:
170
185
 
171
186
  ```js
172
187
  const q = quario({ license: "quario_..." });
package/lib/index.d.ts CHANGED
@@ -7,7 +7,12 @@ export interface XlsxMeta {
7
7
  subject?: string;
8
8
  }
9
9
 
10
- /** Host controls, taken at the factory call. */
10
+ /**
11
+ * Host controls, taken and validated at the factory call: an option this
12
+ * target does not understand, or one of the wrong type, throws a `TypeError`
13
+ * there rather than costing the host the option in silence. `meta` is read
14
+ * there too, so one configured target always writes the same document.
15
+ */
11
16
  export interface XlsxOptions {
12
17
  meta?: XlsxMeta;
13
18
  }
package/lib/index.js CHANGED
@@ -15,10 +15,9 @@
15
15
  * the packing time. Byte-for-byte output is a goal for the writer that will
16
16
  * replace `workbook.js`.
17
17
  */
18
- import { imageError, walk } from "quario";
18
+ import { hostMeta, hostOptions, walk } from "quario";
19
19
  import { field } from "./cell.js";
20
20
  import { merge as under } from "./style.js";
21
- import { pixels } from "./image.js";
22
21
  import { append, create, embed, freeze, mark, place, save, sheet, span } from "./workbook.js";
23
22
 
24
23
  // The options are described once, in the hand-written public declarations, and
@@ -67,14 +66,22 @@ let BANNER = { bold: true, background: "#fff2cc" };
67
66
  * runs the engine's `walk` driver over the handlers below, so large reports
68
67
  * stay cooperative.
69
68
  *
69
+ * Host controls are taken and validated here, at the factory call: an option
70
+ * this target does not understand, or one of the wrong type, throws where the
71
+ * host wrote it rather than costing them the option in silence
72
+ * (`docs/adr/0072`). `meta` is also *read* here, so a render is reproducible
73
+ * from the call that configured it.
74
+ *
70
75
  * @param {XlsxOptions} [options] Host controls (see SCHEMA.md, "The XLSX target").
71
76
  * @returns {{name: "xlsx", compile: (stream: any) => (data?: any) => Promise<Uint8Array>}}
72
77
  * The target (see SCHEMA.md, "Instances and targets").
73
78
  */
74
79
  export function xlsx(options) {
80
+ hostOptions(options, ["meta"], "options");
81
+ let meta = hostMeta(options?.meta, "options.meta");
75
82
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
76
83
  let compile = (stream) => async (data) => {
77
- let workbook = create(options?.meta);
84
+ let workbook = create(meta);
78
85
  let worksheet = sheet(workbook);
79
86
  // The first table anchors the frozen header row. It is global to the
80
87
  // sheet, so later tables never move it.
@@ -197,15 +204,14 @@ export function xlsx(options) {
197
204
  image: (event) => {
198
205
  let known = images.get(event.bytes);
199
206
  if (!known) {
200
- let size = pixels(event.bytes, event.format);
201
- // The engine vouched for the magic numbers, not for the rest of the
202
- // file. A header too short to carry a size leaves nothing to place a
203
- // drawing at, and failing beats dropping the picture in silence
204
- // (SCHEMA.md, "Image item"). Named on the item through the engine's
205
- // own mint, so this failure reads the same from here as it does from
206
- // a target that paints or embeds.
207
- if (!size) throw imageError(event.path, "could not read the image's size from its bytes");
208
- known = { ...size, id: embed(workbook, event.bytes, event.format) };
207
+ // The event states the size in pixels, which is the unit a floating
208
+ // drawing is placed in, so this target has nothing to work out: the
209
+ // engine read the header once for every target (`docs/adr/0067`).
210
+ known = {
211
+ width: event.width,
212
+ height: event.height,
213
+ id: embed(workbook, event.bytes, event.format),
214
+ };
209
215
  images.set(event.bytes, known);
210
216
  }
211
217
  if (split) {
package/lib/style.js CHANGED
@@ -22,6 +22,7 @@ let FAMILY = { sans: "Calibri", serif: "Times New Roman", mono: "Courier New" };
22
22
 
23
23
  // A declared colour as an ARGB string, or null when the value is not one.
24
24
  /** @type {(value: any) => string | null} */
25
+ // fallow-ignore-next-line code-duplication -- each target reads a declared hex into its own type; neither may import a sibling, and the engine's stream is all they share
25
26
  let argb = (value) => {
26
27
  let match = typeof value === "string" && HEX.exec(value);
27
28
  if (!match) return null;
package/lib/workbook.js CHANGED
@@ -10,12 +10,16 @@
10
10
  import ExcelJS from "exceljs";
11
11
 
12
12
  // Host metadata keys as the workbook properties they become -- `author` is the
13
- // only one exceljs names differently.
13
+ // only one exceljs names differently, and this mapping is all this target owns
14
+ // of `meta`. The contract itself (which properties, each a string, never a
15
+ // date) is the engine's `hostMeta`, checked at the factory call, so nothing
16
+ // here restates it: a present property is a string, and a nullish one is a
17
+ // property the host did not write.
14
18
  /** @type {(workbook: any, meta: any) => void} */
15
19
  let describe = (workbook, meta) => {
16
- if (typeof meta.title === "string") workbook.title = meta.title;
17
- if (typeof meta.author === "string") workbook.creator = meta.author;
18
- if (typeof meta.subject === "string") workbook.subject = meta.subject;
20
+ if (meta.title != null) workbook.title = meta.title;
21
+ if (meta.author != null) workbook.creator = meta.author;
22
+ if (meta.subject != null) workbook.subject = meta.subject;
19
23
  };
20
24
 
21
25
  // A fresh workbook. Never the current time: a timestamp would make the same
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/xlsx",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The spreadsheet render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -42,12 +42,12 @@
42
42
  "@arethetypeswrong/cli": "^0.18.3",
43
43
  "@size-limit/preset-small-lib": "^13.0.3",
44
44
  "@types/node": "^22.20.1",
45
- "quario": "^0.7.0",
45
+ "quario": "^0.9.0",
46
46
  "size-limit": "^13.0.3",
47
47
  "typescript": "^7.0.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "quario": "^0.7.0"
50
+ "quario": "^0.9.0"
51
51
  },
52
52
  "size-limit": [
53
53
  {
package/lib/image.js DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * An image's own size, read from its header. A floating drawing is placed in
3
- * pixels, and the event carries the bytes and the format the engine sniffed
4
- * but no dimensions, so the one thing this target needs to place a picture is
5
- * worked out here — from the header alone, never by decoding the image.
6
- *
7
- * The PDF target reads the same two headers for its own placement, in points.
8
- * Restated per target rather than shared, like every other coercion at a
9
- * target's edge: neither package imports the other, and the engine's stream is
10
- * all they have in common.
11
- */
12
-
13
- /** @type {(bytes: Uint8Array, at: number) => number} */
14
- let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
15
-
16
- // The dimensions live in the frame header, the first SOFn marker: every
17
- // code in C0..CF except the three in that range that are not frames --
18
- // DHT, the JPG extension, and DAC.
19
- /** @type {Record<number, number>} */
20
- let SKIP = { 0xc4: 1, 0xc8: 1, 0xcc: 1 };
21
- /** @type {(code: number) => any} */
22
- let sof = (code) => code >= 0xc0 && code <= 0xcf && !SKIP[code];
23
-
24
- /** @type {(bytes: Uint8Array) => { width: number, height: number }} */
25
- let jpeg = (bytes) => {
26
- for (let at = 2; at + 9 < bytes.length; at += 2 + word(bytes, at + 2)) {
27
- if (bytes[at] !== 0xff) break;
28
- if (sof(bytes[at + 1])) return { width: word(bytes, at + 7), height: word(bytes, at + 5) };
29
- }
30
- return { width: 0, height: 0 };
31
- };
32
-
33
- /**
34
- * The image's size in pixels, or null when the header does not carry one —
35
- * bytes the engine vouched the magic numbers of, truncated before the size.
36
- *
37
- * @param {Uint8Array} bytes The image file.
38
- * @param {string} format The engine's sniff: `"png"` or `"jpeg"`.
39
- * @returns {{ width: number, height: number } | null} The size, if readable.
40
- */
41
- export let pixels = (bytes, format) => {
42
- // A PNG carries the two numbers in its IHDR at a fixed offset.
43
- let size = format === "png" ? { width: word(bytes, 18), height: word(bytes, 22) } : jpeg(bytes);
44
- return size.width > 0 && size.height > 0 ? size : null;
45
- };