@quario/xlsx 0.4.0 → 0.5.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 +14 -0
- package/README.md +1 -0
- package/lib/cell.js +2 -2
- package/lib/index.js +32 -6
- package/lib/style.js +18 -27
- package/lib/workbook.js +9 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Chrome merges, data does not.** A cell spanning several columns in the
|
|
15
|
+
header row or a total row is written into the first column it covers, the
|
|
16
|
+
rest of the range is written empty, and the range is merged. Nothing in the
|
|
17
|
+
data region merges, so sort, filter and column selection keep working over
|
|
18
|
+
it.
|
|
19
|
+
|
|
20
|
+
- **`valign`** writes the cell's vertical alignment for table cells and split
|
|
21
|
+
slots. Undeclared writes nothing, so the spreadsheet application keeps its
|
|
22
|
+
own default.
|
|
23
|
+
|
|
10
24
|
## [0.4.0] - 2026-09-03
|
|
11
25
|
|
|
12
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ unread, and all of it may change without a breaking change.
|
|
|
106
106
|
| `size`, `bold`, `italic`, `underline`, `strikethrough` | Font (`strikethrough` → writer's `strike`) |
|
|
107
107
|
| `color`, `background` | Font colour, solid fill |
|
|
108
108
|
| `align` | Horizontal alignment |
|
|
109
|
+
| `valign` | Vertical alignment; undeclared writes nothing, so the spreadsheet application keeps its own default |
|
|
109
110
|
|
|
110
111
|
Style blocks layer row under cell, as everywhere in quario.
|
|
111
112
|
|
package/lib/cell.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* newline gains `wrapText` so the break is visible in the grid.
|
|
8
8
|
*/
|
|
9
9
|
import { text, typed } from "quario";
|
|
10
|
-
import { format as look,
|
|
10
|
+
import { format as look, merge } from "./style.js";
|
|
11
11
|
|
|
12
12
|
/** @type {(value: any, fmt: string) => string | null} */
|
|
13
13
|
let whenNum = (value, fmt) => (typeof value === "number" ? fmt : null);
|
|
@@ -54,7 +54,7 @@ let paintWrap = (out, shown) => {
|
|
|
54
54
|
* @type {(cell: { tokens: any[], style?: any }, under: any, intl?: any) => { value: any, format: any }}
|
|
55
55
|
*/
|
|
56
56
|
let field = (cell, under, intl) => {
|
|
57
|
-
let resolved =
|
|
57
|
+
let resolved = merge(under, cell.style);
|
|
58
58
|
let shown = shownOf(cell, resolved?.format);
|
|
59
59
|
let out = look(resolved);
|
|
60
60
|
paintFmt(out, resolved, shown, intl);
|
package/lib/index.js
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { walk } from "quario";
|
|
19
19
|
import { field } from "./cell.js";
|
|
20
|
-
import {
|
|
20
|
+
import { merge as under } from "./style.js";
|
|
21
21
|
import { pixels } from "./image.js";
|
|
22
|
-
import { append, create, embed, freeze, mark, place, save, sheet } from "./workbook.js";
|
|
22
|
+
import { append, create, embed, freeze, mark, place, save, sheet, span } from "./workbook.js";
|
|
23
23
|
|
|
24
24
|
// The options are described once, in the hand-written public declarations, and
|
|
25
25
|
// read back here — a second copy in JSDoc is a copy that drifts.
|
|
@@ -109,6 +109,30 @@ export function xlsx(options) {
|
|
|
109
109
|
worksheet,
|
|
110
110
|
event.cells.map((/** @type {any} */ each) => cell(each, layer)),
|
|
111
111
|
);
|
|
112
|
+
// Chrome merges, data does not (docs/adr/0047). A cell covering several
|
|
113
|
+
// columns writes its value into the first of them, the rest are written
|
|
114
|
+
// empty, and the range is merged -- so the label stays where the document
|
|
115
|
+
// put it. Reached by the header row and the total rows; a data row cannot
|
|
116
|
+
// span, so `record` above stays the plain path it was.
|
|
117
|
+
/** @type {(each: any, layer: any) => any[]} */
|
|
118
|
+
let covering = (each, layer) => [
|
|
119
|
+
cell(each, layer),
|
|
120
|
+
...Array.from({ length: (each.span || 1) - 1 }, () => cell({ tokens: [] }, layer)),
|
|
121
|
+
];
|
|
122
|
+
/** @type {(cells: any[], layer: any) => number} */
|
|
123
|
+
let merged = (cells, layer) => {
|
|
124
|
+
/** @type {any[]} */
|
|
125
|
+
let fields = [];
|
|
126
|
+
/** @type {{ at: number, width: number }[]} */
|
|
127
|
+
let ranges = [];
|
|
128
|
+
for (let each of cells) {
|
|
129
|
+
if (each.span > 1) ranges.push({ at: fields.length + 1, width: each.span });
|
|
130
|
+
fields.push(...covering(each, layer));
|
|
131
|
+
}
|
|
132
|
+
let row = append(worksheet, fields);
|
|
133
|
+
for (let range of ranges) span(worksheet, row, range.at, range.width);
|
|
134
|
+
return row;
|
|
135
|
+
};
|
|
112
136
|
await walk(stream(data), {
|
|
113
137
|
"report-start": (event) => {
|
|
114
138
|
// The report default over this target's baseline. The marking is
|
|
@@ -181,17 +205,19 @@ export function xlsx(options) {
|
|
|
181
205
|
place(worksheet, known.id, known, append(worksheet, []));
|
|
182
206
|
},
|
|
183
207
|
"table-start": (event) => {
|
|
184
|
-
let
|
|
185
|
-
|
|
208
|
+
let row = merged(
|
|
209
|
+
event.columns
|
|
210
|
+
.filter((/** @type {any} */ column) => column.header)
|
|
211
|
+
.map((/** @type {any} */ column) => column.header),
|
|
212
|
+
event.style,
|
|
186
213
|
);
|
|
187
|
-
let row = append(worksheet, headers);
|
|
188
214
|
if (!frozen) {
|
|
189
215
|
freeze(worksheet, row);
|
|
190
216
|
frozen = true;
|
|
191
217
|
}
|
|
192
218
|
},
|
|
193
219
|
row: (event) => record(event, event.style),
|
|
194
|
-
"total-row": (event) =>
|
|
220
|
+
"total-row": (event) => merged(event.cells, event.style),
|
|
195
221
|
});
|
|
196
222
|
return save(workbook);
|
|
197
223
|
};
|
package/lib/style.js
CHANGED
|
@@ -10,11 +10,8 @@
|
|
|
10
10
|
* target's own edge, never shared engine code.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
/** @type {(value: any) => boolean} */
|
|
14
|
-
let finite = (value) => Number.isFinite(value);
|
|
15
13
|
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
16
14
|
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
17
|
-
let BORDER_PARTS = ["Width", "Style", "Color"];
|
|
18
15
|
/** @type {Record<string, string>} */
|
|
19
16
|
let LINE = { solid: "thin", dashed: "dashed", dotted: "dotted" };
|
|
20
17
|
|
|
@@ -33,28 +30,14 @@ let argb = (value) => {
|
|
|
33
30
|
return "FF" + digits.toUpperCase();
|
|
34
31
|
};
|
|
35
32
|
|
|
36
|
-
// Style blocks layer outward-in:
|
|
37
|
-
//
|
|
38
|
-
//
|
|
33
|
+
// Style blocks layer outward-in: the enclosing block under the cell's own.
|
|
34
|
+
// A row's block reaches here with no box in it -- the engine resolved that
|
|
35
|
+
// half onto the cells themselves, border sides won whole and all (SCHEMA.md,
|
|
36
|
+
// "Style declarations") -- so what layers here is a plain overwrite. The only
|
|
37
|
+
// other block that sits under a cell is a band-role default, which declares
|
|
38
|
+
// no box either.
|
|
39
39
|
/** @type {(under: any, over: any) => any} */
|
|
40
40
|
let merge = (under, over) => (under ? (over ? { ...under, ...over } : under) : over || {});
|
|
41
|
-
/** @type {(style: any, side: string) => boolean} */
|
|
42
|
-
let owns = (style, side) =>
|
|
43
|
-
!!style && BORDER_PARTS.some((part) => Object.hasOwn(style, "border" + side + part));
|
|
44
|
-
/** @type {(out: any, over: any, side: string) => void} */
|
|
45
|
-
let take = (out, over, side) => {
|
|
46
|
-
for (let part of BORDER_PARTS) {
|
|
47
|
-
let name = "border" + side + part;
|
|
48
|
-
if (Object.hasOwn(over, name)) out[name] = over[name];
|
|
49
|
-
else delete out[name];
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
/** @type {(under: any, over: any) => any} */
|
|
53
|
-
let sides = (under, over) => {
|
|
54
|
-
let out = merge(under, over);
|
|
55
|
-
if (over) for (let side of SIDES) if (owns(over, side)) take(out, over, side);
|
|
56
|
-
return out;
|
|
57
|
-
};
|
|
58
41
|
|
|
59
42
|
/** @type {(out: any, key: string, value: any) => any} */
|
|
60
43
|
let put = (out, key, value) => {
|
|
@@ -76,7 +59,7 @@ let face = (family) => {
|
|
|
76
59
|
};
|
|
77
60
|
|
|
78
61
|
/** @type {(value: any) => number | null} */
|
|
79
|
-
let size = (value) => (
|
|
62
|
+
let size = (value) => (Number.isFinite(value) && value > 0 ? value : null);
|
|
80
63
|
/** @type {(value: any) => true | null} */
|
|
81
64
|
let flag = (value) => (value ? true : null);
|
|
82
65
|
/** @type {(value: any) => { argb: string } | null} */
|
|
@@ -113,9 +96,17 @@ let fill = (value) => {
|
|
|
113
96
|
/** @type {(value: any) => any} */
|
|
114
97
|
let align = (value) =>
|
|
115
98
|
value === "left" || value === "center" || value === "right" ? { horizontal: value } : null;
|
|
99
|
+
// The writer's own words for the vertical half, one to one. Undeclared is
|
|
100
|
+
// left to the host application (Excel's bottom) rather than pinned: a
|
|
101
|
+
// spreadsheet user expects its default, and an author who cares declares one.
|
|
102
|
+
let VERTICAL = ["top", "middle", "bottom"];
|
|
103
|
+
/** @type {(value: any) => any} */
|
|
104
|
+
let valign = (value) => (VERTICAL.includes(value) ? value : null);
|
|
105
|
+
/** @type {(style: any) => any} */
|
|
106
|
+
let alignment = (style) => put(align(style.align), "vertical", valign(style.valign));
|
|
116
107
|
|
|
117
108
|
/** @type {(width: any) => boolean} */
|
|
118
|
-
let isStroke = (width) =>
|
|
109
|
+
let isStroke = (width) => Number.isFinite(width) && width > 0;
|
|
119
110
|
/** @type {(named: any) => string | null} */
|
|
120
111
|
let lineOf = (named) => {
|
|
121
112
|
if (typeof named !== "string") return null;
|
|
@@ -144,7 +135,7 @@ let format = (style) => {
|
|
|
144
135
|
let out = put(null, "font", font(style));
|
|
145
136
|
out = put(out, "fill", fill(style.background));
|
|
146
137
|
out = put(out, "border", border(style));
|
|
147
|
-
return put(out, "alignment",
|
|
138
|
+
return put(out, "alignment", alignment(style));
|
|
148
139
|
};
|
|
149
140
|
|
|
150
|
-
export { format,
|
|
141
|
+
export { format, merge };
|
package/lib/workbook.js
CHANGED
|
@@ -68,6 +68,14 @@ let place = (worksheet, id, size, row) => {
|
|
|
68
68
|
});
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
// One merged range across `width` columns of a row, from column `at` (1-based).
|
|
72
|
+
// Chrome merges and data does not (docs/adr/0047), so this reaches the header
|
|
73
|
+
// row and the total rows only.
|
|
74
|
+
/** @type {(worksheet: any, row: number, at: number, width: number) => void} */
|
|
75
|
+
let span = (worksheet, row, at, width) => {
|
|
76
|
+
worksheet.mergeCells(row, at, row, at + width - 1);
|
|
77
|
+
};
|
|
78
|
+
|
|
71
79
|
// Freeze everything above the body so the header row stays visible.
|
|
72
80
|
/** @type {(worksheet: any, row: number) => void} */
|
|
73
81
|
let freeze = (worksheet, row) => {
|
|
@@ -93,4 +101,4 @@ let save = async (workbook) => {
|
|
|
93
101
|
: new Uint8Array(out);
|
|
94
102
|
};
|
|
95
103
|
|
|
96
|
-
export { append, create, embed, freeze, mark, place, save, sheet };
|
|
104
|
+
export { append, create, embed, freeze, mark, place, save, sheet, span };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/xlsx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.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.5.0"
|
|
51
51
|
},
|
|
52
52
|
"size-limit": [
|
|
53
53
|
{
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"quario",
|
|
57
57
|
"exceljs"
|
|
58
58
|
],
|
|
59
|
-
"limit": "
|
|
59
|
+
"limit": "3 kB"
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
62
|
"engines": {
|