@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.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -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;
@@ -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;
@@ -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';
@@ -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); // A size hint the extra-data block restates; the block is the authority.
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;