@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 +78 -48
- package/README.md +47 -32
- package/lib/index.d.ts +6 -1
- package/lib/index.js +18 -12
- package/lib/style.js +1 -0
- package/lib/workbook.js +8 -4
- package/package.json +3 -3
- package/lib/image.js +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @quario/xlsx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 0.8.0
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
### Minor Changes
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
84
|
+
## 0.6.0
|
|
27
85
|
|
|
28
|
-
###
|
|
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
|
-
##
|
|
135
|
+
## 0.5.0
|
|
90
136
|
|
|
91
|
-
###
|
|
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
|
-
##
|
|
148
|
+
## 0.4.0
|
|
104
149
|
|
|
105
|
-
###
|
|
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
|
-
##
|
|
158
|
+
## 0.3.0
|
|
114
159
|
|
|
115
|
-
###
|
|
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
|
-
##
|
|
179
|
+
## 0.2.0
|
|
141
180
|
|
|
142
|
-
###
|
|
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
|
-
##
|
|
217
|
+
## 0.1.0
|
|
188
218
|
|
|
189
|
-
###
|
|
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)
|
|
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
|
|
21
|
-
> can
|
|
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
|
|
37
|
-
|
|
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
|
|
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
|
|
49
|
-
`render`. `
|
|
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
|
|
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
|
|
78
|
-
as that order alone. Group boundaries add no rows of their own
|
|
79
|
-
`reset: "page"
|
|
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
|
|
82
|
-
slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell)
|
|
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
|
|
103
|
-
fraction-digit count
|
|
104
|
-
`{ "kind": "number", "digits": 3 }` is `#,##0.000
|
|
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
|
|
114
|
-
|
|
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
|
|
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
|
|
125
|
-
| `border*` | Cell borders (`solid` → `thin`, `dashed`, `dotted
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 (
|
|
17
|
-
if (
|
|
18
|
-
if (
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
};
|