@shbernal/ts-xlsx 1.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// The buffered `.xlsx` writer: a Workbook model in, an OPC zip package out.
|
|
2
|
+
//
|
|
3
|
+
// It serialises the part of the model that exists today — worksheets; cells holding a
|
|
4
|
+
// number, string, boolean, or formula; column/row formatting; page margins and
|
|
5
|
+
// header/footer; merged ranges; and worksheet tables — into a valid package (content
|
|
6
|
+
// types, relationships, workbook, per-sheet XML, table parts, the default theme and
|
|
7
|
+
// stylesheet, and core/app properties). Styles, images, and the richer value kinds land
|
|
8
|
+
// as the model grows; until then the writer refuses a value it cannot represent
|
|
9
|
+
// faithfully rather than emitting a lossy or corrupt package.
|
|
10
|
+
//
|
|
11
|
+
// This module is the orchestrator: it plans the package graph (via `package-plan.ts`) and
|
|
12
|
+
// stitches the serialised parts (from `workbook-xml.ts` and `worksheet-xml.ts`) into the
|
|
13
|
+
// part map. The row/cell renderer and the sheet's public render types live in
|
|
14
|
+
// `worksheet-xml.ts` and are re-exported here so the streaming writer's import surface is
|
|
15
|
+
// unchanged.
|
|
16
|
+
import { strToU8, zip, zipSync } from 'fflate';
|
|
17
|
+
import { DEFAULT_THEME_XML } from "../../core/theme.js";
|
|
18
|
+
import { AuthoringError } from "../../errors.js";
|
|
19
|
+
import { THEME_PART_PATH } from "../opc/part-paths.js";
|
|
20
|
+
import { relsPartXml } from "../opc/rels.js";
|
|
21
|
+
import { collectComments, commentsXml, vmlDrawingXml } from "./comments.js";
|
|
22
|
+
import { collectHyperlinks, planHyperlinks } from "./hyperlinks.js";
|
|
23
|
+
import { drawingRelsXml, drawingXml } from "./images.js";
|
|
24
|
+
import { planMedia, planPreservedParts, SheetRelIds, } from "./package-plan.js";
|
|
25
|
+
import { pivotCacheDefinitionXml, pivotCacheRecordsXml, pivotTableXml } from "./pivot.js";
|
|
26
|
+
import { REL } from "./relationships.js";
|
|
27
|
+
import { SharedStringTable } from "./shared-strings.js";
|
|
28
|
+
import { StyleRegistry } from "./styles.js";
|
|
29
|
+
import { tableXml } from "./tables.js";
|
|
30
|
+
import { personsXml, threadedCommentsXml } from "./threaded-comments.js";
|
|
31
|
+
import { appPropsXml, contentTypesXml, corePropsXml, FIXED_WORKBOOK_REL_COUNT, rootRelsXml, workbookRelsXml, workbookXml, } from "./workbook-xml.js";
|
|
32
|
+
import { worksheetRelsXml, worksheetXml, } from "./worksheet-xml.js";
|
|
33
|
+
export { buildColumnDefaults, Extent, renderRow, } from "./worksheet-xml.js";
|
|
34
|
+
/**
|
|
35
|
+
* Serialise a workbook into an `.xlsx` package.
|
|
36
|
+
*
|
|
37
|
+
* @throws {@link AuthoringError} if the workbook has no worksheets (a zero-sheet package is corrupt),
|
|
38
|
+
* or holds a value the writer cannot yet represent.
|
|
39
|
+
*/
|
|
40
|
+
export function writeXlsx(workbook, options = {}) {
|
|
41
|
+
return zipSync(buildPackageParts(workbook, options), { level: 6 });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Serialise a workbook into an `.xlsx` package, deflating off the calling thread.
|
|
45
|
+
*
|
|
46
|
+
* Produces the same package {@link writeXlsx} does — every part compresses to identical bytes — and
|
|
47
|
+
* exists for one reason: DEFLATE dominates the cost of writing a large workbook, and {@link writeXlsx}
|
|
48
|
+
* spends all of it on the caller's thread. Here `fflate` deflates each part in a worker, so the event
|
|
49
|
+
* loop keeps turning (stalls drop from the whole write to tens of milliseconds) and parts compress in
|
|
50
|
+
* parallel, which on a multi-sheet workbook also finishes sooner. On a single-sheet workbook there is
|
|
51
|
+
* only one part to deflate, so expect responsiveness rather than speed.
|
|
52
|
+
*
|
|
53
|
+
* Building the parts still happens on the calling thread — only compression moves. That is why there
|
|
54
|
+
* is no `readXlsxAsync` mirroring this: reading is dominated by XML parsing and model building, which
|
|
55
|
+
* no worker can take, and the reader's zip-bomb ceiling is enforced by counting output between
|
|
56
|
+
* synchronous input slices. See ADR-0024.
|
|
57
|
+
*
|
|
58
|
+
* @throws {@link AuthoringError} — as a rejection — under the same conditions as {@link writeXlsx};
|
|
59
|
+
* the part-building it shares happens before any worker is involved. A failure raised by the zip
|
|
60
|
+
* layer itself (including an environment that cannot spawn a worker) propagates unwrapped, exactly
|
|
61
|
+
* as it does from {@link writeXlsx}.
|
|
62
|
+
*/
|
|
63
|
+
export async function writeXlsxAsync(workbook, options = {}) {
|
|
64
|
+
const parts = buildPackageParts(workbook, options);
|
|
65
|
+
return await new Promise((resolve, reject) => {
|
|
66
|
+
zip(parts, { level: 6 }, (error, data) => {
|
|
67
|
+
if (error)
|
|
68
|
+
reject(error);
|
|
69
|
+
else
|
|
70
|
+
resolve(data);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A style registry seeded from a workbook's read-in style layers (differential styles, named cell
|
|
76
|
+
* styles, custom indexed palette), ready to intern authored styles after them. Both the buffered
|
|
77
|
+
* pass and the streaming writer build their registry through here so a cell's style id means the
|
|
78
|
+
* same thing whichever writer emits it.
|
|
79
|
+
*/
|
|
80
|
+
export function createStyleRegistry(workbook) {
|
|
81
|
+
// Font id 0 is the workbook's own default face, resolved from what it declared, what was authored,
|
|
82
|
+
// and its theme's body typeface — never an assumed Calibri, which would re-face every empty cell
|
|
83
|
+
// and change the metric every character-unit column width is expressed in.
|
|
84
|
+
const styles = new StyleRegistry({
|
|
85
|
+
defaultFont: workbook.defaultFont,
|
|
86
|
+
...(workbook.declaredDefaultFont === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { declaredDefaultFont: workbook.declaredDefaultFont }),
|
|
89
|
+
});
|
|
90
|
+
// Seed the differential-style table with the fragments read from a source file so conditional
|
|
91
|
+
// formatting's dxfId references stay valid; styles authored on rules append after them.
|
|
92
|
+
styles.seedDifferentialStyles(workbook.differentialStyles);
|
|
93
|
+
// Seed the named cell-style layer (cellStyleXfs/cellStyles) so each style's facets re-intern into the
|
|
94
|
+
// rebuilt sub-tables and a cell's xfId link stays valid; without any, the default Normal alone emits.
|
|
95
|
+
styles.seedNamedStyles(workbook.namedStyles);
|
|
96
|
+
// Seed the custom indexed-color palette so it re-emits verbatim and an `indexed="…"` colour keeps
|
|
97
|
+
// its intended RGB; a workbook that never overrode the palette seeds nothing and writes no <colors>.
|
|
98
|
+
styles.seedIndexedColors(workbook.indexedColors);
|
|
99
|
+
// Seed the author's "Recent Colors" swatches so they re-emit unchanged rather than being reset.
|
|
100
|
+
styles.seedMruColors(workbook.mruColors);
|
|
101
|
+
// Seed the custom table-style definitions so a table's `styleName` still names a real definition and
|
|
102
|
+
// each element's dxfId still indexes the differential-style table seeded above at its original index.
|
|
103
|
+
styles.seedTableStyles(workbook.tableStyles);
|
|
104
|
+
// Authored styles append after the preserved ones, and intern their elements' formatting after the
|
|
105
|
+
// seeded dxfs — the ordering that keeps every preserved dxfId pointing where it did.
|
|
106
|
+
for (const style of workbook.customTableStyles)
|
|
107
|
+
styles.addTableStyle(style);
|
|
108
|
+
return styles;
|
|
109
|
+
}
|
|
110
|
+
// Resolve one sheet's tail reference ids (the `<drawing>`/`<legacyDrawing>`/`<legacyDrawingHF>`/
|
|
111
|
+
// `<picture>` slots and the slicer list) from its plan. A preserved `<drawing>` and a modeled one are
|
|
112
|
+
// mutually exclusive, so the drawing slot takes whichever exists; a comment's VML rides the legacy-
|
|
113
|
+
// drawing slot; and each preserved slicer surfaces its rel id so the `<x14:slicerList>` can reactivate
|
|
114
|
+
// the widget rather than orphan its part.
|
|
115
|
+
function resolveSheetReferences(plan) {
|
|
116
|
+
const refs = plan.preservedRefs;
|
|
117
|
+
const preservedDrawingRelId = refs.find((ref) => ref.element === 'drawing')?.relId ?? null;
|
|
118
|
+
const legacyDrawingHFRelId = refs.find((ref) => ref.element === 'legacyDrawingHF')?.relId ?? null;
|
|
119
|
+
const slicerRelIds = refs
|
|
120
|
+
.filter((ref) => ref.relType.endsWith('/slicer'))
|
|
121
|
+
.map((ref) => ref.relId);
|
|
122
|
+
return {
|
|
123
|
+
drawingRelId: plan.drawing?.relId ?? preservedDrawingRelId,
|
|
124
|
+
legacyDrawingRelId: plan.comments?.vmlRelId ?? null,
|
|
125
|
+
printerSettingsRelId: plan.printerSettings?.relId ?? null,
|
|
126
|
+
backgroundRelId: plan.background?.relId ?? null,
|
|
127
|
+
legacyDrawingHFRelId,
|
|
128
|
+
slicerRelIds,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Assemble a workbook into the map of OPC package parts (part name → bytes) that make up an `.xlsx`,
|
|
133
|
+
* short of zipping them. This is the whole serialisation — content types, relationships, workbook,
|
|
134
|
+
* per-sheet XML, styles, theme, media, tables, and props — factored out of {@link writeXlsx} so the
|
|
135
|
+
* streaming writer can drive the identical parts through a streamed zip container rather than
|
|
136
|
+
* `zipSync`. Neither writer duplicates a byte of serialisation.
|
|
137
|
+
*
|
|
138
|
+
* @throws {@link AuthoringError} if the workbook has no worksheets, or holds a value the writer cannot represent.
|
|
139
|
+
*/
|
|
140
|
+
export function buildPackageParts(workbook, options = {}) {
|
|
141
|
+
const sheets = workbook.worksheets;
|
|
142
|
+
if (sheets.length === 0) {
|
|
143
|
+
throw new AuthoringError('cannot write a workbook with no worksheets — a zero-sheet package is corrupt to Excel');
|
|
144
|
+
}
|
|
145
|
+
// With the option on, plain string cell values are pooled into a shared-strings table interned
|
|
146
|
+
// during the sheet pass (like the style registry); a null table keeps every string inline.
|
|
147
|
+
const sharedStrings = options.useSharedStrings ? new SharedStringTable() : null;
|
|
148
|
+
// Anchored images share workbook-wide media: every image a sheet references becomes one media part,
|
|
149
|
+
// addressed by a global number. Resolved before the sheet loop so a drawing's embeds can target it.
|
|
150
|
+
const media = planMedia(workbook, sheets);
|
|
151
|
+
// Content the model does not interpret — a vector-shape drawing, a header/footer image, a pivot
|
|
152
|
+
// table and its caches, a slicer — captured on read and re-emitted verbatim onto collision-proof
|
|
153
|
+
// paths. Preserved parts are renumbered past the parts the writer generates of the same kind
|
|
154
|
+
// (drawings, VML, media), so resolving them needs only those generated counts; each sheet's
|
|
155
|
+
// preserved references take their sheet-local rel ids in canonical position in the loop below.
|
|
156
|
+
const generatedDrawingCount = sheets.filter((sheet) => sheet.images.length > 0).length;
|
|
157
|
+
const preserved = planPreservedParts(workbook, generatedDrawingCount, media.parts.length);
|
|
158
|
+
// Plan every sheet's parts in a single pass, drawing each sheet-local relationship id from that
|
|
159
|
+
// sheet's allocator in canonical order: tables, drawing, comments (VML + comments part), printer
|
|
160
|
+
// settings, external hyperlinks, background, preserved references, pivot tables. One running
|
|
161
|
+
// allocator per sheet is what keeps the ids gapless and collision-free — no step re-derives its
|
|
162
|
+
// offset by summing the ones before it, so none can drift into another's id. Part numbers (tables,
|
|
163
|
+
// drawings, pivots) are global across the workbook and counted here in the same pass.
|
|
164
|
+
let tableNumber = 0;
|
|
165
|
+
let drawingNumber = 0;
|
|
166
|
+
let pivotNumber = 0;
|
|
167
|
+
const perSheet = sheets.map((sheet, i) => {
|
|
168
|
+
const rels = new SheetRelIds();
|
|
169
|
+
const tables = sheet.tables.map((table) => ({
|
|
170
|
+
table,
|
|
171
|
+
number: ++tableNumber,
|
|
172
|
+
relId: rels.next(),
|
|
173
|
+
}));
|
|
174
|
+
let drawing = null;
|
|
175
|
+
if (sheet.images.length > 0) {
|
|
176
|
+
const images = sheet.images.map((image, j) => {
|
|
177
|
+
const registered = workbook.getImage(image.imageId);
|
|
178
|
+
return {
|
|
179
|
+
anchor: image.anchor,
|
|
180
|
+
// The embed id is local to the drawing part's own rels, not the sheet's, so it is numbered
|
|
181
|
+
// per image from rId1 rather than drawn from the sheet allocator.
|
|
182
|
+
embedId: `rId${j + 1}`,
|
|
183
|
+
mediaNumber: media.numberById.get(image.imageId),
|
|
184
|
+
extension: registered.extension,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
drawing = { number: ++drawingNumber, relId: rels.next(), images };
|
|
188
|
+
}
|
|
189
|
+
// A conversation and the legacy fallback `<comment>` that binds its cell to it are two halves of one
|
|
190
|
+
// representation, so both are derived from this single list and neither can be emitted without the
|
|
191
|
+
// other. Verified against desktop Excel: a `tc=` fallback whose thread part is absent shows as neither
|
|
192
|
+
// a thread nor a note — the text disappears rather than degrading — and a thread part whose fallback is
|
|
193
|
+
// absent is ignored, leaving the cell blank. A thread with no messages is not one of them: it has
|
|
194
|
+
// nothing to say, and no head id for its replies or its fallback to hang off.
|
|
195
|
+
const threads = sheet.commentThreads.filter((thread) => thread.comments.length > 0);
|
|
196
|
+
const sheetComments = collectComments(sheet, threads);
|
|
197
|
+
const comments = sheetComments.length === 0
|
|
198
|
+
? null
|
|
199
|
+
: {
|
|
200
|
+
number: i + 1,
|
|
201
|
+
comments: sheetComments,
|
|
202
|
+
vmlRelId: rels.next(),
|
|
203
|
+
commentsRelId: rels.next(),
|
|
204
|
+
};
|
|
205
|
+
const threadedComments = threads.length === 0 ? null : { number: i + 1, threads, relId: rels.next() };
|
|
206
|
+
const printerData = sheet.pageSetup.printerSettings;
|
|
207
|
+
const printerSettings = printerData === undefined ? null : { number: i + 1, data: printerData, relId: rels.next() };
|
|
208
|
+
const hyperlinks = planHyperlinks(collectHyperlinks(sheet), rels);
|
|
209
|
+
let background = null;
|
|
210
|
+
if (sheet.backgroundImageId !== undefined) {
|
|
211
|
+
const registered = workbook.getImage(sheet.backgroundImageId);
|
|
212
|
+
if (registered === undefined) {
|
|
213
|
+
throw new AuthoringError(`sheet "${sheet.name}" sets background image id ${sheet.backgroundImageId}, which is not registered on the workbook`);
|
|
214
|
+
}
|
|
215
|
+
background = {
|
|
216
|
+
relId: rels.next(),
|
|
217
|
+
mediaNumber: media.numberById.get(sheet.backgroundImageId),
|
|
218
|
+
extension: registered.extension,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const preservedRefs = (preserved.perSheet[i] ?? []).map((reference) => ({ ...reference, relId: rels.next() }));
|
|
222
|
+
const pivots = sheet.pivotTables.map((table) => {
|
|
223
|
+
const number = ++pivotNumber;
|
|
224
|
+
// Each pivot is numbered globally (its parts and its `cacheId` must be workbook-unique); the
|
|
225
|
+
// workbook relationship reaching its cache is assigned once the modeled workbook rels are known.
|
|
226
|
+
return { number, cacheId: String(number), table, sheetRelId: rels.next(), workbookRelId: '' };
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
tables,
|
|
230
|
+
drawing,
|
|
231
|
+
comments,
|
|
232
|
+
threadedComments,
|
|
233
|
+
printerSettings,
|
|
234
|
+
hyperlinks,
|
|
235
|
+
background,
|
|
236
|
+
preservedRefs,
|
|
237
|
+
pivots,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
const allTables = perSheet.flatMap((plan) => plan.tables);
|
|
241
|
+
const allPivots = perSheet.flatMap((plan) => plan.pivots);
|
|
242
|
+
// Serialise the worksheets first: interning each cell/row fill into the style table is a
|
|
243
|
+
// side effect of that pass, so styles.xml can only be generated once every sheet is done. The
|
|
244
|
+
// streaming writer supplies its own registry (already seeded, and already carrying its eagerly
|
|
245
|
+
// flushed rows' styles); the buffered path seeds a fresh one here.
|
|
246
|
+
const styles = options.styles ?? createStyleRegistry(workbook);
|
|
247
|
+
const sheetXml = sheets.map((sheet, i) => {
|
|
248
|
+
const plan = perSheet[i];
|
|
249
|
+
return worksheetXml(sheet, plan.tables, styles, resolveSheetReferences(plan), plan.hyperlinks, sharedStrings,
|
|
250
|
+
// Exactly one sheet is marked selected; the model resolves which, so no package can ship with
|
|
251
|
+
// none selected (no view initialised on open) or with several (an accidental group selection,
|
|
252
|
+
// where an edit to one sheet lands on all of them).
|
|
253
|
+
i === workbook.activeTabIndex, options.flushed?.get(sheet));
|
|
254
|
+
});
|
|
255
|
+
// The pool is filled only once every sheet is serialised. Emit the part (and its rel + content
|
|
256
|
+
// type) solely when the option is on and at least one string was interned, so a workbook with no
|
|
257
|
+
// string cells never fabricates an empty table.
|
|
258
|
+
const hasSharedStrings = sharedStrings !== null && !sharedStrings.isEmpty;
|
|
259
|
+
const commentNumbers = perSheet
|
|
260
|
+
.map((plan) => plan.comments)
|
|
261
|
+
.filter((c) => c !== null)
|
|
262
|
+
.map((c) => c.number);
|
|
263
|
+
const drawingNumbers = perSheet
|
|
264
|
+
.map((plan) => plan.drawing)
|
|
265
|
+
.filter((d) => d !== null)
|
|
266
|
+
.map((d) => d.number);
|
|
267
|
+
const printerSettingsNumbers = perSheet
|
|
268
|
+
.map((plan) => plan.printerSettings)
|
|
269
|
+
.filter((p) => p !== null)
|
|
270
|
+
.map((p) => p.number);
|
|
271
|
+
const threadedCommentNumbers = perSheet
|
|
272
|
+
.map((plan) => plan.threadedComments)
|
|
273
|
+
.filter((t) => t !== null)
|
|
274
|
+
.map((t) => t.number);
|
|
275
|
+
// The identity registry is emitted only beside the thread parts that point into it. With no conversation
|
|
276
|
+
// in the package nothing can reference a `<person>`, so the part would be a workbook-level relationship
|
|
277
|
+
// to dead weight — and it is the messages, not the registry, that make an identity worth carrying.
|
|
278
|
+
const persons = threadedCommentNumbers.length === 0 ? [] : workbook.persons;
|
|
279
|
+
// A preserved workbook reference's relationship id follows the modeled workbook rels — the sheets,
|
|
280
|
+
// styles, theme, and (when emitted) shared strings and the threaded-comment person registry — so adding
|
|
281
|
+
// one never renumbers an id already used. The workbook body and its rels part are wired from the same
|
|
282
|
+
// assignment, so a pivot cache's `<pivotCaches>` registration and its relationship agree on the id.
|
|
283
|
+
const modeledWorkbookRelCount = sheets.length + FIXED_WORKBOOK_REL_COUNT + (hasSharedStrings ? 1 : 0);
|
|
284
|
+
const personsRelId = persons.length === 0 ? null : `rId${modeledWorkbookRelCount + 1}`;
|
|
285
|
+
const workbookRelBase = modeledWorkbookRelCount + (personsRelId === null ? 0 : 1);
|
|
286
|
+
const preservedWorkbookRels = preserved.workbook.map((ref, i) => ({
|
|
287
|
+
...ref,
|
|
288
|
+
relId: `rId${workbookRelBase + 1 + i}`,
|
|
289
|
+
}));
|
|
290
|
+
// A generated pivot cache's workbook relationship follows the preserved ones; the assignment
|
|
291
|
+
// mutates the shared plan so the `<pivotCaches>` body and the rels part read the same id.
|
|
292
|
+
const pivotWorkbookRelBase = workbookRelBase + preserved.workbook.length;
|
|
293
|
+
allPivots.forEach((pivot, i) => {
|
|
294
|
+
pivot.workbookRelId = `rId${pivotWorkbookRelBase + 1 + i}`;
|
|
295
|
+
});
|
|
296
|
+
const files = {
|
|
297
|
+
'[Content_Types].xml': strToU8(contentTypesXml(sheets.length, allTables, commentNumbers, drawingNumbers, printerSettingsNumbers, media.extensions, hasSharedStrings, preserved.parts, allPivots, preservedWorkbookRels, threadedCommentNumbers, persons.length > 0)),
|
|
298
|
+
'_rels/.rels': strToU8(rootRelsXml(preserved.root)),
|
|
299
|
+
'docProps/core.xml': strToU8(corePropsXml(workbook.properties)),
|
|
300
|
+
'docProps/app.xml': strToU8(appPropsXml()),
|
|
301
|
+
'xl/workbook.xml': strToU8(workbookXml(workbook, preservedWorkbookRels, allPivots)),
|
|
302
|
+
'xl/_rels/workbook.xml.rels': strToU8(workbookRelsXml(sheets.length, hasSharedStrings, personsRelId, preservedWorkbookRels, allPivots)),
|
|
303
|
+
'xl/styles.xml': strToU8(styles.toXml()),
|
|
304
|
+
};
|
|
305
|
+
// A theme read from a source package is emitted through the preserved-part path, closure and all —
|
|
306
|
+
// with any authored overrides already spliced into its entry part by the planner. A workbook without
|
|
307
|
+
// one gets its authored theme, or the library's default, which the stylesheet's `theme="1"` default
|
|
308
|
+
// font still needs something to resolve against.
|
|
309
|
+
if (!preserved.themeEmitted) {
|
|
310
|
+
files[THEME_PART_PATH] = strToU8(workbook.authoredThemeXml() ?? DEFAULT_THEME_XML);
|
|
311
|
+
}
|
|
312
|
+
if (hasSharedStrings) {
|
|
313
|
+
files['xl/sharedStrings.xml'] = strToU8(sharedStrings.toXml());
|
|
314
|
+
}
|
|
315
|
+
// Singular and unnumbered, unlike the per-sheet thread parts: one registry serves the whole workbook.
|
|
316
|
+
if (persons.length > 0)
|
|
317
|
+
files['xl/persons/person.xml'] = strToU8(personsXml(persons));
|
|
318
|
+
for (const part of media.parts) {
|
|
319
|
+
files[`xl/media/image${part.number}.${part.extension}`] = part.data;
|
|
320
|
+
}
|
|
321
|
+
emitSheetParts(files, perSheet, sheetXml);
|
|
322
|
+
for (const { table, number } of allTables) {
|
|
323
|
+
files[`xl/tables/table${number}.xml`] = strToU8(tableXml(table, number));
|
|
324
|
+
}
|
|
325
|
+
emitPivotParts(files, allPivots);
|
|
326
|
+
emitPreservedParts(files, preserved.parts);
|
|
327
|
+
return files;
|
|
328
|
+
}
|
|
329
|
+
// Emit each sheet's own parts: the sheet XML, its rels part (only when the sheet references something),
|
|
330
|
+
// and the drawing/comment/printer-settings parts those relationships point at. `sheetXml[i]` is the
|
|
331
|
+
// already-serialised body for `perSheet[i]`, indexed in lockstep.
|
|
332
|
+
function emitSheetParts(files, perSheet, sheetXml) {
|
|
333
|
+
perSheet.forEach((plan, i) => {
|
|
334
|
+
const { tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots, } = plan;
|
|
335
|
+
const hasExternalHyperlink = hyperlinks.some((link) => link.relId !== undefined);
|
|
336
|
+
files[`xl/worksheets/sheet${i + 1}.xml`] = strToU8(sheetXml[i]);
|
|
337
|
+
if (tables.length > 0 ||
|
|
338
|
+
drawing !== null ||
|
|
339
|
+
comments !== null ||
|
|
340
|
+
threadedComments !== null ||
|
|
341
|
+
printerSettings !== null ||
|
|
342
|
+
background !== null ||
|
|
343
|
+
hasExternalHyperlink ||
|
|
344
|
+
preservedRefs.length > 0 ||
|
|
345
|
+
pivots.length > 0) {
|
|
346
|
+
files[`xl/worksheets/_rels/sheet${i + 1}.xml.rels`] = strToU8(worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots));
|
|
347
|
+
}
|
|
348
|
+
if (printerSettings !== null) {
|
|
349
|
+
files[`xl/printerSettings/printerSettings${printerSettings.number}.bin`] =
|
|
350
|
+
printerSettings.data;
|
|
351
|
+
}
|
|
352
|
+
if (drawing !== null) {
|
|
353
|
+
files[`xl/drawings/drawing${drawing.number}.xml`] = strToU8(drawingXml(drawing.images));
|
|
354
|
+
const targets = drawing.images.map((image) => `../media/image${image.mediaNumber}.${image.extension}`);
|
|
355
|
+
files[`xl/drawings/_rels/drawing${drawing.number}.xml.rels`] = strToU8(drawingRelsXml(targets));
|
|
356
|
+
}
|
|
357
|
+
if (comments !== null) {
|
|
358
|
+
files[`xl/comments${comments.number}.xml`] = strToU8(commentsXml(comments.comments));
|
|
359
|
+
files[`xl/drawings/vmlDrawing${comments.number}.vml`] = strToU8(vmlDrawingXml(comments.comments));
|
|
360
|
+
}
|
|
361
|
+
if (threadedComments !== null) {
|
|
362
|
+
files[`xl/threadedComments/threadedComment${threadedComments.number}.xml`] = strToU8(threadedCommentsXml(threadedComments.threads));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
// Emit every pivot table's three chained parts. A pivot spans a pivot-table part (linked from its host
|
|
367
|
+
// sheet) that references a cache definition, which references its cache records. Each cache carries a
|
|
368
|
+
// rels part naming the next link by `rId1` — the id the definition/table XML resolves against.
|
|
369
|
+
function emitPivotParts(files, allPivots) {
|
|
370
|
+
for (const pivot of allPivots) {
|
|
371
|
+
const { number, cacheId, table } = pivot;
|
|
372
|
+
files[`xl/pivotTables/pivotTable${number}.xml`] = strToU8(pivotTableXml(table, `PivotTable${number}`, cacheId));
|
|
373
|
+
files[`xl/pivotTables/_rels/pivotTable${number}.xml.rels`] = strToU8(relsPartXml([
|
|
374
|
+
{
|
|
375
|
+
id: 'rId1',
|
|
376
|
+
type: REL.pivotCacheDefinition,
|
|
377
|
+
target: `../pivotCache/pivotCacheDefinition${number}.xml`,
|
|
378
|
+
},
|
|
379
|
+
]));
|
|
380
|
+
files[`xl/pivotCache/pivotCacheDefinition${number}.xml`] = strToU8(pivotCacheDefinitionXml(table));
|
|
381
|
+
files[`xl/pivotCache/_rels/pivotCacheDefinition${number}.xml.rels`] = strToU8(relsPartXml([
|
|
382
|
+
{ id: 'rId1', type: REL.pivotCacheRecords, target: `pivotCacheRecords${number}.xml` },
|
|
383
|
+
]));
|
|
384
|
+
files[`xl/pivotCache/pivotCacheRecords${number}.xml`] = strToU8(pivotCacheRecordsXml(table));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Emit the verbatim-preserved parts (and their rewired rels) last: their paths are collision-proof, so
|
|
388
|
+
// ordering against the generated parts does not matter.
|
|
389
|
+
function emitPreservedParts(files, parts) {
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
files[part.path] = part.bytes;
|
|
392
|
+
if (part.relsPath !== null && part.relsXml !== null) {
|
|
393
|
+
files[part.relsPath] = strToU8(part.relsXml);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an x14 extension body in the `<ext>` envelope every worksheet- and workbook-level x14
|
|
3
|
+
* extension shares: a `uri` scoping the extension to its feature (a fixed GUID from
|
|
4
|
+
* {@link namespaces.ts}) plus the `xmlns:x14` binding the extension namespace on the element itself —
|
|
5
|
+
* Excel declares it per `<ext>`, never at the part root. Callers needing an `<extLst>` wrapper add it
|
|
6
|
+
* around the result; the producers the worksheet serialiser gathers return this bare so several exts
|
|
7
|
+
* compose under a single shared `<extLst>`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function x14Ext(uri: string, body: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { X14_NS } from "./namespaces.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an x14 extension body in the `<ext>` envelope every worksheet- and workbook-level x14
|
|
4
|
+
* extension shares: a `uri` scoping the extension to its feature (a fixed GUID from
|
|
5
|
+
* {@link namespaces.ts}) plus the `xmlns:x14` binding the extension namespace on the element itself —
|
|
6
|
+
* Excel declares it per `<ext>`, never at the part root. Callers needing an `<extLst>` wrapper add it
|
|
7
|
+
* around the result; the producers the worksheet serialiser gathers return this bare so several exts
|
|
8
|
+
* compose under a single shared `<extLst>`.
|
|
9
|
+
*/
|
|
10
|
+
export function x14Ext(uri, body) {
|
|
11
|
+
return `<ext uri="${uri}" xmlns:x14="${X14_NS}">${body}</ext>`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface CfbStream {
|
|
2
|
+
/** The exact directory-entry name. At most 31 UTF-16 code units ([MS-CFB] name limit). */
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly data: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export interface CfbStorage {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly children: readonly CfbNode[];
|
|
9
|
+
}
|
|
10
|
+
export type CfbNode = CfbStream | CfbStorage;
|
|
11
|
+
/**
|
|
12
|
+
* Encode a hierarchy of storages and streams into a v3 compound file. The Root Entry is synthesized
|
|
13
|
+
* automatically; `root` is its top-level children. Every stream becomes a directory entry reachable both
|
|
14
|
+
* by linear scan and by tree navigation.
|
|
15
|
+
*
|
|
16
|
+
* @throws {VbaAuthorError} if any name is empty or exceeds 31 characters, sibling names collide, or the
|
|
17
|
+
* project is so large it would need more than 109 FAT sectors (~7 MB — far beyond any real project).
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeCompoundFile(root: readonly CfbNode[]): Uint8Array;
|