@quario/xlsx 0.2.0 → 0.3.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 +27 -0
- package/lib/cell.js +49 -10
- package/lib/index.js +9 -4
- package/lib/style.js +50 -2
- package/lib/workbook.js +2 -3
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`format` maps to a number format; the cell stays typed.** `number` is
|
|
15
|
+
`#,##0.00`, `percent` `0.00%`, `date` `yyyy-mm-dd`, `currency` the
|
|
16
|
+
instance currency code as `"USD"#,##0.00`. A kind on the wrong type
|
|
17
|
+
contributes nothing.
|
|
18
|
+
|
|
19
|
+
- **Cell borders map; padding and flow spacing do not.** `solid` is exceljs
|
|
20
|
+
`thin`; `dashed` and `dotted` keep their names. A header-row, row, or
|
|
21
|
+
total-row box fans onto that row's cells; a cell that named any of a
|
|
22
|
+
side's three keys owns that side whole. Padding is unread: a worksheet
|
|
23
|
+
cell has no inset. Flow spacing is unread: a grid has no flow.
|
|
24
|
+
|
|
25
|
+
- **`page.margin` and report-header `height` are unread.** A grid has no
|
|
26
|
+
page top to pin from.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **A table total emits N rows.** Each `total-row` is one worksheet row, as
|
|
31
|
+
each data row is.
|
|
32
|
+
|
|
33
|
+
- **A cell whose display contains a newline wraps.** `wrapText` is this
|
|
34
|
+
target's mapping of a literal newline as a line break, so `"one\ntwo"` is
|
|
35
|
+
two lines in the grid. Single-line cells are unchanged.
|
|
36
|
+
|
|
10
37
|
## [0.2.0] - 2026-09-01
|
|
11
38
|
|
|
12
39
|
### Added
|
package/lib/cell.js
CHANGED
|
@@ -3,24 +3,63 @@
|
|
|
3
3
|
* the engine's `typed` stream rule. `value` is a native number, boolean, or
|
|
4
4
|
* Date when the cell is one bare interpolation (so numbers stay computable in
|
|
5
5
|
* the sheet), and the display-text join otherwise. `format` is the resolved
|
|
6
|
-
* cell-level formatting from the style module.
|
|
6
|
+
* cell-level formatting from the style module. Display text that contains a
|
|
7
|
+
* newline gains `wrapText` so the break is visible in the grid.
|
|
7
8
|
*/
|
|
8
9
|
import { text, typed } from "quario";
|
|
9
|
-
import { format,
|
|
10
|
+
import { format as look, sides } from "./style.js";
|
|
11
|
+
|
|
12
|
+
/** @type {(value: any, fmt: string) => string | null} */
|
|
13
|
+
let whenNum = (value, fmt) => (typeof value === "number" ? fmt : null);
|
|
14
|
+
/** @type {(value: any) => string | null} */
|
|
15
|
+
let whenDate = (value) => (value instanceof Date ? "yyyy-mm-dd" : null);
|
|
16
|
+
/** @type {(value: any, currency: any) => string | null} */
|
|
17
|
+
let whenMoney = (value, currency) =>
|
|
18
|
+
typeof value === "number" && typeof currency === "string" && currency
|
|
19
|
+
? '"' + currency + '"#,##0.00'
|
|
20
|
+
: null;
|
|
21
|
+
/** @type {Record<string, (value: any, currency: any) => string | null>} */
|
|
22
|
+
let NUMFMT = {
|
|
23
|
+
number: (value) => whenNum(value, "#,##0.00"),
|
|
24
|
+
percent: (value) => whenNum(value, "0.00%"),
|
|
25
|
+
date: (value) => whenDate(value),
|
|
26
|
+
currency: (value, currency) => whenMoney(value, currency),
|
|
27
|
+
};
|
|
28
|
+
/** @type {(kind: any, value: any, currency: any) => string | null} */
|
|
29
|
+
let numFmtOf = (kind, value, currency) => {
|
|
30
|
+
let rule = NUMFMT[kind];
|
|
31
|
+
return rule ? rule(value, currency) : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** @type {(cell: { tokens: any[] }) => any} */
|
|
35
|
+
let shownOf = (cell) => {
|
|
36
|
+
let value = typed(cell.tokens);
|
|
37
|
+
return value !== undefined ? value : text(cell.tokens);
|
|
38
|
+
};
|
|
39
|
+
/** @type {(out: any, resolved: any, shown: any, intl: any) => void} */
|
|
40
|
+
let paintFmt = (out, resolved, shown, intl) => {
|
|
41
|
+
let numFmt = numFmtOf(resolved?.format, shown, intl?.currency);
|
|
42
|
+
if (numFmt) out.numFmt = numFmt;
|
|
43
|
+
};
|
|
44
|
+
/** @type {(out: any, shown: any) => void} */
|
|
45
|
+
let paintWrap = (out, shown) => {
|
|
46
|
+
if (typeof shown === "string" && /[\r\n]/.test(shown))
|
|
47
|
+
out.alignment = { ...out.alignment, wrapText: true };
|
|
48
|
+
};
|
|
10
49
|
|
|
11
50
|
/**
|
|
12
51
|
* Resolve one event cell against its enclosing style (a row's style, or an
|
|
13
52
|
* item's band-role default — the layer under the cell's own).
|
|
14
53
|
*
|
|
15
|
-
* @type {(cell: { tokens: any[], style?: any }, under: any) => { value: any, format: any }}
|
|
54
|
+
* @type {(cell: { tokens: any[], style?: any }, under: any, intl?: any) => { value: any, format: any }}
|
|
16
55
|
*/
|
|
17
|
-
let field = (cell, under) => {
|
|
18
|
-
let resolved =
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
56
|
+
let field = (cell, under, intl) => {
|
|
57
|
+
let resolved = sides(under, cell.style);
|
|
58
|
+
let shown = shownOf(cell);
|
|
59
|
+
let out = look(resolved);
|
|
60
|
+
paintFmt(out, resolved, shown, intl);
|
|
61
|
+
paintWrap(out, shown);
|
|
62
|
+
return { value: shown, format: out };
|
|
24
63
|
};
|
|
25
64
|
|
|
26
65
|
export { field };
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { walk } from "quario";
|
|
19
19
|
import { field } from "./cell.js";
|
|
20
|
-
import {
|
|
20
|
+
import { sides as under } from "./style.js";
|
|
21
21
|
import { pixels } from "./image.js";
|
|
22
22
|
import { append, create, embed, freeze, mark, place, save, sheet } from "./workbook.js";
|
|
23
23
|
|
|
@@ -85,6 +85,8 @@ export function xlsx(options) {
|
|
|
85
85
|
// default if the role carries one. `base` is the first two, settled once
|
|
86
86
|
// per render off `report-start`.
|
|
87
87
|
let base = BASELINE;
|
|
88
|
+
/** @type {{ locale?: string, currency?: string, timeZone?: string } | null} */
|
|
89
|
+
let intl = null;
|
|
88
90
|
// One cell, wearing everything beneath it. Every cell in the sheet goes
|
|
89
91
|
// through here rather than through `field` directly, so `base` -- this
|
|
90
92
|
// target's baseline plus the author's report default -- cannot be
|
|
@@ -92,7 +94,7 @@ export function xlsx(options) {
|
|
|
92
94
|
// marking is the one deliberate exception, and says so where it is
|
|
93
95
|
// written.
|
|
94
96
|
let cell = (/** @type {any} */ value, /** @type {any} */ over = null) =>
|
|
95
|
-
field(value, under(base, over));
|
|
97
|
+
field(value, under(base, over), intl);
|
|
96
98
|
// The band-role default an event sits under, if its role carries one.
|
|
97
99
|
let roleOf = (/** @type {any} */ event) =>
|
|
98
100
|
Object.hasOwn(ROLES, event.role) ? ROLES[event.role] : null;
|
|
@@ -112,6 +114,7 @@ export function xlsx(options) {
|
|
|
112
114
|
// The report default over this target's baseline. The marking is
|
|
113
115
|
// written under the baseline alone: an author's `style` must not be
|
|
114
116
|
// able to resize it (docs/adr/0002).
|
|
117
|
+
intl = { locale: event.locale, currency: event.currency, timeZone: event.timeZone };
|
|
115
118
|
if (event.style) base = under(BASELINE, event.style);
|
|
116
119
|
if (event.marking) {
|
|
117
120
|
mark(workbook, event.marking);
|
|
@@ -178,7 +181,9 @@ export function xlsx(options) {
|
|
|
178
181
|
place(worksheet, known.id, known, append(worksheet, []));
|
|
179
182
|
},
|
|
180
183
|
"table-start": (event) => {
|
|
181
|
-
let headers = event.columns.map((/** @type {any} */ column) =>
|
|
184
|
+
let headers = event.columns.map((/** @type {any} */ column) =>
|
|
185
|
+
cell(column.header, event.style),
|
|
186
|
+
);
|
|
182
187
|
let row = append(worksheet, headers);
|
|
183
188
|
if (!frozen) {
|
|
184
189
|
freeze(worksheet, row);
|
|
@@ -186,7 +191,7 @@ export function xlsx(options) {
|
|
|
186
191
|
}
|
|
187
192
|
},
|
|
188
193
|
row: (event) => record(event, event.style),
|
|
189
|
-
"total-row": (event) => record(event,
|
|
194
|
+
"total-row": (event) => record(event, event.style),
|
|
190
195
|
});
|
|
191
196
|
return save(workbook);
|
|
192
197
|
};
|
package/lib/style.js
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
/** @type {(value: any) => boolean} */
|
|
14
14
|
let finite = (value) => Number.isFinite(value);
|
|
15
15
|
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
16
|
+
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
17
|
+
let BORDER_PARTS = ["Width", "Style", "Color"];
|
|
18
|
+
/** @type {Record<string, string>} */
|
|
19
|
+
let LINE = { solid: "thin", dashed: "dashed", dotted: "dotted" };
|
|
16
20
|
|
|
17
21
|
// The generic families as the fonts spreadsheet apps ship with; any other
|
|
18
22
|
// name passes through verbatim for the host application to resolve.
|
|
@@ -29,9 +33,28 @@ let argb = (value) => {
|
|
|
29
33
|
return "FF" + digits.toUpperCase();
|
|
30
34
|
};
|
|
31
35
|
|
|
32
|
-
// Style blocks layer outward-in: row under cell.
|
|
36
|
+
// Style blocks layer outward-in: row under cell. A border side is won whole
|
|
37
|
+
// by the inner block when that block named any of its three keys, so a cell
|
|
38
|
+
// that failed-soft on a side does not pick up the row's other two names.
|
|
33
39
|
/** @type {(under: any, over: any) => any} */
|
|
34
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
|
+
};
|
|
35
58
|
|
|
36
59
|
/** @type {(out: any, key: string, value: any) => any} */
|
|
37
60
|
let put = (out, key, value) => {
|
|
@@ -91,12 +114,37 @@ let fill = (value) => {
|
|
|
91
114
|
let align = (value) =>
|
|
92
115
|
value === "left" || value === "center" || value === "right" ? { horizontal: value } : null;
|
|
93
116
|
|
|
117
|
+
/** @type {(width: any) => boolean} */
|
|
118
|
+
let isStroke = (width) => finite(width) && width > 0;
|
|
119
|
+
/** @type {(named: any) => string | null} */
|
|
120
|
+
let lineOf = (named) => {
|
|
121
|
+
if (typeof named !== "string") return null;
|
|
122
|
+
return Object.hasOwn(LINE, named) ? LINE[named] : null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** @type {(style: any, side: string) => any} */
|
|
126
|
+
let edge = (style, side) => {
|
|
127
|
+
let line = lineOf(style["border" + side + "Style"]);
|
|
128
|
+
let color = argb(style["border" + side + "Color"]);
|
|
129
|
+
if (!isStroke(style["border" + side + "Width"]) || !line || !color) return null;
|
|
130
|
+
return { style: line, color: { argb: color } };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** @type {(style: any) => any} */
|
|
134
|
+
let border = (style) => {
|
|
135
|
+
let out = null;
|
|
136
|
+
for (let side of SIDES) out = put(out, side.toLowerCase(), edge(style, side));
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
|
|
94
140
|
// The whole-cell reading: the font plus the parts only a cell can carry.
|
|
141
|
+
// Padding is unread: a worksheet cell has no inset (docs/adr/0008).
|
|
95
142
|
/** @type {(style: any) => any} */
|
|
96
143
|
let format = (style) => {
|
|
97
144
|
let out = put(null, "font", font(style));
|
|
98
145
|
out = put(out, "fill", fill(style.background));
|
|
146
|
+
out = put(out, "border", border(style));
|
|
99
147
|
return put(out, "alignment", align(style.align));
|
|
100
148
|
};
|
|
101
149
|
|
|
102
|
-
export { format,
|
|
150
|
+
export { format, sides };
|
package/lib/workbook.js
CHANGED
|
@@ -32,11 +32,10 @@ let create = (meta) => {
|
|
|
32
32
|
/** @type {(workbook: any) => any} */
|
|
33
33
|
let sheet = (workbook) => workbook.addWorksheet("Report");
|
|
34
34
|
|
|
35
|
+
let SLOTS = ["font", "fill", "border", "alignment", "numFmt"];
|
|
35
36
|
/** @type {(cell: any, format: any) => void} */
|
|
36
37
|
let paint = (cell, format) => {
|
|
37
|
-
if (format
|
|
38
|
-
if (format.fill) cell.fill = format.fill;
|
|
39
|
-
if (format.alignment) cell.alignment = format.alignment;
|
|
38
|
+
for (let key of SLOTS) if (format[key]) cell[key] = format[key];
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
// Write one worksheet row from cell descriptors; returns the row number.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/xlsx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.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.3.0"
|
|
51
51
|
},
|
|
52
52
|
"size-limit": [
|
|
53
53
|
{
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"quario",
|
|
57
57
|
"exceljs"
|
|
58
58
|
],
|
|
59
|
-
"limit": "2 kB"
|
|
59
|
+
"limit": "2.4 kB"
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
62
|
"engines": {
|