@shbernal/ts-xlsx 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/io/opc/read-opc.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
// The OPC (Open Packaging Conventions) layer of the reader: resolving relationship targets to part
|
|
2
|
-
// paths, reading a part's `.rels`, resolving a part's declared content type, and walking the transitive
|
|
3
|
-
// closure of parts a preserved reference reaches. Every helper here is pure over the inflated package —
|
|
4
|
-
// it takes part text/bytes accessors and returns paths or records, touching no Workbook model.
|
|
5
1
|
import { strFromU8 } from 'fflate';
|
|
6
2
|
import { openElements } from '../../xml/xml-read.js';
|
|
7
3
|
import { extensionOf, relsPathFor } from './part-paths.js';
|
|
8
|
-
// Bind the part-lookup accessors over an inflated package (a part-path → bytes map).
|
|
9
4
|
export function packageAccessors(files) {
|
|
10
5
|
return {
|
|
11
6
|
partText(path) {
|
|
@@ -15,20 +10,12 @@ export function packageAccessors(files) {
|
|
|
15
10
|
partBytes: (path) => files[path],
|
|
16
11
|
};
|
|
17
12
|
}
|
|
18
|
-
// Whether a <Relationship> carries a resolvable Target and its Type ends with `/<suffix>` (a
|
|
19
|
-
// local-name match, so a namespaced or oddly-cased type still resolves). The type guard lets a
|
|
20
|
-
// matching relationship's Target be read without a further presence check.
|
|
21
13
|
function matchesType(attrs, suffix) {
|
|
22
14
|
return (attrs.Type !== undefined && attrs.Target !== undefined && attrs.Type.endsWith(`/${suffix}`));
|
|
23
15
|
}
|
|
24
|
-
// The Target of the first relationship whose Type ends with `/<suffix>`, or undefined when none is
|
|
25
|
-
// declared — for a single expected reference, where the plural form below would over-gather.
|
|
26
16
|
export function relationshipTargetByType(xml, suffix) {
|
|
27
17
|
return relationshipTargetsByType(xml, suffix)[0];
|
|
28
18
|
}
|
|
29
|
-
// Every Target whose Type ends with `/<suffix>`, in declaration order — for a part class a sheet may
|
|
30
|
-
// reference more than once (a sheet can own several tables), where the singular helper's first-match
|
|
31
|
-
// would miss all but one.
|
|
32
19
|
export function relationshipTargetsByType(xml, suffix) {
|
|
33
20
|
const targets = [];
|
|
34
21
|
for (const { attrs } of openElements(xml, 'Relationship')) {
|
|
@@ -37,10 +24,6 @@ export function relationshipTargetsByType(xml, suffix) {
|
|
|
37
24
|
}
|
|
38
25
|
return targets;
|
|
39
26
|
}
|
|
40
|
-
// The package part a sheet reaches through the first relationship of a given type, already resolved
|
|
41
|
-
// relative to the sheet — the "load the sheet's rels, find the one relationship of this type, resolve
|
|
42
|
-
// its target" preamble every single-part sheet lookup (notes, printer settings, drawing, background)
|
|
43
|
-
// opens with. undefined when the sheet has no rels part or declares no such relationship.
|
|
44
27
|
export function sheetRelTarget(sheetPath, partText, type) {
|
|
45
28
|
const relsXml = partText(relsPathFor(sheetPath));
|
|
46
29
|
if (relsXml === undefined)
|
|
@@ -48,8 +31,6 @@ export function sheetRelTarget(sheetPath, partText, type) {
|
|
|
48
31
|
const target = relationshipTargetByType(relsXml, type);
|
|
49
32
|
return target === undefined ? undefined : resolveRelativePart(sheetPath, target);
|
|
50
33
|
}
|
|
51
|
-
// Resolve a relationship target (relative to the referencing part's directory, or absolute from the
|
|
52
|
-
// package root) into a package part path, collapsing `.`/`..` segments.
|
|
53
34
|
export function resolveRelativePart(basePart, target) {
|
|
54
35
|
if (target.startsWith('/'))
|
|
55
36
|
return target.slice(1);
|
|
@@ -65,8 +46,6 @@ export function resolveRelativePart(basePart, target) {
|
|
|
65
46
|
}
|
|
66
47
|
return out.join('/');
|
|
67
48
|
}
|
|
68
|
-
// A workbook relationship target is relative to the `xl/` directory (`worksheets/sheet1.xml`)
|
|
69
|
-
// or absolute from the package root (`/xl/worksheets/sheet1.xml`); normalise both to a part path.
|
|
70
49
|
export function resolveWorkbookPart(target) {
|
|
71
50
|
if (target.startsWith('/'))
|
|
72
51
|
return target.slice(1);
|
|
@@ -95,9 +74,6 @@ export function parseRelationshipRecords(xml) {
|
|
|
95
74
|
}
|
|
96
75
|
return records;
|
|
97
76
|
}
|
|
98
|
-
// Resolve a package part path to its declared content type the way OPC does: an `<Override>` naming
|
|
99
|
-
// the exact part wins, else the `<Default>` registered for the part's extension. An unknown part
|
|
100
|
-
// falls back to the generic binary type so re-declaring it never emits an empty content type.
|
|
101
77
|
export function contentTypeResolver(contentTypesXml) {
|
|
102
78
|
const overrides = new Map();
|
|
103
79
|
const defaults = new Map();
|
|
@@ -115,11 +91,6 @@ export function contentTypeResolver(contentTypesXml) {
|
|
|
115
91
|
defaults.get(extensionOf(path).toLowerCase()) ??
|
|
116
92
|
'application/octet-stream';
|
|
117
93
|
}
|
|
118
|
-
// Gather the transitive closure of package parts reachable from an entry part — the part itself, then
|
|
119
|
-
// every internal part its relationships target, breadth-first — each with its raw bytes, content type,
|
|
120
|
-
// and (internal) relationships. Returns undefined when the entry part is absent (a dangling reference
|
|
121
|
-
// preserves nothing). A `visited` set dedupes shared parts and bounds the walk to the (finite,
|
|
122
|
-
// inflate-capped) package, so a maliciously self-referential rels graph cannot loop.
|
|
123
94
|
export function capturePartClosure(entryPath, partText, partBytes, contentTypeOf) {
|
|
124
95
|
const parts = [];
|
|
125
96
|
const visited = new Set();
|
|
@@ -137,8 +108,6 @@ export function capturePartClosure(entryPath, partText, partBytes, contentTypeOf
|
|
|
137
108
|
if (relsXml !== undefined) {
|
|
138
109
|
for (const rel of parseRelationshipRecords(relsXml)) {
|
|
139
110
|
if (rel.external) {
|
|
140
|
-
// A linked workbook lives outside the package: keep the wiring verbatim (an externalLink
|
|
141
|
-
// part's pointer to its source), but do not walk into it — there is no package part to visit.
|
|
142
111
|
rels.push({ id: rel.id, type: rel.type, targetPath: rel.target, external: true });
|
|
143
112
|
continue;
|
|
144
113
|
}
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
// The one knob every reader takes, declared apart from any of them.
|
|
2
|
-
//
|
|
3
|
-
// `readXlsx`, `readXlsb`, and the row streamer all inflate the same OPC container under the same
|
|
4
|
-
// bound, so the option bag belongs to none of them individually — and a reader that dispatches to
|
|
5
|
-
// another (the `.xlsx` entry point hands a binary package to the `.xlsb` codec) cannot import it from
|
|
6
|
-
// its peer without the two modules importing each other.
|
|
7
1
|
export const DEFAULT_MAX_UNCOMPRESSED = 512 * 1024 * 1024;
|
package/dist/io/opc/rels.js
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
|
-
// The write side of the relationship graph: a single `<Relationship>` element and the `.rels` part
|
|
2
|
-
// envelope that wraps a set of them. Pure OPC — a `.rels` part has the same shape whatever the parts
|
|
3
|
-
// it wires together are spelled in, so this is stated once here rather than inside a codec.
|
|
4
|
-
//
|
|
5
|
-
// The reader's half of the same graph (resolving targets, walking a part closure) is in `read-opc.ts`.
|
|
6
1
|
import { escapeAttr, XML_DECLARATION } from '../../xml/xml.js';
|
|
7
2
|
import { PKG_RELS_NS } from './namespaces.js';
|
|
8
|
-
// A single `<Relationship>`. An `external` target lives outside the package (a hyperlink URL), so the
|
|
9
|
-
// element carries `TargetMode="External"`; a package-internal target (the default) omits it. The caller
|
|
10
|
-
// escapes the target when it is not a writer-controlled package path.
|
|
11
3
|
export function relationship(id, type, target, options) {
|
|
12
4
|
const mode = options?.external ? ' TargetMode="External"' : '';
|
|
13
5
|
return `<Relationship Id="${id}" Type="${type}" Target="${target}"${mode}/>`;
|
|
14
6
|
}
|
|
15
|
-
// Wrap a part's `<Relationship>` elements in the OPC `.rels` envelope (XML declaration + the namespaced
|
|
16
|
-
// `<Relationships>` root). Every `.rels` part the writer emits shares this envelope; only the elements
|
|
17
|
-
// inside differ, so each caller builds its own list of {@link relationship} strings and hands them here.
|
|
18
7
|
export function relationshipsPart(relationships) {
|
|
19
8
|
return `${XML_DECLARATION}<Relationships xmlns="${PKG_RELS_NS}">${relationships.join('')}</Relationships>`;
|
|
20
9
|
}
|
|
21
10
|
export function preservedRelsXml(rels) {
|
|
22
11
|
return relationshipsPart(rels.map((rel) => relationship(rel.id, rel.type, escapeAttr(rel.target), rel.external ? { external: true } : {})));
|
|
23
12
|
}
|
|
24
|
-
// A `.rels` part for a generated part chain (pivot table → cache definition → cache records). Targets
|
|
25
|
-
// are writer-controlled package paths, so no attribute escaping is needed.
|
|
26
13
|
export function relsPartXml(rels) {
|
|
27
14
|
return relationshipsPart(rels.map((rel) => relationship(rel.id, rel.type, rel.target)));
|
|
28
15
|
}
|
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
// Front of the read pipe: format detection, the inflate bound both readers share, and the typed
|
|
2
|
-
// rejection of everything that is neither an `.xlsx` nor an `.xlsb`.
|
|
3
|
-
//
|
|
4
|
-
// The reader's first job on an arbitrary blob is to reject what it cannot read with a clear, typed
|
|
5
|
-
// error rather than a raw fflate crash. A cheap magic-byte sniff runs *before* the zip layer (so a
|
|
6
|
-
// legacy `.xls` compound file or non-ZIP garbage fails fast, before fflate throws its opaque "end of
|
|
7
|
-
// central directory" error — which can also leak an absolute path from below), and a malformed ZIP is
|
|
8
|
-
// translated after it.
|
|
9
|
-
//
|
|
10
|
-
// The two outcomes are deliberately different types, because they answer different questions. A blob
|
|
11
|
-
// the sniff rejects is the *wrong kind of thing* — {@link UnsupportedFormatError}, branchable on
|
|
12
|
-
// `.format`. A `PK`-headed blob the zip layer then chokes on is the right kind of thing we cannot
|
|
13
|
-
// unpack — {@link PackageReadError}. Whichever it is, the message states the check that actually ran,
|
|
14
|
-
// and callers branch on the type, never on message text.
|
|
15
|
-
//
|
|
16
|
-
// The `.xlsx` and `.xlsb` serialisations share this whole layer — same container, same bound, same
|
|
17
|
-
// rejections — so it is stated once here and neither reader owns it.
|
|
18
1
|
import { PackageReadError, UnsupportedFormatError } from './errors.js';
|
|
19
2
|
import { inflatePackage } from './inflate.js';
|
|
20
|
-
// The OLE2 / Compound File Binary signature ([MS-CFB] 2.2) that opens every legacy `.xls` (and the
|
|
21
|
-
// `.doc`/`.ppt` siblings). `src/vba/cfb.ts` reads this same magic for `vbaProject.bin`; here we need
|
|
22
|
-
// only recognise it, not parse the container.
|
|
23
3
|
const CFB_MAGIC = Uint8Array.of(0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1);
|
|
24
|
-
// The ZIP local-file-header signature (`PK\x03\x04`) that opens every OPC package — `.xlsx`, `.xlsb`,
|
|
25
|
-
// and the rest. An empty or spanned archive starts with a different `PK` marker, but a real workbook
|
|
26
|
-
// package always leads with a local file header.
|
|
27
4
|
const ZIP_LOCAL_FILE_MAGIC = Uint8Array.of(0x50, 0x4b, 0x03, 0x04);
|
|
28
|
-
/** Classify a blob's container by its leading magic bytes alone — no allocation, no inflation. */
|
|
29
5
|
export function sniffContainer(data) {
|
|
30
6
|
if (startsWith(data, CFB_MAGIC))
|
|
31
7
|
return 'cfb';
|
|
@@ -42,24 +18,10 @@ function startsWith(data, magic) {
|
|
|
42
18
|
}
|
|
43
19
|
return true;
|
|
44
20
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Inflate a spreadsheet package (`.xlsx` or `.xlsb` — the container is the same), translating input
|
|
47
|
-
* that is neither into a typed error before or instead of a raw zip failure:
|
|
48
|
-
* - a legacy `.xls` (CFB) blob → {@link UnsupportedFormatError} `'xls'`, caught by the magic sniff so
|
|
49
|
-
* fflate never runs on it;
|
|
50
|
-
* - a non-ZIP blob → {@link UnsupportedFormatError} `'unknown'`, likewise caught before inflation;
|
|
51
|
-
* - a `PK`-headed blob that fflate then rejects as malformed → {@link PackageReadError}, with the
|
|
52
|
-
* underlying zip message discarded so no internals (or path) leak.
|
|
53
|
-
*
|
|
54
|
-
* The bounded-inflation guard (a probable zip bomb) already raises {@link PackageReadError} with its
|
|
55
|
-
* own informative message, and is re-thrown unchanged.
|
|
56
|
-
*/
|
|
57
21
|
export function inflateSpreadsheetPackage(data, cap) {
|
|
58
22
|
const container = sniffContainer(data);
|
|
59
23
|
if (container === 'cfb')
|
|
60
24
|
throw new UnsupportedFormatError('xls');
|
|
61
|
-
// Not the default message: nothing here has looked for a workbook part, and saying so would point
|
|
62
|
-
// an investigation a layer past the one that actually refused.
|
|
63
25
|
if (container === 'other') {
|
|
64
26
|
throw new UnsupportedFormatError('unknown', 'not a valid .xlsx package: the input is not a ZIP');
|
|
65
27
|
}
|
|
@@ -67,23 +29,11 @@ export function inflateSpreadsheetPackage(data, cap) {
|
|
|
67
29
|
return inflatePackage(data, cap);
|
|
68
30
|
}
|
|
69
31
|
catch (err) {
|
|
70
|
-
// The bomb guard already throws the right type with a better message — surface it. Anything else
|
|
71
|
-
// is fflate reporting a malformed archive: same classification (a ZIP we cannot unpack), but its
|
|
72
|
-
// raw text is replaced wholesale rather than wrapped or attached as `cause`, because it comes from
|
|
73
|
-
// a layer whose strings may name internals — or an absolute path — that must not reach a caller.
|
|
74
32
|
if (err instanceof PackageReadError)
|
|
75
33
|
throw err;
|
|
76
34
|
throw new PackageReadError('not a readable .xlsx package: the ZIP container is corrupt or truncated and could not be inflated');
|
|
77
35
|
}
|
|
78
36
|
}
|
|
79
|
-
/**
|
|
80
|
-
* The typed error for an inflated package that carries no `xl/workbook.xml`: a `.xlsb` if its binary
|
|
81
|
-
* `xl/workbook.bin` office document is present, otherwise an unrecognised (non-workbook) ZIP.
|
|
82
|
-
*
|
|
83
|
-
* The `.xlsb` branch takes the caller's own explanation, because whether a binary workbook is
|
|
84
|
-
* readable now depends on *which* entry point was asked: `readXlsx` reads one, the row streamer
|
|
85
|
-
* cannot yet. A single baked-in "not supported" message would be wrong for one of them.
|
|
86
|
-
*/
|
|
87
37
|
export function unsupportedWorkbookPart(partText, xlsbMessage) {
|
|
88
38
|
if (partText('xl/workbook.bin') !== undefined) {
|
|
89
39
|
return new UnsupportedFormatError('xlsb', xlsbMessage);
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
// The resolved cell-format table both codecs read into: what an xf *is* once its id-indirection is
|
|
2
|
-
// flattened, which of the two number-format tables an id resolves against, and what applying an xf to
|
|
3
|
-
// a cell means.
|
|
4
|
-
//
|
|
5
|
-
// None of that is a property of how the style table is spelled — `xl/styles.xml` and `xl/styles.bin`
|
|
6
|
-
// state the same model in XML and BIFF12 records — so it is stated once here, above both codecs. The
|
|
7
|
-
// XML parsing lives in `../xlsx/read-styles.ts`, the record parsing in `../xlsb/read-styles.ts`, and
|
|
8
|
-
// each hands back the same {@link StyleTable}.
|
|
9
1
|
import { applyCellStyle } from '../../core/cell.js';
|
|
10
2
|
import { NAMED_STYLE_ID } from '../../core/internal.js';
|
|
11
|
-
// ECMA-376 reserves numFmt ids below 164 for formats every consumer knows implicitly, so a
|
|
12
|
-
// foreign file may name one with no <numFmt> entry. This maps the standard ids to their
|
|
13
|
-
// codes; id 0 (General) and any unknown id resolve to no format. The writer never emits
|
|
14
|
-
// these — it always defines a custom id — but reading them keeps foreign files faithful.
|
|
15
3
|
const BUILTIN_NUMFMTS = new Map([
|
|
16
4
|
[1, '0'],
|
|
17
5
|
[2, '0.00'],
|
|
@@ -40,11 +28,6 @@ const BUILTIN_NUMFMTS = new Map([
|
|
|
40
28
|
[47, 'mmss.0'],
|
|
41
29
|
[48, '##0.0E+0'],
|
|
42
30
|
[49, '@'],
|
|
43
|
-
// Ids 27..36 and 50..58 are reserved for locale-specific built-in East Asian date/time formats;
|
|
44
|
-
// a file authored in a CJK locale styles date cells with them and, being built-ins, emits no
|
|
45
|
-
// <numFmt>. The exact code is locale-defined — these are the representative Excel forms — but what
|
|
46
|
-
// matters for reading is that each resolves to a non-empty date/time code so the serial reads as a
|
|
47
|
-
// date rather than a bare number.
|
|
48
31
|
[27, '[$-404]e/m/d'],
|
|
49
32
|
[28, '[$-404]e"年"m"月"d"日"'],
|
|
50
33
|
[29, '[$-404]e"年"m"月"d"日"'],
|
|
@@ -65,25 +48,11 @@ const BUILTIN_NUMFMTS = new Map([
|
|
|
65
48
|
[57, '[$-404]yyyy"年"m"月"'],
|
|
66
49
|
[58, '[$-404]m"月"d"日"'],
|
|
67
50
|
]);
|
|
68
|
-
/**
|
|
69
|
-
* The format code a number-format id denotes: the file's own `<numFmt>`/`BrtFmt` declaration if it
|
|
70
|
-
* has one, else the built-in Excel defines for that id. Id 0 is General — the absence of a format —
|
|
71
|
-
* and resolves to nothing so an ordinary cell carries no `numFmt`.
|
|
72
|
-
*/
|
|
73
51
|
export function numFmtCodeFor(id, custom) {
|
|
74
52
|
if (!Number.isInteger(id) || id === 0)
|
|
75
53
|
return undefined;
|
|
76
54
|
return custom.get(id) ?? BUILTIN_NUMFMTS.get(id);
|
|
77
55
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Apply a resolved xf's non-value facets to a cell — the six {@link CellStyle} facets through the
|
|
80
|
-
* shared {@link applyCellStyle}, plus the two links that live on the xf itself rather than in the
|
|
81
|
-
* facet tuple (`quotePrefix`, and the `xfId` pointer into the named-style layer).
|
|
82
|
-
*
|
|
83
|
-
* Shared by every path that commits a cell: the XML reader's ordinary and shared-formula-clone paths,
|
|
84
|
-
* and the BIFF12 reader — so a styled cell keeps its look regardless of which serialisation it came
|
|
85
|
-
* from, and the two cannot drift on what "applying a style" means.
|
|
86
|
-
*/
|
|
87
56
|
export function applyXfToCell(cell, style) {
|
|
88
57
|
if (style === undefined)
|
|
89
58
|
return;
|
package/dist/io/xlsb/errors.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { XlsxError } from '../../errors.js';
|
|
2
|
-
/**
|
|
3
|
-
* Thrown when an `.xlsb` package's binary BIFF12 content cannot be parsed — a record whose declared
|
|
4
|
-
* size runs past the end of its part, a truncated record header, or a structure that does not conform
|
|
5
|
-
* to [MS-XLSB].
|
|
6
|
-
*
|
|
7
|
-
* The record streams are hostile input (a spreadsheet library parses untrusted files), so a malformed
|
|
8
|
-
* part fails closed with this error rather than crashing, hanging, or over-allocating. It is distinct
|
|
9
|
-
* from {@link UnsupportedFormatError}, which reports that the *container* is not a format we read at
|
|
10
|
-
* all; by the time this is raised the input has already been recognised as an `.xlsb`.
|
|
11
|
-
*/
|
|
12
2
|
export class XlsbParseError extends XlsxError {
|
|
13
3
|
name = 'XlsbParseError';
|
|
14
4
|
code = 'malformed-input';
|
package/dist/io/xlsb/formula.js
CHANGED
|
@@ -1,47 +1,11 @@
|
|
|
1
|
-
// Decoding a BIFF12 `Ptg` token stream back into formula text — the one place where the binary and
|
|
2
|
-
// XML serialisations of a workbook are genuinely different *languages* rather than different spellings.
|
|
3
|
-
//
|
|
4
|
-
// An `.xlsx` stores `SUM(A1:A5)/COUNT(A1:A5)` as those nineteen characters. An `.xlsb` stores the same
|
|
5
|
-
// formula as a postfix (reverse-Polish) token stream: two range operands, two calls, a divide. So the
|
|
6
|
-
// decoder is a stack machine — each operand pushes its own text, each operator pops what it needs and
|
|
7
|
-
// pushes the joined result, and a well-formed stream leaves exactly one string behind.
|
|
8
|
-
//
|
|
9
|
-
// Two things make the reconstruction exact rather than approximate:
|
|
10
|
-
//
|
|
11
|
-
// - **Parentheses are stored, not inferred.** Excel emits an explicit `PtgParen` wherever the author
|
|
12
|
-
// typed one, so there is no precedence arithmetic here and no risk of `(1+2)*3` reading back as
|
|
13
|
-
// `1+2*3`. The token stream already says which is which.
|
|
14
|
-
// - **A reference names no sheet.** A 3-D reference carries an *index* into the workbook's
|
|
15
|
-
// `BrtExternSheet` table, which in turn names a span of sheets in a supporting book. Resolving
|
|
16
|
-
// that indirection — and re-quoting the sheet name the way Excel would — is what turns token
|
|
17
|
-
// `ixti=2` back into `'Odd Name'!A1`.
|
|
18
|
-
//
|
|
19
|
-
// **A token this decoder does not know makes the whole formula undecodable, by design.** The stream is
|
|
20
|
-
// self-describing only if every token's length is known, so guessing past an unrecognised token would
|
|
21
|
-
// desynchronise the parse and produce confident nonsense. Instead the decoder returns `undefined` and
|
|
22
|
-
// its caller keeps what it can still trust — the cached result Excel stored beside the formula. The
|
|
23
|
-
// gaps that reach that path are listed in `docs/knowledge/specs/xlsb-binary-format-output.md`.
|
|
24
1
|
import { MAX_COLUMN, numberToColumn } from '../../core/address.js';
|
|
25
2
|
import { quoteSheetName } from '../../core/formula.js';
|
|
26
3
|
import { errorCodeFor, RecordReader } from './primitives.js';
|
|
27
4
|
import { FTAB_USER_DEFINED, fixedArityFor, functionNameFor } from './ptg-functions.js';
|
|
28
|
-
/**
|
|
29
|
-
* Decode a `CellParsedFormula`'s token stream into formula text, in the same on-disk spelling the XML
|
|
30
|
-
* form writes into `<f>` — `_xlfn.`-prefixed function names included, so the caller applies the same
|
|
31
|
-
* `unmangleFunctions` normalisation to either serialisation.
|
|
32
|
-
*
|
|
33
|
-
* @param rgce the token stream.
|
|
34
|
-
* @param rgcb the trailing extra-data block: the array constants, and the cell ranges a precomputed
|
|
35
|
-
* range token refers to. Its entries are consumed in token order.
|
|
36
|
-
* @returns the formula text, or `undefined` if the stream uses a token this reader does not decode.
|
|
37
|
-
* @throws {XlsbParseError} if a token runs past the end of the stream (a malformed formula).
|
|
38
|
-
*/
|
|
39
5
|
export function decodeFormula(rgce, rgcb, scope) {
|
|
40
6
|
const tokens = new RecordReader(rgce);
|
|
41
7
|
const extra = new RecordReader(rgcb);
|
|
42
8
|
const stack = [];
|
|
43
|
-
// Pushing `undefined` is how an undecodable token is reported without unwinding: the loop stops and
|
|
44
|
-
// the arity check below rejects the stream. It keeps every token case a plain expression.
|
|
45
9
|
const push = (text) => {
|
|
46
10
|
if (text === undefined)
|
|
47
11
|
return false;
|
|
@@ -54,13 +18,6 @@ export function decodeFormula(rgce, rgcb, scope) {
|
|
|
54
18
|
}
|
|
55
19
|
return stack.length === 1 ? stack[0] : undefined;
|
|
56
20
|
}
|
|
57
|
-
/**
|
|
58
|
-
* The master cell a token stream defers to, when the stream is nothing but a `PtgExp` (a shared or
|
|
59
|
-
* array formula's member) or a `PtgTbl` (a data-table cell) rather than a formula of its own.
|
|
60
|
-
*
|
|
61
|
-
* The column lives in the extra-data block rather than the token, which is the one place BIFF12
|
|
62
|
-
* splits a single reference across the two halves of a `CellParsedFormula`.
|
|
63
|
-
*/
|
|
64
21
|
export function formulaAnchor(rgce, rgcb) {
|
|
65
22
|
if (rgce.length !== 5 || (rgce[0] !== PTG.Exp && rgce[0] !== PTG.Tbl))
|
|
66
23
|
return undefined;
|
|
@@ -72,7 +29,6 @@ export function formulaAnchor(rgce, rgcb) {
|
|
|
72
29
|
return undefined;
|
|
73
30
|
return { row, column: extra.u32() };
|
|
74
31
|
}
|
|
75
|
-
// One token: decode it, mutate the stack, and report whether the stream is still decodable.
|
|
76
32
|
function step(ptg, tokens, extra, stack, scope, push) {
|
|
77
33
|
const binary = BINARY_OPERATORS.get(ptg);
|
|
78
34
|
if (binary !== undefined) {
|
|
@@ -94,7 +50,6 @@ function step(ptg, tokens, extra, stack, scope, push) {
|
|
|
94
50
|
return operand !== undefined && push(`(${operand})`);
|
|
95
51
|
}
|
|
96
52
|
case PTG.MissArg:
|
|
97
|
-
// An omitted argument — `IF(A1>0,,1)` — is a real operand whose text is nothing at all.
|
|
98
53
|
return push('');
|
|
99
54
|
case PTG.Str:
|
|
100
55
|
return push(quoteString(tokens.shortString()));
|
|
@@ -109,18 +64,15 @@ function step(ptg, tokens, extra, stack, scope, push) {
|
|
|
109
64
|
case PTG.Num:
|
|
110
65
|
return push(numberText(tokens.f64()));
|
|
111
66
|
default:
|
|
112
|
-
// Every remaining token is an operand or call whose meaning is independent of its result class
|
|
113
|
-
// (reference, value, or array) — the class only tells the calculation engine how to coerce it.
|
|
114
67
|
return ptg >= CLASSED_TOKEN_FLOOR
|
|
115
68
|
? operand((ptg & CLASSED_TOKEN_MASK) | CLASSED_TOKEN_FLOOR, tokens, extra, stack, scope, push)
|
|
116
69
|
: false;
|
|
117
70
|
}
|
|
118
71
|
}
|
|
119
|
-
// A class-carrying operand or call token, reduced to its base ptg.
|
|
120
72
|
function operand(base, tokens, extra, stack, scope, push) {
|
|
121
73
|
switch (base) {
|
|
122
74
|
case PTG.Array:
|
|
123
|
-
tokens.skip(14);
|
|
75
|
+
tokens.skip(14);
|
|
124
76
|
return push(arrayConstant(extra));
|
|
125
77
|
case PTG.Func: {
|
|
126
78
|
const name = functionNameFor(tokens.u16());
|
|
@@ -130,8 +82,6 @@ function operand(base, tokens, extra, stack, scope, push) {
|
|
|
130
82
|
case PTG.FuncVar:
|
|
131
83
|
return variadicCall(tokens, stack, push);
|
|
132
84
|
case PTG.Name: {
|
|
133
|
-
// Cited 1-based, and into the *unfiltered* name list — the placeholder names Excel registers for
|
|
134
|
-
// post-2007 functions occupy indices too, even though they are not the workbook's defined names.
|
|
135
85
|
return push(scope.names[tokens.u32() - 1]);
|
|
136
86
|
}
|
|
137
87
|
case PTG.Ref:
|
|
@@ -161,38 +111,23 @@ function operand(base, tokens, extra, stack, scope, push) {
|
|
|
161
111
|
tokens.skip(14);
|
|
162
112
|
return push(REFERENCE_ERROR);
|
|
163
113
|
case PTG.MemArea:
|
|
164
|
-
// A precomputed range: the tokens it was computed from follow inline, so the header is skipped
|
|
165
|
-
// and the walk simply continues into them. Its extra-data entry — the resulting rectangles — is
|
|
166
|
-
// a calculation shortcut with nothing to say about the text, but must still be consumed in order.
|
|
167
114
|
tokens.skip(6);
|
|
168
115
|
return skipExtraRanges(extra);
|
|
169
116
|
default:
|
|
170
117
|
return false;
|
|
171
118
|
}
|
|
172
119
|
}
|
|
173
|
-
// `PtgAttr` ([MS-XLSB] 2.5.97.1): a family of hints the calculation engine leaves in the stream.
|
|
174
|
-
// Almost all are invisible in the formula text — the jump offsets an `IF` uses to skip the branch it
|
|
175
|
-
// did not take, the marker on a volatile function. The one that carries meaning is `bitSum`, Excel's
|
|
176
|
-
// encoding of a single-argument `SUM`, which is a call by any other name.
|
|
177
120
|
function attribute(tokens, stack, push) {
|
|
178
121
|
const flags = tokens.u8();
|
|
179
122
|
const data = tokens.u16();
|
|
180
123
|
if ((flags & ATTR_CHOOSE) !== 0) {
|
|
181
|
-
// The one variable-length attribute: `data` counts CHOOSE's branches, each with a jump offset,
|
|
182
|
-
// plus one for the end of the call.
|
|
183
124
|
tokens.skip((data + 1) * 2);
|
|
184
125
|
return true;
|
|
185
126
|
}
|
|
186
127
|
if ((flags & ATTR_SUM) !== 0)
|
|
187
128
|
return push(call('SUM', 1, stack));
|
|
188
|
-
// `bitSpace` records whitespace the author typed around a token. It is cosmetic — Excel redisplays
|
|
189
|
-
// the formula identically without it — and reattaching it to the right operand is not something a
|
|
190
|
-
// postfix walk can do, so it is dropped rather than misplaced.
|
|
191
129
|
return true;
|
|
192
130
|
}
|
|
193
|
-
// `PtgFuncVar` ([MS-XLSB] 2.5.97.4): a call whose argument count is in the token. Index 255 is not a
|
|
194
|
-
// function at all but the indirection every post-2007 function is called through: the name comes from
|
|
195
|
-
// the stream's first operand, which is a `PtgName` pointing at Excel's `_xlfn.`-prefixed placeholder.
|
|
196
131
|
function variadicCall(tokens, stack, push) {
|
|
197
132
|
const count = tokens.u8() & FUNCVAR_PARAM_MASK;
|
|
198
133
|
const index = tokens.u16() & FUNCVAR_INDEX_MASK;
|
|
@@ -206,16 +141,11 @@ function variadicCall(tokens, stack, push) {
|
|
|
206
141
|
const [name, ...args] = parts;
|
|
207
142
|
return push(`${name}(${args.join(',')})`);
|
|
208
143
|
}
|
|
209
|
-
// Pop `arity` arguments and push the call. Arguments were pushed left to right, so they come off the
|
|
210
|
-
// stack as one contiguous run in source order.
|
|
211
144
|
function call(name, arity, stack) {
|
|
212
145
|
if (stack.length < arity)
|
|
213
146
|
return undefined;
|
|
214
147
|
return `${name}(${stack.splice(stack.length - arity, arity).join(',')})`;
|
|
215
148
|
}
|
|
216
|
-
// `PtgExtraArray` ([MS-XLSB] 2.5.97.2): the elements of an array constant, row-major, behind a
|
|
217
|
-
// row/column count. The element encodings are fixed-width apart from the string, which carries its
|
|
218
|
-
// own length — so the block is walked, never indexed.
|
|
219
149
|
function arrayConstant(extra) {
|
|
220
150
|
const rows = extra.u32();
|
|
221
151
|
const columns = extra.u32();
|
|
@@ -234,7 +164,6 @@ function arrayConstant(extra) {
|
|
|
234
164
|
}
|
|
235
165
|
return `{${lines.join(';')}}`;
|
|
236
166
|
}
|
|
237
|
-
// One `SerAr` ([MS-XLSB] 2.5.129) element of an array constant.
|
|
238
167
|
function arrayElement(extra) {
|
|
239
168
|
switch (extra.u8()) {
|
|
240
169
|
case SER_NUM:
|
|
@@ -252,7 +181,6 @@ function arrayElement(extra) {
|
|
|
252
181
|
return undefined;
|
|
253
182
|
}
|
|
254
183
|
}
|
|
255
|
-
// Consume the `PtgExtraMem` a precomputed-range token owns: a count, then that many cell ranges.
|
|
256
184
|
function skipExtraRanges(extra) {
|
|
257
185
|
const count = extra.u32();
|
|
258
186
|
if (count * RANGE_BYTES > extra.remaining)
|
|
@@ -260,8 +188,6 @@ function skipExtraRanges(extra) {
|
|
|
260
188
|
extra.skip(count * RANGE_BYTES);
|
|
261
189
|
return true;
|
|
262
190
|
}
|
|
263
|
-
// The sheet part of a 3-D reference, `Data!` or `'Odd Name'!` or `Data:More!`, from the index the
|
|
264
|
-
// token carries into the workbook's extern-sheet table.
|
|
265
191
|
function sheetPrefix(ixti, scope) {
|
|
266
192
|
const xti = scope.externSheets[ixti];
|
|
267
193
|
if (xti === undefined || xti.supBook !== scope.selfSupBook)
|
|
@@ -272,8 +198,6 @@ function sheetPrefix(ixti, scope) {
|
|
|
272
198
|
return undefined;
|
|
273
199
|
return `${first === last ? quoteSheetName(first) : quoteSheetName(first, last)}!`;
|
|
274
200
|
}
|
|
275
|
-
// A single cell, from the row and the packed column word every reference token shares. The two high
|
|
276
|
-
// bits of that word say whether each axis is relative, which is exactly where the `$` signs go.
|
|
277
201
|
function cellText(row, packedColumn) {
|
|
278
202
|
const column = packedColumn & COLUMN_MASK;
|
|
279
203
|
if (row > MAX_ROW_INDEX || column >= MAX_COLUMN)
|
|
@@ -282,9 +206,6 @@ function cellText(row, packedColumn) {
|
|
|
282
206
|
const rowAbs = (packedColumn & ROW_RELATIVE) === 0 ? '$' : '';
|
|
283
207
|
return `${columnAbs}${numberToColumn(column + 1)}${rowAbs}${row + 1}`;
|
|
284
208
|
}
|
|
285
|
-
// A range. A range that spans every row of its columns, or every column of its rows, is written in
|
|
286
|
-
// Excel's abbreviated form (`A:A`, `2:2`) — which is not cosmetic: it is the only spelling Excel
|
|
287
|
-
// writes for a whole-column reference, so anything else would fail to match the XML twin.
|
|
288
209
|
function rangeText(rowFirst, rowLast, packedFirst, packedLast) {
|
|
289
210
|
if (rowFirst === 0 && rowLast === MAX_ROW_INDEX) {
|
|
290
211
|
const first = columnOnly(packedFirst);
|
|
@@ -309,19 +230,13 @@ function columnOnly(packedColumn) {
|
|
|
309
230
|
function rowOnly(row, packedColumn) {
|
|
310
231
|
return `${(packedColumn & ROW_RELATIVE) === 0 ? '$' : ''}${row + 1}`;
|
|
311
232
|
}
|
|
312
|
-
// A string literal, in the formula's own escaping: the delimiter is a double quote, and a double quote
|
|
313
|
-
// inside the text is doubled.
|
|
314
233
|
function quoteString(text) {
|
|
315
234
|
return `"${text.replace(/"/g, '""')}"`;
|
|
316
235
|
}
|
|
317
|
-
// A numeric literal. JavaScript and Excel agree on every ordinary number; they part company only at
|
|
318
|
-
// the exponent's case, which is normalised here so `1E+21` does not read back as `1e+21`.
|
|
319
236
|
function numberText(value) {
|
|
320
237
|
return String(value).toUpperCase();
|
|
321
238
|
}
|
|
322
239
|
const REFERENCE_ERROR = '#REF!';
|
|
323
|
-
// The infix operators, by their ptg. `PtgIsect` is Excel's space operator (`A1:A3 A2:A5`) and
|
|
324
|
-
// `PtgUnion` its comma — both are operators despite looking like punctuation.
|
|
325
240
|
const BINARY_OPERATORS = new Map([
|
|
326
241
|
[0x03, '+'],
|
|
327
242
|
[0x04, '-'],
|
|
@@ -339,9 +254,6 @@ const BINARY_OPERATORS = new Map([
|
|
|
339
254
|
[0x10, ','],
|
|
340
255
|
[0x11, ':'],
|
|
341
256
|
]);
|
|
342
|
-
// The token numbers this decoder names. Operand tokens are listed at their *base* value: the stream
|
|
343
|
-
// carries them offset by a result class (+0x20 value, +0x40 array), which changes how the calculation
|
|
344
|
-
// engine coerces the operand but never what it says.
|
|
345
257
|
const PTG = {
|
|
346
258
|
Exp: 0x01,
|
|
347
259
|
Tbl: 0x02,
|
|
@@ -372,27 +284,17 @@ const PTG = {
|
|
|
372
284
|
};
|
|
373
285
|
const CLASSED_TOKEN_FLOOR = 0x20;
|
|
374
286
|
const CLASSED_TOKEN_MASK = 0x1f;
|
|
375
|
-
// `PtgAttr` flag bits.
|
|
376
287
|
const ATTR_CHOOSE = 0x04;
|
|
377
288
|
const ATTR_SUM = 0x10;
|
|
378
|
-
// `PtgFuncVar`'s two packed fields each reserve their top bit for a flag the text does not carry.
|
|
379
289
|
const FUNCVAR_PARAM_MASK = 0x7f;
|
|
380
290
|
const FUNCVAR_INDEX_MASK = 0x7fff;
|
|
381
|
-
// The packed column word shared by every reference token: 14 bits of column, then the two relative-
|
|
382
|
-
// axis flags.
|
|
383
291
|
const COLUMN_MASK = 0x3fff;
|
|
384
292
|
const COLUMN_RELATIVE = 0x4000;
|
|
385
293
|
const ROW_RELATIVE = 0x8000;
|
|
386
294
|
const MAX_ROW_INDEX = 1048575;
|
|
387
|
-
// `SerAr` element tags.
|
|
388
295
|
const SER_NUM = 0x00;
|
|
389
296
|
const SER_STR = 0x01;
|
|
390
297
|
const SER_BOOL = 0x02;
|
|
391
298
|
const SER_ERR = 0x04;
|
|
392
|
-
// An `UncheckedRfX` is four 32-bit bounds.
|
|
393
299
|
const RANGE_BYTES = 16;
|
|
394
|
-
// A bound on an array constant's declared size. The elements themselves are read from the extra-data
|
|
395
|
-
// block, which cannot outrun its own record — but the row × column product is multiplied *before* any
|
|
396
|
-
// of it is read, and a forged pair would otherwise buy a loop of its own choosing. Excel's own limit
|
|
397
|
-
// on an array constant is far below this.
|
|
398
300
|
const MAX_ARRAY_ELEMENTS = 1 << 20;
|