@office-kit/xlsx 0.15.0 → 0.17.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.
Files changed (66) hide show
  1. package/README.md +29 -8
  2. package/dist/cell/cell.d.ts +12 -19
  3. package/dist/{cell-Df3cKRIl.mjs → cell-B5OUUdfI.mjs} +13 -7
  4. package/dist/cell-B5OUUdfI.mjs.map +1 -0
  5. package/dist/{cell-style-CNsET6WU.mjs → cell-style-DjKKPWDL.mjs} +2 -2
  6. package/dist/{cell-style-CNsET6WU.mjs.map → cell-style-DjKKPWDL.mjs.map} +1 -1
  7. package/dist/cell.mjs +1 -1
  8. package/dist/{coordinate-Bt5Gni2I.mjs → coordinate-CxC88uBk.mjs} +31 -2
  9. package/dist/coordinate-CxC88uBk.mjs.map +1 -0
  10. package/dist/{defined-names-Ctu3F6ls.mjs → defined-names-Cmp8rbvI.mjs} +2 -2
  11. package/dist/{defined-names-Ctu3F6ls.mjs.map → defined-names-Cmp8rbvI.mjs.map} +1 -1
  12. package/dist/{differential-BPKa7wXi.mjs → differential-7KGzbliB.mjs} +2 -2
  13. package/dist/{differential-BPKa7wXi.mjs.map → differential-7KGzbliB.mjs.map} +1 -1
  14. package/dist/{drawing-ZJ3h4VHD.mjs → drawing-Cv6UtInY.mjs} +2 -2
  15. package/dist/{drawing-ZJ3h4VHD.mjs.map → drawing-Cv6UtInY.mjs.map} +1 -1
  16. package/dist/drawing.mjs +1 -1
  17. package/dist/{escape-rnWSPpWY.mjs → escape-rA5UpibT.mjs} +26 -3
  18. package/dist/escape-rA5UpibT.mjs.map +1 -0
  19. package/dist/io/load.d.ts +28 -0
  20. package/dist/io.mjs +2 -2
  21. package/dist/{load-CspGc399.mjs → load-DIdQ_zum.mjs} +66 -39
  22. package/dist/{load-CspGc399.mjs.map → load-DIdQ_zum.mjs.map} +1 -1
  23. package/dist/{manifest-pLmx7KYh.mjs → manifest-BHUYpjUG.mjs} +3 -3
  24. package/dist/{manifest-pLmx7KYh.mjs.map → manifest-BHUYpjUG.mjs.map} +1 -1
  25. package/dist/node.mjs +1 -1
  26. package/dist/packaging.mjs +1 -1
  27. package/dist/{reader-DHMxLBQV.mjs → reader-pC7bxYAs.mjs} +30 -7
  28. package/dist/reader-pC7bxYAs.mjs.map +1 -0
  29. package/dist/{save-DgPf1DyF.mjs → save-ax11Gsw0.mjs} +7 -7
  30. package/dist/{save-DgPf1DyF.mjs.map → save-ax11Gsw0.mjs.map} +1 -1
  31. package/dist/schema.mjs +1 -1
  32. package/dist/{serialize-BC2Wu3bR.mjs → serialize-Dmb6Vtt9.mjs} +23 -6
  33. package/dist/serialize-Dmb6Vtt9.mjs.map +1 -0
  34. package/dist/{serializer-C6nm0mRJ.mjs → serializer-dhv07Z6F.mjs} +2 -2
  35. package/dist/{serializer-C6nm0mRJ.mjs.map → serializer-dhv07Z6F.mjs.map} +1 -1
  36. package/dist/streaming/string-table.d.ts +7 -0
  37. package/dist/streaming.mjs +189 -83
  38. package/dist/streaming.mjs.map +1 -1
  39. package/dist/styles.mjs +3 -3
  40. package/dist/{stylesheet-writer-TxsinwbO.mjs → stylesheet-writer-BBYjNSMA.mjs} +201 -118
  41. package/dist/stylesheet-writer-BBYjNSMA.mjs.map +1 -0
  42. package/dist/{table-BrP0RwUd.mjs → table-BFi2WmgG.mjs} +2 -2
  43. package/dist/{table-BrP0RwUd.mjs.map → table-BFi2WmgG.mjs.map} +1 -1
  44. package/dist/utils/cell-number.d.ts +10 -0
  45. package/dist/utils/coordinate.d.ts +11 -0
  46. package/dist/utils/escape.d.ts +17 -0
  47. package/dist/utils/xsd-boolean.d.ts +9 -0
  48. package/dist/utils.mjs +2 -2
  49. package/dist/workbook/shared-strings.d.ts +2 -0
  50. package/dist/{workbook-DQCslHzY.mjs → workbook-CMel6UcI.mjs} +13 -12
  51. package/dist/workbook-CMel6UcI.mjs.map +1 -0
  52. package/dist/workbook.mjs +2 -2
  53. package/dist/worksheet/writer.d.ts +9 -5
  54. package/dist/{worksheet-B2NOQ6dM.mjs → worksheet-Cxn2c-jz.mjs} +3 -3
  55. package/dist/{worksheet-B2NOQ6dM.mjs.map → worksheet-Cxn2c-jz.mjs.map} +1 -1
  56. package/dist/worksheet.mjs +2 -2
  57. package/dist/xml.mjs +2 -2
  58. package/dist/zip.mjs +1 -1
  59. package/package.json +1 -1
  60. package/dist/cell-Df3cKRIl.mjs.map +0 -1
  61. package/dist/coordinate-Bt5Gni2I.mjs.map +0 -1
  62. package/dist/escape-rnWSPpWY.mjs.map +0 -1
  63. package/dist/reader-DHMxLBQV.mjs.map +0 -1
  64. package/dist/serialize-BC2Wu3bR.mjs.map +0 -1
  65. package/dist/stylesheet-writer-TxsinwbO.mjs.map +0 -1
  66. package/dist/workbook-DQCslHzY.mjs.map +0 -1
@@ -1,42 +1,20 @@
1
1
  import { i as OpenXmlIoError, o as OpenXmlSchemaError } from "./exceptions-D-CFwxgm.mjs";
2
- import { s as coordinateToTuple } from "./coordinate-Bt5Gni2I.mjs";
2
+ import { _ as rowNumberFromAttr, c as derivedRowNumber, s as coordinateToTuple } from "./coordinate-CxC88uBk.mjs";
3
3
  import { t as ERROR_CODES } from "./inference-BMvv9cUY.mjs";
4
- import { n as escapeXmlAttr } from "./escape-rnWSPpWY.mjs";
5
- import { _ as ARC_WORKBOOK_RELS, a as findChildren, c as ARC_CONTENT_TYPES, f as ARC_ROOT_RELS, ft as SHEET_MAIN_NS, g as ARC_WORKBOOK, i as findChild, lt as REL_NS, m as ARC_STYLE, p as ARC_SHARED_STRINGS, pt as STYLES_TYPE, ut as SHARED_STRINGS_TYPE, wt as XLSX_TYPE, yt as WORKSHEET_TYPE } from "./tree-BX-sRRVp.mjs";
6
- import { t as stylesheetToBytes, v as serializeCell } from "./stylesheet-writer-TxsinwbO.mjs";
4
+ import { a as unescapeCellString, n as escapeXmlAttr } from "./escape-rA5UpibT.mjs";
5
+ import { _ as ARC_WORKBOOK_RELS, c as ARC_CONTENT_TYPES, f as ARC_ROOT_RELS, ft as SHEET_MAIN_NS, g as ARC_WORKBOOK, lt as REL_NS, m as ARC_STYLE, p as ARC_SHARED_STRINGS, pt as STYLES_TYPE, ut as SHARED_STRINGS_TYPE, wt as XLSX_TYPE, yt as WORKSHEET_TYPE } from "./tree-BX-sRRVp.mjs";
6
+ import { t as stylesheetToBytes, v as serializeCell, x as parseCellNumber } from "./stylesheet-writer-BBYjNSMA.mjs";
7
7
  import { t as parseXml } from "./parser-By6RWZVW.mjs";
8
- import { Q as findById, a as makeManifest, n as addOverride, nt as relsFromBytes, o as manifestFromBytes, rt as relsToBytes, s as manifestToBytes, t as addDefault, tt as makeRelationships } from "./manifest-pLmx7KYh.mjs";
9
- import { ct as buildXfPatch, ht as makeStylesheet, it as addCellXf, lt as defaultCellXf } from "./cell-style-CNsET6WU.mjs";
10
- import { D as makeSharedStrings, S as validateSheetTitle, j as sharedStringsToBytes, k as parseSharedStringsXml } from "./workbook-DQCslHzY.mjs";
11
- import { i as parseStylesheetXml, n as parseDate1904, r as resolveRelTarget } from "./load-CspGc399.mjs";
12
- import { t as openZip } from "./reader-DHMxLBQV.mjs";
8
+ import { r as parseXsdBoolean } from "./serialize-Dmb6Vtt9.mjs";
9
+ import { $ as findByType, Q as findById, a as makeManifest, n as addOverride, nt as relsFromBytes, o as manifestFromBytes, rt as relsToBytes, s as manifestToBytes, t as addDefault, tt as makeRelationships } from "./manifest-BHUYpjUG.mjs";
10
+ import { ct as buildXfPatch, ht as makeStylesheet, it as addCellXf, lt as defaultCellXf } from "./cell-style-DjKKPWDL.mjs";
11
+ import { A as serializeRichString, D as makeSharedStrings, S as validateSheetTitle, k as parseSharedStringsXml } from "./workbook-CMel6UcI.mjs";
12
+ import { a as parseDate1904, c as resolveRelTarget, l as parseStylesheetXml, n as SHARED_STRINGS_PART, o as parseSheetEntries, r as STYLES_PART, s as readOptionalWorkbookPart, t as OFFICE_DOC_REL_TYPE } from "./load-DIdQ_zum.mjs";
13
+ import { t as openZip } from "./reader-pC7bxYAs.mjs";
13
14
  import { t as utf8ByteLength } from "./utf8-OAkCDG5g.mjs";
14
15
  import { t as createZipWriter } from "./writer-C38RBZCy.mjs";
15
16
  import { t as iterParse } from "./iterparse-DZE0zy2M.mjs";
16
17
  //#region src/streaming/read-only.ts
17
- const SHEET_TAG = `{${SHEET_MAIN_NS}}sheet`;
18
- const SHEETS_TAG = `{${SHEET_MAIN_NS}}sheets`;
19
- const parseSheetList = (root, workbookPath, archive) => {
20
- const sheetsEl = findChild(root, SHEETS_TAG);
21
- if (!sheetsEl) return [];
22
- const wbRelsPath = relsPathFor(workbookPath);
23
- const wbRels = archive.has(wbRelsPath) ? relsFromBytes(archive.read(wbRelsPath)) : { rels: [] };
24
- const out = [];
25
- for (const sheet of findChildren(sheetsEl, SHEET_TAG)) {
26
- const name = sheet.attrs["name"];
27
- const rId = sheet.attrs[`{${REL_NS}}id`];
28
- if (!name || !rId) continue;
29
- const rel = findById(wbRels, rId);
30
- if (!rel) continue;
31
- const partPath = resolveRelTarget(workbookPath, rel.target);
32
- out.push({
33
- name,
34
- rId,
35
- partPath
36
- });
37
- }
38
- return out;
39
- };
40
18
  const relsPathFor = (partPath) => {
41
19
  const i = partPath.lastIndexOf("/");
42
20
  if (i < 0) return `_rels/${partPath}.rels`;
@@ -46,16 +24,16 @@ const localName = (qname) => {
46
24
  const i = qname.lastIndexOf("}");
47
25
  return i < 0 ? qname : qname.slice(i + 1);
48
26
  };
49
- const decodeCellValue = (t, vText, inlineText, sst) => {
27
+ const decodeCellValue = (t, vText, inlineText, sst, sheet, col, row) => {
50
28
  switch (t) {
51
- case "n": return vText !== void 0 && vText !== "" ? Number.parseFloat(vText) : null;
29
+ case "n": return parseCellNumber(vText, sheet, col, row);
52
30
  case "s": {
53
31
  if (vText === void 0) return null;
54
32
  const idx = Number.parseInt(vText, 10);
55
33
  if (!Number.isInteger(idx) || idx < 0 || idx >= sst.length) return null;
56
34
  return sst[idx] ?? null;
57
35
  }
58
- case "b": return vText === "1";
36
+ case "b": return parseXsdBoolean(vText) ?? null;
59
37
  case "e":
60
38
  if (!vText || !ERROR_CODES.has(vText)) return null;
61
39
  return {
@@ -72,14 +50,28 @@ const decodeCellValue = (t, vText, inlineText, sst) => {
72
50
  * stream that yields them), yielding one `ReadOnlyCell[]` per row that matches
73
51
  * `opts`.
74
52
  */
75
- async function* iterSheetRows(sheetInput, sst, opts) {
53
+ async function* iterSheetRows(title, sheetInput, sst, opts) {
76
54
  const minRow = opts.minRow ?? 1;
77
55
  const maxRow = opts.maxRow ?? Number.POSITIVE_INFINITY;
78
56
  const minCol = opts.minCol ?? 1;
79
57
  const maxCol = opts.maxCol ?? Number.POSITIVE_INFINITY;
80
58
  let inSheetData = false;
81
59
  let currentRow = -1;
60
+ let nextRow = 1;
82
61
  let currentCells = [];
62
+ let nextCol = 1;
63
+ let pendingCells = [];
64
+ const settleRow = (row) => {
65
+ currentRow = row;
66
+ nextRow = Math.max(nextRow, row + 1);
67
+ if (row >= minRow && row <= maxRow) for (const cell of pendingCells) currentCells.push({
68
+ row,
69
+ col: cell.col,
70
+ value: decodeCellValue(cell.type, cell.text, cell.inline, sst, title, cell.col, row),
71
+ styleId: cell.styleId
72
+ });
73
+ pendingCells = [];
74
+ };
83
75
  let cellOpen = false;
84
76
  let cellRow = 0;
85
77
  let cellCol = 0;
@@ -90,6 +82,7 @@ async function* iterSheetRows(sheetInput, sst, opts) {
90
82
  let inIs = false;
91
83
  let inIsT = false;
92
84
  let isText = "";
85
+ let isRunText = "";
93
86
  for await (const ev of iterParse(sheetInput)) {
94
87
  const e = ev;
95
88
  if (e.kind === "start") {
@@ -100,28 +93,35 @@ async function* iterSheetRows(sheetInput, sst, opts) {
100
93
  }
101
94
  switch (local) {
102
95
  case "row": {
103
- e.attrs;
104
96
  const rRaw = e.attrs["r"];
105
- currentRow = rRaw ? Number.parseInt(rRaw, 10) : currentRow + 1;
97
+ if (rRaw === void 0) currentRow = 0;
98
+ else {
99
+ currentRow = rowNumberFromAttr(rRaw, "loadWorkbookStream");
100
+ nextRow = Math.max(nextRow, currentRow + 1);
101
+ }
106
102
  currentCells = [];
103
+ pendingCells = [];
104
+ nextCol = 1;
107
105
  break;
108
106
  }
109
107
  case "c": {
110
108
  if (currentRow < 0) break;
111
- if (currentRow < minRow || currentRow > maxRow) break;
109
+ const ref = e.attrs["r"];
110
+ if (currentRow === 0 && ref) settleRow(coordinateToTuple(ref).row);
111
+ if (currentRow !== 0 && (currentRow < minRow || currentRow > maxRow)) break;
112
112
  cellOpen = true;
113
113
  cellType = e.attrs["t"] ?? "n";
114
114
  const sRaw = e.attrs["s"];
115
115
  cellStyleId = sRaw ? Number.parseInt(sRaw, 10) || 0 : 0;
116
- const ref = e.attrs["r"];
117
116
  if (ref) {
118
117
  const tup = coordinateToTuple(ref);
119
118
  cellRow = tup.row;
120
119
  cellCol = tup.col;
121
120
  } else {
122
121
  cellRow = currentRow;
123
- cellCol = (currentCells[currentCells.length - 1]?.col ?? 0) + 1;
122
+ cellCol = nextCol;
124
123
  }
124
+ nextCol = cellCol + 1;
125
125
  vText = "";
126
126
  isText = "";
127
127
  break;
@@ -133,7 +133,10 @@ async function* iterSheetRows(sheetInput, sst, opts) {
133
133
  if (cellOpen) inIs = true;
134
134
  break;
135
135
  case "t":
136
- if (inIs) inIsT = true;
136
+ if (inIs) {
137
+ inIsT = true;
138
+ isRunText = "";
139
+ }
137
140
  break;
138
141
  default: break;
139
142
  }
@@ -141,7 +144,7 @@ async function* iterSheetRows(sheetInput, sst, opts) {
141
144
  }
142
145
  if (e.kind === "text") {
143
146
  if (inV) vText += e.text;
144
- else if (inIsT) isText += e.text;
147
+ else if (inIsT) isRunText += e.text;
145
148
  continue;
146
149
  }
147
150
  const local = localName(e.name);
@@ -151,6 +154,7 @@ async function* iterSheetRows(sheetInput, sst, opts) {
151
154
  inSheetData = false;
152
155
  return;
153
156
  case "row":
157
+ if (currentRow === 0) settleRow(derivedRowNumber(nextRow, "loadWorkbookStream"));
154
158
  if (currentRow >= minRow && currentRow <= maxRow && currentCells.length > 0) yield currentCells;
155
159
  if (currentRow > maxRow) {
156
160
  inSheetData = false;
@@ -160,8 +164,15 @@ async function* iterSheetRows(sheetInput, sst, opts) {
160
164
  currentCells = [];
161
165
  break;
162
166
  case "c":
163
- if (cellOpen && cellCol >= minCol && cellCol <= maxCol && cellRow >= minRow && cellRow <= maxRow) {
164
- const value = decodeCellValue(cellType, vText, isText, sst);
167
+ if (cellOpen && cellRow === 0 && cellCol >= minCol && cellCol <= maxCol) pendingCells.push({
168
+ col: cellCol,
169
+ type: cellType,
170
+ text: vText,
171
+ inline: isText,
172
+ styleId: cellStyleId
173
+ });
174
+ else if (cellOpen && cellCol >= minCol && cellCol <= maxCol && cellRow >= minRow && cellRow <= maxRow) {
175
+ const value = decodeCellValue(cellType, vText, isText, sst, title, cellCol, cellRow);
165
176
  currentCells.push({
166
177
  row: cellRow,
167
178
  col: cellCol,
@@ -178,7 +189,10 @@ async function* iterSheetRows(sheetInput, sst, opts) {
178
189
  inIs = false;
179
190
  break;
180
191
  case "t":
181
- if (inIs) inIsT = false;
192
+ if (inIsT) {
193
+ isText += unescapeCellString(isRunText);
194
+ inIsT = false;
195
+ }
182
196
  break;
183
197
  default: break;
184
198
  }
@@ -187,7 +201,9 @@ async function* iterSheetRows(sheetInput, sst, opts) {
187
201
  const isXmlSpace = (b) => b === 32 || b === 9 || b === 10 || b === 13;
188
202
  /**
189
203
  * Read the `r="N"` row number out of a `<row …>` attribute region, scanning
190
- * bytes directly. Returns -1 when the attribute is absent or malformed.
204
+ * bytes directly. Returns -1 when the attribute is absent, or holds anything
205
+ * `parseRowNumberAttr` rejects: the two accept the same shapes, so the index
206
+ * can never seek by a number the SAX walk reads differently.
191
207
  *
192
208
  * Decoding the region to a string and running a regex instead costs one
193
209
  * TextDecoder, one string and one match array per row, which on a million-row
@@ -197,9 +213,11 @@ const readRowAttr = (bytes, from, to) => {
197
213
  for (let p = from; p + 3 < to; p++) {
198
214
  if (!isXmlSpace(bytes[p])) continue;
199
215
  if (bytes[p + 1] !== 114 || bytes[p + 2] !== 61 || bytes[p + 3] !== 34) continue;
216
+ let q = p + 4;
217
+ while (q < to && isXmlSpace(bytes[q])) q++;
218
+ if (bytes[q] === 43) q++;
200
219
  let value = 0;
201
220
  let digits = 0;
202
- let q = p + 4;
203
221
  while (q < to) {
204
222
  const d = bytes[q];
205
223
  if (d === void 0 || d < 48 || d > 57) break;
@@ -207,7 +225,8 @@ const readRowAttr = (bytes, from, to) => {
207
225
  digits++;
208
226
  q++;
209
227
  }
210
- if (digits > 0 && q < to && bytes[q] === 34) return value;
228
+ while (q < to && isXmlSpace(bytes[q])) q++;
229
+ if (digits > 0 && value >= 1 && value <= 1048576 && q < to && bytes[q] === 34) return value;
211
230
  }
212
231
  return -1;
213
232
  };
@@ -270,9 +289,14 @@ const findSheetDataTagEnd = (bytes) => {
270
289
  * `sheetDataEnd` is the byte offset of `</sheetData>` so callers can clip the
271
290
  * region that gets handed to saxes. `sheetDataTagEnd` includes the original
272
291
  * namespace-bearing ancestor tags, reused by {@link replayFromRow}.
292
+ *
293
+ * `hasUnnumberedRow` reports a `<row>` this scan cannot number, which makes the
294
+ * whole sheet unseekable: a row without `@r` takes the row its first cell
295
+ * names, and reading cell refs is the SAX walk's job.
273
296
  */
274
297
  const buildRowOffsetIndex = (bytes) => {
275
298
  const out = [];
299
+ let hasUnnumberedRow = false;
276
300
  let sheetDataEnd = -1;
277
301
  let i = 0;
278
302
  while (i < bytes.length) {
@@ -302,13 +326,15 @@ const buildRowOffsetIndex = (bytes) => {
302
326
  row,
303
327
  offset: start
304
328
  });
329
+ else hasUnnumberedRow = true;
305
330
  i = j + 1;
306
331
  }
307
332
  if (sheetDataEnd < 0) sheetDataEnd = bytes.length;
308
333
  return {
309
334
  index: out,
310
335
  sheetDataEnd,
311
- sheetDataTagEnd: findSheetDataTagEnd(bytes)
336
+ sheetDataTagEnd: findSheetDataTagEnd(bytes),
337
+ hasUnnumberedRow
312
338
  };
313
339
  };
314
340
  /**
@@ -358,7 +384,9 @@ const replayFromRow = (bytes, sheetDataTagEnd, fromOffset, sheetDataEnd) => {
358
384
  * `iterRows({ minRow > 1 })` needs random access instead, so the first band
359
385
  * query inflates the part and indexes its row offsets, and holds both for the
360
386
  * life of the worksheet handle or until the workbook closes. Subsequent band
361
- * queries jump straight to the first matching row without inflating or scanning again.
387
+ * queries jump straight to the first matching row without inflating or scanning
388
+ * again. A sheet whose rows omit `@r` cannot be indexed by row number, so its
389
+ * band queries keep streaming and hold nothing.
362
390
  */
363
391
  const makeStreamingReadOnlyWorksheet = (title, archive, partPath, sst, indexes) => {
364
392
  const cacheKey = {};
@@ -366,9 +394,13 @@ const makeStreamingReadOnlyWorksheet = (title, archive, partPath, sst, indexes)
366
394
  let cached = indexes.byWorksheet.get(cacheKey);
367
395
  if (!cached) {
368
396
  const bytes = archive.read(partPath);
369
- cached = {
397
+ const { hasUnnumberedRow, index, sheetDataEnd, sheetDataTagEnd } = buildRowOffsetIndex(bytes);
398
+ cached = hasUnnumberedRow ? { seekable: false } : {
399
+ seekable: true,
370
400
  bytes,
371
- ...buildRowOffsetIndex(bytes)
401
+ index,
402
+ sheetDataEnd,
403
+ sheetDataTagEnd
372
404
  };
373
405
  indexes.byWorksheet.set(cacheKey, cached);
374
406
  }
@@ -376,14 +408,16 @@ const makeStreamingReadOnlyWorksheet = (title, archive, partPath, sst, indexes)
376
408
  };
377
409
  const iterRows = (opts = {}) => {
378
410
  const minRow = opts.minRow ?? 1;
379
- if (minRow <= 1) return iterSheetRows(archive.readStream(partPath), sst, opts);
380
- const { bytes, index, sheetDataEnd, sheetDataTagEnd } = ensureIndexed();
381
- if (index.length === 0 || sheetDataTagEnd < 0) return iterSheetRows(bytes, sst, opts);
411
+ if (minRow <= 1) return iterSheetRows(title, archive.readStream(partPath), sst, opts);
412
+ const seek = ensureIndexed();
413
+ if (!seek.seekable) return iterSheetRows(title, archive.readStream(partPath), sst, opts);
414
+ const { bytes, index, sheetDataEnd, sheetDataTagEnd } = seek;
415
+ if (index.length === 0 || sheetDataTagEnd < 0) return iterSheetRows(title, bytes, sst, opts);
382
416
  const pos = firstRowAtOrAfter(index, minRow);
383
417
  if (pos < 0) return (async function* () {})();
384
418
  const target = index[pos];
385
- if (!target) return iterSheetRows(bytes, sst, opts);
386
- return iterSheetRows(replayFromRow(bytes, sheetDataTagEnd, target.offset, sheetDataEnd), sst, opts);
419
+ if (!target) return iterSheetRows(title, bytes, sst, opts);
420
+ return iterSheetRows(title, replayFromRow(bytes, sheetDataTagEnd, target.offset, sheetDataEnd), sst, opts);
387
421
  };
388
422
  const iterValues = async function* (opts = {}) {
389
423
  for await (const row of iterRows(opts)) yield row.map((c) => c.value);
@@ -399,16 +433,16 @@ const makeStreamingReadOnlyWorksheet = (title, archive, partPath, sst, indexes)
399
433
  * sheet list / styles / shared strings. Per the project-wide "no classes" rule
400
434
  * (CLAUDE.md), the workbook is a plain object closing over the archive handle.
401
435
  */
402
- const makeStreamingReadOnlyWorkbook = (sheetNames, styles, date1904, archive, entries, sst) => {
436
+ const makeStreamingReadOnlyWorkbook = (sheetNames, styles, date1904, archive, partPathByName, sst) => {
403
437
  const indexes = { byWorksheet: /* @__PURE__ */ new WeakMap() };
404
438
  return {
405
439
  sheetNames,
406
440
  styles,
407
441
  date1904,
408
442
  openWorksheet(name) {
409
- const entry = entries.get(name);
410
- if (!entry) throw new OpenXmlSchemaError(`loadWorkbookStream: no worksheet named "${name}"`);
411
- return makeStreamingReadOnlyWorksheet(name, archive, entry.partPath, sst, indexes);
443
+ const partPath = partPathByName.get(name);
444
+ if (partPath === void 0) throw new OpenXmlSchemaError(`loadWorkbookStream: no worksheet named "${name}"`);
445
+ return makeStreamingReadOnlyWorksheet(name, archive, partPath, sst, indexes);
412
446
  },
413
447
  async close() {
414
448
  archive.close();
@@ -422,21 +456,94 @@ async function loadWorkbookStream(source, opts = {}) {
422
456
  if (!archive.has("[Content_Types].xml")) throw new OpenXmlSchemaError(`loadWorkbookStream: missing "${ARC_CONTENT_TYPES}"`);
423
457
  manifestFromBytes(archive.read(ARC_CONTENT_TYPES));
424
458
  if (!archive.has(ARC_ROOT_RELS)) throw new OpenXmlSchemaError(`loadWorkbookStream: missing "${ARC_ROOT_RELS}"`);
425
- const officeDocRel = relsFromBytes(archive.read(ARC_ROOT_RELS)).rels.find((r) => r.type === `${REL_NS}/officeDocument`);
459
+ const officeDocRel = findByType(relsFromBytes(archive.read(ARC_ROOT_RELS)), OFFICE_DOC_REL_TYPE);
426
460
  if (!officeDocRel) throw new OpenXmlSchemaError(`loadWorkbookStream: no officeDocument relationship in root rels`);
427
461
  const workbookPath = resolveRelTarget("", officeDocRel.target);
428
462
  if (!archive.has(workbookPath)) throw new OpenXmlSchemaError(`loadWorkbookStream: workbook part "${workbookPath}" missing`);
429
463
  const workbookRoot = parseXml(archive.read(workbookPath));
430
- const sheetEntries = parseSheetList(workbookRoot, workbookPath, archive);
431
- const entryMap = /* @__PURE__ */ new Map();
432
- for (const e of sheetEntries) entryMap.set(e.name, e);
433
- let sst = makeSharedStrings();
434
- if (archive.has(ARC_SHARED_STRINGS)) sst = parseSharedStringsXml(archive.read(ARC_SHARED_STRINGS));
435
- let styles = makeStylesheet();
436
- if (archive.has(ARC_STYLE)) styles = parseStylesheetXml(archive.read(ARC_STYLE));
437
- return makeStreamingReadOnlyWorkbook(sheetEntries.map((e) => e.name), styles, parseDate1904(workbookRoot), archive, entryMap, sst.entries.map((e) => typeof e === "string" ? e : e.runs.map((r) => r.text).join("")));
464
+ const declaredSheets = parseSheetEntries(workbookRoot);
465
+ const wbRelsPath = relsPathFor(workbookPath);
466
+ if (declaredSheets.length > 0 && !archive.has(wbRelsPath)) throw new OpenXmlSchemaError(`loadWorkbookStream: workbook has sheets but rels part "${wbRelsPath}" is missing`);
467
+ const wbRels = archive.has(wbRelsPath) ? relsFromBytes(archive.read(wbRelsPath)) : makeRelationships();
468
+ const partPathByName = /* @__PURE__ */ new Map();
469
+ for (const declared of declaredSheets) {
470
+ if (partPathByName.has(declared.name)) throw new OpenXmlSchemaError(`loadWorkbookStream: duplicate sheet name "${declared.name}"`);
471
+ const rel = findById(wbRels, declared.rId);
472
+ if (!rel) throw new OpenXmlSchemaError(`loadWorkbookStream: sheet "${declared.name}" rId "${declared.rId}" has no matching rels entry`);
473
+ partPathByName.set(declared.name, resolveRelTarget(workbookPath, rel.target));
474
+ }
475
+ const sstBytes = readOptionalWorkbookPart(archive, workbookPath, wbRels, SHARED_STRINGS_PART);
476
+ const sst = sstBytes === void 0 ? makeSharedStrings() : parseSharedStringsXml(sstBytes);
477
+ const stylesBytes = readOptionalWorkbookPart(archive, workbookPath, wbRels, STYLES_PART);
478
+ const styles = stylesBytes === void 0 ? makeStylesheet() : parseStylesheetXml(stylesBytes);
479
+ return makeStreamingReadOnlyWorkbook(declaredSheets.map((e) => e.name), styles, parseDate1904(workbookRoot), archive, partPathByName, sst.entries.map((e) => typeof e === "string" ? e : e.runs.map((r) => r.text).join("")));
438
480
  }
439
481
  //#endregion
482
+ //#region src/streaming/string-table.ts
483
+ const MAX_ENTRIES = 1e5;
484
+ const MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
485
+ const UTF16_BYTES_PER_UNIT = 2;
486
+ const XML_CHUNK_UNITS = 16 * 1024;
487
+ const createWriteOnlyStringTable = () => {
488
+ const entries = [];
489
+ const plainIndex = /* @__PURE__ */ new Map();
490
+ const richIndex = /* @__PURE__ */ new Map();
491
+ let payloadBytes = 0;
492
+ let full = false;
493
+ const serialize = (value) => {
494
+ const plain = typeof value === "string";
495
+ const key = plain ? value : serializeRichString(value);
496
+ const index = plain ? plainIndex : richIndex;
497
+ const cached = index.get(key);
498
+ if (cached !== void 0) return {
499
+ type: "s",
500
+ xml: `<v>${cached}</v>`
501
+ };
502
+ const body = plain ? serializeRichString(value) : key;
503
+ const bytes = (key.length + body.length) * UTF16_BYTES_PER_UNIT;
504
+ if (!full && entries.length < MAX_ENTRIES && payloadBytes + bytes <= MAX_PAYLOAD_BYTES) {
505
+ const id = entries.length;
506
+ entries.push(body);
507
+ index.set(key, id);
508
+ payloadBytes += bytes;
509
+ return {
510
+ type: "s",
511
+ xml: `<v>${id}</v>`
512
+ };
513
+ }
514
+ full = true;
515
+ return {
516
+ type: "inlineStr",
517
+ xml: `<is>${body}</is>`
518
+ };
519
+ };
520
+ const write = async (stream) => {
521
+ const encoder = new TextEncoder();
522
+ let pending = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="${SHEET_MAIN_NS}" count="${entries.length}" uniqueCount="${entries.length}">`;
523
+ const append = (text) => {
524
+ pending += text;
525
+ while (pending.length >= XML_CHUNK_UNITS) {
526
+ let end = XML_CHUNK_UNITS;
527
+ const last = pending.charCodeAt(end - 1);
528
+ if (last >= 55296 && last <= 56319) end--;
529
+ stream.write(encoder.encode(pending.slice(0, end)));
530
+ pending = pending.slice(end);
531
+ }
532
+ };
533
+ for (const body of entries) append(`<si>${body}</si>`);
534
+ append("</sst>");
535
+ if (pending.length > 0) stream.write(encoder.encode(pending));
536
+ await stream.end();
537
+ };
538
+ return {
539
+ serialize,
540
+ write,
541
+ get size() {
542
+ return entries.length;
543
+ }
544
+ };
545
+ };
546
+ //#endregion
440
547
  //#region src/streaming/write-only.ts
441
548
  const escapeAttr = escapeXmlAttr;
442
549
  const validateTitle = (title, taken) => {
@@ -467,10 +574,10 @@ const FLUSH_THRESHOLD_BYTES = 64 * 1024;
467
574
  * closure state.
468
575
  *
469
576
  * The worksheet streams its `<sheetData>` body chunk-by-chunk through the ZIP
470
- * writer's `addStreamingEntry` API, so the heap footprint stays at one ~64 KB
471
- * pending text buffer plus deflate scratch no Cell objects, no all-rows
472
- * accumulation. The XML envelope (decl / worksheet open / cols / sheetData
473
- * open) flushes on the first `appendRow` (or `close()` if the sheet is empty);
577
+ * writer's `addStreamingEntry` API. Row buffering stays at ~64 KB plus the
578
+ * current row and deflate scratch; strings have a separate workbook-wide cap.
579
+ * The XML envelope (decl / worksheet open / cols / sheetData open) flushes on
580
+ * the first `appendRow` (or `close()` if the sheet is empty);
474
581
  * column widths staged via `setColumnWidth` *must* land before the first row.
475
582
  */
476
583
  const makeWriteOnlyWorksheet = (state, title, sheetId) => {
@@ -479,9 +586,8 @@ const makeWriteOnlyWorksheet = (state, title, sheetId) => {
479
586
  let headerFlushed = false;
480
587
  const columnWidths = /* @__PURE__ */ new Map();
481
588
  const dummyCtx = {
482
- sharedStrings: state.sst,
483
- styles: state.styles,
484
- rels: makeRelationships()
589
+ sharedStrings: makeSharedStrings(),
590
+ styles: state.styles
485
591
  };
486
592
  const encoder = new TextEncoder();
487
593
  const stream = state.writer.addStreamingEntry(`xl/worksheets/sheet${sheetId}.xml`);
@@ -532,7 +638,7 @@ const makeWriteOnlyWorksheet = (state, title, sheetId) => {
532
638
  col,
533
639
  value,
534
640
  styleId
535
- }, dummyCtx);
641
+ }, dummyCtx, state.strings.serialize);
536
642
  }
537
643
  xml += "</row>";
538
644
  writeText(xml);
@@ -576,7 +682,7 @@ const makeWriteOnlyWorkbook = (sink, zipOpts) => {
576
682
  addCellXf(styles, defaultCellXf());
577
683
  const state = {
578
684
  styles,
579
- sst: makeSharedStrings(),
685
+ strings: createWriteOnlyStringTable(),
580
686
  sheets: [],
581
687
  finalised: false,
582
688
  hasOpenWorksheet: false,
@@ -616,7 +722,7 @@ const makeWriteOnlyWorkbook = (sink, zipOpts) => {
616
722
  };
617
723
  async function finalizeImpl(state, writer) {
618
724
  await writer.addEntry(ARC_STYLE, stylesheetToBytes(state.styles));
619
- if (state.sst.entries.length > 0) await writer.addEntry(ARC_SHARED_STRINGS, sharedStringsToBytes(state.sst));
725
+ if (state.strings.size > 0) await state.strings.write(writer.addStreamingEntry(ARC_SHARED_STRINGS));
620
726
  const workbookXml = serializeWorkbookXml(state.sheets);
621
727
  await writer.addEntry(ARC_WORKBOOK, new TextEncoder().encode(workbookXml));
622
728
  const wbRels = makeRelationships();
@@ -627,7 +733,7 @@ async function finalizeImpl(state, writer) {
627
733
  target: `worksheets/sheet${s.sheetId}.xml`
628
734
  });
629
735
  });
630
- if (state.sst.entries.length > 0) wbRels.rels.push({
736
+ if (state.strings.size > 0) wbRels.rels.push({
631
737
  id: `rId${wbRels.rels.length + 1}`,
632
738
  type: `${REL_NS}/sharedStrings`,
633
739
  target: "sharedStrings.xml"
@@ -651,7 +757,7 @@ async function finalizeImpl(state, writer) {
651
757
  addOverride(manifest, `/${ARC_WORKBOOK}`, XLSX_TYPE);
652
758
  for (const s of state.sheets) addOverride(manifest, `/xl/worksheets/sheet${s.sheetId}.xml`, WORKSHEET_TYPE);
653
759
  addOverride(manifest, `/${ARC_STYLE}`, STYLES_TYPE);
654
- if (state.sst.entries.length > 0) addOverride(manifest, `/${ARC_SHARED_STRINGS}`, SHARED_STRINGS_TYPE);
760
+ if (state.strings.size > 0) addOverride(manifest, `/${ARC_SHARED_STRINGS}`, SHARED_STRINGS_TYPE);
655
761
  await writer.addEntry(ARC_CONTENT_TYPES, manifestToBytes(manifest));
656
762
  await writer.finalize();
657
763
  }