@shbernal/ts-xlsx 2.0.0 → 2.1.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 (106) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +26 -4
  5. package/dist/core/address.js +21 -9
  6. package/dist/core/autofilter.d.ts +11 -0
  7. package/dist/core/autofilter.js +35 -10
  8. package/dist/core/color-resolution.d.ts +1 -1
  9. package/dist/core/color-resolution.js +7 -8
  10. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  11. package/dist/core/conditional-formatting-overlay.js +11 -0
  12. package/dist/core/conditional-formatting.d.ts +45 -5
  13. package/dist/core/conditional-formatting.js +88 -0
  14. package/dist/core/data-validation-overlay.d.ts +11 -0
  15. package/dist/core/data-validation-overlay.js +21 -2
  16. package/dist/core/data-validation.d.ts +9 -2
  17. package/dist/core/data-validation.js +34 -0
  18. package/dist/core/grid-edits.d.ts +16 -0
  19. package/dist/core/grid-edits.js +26 -8
  20. package/dist/core/grid-shift.d.ts +19 -0
  21. package/dist/core/grid-shift.js +8 -0
  22. package/dist/core/image.d.ts +2 -0
  23. package/dist/core/image.js +4 -0
  24. package/dist/core/merge.d.ts +12 -1
  25. package/dist/core/merge.js +36 -2
  26. package/dist/core/page-setup.d.ts +16 -2
  27. package/dist/core/page-setup.js +8 -1
  28. package/dist/core/pivot-table.d.ts +2 -0
  29. package/dist/core/pivot-table.js +25 -16
  30. package/dist/core/protection.d.ts +3 -0
  31. package/dist/core/protection.js +20 -14
  32. package/dist/core/row-input.d.ts +7 -0
  33. package/dist/core/row-input.js +10 -8
  34. package/dist/core/style.d.ts +27 -0
  35. package/dist/core/style.js +82 -67
  36. package/dist/core/table.d.ts +25 -11
  37. package/dist/core/table.js +67 -31
  38. package/dist/core/theme.d.ts +3 -1
  39. package/dist/core/theme.js +4 -0
  40. package/dist/core/workbook.d.ts +2 -2
  41. package/dist/core/worksheet-comments.d.ts +8 -0
  42. package/dist/core/worksheet-comments.js +18 -0
  43. package/dist/core/worksheet.d.ts +15 -2
  44. package/dist/core/worksheet.js +28 -48
  45. package/dist/customui/ribbon.js +24 -23
  46. package/dist/entries/core.d.ts +3 -3
  47. package/dist/entries/csv.d.ts +1 -1
  48. package/dist/entries/node-unavailable.d.ts +15 -0
  49. package/dist/entries/node-unavailable.js +20 -0
  50. package/dist/entries/node.d.ts +1 -0
  51. package/dist/entries/node.js +1 -0
  52. package/dist/entries/xlsx.d.ts +0 -1
  53. package/dist/entries/xlsx.js +0 -1
  54. package/dist/io/csv/read.js +1 -1
  55. package/dist/io/csv/write.d.ts +12 -2
  56. package/dist/io/csv/write.js +17 -2
  57. package/dist/io/opc/inflate.js +2 -12
  58. package/dist/io/opc/rels.d.ts +11 -0
  59. package/dist/io/opc/rels.js +3 -2
  60. package/dist/io/style/xf-style.d.ts +7 -2
  61. package/dist/io/style/xf-style.js +6 -1
  62. package/dist/io/xlsx/cell-accumulator.d.ts +1 -3
  63. package/dist/io/xlsx/cell-accumulator.js +20 -58
  64. package/dist/io/xlsx/cell-value.d.ts +1 -1
  65. package/dist/io/xlsx/cell-value.js +7 -4
  66. package/dist/io/xlsx/color-xml.d.ts +1 -1
  67. package/dist/io/xlsx/color-xml.js +7 -6
  68. package/dist/io/xlsx/comments.js +4 -2
  69. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  70. package/dist/io/xlsx/conditional-formatting.js +60 -45
  71. package/dist/io/xlsx/data-validation.d.ts +1 -1
  72. package/dist/io/xlsx/data-validation.js +35 -33
  73. package/dist/io/xlsx/hyperlinks.js +6 -14
  74. package/dist/io/xlsx/images.js +19 -22
  75. package/dist/io/xlsx/read-pivot.js +2 -8
  76. package/dist/io/xlsx/read-shared-strings.js +8 -47
  77. package/dist/io/xlsx/read-styles.js +2 -1
  78. package/dist/io/xlsx/read-worksheet.js +14 -16
  79. package/dist/io/xlsx/read.d.ts +1 -1
  80. package/dist/io/xlsx/read.js +44 -74
  81. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  82. package/dist/io/xlsx/rich-runs.js +94 -30
  83. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  84. package/dist/io/xlsx/sheet-properties.js +13 -9
  85. package/dist/io/xlsx/styles.d.ts +3 -15
  86. package/dist/io/xlsx/styles.js +63 -88
  87. package/dist/io/xlsx/tables.js +13 -13
  88. package/dist/io/xlsx/theme-xml.js +7 -4
  89. package/dist/io/xlsx/threaded-comments.js +19 -19
  90. package/dist/io/xlsx/workbook-xml.js +14 -8
  91. package/dist/io/xlsx/worksheet-xml.js +34 -37
  92. package/dist/io/xlsx/write-stream.d.ts +8 -1
  93. package/dist/io/xlsx/write-stream.js +10 -11
  94. package/dist/sha512.d.ts +2 -0
  95. package/dist/sha512.js +146 -0
  96. package/dist/vba/bytes.d.ts +1 -2
  97. package/dist/vba/bytes.js +1 -12
  98. package/dist/vba/dir-records.d.ts +53 -0
  99. package/dist/vba/dir-records.js +28 -0
  100. package/dist/vba/project-editor.js +5 -34
  101. package/dist/vba/project.js +2 -18
  102. package/dist/xml/xml-read.d.ts +60 -0
  103. package/dist/xml/xml-read.js +47 -0
  104. package/dist/xml/xml.d.ts +43 -1
  105. package/dist/xml/xml.js +17 -5
  106. package/package.json +21 -4
@@ -88,7 +88,14 @@ export declare class WorksheetStreamWriter {
88
88
  */
89
89
  flushRow(number: number, cells: readonly Cell[]): void;
90
90
  flushedSheet(): FlushedSheet | undefined;
91
- /** Address a cell by its A1 reference to read or style it before the sheet is committed. */
91
+ /**
92
+ * Address a cell by its A1 reference to read or style it before the sheet is committed. The row it
93
+ * names must still be live: a row whose {@link StreamedRow.commit} has run is finished.
94
+ *
95
+ * @throws {AuthoringError} if the reference names an already-committed row. That row's `<row>` is
96
+ * rendered and its cells are released, so the cell this would materialise could only be written
97
+ * as a second row carrying the same number.
98
+ */
92
99
  getCell(reference: string): Cell;
93
100
  /**
94
101
  * Attach a data validation to a range before the sheet is committed. Delegates to the model, so the
@@ -1,7 +1,8 @@
1
1
  import { createWriteStream } from 'node:fs';
2
2
  import { PassThrough } from 'node:stream';
3
3
  import { Zip, ZipDeflate } from 'fflate';
4
- import { encodeAddress } from '../../core/address.js';
4
+ import { concat } from '../../bytes.js';
5
+ import { encodeAddress, tryDecodeCellRef } from '../../core/address.js';
5
6
  import { INTERNAL } from '../../core/internal.js';
6
7
  import { isSharedFormulaValue } from '../../core/value.js';
7
8
  import { Workbook } from '../../core/workbook.js';
@@ -36,6 +37,7 @@ export class WorksheetStreamWriter {
36
37
  #lastRow = 0;
37
38
  #columnDefaults;
38
39
  #flushedRows = [];
40
+ #flushedNumbers = new Set();
39
41
  #extent = new Extent();
40
42
  #maxRowOutlineLevel = 0;
41
43
  constructor(sheet, eager, styles) {
@@ -97,6 +99,7 @@ export class WorksheetStreamWriter {
97
99
  this.#flushedRows.push({ number, xml });
98
100
  this.#extent.add(number, minCol, maxCol);
99
101
  }
102
+ this.#flushedNumbers.add(number);
100
103
  this.#sheet[INTERNAL].evictRow(number);
101
104
  }
102
105
  flushedSheet() {
@@ -110,6 +113,12 @@ export class WorksheetStreamWriter {
110
113
  }
111
114
  getCell(reference) {
112
115
  this.#assertOpen();
116
+ const target = tryDecodeCellRef(reference);
117
+ if (target !== undefined && this.#flushedNumbers.has(target.row)) {
118
+ throw new AuthoringError(`cell ${reference} of streamed sheet "${this.#sheet.name}" is in row ${target.row}, which is ` +
119
+ 'already committed: its bytes are written and its cells released, so this value could only ' +
120
+ 'be emitted as a second row with that number. Style a row before committing it.');
121
+ }
113
122
  return this.#sheet.getCell(reference);
114
123
  }
115
124
  addDataValidation(sqref, rule, options = {}) {
@@ -252,13 +261,3 @@ function streamZipPackage(parts, onChunk) {
252
261
  zip.end();
253
262
  });
254
263
  }
255
- function concat(chunks) {
256
- const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
257
- const out = new Uint8Array(total);
258
- let offset = 0;
259
- for (const chunk of chunks) {
260
- out.set(chunk, offset);
261
- offset += chunk.length;
262
- }
263
- return out;
264
- }
@@ -0,0 +1,2 @@
1
+ /** The SHA-512 digest of `message`, as its 64 bytes. */
2
+ export declare function sha512(message: Uint8Array): Uint8Array;
package/dist/sha512.js ADDED
@@ -0,0 +1,146 @@
1
+ const BLOCK_BYTES = 128;
2
+ const DIGEST_BYTES = 64;
3
+ const K = new Uint32Array([
4
+ 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,
5
+ 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,
6
+ 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,
7
+ 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,
8
+ 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,
9
+ 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,
10
+ 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,
11
+ 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,
12
+ 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,
13
+ 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,
14
+ 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,
15
+ 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,
16
+ 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,
17
+ 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,
18
+ 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,
19
+ 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,
20
+ 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,
21
+ 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,
22
+ 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,
23
+ 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817,
24
+ ]);
25
+ const INITIAL = new Uint32Array([
26
+ 0x6a09e667, 0xf3bcc908, 0xbb67ae85, 0x84caa73b, 0x3c6ef372, 0xfe94f82b, 0xa54ff53a, 0x5f1d36f1,
27
+ 0x510e527f, 0xade682d1, 0x9b05688c, 0x2b3e6c1f, 0x1f83d9ab, 0xfb41bd6b, 0x5be0cd19, 0x137e2179,
28
+ ]);
29
+ function word(words, index) {
30
+ return words[index] ?? 0;
31
+ }
32
+ export function sha512(message) {
33
+ const blocks = Math.ceil((message.length + 17) / BLOCK_BYTES);
34
+ const padded = new Uint8Array(blocks * BLOCK_BYTES);
35
+ padded.set(message);
36
+ padded[message.length] = 0x80;
37
+ const view = new DataView(padded.buffer);
38
+ const bits = message.length * 8;
39
+ view.setUint32(padded.length - 8, Math.floor(bits / 0x1_0000_0000), false);
40
+ view.setUint32(padded.length - 4, bits >>> 0, false);
41
+ const h = Uint32Array.from(INITIAL);
42
+ const schedule = new Uint32Array(160);
43
+ for (let block = 0; block < blocks; block++)
44
+ compress(h, view, block * BLOCK_BYTES, schedule);
45
+ const digest = new Uint8Array(DIGEST_BYTES);
46
+ const out = new DataView(digest.buffer);
47
+ for (let i = 0; i < 16; i++)
48
+ out.setUint32(i * 4, word(h, i), false);
49
+ return digest;
50
+ }
51
+ function compress(h, input, offset, schedule) {
52
+ for (let t = 0; t < 16; t++) {
53
+ schedule[t * 2] = input.getUint32(offset + t * 8, false);
54
+ schedule[t * 2 + 1] = input.getUint32(offset + t * 8 + 4, false);
55
+ }
56
+ for (let t = 16; t < 80; t++) {
57
+ const xHi = word(schedule, (t - 15) * 2);
58
+ const xLo = word(schedule, (t - 15) * 2 + 1);
59
+ const s0Hi = ((xHi >>> 1) | (xLo << 31)) ^ ((xHi >>> 8) | (xLo << 24)) ^ (xHi >>> 7);
60
+ const s0Lo = ((xLo >>> 1) | (xHi << 31)) ^ ((xLo >>> 8) | (xHi << 24)) ^ ((xLo >>> 7) | (xHi << 25));
61
+ const yHi = word(schedule, (t - 2) * 2);
62
+ const yLo = word(schedule, (t - 2) * 2 + 1);
63
+ const s1Hi = ((yHi >>> 19) | (yLo << 13)) ^ ((yLo >>> 29) | (yHi << 3)) ^ (yHi >>> 6);
64
+ const s1Lo = ((yLo >>> 19) | (yHi << 13)) ^ ((yHi >>> 29) | (yLo << 3)) ^ ((yLo >>> 6) | (yHi << 26));
65
+ const lo = (s1Lo >>> 0) +
66
+ word(schedule, (t - 7) * 2 + 1) +
67
+ (s0Lo >>> 0) +
68
+ word(schedule, (t - 16) * 2 + 1);
69
+ const hi = (s1Hi >>> 0) +
70
+ word(schedule, (t - 7) * 2) +
71
+ (s0Hi >>> 0) +
72
+ word(schedule, (t - 16) * 2) +
73
+ Math.floor(lo / 0x1_0000_0000);
74
+ schedule[t * 2] = hi >>> 0;
75
+ schedule[t * 2 + 1] = lo >>> 0;
76
+ }
77
+ let aHi = word(h, 0);
78
+ let aLo = word(h, 1);
79
+ let bHi = word(h, 2);
80
+ let bLo = word(h, 3);
81
+ let cHi = word(h, 4);
82
+ let cLo = word(h, 5);
83
+ let dHi = word(h, 6);
84
+ let dLo = word(h, 7);
85
+ let eHi = word(h, 8);
86
+ let eLo = word(h, 9);
87
+ let fHi = word(h, 10);
88
+ let fLo = word(h, 11);
89
+ let gHi = word(h, 12);
90
+ let gLo = word(h, 13);
91
+ let hHi = word(h, 14);
92
+ let hLo = word(h, 15);
93
+ for (let t = 0; t < 80; t++) {
94
+ const bigS1Hi = ((eHi >>> 14) | (eLo << 18)) ^ ((eHi >>> 18) | (eLo << 14)) ^ ((eLo >>> 9) | (eHi << 23));
95
+ const bigS1Lo = ((eLo >>> 14) | (eHi << 18)) ^ ((eLo >>> 18) | (eHi << 14)) ^ ((eHi >>> 9) | (eLo << 23));
96
+ const chHi = (eHi & fHi) ^ (~eHi & gHi);
97
+ const chLo = (eLo & fLo) ^ (~eLo & gLo);
98
+ const t1Lo = hLo + (bigS1Lo >>> 0) + (chLo >>> 0) + word(K, t * 2 + 1) + word(schedule, t * 2 + 1);
99
+ const t1Hi = (hHi +
100
+ (bigS1Hi >>> 0) +
101
+ (chHi >>> 0) +
102
+ word(K, t * 2) +
103
+ word(schedule, t * 2) +
104
+ Math.floor(t1Lo / 0x1_0000_0000)) >>>
105
+ 0;
106
+ const t1LoWrapped = t1Lo >>> 0;
107
+ const bigS0Hi = ((aHi >>> 28) | (aLo << 4)) ^ ((aLo >>> 2) | (aHi << 30)) ^ ((aLo >>> 7) | (aHi << 25));
108
+ const bigS0Lo = ((aLo >>> 28) | (aHi << 4)) ^ ((aHi >>> 2) | (aLo << 30)) ^ ((aHi >>> 7) | (aLo << 25));
109
+ const majHi = (aHi & bHi) ^ (aHi & cHi) ^ (bHi & cHi);
110
+ const majLo = (aLo & bLo) ^ (aLo & cLo) ^ (bLo & cLo);
111
+ const t2Lo = (bigS0Lo >>> 0) + (majLo >>> 0);
112
+ const t2Hi = ((bigS0Hi >>> 0) + (majHi >>> 0) + Math.floor(t2Lo / 0x1_0000_0000)) >>> 0;
113
+ const t2LoWrapped = t2Lo >>> 0;
114
+ hHi = gHi;
115
+ hLo = gLo;
116
+ gHi = fHi;
117
+ gLo = fLo;
118
+ fHi = eHi;
119
+ fLo = eLo;
120
+ const nextELo = dLo + t1LoWrapped;
121
+ eHi = (dHi + t1Hi + Math.floor(nextELo / 0x1_0000_0000)) >>> 0;
122
+ eLo = nextELo >>> 0;
123
+ dHi = cHi;
124
+ dLo = cLo;
125
+ cHi = bHi;
126
+ cLo = bLo;
127
+ bHi = aHi;
128
+ bLo = aLo;
129
+ const nextALo = t1LoWrapped + t2LoWrapped;
130
+ aHi = (t1Hi + t2Hi + Math.floor(nextALo / 0x1_0000_0000)) >>> 0;
131
+ aLo = nextALo >>> 0;
132
+ }
133
+ addInto(h, 0, aHi, aLo);
134
+ addInto(h, 2, bHi, bLo);
135
+ addInto(h, 4, cHi, cLo);
136
+ addInto(h, 6, dHi, dLo);
137
+ addInto(h, 8, eHi, eLo);
138
+ addInto(h, 10, fHi, fLo);
139
+ addInto(h, 12, gHi, gLo);
140
+ addInto(h, 14, hHi, hLo);
141
+ }
142
+ function addInto(h, index, hi, lo) {
143
+ const sumLo = word(h, index + 1) + lo;
144
+ h[index] = (word(h, index) + hi + Math.floor(sumLo / 0x1_0000_0000)) >>> 0;
145
+ h[index + 1] = sumLo >>> 0;
146
+ }
@@ -1,3 +1,4 @@
1
+ export { concat } from '../bytes.ts';
1
2
  /** Read a little-endian `uint16`. @throws {VbaParseError} if the two bytes are not both in `buf`. */
2
3
  export declare function readU16(buf: Uint8Array, at: number): number;
3
4
  /** Read a little-endian `uint32`. @throws {VbaParseError} if the four bytes are not all in `buf`. */
@@ -8,5 +9,3 @@ export declare function readU32(buf: Uint8Array, at: number): number;
8
9
  * producer and a half code unit carries nothing to decode.
9
10
  */
10
11
  export declare function decodeUtf16le(bytes: Uint8Array): string;
11
- /** Join byte chunks into one buffer. */
12
- export declare function concat(chunks: Uint8Array[]): Uint8Array;
package/dist/vba/bytes.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { concat } from '../bytes.js';
1
2
  import { VbaParseError } from './errors.js';
2
3
  function truncated(at, need, length) {
3
4
  return new VbaParseError(`read of ${need} bytes at offset ${at} runs past the end of a ${length}-byte buffer`);
@@ -26,15 +27,3 @@ export function decodeUtf16le(bytes) {
26
27
  }
27
28
  return s;
28
29
  }
29
- export function concat(chunks) {
30
- let total = 0;
31
- for (const c of chunks)
32
- total += c.length;
33
- const out = new Uint8Array(total);
34
- let off = 0;
35
- for (const c of chunks) {
36
- out.set(c, off);
37
- off += c.length;
38
- }
39
- return out;
40
- }
@@ -0,0 +1,53 @@
1
+ /** PROJECTCODEPAGE: the code page every MBCS name in the project decodes through. */
2
+ export declare const REC_PROJECT_CODEPAGE = 3;
3
+ /** PROJECTVERSION: the record whose `Size` under-counts its payload by the trailing VersionMinor. */
4
+ export declare const REC_PROJECT_VERSION = 9;
5
+ /** REFERENCEREGISTERED: a reference to a registered type library, by libid. */
6
+ export declare const REC_REFERENCE_REGISTERED = 13;
7
+ /** PROJECTMODULES MODULES_COUNT: how many module blocks follow, and the first record after the
8
+ * reference array, which has no count of its own. */
9
+ export declare const REC_MODULES_COUNT = 15;
10
+ /** REFERENCENAME: a reference's MBCS name. */
11
+ export declare const REC_REFERENCE_NAME = 22;
12
+ /** MODULENAME: opens a module's record block. */
13
+ export declare const REC_MODULE_NAME = 25;
14
+ /** MODULESTREAMNAME: the storage name the module's p-code lives under. */
15
+ export declare const REC_MODULE_STREAMNAME = 26;
16
+ /** MODULETYPE for a standard module. */
17
+ export declare const REC_MODULE_TYPE_PROCEDURAL = 33;
18
+ /** MODULETYPE for a document module (`ThisWorkbook`, a sheet's code-behind). */
19
+ export declare const REC_MODULE_TYPE_DOCUMENT = 34;
20
+ /** MODULEENDOFBLOCK: closes a module's record block. */
21
+ export declare const REC_MODULE_TERMINATOR = 43;
22
+ /** MODULEOFFSET: where the module's source begins inside its stream. */
23
+ export declare const REC_MODULE_OFFSET = 49;
24
+ /**
25
+ * REFERENCENAME's Unicode half. A *literal* `0x003E` marker rather than a nested record id, but it is
26
+ * laid out as its own Id+Size+data TLV, so a generic walk sees REFERENCENAME as two chained records,
27
+ * exactly like MODULENAME/MODULENAME_UNICODE. Verified against a real Excel-authored dir stream
28
+ * (2026-07-23).
29
+ */
30
+ export declare const REC_REFERENCE_NAME_UNICODE = 62;
31
+ /** One record as the walk sees it. */
32
+ export interface DirRecord {
33
+ /** The record id; one of the `REC_*` constants above, or a record this library does not consume. */
34
+ readonly id: number;
35
+ /** Offset of the record's `Id` field: where a splice that drops the record cuts from. */
36
+ readonly recordStart: number;
37
+ /** Offset of the record's payload. */
38
+ readonly dataStart: number;
39
+ /** The `Size` field, which is the payload length for every record but PROJECTVERSION. */
40
+ readonly size: number;
41
+ /** Offset of the next record, PROJECTVERSION's uncounted VersionMinor already skipped. Never
42
+ * recompute this from `dataStart + size`: that is the misalignment this module exists to prevent. */
43
+ readonly end: number;
44
+ }
45
+ /**
46
+ * Walk a decompressed `dir` stream, yielding each record in order. Consumers may stop early.
47
+ *
48
+ * @param context the phrase a truncated record's error carries, naming what the caller was doing:
49
+ * the message reaches a user who has no idea what a TLV is but does know they were removing a
50
+ * module.
51
+ * @throws {VbaParseError} when a record's payload runs past the end of the stream.
52
+ */
53
+ export declare function dirRecords(dir: Uint8Array, context: string): Generator<DirRecord>;
@@ -0,0 +1,28 @@
1
+ import { readU16, readU32 } from './bytes.js';
2
+ import { VbaParseError } from './errors.js';
3
+ export const REC_PROJECT_CODEPAGE = 0x0003;
4
+ export const REC_PROJECT_VERSION = 0x0009;
5
+ export const REC_REFERENCE_REGISTERED = 0x000d;
6
+ export const REC_MODULES_COUNT = 0x000f;
7
+ export const REC_REFERENCE_NAME = 0x0016;
8
+ export const REC_MODULE_NAME = 0x0019;
9
+ export const REC_MODULE_STREAMNAME = 0x001a;
10
+ export const REC_MODULE_TYPE_PROCEDURAL = 0x0021;
11
+ export const REC_MODULE_TYPE_DOCUMENT = 0x0022;
12
+ export const REC_MODULE_TERMINATOR = 0x002b;
13
+ export const REC_MODULE_OFFSET = 0x0031;
14
+ export const REC_REFERENCE_NAME_UNICODE = 0x003e;
15
+ export function* dirRecords(dir, context) {
16
+ let pos = 0;
17
+ while (pos + 6 <= dir.length) {
18
+ const id = readU16(dir, pos);
19
+ const size = readU32(dir, pos + 2);
20
+ const dataStart = pos + 6;
21
+ if (dataStart + size > dir.length) {
22
+ throw new VbaParseError(`dir record 0x${id.toString(16)} ${context}`);
23
+ }
24
+ const end = dataStart + size + (id === REC_PROJECT_VERSION ? 2 : 0);
25
+ yield { id, recordStart: pos, dataStart, size, end };
26
+ pos = end;
27
+ }
28
+ }
@@ -1,7 +1,8 @@
1
- import { readU16, readU32 } from './bytes.js';
1
+ import { readU16 } from './bytes.js';
2
2
  import { writeCompoundFile } from './cfb-writer.js';
3
3
  import { CompoundFile } from './cfb.js';
4
4
  import { decoderForCodePage, encoderForCodePage } from './codepage.js';
5
+ import { dirRecords, REC_MODULE_NAME, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULES_COUNT, REC_REFERENCE_NAME, REC_REFERENCE_NAME_UNICODE, REC_REFERENCE_REGISTERED, } from './dir-records.js';
5
6
  import { VbaAuthorError, VbaParseError } from './errors.js';
6
7
  import { compressContainer, decompressContainer } from './ms-ovba.js';
7
8
  import { parseVbaProject } from './project.js';
@@ -10,14 +11,6 @@ const DIR_STREAM = 'dir';
10
11
  const PROJECT_STREAM = 'PROJECT';
11
12
  const PROJECTWM_STREAM = 'PROJECTwm';
12
13
  const VBA_STORAGE = 'VBA';
13
- const REC_MODULE_NAME = 0x0019;
14
- const REC_MODULE_STREAMNAME = 0x001a;
15
- const REC_MODULE_TERMINATOR = 0x002b;
16
- const REC_PROJECT_VERSION = 0x0009;
17
- const REC_MODULES_COUNT = 0x000f;
18
- const REC_REFERENCE_NAME = 0x0016;
19
- const REC_REFERENCE_NAME_UNICODE = 0x003e;
20
- const REC_REFERENCE_REGISTERED = 0x000d;
21
14
  export function removeVbaModule(bin, name) {
22
15
  const project = parseVbaProject(bin);
23
16
  const nameKey = name.toUpperCase();
@@ -124,18 +117,7 @@ function buildReferenceDirRecords(ref, encode) {
124
117
  }
125
118
  function insertReferenceDirRecords(dir, records) {
126
119
  let insertAt = -1;
127
- let pos = 0;
128
- while (pos + 6 <= dir.length) {
129
- const recordStart = pos;
130
- const id = readU16(dir, pos);
131
- const size = readU32(dir, pos + 2);
132
- const dataStart = pos + 6;
133
- if (dataStart + size > dir.length) {
134
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while adding a reference`);
135
- }
136
- pos = dataStart + size;
137
- if (id === REC_PROJECT_VERSION)
138
- pos += 2;
120
+ for (const { id, recordStart } of dirRecords(dir, 'overruns while adding a reference')) {
139
121
  if (id === REC_MODULES_COUNT) {
140
122
  insertAt = recordStart;
141
123
  break;
@@ -157,18 +139,7 @@ function removeModuleDirRecord(dir, streamName, codePage) {
157
139
  let removeStart = -1;
158
140
  let removeEnd = -1;
159
141
  let currentStream;
160
- let pos = 0;
161
- while (pos + 6 <= dir.length) {
162
- const recordStart = pos;
163
- const id = readU16(dir, pos);
164
- const size = readU32(dir, pos + 2);
165
- const dataStart = pos + 6;
166
- if (dataStart + size > dir.length) {
167
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns while removing a module`);
168
- }
169
- pos = dataStart + size;
170
- if (id === REC_PROJECT_VERSION)
171
- pos += 2;
142
+ for (const { id, recordStart, dataStart, size, end } of dirRecords(dir, 'overruns while removing a module')) {
172
143
  if (id === REC_MODULES_COUNT) {
173
144
  if (size < 2)
174
145
  throw new VbaParseError('PROJECTMODULES MODULES_COUNT record is malformed');
@@ -183,7 +154,7 @@ function removeModuleDirRecord(dir, streamName, codePage) {
183
154
  else if (id === REC_MODULE_TERMINATOR) {
184
155
  if (currentStream === streamName) {
185
156
  removeStart = blockStart;
186
- removeEnd = pos;
157
+ removeEnd = end;
187
158
  }
188
159
  currentStream = undefined;
189
160
  blockStart = -1;
@@ -1,6 +1,7 @@
1
1
  import { readU16, readU32 } from './bytes.js';
2
2
  import { CompoundFile } from './cfb.js';
3
3
  import { decoderForCodePage } from './codepage.js';
4
+ import { dirRecords, REC_MODULE_NAME, REC_MODULE_OFFSET, REC_MODULE_STREAMNAME, REC_MODULE_TERMINATOR, REC_MODULE_TYPE_DOCUMENT, REC_MODULE_TYPE_PROCEDURAL, REC_PROJECT_CODEPAGE, } from './dir-records.js';
4
5
  import { VbaParseError } from './errors.js';
5
6
  import { decompressContainer } from './ms-ovba.js';
6
7
  export const VBA_PROJECT_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
@@ -14,14 +15,6 @@ const SIGNATURE_KIND_BY_REL_SEGMENT = {
14
15
  export function vbaProjectSignatureKind(relType) {
15
16
  return SIGNATURE_KIND_BY_REL_SEGMENT[relType.slice(relType.lastIndexOf('/') + 1)];
16
17
  }
17
- const REC_PROJECT_CODEPAGE = 0x0003;
18
- const REC_MODULE_NAME = 0x0019;
19
- const REC_MODULE_STREAMNAME = 0x001a;
20
- const REC_MODULE_TYPE_PROCEDURAL = 0x0021;
21
- const REC_MODULE_TYPE_DOCUMENT = 0x0022;
22
- const REC_MODULE_OFFSET = 0x0031;
23
- const REC_MODULE_TERMINATOR = 0x002b;
24
- const REC_PROJECT_VERSION = 0x0009;
25
18
  export function parseVbaProject(bin) {
26
19
  const cfb = new CompoundFile(bin);
27
20
  const dirCompressed = cfb.readStream('dir');
@@ -31,16 +24,7 @@ export function parseVbaProject(bin) {
31
24
  let codePage = 1252;
32
25
  const rawModules = [];
33
26
  let pending = {};
34
- let pos = 0;
35
- while (pos + 6 <= dir.length) {
36
- const id = readU16(dir, pos);
37
- const size = readU32(dir, pos + 2);
38
- const dataStart = pos + 6;
39
- if (dataStart + size > dir.length)
40
- throw new VbaParseError(`dir record 0x${id.toString(16)} overruns stream`);
41
- pos = dataStart + size;
42
- if (id === REC_PROJECT_VERSION)
43
- pos += 2;
27
+ for (const { id, dataStart, size } of dirRecords(dir, 'overruns stream')) {
44
28
  switch (id) {
45
29
  case REC_PROJECT_CODEPAGE:
46
30
  if (size >= 2)
@@ -134,3 +134,63 @@ export declare function numFinite(val: string | undefined, min?: number): number
134
134
  * re-spelled into a number that would not re-write byte-clean. Callers layer their own type rules
135
135
  * (a data-validation `list`/`custom` operand stays a string regardless of what it looks like). */
136
136
  export declare function coerceNumericLiteral(text: string): string | number;
137
+ /** Narrow an enumerated attribute through its guard; `undefined` when absent or not a member. */
138
+ export declare function enumToken<T extends string>(val: string | undefined, isMember: (candidate: string) => candidate is T): T | undefined;
139
+ /**
140
+ * Gathers one element's character data across the open/text/close events a SAX parse delivers it in.
141
+ *
142
+ * Nine parsers used to open-code this: latch a flag and clear a buffer on the open, append every
143
+ * chunk while latched, consume the buffer and unlatch on the close. Six spellings of one idea, and
144
+ * none of them honoured the one thing {@link SaxHandlers.onOpen} warns about. A self-closing `<x/>`
145
+ * fires no matching close, so `<t/>`, `<text/>`, `<xm:f/>` and `<totalsRowFormula/>`, all legal and
146
+ * all written by real files, latched a capture that nothing would ever close. What kept that from
147
+ * corrupting anything was the order the next open happened to reset things in, which is an accident
148
+ * rather than a property anyone chose, on a path that reads untrusted input. Taking `selfClosing`
149
+ * here makes it structural, once.
150
+ *
151
+ * The other thing the open-coded versions disagreed on is what an unrelated element opening
152
+ * mid-capture should do. Ending the capture is never what a caller wants: the text belongs to the
153
+ * element that opened it, and a nested or sibling element is not that element. So an open that is
154
+ * not for a captured name leaves an capture in progress alone, and {@link close} answers only for
155
+ * the element that started it.
156
+ *
157
+ * Decoding stays outside. A `<t>` needs `decodeSpreadsheetText` over the whole element and never
158
+ * over a chunk, an `<xm:f>` needs nothing, and a coordinate needs a number: the caller knows which.
159
+ */
160
+ export declare class TextCapture {
161
+ #private;
162
+ /** @param names the element local name, or the set of names this instance may capture. */
163
+ constructor(names: string | Iterable<string>);
164
+ /** Whether a capture is currently open. */
165
+ get capturing(): boolean;
166
+ /** Begin capturing `local` if it is one of this instance's names and is not self-closing. */
167
+ open(local: string, selfClosing: boolean): void;
168
+ /** Feed a chunk of character data; ignored when no capture is open. */
169
+ text(chunk: string): void;
170
+ /** The gathered text when `local` closes the captured element, else `undefined`. Unlatches. */
171
+ close(local: string): string | undefined;
172
+ }
173
+ /**
174
+ * Yield each named element's text as that element closes, as `{local, text}`.
175
+ *
176
+ * The third member of the pull-shaped family beside {@link openElements} ("scan opens, read
177
+ * attributes") and {@link closeEmptyElements}: this one is "capture these elements' text, tell me
178
+ * each as it closes". A parser whose whole job is reading a handful of text elements out of a part
179
+ * writes a `for..of` over it instead of a {@link parseXml} handler triple whose open and text arms
180
+ * are the same three lines every time.
181
+ *
182
+ * It is deliberately not for every {@link TextCapture} caller. A parser that interleaves capture
183
+ * with per-element state of its own (a `<dataValidation>` gathering formulae, a `<tableColumn>`
184
+ * attaching a totals formula to the column it is inside) needs the open and attribute events too,
185
+ * and stays bespoke; forcing it through here would trade a handler triple for a second pass.
186
+ *
187
+ * A self-closing `<x/>` carries no text and fires no close, so it yields nothing, which is the
188
+ * behaviour {@link TextCapture} exists to make structural rather than a branch each caller
189
+ * remembers.
190
+ *
191
+ * Throws {@link XmlParseError} on malformed markup.
192
+ */
193
+ export declare function capturedText(source: string, names: string | Iterable<string>): Generator<{
194
+ local: string;
195
+ text: string;
196
+ }>;
@@ -225,3 +225,50 @@ export function coerceNumericLiteral(text) {
225
225
  const trimmed = text.trim();
226
226
  return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : text;
227
227
  }
228
+ export function enumToken(val, isMember) {
229
+ return val !== undefined && isMember(val) ? val : undefined;
230
+ }
231
+ export class TextCapture {
232
+ #names;
233
+ #capturing;
234
+ #text = '';
235
+ constructor(names) {
236
+ this.#names = new Set(typeof names === 'string' ? [names] : names);
237
+ }
238
+ get capturing() {
239
+ return this.#capturing !== undefined;
240
+ }
241
+ open(local, selfClosing) {
242
+ if (selfClosing || !this.#names.has(local))
243
+ return;
244
+ this.#capturing = local;
245
+ this.#text = '';
246
+ }
247
+ text(chunk) {
248
+ if (this.#capturing !== undefined)
249
+ this.#text += chunk;
250
+ }
251
+ close(local) {
252
+ if (this.#capturing !== local)
253
+ return undefined;
254
+ this.#capturing = undefined;
255
+ return this.#text;
256
+ }
257
+ }
258
+ export function* capturedText(source, names) {
259
+ const capture = new TextCapture(names);
260
+ for (const event of xmlEvents(source)) {
261
+ if (event.kind === 'open') {
262
+ capture.open(localName(event.name), event.selfClosing);
263
+ }
264
+ else if (event.kind === 'text') {
265
+ capture.text(event.text);
266
+ }
267
+ else {
268
+ const local = localName(event.name);
269
+ const text = capture.close(local);
270
+ if (text !== undefined)
271
+ yield { local, text };
272
+ }
273
+ }
274
+ }
package/dist/xml/xml.d.ts CHANGED
@@ -50,11 +50,53 @@ export declare function stripFormulaEquals(value: string | number): string;
50
50
  * entirely: the two-state-plus-absent contract every flag writer here shares.
51
51
  */
52
52
  export declare function boolAttr(name: string, value: boolean | undefined): string;
53
+ /**
54
+ * A free-string attribute rendered with a leading space (` name="a &amp; b"`), or '' when the value
55
+ * is undefined. The third of the trio with {@link boolAttr} and {@link numAttr}: the value is prose
56
+ * the caller chose, so it is escaped rather than checked.
57
+ *
58
+ * A token from a closed OOXML enumeration is the other case and goes through {@link checkedToken}
59
+ * instead. Escaping a bogus token would produce a well-formed document that Excel still rejects,
60
+ * which hides the mistake in the file rather than raising it at the call.
61
+ */
62
+ export declare function textAttr(name: string, value: string | undefined): string;
63
+ /**
64
+ * Refuse a token from a closed OOXML enumeration that the writer would otherwise emit verbatim, and
65
+ * return it unchanged when it belongs. The public types already forbid an out-of-contract value, so
66
+ * this fires only for one smuggled past them by an untyped caller, a `JSON.parse`, or a cast.
67
+ *
68
+ * The writer must never serialise such a value: it would be schema-invalid OOXML that Excel
69
+ * sometimes tolerates yet the library's own reader, which narrows every such token through the same
70
+ * guard, discards on read-back. Refusing at the write boundary keeps the two symmetric, garbage out
71
+ * refused exactly as garbage in, so a value the writer accepts is always one that round-trips.
72
+ *
73
+ * @throws {AuthoringError} naming the value and the enumeration.
74
+ */
75
+ export declare function checkedToken(value: string, isValid: (candidate: string) => boolean, kind: string): string;
53
76
  /**
54
77
  * A numeric attribute rendered with a leading space (` name="42"`), or '' when the value is undefined
55
78
  * so that a count an author never set stays out of the element rather than fabricating a default.
79
+ *
80
+ * The value goes through {@link numberText}, so a non-finite one is refused here rather than written
81
+ * as `NaN`. Every attribute on this path is `xsd:double` or `xsd:unsignedInt`, neither of which has a
82
+ * spelling for it, so the alternative is a package Excel reports as damaged.
83
+ *
84
+ * @throws {AuthoringError} when the value is not finite.
85
+ */
86
+ export declare function numAttr(name: string, value: number | undefined): string;
87
+ /**
88
+ * Refuse a number OOXML cannot spell. Every numeric attribute in the format is `xsd:double`,
89
+ * `xsd:unsignedInt` or a bounded flavour of one, and none of those lexical spaces has a form for a
90
+ * NaN or an infinity, so a value that reaches the file as `NaN` is a package Excel reports as
91
+ * damaged.
92
+ *
93
+ * Exported for the callers that must refuse before they write. A number can be unwritable and still
94
+ * be read on the way to the bytes: compared, summed, walked. A comparison against `NaN` or an
95
+ * infinity silently takes the wrong branch long before the value would have been serialised.
96
+ *
97
+ * @throws {AuthoringError} naming the value.
56
98
  */
57
- export declare function attr(name: string, value: number | undefined): string;
99
+ export declare function assertWritableNumber(value: number): void;
58
100
  /**
59
101
  * A finite number serialises as its shortest round-trippable decimal; a non-finite one
60
102
  * has no OOXML numeric representation, so the writer refuses it rather than emit `NaN`.
package/dist/xml/xml.js CHANGED
@@ -55,13 +55,25 @@ export function stripFormulaEquals(value) {
55
55
  export function boolAttr(name, value) {
56
56
  return value === undefined ? '' : ` ${name}="${value ? 1 : 0}"`;
57
57
  }
58
- export function attr(name, value) {
59
- return value === undefined ? '' : ` ${name}="${value}"`;
58
+ export function textAttr(name, value) {
59
+ return value === undefined ? '' : ` ${name}="${escapeAttr(value)}"`;
60
60
  }
61
- export function numberText(value) {
62
- if (!Number.isFinite(value)) {
63
- throw new AuthoringError(`cannot write a non-finite number (${value}): it has no OOXML representation`);
61
+ export function checkedToken(value, isValid, kind) {
62
+ if (!isValid(value)) {
63
+ throw new AuthoringError(`Invalid ${kind} ${JSON.stringify(value)}: not a value the OOXML enumeration allows`);
64
64
  }
65
+ return value;
66
+ }
67
+ export function numAttr(name, value) {
68
+ return value === undefined ? '' : ` ${name}="${numberText(value)}"`;
69
+ }
70
+ export function assertWritableNumber(value) {
71
+ if (Number.isFinite(value))
72
+ return;
73
+ throw new AuthoringError(`cannot write a non-finite number (${value}): it has no OOXML representation`);
74
+ }
75
+ export function numberText(value) {
76
+ assertWritableNumber(value);
65
77
  return String(value);
66
78
  }
67
79
  export const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';