@shbernal/ts-xlsx 3.1.0 → 3.2.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 (214) hide show
  1. package/README.md +30 -23
  2. package/dist/bytes.d.ts +3 -2
  3. package/dist/core/address.d.ts +36 -0
  4. package/dist/core/address.js +47 -9
  5. package/dist/core/array-formula-ranges.d.ts +33 -0
  6. package/dist/core/array-formula-ranges.js +62 -0
  7. package/dist/core/color-resolution.js +2 -2
  8. package/dist/core/column.js +3 -2
  9. package/dist/core/comment-thread.d.ts +7 -3
  10. package/dist/core/conditional-formatting-overlay.d.ts +9 -0
  11. package/dist/core/conditional-formatting-overlay.js +28 -1
  12. package/dist/core/conditional-formatting.d.ts +98 -10
  13. package/dist/core/conditional-formatting.js +42 -1
  14. package/dist/core/data-validation-overlay.d.ts +5 -0
  15. package/dist/core/data-validation-overlay.js +15 -0
  16. package/dist/core/data-validation.d.ts +16 -0
  17. package/dist/core/data-validation.js +15 -0
  18. package/dist/core/date-format.js +14 -4
  19. package/dist/core/date.js +1 -1
  20. package/dist/core/formula-references.d.ts +26 -0
  21. package/dist/core/formula-references.js +173 -0
  22. package/dist/core/formula-scan.d.ts +2 -0
  23. package/dist/core/formula-scan.js +58 -0
  24. package/dist/core/formula.d.ts +51 -18
  25. package/dist/core/formula.js +77 -86
  26. package/dist/core/function-values.d.ts +4 -0
  27. package/dist/core/function-values.js +58 -0
  28. package/dist/core/future-functions.d.ts +3 -0
  29. package/dist/core/{modern-functions.js → future-functions.js} +119 -88
  30. package/dist/core/grid-edits.d.ts +28 -0
  31. package/dist/core/grid-edits.js +193 -25
  32. package/dist/core/hyperlink.d.ts +39 -0
  33. package/dist/core/hyperlink.js +88 -0
  34. package/dist/core/image.d.ts +36 -5
  35. package/dist/core/image.js +9 -0
  36. package/dist/core/internal.d.ts +3 -1
  37. package/dist/core/limits.d.ts +5 -3
  38. package/dist/core/merge.d.ts +13 -0
  39. package/dist/core/merge.js +15 -1
  40. package/dist/core/page-setup.d.ts +26 -11
  41. package/dist/core/pivot-table.d.ts +49 -10
  42. package/dist/core/pivot-table.js +55 -21
  43. package/dist/core/protection.d.ts +66 -4
  44. package/dist/core/protection.js +4 -0
  45. package/dist/core/row.js +3 -2
  46. package/dist/core/structured-reference.d.ts +7 -0
  47. package/dist/core/structured-reference.js +7 -0
  48. package/dist/core/table.d.ts +38 -8
  49. package/dist/core/table.js +37 -7
  50. package/dist/core/theme.d.ts +2 -0
  51. package/dist/core/theme.js +4 -1
  52. package/dist/core/value.d.ts +51 -28
  53. package/dist/core/value.js +15 -12
  54. package/dist/core/workbook-styles.d.ts +17 -1
  55. package/dist/core/workbook-styles.js +4 -0
  56. package/dist/core/workbook-theme.js +5 -3
  57. package/dist/core/workbook.d.ts +20 -3
  58. package/dist/core/workbook.js +26 -5
  59. package/dist/core/worksheet-merges.d.ts +9 -4
  60. package/dist/core/worksheet-merges.js +17 -11
  61. package/dist/core/worksheet-model.d.ts +1 -1
  62. package/dist/core/worksheet-model.js +4 -0
  63. package/dist/core/worksheet-pictures.d.ts +3 -3
  64. package/dist/core/worksheet-pictures.js +7 -6
  65. package/dist/core/worksheet.d.ts +110 -21
  66. package/dist/core/worksheet.js +174 -49
  67. package/dist/entries/core.d.ts +7 -5
  68. package/dist/entries/core.js +4 -3
  69. package/dist/entries/errors.d.ts +1 -0
  70. package/dist/entries/errors.js +1 -0
  71. package/dist/errors.js +1 -1
  72. package/dist/io/cell-metadata/metadata.d.ts +42 -0
  73. package/dist/io/cell-metadata/metadata.js +35 -0
  74. package/dist/io/cell-metadata/rich-values.d.ts +11 -0
  75. package/dist/io/cell-metadata/rich-values.js +80 -0
  76. package/dist/io/csv/delimiter.d.ts +14 -2
  77. package/dist/io/csv/delimiter.js +12 -0
  78. package/dist/io/csv/errors.d.ts +14 -0
  79. package/dist/io/csv/errors.js +5 -0
  80. package/dist/io/csv/read.d.ts +6 -1
  81. package/dist/io/csv/read.js +46 -24
  82. package/dist/io/csv/write.d.ts +10 -3
  83. package/dist/io/csv/write.js +16 -7
  84. package/dist/io/opc/part-paths.js +4 -4
  85. package/dist/io/opc/read-opc.d.ts +4 -0
  86. package/dist/io/opc/read-opc.js +10 -4
  87. package/dist/io/opc/strict-relationships.d.ts +2 -0
  88. package/dist/io/opc/strict-relationships.js +12 -0
  89. package/dist/io/read-policy/column-budget.d.ts +26 -0
  90. package/dist/io/{xlsx → read-policy}/column-budget.js +4 -8
  91. package/dist/io/read-policy/read-repair.d.ts +74 -0
  92. package/dist/io/read-policy/read-repair.js +119 -0
  93. package/dist/io/style/cell-style-resolution.d.ts +25 -0
  94. package/dist/io/style/cell-style-resolution.js +30 -0
  95. package/dist/io/xlsb/formula.js +4 -2
  96. package/dist/io/xlsb/ptg-functions.js +110 -0
  97. package/dist/io/xlsb/read-metadata.d.ts +7 -0
  98. package/dist/io/xlsb/read-metadata.js +69 -0
  99. package/dist/io/xlsb/read-worksheet.d.ts +6 -0
  100. package/dist/io/xlsb/read-worksheet.js +95 -37
  101. package/dist/io/xlsb/read.js +26 -8
  102. package/dist/io/xlsb/record-types.d.ts +13 -0
  103. package/dist/io/xlsb/record-types.js +13 -0
  104. package/dist/io/xlsb/sheet-protection.d.ts +20 -0
  105. package/dist/io/xlsb/sheet-protection.js +60 -0
  106. package/dist/io/xlsx/cell-accumulator.d.ts +20 -12
  107. package/dist/io/xlsx/cell-accumulator.js +49 -19
  108. package/dist/io/xlsx/cell-metadata.d.ts +39 -0
  109. package/dist/io/xlsx/cell-metadata.js +162 -0
  110. package/dist/io/xlsx/cell-value.d.ts +16 -4
  111. package/dist/io/xlsx/cell-value.js +38 -21
  112. package/dist/io/xlsx/color-xml.js +1 -1
  113. package/dist/io/xlsx/column-span.d.ts +11 -0
  114. package/dist/io/xlsx/column-span.js +9 -0
  115. package/dist/io/xlsx/comments.d.ts +3 -28
  116. package/dist/io/xlsx/comments.js +3 -65
  117. package/dist/io/xlsx/conditional-formatting.d.ts +42 -25
  118. package/dist/io/xlsx/conditional-formatting.js +424 -129
  119. package/dist/io/xlsx/data-validation.d.ts +12 -7
  120. package/dist/io/xlsx/data-validation.js +41 -29
  121. package/dist/io/xlsx/edit-vba.js +43 -36
  122. package/dist/io/xlsx/hyperlinks.d.ts +16 -50
  123. package/dist/io/xlsx/hyperlinks.js +8 -37
  124. package/dist/io/xlsx/images.d.ts +49 -16
  125. package/dist/io/xlsx/images.js +145 -44
  126. package/dist/io/xlsx/namespaces.d.ts +8 -0
  127. package/dist/io/xlsx/namespaces.js +4 -0
  128. package/dist/io/xlsx/package-plan.d.ts +48 -15
  129. package/dist/io/xlsx/package-plan.js +46 -9
  130. package/dist/io/xlsx/part-names.d.ts +6 -8
  131. package/dist/io/xlsx/part-names.js +3 -7
  132. package/dist/io/xlsx/pivot.d.ts +5 -5
  133. package/dist/io/xlsx/pivot.js +23 -22
  134. package/dist/io/xlsx/preserved-splices.d.ts +6 -0
  135. package/dist/io/xlsx/preserved-splices.js +74 -0
  136. package/dist/io/xlsx/read-comments.d.ts +29 -0
  137. package/dist/io/xlsx/read-comments.js +75 -0
  138. package/dist/io/xlsx/read-parts.d.ts +2 -2
  139. package/dist/io/xlsx/read-parts.js +44 -37
  140. package/dist/io/xlsx/read-pivot.js +9 -3
  141. package/dist/io/xlsx/read-rich-runs.d.ts +8 -5
  142. package/dist/io/xlsx/read-rich-runs.js +18 -1
  143. package/dist/io/xlsx/read-rows.d.ts +18 -7
  144. package/dist/io/xlsx/read-rows.js +47 -37
  145. package/dist/io/xlsx/read-styles.d.ts +0 -25
  146. package/dist/io/xlsx/read-styles.js +39 -34
  147. package/dist/io/xlsx/read-workbook-xml.d.ts +6 -0
  148. package/dist/io/xlsx/read-workbook-xml.js +9 -4
  149. package/dist/io/xlsx/read-worksheet.d.ts +8 -3
  150. package/dist/io/xlsx/read-worksheet.js +45 -23
  151. package/dist/io/xlsx/read.d.ts +0 -4
  152. package/dist/io/xlsx/read.js +17 -21
  153. package/dist/io/xlsx/relationships.d.ts +3 -0
  154. package/dist/io/xlsx/relationships.js +4 -0
  155. package/dist/io/xlsx/row-position.d.ts +3 -1
  156. package/dist/io/xlsx/row-xml.d.ts +18 -7
  157. package/dist/io/xlsx/row-xml.js +54 -35
  158. package/dist/io/xlsx/sheet-properties.js +68 -42
  159. package/dist/io/xlsx/strict-parts.d.ts +7 -0
  160. package/dist/io/xlsx/strict-parts.js +171 -0
  161. package/dist/io/xlsx/style-elements.d.ts +0 -2
  162. package/dist/io/xlsx/style-elements.js +9 -29
  163. package/dist/io/xlsx/styles.d.ts +5 -0
  164. package/dist/io/xlsx/styles.js +11 -5
  165. package/dist/io/xlsx/tables.d.ts +10 -2
  166. package/dist/io/xlsx/tables.js +49 -42
  167. package/dist/io/xlsx/theme-xml.d.ts +6 -0
  168. package/dist/io/xlsx/theme-xml.js +16 -19
  169. package/dist/io/xlsx/threaded-comments.d.ts +6 -1
  170. package/dist/io/xlsx/threaded-comments.js +14 -7
  171. package/dist/io/xlsx/workbook-xml.d.ts +13 -24
  172. package/dist/io/xlsx/workbook-xml.js +15 -33
  173. package/dist/io/xlsx/worksheet-xml.d.ts +5 -2
  174. package/dist/io/xlsx/worksheet-xml.js +37 -69
  175. package/dist/io/xlsx/write-stream.d.ts +28 -6
  176. package/dist/io/xlsx/write-stream.js +42 -17
  177. package/dist/io/xlsx/write.d.ts +11 -4
  178. package/dist/io/xlsx/write.js +153 -90
  179. package/dist/rel-type.d.ts +9 -0
  180. package/dist/rel-type.js +3 -0
  181. package/dist/vba/bytes.d.ts +2 -0
  182. package/dist/vba/bytes.js +11 -0
  183. package/dist/vba/cfb-format.d.ts +47 -0
  184. package/dist/vba/cfb-format.js +34 -0
  185. package/dist/vba/cfb-writer.d.ts +1 -2
  186. package/dist/vba/cfb-writer.js +33 -36
  187. package/dist/vba/cfb.d.ts +9 -4
  188. package/dist/vba/cfb.js +55 -43
  189. package/dist/vba/dir-records.d.ts +6 -0
  190. package/dist/vba/dir-records.js +10 -2
  191. package/dist/vba/errors.d.ts +4 -4
  192. package/dist/vba/index.d.ts +0 -2
  193. package/dist/vba/index.js +0 -2
  194. package/dist/vba/ms-ovba.d.ts +6 -2
  195. package/dist/vba/ms-ovba.js +15 -6
  196. package/dist/vba/project-editor.js +102 -87
  197. package/dist/vba/project.d.ts +4 -0
  198. package/dist/vba/project.js +12 -6
  199. package/dist/vba/vba-encoding.d.ts +4 -10
  200. package/dist/vba/vba-encoding.js +2 -15
  201. package/dist/xml/xml-read.d.ts +29 -1
  202. package/dist/xml/xml-read.js +56 -0
  203. package/dist/xml/xml-scan.d.ts +13 -0
  204. package/dist/xml/xml-scan.js +58 -7
  205. package/dist/xml/xml.d.ts +33 -1
  206. package/dist/xml/xml.js +22 -0
  207. package/package.json +5 -4
  208. package/skills/ts-xlsx-upstream/SKILL.md +21 -13
  209. package/dist/core/modern-functions.d.ts +0 -1
  210. package/dist/io/xlsx/cell-style-resolution.d.ts +0 -25
  211. package/dist/io/xlsx/cell-style-resolution.js +0 -29
  212. package/dist/io/xlsx/column-budget.d.ts +0 -26
  213. package/dist/io/xlsx/read-repair.d.ts +0 -26
  214. package/dist/io/xlsx/read-repair.js +0 -39
@@ -1,17 +1,16 @@
1
- import { quoted } from '../errors.js';
2
- import { readU16, spliceBytes, writeU16 } from './bytes.js';
1
+ import { utf16leBytes } from '../bytes.js';
2
+ import { InternalError, quoted } from '../errors.js';
3
+ import { hex } from '../hex.js';
4
+ import { concat, readU16, spliceBytes, writeU16, writeU32 } from './bytes.js';
5
+ import { sameEntryName } from './cfb-format.js';
3
6
  import { isStream, writeCompoundFile } from './cfb-writer.js';
4
7
  import { CompoundFile } from './cfb.js';
5
8
  import { decoderForCodePage, encoderForCodePage } from './codepage.js';
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';
9
+ import { dirRecord, 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';
7
10
  import { VbaAuthorError, VbaParseError } from './errors.js';
8
11
  import { compressContainer, decompressContainer } from './ms-ovba.js';
9
- import { parseVbaProjectIn } from './project.js';
10
- import { push, u16, u32, utf16le, validateVbaName } from './vba-encoding.js';
11
- const DIR_STREAM = 'dir';
12
- const PROJECT_STREAM = 'PROJECT';
13
- const PROJECTWM_STREAM = 'PROJECTwm';
14
- const VBA_STORAGE = 'VBA';
12
+ import { DIR_PATH, parseVbaProjectIn, PROJECT_PATH, PROJECTWM_PATH, VBA_STORAGE } from './project.js';
13
+ import { validateReferenceName } from './vba-encoding.js';
15
14
  export function removeVbaModule(bin, name) {
16
15
  const cfb = new CompoundFile(bin);
17
16
  const project = parseVbaProjectIn(cfb);
@@ -24,37 +23,38 @@ export function removeVbaModule(bin, name) {
24
23
  'primitive cannot verify');
25
24
  }
26
25
  const decoder = decoderForCodePage(project.codePage);
27
- const dirCompressed = cfb.readStream(DIR_STREAM);
26
+ const dirCompressed = cfb.readStream(DIR_PATH);
28
27
  if (!dirCompressed)
29
28
  throw new VbaParseError("VBA project has no 'dir' stream");
30
29
  const patchedDir = removeModuleDirRecord(decompressContainer(dirCompressed), module.streamName, decoder);
31
- const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
32
- const encode = encoderForCodePage(project.codePage);
33
- const projectText = cfb.readStream(PROJECT_STREAM);
30
+ const replacements = [{ path: DIR_PATH, data: compressContainer(patchedDir) }];
31
+ const projectText = cfb.readStream(PROJECT_PATH);
34
32
  if (projectText) {
35
- replacements.set(PROJECT_STREAM, encode(removeProjectStreamLines(decoder.decode(projectText), module.name, module.kind)));
33
+ replacements.push({
34
+ path: PROJECT_PATH,
35
+ data: removeProjectStreamLines(projectText, module.name, module.kind, decoder),
36
+ });
36
37
  }
37
- const projectwm = cfb.readStream(PROJECTWM_STREAM);
38
+ const projectwm = cfb.readStream(PROJECTWM_PATH);
38
39
  if (projectwm) {
39
- replacements.set(PROJECTWM_STREAM, removeProjectwmRecord(projectwm, project.modules.length, module.name, decoder));
40
- }
41
- const applied = new Set();
42
- const withReplacements = replaceStreams(cfb.tree(), replacements, applied);
43
- if (!applied.has(DIR_STREAM))
44
- throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
45
- const removed = new Set();
46
- const newTree = removeFromStorage(withReplacements, VBA_STORAGE, module.streamName, removed);
47
- if (!removed.has(VBA_STORAGE)) {
48
- throw new VbaParseError(`module stream ${quoted(module.streamName)} is not in the ${quoted(VBA_STORAGE)} storage`);
40
+ replacements.push({
41
+ path: PROJECTWM_PATH,
42
+ data: removeProjectwmRecord(projectwm, project.modules.length, module.name, decoder),
43
+ });
49
44
  }
45
+ const newTree = withoutStream(replaceStreams(cfb.tree(), replacements), [
46
+ VBA_STORAGE,
47
+ module.streamName,
48
+ ]);
50
49
  return writeCompoundFile(newTree);
51
50
  }
52
51
  const MAX_LIBID_VERSION = 0xffff;
53
52
  const MAX_LIBID_LCID = 0xffffffff;
54
53
  const MAX_DISPLAY_NAME_CHARS = 255;
54
+ const MAX_LIBID_PATH_CHARS = 32_767;
55
55
  const GUID_PATTERN = /^\{?([0-9A-Fa-f]{8})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{12})\}?$/;
56
56
  function normalizeReference(ref) {
57
- validateVbaName(ref.name, 'reference');
57
+ validateReferenceName(ref.name);
58
58
  const guidMatch = GUID_PATTERN.exec(ref.guid.trim());
59
59
  if (!guidMatch)
60
60
  throw new VbaAuthorError(`invalid reference GUID ${quoted(ref.guid)}`);
@@ -71,6 +71,9 @@ function normalizeReference(ref) {
71
71
  if (!Number.isInteger(lcid) || lcid < 0 || lcid > MAX_LIBID_LCID) {
72
72
  throw new VbaAuthorError(`reference lcid must be an integer in [0, 0xFFFFFFFF], got ${lcid}`);
73
73
  }
74
+ if (ref.path.length > MAX_LIBID_PATH_CHARS) {
75
+ throw new VbaAuthorError(`reference path is ${ref.path.length} characters long, past the ${MAX_LIBID_PATH_CHARS} a Windows path can hold`);
76
+ }
74
77
  if (ref.path.length === 0 || ref.path.includes('\0') || ref.path.includes('#')) {
75
78
  throw new VbaAuthorError(`invalid reference path ${quoted(ref.path)} (must be non-empty and contain no NUL or '#')`);
76
79
  }
@@ -80,8 +83,7 @@ function normalizeReference(ref) {
80
83
  displayName.includes('\0')) {
81
84
  throw new VbaAuthorError(`invalid reference display name ${quoted(displayName)}`);
82
85
  }
83
- const libid = `*\\G${guid}#${ref.majorVersion.toString(16).toUpperCase()}.` +
84
- `${ref.minorVersion.toString(16).toUpperCase()}#${lcid.toString(16).toUpperCase()}` +
86
+ const libid = `*\\G${guid}#${hex(ref.majorVersion, 1)}.${hex(ref.minorVersion, 1)}#${hex(lcid, 1)}` +
85
87
  `#${ref.path}#${displayName}`;
86
88
  return { name: ref.name, libid };
87
89
  }
@@ -89,31 +91,23 @@ export function addVbaReference(bin, ref) {
89
91
  const normalized = normalizeReference(ref);
90
92
  const cfb = new CompoundFile(bin);
91
93
  const encode = encoderForCodePage(parseVbaProjectIn(cfb).codePage);
92
- const dirCompressed = cfb.readStream(DIR_STREAM);
94
+ const dirCompressed = cfb.readStream(DIR_PATH);
93
95
  if (!dirCompressed)
94
96
  throw new VbaParseError("VBA project has no 'dir' stream");
95
97
  const records = buildReferenceDirRecords(normalized, encode);
96
98
  const patchedDir = insertReferenceDirRecords(decompressContainer(dirCompressed), records);
97
- const replacements = new Map([[DIR_STREAM, compressContainer(patchedDir)]]);
98
- const applied = new Set();
99
- const newTree = replaceStreams(cfb.tree(), replacements, applied);
100
- if (!applied.has(DIR_STREAM))
101
- throw new VbaParseError("VBA project 'dir' stream is not in the container tree");
102
- return writeCompoundFile(newTree);
99
+ return writeCompoundFile(replaceStreams(cfb.tree(), [{ path: DIR_PATH, data: compressContainer(patchedDir) }]));
103
100
  }
104
101
  function buildReferenceDirRecords(ref, encode) {
105
- const r = [];
106
- const nameBytes = [...encode(ref.name)];
107
- push(r, REC_REFERENCE_NAME, nameBytes);
108
- push(r, REC_REFERENCE_NAME_UNICODE, utf16le(ref.name));
109
- const libidBytes = [...encode(ref.libid)];
110
- push(r, REC_REFERENCE_REGISTERED, [
111
- ...u32(libidBytes.length),
112
- ...libidBytes,
113
- ...u32(0),
114
- ...u16(0),
102
+ const libid = encode(ref.libid);
103
+ const sizeOfLibid = new Uint8Array(4);
104
+ writeU32(sizeOfLibid, 0, libid.length);
105
+ const reserved = new Uint8Array(6);
106
+ return concat([
107
+ dirRecord(REC_REFERENCE_NAME, encode(ref.name)),
108
+ dirRecord(REC_REFERENCE_NAME_UNICODE, utf16leBytes(ref.name)),
109
+ dirRecord(REC_REFERENCE_REGISTERED, concat([sizeOfLibid, libid, reserved])),
115
110
  ]);
116
- return r;
117
111
  }
118
112
  function insertReferenceDirRecords(dir, records) {
119
113
  let insertAt = -1;
@@ -125,7 +119,7 @@ function insertReferenceDirRecords(dir, records) {
125
119
  }
126
120
  if (insertAt < 0)
127
121
  throw new VbaParseError('dir stream is missing MODULES_COUNT');
128
- return spliceBytes(dir, insertAt, insertAt, Uint8Array.from(records));
122
+ return spliceBytes(dir, insertAt, insertAt, records);
129
123
  }
130
124
  function removeModuleDirRecord(dir, streamName, decoder) {
131
125
  let countAt = -1;
@@ -171,26 +165,36 @@ function removeModuleDirRecord(dir, streamName, decoder) {
171
165
  writeU16(out, countAt, count - 1);
172
166
  return out;
173
167
  }
174
- function removeProjectStreamLines(text, name, kind) {
175
- const eol = text.includes('\r\n') ? '\r\n' : '\n';
176
- const lines = text.split(/\r\n|\r|\n/);
168
+ function removeProjectStreamLines(stream, name, kind, decoder) {
169
+ const lines = projectLines(stream, decoder);
177
170
  const declLine = `${kind === 'procedural' ? 'Module' : 'Class'}=${name}`;
178
- const declIndex = lines.indexOf(declLine);
179
- if (declIndex >= 0)
180
- lines.splice(declIndex, 1);
181
- const wsIndex = lines.findIndex((l) => l.trim() === '[Workspace]');
182
- if (wsIndex >= 0) {
183
- for (let i = wsIndex + 1; i < lines.length; i++) {
184
- const l = lines[i];
185
- if (l.trim() === '' || l.startsWith('['))
186
- break;
187
- if (l.startsWith(`${name}=`)) {
188
- lines.splice(i, 1);
189
- break;
190
- }
171
+ const removals = [];
172
+ const decl = lines.find((line) => line.text === declLine);
173
+ if (decl !== undefined)
174
+ removals.push(decl);
175
+ const wsIndex = lines.findIndex((line) => line.text.trim() === '[Workspace]');
176
+ for (const line of wsIndex < 0 ? [] : lines.slice(wsIndex + 1)) {
177
+ if (line.text.trim() === '' || line.text.startsWith('['))
178
+ break;
179
+ if (line.text.startsWith(`${name}=`)) {
180
+ removals.push(line);
181
+ break;
191
182
  }
192
183
  }
193
- return lines.join(eol);
184
+ return removals
185
+ .sort((a, b) => b.start - a.start)
186
+ .reduce((bytes, line) => spliceBytes(bytes, line.start, line.end), stream);
187
+ }
188
+ function projectLines(stream, decoder) {
189
+ const lines = [];
190
+ for (let start = 0; start < stream.length;) {
191
+ const lf = stream.indexOf(0x0a, start);
192
+ const end = lf < 0 ? stream.length : lf + 1;
193
+ const contentEnd = lf > start && stream[lf - 1] === 0x0d ? lf - 1 : lf < 0 ? stream.length : lf;
194
+ lines.push({ text: decoder.decode(stream.subarray(start, contentEnd)), start, end });
195
+ start = end;
196
+ }
197
+ return lines;
194
198
  }
195
199
  function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
196
200
  let pos = 0;
@@ -220,30 +224,41 @@ function removeProjectwmRecord(wm, existingModuleCount, name, decoder) {
220
224
  }
221
225
  return spliceBytes(wm, removeStart, removeEnd);
222
226
  }
223
- function removeFromStorage(nodes, storageName, streamName, removed) {
224
- return nodes.map((n) => {
225
- if (isStream(n))
226
- return n;
227
- const children = removeFromStorage(n.children, storageName, streamName, removed);
228
- if (n.name === storageName && !removed.has(storageName)) {
229
- const filtered = children.filter((c) => !(isStream(c) && c.name === streamName));
230
- if (filtered.length !== children.length)
231
- removed.add(storageName);
232
- return { name: n.name, children: filtered };
233
- }
234
- return { name: n.name, children };
235
- });
236
- }
237
- function replaceStreams(nodes, replacements, applied) {
238
- return nodes.map((node) => {
239
- if (isStream(node)) {
240
- const data = replacements.get(node.name);
241
- if (data !== undefined) {
242
- applied.add(node.name);
243
- return { name: node.name, data };
244
- }
227
+ function replaceStreams(nodes, replacements) {
228
+ let applied = 0;
229
+ const rebuild = (level, pending, depth) => level.map((node) => {
230
+ const here = pending.filter((replacement) => {
231
+ const segment = replacement.path[depth];
232
+ return segment !== undefined && sameEntryName(segment, node.name);
233
+ });
234
+ if (here.length === 0)
245
235
  return node;
236
+ if (isStream(node)) {
237
+ const replacement = here.find((candidate) => candidate.path.length === depth + 1);
238
+ if (replacement === undefined)
239
+ return node;
240
+ applied++;
241
+ return { name: node.name, data: replacement.data };
246
242
  }
247
- return { name: node.name, children: replaceStreams(node.children, replacements, applied) };
243
+ return { name: node.name, children: rebuild(node.children, here, depth + 1) };
248
244
  });
245
+ const rebuilt = rebuild(nodes, replacements, 0);
246
+ if (applied !== replacements.length) {
247
+ throw new InternalError('a VBA stream read by its path is missing from the container tree');
248
+ }
249
+ return rebuilt;
250
+ }
251
+ function withoutStream(nodes, path) {
252
+ const [name, ...rest] = path;
253
+ const index = nodes.findIndex((node) => name !== undefined &&
254
+ sameEntryName(node.name, name) &&
255
+ isStream(node) === (rest.length === 0));
256
+ const target = nodes[index];
257
+ if (target === undefined) {
258
+ throw new InternalError('a VBA module stream read by its path is missing from the container tree');
259
+ }
260
+ const kept = isStream(target)
261
+ ? []
262
+ : [{ name: target.name, children: withoutStream(target.children, rest) }];
263
+ return [...nodes.slice(0, index), ...kept, ...nodes.slice(index + 1)];
249
264
  }
@@ -43,6 +43,10 @@ export interface VbaProjectSignature {
43
43
  * carries (`.../office/2006/...` vs `.../2014/...` vs `.../2020/...`).
44
44
  */
45
45
  export declare function vbaProjectSignatureKind(relType: string): VbaProjectSignatureKind | undefined;
46
+ export declare const VBA_STORAGE = "VBA";
47
+ export declare const DIR_PATH: readonly string[];
48
+ export declare const PROJECT_PATH: readonly string[];
49
+ export declare const PROJECTWM_PATH: readonly string[];
46
50
  export declare function parseVbaProject(bin: Uint8Array, maxOutput?: number): VbaProject;
47
51
  /**
48
52
  * As {@link parseVbaProject}, over a container the caller has already opened.
@@ -1,4 +1,5 @@
1
1
  import { quoted } from '../errors.js';
2
+ import { relTypeSegment } from '../rel-type.js';
2
3
  import { readU16, readU32 } from './bytes.js';
3
4
  import { CompoundFile } from './cfb.js';
4
5
  import { decoderForCodePage } from './codepage.js';
@@ -14,17 +15,21 @@ const SIGNATURE_KIND_BY_REL_SEGMENT = new Map([
14
15
  ['vbaProjectSignatureV3', 'v3'],
15
16
  ]);
16
17
  export function vbaProjectSignatureKind(relType) {
17
- return SIGNATURE_KIND_BY_REL_SEGMENT.get(relType.slice(relType.lastIndexOf('/') + 1));
18
+ return SIGNATURE_KIND_BY_REL_SEGMENT.get(relTypeSegment(relType));
18
19
  }
19
20
  function isComplete(module) {
20
21
  return module.streamName !== undefined && module.offset !== undefined;
21
22
  }
23
+ export const VBA_STORAGE = 'VBA';
24
+ export const DIR_PATH = [VBA_STORAGE, 'dir'];
25
+ export const PROJECT_PATH = ['PROJECT'];
26
+ export const PROJECTWM_PATH = ['PROJECTwm'];
22
27
  export function parseVbaProject(bin, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
23
28
  return parseVbaProjectIn(new CompoundFile(bin), maxOutput);
24
29
  }
25
30
  export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
26
31
  const budget = new DecompressionBudget(maxOutput);
27
- const dirCompressed = cfb.readStream('dir');
32
+ const dirCompressed = cfb.readStream(DIR_PATH);
28
33
  if (!dirCompressed)
29
34
  throw new VbaParseError("VBA project has no 'dir' stream");
30
35
  const dir = budget.spend(dirCompressed, 0);
@@ -78,9 +83,10 @@ export function parseVbaProjectIn(cfb, maxOutput = DEFAULT_MAX_PROJECT_OUTPUT) {
78
83
  return { codePage, modules };
79
84
  }
80
85
  function readModuleSource(cfb, streamName, textOffset, decoder, budget) {
81
- const stream = cfb.readStream(streamName);
82
- if (!stream)
83
- throw new VbaParseError(`module stream ${quoted(streamName)} not found in container`);
86
+ const stream = cfb.readStream([VBA_STORAGE, streamName]);
87
+ if (!stream) {
88
+ throw new VbaParseError(`module stream ${quoted(streamName)} not found in the ${quoted(VBA_STORAGE)} storage`);
89
+ }
84
90
  return decoder.decode(budget.spend(stream, textOffset));
85
91
  }
86
92
  const DEFAULT_MAX_PROJECT_OUTPUT = 64 * 1024 * 1024;
@@ -103,7 +109,7 @@ const MODULE_KIND_BY_PROJECT_KEYWORD = new Map([
103
109
  ]);
104
110
  function readProjectStreamKinds(cfb, decoder) {
105
111
  const kinds = new Map();
106
- const stream = cfb.readStream('PROJECT');
112
+ const stream = cfb.readStream(PROJECT_PATH);
107
113
  if (!stream)
108
114
  return kinds;
109
115
  const text = decoder.decode(stream);
@@ -1,14 +1,8 @@
1
1
  /**
2
- * Validate a module, project, or reference name against the shared VBA identifier contract: a valid
3
- * identifier, at most 31 characters (the CFB stream-name limit, which doubles as VBA's own module-name
4
- * limit). Used by {@link project-editor.ts | project-editor}'s structural edits.
2
+ * Validate a library reference's name against the VBA identifier contract: a valid identifier, at
3
+ * most 31 characters (the CFB stream-name limit, which VBA also applies to its names). The reference
4
+ * is the one new name {@link project-editor.ts | project-editor}'s structural edits write.
5
5
  *
6
6
  * @throws {VbaAuthorError} if `name` is not a valid VBA identifier or exceeds 31 characters.
7
7
  */
8
- export declare function validateVbaName(name: string, what: 'project' | 'module' | 'reference'): void;
9
- /** Append one `dir`-stream TLV record (Id, Size, data) to `out`. */
10
- export declare function push(out: number[], id: number, data: number[]): void;
11
- export declare function u16(n: number): number[];
12
- export declare function u32(n: number): number[];
13
- /** A name as NUL-free UTF-16LE code units: the encoding [MS-OVBA] uses for every "Unicode" name field. */
14
- export declare function utf16le(s: string): number[];
8
+ export declare function validateReferenceName(name: string): void;
@@ -1,22 +1,9 @@
1
- import { utf16leBytes } from '../bytes.js';
2
1
  import { quoted } from '../errors.js';
3
2
  import { MAX_NAME_CHARS } from './cfb-format.js';
4
3
  import { VbaAuthorError } from './errors.js';
5
4
  const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
6
- export function validateVbaName(name, what) {
5
+ export function validateReferenceName(name) {
7
6
  if (!IDENTIFIER.test(name) || name.length > MAX_NAME_CHARS) {
8
- throw new VbaAuthorError(`invalid ${what} name ${quoted(name)} (must be a VBA identifier ≤ 31 chars)`);
7
+ throw new VbaAuthorError(`invalid reference name ${quoted(name)} (must be a VBA identifier ≤ 31 chars)`);
9
8
  }
10
9
  }
11
- export function push(out, id, data) {
12
- out.push(...u16(id), ...u32(data.length), ...data);
13
- }
14
- export function u16(n) {
15
- return [n & 0xff, (n >> 8) & 0xff];
16
- }
17
- export function u32(n) {
18
- return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff];
19
- }
20
- export function utf16le(s) {
21
- return [...utf16leBytes(s)];
22
- }
@@ -94,6 +94,34 @@ export interface ElementRange {
94
94
  * range is not a range.
95
95
  */
96
96
  export declare function elementRange(source: string, path: readonly string[]): ElementRange | undefined;
97
+ /** A tag as {@link tagRanges} yields it. */
98
+ export interface TagRange {
99
+ /** Offset of the tag's `<`. */
100
+ readonly start: number;
101
+ /** One past the tag's `>`. */
102
+ readonly end: number;
103
+ /** The name as written, namespace prefix included. */
104
+ readonly name: string;
105
+ /** The parsed attributes of an opening tag; empty for a closing one. */
106
+ readonly attrs: XmlAttributes;
107
+ readonly close: boolean;
108
+ readonly selfClosing: boolean;
109
+ }
110
+ /**
111
+ * Every tag in `source`, opening and closing, as offsets into the source: for an edit that rewrites a
112
+ * tag wherever it sits rather than one element a path names, such as translating the namespaces a
113
+ * part declares. A caller that resolves names keeps its own `NamespaceScope` in step, opening it on
114
+ * each opening tag and closing it on each closing one and after each self-closing one.
115
+ */
116
+ export declare function tagRanges(source: string): Generator<TagRange>;
117
+ /**
118
+ * Locate every element with the local name `local`, at any depth, as offsets into the source, in
119
+ * document order: {@link elementRange} for a part that repeats the element an edit is after, such as
120
+ * the `<c:f>` behind each of a chart's series. A same-named element nested in a match is part of that
121
+ * match's content rather than a range of its own. An element that never closes throws
122
+ * {@link XmlParseError}.
123
+ */
124
+ export declare function elementRanges(source: string, local: string): Generator<ElementRange>;
97
125
  /** An element start surfaced by {@link openElements}: its qualified `name`, the namespace-stripped
98
126
  * `local` name the filter matched on, and its already-decoded `attrs`. */
99
127
  export interface OpenElement {
@@ -188,7 +216,7 @@ export declare function parseXml(source: string, handlers: SaxHandlers, options?
188
216
  * The other thing the open-coded versions disagreed on is what an unrelated element opening
189
217
  * mid-capture should do. Ending the capture is never what a caller wants: the text belongs to the
190
218
  * element that opened it, and a nested or sibling element is not that element. So an open that is
191
- * not for a captured name leaves an capture in progress alone, and {@link close} answers only for
219
+ * not for a captured name leaves a capture in progress alone, and {@link close} answers only for
192
220
  * the element that started it.
193
221
  *
194
222
  * Decoding stays outside. A `<t>` needs `decodeSpreadsheetText` over the whole element and never
@@ -132,6 +132,62 @@ export function elementRange(source, path) {
132
132
  }
133
133
  return undefined;
134
134
  }
135
+ const NO_ATTRIBUTES = Object.freeze(Object.create(null));
136
+ export function* tagRanges(source) {
137
+ for (const { lt, tag } of rawTags(source)) {
138
+ yield {
139
+ start: lt,
140
+ end: tag.next,
141
+ name: tag.name,
142
+ attrs: tag.close ? NO_ATTRIBUTES : parseAttributes(tag.attrSource),
143
+ close: tag.close,
144
+ selfClosing: tag.selfClosing,
145
+ };
146
+ }
147
+ }
148
+ export function* elementRanges(source, local) {
149
+ let pending;
150
+ let depth = 0;
151
+ for (const { lt, tag, local: tagLocal } of rawTags(source)) {
152
+ if (tagLocal !== local)
153
+ continue;
154
+ if (pending !== undefined) {
155
+ if (tag.close) {
156
+ if (depth > 0)
157
+ depth -= 1;
158
+ else {
159
+ const open = pending;
160
+ pending = undefined;
161
+ yield { ...open, end: tag.next, contentEnd: lt };
162
+ }
163
+ }
164
+ else if (!tag.selfClosing) {
165
+ depth += 1;
166
+ }
167
+ continue;
168
+ }
169
+ if (tag.close)
170
+ continue;
171
+ const attrs = parseAttributes(tag.attrSource);
172
+ if (tag.selfClosing) {
173
+ yield {
174
+ start: lt,
175
+ end: tag.next,
176
+ contentStart: tag.next,
177
+ contentEnd: tag.next,
178
+ name: tag.name,
179
+ attrs,
180
+ };
181
+ }
182
+ else {
183
+ pending = { start: lt, contentStart: tag.next, name: tag.name, attrs };
184
+ depth = 0;
185
+ }
186
+ }
187
+ if (pending !== undefined) {
188
+ throw new XmlParseError(`unterminated <${pending.name}> element`);
189
+ }
190
+ }
135
191
  export function* openElements(source, ...localNames) {
136
192
  const filter = localNames.length > 0 ? new Set(localNames) : undefined;
137
193
  const scope = new NamespaceScope();
@@ -35,6 +35,19 @@ export type XmlEvent = {
35
35
  * codec keeps importing none of the serialisation vocabulary.
36
36
  */
37
37
  export declare function decodeEntities(value: string): string;
38
+ /**
39
+ * A tag's attributes: each a name, then `=`, then a value in either quote style. Matching the quotes
40
+ * is what lets a delimiter-respecting scan find a tag's end even when a value holds a `>` (legal but
41
+ * rare). Names may carry a namespace prefix (`r:id`, `xml:space`). Anything that is not an attribute,
42
+ * such as a name nothing assigns or a quote never closed, is skipped rather than refused.
43
+ *
44
+ * Linear in the source, and that is the point of it being hand-written. The regex it replaced
45
+ * backtracked over a run of name characters no `=` followed from every position inside the run, so a
46
+ * single junk token in a tag cost the square of its length: a megabyte of one letter, a kilobyte
47
+ * zipped, cost minutes inside either reader. Here a dropped token resumes the scan where the token
48
+ * ended, and a value's closing quote is found with one `indexOf`, so no character is visited more
49
+ * than a constant number of times.
50
+ */
38
51
  export declare function parseAttributes(source: string): XmlAttributes;
39
52
  export type Markup = {
40
53
  readonly kind: 'comment' | 'pi' | 'declaration';
@@ -26,18 +26,69 @@ export function decodeEntities(value) {
26
26
  function admitText(value) {
27
27
  return stripUnrepresentable(decodeEntities(value));
28
28
  }
29
- const ATTRIBUTE = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
29
+ const EQUALS = 0x3d;
30
+ const SLASH = 0x2f;
31
+ const GREATER_THAN = 0x3e;
32
+ const DOUBLE_QUOTE = 0x22;
33
+ const SINGLE_QUOTE = 0x27;
34
+ const WIDE_WHITESPACE = /\s/;
35
+ function isScanWhitespace(code) {
36
+ if (code < 0x80)
37
+ return code === 0x20 || (code >= 0x09 && code <= 0x0d);
38
+ return WIDE_WHITESPACE.test(String.fromCharCode(code));
39
+ }
40
+ function skipScanWhitespace(source, from) {
41
+ let i = from;
42
+ while (i < source.length && isScanWhitespace(source.charCodeAt(i)))
43
+ i++;
44
+ return i;
45
+ }
46
+ function endsName(code) {
47
+ if (code > GREATER_THAN && code < 0xa0)
48
+ return false;
49
+ return isScanWhitespace(code) || code === EQUALS || code === SLASH || code === GREATER_THAN;
50
+ }
30
51
  export function parseAttributes(source) {
31
52
  const attrs = Object.create(null);
32
- ATTRIBUTE.lastIndex = 0;
33
- let match = ATTRIBUTE.exec(source);
34
- while (match !== null) {
35
- const value = match[2] ?? match[3] ?? '';
36
- attrs[match[1]] = admitText(value);
37
- match = ATTRIBUTE.exec(source);
53
+ const length = source.length;
54
+ let i = 0;
55
+ while (i < length) {
56
+ const nameStart = i;
57
+ while (i < length && !endsName(source.charCodeAt(i)))
58
+ i++;
59
+ if (i === nameStart) {
60
+ i++;
61
+ continue;
62
+ }
63
+ let cursor = skipScanWhitespace(source, i);
64
+ if (source.charCodeAt(cursor) !== EQUALS) {
65
+ i = cursor;
66
+ continue;
67
+ }
68
+ cursor = skipScanWhitespace(source, cursor + 1);
69
+ const quote = source.charCodeAt(cursor);
70
+ const close = quote === DOUBLE_QUOTE || quote === SINGLE_QUOTE
71
+ ? source.indexOf(quote === DOUBLE_QUOTE ? '"' : "'", cursor + 1)
72
+ : -1;
73
+ if (close === -1) {
74
+ i = cursor;
75
+ continue;
76
+ }
77
+ attrs[source.slice(nameStart, i)] = admitText(normalizeAttributeWhitespace(source.slice(cursor + 1, close)));
78
+ i = close + 1;
38
79
  }
39
80
  return attrs;
40
81
  }
82
+ const ATTRIBUTE_WHITESPACE = /\r\n|[\t\n\r]/g;
83
+ function normalizeAttributeWhitespace(raw) {
84
+ for (let i = 0; i < raw.length; i++) {
85
+ const code = raw.charCodeAt(i);
86
+ if (code === 0x09 || code === 0x0a || code === 0x0d) {
87
+ return raw.replace(ATTRIBUTE_WHITESPACE, ' ');
88
+ }
89
+ }
90
+ return raw;
91
+ }
41
92
  function findTagEnd(source, start) {
42
93
  let quote = '';
43
94
  for (let i = start + 1; i < source.length; i++) {
package/dist/xml/xml.d.ts CHANGED
@@ -31,7 +31,7 @@ export declare function escapeFormatCode(code: string): string;
31
31
  * `escapeText` runs last and sees no unrepresentable character left, so its guard is a
32
32
  * standing proof that the escape was complete rather than a second check of the same thing.
33
33
  *
34
- * Reversed by `decodeSpreadsheetText` in `./xml-scan.ts`, whose single left-to-right pass is what
34
+ * Reversed by `decodeSpreadsheetText` in `./xml-attrs.ts`, whose single left-to-right pass is what
35
35
  * makes the `_x005F_` step above reversible. Change either and read its comment first.
36
36
  */
37
37
  export declare function escapeSpreadsheetText(value: string): string;
@@ -42,6 +42,15 @@ export declare function escapeSpreadsheetText(value: string): string;
42
42
  * identically on the way back.
43
43
  */
44
44
  export declare function textElement(value: string): string;
45
+ /**
46
+ * Re-render an opening tag, `<name …>` or the empty `<name …/>`, from its parsed name and attributes,
47
+ * for an edit that changes an attribute of an element a part already carries. Attribute order is the
48
+ * scanner's, which is the source's, so an element whose attributes an edit merges over keeps them
49
+ * where they were; only the quoting and the entity spelling become this writer's.
50
+ */
51
+ export declare function startTag(name: string, attrs: {
52
+ readonly [attribute: string]: string | undefined;
53
+ }, selfClosing: boolean): string;
45
54
  /**
46
55
  * A boolean attribute rendered with a leading space (` name="1"` / ` name="0"`), or '' when the value
47
56
  * is undefined. OOXML booleans serialise as 1/0; emitting the explicit `="0"` lets a writer force a
@@ -83,6 +92,29 @@ export declare function checkedToken(value: string, isValid: (candidate: string)
83
92
  * @throws {AuthoringError} when the value is not finite.
84
93
  */
85
94
  export declare function numAttr(name: string, value: number | undefined): string;
95
+ /**
96
+ * An integer attribute rendered with a leading space (` name="3"`), or '' when the value is undefined.
97
+ *
98
+ * For an `xsd:int` or `xsd:unsignedInt` attribute, whose lexical space has no spelling for a fraction
99
+ * any more than for a NaN: `priority="1.5"` and `rank="-1"` make a package the schema rejects, so the
100
+ * value is refused here rather than written. `min` is the floor the attribute's type implies, `0` for
101
+ * an unsigned one.
102
+ *
103
+ * @throws {AuthoringError} when the value is not finite, not a safe integer, or below `min`.
104
+ */
105
+ export declare function intAttr(name: string, value: number | undefined, min?: number): string;
106
+ /**
107
+ * A numeric attribute written only when it differs from its schema default, with a leading space, or
108
+ * '' when the value is undefined or equals `defaultValue`.
109
+ *
110
+ * The number is checked before it is compared, because the two answer different questions: whether
111
+ * the value can be spelled, and whether it is worth spelling. Tested for truthiness instead, `NaN` is
112
+ * falsy and was omitted as if it were a default of zero, so a gradient fill with `degree: NaN` was
113
+ * written as an ordinary gradient rather than refused.
114
+ *
115
+ * @throws {AuthoringError} when the value is not finite.
116
+ */
117
+ export declare function nonDefaultNumAttr(name: string, value: number | undefined, defaultValue: number): string;
86
118
  /**
87
119
  * Refuse a `Date` OOXML cannot spell, naming the property that carries it.
88
120
  *