werkmap 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/README.md +10 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +107 -8
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -151,6 +151,16 @@ Rich text is a **bare array** — there is no wrapper object, because nothing el
|
|
|
151
151
|
|
|
152
152
|
One merged range across `width` columns of `row`, starting at the 1-based column `at`.
|
|
153
153
|
|
|
154
|
+
### `sheet.link(row, at, target)`
|
|
155
|
+
|
|
156
|
+
Link the cell at 1-based `row` and column `at`. `target` names exactly one of the two destinations the format has: `{ url }`, somewhere outside this workbook, or `{ location }`, a reference inside it — a cell as `'Sheet2'!A1`, or a defined name.
|
|
157
|
+
|
|
158
|
+
Linking **writes no cell**. What a reader shows is whatever the row already put there, which is why the row must exist first, as a merge's must. A reader holds one link per cell, so a second link on the same cell throws rather than quietly replacing the first, and an empty destination throws rather than becoming a link onto nothing.
|
|
159
|
+
|
|
160
|
+
Cells pointing at the same `url` share one relationship, however many of them there are — a column of a thousand rows linking one address writes one. An internal `location` needs no relationship at all.
|
|
161
|
+
|
|
162
|
+
A worksheet that never calls this carries **no `hyperlinks` element**, and no relationship part unless it also places a picture.
|
|
163
|
+
|
|
154
164
|
### `sheet.freeze(rows)`
|
|
155
165
|
|
|
156
166
|
Freeze the top `rows` rows. `0` clears.
|
package/lib/index.d.ts
CHANGED
|
@@ -165,6 +165,13 @@ export interface RowOptions {
|
|
|
165
165
|
collapsed?: boolean;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Where one linked cell points: outside the workbook, or inside it. A
|
|
170
|
+
* `location` is a reference this file can resolve — a cell as `'Sheet2'!A1`,
|
|
171
|
+
* or a defined name.
|
|
172
|
+
*/
|
|
173
|
+
export type LinkTarget = { url: string } | { location: string };
|
|
174
|
+
|
|
168
175
|
export interface Sheet {
|
|
169
176
|
/** This sheet's name. */
|
|
170
177
|
readonly name: string;
|
|
@@ -181,6 +188,15 @@ export interface Sheet {
|
|
|
181
188
|
* Throws on a width below 2, an overlap, or a row that does not exist yet.
|
|
182
189
|
*/
|
|
183
190
|
merge(row: number, at: number, width: number): void;
|
|
191
|
+
/**
|
|
192
|
+
* Link one cell: a destination outside this workbook (`url`) or a reference
|
|
193
|
+
* inside it (`location`), exactly one of the two.
|
|
194
|
+
*
|
|
195
|
+
* Linking writes no cell — a reader shows whatever the row already put
|
|
196
|
+
* there — so the row must exist first, as a merge's must. A reader holds
|
|
197
|
+
* one link per cell, so a second on the same cell throws.
|
|
198
|
+
*/
|
|
199
|
+
link(row: number, at: number, target: LinkTarget): void;
|
|
184
200
|
/** Freeze the top `count` rows. `0` clears. */
|
|
185
201
|
freeze(count: number): void;
|
|
186
202
|
/**
|
package/lib/index.js
CHANGED
|
@@ -1116,6 +1116,59 @@ const requireFilter = (range, rows, columns) => {
|
|
|
1116
1116
|
return { top, left, bottom, right };
|
|
1117
1117
|
};
|
|
1118
1118
|
|
|
1119
|
+
/**
|
|
1120
|
+
* @typedef {{ url: string } | { location: string }} LinkTarget
|
|
1121
|
+
* @typedef {{ ref: string } & LinkTarget} Link
|
|
1122
|
+
*/
|
|
1123
|
+
|
|
1124
|
+
// A link names one of the two: a destination outside the workbook, or a
|
|
1125
|
+
// reference inside it. Both at once names two destinations and neither names
|
|
1126
|
+
// none, so the count is what this checks rather than each key in turn.
|
|
1127
|
+
/** @param {unknown} target @returns {LinkTarget} */
|
|
1128
|
+
const requireTarget = (target) => {
|
|
1129
|
+
const url = /** @type {any} */ (target)?.url;
|
|
1130
|
+
const location = /** @type {any} */ (target)?.location;
|
|
1131
|
+
if ((url === undefined) === (location === undefined))
|
|
1132
|
+
throw RangeError(
|
|
1133
|
+
`link: expected exactly one of url and location, got ${JSON.stringify(target)}`,
|
|
1134
|
+
);
|
|
1135
|
+
if (url === undefined) return { location: requireDestination(location, "link: location") };
|
|
1136
|
+
return { url: requireDestination(url, "link: url") };
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// A destination is a string with something in it: an empty one is a link a
|
|
1140
|
+
// reader opens onto nothing, which is a caller's mistake rather than a link.
|
|
1141
|
+
/** @param {unknown} value @param {string} where */
|
|
1142
|
+
const requireDestination = (value, where) => {
|
|
1143
|
+
const text = requireString(value, where);
|
|
1144
|
+
if (text === "") throw RangeError(`${where}: expected a destination, got an empty string`);
|
|
1145
|
+
return text;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// The destinations outside the workbook, deduplicated and in first-seen
|
|
1149
|
+
// order: one relationship each, however many cells point at it. The same walk
|
|
1150
|
+
// answers for the sheet part and for its relationships, so the two cannot
|
|
1151
|
+
// disagree about which id a link carries.
|
|
1152
|
+
/** @param {ReadonlyArray<Link>} links */
|
|
1153
|
+
const externals = (links) => [
|
|
1154
|
+
...new Set(links.filter((link) => "url" in link).map((link) => /** @type {any} */ (link).url)),
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1157
|
+
// Every link of one sheet, in call order. An external one points at a
|
|
1158
|
+
// relationship of this worksheet's own part; an internal one carries its
|
|
1159
|
+
// reference and needs none. `first` is the id the relationships start at,
|
|
1160
|
+
// which is after the drawing where the sheet has one.
|
|
1161
|
+
/** @param {ReadonlyArray<Link>} links @param {number} first */
|
|
1162
|
+
const hyperlinksXml = (links, first) => {
|
|
1163
|
+
if (links.length === 0) return "";
|
|
1164
|
+
const targets = externals(links);
|
|
1165
|
+
const one = (/** @type {Link} */ link) =>
|
|
1166
|
+
"url" in link
|
|
1167
|
+
? `<hyperlink ref="${link.ref}" r:id="rId${first + targets.indexOf(link.url)}"/>`
|
|
1168
|
+
: `<hyperlink ref="${link.ref}" location="${esc(link.location)}"/>`;
|
|
1169
|
+
return `<hyperlinks>${links.map(one).join("")}</hyperlinks>`;
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1119
1172
|
/** @param {ReadonlyArray<Range>} merges */
|
|
1120
1173
|
const mergesXml = (merges) =>
|
|
1121
1174
|
merges.length === 0
|
|
@@ -1232,6 +1285,8 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
1232
1285
|
const merges = [];
|
|
1233
1286
|
/** @type {Array<{ id: number, row: number, col: number, width: number, height: number }>} */
|
|
1234
1287
|
const pictures = [];
|
|
1288
|
+
/** @type {Link[]} */
|
|
1289
|
+
const links = [];
|
|
1235
1290
|
let frozen = 0;
|
|
1236
1291
|
let widest = 1;
|
|
1237
1292
|
// The deepest outline level any row carries; 0 writes no `sheetFormatPr`.
|
|
@@ -1306,6 +1361,34 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
1306
1361
|
widest = Math.max(widest, range.right);
|
|
1307
1362
|
},
|
|
1308
1363
|
|
|
1364
|
+
/**
|
|
1365
|
+
* Link one cell. `target` names exactly one of `url`, a destination
|
|
1366
|
+
* outside this workbook, and `location`, a reference inside it -- a cell
|
|
1367
|
+
* as `'Sheet2'!A1`, or a defined name.
|
|
1368
|
+
*
|
|
1369
|
+
* Linking writes no cell: what a reader shows is whatever the row already
|
|
1370
|
+
* put there, which is why the row must exist first, as a merge's must. A
|
|
1371
|
+
* reader holds one link per cell, so a second on the same cell throws
|
|
1372
|
+
* rather than quietly replacing the first.
|
|
1373
|
+
*
|
|
1374
|
+
* @param {number} row 1-based
|
|
1375
|
+
* @param {number} at 1-based column
|
|
1376
|
+
* @param {LinkTarget} target
|
|
1377
|
+
*/
|
|
1378
|
+
link(row, at, target) {
|
|
1379
|
+
requireIndex(row, MAX_ROW, "link: row");
|
|
1380
|
+
requireIndex(at, MAX_COLUMN, "link: at");
|
|
1381
|
+
requireWritten(row, rows.length, "row", "link");
|
|
1382
|
+
const ref = `${letters(at)}${row}`;
|
|
1383
|
+
if (links.some((other) => other.ref === ref))
|
|
1384
|
+
throw RangeError(`link: ${ref} already carries a link`);
|
|
1385
|
+
links.push({ ref, ...requireTarget(target) });
|
|
1386
|
+
},
|
|
1387
|
+
|
|
1388
|
+
get links() {
|
|
1389
|
+
return links;
|
|
1390
|
+
},
|
|
1391
|
+
|
|
1309
1392
|
/**
|
|
1310
1393
|
* How this worksheet prints. Every key is optional, and a worksheet that
|
|
1311
1394
|
* never calls this carries no print setup at all: a reader's own defaults
|
|
@@ -1409,6 +1492,7 @@ const worksheet = (name, styles, sst, knows) => {
|
|
|
1409
1492
|
`<sheetData>${rows.join("")}</sheetData>` +
|
|
1410
1493
|
filtered +
|
|
1411
1494
|
mergesXml(merges) +
|
|
1495
|
+
hyperlinksXml(links, drawing === null ? 1 : drawing + 1) +
|
|
1412
1496
|
printXml(printing) +
|
|
1413
1497
|
(drawing === null ? "" : `<drawing r:id="rId${drawing}"/>`) +
|
|
1414
1498
|
`</worksheet>`
|
|
@@ -1608,9 +1692,28 @@ const workbookRelsXml = (sheets) =>
|
|
|
1608
1692
|
* @typedef {{ sheet: ReturnType<typeof worksheet>, index: number }} Drawing
|
|
1609
1693
|
*/
|
|
1610
1694
|
|
|
1695
|
+
// What one worksheet's own part points at: its drawing, where it has one, and
|
|
1696
|
+
// then a relationship per destination outside the workbook. The drawing leads
|
|
1697
|
+
// because it did before links existed, and `hyperlinksXml` counts from the
|
|
1698
|
+
// same place.
|
|
1699
|
+
/** @param {number} at @param {ReadonlyArray<Link>} links */
|
|
1700
|
+
const sheetRels = (at, links) =>
|
|
1701
|
+
DECLARATION +
|
|
1702
|
+
`<Relationships xmlns="${NS_PKG_REL}">` +
|
|
1703
|
+
(at === -1
|
|
1704
|
+
? ""
|
|
1705
|
+
: `<Relationship Id="rId1" Type="${REL}/drawing" Target="../drawings/drawing${at + 1}.xml"/>`) +
|
|
1706
|
+
externals(links)
|
|
1707
|
+
.map(
|
|
1708
|
+
(url, index) =>
|
|
1709
|
+
`<Relationship Id="rId${(at === -1 ? 1 : 2) + index}" Type="${REL}/hyperlink" Target="${esc(url)}" TargetMode="External"/>`,
|
|
1710
|
+
)
|
|
1711
|
+
.join("") +
|
|
1712
|
+
`</Relationships>`;
|
|
1713
|
+
|
|
1611
1714
|
/**
|
|
1612
|
-
* Every worksheet part, and the relationship part of each sheet that
|
|
1613
|
-
* a drawing.
|
|
1715
|
+
* Every worksheet part, and the relationship part of each sheet that points
|
|
1716
|
+
* at anything of its own — a drawing, a link, or both.
|
|
1614
1717
|
* @param {Part} part
|
|
1615
1718
|
* @param {ReadonlyArray<ReturnType<typeof worksheet>>} sheets
|
|
1616
1719
|
* @param {ReadonlyArray<Drawing>} drawings
|
|
@@ -1619,12 +1722,8 @@ const sheetParts = (part, sheets, drawings) => {
|
|
|
1619
1722
|
for (const [index, sheet] of sheets.entries()) {
|
|
1620
1723
|
const at = drawings.findIndex((each) => each.index === index);
|
|
1621
1724
|
part(`xl/worksheets/sheet${index + 1}.xml`, sheet.xml(at === -1 ? null : 1));
|
|
1622
|
-
if (at !== -1)
|
|
1623
|
-
part(
|
|
1624
|
-
`xl/worksheets/_rels/sheet${index + 1}.xml.rels`,
|
|
1625
|
-
DECLARATION +
|
|
1626
|
-
`<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${REL}/drawing" Target="../drawings/drawing${at + 1}.xml"/></Relationships>`,
|
|
1627
|
-
);
|
|
1725
|
+
if (at !== -1 || externals(sheet.links).length > 0)
|
|
1726
|
+
part(`xl/worksheets/_rels/sheet${index + 1}.xml.rels`, sheetRels(at, sheet.links));
|
|
1628
1727
|
}
|
|
1629
1728
|
};
|
|
1630
1729
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "werkmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Tiny, CSP-safe OOXML spreadsheet writer. Write-only, zero dependencies, byte-identical output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"check": "run-s fmt:check lint fallow size test test:browser",
|
|
27
|
+
"check": "run-s fmt:check lint fallow:lint size test fallow:health test:browser",
|
|
28
28
|
"bench": "node --disallow-code-generation-from-strings --expose-gc bench/index.js",
|
|
29
29
|
"commitlint": "commitlint",
|
|
30
|
-
"fallow": "fallow",
|
|
30
|
+
"fallow": "run-s fallow:lint fallow:health",
|
|
31
|
+
"fallow:health": "fallow health --coverage coverage/coverage-final.json",
|
|
32
|
+
"fallow:lint": "fallow dead-code && fallow dupes",
|
|
31
33
|
"fmt": "oxfmt",
|
|
32
34
|
"fmt:check": "oxfmt --check",
|
|
33
35
|
"lint": "oxlint",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"size-limit": [
|
|
58
60
|
{
|
|
59
61
|
"path": "lib/index.js",
|
|
60
|
-
"limit": "8 kB"
|
|
62
|
+
"limit": "8.5 kB"
|
|
61
63
|
}
|
|
62
64
|
],
|
|
63
65
|
"engines": {
|