@shbernal/ts-xlsx 2.1.0 → 3.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 (225) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +5 -1
  16. package/dist/core/column.js +34 -35
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +9 -3
  27. package/dist/core/data-validation.js +21 -18
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +14 -10
  39. package/dist/core/image.js +9 -9
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -1,33 +1,34 @@
1
- import { readU16 } from './bytes.js';
2
- import { writeCompoundFile } from './cfb-writer.js';
1
+ import { quoted } from '../errors.js';
2
+ import { readU16, spliceBytes, writeU16 } from './bytes.js';
3
+ import { isStream, writeCompoundFile } from './cfb-writer.js';
3
4
  import { CompoundFile } from './cfb.js';
4
5
  import { decoderForCodePage, encoderForCodePage } from './codepage.js';
5
6
  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';
6
7
  import { VbaAuthorError, VbaParseError } from './errors.js';
7
8
  import { compressContainer, decompressContainer } from './ms-ovba.js';
8
- import { parseVbaProject } from './project.js';
9
+ import { parseVbaProjectIn } from './project.js';
9
10
  import { push, u16, u32, utf16le, validateVbaName } from './vba-encoding.js';
10
11
  const DIR_STREAM = 'dir';
11
12
  const PROJECT_STREAM = 'PROJECT';
12
13
  const PROJECTWM_STREAM = 'PROJECTwm';
13
14
  const VBA_STORAGE = 'VBA';
14
15
  export function removeVbaModule(bin, name) {
15
- const project = parseVbaProject(bin);
16
+ const cfb = new CompoundFile(bin);
17
+ const project = parseVbaProjectIn(cfb);
16
18
  const nameKey = name.toUpperCase();
17
19
  const module = project.modules.find((m) => m.name.toUpperCase() === nameKey);
18
20
  if (!module)
19
- throw new VbaAuthorError(`module '${name}' is not in the VBA project`);
21
+ throw new VbaAuthorError(`module ${quoted(name)} is not in the VBA project`);
20
22
  if (module.kind !== 'procedural' && module.kind !== 'class') {
21
- throw new VbaAuthorError(`cannot remove module '${name}': its kind '${module.kind}' is tied to host linkage this ` +
23
+ throw new VbaAuthorError(`cannot remove module ${quoted(name)}: its kind ${quoted(module.kind)} is tied to host linkage this ` +
22
24
  'primitive cannot verify');
23
25
  }
24
- const cfb = new CompoundFile(bin);
26
+ const decoder = decoderForCodePage(project.codePage);
25
27
  const dirCompressed = cfb.readStream(DIR_STREAM);
26
28
  if (!dirCompressed)
27
29
  throw new VbaParseError("VBA project has no 'dir' stream");
28
- const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, project.codePage);
30
+ const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, decoder);
29
31
  const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
30
- const decoder = decoderForCodePage(project.codePage);
31
32
  const encode = encoderForCodePage(project.codePage);
32
33
  const projectText = cfb.readStream(PROJECT_STREAM);
33
34
  if (projectText) {
@@ -44,7 +45,7 @@ export function removeVbaModule(bin, name) {
44
45
  const removed = new Set();
45
46
  const newTree = removeFromStorage(withReplacements, VBA_STORAGE, module.streamName, removed);
46
47
  if (!removed.has(VBA_STORAGE)) {
47
- throw new VbaParseError(`module stream '${module.streamName}' is not in the '${VBA_STORAGE}' storage`);
48
+ throw new VbaParseError(`module stream ${quoted(module.streamName)} is not in the ${quoted(VBA_STORAGE)} storage`);
48
49
  }
49
50
  return writeCompoundFile(newTree);
50
51
  }
@@ -56,7 +57,7 @@ function normalizeReference(ref) {
56
57
  validateVbaName(ref.name, 'reference');
57
58
  const guidMatch = GUID_PATTERN.exec(ref.guid.trim());
58
59
  if (!guidMatch)
59
- throw new VbaAuthorError(`invalid reference GUID '${ref.guid}'`);
60
+ throw new VbaAuthorError(`invalid reference GUID ${quoted(ref.guid)}`);
60
61
  const guid = `{${guidMatch.slice(1, 6).join('-').toUpperCase()}}`;
61
62
  for (const [field, value] of [
62
63
  ['majorVersion', ref.majorVersion],
@@ -71,13 +72,13 @@ function normalizeReference(ref) {
71
72
  throw new VbaAuthorError(`reference lcid must be an integer in [0, 0xFFFFFFFF], got ${lcid}`);
72
73
  }
73
74
  if (ref.path.length === 0 || ref.path.includes('\0') || ref.path.includes('#')) {
74
- throw new VbaAuthorError(`invalid reference path '${ref.path}' (must be non-empty and contain no NUL or '#')`);
75
+ throw new VbaAuthorError(`invalid reference path ${quoted(ref.path)} (must be non-empty and contain no NUL or '#')`);
75
76
  }
76
77
  const displayName = ref.displayName ?? ref.name;
77
78
  if (displayName.length === 0 ||
78
79
  displayName.length > MAX_DISPLAY_NAME_CHARS ||
79
80
  displayName.includes('\0')) {
80
- throw new VbaAuthorError(`invalid reference display name '${displayName}'`);
81
+ throw new VbaAuthorError(`invalid reference display name ${quoted(displayName)}`);
81
82
  }
82
83
  const libid = `*\\G${guid}#${ref.majorVersion.toString(16).toUpperCase()}.` +
83
84
  `${ref.minorVersion.toString(16).toUpperCase()}#${lcid.toString(16).toUpperCase()}` +
@@ -86,9 +87,8 @@ function normalizeReference(ref) {
86
87
  }
87
88
  export function addVbaReference(bin, ref) {
88
89
  const normalized = normalizeReference(ref);
89
- const project = parseVbaProject(bin);
90
- const encode = encoderForCodePage(project.codePage);
91
90
  const cfb = new CompoundFile(bin);
91
+ const encode = encoderForCodePage(parseVbaProjectIn(cfb).codePage);
92
92
  const dirCompressed = cfb.readStream(DIR_STREAM);
93
93
  if (!dirCompressed)
94
94
  throw new VbaParseError("VBA project has no 'dir' stream");
@@ -125,15 +125,9 @@ function insertReferenceDirRecords(dir, records) {
125
125
  }
126
126
  if (insertAt < 0)
127
127
  throw new VbaParseError('dir stream is missing MODULES_COUNT');
128
- const rec = Uint8Array.from(records);
129
- const out = new Uint8Array(dir.length + rec.length);
130
- out.set(dir.subarray(0, insertAt), 0);
131
- out.set(rec, insertAt);
132
- out.set(dir.subarray(insertAt), insertAt + rec.length);
133
- return out;
128
+ return spliceBytes(dir, insertAt, insertAt, Uint8Array.from(records));
134
129
  }
135
- function removeModuleDirRecord(dir, streamName, codePage) {
136
- const decoder = decoderForCodePage(codePage);
130
+ function removeModuleDirRecord(dir, streamName, decoder) {
137
131
  let countAt = -1;
138
132
  let blockStart = -1;
139
133
  let removeStart = -1;
@@ -143,7 +137,8 @@ function removeModuleDirRecord(dir, streamName, codePage) {
143
137
  if (id === REC_MODULES_COUNT) {
144
138
  if (size < 2)
145
139
  throw new VbaParseError('PROJECTMODULES MODULES_COUNT record is malformed');
146
- countAt = dataStart;
140
+ if (countAt < 0)
141
+ countAt = dataStart;
147
142
  }
148
143
  else if (id === REC_MODULE_NAME) {
149
144
  blockStart = recordStart;
@@ -163,14 +158,17 @@ function removeModuleDirRecord(dir, streamName, codePage) {
163
158
  if (countAt < 0)
164
159
  throw new VbaParseError('dir stream is missing MODULES_COUNT');
165
160
  if (removeStart < 0 || removeEnd < 0) {
166
- throw new VbaParseError(`module stream '${streamName}' not found in the dir stream`);
167
- }
168
- const out = new Uint8Array(dir.length - (removeEnd - removeStart));
169
- out.set(dir.subarray(0, removeStart), 0);
170
- out.set(dir.subarray(removeEnd), removeStart);
171
- const newCount = readU16(out, countAt) - 1;
172
- out[countAt] = newCount & 0xff;
173
- out[countAt + 1] = (newCount >> 8) & 0xff;
161
+ throw new VbaParseError(`module stream ${quoted(streamName)} not found in the dir stream`);
162
+ }
163
+ if (countAt >= removeStart) {
164
+ throw new VbaParseError('dir stream declares MODULES_COUNT after a module block');
165
+ }
166
+ const count = readU16(dir, countAt);
167
+ if (count === 0) {
168
+ throw new VbaParseError('dir stream declares zero modules but carries a module block');
169
+ }
170
+ const out = spliceBytes(dir, removeStart, removeEnd);
171
+ writeU16(out, countAt, count - 1);
174
172
  return out;
175
173
  }
176
174
  function removeProjectStreamLines(text, name, kind) {
@@ -218,20 +216,17 @@ function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
218
216
  }
219
217
  }
220
218
  if (removeStart < 0 || removeEnd < 0) {
221
- throw new VbaParseError(`module '${name}' not found in the PROJECTwm stream`);
219
+ throw new VbaParseError(`module ${quoted(name)} not found in the PROJECTwm stream`);
222
220
  }
223
- const out = new Uint8Array(wm.length - (removeEnd - removeStart));
224
- out.set(wm.subarray(0, removeStart), 0);
225
- out.set(wm.subarray(removeEnd), removeStart);
226
- return out;
221
+ return spliceBytes(wm, removeStart, removeEnd);
227
222
  }
228
223
  function removeFromStorage(nodes, storageName, streamName, removed) {
229
224
  return nodes.map((n) => {
230
- if ('data' in n)
225
+ if (isStream(n))
231
226
  return n;
232
227
  const children = removeFromStorage(n.children, storageName, streamName, removed);
233
228
  if (n.name === storageName && !removed.has(storageName)) {
234
- const filtered = children.filter((c) => !('data' in c && c.name === streamName));
229
+ const filtered = children.filter((c) => !(isStream(c) && c.name === streamName));
235
230
  if (filtered.length !== children.length)
236
231
  removed.add(storageName);
237
232
  return { name: n.name, children: filtered };
@@ -241,7 +236,7 @@ function removeFromStorage(nodes, storageName, streamName, removed) {
241
236
  }
242
237
  function replaceStreams(nodes, replacements, applied) {
243
238
  return nodes.map((node) => {
244
- if ('data' in node) {
239
+ if (isStream(node)) {
245
240
  const data = replacements.get(node.name);
246
241
  if (data !== undefined) {
247
242
  applied.add(node.name);
@@ -1,3 +1,4 @@
1
+ import { CompoundFile } from './cfb.ts';
1
2
  /** How a module participates in the project: the classification the VBA editor shows. */
2
3
  export type VbaModuleKind = 'procedural' | 'document' | 'class' | 'designer';
3
4
  export interface VbaModule {
@@ -42,4 +43,13 @@ export interface VbaProjectSignature {
42
43
  * carries (`.../office/2006/...` vs `.../2014/...` vs `.../2020/...`).
43
44
  */
44
45
  export declare function vbaProjectSignatureKind(relType: string): VbaProjectSignatureKind | undefined;
45
- export declare function parseVbaProject(bin: Uint8Array): VbaProject;
46
+ export declare function parseVbaProject(bin: Uint8Array, maxOutput?: number): VbaProject;
47
+ /**
48
+ * As {@link parseVbaProject}, over a container the caller has already opened.
49
+ *
50
+ * Both project editors parse fail-closed first and then need the container itself, and each was
51
+ * building a second `CompoundFile` over the same bytes: a second walk of the DIFAT, FAT, mini-FAT,
52
+ * directory and mini-stream, which is roughly double the cost and double the peak allocation of every
53
+ * edit. Opening it once and handing it in is the whole of the fix.
54
+ */
55
+ export declare function parseVbaProjectIn(cfb: CompoundFile, maxOutput?: number): VbaProject;
@@ -1,3 +1,4 @@
1
+ import { quoted } from '../errors.js';
1
2
  import { readU16, readU32 } from './bytes.js';
2
3
  import { CompoundFile } from './cfb.js';
3
4
  import { decoderForCodePage } from './codepage.js';
@@ -7,20 +8,26 @@ import { decompressContainer } from './ms-ovba.js';
7
8
  export const VBA_PROJECT_REL_TYPE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
8
9
  export const VBA_PROJECT_CONTENT_TYPE = 'application/vnd.ms-office.vbaProject';
9
10
  export const VBA_PROJECT_PART_PATH = 'xl/vbaProject.bin';
10
- const SIGNATURE_KIND_BY_REL_SEGMENT = {
11
- vbaProjectSignature: 'legacy',
12
- vbaProjectSignatureAgile: 'agile',
13
- vbaProjectSignatureV3: 'v3',
14
- };
11
+ const SIGNATURE_KIND_BY_REL_SEGMENT = new Map([
12
+ ['vbaProjectSignature', 'legacy'],
13
+ ['vbaProjectSignatureAgile', 'agile'],
14
+ ['vbaProjectSignatureV3', 'v3'],
15
+ ]);
15
16
  export function vbaProjectSignatureKind(relType) {
16
- return SIGNATURE_KIND_BY_REL_SEGMENT[relType.slice(relType.lastIndexOf('/') + 1)];
17
+ return SIGNATURE_KIND_BY_REL_SEGMENT.get(relType.slice(relType.lastIndexOf('/') + 1));
17
18
  }
18
- export function parseVbaProject(bin) {
19
- const cfb = new CompoundFile(bin);
19
+ function isComplete(module) {
20
+ return module.streamName !== undefined && module.offset !== undefined;
21
+ }
22
+ export function parseVbaProject(bin, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
23
+ return parseVbaProjectIn(new CompoundFile(bin), maxOutput);
24
+ }
25
+ export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
26
+ const budget = new DecompressionBudget(maxOutput);
20
27
  const dirCompressed = cfb.readStream('dir');
21
28
  if (!dirCompressed)
22
29
  throw new VbaParseError("VBA project has no 'dir' stream");
23
- const dir = decompressContainer(dirCompressed);
30
+ const dir = budget.spend(dirCompressed, 0);
24
31
  let codePage = 1252;
25
32
  const rawModules = [];
26
33
  let pending = {};
@@ -47,7 +54,7 @@ export function parseVbaProject(bin) {
47
54
  pending.offset = readU32(dir, dataStart);
48
55
  break;
49
56
  case REC_MODULE_TERMINATOR:
50
- if (pending.streamName !== undefined && pending.offset !== undefined)
57
+ if (isComplete(pending))
51
58
  rawModules.push(pending);
52
59
  pending = {};
53
60
  break;
@@ -65,34 +72,46 @@ export function parseVbaProject(bin) {
65
72
  name,
66
73
  streamName,
67
74
  kind,
68
- source: readModuleSource(cfb, streamName, m.offset, decoder),
75
+ source: readModuleSource(cfb, streamName, m.offset, decoder, budget),
69
76
  };
70
77
  });
71
78
  return { codePage, modules };
72
79
  }
73
- function readModuleSource(cfb, streamName, textOffset, decoder) {
80
+ function readModuleSource(cfb, streamName, textOffset, decoder, budget) {
74
81
  const stream = cfb.readStream(streamName);
75
82
  if (!stream)
76
- throw new VbaParseError(`module stream '${streamName}' not found in container`);
77
- return decoder.decode(decompressContainer(stream, textOffset));
83
+ throw new VbaParseError(`module stream ${quoted(streamName)} not found in container`);
84
+ return decoder.decode(budget.spend(stream, textOffset));
85
+ }
86
+ const DEFAULT_MAX_PROJECT_OUTPUT = 64 * 1024 * 1024;
87
+ class DecompressionBudget {
88
+ #remaining;
89
+ constructor(total) {
90
+ this.#remaining = total;
91
+ }
92
+ spend(stream, start) {
93
+ const out = decompressContainer(stream, start, this.#remaining);
94
+ this.#remaining -= out.length;
95
+ return out;
96
+ }
78
97
  }
98
+ const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
99
+ ['Document', 'document'],
100
+ ['Module', 'procedural'],
101
+ ['Class', 'class'],
102
+ ['BaseClass', 'designer'],
103
+ ]);
79
104
  function readProjectStreamKinds(cfb, decoder) {
80
105
  const kinds = new Map();
81
106
  const stream = cfb.readStream('PROJECT');
82
107
  if (!stream)
83
108
  return kinds;
84
109
  const text = decoder.decode(stream);
85
- const keyword = {
86
- Document: 'document',
87
- Module: 'procedural',
88
- Class: 'class',
89
- BaseClass: 'designer',
90
- };
91
110
  for (const line of text.split(/\r\n|\r|\n/)) {
92
111
  const eq = line.indexOf('=');
93
112
  if (eq < 0)
94
113
  continue;
95
- const kind = keyword[line.slice(0, eq)];
114
+ const kind = MODULE_KIND_BY_PROJECT_KEYWORD.get(line.slice(0, eq));
96
115
  if (kind === undefined)
97
116
  continue;
98
117
  const name = (line.slice(eq + 1).split('/')[0] ?? '').trim();
@@ -1,9 +1,11 @@
1
+ import { utf16leBytes } from '../bytes.js';
2
+ import { quoted } from '../errors.js';
3
+ import { MAX_NAME_CHARS } from './cfb-format.js';
1
4
  import { VbaAuthorError } from './errors.js';
2
5
  const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
3
- const MAX_NAME_CHARS = 31;
4
6
  export function validateVbaName(name, what) {
5
7
  if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
6
- throw new VbaAuthorError(`invalid ${what} name '${name}' (must be a VBA identifier ≤ 31 chars)`);
8
+ throw new VbaAuthorError(`invalid ${what} name ${quoted(name)} (must be a VBA identifier ≤ 31 chars)`);
7
9
  }
8
10
  }
9
11
  export function push(out, id, data) {
@@ -16,8 +18,5 @@ export function u32(n) {
16
18
  return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff];
17
19
  }
18
20
  export function utf16le(s) {
19
- const out = [];
20
- for (let i = 0; i < s.length; i++)
21
- out.push(...u16(s.charCodeAt(i)));
22
- return out;
21
+ return [...utf16leBytes(s)];
23
22
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The SpreadsheetML `_xHHHH_` escape, in the only place it may appear: a complete cell-text value.
3
+ *
4
+ * The mirror of `escapeSpreadsheetText` in `./xml.ts`, and it sits here rather than beside it for
5
+ * the same reason `decodeEntities` sits apart from `escapeText`: the write helpers carry an
6
+ * `AuthoringError` and a whole serialisation vocabulary the reader has no business importing.
7
+ *
8
+ * **One left-to-right pass, and that is load-bearing.** `005F` maps to `_` like any other code
9
+ * point, with no special case, because a single pass already gives the underscore escape its
10
+ * meaning: in `_x005F_x0041_` the match at 0 yields `_` and scanning resumes at `x0041_`, which has
11
+ * no leading underscore left to start an escape. So the value reads back as the literal seven
12
+ * characters `_x0041_` the author wrote. Decoding `_x005F_` in a pass of its own, before or after
13
+ * the rest, collapses that to `A` and loses the distinction the encoder went to trouble to keep.
14
+ * Excel agrees: it reads that cell as `_x0041_`.
15
+ *
16
+ * The decode is unconditional, not a repair of characters XML cannot carry. Excel reads
17
+ * `a_x0009_b` as a tab even though a literal tab would have been perfectly legal there, so a
18
+ * decoder that only handled the illegal range would disagree with Excel on files Excel wrote.
19
+ */
20
+ export declare function decodeSpreadsheetText(value: string): string;
21
+ /** An OOXML integer attribute at or above `min` (default: unbounded below); `undefined` when the
22
+ * attribute is absent, blank, fractional, not a number, or below the floor. Integers past
23
+ * `Number.MAX_SAFE_INTEGER` read as `undefined` too: no index or count is usable out there, and
24
+ * arithmetic on one silently lies. */
25
+ export declare function numInteger(val: string | undefined, min?: number): number | undefined;
26
+ /** An OOXML decimal attribute at or above `min` (default: unbounded below); `undefined` when the
27
+ * attribute is absent, blank, not a number, or below the floor. Infinities are not finite numbers
28
+ * and read as `undefined`. */
29
+ export declare function numFinite(val: string | undefined, min?: number): number | undefined;
30
+ /** Read an operand's text as a number only when it is a canonical decimal literal (optional sign,
31
+ * digits, optional fraction). A cell reference, defined name, expression, or exotically-spelled
32
+ * number (`1E5`, hex) keeps its verbatim text, so it is neither coerced to `NaN` and lost nor
33
+ * re-spelled into a number that would not re-write byte-clean. Callers layer their own type rules
34
+ * (a data-validation `list`/`custom` operand stays a string regardless of what it looks like). */
35
+ export declare function coerceNumericLiteral(text: string): string | number;
36
+ /** Narrow an enumerated attribute through its guard; `undefined` when absent or not a member. */
37
+ export declare function enumToken<T extends string>(val: string | undefined, isMember: (candidate: string) => candidate is T): T | undefined;
@@ -0,0 +1,30 @@
1
+ export function decodeSpreadsheetText(value) {
2
+ if (!value.includes('_'))
3
+ return value;
4
+ return value.replace(/_x([0-9A-Fa-f]{4})_/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
5
+ }
6
+ export function numInteger(val, min = -Number.MAX_SAFE_INTEGER) {
7
+ const n = parseAttrNumber(val);
8
+ if (n === undefined || !Number.isSafeInteger(n) || n < min)
9
+ return undefined;
10
+ return n;
11
+ }
12
+ export function numFinite(val, min = -Infinity) {
13
+ const n = parseAttrNumber(val);
14
+ if (n === undefined || n < min)
15
+ return undefined;
16
+ return n;
17
+ }
18
+ function parseAttrNumber(val) {
19
+ if (val === undefined || val.trim() === '')
20
+ return undefined;
21
+ const n = Number(val);
22
+ return Number.isFinite(n) ? n : undefined;
23
+ }
24
+ export function coerceNumericLiteral(text) {
25
+ const trimmed = text.trim();
26
+ return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : text;
27
+ }
28
+ export function enumToken(val, isMember) {
29
+ return val !== undefined && isMember(val) ? val : undefined;
30
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The characters an XML 1.0 document cannot carry, whatever escape you reach for.
3
+ *
4
+ * Three classes: the C0 controls outside the tab/LF/CR the `Char` production allows, the two
5
+ * noncharacters at the top of the BMP, and unpaired surrogates. The last are not an XML
6
+ * problem but a UTF-8 one: the encoder substitutes U+FFFD for a lone surrogate, so the
7
+ * package validates and the value is quietly gone, which is the same loss by a different
8
+ * route. U+007F and the C1 controls are deliberately absent: XML 1.1 forbids them, OOXML is
9
+ * 1.0.
10
+ *
11
+ * The `u` flag is load-bearing. It makes the pattern match code points, so an astral
12
+ * character is one unit that no surrogate range can match, and `[\u{D800}-\u{DFFF}]` means
13
+ * exactly "a surrogate that is not part of a pair" with no lookaround.
14
+ */
15
+ export declare const XML_UNREPRESENTABLE: RegExp;
16
+ /** The same rule with the `g` flag, for the escapes that rewrite every occurrence. */
17
+ export declare const XML_UNREPRESENTABLE_GLOBAL: RegExp;
18
+ /**
19
+ * Whether a code point may appear in an XML 1.0 document at all.
20
+ *
21
+ * The reader's question, and the reason this module exists. A numeric character reference names a
22
+ * code point directly, so a file is free to name one the format has no representation for; decoding
23
+ * it anyway puts a value in the model that the writer is *guaranteed* to refuse, turning a hostile
24
+ * file into an `AuthoringError` blaming the caller on the next save.
25
+ *
26
+ * A code point outside Unicode entirely is not this function's business -- the caller has already
27
+ * bounded the number before it can name a character -- so an out-of-range argument is refused here
28
+ * as well rather than throwing out of `String.fromCodePoint`.
29
+ */
30
+ export declare function isRepresentableCodePoint(codePoint: number): boolean;
31
+ /**
32
+ * Drop every code point XML 1.0 cannot carry.
33
+ *
34
+ * For text the scanner read out of a document, where there is no verbatim form to fall back to: a
35
+ * *reference* to an unrepresentable character can be left as the `&#1;` the file wrote, but a raw
36
+ * one has no spelling of its own to keep. Such a character makes the document ill-formed by the
37
+ * `Char` production, so it was never legally in the file; admitting it would put a value in the
38
+ * model that the writer must then refuse, which reports a corrupt input as the caller's mistake.
39
+ */
40
+ export declare function stripUnrepresentable(text: string): string;
@@ -0,0 +1,12 @@
1
+ export const XML_UNREPRESENTABLE = /[\u{0}-\u{8}\u{B}\u{C}\u{E}-\u{1F}\u{FFFE}\u{FFFF}\u{D800}-\u{DFFF}]/u;
2
+ export const XML_UNREPRESENTABLE_GLOBAL = new RegExp(XML_UNREPRESENTABLE.source, 'gu');
3
+ export function isRepresentableCodePoint(codePoint) {
4
+ if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff)
5
+ return false;
6
+ return !XML_UNREPRESENTABLE.test(String.fromCodePoint(codePoint));
7
+ }
8
+ export function stripUnrepresentable(text) {
9
+ if (!XML_UNREPRESENTABLE.test(text))
10
+ return text;
11
+ return text.replace(XML_UNREPRESENTABLE_GLOBAL, '');
12
+ }
@@ -0,0 +1,31 @@
1
+ import { type XmlAttributes } from './xml-scan.ts';
2
+ /**
3
+ * The prefix-to-namespace bindings in force at the current point of a scan.
4
+ *
5
+ * Properly scoped: a declaration binds only within the element that carries it, so the bindings are
6
+ * pushed and popped with the element stack. In practice OOXML puts them all on the part's root, but
7
+ * a reader that assumed so would be making the same kind of assumption this class exists to remove.
8
+ */
9
+ export declare class NamespaceScope {
10
+ #private;
11
+ /** Enter an element, taking any `xmlns` declarations it carries. */
12
+ open(attrs: XmlAttributes): void;
13
+ /** Leave an element, restoring whatever its declarations shadowed. */
14
+ close(): void;
15
+ /**
16
+ * The namespace URI a qualified *element* name resolves to, or `undefined` when its prefix is
17
+ * unbound. An unprefixed element takes the default namespace, which is what makes
18
+ * `<worksheet xmlns="…main">` and `<x:worksheet xmlns:x="…main">` the same document.
19
+ */
20
+ elementNamespace(qualified: string): string | undefined;
21
+ /** Whether a qualified element name is in `uri`. */
22
+ isElementIn(qualified: string, uri: string): boolean;
23
+ /**
24
+ * An attribute's value looked up by namespace and local name rather than by qualified name.
25
+ *
26
+ * An *unprefixed* attribute is in no namespace at all, never the default one, which is why this
27
+ * only ever matches a prefixed spelling. Every namespaced attribute OOXML uses (`r:id`, `r:embed`,
28
+ * `xml:space`) is written with a prefix for exactly that reason.
29
+ */
30
+ attr(attrs: XmlAttributes, uri: string, local: string): string | undefined;
31
+ }
@@ -0,0 +1,50 @@
1
+ import {} from './xml-scan.js';
2
+ const XMLNS = 'xmlns';
3
+ const XMLNS_PREFIX = 'xmlns:';
4
+ export class NamespaceScope {
5
+ #bindings = new Map();
6
+ #undo = [];
7
+ open(attrs) {
8
+ let undo;
9
+ for (const name in attrs) {
10
+ const prefix = name.startsWith(XMLNS_PREFIX)
11
+ ? name.slice(XMLNS_PREFIX.length)
12
+ : name === XMLNS
13
+ ? ''
14
+ : null;
15
+ if (prefix === null)
16
+ continue;
17
+ (undo ??= []).push([prefix, this.#bindings.get(prefix)]);
18
+ this.#bindings.set(prefix, attrs[name] ?? '');
19
+ }
20
+ this.#undo.push(undo);
21
+ }
22
+ close() {
23
+ const undo = this.#undo.pop();
24
+ if (undo === undefined)
25
+ return;
26
+ for (const [prefix, previous] of undo) {
27
+ if (previous === undefined)
28
+ this.#bindings.delete(prefix);
29
+ else
30
+ this.#bindings.set(prefix, previous);
31
+ }
32
+ }
33
+ elementNamespace(qualified) {
34
+ const colon = qualified.indexOf(':');
35
+ return this.#bindings.get(colon === -1 ? '' : qualified.slice(0, colon));
36
+ }
37
+ isElementIn(qualified, uri) {
38
+ return this.elementNamespace(qualified) === uri;
39
+ }
40
+ attr(attrs, uri, local) {
41
+ for (const [prefix, bound] of this.#bindings) {
42
+ if (prefix === '' || bound !== uri)
43
+ continue;
44
+ const value = attrs[`${prefix}:${local}`];
45
+ if (value !== undefined)
46
+ return value;
47
+ }
48
+ return undefined;
49
+ }
50
+ }