@quario/xlsx 0.5.0 → 0.6.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 +63 -0
- package/README.md +25 -5
- package/lib/cell.js +62 -22
- package/lib/index.js +20 -12
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A date cell's number format follows the `form` its report declares.** The
|
|
15
|
+
four forms map to `dd/mm/yy`, `dd mmm yyyy`, `dd mmmm yyyy` and
|
|
16
|
+
`dddd, dd mmmm yyyy`. The shape is the document's; the language a month or
|
|
17
|
+
weekday name is spelled in stays the reader's, because that is what a
|
|
18
|
+
spreadsheet application supplies and pinning it would stop the file reading
|
|
19
|
+
naturally for whoever opens it.
|
|
20
|
+
|
|
21
|
+
- **A declared digit count builds the pattern.** A `number` cell asking for
|
|
22
|
+
three digits writes `#,##0.000`, and a `currency` cell asking for none writes
|
|
23
|
+
`"EUR"#,##0` — the same count the page presents, taken off the declaration
|
|
24
|
+
the engine resolved rather than computed here.
|
|
25
|
+
|
|
26
|
+
- **A cell's own `currency` code picks the number format**, ahead of the
|
|
27
|
+
instance's default: `"JPY"#,##0` where the row says `JPY`, since the code
|
|
28
|
+
decides the digits too. The cell stays a number either way. A code the engine
|
|
29
|
+
could not accept writes no number format at all, rather than labelling the
|
|
30
|
+
cell in the instance's currency.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **A `date` cell with no declared form now writes `dd mmm yyyy`, not
|
|
35
|
+
`yyyy-mm-dd`.** A bare `date` means the `medium` form throughout, so the grid
|
|
36
|
+
agrees with the page. A report that wants the ISO shape has no form for it;
|
|
37
|
+
the nearest is `{ "kind": "date", "form": "short" }`, which writes
|
|
38
|
+
`dd/mm/yy`. The cell is still a real typed date either way.
|
|
39
|
+
|
|
40
|
+
- **A cell's number format now comes from the engine's fraction-digit count
|
|
41
|
+
rather than from four patterns this target held of its own.** The visible
|
|
42
|
+
change is `currency`: the pattern followed the currency's own minor units,
|
|
43
|
+
so an amount in a currency with none — JPY — is `"JPY"#,##0` where it was
|
|
44
|
+
`"JPY"#,##0.00`, and one with three is `"BHD"#,##0.000`. `number`
|
|
45
|
+
(`#,##0.00`), `percent` (`0.00%`) and `date` (`yyyy-mm-dd`) are unchanged in
|
|
46
|
+
a worksheet; what changed for those is that HTML and PDF now show the same
|
|
47
|
+
decimals the grid always did, instead of their own.
|
|
48
|
+
|
|
49
|
+
- A `currency` code that is not a readable currency no longer produces a number
|
|
50
|
+
format at all. The cell keeps its value; before, the code was pasted straight
|
|
51
|
+
into a pattern while every other target fell back to the unformatted value.
|
|
52
|
+
|
|
53
|
+
Only the digit count is shared. The grouping separator and the symbol's
|
|
54
|
+
position still come from the application that opens the file, which is what
|
|
55
|
+
lets one workbook read naturally wherever it is opened.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- A `format` kind reaching this target from an `=` expression is now looked up
|
|
60
|
+
as an own key. Before, a kind resolving to the name of a built-in object
|
|
61
|
+
member threw out of the render (`valueOf`, `hasOwnProperty`) or wrote
|
|
62
|
+
`[object Undefined]` into the cell's number format (`toString`). A literal
|
|
63
|
+
was never affected: it is checked against the four kinds.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **An image whose size cannot be read now names the item that asked for it.**
|
|
68
|
+
The failure said only that the size could not be read from the bytes, so a
|
|
69
|
+
report with two pictures gave no way to tell which one was bad. The message
|
|
70
|
+
now begins with the item's `source` path, as every other render error does
|
|
71
|
+
and as it does from every other target.
|
|
72
|
+
|
|
10
73
|
## [0.5.0] - 2026-09-05
|
|
11
74
|
|
|
12
75
|
### Added
|
package/README.md
CHANGED
|
@@ -74,10 +74,13 @@ host. Render-time failures reject with located errors.
|
|
|
74
74
|
|
|
75
75
|
One worksheet, named `Report`, with the banded walk flattened onto it as rows in render order:
|
|
76
76
|
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
|
|
78
|
-
as that order alone. Group boundaries add no rows of their own, and `break: "page"`
|
|
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`.
|
|
79
80
|
|
|
80
|
-
Each item takes one row with its cell in the first column, unmerged
|
|
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 unread, a slot rendering nothing an empty cell), and a
|
|
83
|
+
spanning header or total cell is a merged range. Two band roles carry an
|
|
81
84
|
omakase default that the author's own style always overrides: report-header items render bold at
|
|
82
85
|
size 14, group-header items render bold.
|
|
83
86
|
|
|
@@ -92,11 +95,23 @@ inert literal text: no formula-injection surface, and no apostrophe mangling eit
|
|
|
92
95
|
|
|
93
96
|
A hidden cell keeps its column slot as an empty cell that retains its style.
|
|
94
97
|
|
|
98
|
+
### Number formats
|
|
99
|
+
|
|
100
|
+
`format` writes a number format so the grid presents what the other targets do. `date` builds
|
|
101
|
+
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
|
|
105
|
+
— its own `currency` when it declares one, else the instance's — over that currency's minor
|
|
106
|
+
units unless `digits` says otherwise: `"USD"#,##0.00`, `"JPY"#,##0`, `"BHD"#,##0.000`. A kind
|
|
107
|
+
on the wrong type, or a code the engine cannot read, writes no format.
|
|
108
|
+
|
|
95
109
|
### Images
|
|
96
110
|
|
|
97
111
|
An image item is not a cell: it takes one worksheet row as its anchor and floats over the sheet as a
|
|
98
112
|
drawing at its natural pixel size. Placement here is best-effort. `fit`, `alt`, and `style` go
|
|
99
|
-
unread, and all of it may change without a breaking change.
|
|
113
|
+
unread, and all of it may change without a breaking change. Bytes too short to carry a size reject
|
|
114
|
+
the render rather than vanish.
|
|
100
115
|
|
|
101
116
|
### Styles
|
|
102
117
|
|
|
@@ -107,8 +122,13 @@ unread, and all of it may change without a breaking change.
|
|
|
107
122
|
| `color`, `background` | Font colour, solid fill |
|
|
108
123
|
| `align` | Horizontal alignment |
|
|
109
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 |
|
|
126
|
+
| `format`, `currency` | A number format — see [Number formats](#number-formats) |
|
|
127
|
+
| `uppercase`, `padding*`, `spaceBefore`, `spaceAfter` | Not read: a grid has no text-transform, no cell inset of its own, and no flow |
|
|
110
128
|
|
|
111
|
-
Style blocks layer row under cell, as everywhere in quario.
|
|
129
|
+
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.
|
|
112
132
|
|
|
113
133
|
### Freeze
|
|
114
134
|
|
package/lib/cell.js
CHANGED
|
@@ -6,39 +6,79 @@
|
|
|
6
6
|
* cell-level formatting from the style module. Display text that contains a
|
|
7
7
|
* newline gains `wrapText` so the break is visible in the grid.
|
|
8
8
|
*/
|
|
9
|
-
import { text, typed } from "quario";
|
|
9
|
+
import { currencyOf, text, typed } from "quario";
|
|
10
10
|
import { format as look, merge } from "./style.js";
|
|
11
11
|
|
|
12
|
+
// The digit count is the engine's, never this target's: it rides on the
|
|
13
|
+
// resolved declaration the stream carries, the same one HTML and PDF
|
|
14
|
+
// stringify from, so a cell shows the same decimals in the grid as it does on
|
|
15
|
+
// the page (docs/adr/0054, docs/adr/0056). What stays this target's own is
|
|
16
|
+
// the form — the reader's application supplies the grouping separator, the
|
|
17
|
+
// symbol's place, and the language a month name is spelled in.
|
|
18
|
+
/** @type {(digits: number | undefined) => string} */
|
|
19
|
+
let places = (digits) => (digits ? "." + "0".repeat(digits) : "");
|
|
20
|
+
|
|
12
21
|
/** @type {(value: any, fmt: string) => string | null} */
|
|
13
22
|
let whenNum = (value, fmt) => (typeof value === "number" ? fmt : null);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** @type {Record<string,
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// A date's form is honoured as far as a sheet can honour one: the *shape* is
|
|
24
|
+
// the author's, and the language of a month or weekday name is the reader's,
|
|
25
|
+
// because Excel spells `mmmm` and `dddd` in whoever opens the file's
|
|
26
|
+
// application language. Pinning that would take a `[$-...]` locale prefix and
|
|
27
|
+
// would stop the sheet following its reader, which docs/adr/0054 refuses. The
|
|
28
|
+
// day-first ordering is the author's here too, and that is the second half of
|
|
29
|
+
// the same approximation (CONTEXT.md, "Form").
|
|
30
|
+
/** @type {Record<string, string>} */
|
|
31
|
+
let DATE_FMT = {
|
|
32
|
+
short: "dd/mm/yy",
|
|
33
|
+
medium: "dd mmm yyyy",
|
|
34
|
+
long: "dd mmmm yyyy",
|
|
35
|
+
full: "dddd, dd mmmm yyyy",
|
|
27
36
|
};
|
|
28
|
-
/** @type {(
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
/** @type {(value: any, decl: any) => string | null} */
|
|
38
|
+
let whenDate = (value, decl) =>
|
|
39
|
+
value instanceof Date && Object.hasOwn(DATE_FMT, decl.form) ? DATE_FMT[decl.form] : null;
|
|
40
|
+
/** @type {(value: any, decl: any, currency: any) => string | null} */
|
|
41
|
+
let whenMoney = (value, decl, currency) => {
|
|
42
|
+
// The value first: a text cell in a money column would otherwise pay for a
|
|
43
|
+
// pattern it cannot wear. Then the count — a code the engine will not read
|
|
44
|
+
// resolved to no digits at all, so the cell keeps its value and gains no
|
|
45
|
+
// number format, the same withdrawal `format()` makes.
|
|
46
|
+
if (typeof value !== "number" || decl.digits === undefined) return null;
|
|
47
|
+
return '"' + currency + '"#,##0' + places(decl.digits);
|
|
32
48
|
};
|
|
49
|
+
/** @type {Record<string, (value: any, decl: any, currency: any) => string | null>} */
|
|
50
|
+
let NUMFMT = {
|
|
51
|
+
number: (value, decl) => whenNum(value, "#,##0" + places(decl.digits)),
|
|
52
|
+
percent: (value, decl) => whenNum(value, "0" + places(decl.digits) + "%"),
|
|
53
|
+
date: whenDate,
|
|
54
|
+
currency: whenMoney,
|
|
55
|
+
};
|
|
56
|
+
// Own-key lookup, as everywhere a resolved declaration indexes a table: the
|
|
57
|
+
// declaration reaches here having been resolved from author data, and
|
|
58
|
+
// `valueOf` or `toString` as a kind would otherwise reach a member of
|
|
59
|
+
// `Object.prototype` and either throw out of the render or write its return
|
|
60
|
+
// value as a number format.
|
|
61
|
+
/** @type {(decl: any, value: any, currency: any) => string | null} */
|
|
62
|
+
let numFmtOf = (decl, value, currency) =>
|
|
63
|
+
!!decl && typeof decl === "object" && Object.hasOwn(NUMFMT, decl.kind)
|
|
64
|
+
? NUMFMT[decl.kind](value, decl, currency)
|
|
65
|
+
: null;
|
|
33
66
|
|
|
34
|
-
/** @type {(cell: { tokens: any[] },
|
|
35
|
-
let shownOf = (cell,
|
|
36
|
-
let value = typed(cell.tokens,
|
|
67
|
+
/** @type {(cell: { tokens: any[] }, decl: any) => any} */
|
|
68
|
+
let shownOf = (cell, decl) => {
|
|
69
|
+
let value = typed(cell.tokens, decl);
|
|
37
70
|
return value !== undefined ? value : text(cell.tokens);
|
|
38
71
|
};
|
|
72
|
+
// The code is spliced into the format string rather than looked up as a
|
|
73
|
+
// symbol, so it must be a code and nothing else. It is: the engine gates a
|
|
74
|
+
// resolved `currency` before it crosses, which is why an author-data code
|
|
75
|
+
// (`currency: "=@.ccy"`) can reach a number format at all (docs/adr/0041).
|
|
76
|
+
// Which code wins is the engine's rule too, taken from `currencyOf` rather
|
|
77
|
+
// than restated: this target needs the code itself where the display targets
|
|
78
|
+
// need presented text, and the two must not drift apart about a denomination.
|
|
39
79
|
/** @type {(out: any, resolved: any, shown: any, intl: any) => void} */
|
|
40
80
|
let paintFmt = (out, resolved, shown, intl) => {
|
|
41
|
-
let numFmt = numFmtOf(resolved?.format, shown, intl
|
|
81
|
+
let numFmt = numFmtOf(resolved?.format, shown, currencyOf(resolved, intl));
|
|
42
82
|
if (numFmt) out.numFmt = numFmt;
|
|
43
83
|
};
|
|
44
84
|
/** @type {(out: any, shown: any) => void} */
|
package/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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 { walk } from "quario";
|
|
18
|
+
import { imageError, walk } from "quario";
|
|
19
19
|
import { field } from "./cell.js";
|
|
20
20
|
import { merge as under } from "./style.js";
|
|
21
21
|
import { pixels } from "./image.js";
|
|
@@ -39,7 +39,10 @@ import { append, create, embed, freeze, mark, place, save, sheet, span } from ".
|
|
|
39
39
|
// them. `@quario/html/style.css` and `packages/viewer/lib/style.js` supply them for
|
|
40
40
|
// that target; keep all four in agreement.
|
|
41
41
|
/** @type {Record<string, any>} */
|
|
42
|
-
let ROLES = {
|
|
42
|
+
let ROLES = {
|
|
43
|
+
"report-header": { bold: true, size: 14 },
|
|
44
|
+
"group-header": { bold: true },
|
|
45
|
+
};
|
|
43
46
|
|
|
44
47
|
// This target's baseline type size, written into every cell rather than left
|
|
45
48
|
// to the writer's own 11. The PDF target carries the same 10, and once a report
|
|
@@ -138,7 +141,11 @@ export function xlsx(options) {
|
|
|
138
141
|
// The report default over this target's baseline. The marking is
|
|
139
142
|
// written under the baseline alone: an author's `style` must not be
|
|
140
143
|
// able to resize it (docs/adr/0002).
|
|
141
|
-
intl = {
|
|
144
|
+
intl = {
|
|
145
|
+
locale: event.locale,
|
|
146
|
+
currency: event.currency,
|
|
147
|
+
timeZone: event.timeZone,
|
|
148
|
+
};
|
|
142
149
|
if (event.style) base = under(BASELINE, event.style);
|
|
143
150
|
if (event.marking) {
|
|
144
151
|
mark(workbook, event.marking);
|
|
@@ -163,7 +170,11 @@ export function xlsx(options) {
|
|
|
163
170
|
"split-start": (event) => {
|
|
164
171
|
// The split's own style layers over its band-role default, so a styled
|
|
165
172
|
// split keeps the weight a plain item in the same band would have.
|
|
166
|
-
split = {
|
|
173
|
+
split = {
|
|
174
|
+
fields: [],
|
|
175
|
+
images: [],
|
|
176
|
+
under: under(roleOf(event), event.style),
|
|
177
|
+
};
|
|
167
178
|
},
|
|
168
179
|
"split-end": () => {
|
|
169
180
|
// The engine emits the bracket as one array, so a `split-end` always
|
|
@@ -188,8 +199,10 @@ export function xlsx(options) {
|
|
|
188
199
|
// The engine vouched for the magic numbers, not for the rest of the
|
|
189
200
|
// file. A header too short to carry a size leaves nothing to place a
|
|
190
201
|
// drawing at, and failing beats dropping the picture in silence
|
|
191
|
-
// (SCHEMA.md, "Image item").
|
|
192
|
-
|
|
202
|
+
// (SCHEMA.md, "Image item"). Named on the item through the engine's
|
|
203
|
+
// own mint, so this failure reads the same from here as it does from
|
|
204
|
+
// a target that paints or embeds.
|
|
205
|
+
if (!size) throw imageError(event.path, "could not read the image's size from its bytes");
|
|
193
206
|
known = { ...size, id: embed(workbook, event.bytes, event.format) };
|
|
194
207
|
images.set(event.bytes, known);
|
|
195
208
|
}
|
|
@@ -205,12 +218,7 @@ export function xlsx(options) {
|
|
|
205
218
|
place(worksheet, known.id, known, append(worksheet, []));
|
|
206
219
|
},
|
|
207
220
|
"table-start": (event) => {
|
|
208
|
-
let row = merged(
|
|
209
|
-
event.columns
|
|
210
|
-
.filter((/** @type {any} */ column) => column.header)
|
|
211
|
-
.map((/** @type {any} */ column) => column.header),
|
|
212
|
-
event.style,
|
|
213
|
-
);
|
|
221
|
+
let row = merged(event.header.cells, event.header.style);
|
|
214
222
|
if (!frozen) {
|
|
215
223
|
freeze(worksheet, row);
|
|
216
224
|
frozen = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/xlsx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.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.6.0"
|
|
51
51
|
},
|
|
52
52
|
"size-limit": [
|
|
53
53
|
{
|