@shbernal/ts-xlsx 2.1.0 → 3.0.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 (100) hide show
  1. package/dist/core/autofilter.d.ts +1 -1
  2. package/dist/core/autofilter.js +3 -5
  3. package/dist/core/axis-handle.d.ts +8 -0
  4. package/dist/core/axis-handle.js +14 -0
  5. package/dist/core/cell.d.ts +24 -0
  6. package/dist/core/cell.js +9 -1
  7. package/dist/core/column.d.ts +4 -1
  8. package/dist/core/column.js +31 -35
  9. package/dist/core/conditional-formatting.d.ts +5 -5
  10. package/dist/core/conditional-formatting.js +11 -25
  11. package/dist/core/data-validation.d.ts +3 -3
  12. package/dist/core/data-validation.js +7 -15
  13. package/dist/core/image.d.ts +1 -1
  14. package/dist/core/image.js +2 -4
  15. package/dist/core/merge-index.d.ts +31 -0
  16. package/dist/core/merge-index.js +60 -0
  17. package/dist/core/merge.d.ts +0 -10
  18. package/dist/core/merge.js +0 -8
  19. package/dist/core/page-setup.d.ts +72 -2
  20. package/dist/core/page-setup.js +34 -8
  21. package/dist/core/pivot-table.d.ts +1 -1
  22. package/dist/core/pivot-table.js +4 -8
  23. package/dist/core/row.d.ts +4 -1
  24. package/dist/core/row.js +19 -23
  25. package/dist/core/style.d.ts +51 -7
  26. package/dist/core/style.js +29 -34
  27. package/dist/core/table.d.ts +1 -1
  28. package/dist/core/table.js +3 -5
  29. package/dist/core/used-extent.d.ts +44 -0
  30. package/dist/core/used-extent.js +102 -0
  31. package/dist/core/worksheet.d.ts +17 -6
  32. package/dist/core/worksheet.js +48 -42
  33. package/dist/customui/ribbon.js +4 -6
  34. package/dist/io/opc/inflate.js +1 -1
  35. package/dist/io/opc/part-paths.d.ts +1 -0
  36. package/dist/io/opc/part-paths.js +15 -0
  37. package/dist/io/opc/read-opc.d.ts +22 -3
  38. package/dist/io/opc/read-opc.js +11 -39
  39. package/dist/io/opc/rels.d.ts +1 -6
  40. package/dist/io/opc/rels.js +1 -4
  41. package/dist/io/style/xf-style.d.ts +26 -2
  42. package/dist/io/style/xf-style.js +6 -0
  43. package/dist/io/xlsb/read-styles.js +6 -3
  44. package/dist/io/xlsb/read.d.ts +1 -1
  45. package/dist/io/xlsb/read.js +5 -8
  46. package/dist/io/xlsx/cell-accumulator.d.ts +1 -1
  47. package/dist/io/xlsx/cell-accumulator.js +1 -1
  48. package/dist/io/xlsx/cell-value.js +1 -1
  49. package/dist/io/xlsx/color-xml.js +1 -1
  50. package/dist/io/xlsx/comments.js +2 -1
  51. package/dist/io/xlsx/conditional-formatting.d.ts +3 -2
  52. package/dist/io/xlsx/conditional-formatting.js +22 -21
  53. package/dist/io/xlsx/data-validation.d.ts +9 -7
  54. package/dist/io/xlsx/data-validation.js +10 -9
  55. package/dist/io/xlsx/edit-vba.js +4 -11
  56. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  57. package/dist/io/xlsx/hyperlinks.js +20 -17
  58. package/dist/io/xlsx/images.js +15 -14
  59. package/dist/io/xlsx/package-plan.d.ts +9 -1
  60. package/dist/io/xlsx/package-plan.js +29 -16
  61. package/dist/io/xlsx/part-names.d.ts +41 -0
  62. package/dist/io/xlsx/part-names.js +46 -0
  63. package/dist/io/xlsx/read-pivot.js +2 -1
  64. package/dist/io/xlsx/read-rows.js +12 -13
  65. package/dist/io/xlsx/read-shared-strings.js +2 -1
  66. package/dist/io/xlsx/read-styles.d.ts +1 -1
  67. package/dist/io/xlsx/read-styles.js +63 -42
  68. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  69. package/dist/io/xlsx/read-worksheet.js +43 -56
  70. package/dist/io/xlsx/read.d.ts +2 -3
  71. package/dist/io/xlsx/read.js +81 -73
  72. package/dist/io/xlsx/rich-runs.d.ts +1 -1
  73. package/dist/io/xlsx/rich-runs.js +2 -1
  74. package/dist/io/xlsx/sheet-properties.js +21 -28
  75. package/dist/io/xlsx/styles.js +23 -20
  76. package/dist/io/xlsx/tables.js +2 -1
  77. package/dist/io/xlsx/theme-xml.js +1 -1
  78. package/dist/io/xlsx/threaded-comments.js +2 -1
  79. package/dist/io/xlsx/workbook-xml.js +29 -28
  80. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  81. package/dist/io/xlsx/worksheet-xml.js +14 -16
  82. package/dist/io/xlsx/write-stream.d.ts +6 -0
  83. package/dist/io/xlsx/write-stream.js +30 -22
  84. package/dist/io/xlsx/write.d.ts +5 -0
  85. package/dist/io/xlsx/write.js +155 -131
  86. package/dist/token-set.d.ts +15 -0
  87. package/dist/token-set.js +4 -0
  88. package/dist/vba/cfb-format.d.ts +24 -0
  89. package/dist/vba/cfb-format.js +12 -0
  90. package/dist/vba/cfb-writer.js +17 -17
  91. package/dist/vba/cfb.js +1 -7
  92. package/dist/vba/ms-ovba.js +32 -11
  93. package/dist/vba/project.js +13 -13
  94. package/dist/vba/vba-encoding.js +1 -1
  95. package/dist/xml/xml-read.d.ts +59 -74
  96. package/dist/xml/xml-read.js +83 -176
  97. package/dist/xml/xml-scan.d.ts +100 -0
  98. package/dist/xml/xml-scan.js +208 -0
  99. package/dist/xml/xml.d.ts +1 -1
  100. package/package.json +2 -3
@@ -187,23 +187,17 @@ export class WorkbookStreamWriter {
187
187
  return this.#stream;
188
188
  }
189
189
  addImage(options) {
190
- if (this.#committed) {
191
- throw new AuthoringError('the workbook is already committed: no more images can be registered');
192
- }
190
+ this.#assertOpen('no more images can be registered');
193
191
  return this.#workbook.addImage(options);
194
192
  }
195
193
  addWorksheet(name, options = {}) {
196
- if (this.#committed) {
197
- throw new AuthoringError('the workbook is already committed: no more worksheets can be added');
198
- }
194
+ this.#assertOpen('no more worksheets can be added');
199
195
  const sheet = new WorksheetStreamWriter(this.#workbook.addWorksheet(name, options), this.#eager, this.#styles);
200
196
  this.#sheets.push(sheet);
201
197
  return sheet;
202
198
  }
203
199
  async commit() {
204
- if (this.#committed) {
205
- throw new AuthoringError('the workbook is already committed');
206
- }
200
+ this.#assertOpen('its package is already assembled and cannot be re-emitted');
207
201
  this.#committed = true;
208
202
  for (const sheet of this.#sheets)
209
203
  sheet.commit();
@@ -215,22 +209,36 @@ export class WorkbookStreamWriter {
215
209
  if (sheetFlushed)
216
210
  flushed.set(sheet.model, sheetFlushed);
217
211
  }
218
- const parts = buildPackageParts(this.#workbook, {
219
- ...this.#writeOptions,
220
- styles: this.#styles,
221
- flushed,
222
- });
223
212
  const owned = this.#stream;
224
213
  const sink = this.#sink;
225
214
  const sinkSettled = sink ? settleOnFinish(sink) : undefined;
226
- const bytes = await streamZipPackage(parts, (chunk) => {
227
- owned?.write(chunk);
228
- sink?.write(chunk);
229
- });
230
- owned?.end();
231
- sink?.end();
232
- await sinkSettled;
233
- return bytes;
215
+ try {
216
+ const parts = buildPackageParts(this.#workbook, {
217
+ ...this.#writeOptions,
218
+ styles: this.#styles,
219
+ flushed,
220
+ });
221
+ const bytes = await streamZipPackage(parts, (chunk) => {
222
+ owned?.write(chunk);
223
+ sink?.write(chunk);
224
+ });
225
+ owned?.end();
226
+ sink?.end();
227
+ await sinkSettled;
228
+ return bytes;
229
+ }
230
+ catch (error) {
231
+ const failure = error instanceof Error ? error : new Error(String(error));
232
+ sinkSettled?.catch(() => { });
233
+ owned?.destroy(failure);
234
+ sink?.destroy(failure);
235
+ throw error;
236
+ }
237
+ }
238
+ #assertOpen(what) {
239
+ if (this.#committed) {
240
+ throw new AuthoringError(`the workbook is already committed: ${what}`);
241
+ }
234
242
  }
235
243
  }
236
244
  function settleOnFinish(sink) {
@@ -81,6 +81,11 @@ export declare function createStyleRegistry(workbook: Workbook): StyleRegistry;
81
81
  * streaming writer can drive the identical parts through a streamed zip container rather than
82
82
  * `zipSync`. Neither writer duplicates a byte of serialisation.
83
83
  *
84
+ * Three steps, and their order is load-bearing. The package is planned in full before anything is
85
+ * serialised, so a drawing's embed and a pivot's cache can target a part number that already exists.
86
+ * The sheets are serialised before the parts are emitted, because interning a style is a side effect
87
+ * of that pass and `xl/styles.xml` is one of the parts emitted in the third step.
88
+ *
84
89
  * @throws {AuthoringError} if the workbook has no worksheets, or holds a value the writer cannot represent.
85
90
  */
86
91
  export declare function buildPackageParts(workbook: Workbook, options?: InternalWriteOptions): Record<string, Uint8Array>;
@@ -1,12 +1,13 @@
1
1
  import { strToU8, zip, zipSync } from 'fflate';
2
2
  import { AuthoringError } from '../../errors.js';
3
- import { THEME_PART_PATH } from '../opc/part-paths.js';
3
+ import { relativePartPath, relsPathFor, THEME_PART_PATH } from '../opc/part-paths.js';
4
4
  import { relsPartXml } from '../opc/rels.js';
5
5
  import { FIXED_ENTRY_MTIME } from '../opc/zip-mtime.js';
6
6
  import { collectComments, commentsXml, vmlDrawingXml } from './comments.js';
7
7
  import { collectHyperlinks, planHyperlinks } from './hyperlinks.js';
8
8
  import { drawingRelsXml, drawingXml } from './images.js';
9
9
  import { planMedia, planPreservedParts, SheetRelIds, } from './package-plan.js';
10
+ import { APP_PROPS_PART, commentsPart, CORE_PROPS_PART, drawingPart, mediaPart, PERSONS_PART, pivotCacheDefinitionPart, pivotCacheRecordsPart, pivotTablePart, printerSettingsPart, SHARED_STRINGS_PART, STYLES_PART, tablePart, threadedCommentsPart, vmlDrawingPart, WORKBOOK_PART, worksheetPart, } from './part-names.js';
10
11
  import { pivotCacheDefinitionXml, pivotCacheRecordsXml, pivotTableXml } from './pivot.js';
11
12
  import { REL } from './relationships.js';
12
13
  import { SharedStringTable } from './shared-strings.js';
@@ -47,6 +48,72 @@ export function createStyleRegistry(workbook) {
47
48
  styles.addTableStyle(style);
48
49
  return styles;
49
50
  }
51
+ function planSheet(context) {
52
+ const { sheet, index, media, preserved, numbering } = context;
53
+ const rels = new SheetRelIds();
54
+ const tables = sheet.tables.map((table) => ({
55
+ table,
56
+ number: ++numbering.table,
57
+ relId: rels.next(),
58
+ }));
59
+ let drawing = null;
60
+ if (sheet.images.length > 0) {
61
+ const images = sheet.images.map((image, j) => {
62
+ const { number, image: registered } = media.resolve(image.imageId);
63
+ return {
64
+ anchor: image.anchor,
65
+ embedId: `rId${j + 1}`,
66
+ mediaNumber: number,
67
+ extension: registered.extension,
68
+ };
69
+ });
70
+ drawing = { number: ++numbering.drawing, relId: rels.next(), images };
71
+ }
72
+ const threads = sheet.commentThreads.filter((thread) => thread.comments.length > 0);
73
+ const sheetComments = collectComments(sheet, threads);
74
+ const comments = sheetComments.length === 0
75
+ ? null
76
+ : {
77
+ number: index + 1,
78
+ comments: sheetComments,
79
+ vmlRelId: rels.next(),
80
+ commentsRelId: rels.next(),
81
+ };
82
+ const threadedComments = threads.length === 0 ? null : { number: index + 1, threads, relId: rels.next() };
83
+ const printerData = sheet.pageSetup.printerSettings;
84
+ const printerSettings = printerData === undefined ? null : { number: index + 1, data: printerData, relId: rels.next() };
85
+ const hyperlinks = planHyperlinks(collectHyperlinks(sheet), rels);
86
+ let background = null;
87
+ if (sheet.backgroundImageId !== undefined) {
88
+ const { number, image } = media.resolve(sheet.backgroundImageId);
89
+ background = { relId: rels.next(), mediaNumber: number, extension: image.extension };
90
+ }
91
+ const preservedRefs = (preserved.perSheet[index] ?? []).map((reference) => ({ ...reference, relId: rels.next() }));
92
+ const pivots = sheet.pivotTables.map((table) => {
93
+ const number = ++numbering.pivot;
94
+ return { number, cacheId: String(number), table, sheetRelId: rels.next(), workbookRelId: '' };
95
+ });
96
+ return {
97
+ tables,
98
+ drawing,
99
+ comments,
100
+ threadedComments,
101
+ printerSettings,
102
+ hyperlinks,
103
+ background,
104
+ preservedRefs,
105
+ pivots,
106
+ };
107
+ }
108
+ function numbersOf(plans, pick) {
109
+ const numbers = [];
110
+ for (const plan of plans) {
111
+ const part = pick(plan);
112
+ if (part !== null)
113
+ numbers.push(part.number);
114
+ }
115
+ return numbers;
116
+ }
50
117
  function resolveSheetReferences(plan) {
51
118
  const refs = plan.preservedRefs;
52
119
  const preservedDrawingRelId = refs.find((ref) => ref.element === 'drawing')?.relId ?? null;
@@ -63,125 +130,65 @@ function resolveSheetReferences(plan) {
63
130
  slicerRelIds,
64
131
  };
65
132
  }
66
- export function buildPackageParts(workbook, options = {}) {
67
- const sheets = workbook.worksheets;
68
- if (sheets.length === 0) {
69
- throw new AuthoringError('cannot write a workbook with no worksheets: a zero-sheet package is corrupt to Excel');
70
- }
71
- const sharedStrings = options.useSharedStrings ? new SharedStringTable() : null;
133
+ function assignWorkbookRelIds(context) {
134
+ const { sheetCount, hasSharedStrings, hasPersons, preservedWorkbook, pivots } = context;
135
+ const modeledCount = sheetCount + FIXED_WORKBOOK_REL_COUNT + (hasSharedStrings ? 1 : 0);
136
+ const personsRelId = hasPersons ? `rId${modeledCount + 1}` : null;
137
+ const preservedBase = modeledCount + (personsRelId === null ? 0 : 1);
138
+ const preservedWorkbookRels = preservedWorkbook.map((ref, i) => ({
139
+ ...ref,
140
+ relId: `rId${preservedBase + 1 + i}`,
141
+ }));
142
+ const pivotBase = preservedBase + preservedWorkbook.length;
143
+ pivots.forEach((pivot, i) => {
144
+ pivot.workbookRelId = `rId${pivotBase + 1 + i}`;
145
+ });
146
+ return { personsRelId, preservedWorkbookRels };
147
+ }
148
+ function planPackage(workbook, sheets) {
72
149
  const media = planMedia(workbook, sheets);
73
150
  const generatedDrawingCount = sheets.filter((sheet) => sheet.images.length > 0).length;
74
151
  const preserved = planPreservedParts(workbook, generatedDrawingCount, media.parts.length);
75
- let tableNumber = 0;
76
- let drawingNumber = 0;
77
- let pivotNumber = 0;
78
- const perSheet = sheets.map((sheet, i) => {
79
- const rels = new SheetRelIds();
80
- const tables = sheet.tables.map((table) => ({
81
- table,
82
- number: ++tableNumber,
83
- relId: rels.next(),
84
- }));
85
- let drawing = null;
86
- if (sheet.images.length > 0) {
87
- const images = sheet.images.map((image, j) => {
88
- const registered = workbook.getImage(image.imageId);
89
- return {
90
- anchor: image.anchor,
91
- embedId: `rId${j + 1}`,
92
- mediaNumber: media.numberById.get(image.imageId),
93
- extension: registered.extension,
94
- };
95
- });
96
- drawing = { number: ++drawingNumber, relId: rels.next(), images };
97
- }
98
- const threads = sheet.commentThreads.filter((thread) => thread.comments.length > 0);
99
- const sheetComments = collectComments(sheet, threads);
100
- const comments = sheetComments.length === 0
101
- ? null
102
- : {
103
- number: i + 1,
104
- comments: sheetComments,
105
- vmlRelId: rels.next(),
106
- commentsRelId: rels.next(),
107
- };
108
- const threadedComments = threads.length === 0 ? null : { number: i + 1, threads, relId: rels.next() };
109
- const printerData = sheet.pageSetup.printerSettings;
110
- const printerSettings = printerData === undefined ? null : { number: i + 1, data: printerData, relId: rels.next() };
111
- const hyperlinks = planHyperlinks(collectHyperlinks(sheet), rels);
112
- let background = null;
113
- if (sheet.backgroundImageId !== undefined) {
114
- const registered = workbook.getImage(sheet.backgroundImageId);
115
- if (registered === undefined) {
116
- throw new AuthoringError(`sheet "${sheet.name}" sets background image id ${sheet.backgroundImageId}, which is not registered on the workbook`);
117
- }
118
- background = {
119
- relId: rels.next(),
120
- mediaNumber: media.numberById.get(sheet.backgroundImageId),
121
- extension: registered.extension,
122
- };
123
- }
124
- const preservedRefs = (preserved.perSheet[i] ?? []).map((reference) => ({ ...reference, relId: rels.next() }));
125
- const pivots = sheet.pivotTables.map((table) => {
126
- const number = ++pivotNumber;
127
- return { number, cacheId: String(number), table, sheetRelId: rels.next(), workbookRelId: '' };
128
- });
129
- return {
130
- tables,
131
- drawing,
132
- comments,
133
- threadedComments,
134
- printerSettings,
135
- hyperlinks,
136
- background,
137
- preservedRefs,
138
- pivots,
139
- };
140
- });
141
- const allTables = perSheet.flatMap((plan) => plan.tables);
142
- const allPivots = perSheet.flatMap((plan) => plan.pivots);
143
- const styles = options.styles ?? createStyleRegistry(workbook);
144
- const sheetXml = sheets.map((sheet, i) => {
145
- const plan = perSheet[i];
146
- return worksheetXml(sheet, plan.tables, styles, resolveSheetReferences(plan), plan.hyperlinks, sharedStrings, i === workbook.activeTabIndex, options.flushed?.get(sheet));
152
+ const numbering = { table: 0, drawing: 0, pivot: 0 };
153
+ const perSheet = sheets.map((sheet, index) => planSheet({ sheet, index, media, preserved, numbering }));
154
+ return {
155
+ media,
156
+ preserved,
157
+ perSheet,
158
+ allTables: perSheet.flatMap((plan) => plan.tables),
159
+ allPivots: perSheet.flatMap((plan) => plan.pivots),
160
+ };
161
+ }
162
+ function serialiseSheets(workbook, sheets, plan, styles, sharedStrings, flushed) {
163
+ return sheets.map((sheet, i) => {
164
+ const sheetPlan = plan.perSheet[i];
165
+ return worksheetXml(sheet, sheetPlan.tables, styles, resolveSheetReferences(sheetPlan), sheetPlan.hyperlinks, sharedStrings, i === workbook.activeTabIndex, flushed?.get(sheet));
147
166
  });
167
+ }
168
+ function emitPackageParts(context) {
169
+ const { workbook, sheets, plan, styles, sharedStrings, sheetXml } = context;
170
+ const { media, preserved, perSheet, allTables, allPivots } = plan;
148
171
  const hasSharedStrings = sharedStrings !== null && !sharedStrings.isEmpty;
149
- const commentNumbers = perSheet
150
- .map((plan) => plan.comments)
151
- .filter((c) => c !== null)
152
- .map((c) => c.number);
153
- const drawingNumbers = perSheet
154
- .map((plan) => plan.drawing)
155
- .filter((d) => d !== null)
156
- .map((d) => d.number);
157
- const printerSettingsNumbers = perSheet
158
- .map((plan) => plan.printerSettings)
159
- .filter((p) => p !== null)
160
- .map((p) => p.number);
161
- const threadedCommentNumbers = perSheet
162
- .map((plan) => plan.threadedComments)
163
- .filter((t) => t !== null)
164
- .map((t) => t.number);
172
+ const commentNumbers = numbersOf(perSheet, (sheetPlan) => sheetPlan.comments);
173
+ const drawingNumbers = numbersOf(perSheet, (sheetPlan) => sheetPlan.drawing);
174
+ const printerSettingsNumbers = numbersOf(perSheet, (sheetPlan) => sheetPlan.printerSettings);
175
+ const threadedCommentNumbers = numbersOf(perSheet, (sheetPlan) => sheetPlan.threadedComments);
165
176
  const persons = threadedCommentNumbers.length === 0 ? [] : workbook.persons;
166
- const modeledWorkbookRelCount = sheets.length + FIXED_WORKBOOK_REL_COUNT + (hasSharedStrings ? 1 : 0);
167
- const personsRelId = persons.length === 0 ? null : `rId${modeledWorkbookRelCount + 1}`;
168
- const workbookRelBase = modeledWorkbookRelCount + (personsRelId === null ? 0 : 1);
169
- const preservedWorkbookRels = preserved.workbook.map((ref, i) => ({
170
- ...ref,
171
- relId: `rId${workbookRelBase + 1 + i}`,
172
- }));
173
- const pivotWorkbookRelBase = workbookRelBase + preserved.workbook.length;
174
- allPivots.forEach((pivot, i) => {
175
- pivot.workbookRelId = `rId${pivotWorkbookRelBase + 1 + i}`;
177
+ const { personsRelId, preservedWorkbookRels } = assignWorkbookRelIds({
178
+ sheetCount: sheets.length,
179
+ hasSharedStrings,
180
+ hasPersons: persons.length > 0,
181
+ preservedWorkbook: preserved.workbook,
182
+ pivots: allPivots,
176
183
  });
177
184
  const files = {
178
185
  '[Content_Types].xml': strToU8(contentTypesXml(sheets.length, allTables, commentNumbers, drawingNumbers, printerSettingsNumbers, media.extensions, hasSharedStrings, preserved.parts, allPivots, preservedWorkbookRels, threadedCommentNumbers, persons.length > 0)),
179
186
  '_rels/.rels': strToU8(rootRelsXml(preserved.root)),
180
- 'docProps/core.xml': strToU8(corePropsXml(workbook.properties)),
181
- 'docProps/app.xml': strToU8(appPropsXml(workbook.properties)),
182
- 'xl/workbook.xml': strToU8(workbookXml(workbook, preservedWorkbookRels, allPivots)),
183
- 'xl/_rels/workbook.xml.rels': strToU8(workbookRelsXml(sheets.length, hasSharedStrings, personsRelId, preservedWorkbookRels, allPivots)),
184
- 'xl/styles.xml': strToU8(styles.toXml()),
187
+ [CORE_PROPS_PART]: strToU8(corePropsXml(workbook.properties)),
188
+ [APP_PROPS_PART]: strToU8(appPropsXml(workbook.properties)),
189
+ [WORKBOOK_PART]: strToU8(workbookXml(workbook, preservedWorkbookRels, allPivots)),
190
+ [relsPathFor(WORKBOOK_PART)]: strToU8(workbookRelsXml(sheets.length, hasSharedStrings, personsRelId, preservedWorkbookRels, allPivots)),
191
+ [STYLES_PART]: strToU8(styles.toXml()),
185
192
  };
186
193
  if (!preserved.themeEmitted) {
187
194
  const overrides = workbook.themeOverrides;
@@ -190,26 +197,37 @@ export function buildPackageParts(workbook, options = {}) {
190
197
  : applyThemeOverrides(DEFAULT_THEME_XML, overrides));
191
198
  }
192
199
  if (hasSharedStrings) {
193
- files['xl/sharedStrings.xml'] = strToU8(sharedStrings.toXml());
200
+ files[SHARED_STRINGS_PART] = strToU8(sharedStrings.toXml());
194
201
  }
195
202
  if (persons.length > 0)
196
- files['xl/persons/person.xml'] = strToU8(personsXml(persons));
203
+ files[PERSONS_PART] = strToU8(personsXml(persons));
197
204
  for (const part of media.parts) {
198
- files[`xl/media/image${part.number}.${part.extension}`] = part.data;
205
+ files[mediaPart(part.number, part.extension)] = part.data;
199
206
  }
200
207
  emitSheetParts(files, perSheet, sheetXml);
201
208
  for (const { table, number } of allTables) {
202
- files[`xl/tables/table${number}.xml`] = strToU8(tableXml(table, number));
209
+ files[tablePart(number)] = strToU8(tableXml(table, number));
203
210
  }
204
211
  emitPivotParts(files, allPivots);
205
212
  emitPreservedParts(files, preserved.parts);
206
213
  return files;
207
214
  }
215
+ export function buildPackageParts(workbook, options = {}) {
216
+ const sheets = workbook.worksheets;
217
+ if (sheets.length === 0) {
218
+ throw new AuthoringError('cannot write a workbook with no worksheets: a zero-sheet package is corrupt to Excel');
219
+ }
220
+ const sharedStrings = options.useSharedStrings ? new SharedStringTable() : null;
221
+ const styles = options.styles ?? createStyleRegistry(workbook);
222
+ const plan = planPackage(workbook, sheets);
223
+ const sheetXml = serialiseSheets(workbook, sheets, plan, styles, sharedStrings, options.flushed);
224
+ return emitPackageParts({ workbook, sheets, plan, styles, sharedStrings, sheetXml });
225
+ }
208
226
  function emitSheetParts(files, perSheet, sheetXml) {
209
227
  perSheet.forEach((plan, i) => {
210
228
  const { tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots, } = plan;
211
229
  const hasExternalHyperlink = hyperlinks.some((link) => link.relId !== undefined);
212
- files[`xl/worksheets/sheet${i + 1}.xml`] = strToU8(sheetXml[i]);
230
+ files[worksheetPart(i + 1)] = strToU8(sheetXml[i]);
213
231
  if (tables.length > 0 ||
214
232
  drawing !== null ||
215
233
  comments !== null ||
@@ -219,42 +237,48 @@ function emitSheetParts(files, perSheet, sheetXml) {
219
237
  hasExternalHyperlink ||
220
238
  preservedRefs.length > 0 ||
221
239
  pivots.length > 0) {
222
- files[`xl/worksheets/_rels/sheet${i + 1}.xml.rels`] = strToU8(worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots));
240
+ files[relsPathFor(worksheetPart(i + 1))] = strToU8(worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots));
223
241
  }
224
242
  if (printerSettings !== null) {
225
- files[`xl/printerSettings/printerSettings${printerSettings.number}.bin`] =
226
- printerSettings.data;
243
+ files[printerSettingsPart(printerSettings.number)] = printerSettings.data;
227
244
  }
228
245
  if (drawing !== null) {
229
- files[`xl/drawings/drawing${drawing.number}.xml`] = strToU8(drawingXml(drawing.images));
230
- const targets = drawing.images.map((image) => `../media/image${image.mediaNumber}.${image.extension}`);
231
- files[`xl/drawings/_rels/drawing${drawing.number}.xml.rels`] = strToU8(drawingRelsXml(targets));
246
+ const drawingPath = drawingPart(drawing.number);
247
+ files[drawingPath] = strToU8(drawingXml(drawing.images));
248
+ const targets = drawing.images.map((image) => relativePartPath(drawingPath, mediaPart(image.mediaNumber, image.extension)));
249
+ files[relsPathFor(drawingPath)] = strToU8(drawingRelsXml(targets));
232
250
  }
233
251
  if (comments !== null) {
234
- files[`xl/comments${comments.number}.xml`] = strToU8(commentsXml(comments.comments));
235
- files[`xl/drawings/vmlDrawing${comments.number}.vml`] = strToU8(vmlDrawingXml(comments.comments));
252
+ files[commentsPart(comments.number)] = strToU8(commentsXml(comments.comments));
253
+ files[vmlDrawingPart(comments.number)] = strToU8(vmlDrawingXml(comments.comments));
236
254
  }
237
255
  if (threadedComments !== null) {
238
- files[`xl/threadedComments/threadedComment${threadedComments.number}.xml`] = strToU8(threadedCommentsXml(threadedComments.threads));
256
+ files[threadedCommentsPart(threadedComments.number)] = strToU8(threadedCommentsXml(threadedComments.threads));
239
257
  }
240
258
  });
241
259
  }
242
260
  function emitPivotParts(files, allPivots) {
243
261
  for (const pivot of allPivots) {
244
262
  const { number, cacheId, table } = pivot;
245
- files[`xl/pivotTables/pivotTable${number}.xml`] = strToU8(pivotTableXml(table, `PivotTable${number}`, cacheId));
246
- files[`xl/pivotTables/_rels/pivotTable${number}.xml.rels`] = strToU8(relsPartXml([
263
+ const tablePath = pivotTablePart(number);
264
+ const definitionPath = pivotCacheDefinitionPart(number);
265
+ files[tablePath] = strToU8(pivotTableXml(table, `PivotTable${number}`, cacheId));
266
+ files[relsPathFor(tablePath)] = strToU8(relsPartXml([
247
267
  {
248
268
  id: 'rId1',
249
269
  type: REL.pivotCacheDefinition,
250
- target: `../pivotCache/pivotCacheDefinition${number}.xml`,
270
+ target: relativePartPath(tablePath, definitionPath),
251
271
  },
252
272
  ]));
253
- files[`xl/pivotCache/pivotCacheDefinition${number}.xml`] = strToU8(pivotCacheDefinitionXml(table));
254
- files[`xl/pivotCache/_rels/pivotCacheDefinition${number}.xml.rels`] = strToU8(relsPartXml([
255
- { id: 'rId1', type: REL.pivotCacheRecords, target: `pivotCacheRecords${number}.xml` },
273
+ files[definitionPath] = strToU8(pivotCacheDefinitionXml(table));
274
+ files[relsPathFor(definitionPath)] = strToU8(relsPartXml([
275
+ {
276
+ id: 'rId1',
277
+ type: REL.pivotCacheRecords,
278
+ target: relativePartPath(definitionPath, pivotCacheRecordsPart(number)),
279
+ },
256
280
  ]));
257
- files[`xl/pivotCache/pivotCacheRecords${number}.xml`] = strToU8(pivotCacheRecordsXml(table));
281
+ files[pivotCacheRecordsPart(number)] = strToU8(pivotCacheRecordsXml(table));
258
282
  }
259
283
  }
260
284
  function emitPreservedParts(files, parts) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Build a narrowing predicate for a closed token union.
3
+ *
4
+ * `members` is keyed by the union, so the compiler refuses a foreign member and an omitted one
5
+ * alike. A bare list of string literals is checked one way only: it cannot name a token the union
6
+ * does not, but it can quietly omit one the union does, and a guard narrower than its union
7
+ * silently drops a token real files legitimately carry.
8
+ *
9
+ * Deriving the union from the list instead (`(typeof TOKENS)[number]`, as `table-style.ts` does)
10
+ * would make divergence impossible rather than merely detected, and is the better shape for a type
11
+ * that is not public. It is not available to a published union: the API reference renders a
12
+ * declaration by slicing its own source text, so a derived alias reaches the docs as that
13
+ * expression instead of as its members.
14
+ */
15
+ export declare function tokenSet<T extends string>(members: Record<T, true>): (value: string) => value is T;
@@ -0,0 +1,4 @@
1
+ export function tokenSet(members) {
2
+ const set = new Set(Object.keys(members));
3
+ return (value) => set.has(value);
4
+ }
@@ -0,0 +1,24 @@
1
+ /** Sector chain markers ([MS-CFB] 2.2). */
2
+ export declare const FREESECT = 4294967295;
3
+ export declare const ENDOFCHAIN = 4294967294;
4
+ export declare const FATSECT = 4294967293;
5
+ export declare const DIFSECT = 4294967292;
6
+ /**
7
+ * Sector values 0xFFFFFFFA..0xFFFFFFFF are the reserved markers above, not data-sector indices, so
8
+ * any value at or above this ceiling is chain-terminal. A reader walking a hostile chain tests the
9
+ * ceiling rather than the four markers by name: an unassigned reserved value is terminal too.
10
+ */
11
+ export declare const MAX_REGULAR_SECTOR = 4294967290;
12
+ /** Directory-tree links reuse the sector convention: NOSTREAM, and any value at or above
13
+ * {@link MAX_REGULAR_SECTOR}, mean "no such sibling or child". */
14
+ export declare const NOSTREAM = 4294967295;
15
+ /** Object types ([MS-CFB] 2.6.1). */
16
+ export declare const TYPE_EMPTY = 0;
17
+ export declare const TYPE_STORAGE = 1;
18
+ export declare const TYPE_STREAM = 2;
19
+ export declare const TYPE_ROOT = 5;
20
+ /** A directory entry is a fixed 128 bytes ([MS-CFB] 2.6.1), whatever the sector size. */
21
+ export declare const DIR_ENTRY_SIZE = 128;
22
+ /** 32 UTF-16 code units including the NUL terminator. This is also why a VBA module name is capped
23
+ * at 31: the module is a stream, and the stream's name is the module's. */
24
+ export declare const MAX_NAME_CHARS = 31;
@@ -0,0 +1,12 @@
1
+ export const FREESECT = 0xffffffff;
2
+ export const ENDOFCHAIN = 0xfffffffe;
3
+ export const FATSECT = 0xfffffffd;
4
+ export const DIFSECT = 0xfffffffc;
5
+ export const MAX_REGULAR_SECTOR = 0xfffffffa;
6
+ export const NOSTREAM = 0xffffffff;
7
+ export const TYPE_EMPTY = 0;
8
+ export const TYPE_STORAGE = 1;
9
+ export const TYPE_STREAM = 2;
10
+ export const TYPE_ROOT = 5;
11
+ export const DIR_ENTRY_SIZE = 128;
12
+ export const MAX_NAME_CHARS = 31;
@@ -1,20 +1,11 @@
1
+ import { DIFSECT, DIR_ENTRY_SIZE, ENDOFCHAIN, FATSECT, FREESECT, MAX_NAME_CHARS, NOSTREAM, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
1
2
  import { VbaAuthorError } from './errors.js';
2
3
  const SECTOR = 512;
3
4
  const MINI_SECTOR = 64;
4
5
  const MINI_CUTOFF = 4096;
5
- const DIR_ENTRY_SIZE = 128;
6
6
  const ENTRIES_PER_DIR_SECTOR = SECTOR / DIR_ENTRY_SIZE;
7
7
  const FAT_ENTRIES_PER_SECTOR = SECTOR / 4;
8
8
  const DIFAT_HEADER_SLOTS = 109;
9
- const MAX_NAME_CHARS = 31;
10
- const FREESECT = 0xffffffff;
11
- const ENDOFCHAIN = 0xfffffffe;
12
- const FATSECT = 0xfffffffd;
13
- const DIFSECT = 0xfffffffc;
14
- const NOSTREAM = 0xffffffff;
15
- const TYPE_STORAGE = 1;
16
- const TYPE_STREAM = 2;
17
- const TYPE_ROOT = 5;
18
9
  const COLOR_BLACK = 1;
19
10
  function isStream(node) {
20
11
  return 'data' in node;
@@ -36,7 +27,8 @@ export function writeCompoundFile(root) {
36
27
  return entry;
37
28
  };
38
29
  const rootEntry = addEntry('Root Entry', TYPE_ROOT, ENDOFCHAIN);
39
- const miniBytes = [];
30
+ const miniChunks = [];
31
+ let miniLength = 0;
40
32
  const miniFat = [];
41
33
  const bigStreams = [];
42
34
  const addNode = (node, siblings) => {
@@ -50,11 +42,14 @@ export function writeCompoundFile(root) {
50
42
  bigStreams.push({ entry, data: node.data, sectors: Math.ceil(node.data.length / SECTOR) });
51
43
  }
52
44
  else {
53
- const startMini = miniBytes.length / MINI_SECTOR;
45
+ const startMini = miniLength / MINI_SECTOR;
54
46
  const numMini = Math.ceil(node.data.length / MINI_SECTOR);
55
47
  for (let k = 0; k < numMini; k++)
56
48
  miniFat.push(k < numMini - 1 ? startMini + k + 1 : ENDOFCHAIN);
57
- miniBytes.push(...node.data, ...new Array(numMini * MINI_SECTOR - node.data.length).fill(0));
49
+ const padded = new Uint8Array(numMini * MINI_SECTOR);
50
+ padded.set(node.data);
51
+ miniChunks.push(padded);
52
+ miniLength += padded.length;
58
53
  entry.startSector = startMini;
59
54
  }
60
55
  }
@@ -74,7 +69,7 @@ export function writeCompoundFile(root) {
74
69
  linkChildren(rootEntry, rootKids);
75
70
  const dirSectors = Math.ceil(entries.length / ENTRIES_PER_DIR_SECTOR);
76
71
  const miniFatSectors = miniFat.length > 0 ? Math.ceil((miniFat.length * 4) / FAT_ENTRIES_PER_SECTOR) : 0;
77
- const miniStreamSectors = Math.ceil(miniBytes.length / SECTOR);
72
+ const miniStreamSectors = Math.ceil(miniLength / SECTOR);
78
73
  const baseSectors = dirSectors +
79
74
  miniFatSectors +
80
75
  miniStreamSectors +
@@ -112,7 +107,7 @@ export function writeCompoundFile(root) {
112
107
  cursor += difatSectors;
113
108
  const totalSectors = cursor;
114
109
  rootEntry.startSector = miniStreamStart;
115
- rootEntry.size = miniBytes.length;
110
+ rootEntry.size = miniLength;
116
111
  const fat = new Array(fatSectors * FAT_ENTRIES_PER_SECTOR).fill(FREESECT);
117
112
  const chainRegion = (start, count) => {
118
113
  for (let k = 0; k < count; k++)
@@ -146,8 +141,13 @@ export function writeCompoundFile(root) {
146
141
  for (let i = 0; i < miniFatSectors * FAT_ENTRIES_PER_SECTOR; i++) {
147
142
  dv.setUint32(at(miniFatStart) + i * 4, miniFat[i] ?? FREESECT, true);
148
143
  }
149
- if (miniBytes.length > 0)
150
- buf.set(Uint8Array.from(miniBytes), at(miniStreamStart));
144
+ if (miniLength > 0) {
145
+ let offset = at(miniStreamStart);
146
+ for (const chunk of miniChunks) {
147
+ buf.set(chunk, offset);
148
+ offset += chunk.length;
149
+ }
150
+ }
151
151
  for (const big of bigStreams)
152
152
  buf.set(big.data, at(big.entry.startSector));
153
153
  for (const [i, sector] of fat.entries())
package/dist/vba/cfb.js CHANGED
@@ -1,14 +1,8 @@
1
1
  import { concat, decodeUtf16le, readU16, readU32 } from './bytes.js';
2
+ import { DIR_ENTRY_SIZE, MAX_REGULAR_SECTOR, NOSTREAM, TYPE_EMPTY, TYPE_ROOT, TYPE_STORAGE, TYPE_STREAM, } from './cfb-format.js';
2
3
  import { VbaParseError } from './errors.js';
3
- const MAX_REGULAR_SECTOR = 0xfffffffa;
4
- const NOSTREAM = 0xffffffff;
5
- const TYPE_EMPTY = 0;
6
- const TYPE_STORAGE = 1;
7
- const TYPE_STREAM = 2;
8
- const TYPE_ROOT = 5;
9
4
  const CFB_SIGNATURE_LO = 0xe011cfd0;
10
5
  const CFB_SIGNATURE_HI = 0xe11ab1a1;
11
- const DIR_ENTRY_SIZE = 128;
12
6
  export class CompoundFile {
13
7
  #buf;
14
8
  #sectorSize;