@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,134 @@
|
|
|
1
|
+
// Reading a pivot table back from its OOXML parts — the inverse of `pivot.ts`.
|
|
2
|
+
//
|
|
3
|
+
// A written pivot round-trips today by byte-preservation: the reader keeps the pivot parts verbatim
|
|
4
|
+
// and re-emits them. That keeps the file faithful, but leaves the pivot opaque to the model — a
|
|
5
|
+
// `.model` copy cannot carry a pivot it cannot see. This module reconstructs the *semantic* shape of
|
|
6
|
+
// a pivot (its source, field roles, value field, and aggregation) from the `pivotTableDefinition`
|
|
7
|
+
// and its `pivotCacheDefinition`, so a loaded pivot becomes inspectable data rather than an opaque
|
|
8
|
+
// blob.
|
|
9
|
+
//
|
|
10
|
+
// Read leniently: these parts describe a file that already exists, so a missing or unrecognised
|
|
11
|
+
// attribute yields a sensible default rather than a throw — the strict invariants belong on the
|
|
12
|
+
// authoring path (`core/pivot-table.ts`), not here. A hostile part therefore degrades to an
|
|
13
|
+
// incomplete model; it never crashes the reader.
|
|
14
|
+
import { pivotMetricFromSubtotal, } from "../../core/pivot-table.js";
|
|
15
|
+
import { localName, parseXml } from "../../xml/xml-read.js";
|
|
16
|
+
/** Reconstruct a pivot's semantic model from its two definition parts. The records part is not
|
|
17
|
+
* consulted: the cache's field catalogue and the table's field roles fully describe the pivot's
|
|
18
|
+
* shape, and the aggregated values are Excel's to compute on refresh. */
|
|
19
|
+
export function parsePivotTable(tableXml, cacheXml) {
|
|
20
|
+
const { fields, source } = parsePivotCacheDefinition(cacheXml);
|
|
21
|
+
const def = parsePivotTableDefinition(tableXml);
|
|
22
|
+
return {
|
|
23
|
+
name: def.name,
|
|
24
|
+
cacheId: def.cacheId,
|
|
25
|
+
source,
|
|
26
|
+
fields,
|
|
27
|
+
rowFields: def.rowFields,
|
|
28
|
+
columnFields: def.columnFields,
|
|
29
|
+
valueField: def.valueField,
|
|
30
|
+
valueFieldName: fields[def.valueField]?.name ?? '',
|
|
31
|
+
valueCaption: def.valueCaption,
|
|
32
|
+
metric: def.metric,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The field catalogue and worksheet source from a `pivotCacheDefinition`. Fields are collected in
|
|
36
|
+
* document order — the order a table's `fld`/`x` indices address them by. */
|
|
37
|
+
function parsePivotCacheDefinition(cacheXml) {
|
|
38
|
+
const fields = [];
|
|
39
|
+
// A worksheet source is the assumed default until proven otherwise: it is what our writer emits and
|
|
40
|
+
// the overwhelmingly common shape, and its `<worksheetSource>` child fills in the coordinates. A
|
|
41
|
+
// `<cacheSource type>` we recognise overrides the kind; an unrecognised one degrades to `unknown`.
|
|
42
|
+
let source = { kind: 'worksheet', sheet: '', ref: '' };
|
|
43
|
+
parseXml(cacheXml, {
|
|
44
|
+
onOpen(name, attrs) {
|
|
45
|
+
const local = localName(name);
|
|
46
|
+
if (local === 'cacheField' && attrs.name !== undefined) {
|
|
47
|
+
fields.push({ name: attrs.name });
|
|
48
|
+
}
|
|
49
|
+
else if (local === 'cacheSource') {
|
|
50
|
+
source = { ...source, kind: sourceKind(attrs.type) };
|
|
51
|
+
}
|
|
52
|
+
else if (local === 'worksheetSource') {
|
|
53
|
+
source = { ...source, sheet: attrs.sheet ?? '', ref: attrs.ref ?? '' };
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return { fields, source };
|
|
58
|
+
}
|
|
59
|
+
/** The layout half of a pivot: its name, cache id, axis field roles, and the single value field.
|
|
60
|
+
* `<field x>` appears identically inside `<rowFields>` and `<colFields>`, so the current container is
|
|
61
|
+
* tracked to route each into the right axis. Only the first `<dataField>` is modeled — the authoring
|
|
62
|
+
* model supports one value field — and any further ones are ignored rather than rejected. */
|
|
63
|
+
function parsePivotTableDefinition(tableXml) {
|
|
64
|
+
let name = '';
|
|
65
|
+
let cacheId = '';
|
|
66
|
+
const rowFields = [];
|
|
67
|
+
const columnFields = [];
|
|
68
|
+
let valueField = -1;
|
|
69
|
+
let valueCaption = '';
|
|
70
|
+
let metric = 'sum';
|
|
71
|
+
let seenDataField = false;
|
|
72
|
+
let axis = null;
|
|
73
|
+
parseXml(tableXml, {
|
|
74
|
+
onOpen(elementName, attrs) {
|
|
75
|
+
switch (localName(elementName)) {
|
|
76
|
+
case 'pivotTableDefinition':
|
|
77
|
+
name = attrs.name ?? '';
|
|
78
|
+
cacheId = attrs.cacheId ?? '';
|
|
79
|
+
break;
|
|
80
|
+
case 'rowFields':
|
|
81
|
+
axis = 'row';
|
|
82
|
+
break;
|
|
83
|
+
case 'colFields':
|
|
84
|
+
axis = 'col';
|
|
85
|
+
break;
|
|
86
|
+
case 'field': {
|
|
87
|
+
if (axis === null)
|
|
88
|
+
break;
|
|
89
|
+
const index = toIndex(attrs.x);
|
|
90
|
+
if (index >= 0)
|
|
91
|
+
(axis === 'row' ? rowFields : columnFields).push(index);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'dataField': {
|
|
95
|
+
if (seenDataField)
|
|
96
|
+
break;
|
|
97
|
+
seenDataField = true;
|
|
98
|
+
valueField = toIndex(attrs.fld);
|
|
99
|
+
valueCaption = attrs.name ?? '';
|
|
100
|
+
metric = pivotMetricFromSubtotal(attrs.subtotal);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
onClose(elementName) {
|
|
106
|
+
const local = localName(elementName);
|
|
107
|
+
if (local === 'rowFields' || local === 'colFields')
|
|
108
|
+
axis = null;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return { name, cacheId, rowFields, columnFields, valueField, valueCaption, metric };
|
|
112
|
+
}
|
|
113
|
+
const SOURCE_KINDS = new Set([
|
|
114
|
+
'worksheet',
|
|
115
|
+
'external',
|
|
116
|
+
'consolidation',
|
|
117
|
+
'scenario',
|
|
118
|
+
]);
|
|
119
|
+
/** Map a `<cacheSource type>` to a known kind. Absent reads as `worksheet` (the spec default and what
|
|
120
|
+
* our writer emits); an unrecognised value reads as `unknown` rather than throwing, keeping the read
|
|
121
|
+
* lenient while still telling a consumer the declared source is not one we model. */
|
|
122
|
+
function sourceKind(type) {
|
|
123
|
+
if (type === undefined)
|
|
124
|
+
return 'worksheet';
|
|
125
|
+
return SOURCE_KINDS.has(type) ? type : 'unknown';
|
|
126
|
+
}
|
|
127
|
+
/** Parse a non-negative field index attribute, or -1 when it is absent or not a whole number — a
|
|
128
|
+
* hostile `x="../etc"` can never become a wild array index this way. */
|
|
129
|
+
function toIndex(value) {
|
|
130
|
+
if (value === undefined)
|
|
131
|
+
return -1;
|
|
132
|
+
const index = Number(value);
|
|
133
|
+
return Number.isInteger(index) && index >= 0 ? index : -1;
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PivotTable } from '../../core/pivot-table.ts';
|
|
2
|
+
/** The `pivotCacheDefinition` part: the source reference and the field catalogue. `r:id="rId1"`
|
|
3
|
+
* names the cache-records part through this cache's own rels part. `refreshOnLoad="1"` tells Excel to
|
|
4
|
+
* rebuild the pivot body from the cache on open, so the layout stays correct without us computing it
|
|
5
|
+
* to the pixel. `recordCount` is the number of source data rows. */
|
|
6
|
+
export declare function pivotCacheDefinitionXml(table: PivotTable): string;
|
|
7
|
+
/** The `pivotCacheRecords` part: one `<r>` per source data row, each cell either an index into an
|
|
8
|
+
* axis field's shared-items catalogue (`<x>`) or an inline value (`<n>`/`<s>`/`<m>`). */
|
|
9
|
+
export declare function pivotCacheRecordsXml(table: PivotTable): string;
|
|
10
|
+
/** The `pivotTableDefinition` part placed on the destination sheet: the field layout that binds the
|
|
11
|
+
* cache (by `cacheId`) to the row/column axes and the summed value field. */
|
|
12
|
+
export declare function pivotTableXml(table: PivotTable, name: string, cacheId: string): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// OOXML rendering for a modeled pivot table: the three parts a pivot needs — the cache definition
|
|
2
|
+
// (field catalogue), the cache records (a copy of the source rows), and the pivot table definition
|
|
3
|
+
// (the layout). The semantic computation lives in `core/pivot-table.ts`; this file only serialises.
|
|
4
|
+
//
|
|
5
|
+
// Every value that reaches an attribute is run through `escapeAttr`, so source strings carrying XML
|
|
6
|
+
// specials (`Smith & Co`, `<West>`, `It's "best"`) become well-formed markup rather than corrupting
|
|
7
|
+
// the package — the whole point of the shared-item escaping this module guarantees.
|
|
8
|
+
import { encodeAddress } from "../../core/address.js";
|
|
9
|
+
import { escapeAttr, XML_DECLARATION } from "../../xml/xml.js";
|
|
10
|
+
import { RELATIONSHIPS_NS } from "../opc/namespaces.js";
|
|
11
|
+
import { SPREADSHEETML_NS } from "./namespaces.js";
|
|
12
|
+
// Excel's default caption prefix for each aggregation ("Sum of Amount", "Average of Amount"). A
|
|
13
|
+
// metric's name is also its `subtotal` value, which is why the record key equals the enum member.
|
|
14
|
+
const METRIC_CAPTIONS = {
|
|
15
|
+
sum: 'Sum',
|
|
16
|
+
count: 'Count',
|
|
17
|
+
countNums: 'Count',
|
|
18
|
+
average: 'Average',
|
|
19
|
+
max: 'Max',
|
|
20
|
+
min: 'Min',
|
|
21
|
+
product: 'Product',
|
|
22
|
+
stdDev: 'StdDev',
|
|
23
|
+
stdDevp: 'StdDevp',
|
|
24
|
+
var: 'Var',
|
|
25
|
+
varp: 'Varp',
|
|
26
|
+
};
|
|
27
|
+
/** The `pivotCacheDefinition` part: the source reference and the field catalogue. `r:id="rId1"`
|
|
28
|
+
* names the cache-records part through this cache's own rels part. `refreshOnLoad="1"` tells Excel to
|
|
29
|
+
* rebuild the pivot body from the cache on open, so the layout stays correct without us computing it
|
|
30
|
+
* to the pixel. `recordCount` is the number of source data rows. */
|
|
31
|
+
export function pivotCacheDefinitionXml(table) {
|
|
32
|
+
const fields = table.cacheFields
|
|
33
|
+
.map((field) => {
|
|
34
|
+
const shared = field.sharedItems;
|
|
35
|
+
if (shared !== null) {
|
|
36
|
+
const items = shared.map(sharedItemXml).join('');
|
|
37
|
+
const blank = field.containsBlank ? ' containsBlank="1"' : '';
|
|
38
|
+
return (`<cacheField name="${escapeAttr(field.name)}" numFmtId="0">` +
|
|
39
|
+
`<sharedItems${blank} count="${shared.length}">${items}</sharedItems>` +
|
|
40
|
+
`</cacheField>`);
|
|
41
|
+
}
|
|
42
|
+
const blank = field.containsBlank ? ' containsBlank="1"' : '';
|
|
43
|
+
const numeric = field.numeric;
|
|
44
|
+
const descriptor = numeric === null
|
|
45
|
+
? `<sharedItems${blank}/>`
|
|
46
|
+
: `<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" ` +
|
|
47
|
+
`containsInteger="${numeric.allInteger ? 1 : 0}"${blank} ` +
|
|
48
|
+
`minValue="${numeric.min}" maxValue="${numeric.max}"/>`;
|
|
49
|
+
return `<cacheField name="${escapeAttr(field.name)}" numFmtId="0">${descriptor}</cacheField>`;
|
|
50
|
+
})
|
|
51
|
+
.join('');
|
|
52
|
+
return (XML_DECLARATION +
|
|
53
|
+
`<pivotCacheDefinition xmlns="${SPREADSHEETML_NS}" xmlns:r="${RELATIONSHIPS_NS}" r:id="rId1" refreshOnLoad="1" ` +
|
|
54
|
+
`refreshedBy="ts-xlsx" createdVersion="8" refreshedVersion="8" minRefreshableVersion="3" ` +
|
|
55
|
+
`recordCount="${table.records.length}">` +
|
|
56
|
+
`<cacheSource type="worksheet">` +
|
|
57
|
+
`<worksheetSource ref="${escapeAttr(table.sourceRef)}" sheet="${escapeAttr(table.sourceSheetName)}"/>` +
|
|
58
|
+
`</cacheSource>` +
|
|
59
|
+
`<cacheFields count="${table.cacheFields.length}">${fields}</cacheFields>` +
|
|
60
|
+
`</pivotCacheDefinition>`);
|
|
61
|
+
}
|
|
62
|
+
/** The `pivotCacheRecords` part: one `<r>` per source data row, each cell either an index into an
|
|
63
|
+
* axis field's shared-items catalogue (`<x>`) or an inline value (`<n>`/`<s>`/`<m>`). */
|
|
64
|
+
export function pivotCacheRecordsXml(table) {
|
|
65
|
+
const rows = table.records
|
|
66
|
+
.map((record) => `<r>${record.map(recordCellXml).join('')}</r>`)
|
|
67
|
+
.join('');
|
|
68
|
+
return (XML_DECLARATION +
|
|
69
|
+
`<pivotCacheRecords xmlns="${SPREADSHEETML_NS}" xmlns:r="${RELATIONSHIPS_NS}" count="${table.records.length}">` +
|
|
70
|
+
rows +
|
|
71
|
+
`</pivotCacheRecords>`);
|
|
72
|
+
}
|
|
73
|
+
/** The `pivotTableDefinition` part placed on the destination sheet: the field layout that binds the
|
|
74
|
+
* cache (by `cacheId`) to the row/column axes and the summed value field. */
|
|
75
|
+
export function pivotTableXml(table, name, cacheId) {
|
|
76
|
+
const rowField = table.rowFields[0];
|
|
77
|
+
const columnField = table.columnFields[0];
|
|
78
|
+
const rowGroups = table.cacheFields[rowField]?.sharedItems?.length ?? 1;
|
|
79
|
+
const columnGroups = table.cacheFields[columnField]?.sharedItems?.length ?? 1;
|
|
80
|
+
// A generous bounding box on the destination sheet: a row-label column plus one column per column
|
|
81
|
+
// group plus a grand-total column; two header rows plus one row per row group plus a grand total.
|
|
82
|
+
// Excel recomputes the exact extent from the cache on refresh, so this only has to be valid.
|
|
83
|
+
const location = `A1:${encodeAddress(2 + columnGroups, 3 + rowGroups)}`;
|
|
84
|
+
const pivotFields = table.cacheFields
|
|
85
|
+
.map((field, index) => {
|
|
86
|
+
if (table.rowFields.includes(index) || table.columnFields.includes(index)) {
|
|
87
|
+
const axis = table.rowFields.includes(index) ? 'axisRow' : 'axisCol';
|
|
88
|
+
const items = field.sharedItems ?? [];
|
|
89
|
+
const entries = `${items.map((_item, i) => `<item x="${i}"/>`).join('')}<item t="default"/>`;
|
|
90
|
+
return `<pivotField axis="${axis}" showAll="0"><items count="${items.length + 1}">${entries}</items></pivotField>`;
|
|
91
|
+
}
|
|
92
|
+
if (index === table.valueField)
|
|
93
|
+
return '<pivotField dataField="1" showAll="0"/>';
|
|
94
|
+
return '<pivotField showAll="0"/>';
|
|
95
|
+
})
|
|
96
|
+
.join('');
|
|
97
|
+
const rowFields = table.rowFields.map((index) => `<field x="${index}"/>`).join('');
|
|
98
|
+
const columnFields = table.columnFields.map((index) => `<field x="${index}"/>`).join('');
|
|
99
|
+
return (XML_DECLARATION +
|
|
100
|
+
`<pivotTableDefinition xmlns="${SPREADSHEETML_NS}" xmlns:r="${RELATIONSHIPS_NS}" name="${escapeAttr(name)}" ` +
|
|
101
|
+
`cacheId="${escapeAttr(cacheId)}" applyNumberFormats="0" applyBorderFormats="0" ` +
|
|
102
|
+
`applyFontFormats="0" applyPatternFormats="0" applyAlignmentFormats="0" ` +
|
|
103
|
+
`applyWidthHeightFormats="1" dataCaption="Values" updatedVersion="8" minRefreshableVersion="3" ` +
|
|
104
|
+
`useAutoFormatting="1" itemPrintTitles="1" createdVersion="8" indent="0" outline="1" ` +
|
|
105
|
+
`outlineData="1" multipleFieldFilters="0">` +
|
|
106
|
+
`<location ref="${location}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>` +
|
|
107
|
+
`<pivotFields count="${table.cacheFields.length}">${pivotFields}</pivotFields>` +
|
|
108
|
+
`<rowFields count="${table.rowFields.length}">${rowFields}</rowFields>` +
|
|
109
|
+
`<rowItems count="1"><i t="grand"><x/></i></rowItems>` +
|
|
110
|
+
`<colFields count="${table.columnFields.length}">${columnFields}</colFields>` +
|
|
111
|
+
`<colItems count="1"><i t="grand"><x/></i></colItems>` +
|
|
112
|
+
`<dataFields count="1">` +
|
|
113
|
+
dataFieldXml(table) +
|
|
114
|
+
`</dataFields>` +
|
|
115
|
+
`<pivotTableStyleInfo name="PivotStyleLight16" showRowHeaders="1" showColHeaders="1" ` +
|
|
116
|
+
`showRowStripes="0" showColStripes="0" showLastColumn="1"/>` +
|
|
117
|
+
`</pivotTableDefinition>`);
|
|
118
|
+
}
|
|
119
|
+
/** The `<dataField>` that names the aggregated column and selects its function. `sum` is Excel's
|
|
120
|
+
* implicit default, so its `subtotal` attribute is omitted; every other metric names itself. */
|
|
121
|
+
function dataFieldXml(table) {
|
|
122
|
+
const caption = `${METRIC_CAPTIONS[table.metric]} of ${table.valueFieldName}`;
|
|
123
|
+
const subtotal = table.metric === 'sum' ? '' : ` subtotal="${table.metric}"`;
|
|
124
|
+
return (`<dataField name="${escapeAttr(caption)}" fld="${table.valueField}"${subtotal} ` +
|
|
125
|
+
`baseField="0" baseItem="0"/>`);
|
|
126
|
+
}
|
|
127
|
+
function sharedItemXml(item) {
|
|
128
|
+
switch (item.kind) {
|
|
129
|
+
case 'string':
|
|
130
|
+
return `<s v="${escapeAttr(item.value)}"/>`;
|
|
131
|
+
case 'number':
|
|
132
|
+
return `<n v="${item.value}"/>`;
|
|
133
|
+
case 'blank':
|
|
134
|
+
return '<m/>';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function recordCellXml(cell) {
|
|
138
|
+
if (cell.kind === 'index')
|
|
139
|
+
return `<x v="${cell.index}"/>`;
|
|
140
|
+
return sharedItemXml(cell);
|
|
141
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CellValue } from '../../core/value.ts';
|
|
2
|
+
import { type ReadXlsxOptions, type XfStyle } from './read.ts';
|
|
3
|
+
export interface ReadSheetRowsOptions extends ReadXlsxOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Which worksheet to stream: its name, or its 1-based position in the workbook. Defaults to the
|
|
6
|
+
* first sheet.
|
|
7
|
+
*/
|
|
8
|
+
readonly sheet?: string | number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The resolved style facets of a streamed cell — its own `<c s>` cell format, flattened exactly as
|
|
12
|
+
* the buffered reader resolves it. Present only when the cell carries a format; a consumer can copy
|
|
13
|
+
* these straight onto a writer cell to preserve its look through a streaming read→write.
|
|
14
|
+
*/
|
|
15
|
+
export type StreamedCellStyle = XfStyle;
|
|
16
|
+
/** One non-empty cell in a {@link StreamedRow}. */
|
|
17
|
+
export interface StreamedCell {
|
|
18
|
+
/** 1-based column index. */
|
|
19
|
+
readonly col: number;
|
|
20
|
+
/** Canonical A1 address (`"B3"`). */
|
|
21
|
+
readonly address: string;
|
|
22
|
+
/** The decoded value — identical to what `readXlsx` would produce for the same cell. */
|
|
23
|
+
readonly value: CellValue;
|
|
24
|
+
/** The cell's resolved style facets, or absent when the cell carries no format of its own. */
|
|
25
|
+
readonly style?: StreamedCellStyle;
|
|
26
|
+
}
|
|
27
|
+
/** One worksheet row, as yielded by {@link readSheetRows} / {@link StreamedSheet.rows}. */
|
|
28
|
+
export interface StreamedRow {
|
|
29
|
+
/** 1-based row index. */
|
|
30
|
+
readonly number: number;
|
|
31
|
+
/** Whether the row declares itself hidden. */
|
|
32
|
+
readonly hidden: boolean;
|
|
33
|
+
/** The row's non-empty cells, in column order. An empty (or purely style-only) row yields none. */
|
|
34
|
+
readonly cells: readonly StreamedCell[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One worksheet, as yielded by {@link readWorkbookStream}. The sheet's {@link rows} stream one at a
|
|
38
|
+
* time; its {@link hiddenColumns} and {@link merges} are populated by that same single pass.
|
|
39
|
+
*
|
|
40
|
+
* The two summaries are resolved lazily: reading either accessor drives a full scan of the sheet if
|
|
41
|
+
* its rows have not already been consumed, so their order relative to `rows()` never matters. (When
|
|
42
|
+
* rows *are* consumed first — the streaming idiom — the accessors reuse that pass and re-scan
|
|
43
|
+
* nothing.)
|
|
44
|
+
*/
|
|
45
|
+
export interface StreamedSheet {
|
|
46
|
+
/** The worksheet's declared name, joined from the workbook part — never a positional placeholder. */
|
|
47
|
+
readonly name: string;
|
|
48
|
+
/** Stream this sheet's rows, one at a time, in sheet order. */
|
|
49
|
+
rows(): Generator<StreamedRow>;
|
|
50
|
+
/** 1-based indices of columns the sheet declares hidden, ascending. */
|
|
51
|
+
readonly hiddenColumns: readonly number[];
|
|
52
|
+
/** The sheet's merged ranges, as canonical A1 range strings, in declaration order. */
|
|
53
|
+
readonly merges: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stream a worksheet's rows from an `.xlsx` package, yielding each in sheet order without building
|
|
57
|
+
* the workbook model. Only rows the sheet actually declares are yielded, and within a row only its
|
|
58
|
+
* non-empty cells — a blank or style-only cell contributes nothing, matching the intent of a data
|
|
59
|
+
* read.
|
|
60
|
+
*
|
|
61
|
+
* @param data The raw `.xlsx` bytes.
|
|
62
|
+
* @param options Sheet selector and the inflate bound (see {@link ReadSheetRowsOptions}).
|
|
63
|
+
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
64
|
+
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
65
|
+
* @throws {@link PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
66
|
+
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
67
|
+
* @throws {@link XlsxParseError} if the package's workbook part declares no worksheets.
|
|
68
|
+
* @throws {RangeError} / {@link AuthoringError} if `options.sheet` selects a position, or a name,
|
|
69
|
+
* that no worksheet has.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readSheetRows(data: Uint8Array, options?: ReadSheetRowsOptions): Generator<StreamedRow>;
|
|
72
|
+
/**
|
|
73
|
+
* Stream every worksheet of an `.xlsx` package in workbook order, without building the workbook
|
|
74
|
+
* model. Each yielded {@link StreamedSheet} carries the declared sheet name and lets the caller
|
|
75
|
+
* stream that sheet's rows and read its hidden-column and merge summaries — the streaming analogue
|
|
76
|
+
* of walking `readXlsx(data).worksheets`.
|
|
77
|
+
*
|
|
78
|
+
* @param data The raw `.xlsx` bytes.
|
|
79
|
+
* @param options The inflate bound (see {@link ReadXlsxOptions}).
|
|
80
|
+
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
81
|
+
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
82
|
+
* @throws {@link PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
83
|
+
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
84
|
+
*/
|
|
85
|
+
export declare function readWorkbookStream(data: Uint8Array, options?: ReadXlsxOptions): Generator<StreamedSheet>;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Streaming row reader: yield a worksheet's rows one at a time, without ever building the whole
|
|
2
|
+
// {@link Workbook} model.
|
|
3
|
+
//
|
|
4
|
+
// `readXlsx` materialises every cell of every sheet as a live `Cell` object held in nested Maps —
|
|
5
|
+
// fine for editing, but for a large sheet read purely to extract its data it holds the entire grid
|
|
6
|
+
// in memory at once. This reader instead *pulls* the sheet's XML through `xmlEvents` and yields a
|
|
7
|
+
// plain {@link StreamedRow} at each `</row>`, retaining only the row currently in hand. Peak model
|
|
8
|
+
// memory is one row, not the sheet.
|
|
9
|
+
//
|
|
10
|
+
// Two entry points sit on the same scanner:
|
|
11
|
+
// - {@link readSheetRows} streams a single selected sheet's rows (the terse data-extraction case).
|
|
12
|
+
// - {@link readWorkbookStream} yields a {@link StreamedSheet} per worksheet in workbook order, so a
|
|
13
|
+
// caller can walk every sheet — each sheet's rows still stream one at a time.
|
|
14
|
+
//
|
|
15
|
+
// Scope of this slice: the package is still inflated whole (bounded by the running counter in
|
|
16
|
+
// `./inflate.ts`) and shared strings / styles are read as whole parts — both are legitimately
|
|
17
|
+
// document-sized and cheap. What this avoids is retaining N materialised cells. A later slice can
|
|
18
|
+
// make the inflate itself per-part lazy; the pull primitive this stands on (`xmlEvents`) is the
|
|
19
|
+
// same one that path will use.
|
|
20
|
+
import { MAX_COLUMN } from "../../core/address.js";
|
|
21
|
+
import { AuthoringError } from "../../errors.js";
|
|
22
|
+
import { boolStrict, closeEmptyElements, localName, xmlEvents } from "../../xml/xml-read.js";
|
|
23
|
+
import { packageAccessors } from "../opc/read-opc.js";
|
|
24
|
+
import { inflateSpreadsheetPackage, unsupportedWorkbookPart } from "../opc/sniff-format.js";
|
|
25
|
+
import { CellAccumulator } from "./cell-accumulator.js";
|
|
26
|
+
import { XlsxParseError } from "./errors.js";
|
|
27
|
+
import { DEFAULT_MAX_UNCOMPRESSED, parseRelationships, parseStyleTable, parseWorkbookSheets, resolveWorkbookPart, } from "./read.js";
|
|
28
|
+
import { parseSharedStrings } from "./shared-strings-read.js";
|
|
29
|
+
/**
|
|
30
|
+
* Stream a worksheet's rows from an `.xlsx` package, yielding each in sheet order without building
|
|
31
|
+
* the workbook model. Only rows the sheet actually declares are yielded, and within a row only its
|
|
32
|
+
* non-empty cells — a blank or style-only cell contributes nothing, matching the intent of a data
|
|
33
|
+
* read.
|
|
34
|
+
*
|
|
35
|
+
* @param data The raw `.xlsx` bytes.
|
|
36
|
+
* @param options Sheet selector and the inflate bound (see {@link ReadSheetRowsOptions}).
|
|
37
|
+
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
38
|
+
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
39
|
+
* @throws {@link PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
40
|
+
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
41
|
+
* @throws {@link XlsxParseError} if the package's workbook part declares no worksheets.
|
|
42
|
+
* @throws {RangeError} / {@link AuthoringError} if `options.sheet` selects a position, or a name,
|
|
43
|
+
* that no worksheet has.
|
|
44
|
+
*/
|
|
45
|
+
export function* readSheetRows(data, options = {}) {
|
|
46
|
+
const pkg = openPackage(data, options.maxUncompressedBytes);
|
|
47
|
+
const chosen = pickSheet(pkg.sheets, options.sheet);
|
|
48
|
+
const sheetXml = pkg.sheetXml(chosen.relId);
|
|
49
|
+
// The sheet is named but its part is missing (a truncated or foreign package) — it has no rows.
|
|
50
|
+
if (sheetXml === undefined)
|
|
51
|
+
return;
|
|
52
|
+
yield* scanSheet(sheetXml, pkg.sharedStrings, pkg.xfStyles, new Set(), []);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stream every worksheet of an `.xlsx` package in workbook order, without building the workbook
|
|
56
|
+
* model. Each yielded {@link StreamedSheet} carries the declared sheet name and lets the caller
|
|
57
|
+
* stream that sheet's rows and read its hidden-column and merge summaries — the streaming analogue
|
|
58
|
+
* of walking `readXlsx(data).worksheets`.
|
|
59
|
+
*
|
|
60
|
+
* @param data The raw `.xlsx` bytes.
|
|
61
|
+
* @param options The inflate bound (see {@link ReadXlsxOptions}).
|
|
62
|
+
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
63
|
+
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
64
|
+
* @throws {@link PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
65
|
+
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
66
|
+
*/
|
|
67
|
+
export function* readWorkbookStream(data, options = {}) {
|
|
68
|
+
const pkg = openPackage(data, options.maxUncompressedBytes);
|
|
69
|
+
for (const sheet of pkg.sheets) {
|
|
70
|
+
// A named sheet whose part is missing (truncated/foreign package) still surfaces — with no rows,
|
|
71
|
+
// no hidden columns, and no merges — rather than vanishing from the workbook's sheet list.
|
|
72
|
+
const xml = pkg.sheetXml(sheet.relId) ?? '';
|
|
73
|
+
yield new StreamedSheetReader(sheet.name, xml, pkg.sharedStrings, pkg.xfStyles);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function openPackage(data, maxUncompressedBytes) {
|
|
77
|
+
const cap = maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
|
|
78
|
+
const { partText: text } = packageAccessors(inflateSpreadsheetPackage(data, cap));
|
|
79
|
+
const workbookXml = text('xl/workbook.xml');
|
|
80
|
+
// A binary `.xlsb` is a workbook this library *can* read — just not through here. Row streaming is
|
|
81
|
+
// built on the XML worksheet parser, so the binary cell table has no streaming path yet; say so,
|
|
82
|
+
// rather than reporting the format as unreadable when `readXlsx` would take the very same bytes.
|
|
83
|
+
if (workbookXml === undefined) {
|
|
84
|
+
throw unsupportedWorkbookPart(text, 'the binary .xlsb format (BIFF12) cannot be row-streamed yet; read it with readXlsx or readXlsb');
|
|
85
|
+
}
|
|
86
|
+
const sheets = parseWorkbookSheets(workbookXml);
|
|
87
|
+
const rels = parseRelationships(text('xl/_rels/workbook.xml.rels') ?? '');
|
|
88
|
+
const sharedStrings = parseSharedStrings(text('xl/sharedStrings.xml') ?? '');
|
|
89
|
+
const { cellXfs: xfStyles } = parseStyleTable(text('xl/styles.xml') ?? '');
|
|
90
|
+
return {
|
|
91
|
+
sheets,
|
|
92
|
+
sharedStrings,
|
|
93
|
+
xfStyles,
|
|
94
|
+
sheetXml(relId) {
|
|
95
|
+
const target = rels.get(relId);
|
|
96
|
+
const path = target === undefined ? undefined : resolveWorkbookPart(target);
|
|
97
|
+
return path === undefined ? undefined : text(path);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function pickSheet(sheets, selector) {
|
|
102
|
+
const first = sheets[0];
|
|
103
|
+
if (first === undefined)
|
|
104
|
+
throw new XlsxParseError('workbook names no worksheets');
|
|
105
|
+
if (selector === undefined)
|
|
106
|
+
return first;
|
|
107
|
+
if (typeof selector === 'number') {
|
|
108
|
+
const sheet = sheets[selector - 1];
|
|
109
|
+
if (sheet === undefined)
|
|
110
|
+
throw new RangeError(`no worksheet at position ${selector}`);
|
|
111
|
+
return sheet;
|
|
112
|
+
}
|
|
113
|
+
const sheet = sheets.find((candidate) => candidate.name === selector);
|
|
114
|
+
if (sheet === undefined)
|
|
115
|
+
throw new AuthoringError(`no worksheet named ${JSON.stringify(selector)}`);
|
|
116
|
+
return sheet;
|
|
117
|
+
}
|
|
118
|
+
// A single worksheet exposed by readWorkbookStream. Its rows() re-scans on each call (a fresh pass,
|
|
119
|
+
// so it is safely re-iterable); the hidden-column and merge accessors reuse a completed scan or, if
|
|
120
|
+
// the rows were never drained, drive one of their own. The hidden/merge state is filled in by the
|
|
121
|
+
// same scanSheet pass that yields the rows.
|
|
122
|
+
class StreamedSheetReader {
|
|
123
|
+
name;
|
|
124
|
+
#xml;
|
|
125
|
+
#sharedStrings;
|
|
126
|
+
#xfStyles;
|
|
127
|
+
#hiddenColumns = new Set();
|
|
128
|
+
#merges = [];
|
|
129
|
+
#scanned = false;
|
|
130
|
+
constructor(name, xml, sharedStrings, xfStyles) {
|
|
131
|
+
this.name = name;
|
|
132
|
+
this.#xml = xml;
|
|
133
|
+
this.#sharedStrings = sharedStrings;
|
|
134
|
+
this.#xfStyles = xfStyles;
|
|
135
|
+
}
|
|
136
|
+
*rows() {
|
|
137
|
+
this.#hiddenColumns = new Set();
|
|
138
|
+
this.#merges = [];
|
|
139
|
+
this.#scanned = false;
|
|
140
|
+
yield* scanSheet(this.#xml, this.#sharedStrings, this.#xfStyles, this.#hiddenColumns, this.#merges);
|
|
141
|
+
this.#scanned = true;
|
|
142
|
+
}
|
|
143
|
+
get hiddenColumns() {
|
|
144
|
+
this.#ensureScanned();
|
|
145
|
+
return [...this.#hiddenColumns].sort((a, b) => a - b);
|
|
146
|
+
}
|
|
147
|
+
get merges() {
|
|
148
|
+
this.#ensureScanned();
|
|
149
|
+
return this.#merges;
|
|
150
|
+
}
|
|
151
|
+
// Drain a scan purely for its summaries when the caller reads them without (or before) iterating
|
|
152
|
+
// rows. A completed row iteration already set #scanned, so this re-scans nothing in the common
|
|
153
|
+
// streaming idiom.
|
|
154
|
+
#ensureScanned() {
|
|
155
|
+
if (this.#scanned)
|
|
156
|
+
return;
|
|
157
|
+
for (const _row of this.rows()) {
|
|
158
|
+
// The rows themselves are irrelevant here — we only want the hidden/merge side effects.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// A formatted-but-empty `<c/>` is expanded to open+close so it finalises once on close, matching
|
|
163
|
+
// the buffered reader; the text-bearing `<f/>`/`<v/>`/`<t/>` are excluded so an empty one never
|
|
164
|
+
// commits (their close captures text, which an empty tag has none of).
|
|
165
|
+
const CELL_EMPTY_CLOSE = new Set(['c']);
|
|
166
|
+
// Pull the sheet XML through the event stream, yielding a StreamedRow at each `</row>`, while
|
|
167
|
+
// recording the sheet's hidden columns (from `<col hidden>`, before <sheetData>) and merged ranges
|
|
168
|
+
// (from `<mergeCells>`, after <sheetData>) into the caller-supplied collectors. The cell state
|
|
169
|
+
// mirrors the buffered reader's `parseWorksheet` (same self-closing-`<c/>` handling, same capture
|
|
170
|
+
// flags), but commits into a row buffer that is handed off and discarded per row rather than into a
|
|
171
|
+
// persistent Worksheet — that hand-off is what bounds retained memory to one row.
|
|
172
|
+
function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
|
|
173
|
+
let rowNumber = 0;
|
|
174
|
+
let lastRow = 0;
|
|
175
|
+
let rowHidden = false;
|
|
176
|
+
let cells = [];
|
|
177
|
+
// The in-flight `<c>`, gathered exactly as the buffered reader gathers it. This reader drives the
|
|
178
|
+
// same beginCell/setFormula/setValue/appendText methods, then takes only the cell's plain decoded
|
|
179
|
+
// value (via decode) — never the shared-formula / data-table resolution the buffered finalize adds,
|
|
180
|
+
// which a data read does not want. Rich `<r>` runs are deliberately not opened here, so a rich
|
|
181
|
+
// inline string flattens to its concatenated text as a streamed value always has.
|
|
182
|
+
const cell = new CellAccumulator();
|
|
183
|
+
let inInlineString = false;
|
|
184
|
+
let capture = false;
|
|
185
|
+
let text = '';
|
|
186
|
+
const finalizeCell = () => {
|
|
187
|
+
if (cell.ref === '' || cell.col < 0)
|
|
188
|
+
return;
|
|
189
|
+
const style = cell.styleIndex >= 0 ? xfStyles[cell.styleIndex] : undefined;
|
|
190
|
+
const value = cell.decode(sharedStrings, style);
|
|
191
|
+
// A blank or purely style-only cell decodes to null; a data read wants only cells that carry
|
|
192
|
+
// something (a formula object, an empty string, a false, and a 0 all count — only null drops).
|
|
193
|
+
if (value !== null) {
|
|
194
|
+
const { col, ref } = cell;
|
|
195
|
+
cells.push(style ? { col, address: ref, value, style } : { col, address: ref, value });
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
for (const event of closeEmptyElements(xmlEvents(xml), CELL_EMPTY_CLOSE)) {
|
|
199
|
+
if (event.kind === 'text') {
|
|
200
|
+
if (capture)
|
|
201
|
+
text += event.text;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (event.kind === 'open') {
|
|
205
|
+
const local = localName(event.name);
|
|
206
|
+
text = '';
|
|
207
|
+
capture = false;
|
|
208
|
+
switch (local) {
|
|
209
|
+
case 'row': {
|
|
210
|
+
const declared = Number(event.attrs.r);
|
|
211
|
+
rowNumber = Number.isInteger(declared) && declared >= 1 ? declared : lastRow + 1;
|
|
212
|
+
lastRow = rowNumber;
|
|
213
|
+
rowHidden = boolStrict(event.attrs.hidden);
|
|
214
|
+
cells = [];
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case 'col':
|
|
218
|
+
collectHiddenColumn(event.attrs, hiddenColumns);
|
|
219
|
+
break;
|
|
220
|
+
case 'mergeCell':
|
|
221
|
+
if (event.attrs.ref !== undefined)
|
|
222
|
+
merges.push(event.attrs.ref);
|
|
223
|
+
break;
|
|
224
|
+
case 'c':
|
|
225
|
+
cell.beginCell(event.attrs);
|
|
226
|
+
break;
|
|
227
|
+
case 'is':
|
|
228
|
+
inInlineString = true;
|
|
229
|
+
cell.beginInlineString();
|
|
230
|
+
break;
|
|
231
|
+
case 'f':
|
|
232
|
+
capture = true;
|
|
233
|
+
cell.beginFormula(event.attrs, event.selfClosing);
|
|
234
|
+
break;
|
|
235
|
+
case 'v':
|
|
236
|
+
case 't':
|
|
237
|
+
capture = true;
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (event.selfClosing && (local === 'f' || local === 'v'))
|
|
243
|
+
capture = false;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// close
|
|
247
|
+
const local = localName(event.name);
|
|
248
|
+
switch (local) {
|
|
249
|
+
case 'f':
|
|
250
|
+
cell.setFormula(text);
|
|
251
|
+
break;
|
|
252
|
+
case 'v':
|
|
253
|
+
cell.setValue(text);
|
|
254
|
+
break;
|
|
255
|
+
case 't':
|
|
256
|
+
cell.appendText(text, inInlineString);
|
|
257
|
+
break;
|
|
258
|
+
case 'is':
|
|
259
|
+
inInlineString = false;
|
|
260
|
+
break;
|
|
261
|
+
case 'c':
|
|
262
|
+
finalizeCell();
|
|
263
|
+
break;
|
|
264
|
+
case 'row':
|
|
265
|
+
yield { number: rowNumber, hidden: rowHidden, cells };
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
capture = false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Record the hidden columns a `<col min max hidden>` element declares. The span is clamped to the
|
|
274
|
+
// format's column ceiling and gathered into a Set, so even a hostile file full of full-width hidden
|
|
275
|
+
// spans can add at most MAX_COLUMN distinct entries — never an unbounded allocation.
|
|
276
|
+
function collectHiddenColumn(attrs, hiddenColumns) {
|
|
277
|
+
if (attrs.hidden !== '1' && attrs.hidden !== 'true')
|
|
278
|
+
return;
|
|
279
|
+
const min = Number(attrs.min);
|
|
280
|
+
const max = Number(attrs.max);
|
|
281
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1)
|
|
282
|
+
return;
|
|
283
|
+
const last = Math.min(max, MAX_COLUMN);
|
|
284
|
+
for (let index = min; index <= last; index++)
|
|
285
|
+
hiddenColumns.add(index);
|
|
286
|
+
}
|