werkmap 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/README.md +8 -4
- package/lib/index.d.ts +13 -2
- package/lib/index.js +46 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# werkmap
|
|
2
2
|
|
|
3
|
-
A tiny spreadsheet writer for JavaScript. It writes an `.xlsx` file — rows, typed cells, styles, merged ranges, a frozen header, floating images — and nothing else. It does not read one. _Werkmap_ is Dutch for a workbook, which is the one thing this package makes.
|
|
3
|
+
A tiny spreadsheet writer for JavaScript. It writes an `.xlsx` file — rows, typed cells, styles, merged ranges, a frozen header, outline levels, floating images — and nothing else. It does not read one. _Werkmap_ is Dutch for a workbook, which is the one thing this package makes.
|
|
4
4
|
|
|
5
5
|
Writing is the whole surface, so the package stays small enough to audit. The container is written against the ZIP specification over `CompressionStream`, every part is a string, and no value ever turns into code — so it runs unchanged under a Content Security Policy with no `unsafe-*` of any kind, which is what a spreadsheet export in the browser usually cannot do.
|
|
6
6
|
|
|
7
7
|
- **Byte-identical output.** Nothing consults a clock, a locale or a random source, and the ZIP epoch is pinned, so two renders of the same report are the same bytes and a build that caches by content hash keeps working.
|
|
8
8
|
- **Foreign readers accept it.** The suite loads every workbook it writes back through ExcelJS, an independent implementation — so the tests prove a real reader opens the file, not just that the bytes look plausible.
|
|
9
|
-
- **Zero dependencies.** 7.
|
|
9
|
+
- **Zero dependencies.** 7.1 kB minified and brotlied, for the whole writer.
|
|
10
10
|
- **Strict CSP, including in the browser.** No `unsafe-eval` and no `unsafe-inline`. A Chromium page under `default-src 'none'; script-src 'self'` writes a workbook and reports any violation back, and the Node suite runs on `--disallow-code-generation-from-strings`.
|
|
11
11
|
- **Write-only, deliberately.** No reader, no formula engine, no chart support — see [Is werkmap the right tool?](#is-werkmap-the-right-tool) before you install it.
|
|
12
12
|
- **Hardened.** 72 tests at 100% branch coverage.
|
|
@@ -114,10 +114,14 @@ Any number of sheets, in call order. Throws on a name a reader refuses: empty, o
|
|
|
114
114
|
|
|
115
115
|
The finished package. This does not seal the document: call it as often as you like, and the same call sequence yields the same bytes.
|
|
116
116
|
|
|
117
|
-
### `sheet.row(cells) -> number`
|
|
117
|
+
### `sheet.row(cells, options?) -> number`
|
|
118
118
|
|
|
119
119
|
`cells` is an array; the returned number is the row's 1-based position, which `merge`, `freeze` and `place` take.
|
|
120
120
|
|
|
121
|
+
`options.level` is the row's **outline level**, an integer from 0 to 7. A reader draws the levels as collapsible groups in its left margin — the way Excel's own Group command does — and reads the summary row as the one **below** each group, which is where a total row sits. `0`, the default, is a row outside any group, and a row given no options is at 0. Nothing is hidden or collapsed: every row you wrote is in the document and open, and what the reader does with the controls is theirs.
|
|
122
|
+
|
|
123
|
+
A sheet that outlines any row also states the workbook's default row height, 15 points, because the format requires it beside the deepest level. A sheet with no outline states neither, and a reader keeps its own default. That is the one place this writer names a row height, and it names the default rather than one of its own.
|
|
124
|
+
|
|
121
125
|
Each element is `{ value, style }`, or `null` for an empty unstyled cell. `{ value: null, style }` is an empty **styled** cell, which is what a merged span's remaining columns need.
|
|
122
126
|
|
|
123
127
|
`value` is one of:
|
|
@@ -151,7 +155,7 @@ The list **replaces** rather than merging, and an empty list clears — a hole i
|
|
|
151
155
|
|
|
152
156
|
What is written is the number you gave, verbatim. Excel may report a slightly different one after a round trip, because it re-derives a width from the default font's digit width — that is the reader's arithmetic, not this writer's.
|
|
153
157
|
|
|
154
|
-
There is no `hidden`, no outline level and no per-column style: a zero width is the back door to a hidden column, so `0` throws and points at `null`.
|
|
158
|
+
There is no `hidden`, no column outline level and no per-column style: a zero width is the back door to a hidden column, so `0` throws and points at `null`.
|
|
155
159
|
|
|
156
160
|
### `sheet.filter(range)`
|
|
157
161
|
|
package/lib/index.d.ts
CHANGED
|
@@ -99,14 +99,25 @@ export interface FilterRange {
|
|
|
99
99
|
right: number;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** What a row says about itself beyond its cells. */
|
|
103
|
+
export interface RowOptions {
|
|
104
|
+
/**
|
|
105
|
+
* The row's outline level, an integer from 0 to 7. A reader draws the
|
|
106
|
+
* levels as collapsible groups in its margin, with the summary row read as
|
|
107
|
+
* the one below the group. `0`, the default, is a row outside any group.
|
|
108
|
+
*/
|
|
109
|
+
level?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
export interface Sheet {
|
|
103
113
|
/** This sheet's name. */
|
|
104
114
|
readonly name: string;
|
|
105
115
|
/**
|
|
106
116
|
* Append a row. `null` is an empty unstyled cell. Returns the row's 1-based
|
|
107
|
-
* position, which `merge` and `place` take.
|
|
117
|
+
* position, which `merge` and `place` take. `options.level` puts the row at
|
|
118
|
+
* an outline level.
|
|
108
119
|
*/
|
|
109
|
-
row(cells: readonly (Cell | null)[]): number;
|
|
120
|
+
row(cells: readonly (Cell | null)[], options?: RowOptions): number;
|
|
110
121
|
/**
|
|
111
122
|
* Merge `width` columns of `row`, starting at the 1-based column `at`.
|
|
112
123
|
* Throws on a width below 2, an overlap, or a row that does not exist yet.
|
package/lib/index.js
CHANGED
|
@@ -730,6 +730,41 @@ const MAX_WIDTH = 255;
|
|
|
730
730
|
// EMU per CSS pixel at 96 dpi.
|
|
731
731
|
const EMU = 9525;
|
|
732
732
|
|
|
733
|
+
// Excel's ceiling for a row outline level.
|
|
734
|
+
const MAX_LEVEL = 7;
|
|
735
|
+
|
|
736
|
+
/** @param {unknown} level */
|
|
737
|
+
const isLevel = (level) => level === 0 || isIndex(level, MAX_LEVEL);
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* A row's outline level, or 0 for a row that said nothing.
|
|
741
|
+
* @param {unknown} options
|
|
742
|
+
*/
|
|
743
|
+
const requireLevel = (options) => {
|
|
744
|
+
const level = requireRecord(options ?? {}, "row: options", "{ level }").level ?? 0;
|
|
745
|
+
if (!isLevel(level))
|
|
746
|
+
throw RangeError(
|
|
747
|
+
`row: level: expected an integer between 0 and ${MAX_LEVEL}, got ${JSON.stringify(level)}`,
|
|
748
|
+
);
|
|
749
|
+
return /** @type {number} */ (level);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* A row's opening tag: the level rides along only where there is one.
|
|
754
|
+
* @param {number} at
|
|
755
|
+
* @param {number} level
|
|
756
|
+
*/
|
|
757
|
+
const rowOpen = (at, level) => `<row r="${at}"${level === 0 ? "" : ` outlineLevel="${level}"`}>`;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The sheet's `sheetFormatPr`, or nothing for a sheet that outlines no row.
|
|
761
|
+
* The format requires a default row height beside the level count; 15 is
|
|
762
|
+
* Excel's own for the default 11-point font.
|
|
763
|
+
* @param {number} deepest
|
|
764
|
+
*/
|
|
765
|
+
const formatXml = (deepest) =>
|
|
766
|
+
deepest === 0 ? "" : `<sheetFormatPr defaultRowHeight="15" outlineLevelRow="${deepest}"/>`;
|
|
767
|
+
|
|
733
768
|
/**
|
|
734
769
|
* A `Date` as the serial Excel stores. An invalid one has no serial to give.
|
|
735
770
|
* @param {Date} date
|
|
@@ -982,6 +1017,8 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
982
1017
|
const pictures = [];
|
|
983
1018
|
let frozen = 0;
|
|
984
1019
|
let widest = 1;
|
|
1020
|
+
// The deepest outline level any row carries; 0 writes no `sheetFormatPr`.
|
|
1021
|
+
let deepest = 0;
|
|
985
1022
|
// This sheet's `autoFilter` element, or the empty string for a sheet that
|
|
986
1023
|
// asked for none. Rendered where it is set, the way a row is.
|
|
987
1024
|
let filtered = "";
|
|
@@ -1015,18 +1052,23 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
1015
1052
|
return {
|
|
1016
1053
|
name,
|
|
1017
1054
|
|
|
1018
|
-
/**
|
|
1019
|
-
|
|
1055
|
+
/**
|
|
1056
|
+
* @param {ReadonlyArray<unknown>} cells
|
|
1057
|
+
* @param {unknown} [options]
|
|
1058
|
+
*/
|
|
1059
|
+
row(cells, options) {
|
|
1020
1060
|
if (!Array.isArray(cells))
|
|
1021
1061
|
throw TypeError(`row: expected an array of cells, got ${JSON.stringify(cells)}`);
|
|
1022
1062
|
const at = rows.length + 1;
|
|
1023
1063
|
if (cells.length > MAX_COLUMN)
|
|
1024
1064
|
throw RangeError(`row: a sheet holds at most ${MAX_COLUMN} columns`);
|
|
1065
|
+
const level = requireLevel(options);
|
|
1066
|
+
deepest = Math.max(deepest, level);
|
|
1025
1067
|
|
|
1026
1068
|
let body = "";
|
|
1027
1069
|
for (let index = 0; index < cells.length; index++) body += cellAt(cells[index], index, at);
|
|
1028
1070
|
widest = Math.max(widest, cells.length);
|
|
1029
|
-
rows.push(
|
|
1071
|
+
rows.push(rowOpen(at, level) + body + "</row>");
|
|
1030
1072
|
return at;
|
|
1031
1073
|
},
|
|
1032
1074
|
|
|
@@ -1145,6 +1187,7 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
1145
1187
|
properties +
|
|
1146
1188
|
`<dimension ref="${dimension}"/>` +
|
|
1147
1189
|
`<sheetViews>${paneXml(frozen)}</sheetViews>` +
|
|
1190
|
+
formatXml(deepest) +
|
|
1148
1191
|
colsXml(columns) +
|
|
1149
1192
|
`<sheetData>${rows.join("")}</sheetData>` +
|
|
1150
1193
|
filtered +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "werkmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Tiny, CSP-safe OOXML spreadsheet writer. Write-only, zero dependencies, byte-identical output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"size-limit": [
|
|
58
58
|
{
|
|
59
59
|
"path": "lib/index.js",
|
|
60
|
-
"limit": "7 kB"
|
|
60
|
+
"limit": "7.5 kB"
|
|
61
61
|
}
|
|
62
62
|
],
|
|
63
63
|
"engines": {
|