@shbernal/ts-xlsx 1.1.0 → 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 (127) 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.js +0 -131
  5. package/dist/core/color-resolution.js +0 -60
  6. package/dist/core/column.js +0 -60
  7. package/dist/core/comment-thread.js +0 -45
  8. package/dist/core/conditional-formatting-overlay.js +0 -11
  9. package/dist/core/conditional-formatting.js +0 -10
  10. package/dist/core/containers.js +0 -15
  11. package/dist/core/data-validation-overlay.js +0 -25
  12. package/dist/core/data-validation.js +0 -7
  13. package/dist/core/date.js +0 -39
  14. package/dist/core/formula.js +0 -117
  15. package/dist/core/grid-edits.js +0 -38
  16. package/dist/core/image.js +0 -25
  17. package/dist/core/internal.js +0 -27
  18. package/dist/core/limits.js +0 -45
  19. package/dist/core/merge.d.ts +18 -0
  20. package/dist/core/merge.js +22 -7
  21. package/dist/core/modern-functions.js +0 -17
  22. package/dist/core/page-setup.js +0 -5
  23. package/dist/core/pivot-table.d.ts +1 -1
  24. package/dist/core/pivot-table.js +0 -48
  25. package/dist/core/preserved.js +0 -4
  26. package/dist/core/protection.js +0 -26
  27. package/dist/core/range.js +0 -112
  28. package/dist/core/row-input.d.ts +24 -0
  29. package/dist/core/row-input.js +29 -0
  30. package/dist/core/row.js +0 -66
  31. package/dist/core/style.js +0 -26
  32. package/dist/core/table-style.js +0 -28
  33. package/dist/core/table.js +0 -97
  34. package/dist/core/text-metrics.js +0 -40
  35. package/dist/core/theme.js +0 -85
  36. package/dist/core/value.js +0 -94
  37. package/dist/core/workbook-protection.js +0 -13
  38. package/dist/core/workbook.js +0 -380
  39. package/dist/core/worksheet-model.js +2 -23
  40. package/dist/core/worksheet.js +7 -506
  41. package/dist/customui/errors.js +0 -10
  42. package/dist/customui/index.js +0 -5
  43. package/dist/customui/ribbon.js +0 -43
  44. package/dist/entries/core.js +0 -6
  45. package/dist/entries/csv.js +0 -4
  46. package/dist/entries/customui.js +0 -2
  47. package/dist/entries/errors.js +0 -13
  48. package/dist/entries/vba.js +0 -5
  49. package/dist/entries/xlsb.js +0 -6
  50. package/dist/entries/xlsx.js +0 -13
  51. package/dist/errors.d.ts +14 -0
  52. package/dist/errors.js +5 -56
  53. package/dist/index.js +0 -12
  54. package/dist/io/csv/read.js +0 -18
  55. package/dist/io/csv/write.js +0 -18
  56. package/dist/io/opc/errors.js +0 -30
  57. package/dist/io/opc/inflate.js +0 -35
  58. package/dist/io/opc/namespaces.js +0 -15
  59. package/dist/io/opc/part-paths.js +0 -11
  60. package/dist/io/opc/read-opc.js +0 -31
  61. package/dist/io/opc/read-options.js +0 -6
  62. package/dist/io/opc/rels.js +0 -13
  63. package/dist/io/opc/sniff-format.js +0 -50
  64. package/dist/io/style/xf-style.js +0 -31
  65. package/dist/io/xlsb/errors.js +0 -10
  66. package/dist/io/xlsb/formula.js +1 -99
  67. package/dist/io/xlsb/primitives.js +1 -83
  68. package/dist/io/xlsb/ptg-functions.js +0 -34
  69. package/dist/io/xlsb/read-shared-strings.js +0 -7
  70. package/dist/io/xlsb/read-styles.js +3 -66
  71. package/dist/io/xlsb/read-worksheet.js +4 -87
  72. package/dist/io/xlsb/read.js +2 -60
  73. package/dist/io/xlsb/record-stream.js +0 -28
  74. package/dist/io/xlsb/record-types.js +0 -19
  75. package/dist/io/xlsx/cell-accumulator.js +0 -32
  76. package/dist/io/xlsx/cell-value.js +0 -29
  77. package/dist/io/xlsx/color-xml.d.ts +6 -0
  78. package/dist/io/xlsx/color-xml.js +42 -0
  79. package/dist/io/xlsx/comments.js +0 -84
  80. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  81. package/dist/io/xlsx/conditional-formatting.js +1 -97
  82. package/dist/io/xlsx/data-validation.js +0 -56
  83. package/dist/io/xlsx/edit-vba.js +0 -54
  84. package/dist/io/xlsx/errors.js +0 -12
  85. package/dist/io/xlsx/hyperlinks.js +0 -40
  86. package/dist/io/xlsx/images.js +0 -32
  87. package/dist/io/xlsx/namespaces.js +0 -42
  88. package/dist/io/xlsx/package-plan.js +0 -47
  89. package/dist/io/xlsx/pivot.js +0 -22
  90. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  91. package/dist/io/xlsx/read-rows.js +1 -83
  92. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  93. package/dist/io/xlsx/read-styles.d.ts +25 -1
  94. package/dist/io/xlsx/read-styles.js +61 -108
  95. package/dist/io/xlsx/read-worksheet.js +1 -88
  96. package/dist/io/xlsx/read.js +3 -211
  97. package/dist/io/xlsx/relationships.js +0 -11
  98. package/dist/io/xlsx/rich-runs.js +0 -16
  99. package/dist/io/xlsx/rich-text.js +0 -12
  100. package/dist/io/xlsx/shared-formulas.js +0 -7
  101. package/dist/io/xlsx/shared-strings.js +0 -21
  102. package/dist/io/xlsx/sheet-properties.js +1 -64
  103. package/dist/io/xlsx/styles.d.ts +1 -29
  104. package/dist/io/xlsx/styles.js +2 -366
  105. package/dist/io/xlsx/tables.js +5 -52
  106. package/dist/io/xlsx/threaded-comments.js +0 -103
  107. package/dist/io/xlsx/workbook-xml.js +0 -94
  108. package/dist/io/xlsx/worksheet-xml.js +0 -174
  109. package/dist/io/xlsx/write-stream.js +0 -137
  110. package/dist/io/xlsx/write.js +1 -134
  111. package/dist/io/xlsx/x14-ext.js +0 -8
  112. package/dist/vba/cfb-writer.js +12 -59
  113. package/dist/vba/cfb.js +2 -32
  114. package/dist/vba/codepage.js +1 -23
  115. package/dist/vba/errors.js +0 -15
  116. package/dist/vba/index.js +0 -10
  117. package/dist/vba/ms-ovba.js +3 -46
  118. package/dist/vba/project-editor.js +4 -89
  119. package/dist/vba/project.js +2 -34
  120. package/dist/vba/vba-encoding.js +1 -17
  121. package/dist/xml/errors.js +0 -10
  122. package/dist/xml/xml-read.js +3 -82
  123. package/dist/xml/xml.js +0 -39
  124. package/package.json +8 -9
  125. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  126. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  127. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -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;
@@ -1,35 +1,8 @@
1
- // The [MS-XLSB] primitive types a record payload is built from, behind one bounds-checked cursor.
2
- //
3
- // Binary parsing is where a reader most easily goes wrong in two directions at once: silently reading
4
- // past the end of a buffer, and allocating on a length the file (not the reader) chose. `RecordReader`
5
- // closes both. Every read goes through a single `#take`, so overrunning the record is impossible
6
- // rather than merely unlikely; and a length-prefixed string checks its byte count against what the
7
- // record actually holds *before* a single character is materialised, so a forged `cch` costs one
8
- // comparison instead of gigabytes.
9
- //
10
- // Structures decoded here — RkNumber, XLWideString, BrtColor, Cell, UncheckedRfX, BErr — are shared
11
- // across the workbook, worksheet, styles, and shared-string parsers; nothing part-specific lives here.
12
1
  import { isErrorCode } from '../../core/value.js';
13
2
  import { XlsbParseError } from './errors.js';
14
- // A length-prefixed string is decoded in code-unit batches rather than one `String.fromCharCode` call
15
- // per character (quadratic concatenation) or one spread of every unit (which blows the argument limit
16
- // on a long string). 4096 is comfortably under every engine's limit and makes the batching invisible.
17
3
  const CHARS_PER_BATCH = 4096;
18
- // `XLNullableWideString` marks "no string" with a character count of 0xFFFFFFFF rather than 0 — an
19
- // empty string and an absent one are different values (a sheet's relationship id is nullable; its
20
- // name is not).
21
4
  const NULL_STRING_LENGTH = 0xffffffff;
22
- // One reusable 8-byte window for reassembling an RkNumber's truncated double. The alternative — a
23
- // fresh ArrayBuffer per RK cell — would allocate once per numeric cell in the workbook, on the single
24
- // hottest path in the reader. Safe to share: the write and the read below are one synchronous pair.
25
5
  const rkScratch = new DataView(new ArrayBuffer(8));
26
- /**
27
- * A bounds-checked cursor over one record's payload.
28
- *
29
- * Each accessor advances the cursor by exactly the bytes it consumed, so a record is decoded by
30
- * naming its fields in order. Reading past the payload throws {@link XlsbParseError} — a record that
31
- * is shorter than its own definition is a malformed file, not a case to guess through.
32
- */
33
6
  export class RecordReader {
34
7
  #data;
35
8
  #view;
@@ -38,24 +11,15 @@ export class RecordReader {
38
11
  this.#data = data;
39
12
  this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength);
40
13
  }
41
- /** Bytes left in the record. */
42
14
  get remaining() {
43
15
  return this.#data.length - this.#offset;
44
16
  }
45
- /** Whether the whole payload has been consumed. */
46
17
  get done() {
47
18
  return this.remaining <= 0;
48
19
  }
49
- /** Advance past `count` bytes without decoding them — a reserved or unmodelled field. */
50
20
  skip(count) {
51
21
  this.#take(count);
52
22
  }
53
- /**
54
- * The next `count` bytes as a **view**, for a field whose own decoding happens elsewhere — a formula
55
- * token stream, whose meaning depends on workbook tables this record knows nothing about. A view
56
- * rather than a copy for the same reason a record's payload is one: the declared length comes from
57
- * the file, so it must bound a read, never an allocation.
58
- */
59
23
  bytes(count) {
60
24
  return this.#data.subarray(this.#take(count), this.#offset);
61
25
  }
@@ -74,57 +38,32 @@ export class RecordReader {
74
38
  i32() {
75
39
  return this.#view.getInt32(this.#take(4), true);
76
40
  }
77
- /** An `Xnum` ([MS-XLSB] 2.5.172): a little-endian IEEE-754 double. */
78
41
  f64() {
79
42
  return this.#view.getFloat64(this.#take(8), true);
80
43
  }
81
- /**
82
- * An `RkNumber` ([MS-XLSB] 2.5.122): a number packed into 32 bits. Two flag bits steal the low end
83
- * of the word — `fInt` says the remaining 30 bits are a signed integer rather than the *high* 30
84
- * bits of a double whose low 34 bits are zero, and `fX100` says the result was scaled up by 100 to
85
- * keep two decimal places in the integer form. It exists because most real spreadsheet numbers are
86
- * small integers or two-decimal currency, and this stores them in half the bytes of a double.
87
- */
88
44
  rk() {
89
- // Read signed: the `fInt` branch needs an arithmetic shift to sign-extend a negative 30-bit
90
- // integer, which only works on a value JavaScript already considers negative.
91
45
  const raw = this.i32();
92
46
  const value = (raw & 0b10) !== 0 ? raw >> 2 : truncatedDouble(raw & ~0b11);
93
47
  return (raw & 0b01) !== 0 ? value / 100 : value;
94
48
  }
95
- /** An `XLWideString` ([MS-XLSB] 2.5.169): a 4-byte character count then that many UTF-16LE units. */
96
49
  wideString() {
97
50
  return this.#characters(this.u32());
98
51
  }
99
- /**
100
- * A UTF-16 string whose character count is 16-bit rather than 32-bit — the form used *inside* a
101
- * formula token stream (`PtgStr`, and the string elements of an array constant), where a 4-byte
102
- * count on every literal would be pure overhead.
103
- */
104
52
  shortString() {
105
53
  return this.#characters(this.u16());
106
54
  }
107
- /** An `XLNullableWideString` ([MS-XLSB] 2.5.166): an {@link wideString} that can also be absent. */
108
55
  nullableWideString() {
109
56
  const length = this.u32();
110
57
  return length === NULL_STRING_LENGTH ? undefined : this.#characters(length);
111
58
  }
112
- /**
113
- * A `RichStr` ([MS-XLSB] 2.5.124): a string that may carry per-run formatting and phonetic guides.
114
- * Only the text is returned — the run and phonetic tails are left unread, which is safe because the
115
- * record's framing (not this cursor) bounds where the payload ends.
116
- */
117
59
  richString() {
118
- this.skip(1); // fRichStr / fExtStr flags: which optional tails follow the text.
60
+ this.skip(1);
119
61
  return this.wideString();
120
62
  }
121
- /** The `Cell` structure ([MS-XLSB] 2.5.10) that opens every cell record. */
122
63
  cell() {
123
64
  const column = this.u32();
124
- // The style index shares its word with a phonetic-display flag in the high byte.
125
65
  return { column, styleIndex: this.u32() & 0x00ffffff };
126
66
  }
127
- /** An `UncheckedRfX` ([MS-XLSB] 2.5.155): four zero-based, inclusive range bounds. */
128
67
  range() {
129
68
  return {
130
69
  rowFirst: this.u32(),
@@ -133,14 +72,6 @@ export class RecordReader {
133
72
  colLast: this.u32(),
134
73
  };
135
74
  }
136
- /**
137
- * A `BrtColor` ([MS-XLSB] 2.4.337), mapped onto the model's {@link Color}.
138
- *
139
- * The four encodings are mutually exclusive and the type tag picks which of the payload's fields
140
- * carry meaning; the rest are explicitly undefined. An *automatic* colour (type 0) names nothing at
141
- * all, and reads back as no colour — the same absence the XML reader produces for `<color auto="1"/>`,
142
- * so a cell whose font colour was never set does not gain one on read.
143
- */
144
75
  color() {
145
76
  const flags = this.u8();
146
77
  const index = this.u8();
@@ -152,15 +83,11 @@ export class RecordReader {
152
83
  const base = colorByType(flags >> 1, index, alpha, red, green, blue);
153
84
  if (base === undefined)
154
85
  return undefined;
155
- // Tint is stored as a fraction of the signed 16-bit range, where the extreme values mean 100%
156
- // lightening/darkening; the model carries it as OOXML does, in [-1, 1].
157
86
  if (tintAndShade === 0)
158
87
  return base;
159
88
  return { ...base, tint: Math.max(-1, Math.min(1, tintAndShade / 0x7fff)) };
160
89
  }
161
90
  #characters(count) {
162
- // Check the byte count against the record *before* building anything: this is the guard that
163
- // makes a forged character count a cheap failure rather than an allocation the file chose.
164
91
  const start = this.#take(count * 2);
165
92
  let text = '';
166
93
  let batch = [];
@@ -173,8 +100,6 @@ export class RecordReader {
173
100
  }
174
101
  return batch.length > 0 ? text + String.fromCharCode(...batch) : text;
175
102
  }
176
- // The single choke point every read passes through. Returns the offset the caller may read from,
177
- // having proven that `count` bytes are there.
178
103
  #take(count) {
179
104
  if (count > this.remaining) {
180
105
  throw new XlsbParseError(`BIFF12 record field needs ${count} bytes but only ${this.remaining} remain in the record`);
@@ -184,7 +109,6 @@ export class RecordReader {
184
109
  return start;
185
110
  }
186
111
  }
187
- /** A `BErr` ([MS-XLSB] 2.5.98.2) error code, as the model's error string. */
188
112
  export function errorCodeFor(code) {
189
113
  const text = BERR_CODES.get(code);
190
114
  return text !== undefined && isErrorCode(text) ? text : undefined;
@@ -199,14 +123,11 @@ const BERR_CODES = new Map([
199
123
  [0x2a, '#N/A'],
200
124
  [0x2b, '#GETTING_DATA'],
201
125
  ]);
202
- // Rebuild the double whose *high* 32 bits are `high` and whose low 34 bits [MS-XLSB] guarantees to be
203
- // zero. Written big-endian so the given word lands in the high half regardless of host endianness.
204
126
  function truncatedDouble(high) {
205
127
  rkScratch.setInt32(0, high, false);
206
128
  rkScratch.setUint32(4, 0, false);
207
129
  return rkScratch.getFloat64(0, false);
208
130
  }
209
- // The `xColorType` tag ([MS-XLSB] 2.4.337) selects which of a BrtColor's fields carry the colour.
210
131
  const COLOR_TYPE_INDEXED = 1;
211
132
  const COLOR_TYPE_RGB = 2;
212
133
  const COLOR_TYPE_THEME = 3;
@@ -217,9 +138,6 @@ function colorByType(type, index, alpha, red, green, blue) {
217
138
  case COLOR_TYPE_RGB:
218
139
  return { argb: hexByte(alpha) + hexByte(red) + hexByte(green) + hexByte(blue) };
219
140
  case COLOR_TYPE_THEME:
220
- // The binary theme index and OOXML's `theme="…"` attribute both count the `clrScheme`
221
- // subelements in declaration order (dk1, lt1, dk2, lt2, accent1–6, hlink, folHlink), so the
222
- // index carries across unchanged.
223
141
  return { theme: index };
224
142
  default:
225
143
  return undefined;