@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.
- package/dist/core/autofilter.d.ts +1 -1
- package/dist/core/autofilter.js +3 -5
- package/dist/core/axis-handle.d.ts +8 -0
- package/dist/core/axis-handle.js +14 -0
- package/dist/core/cell.d.ts +24 -0
- package/dist/core/cell.js +9 -1
- package/dist/core/column.d.ts +4 -1
- package/dist/core/column.js +31 -35
- package/dist/core/conditional-formatting.d.ts +5 -5
- package/dist/core/conditional-formatting.js +11 -25
- package/dist/core/data-validation.d.ts +3 -3
- package/dist/core/data-validation.js +7 -15
- package/dist/core/image.d.ts +1 -1
- package/dist/core/image.js +2 -4
- package/dist/core/merge-index.d.ts +31 -0
- package/dist/core/merge-index.js +60 -0
- package/dist/core/merge.d.ts +0 -10
- package/dist/core/merge.js +0 -8
- package/dist/core/page-setup.d.ts +72 -2
- package/dist/core/page-setup.js +34 -8
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +4 -8
- package/dist/core/row.d.ts +4 -1
- package/dist/core/row.js +19 -23
- package/dist/core/style.d.ts +51 -7
- package/dist/core/style.js +29 -34
- package/dist/core/table.d.ts +1 -1
- package/dist/core/table.js +3 -5
- package/dist/core/used-extent.d.ts +44 -0
- package/dist/core/used-extent.js +102 -0
- package/dist/core/worksheet.d.ts +17 -6
- package/dist/core/worksheet.js +48 -42
- package/dist/customui/ribbon.js +4 -6
- package/dist/io/opc/inflate.js +1 -1
- package/dist/io/opc/part-paths.d.ts +1 -0
- package/dist/io/opc/part-paths.js +15 -0
- package/dist/io/opc/read-opc.d.ts +22 -3
- package/dist/io/opc/read-opc.js +11 -39
- package/dist/io/opc/rels.d.ts +1 -6
- package/dist/io/opc/rels.js +1 -4
- package/dist/io/style/xf-style.d.ts +26 -2
- package/dist/io/style/xf-style.js +6 -0
- package/dist/io/xlsb/read-styles.js +6 -3
- package/dist/io/xlsb/read.d.ts +1 -1
- package/dist/io/xlsb/read.js +5 -8
- package/dist/io/xlsx/cell-accumulator.d.ts +1 -1
- package/dist/io/xlsx/cell-accumulator.js +1 -1
- package/dist/io/xlsx/cell-value.js +1 -1
- package/dist/io/xlsx/color-xml.js +1 -1
- package/dist/io/xlsx/comments.js +2 -1
- package/dist/io/xlsx/conditional-formatting.d.ts +3 -2
- package/dist/io/xlsx/conditional-formatting.js +22 -21
- package/dist/io/xlsx/data-validation.d.ts +9 -7
- package/dist/io/xlsx/data-validation.js +10 -9
- package/dist/io/xlsx/edit-vba.js +4 -11
- package/dist/io/xlsx/hyperlinks.d.ts +4 -2
- package/dist/io/xlsx/hyperlinks.js +20 -17
- package/dist/io/xlsx/images.js +15 -14
- package/dist/io/xlsx/package-plan.d.ts +9 -1
- package/dist/io/xlsx/package-plan.js +29 -16
- package/dist/io/xlsx/part-names.d.ts +41 -0
- package/dist/io/xlsx/part-names.js +46 -0
- package/dist/io/xlsx/read-pivot.js +2 -1
- package/dist/io/xlsx/read-rows.js +12 -13
- package/dist/io/xlsx/read-shared-strings.js +2 -1
- package/dist/io/xlsx/read-styles.d.ts +1 -1
- package/dist/io/xlsx/read-styles.js +63 -42
- package/dist/io/xlsx/read-worksheet.d.ts +10 -1
- package/dist/io/xlsx/read-worksheet.js +43 -56
- package/dist/io/xlsx/read.d.ts +2 -3
- package/dist/io/xlsx/read.js +81 -73
- package/dist/io/xlsx/rich-runs.d.ts +1 -1
- package/dist/io/xlsx/rich-runs.js +2 -1
- package/dist/io/xlsx/sheet-properties.js +21 -28
- package/dist/io/xlsx/styles.js +23 -20
- package/dist/io/xlsx/tables.js +2 -1
- package/dist/io/xlsx/theme-xml.js +1 -1
- package/dist/io/xlsx/threaded-comments.js +2 -1
- package/dist/io/xlsx/workbook-xml.js +29 -28
- package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
- package/dist/io/xlsx/worksheet-xml.js +14 -16
- package/dist/io/xlsx/write-stream.d.ts +6 -0
- package/dist/io/xlsx/write-stream.js +30 -22
- package/dist/io/xlsx/write.d.ts +5 -0
- package/dist/io/xlsx/write.js +155 -131
- package/dist/token-set.d.ts +15 -0
- package/dist/token-set.js +4 -0
- package/dist/vba/cfb-format.d.ts +24 -0
- package/dist/vba/cfb-format.js +12 -0
- package/dist/vba/cfb-writer.js +17 -17
- package/dist/vba/cfb.js +1 -7
- package/dist/vba/ms-ovba.js +32 -11
- package/dist/vba/project.js +13 -13
- package/dist/vba/vba-encoding.js +1 -1
- package/dist/xml/xml-read.d.ts +59 -74
- package/dist/xml/xml-read.js +83 -176
- package/dist/xml/xml-scan.d.ts +100 -0
- package/dist/xml/xml-scan.js +208 -0
- package/dist/xml/xml.d.ts +1 -1
- package/package.json +2 -3
|
@@ -187,23 +187,17 @@ export class WorkbookStreamWriter {
|
|
|
187
187
|
return this.#stream;
|
|
188
188
|
}
|
|
189
189
|
addImage(options) {
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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) {
|
package/dist/io/xlsx/write.d.ts
CHANGED
|
@@ -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>;
|
package/dist/io/xlsx/write.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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[
|
|
200
|
+
files[SHARED_STRINGS_PART] = strToU8(sharedStrings.toXml());
|
|
194
201
|
}
|
|
195
202
|
if (persons.length > 0)
|
|
196
|
-
files[
|
|
203
|
+
files[PERSONS_PART] = strToU8(personsXml(persons));
|
|
197
204
|
for (const part of media.parts) {
|
|
198
|
-
files[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
226
|
-
printerSettings.data;
|
|
243
|
+
files[printerSettingsPart(printerSettings.number)] = printerSettings.data;
|
|
227
244
|
}
|
|
228
245
|
if (drawing !== null) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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[
|
|
235
|
-
files[
|
|
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[
|
|
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
|
-
|
|
246
|
-
|
|
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:
|
|
270
|
+
target: relativePartPath(tablePath, definitionPath),
|
|
251
271
|
},
|
|
252
272
|
]));
|
|
253
|
-
files[
|
|
254
|
-
files[
|
|
255
|
-
{
|
|
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[
|
|
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,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;
|
package/dist/vba/cfb-writer.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 (
|
|
150
|
-
|
|
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;
|