@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,18 +1,3 @@
1
- // The buffered `.xlsb` reader: a binary OPC package in, a {@link Workbook} model out.
2
- //
3
- // An `.xlsb` is the *same* OPC/ZIP container and the *same* relationship graph as an `.xlsx` — the
4
- // workbook part points at its sheets, its shared strings, and its style sheet through ordinary
5
- // `.rels` XML. Only the office-document parts differ: `xl/workbook.bin`, `xl/worksheets/sheetN.bin`
6
- // and friends are BIFF12 record streams instead of XML. So this module reuses the container layer
7
- // wholesale (`../opc/`, the bounded inflater) and swaps only the part parsers, which is
8
- // exactly the "two codecs over one model" shape the format note argues for.
9
- //
10
- // The model produced is the one `readXlsx` produces, not a parallel one: the same `Workbook`, the
11
- // same `XfStyle` table, the same cells. That is what lets a caller convert between the two forms —
12
- // and what the corpus asserts, by reading a workbook Excel saved in both forms and comparing.
13
- //
14
- // Not yet decoded (each its own slice of work, none silently wrong): rich-text runs, tables, pivots,
15
- // and conditional formatting.
16
1
  import { unmangleFunctions } from '../../core/formula.js';
17
2
  import { INTERNAL } from '../../core/internal.js';
18
3
  import { Workbook } from '../../core/workbook.js';
@@ -27,26 +12,11 @@ import { parseStyleTable } from './read-styles.js';
27
12
  import { parseWorksheet } from './read-worksheet.js';
28
13
  import { readRecords } from './record-stream.js';
29
14
  import { BRT } from './record-types.js';
30
- /** The office-document part every `.xlsb` package is entered through. */
31
15
  export const XLSB_WORKBOOK_PART = 'xl/workbook.bin';
32
- /**
33
- * Read an `.xlsb` (binary BIFF12) package into a {@link Workbook}.
34
- *
35
- * @throws {UnsupportedFormatError} if the input is not an `.xlsb` package — a legacy `.xls`
36
- * (`.format === 'xls'`), an XML `.xlsx` or unrecognised blob (`'unknown'`).
37
- * @throws {XlsbParseError} if a binary part is malformed.
38
- * @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
39
- * truncated archive, or one exceeding the inflate bound (a probable zip bomb).
40
- */
41
16
  export function readXlsb(data, options = {}) {
42
17
  const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
43
18
  return readXlsbPackage(inflateSpreadsheetPackage(data, cap));
44
19
  }
45
- /**
46
- * Build the model from an already-inflated `.xlsb` package. Separate from {@link readXlsb} so the
47
- * `.xlsx` reader can hand over a package it has already inflated and classified, rather than
48
- * inflating the same bytes twice.
49
- */
50
20
  export function readXlsbPackage(files) {
51
21
  const { partText, partBytes } = packageAccessors(files);
52
22
  const workbookPart = partBytes(XLSB_WORKBOOK_PART);
@@ -57,12 +27,8 @@ export function readXlsbPackage(files) {
57
27
  const sharedStrings = parseSharedStrings(partBytes('xl/sharedStrings.bin'));
58
28
  const { cellXfs, namedStyles, defaultFont } = parseStyleTable(partBytes('xl/styles.bin'));
59
29
  const workbook = new Workbook();
60
- // As in the XML reader, the named-style layer is restored only when a file declares more than the
61
- // Normal default, so an ordinary workbook keeps an empty table and writes just that default back.
62
30
  if (namedStyles.length > 1)
63
31
  workbook[INTERNAL].restoreNamedStyles(namedStyles);
64
- // As in the XML reader, font 0 is the workbook's declared default and must survive a re-write; an
65
- // assumed Calibri in its place changes every empty cell and every character-unit column width.
66
32
  workbook[INTERNAL].restoreDefaultFont(defaultFont);
67
33
  const declaration = readWorkbookPart(workbookPart);
68
34
  const scope = {
@@ -82,17 +48,12 @@ export function readXlsbPackage(files) {
82
48
  workbook.defineName(defined);
83
49
  return workbook;
84
50
  }
85
- // One pass over `xl/workbook.bin`, gathering everything the rest of the read depends on: the sheet
86
- // bundle, the externals block a 3-D reference resolves through, and the defined names.
87
51
  function readWorkbookPart(part) {
88
52
  const sheets = [];
89
53
  const names = [];
90
54
  let externSheets = [];
91
55
  let inBundle = false;
92
56
  let inExternals = false;
93
- // A workbook with no external links declares exactly one supporting book: itself. Rather than
94
- // enumerate every record type that could open another — and risk miscounting into a *wrong* sheet
95
- // name — anything else inside the externals block disqualifies the whole table.
96
57
  let supportingBooks = 0;
97
58
  let selfSupBook;
98
59
  for (const record of readRecords(part)) {
@@ -122,21 +83,17 @@ function readWorkbookPart(part) {
122
83
  selfSupBook: supportingBooks === 1 ? selfSupBook : undefined,
123
84
  };
124
85
  }
125
- // `BrtBundleSh` ([MS-XLSB] 2.4.303) — the binary spelling of `<sheet name state r:id/>`.
126
86
  function readSheet(data) {
127
87
  const reader = new RecordReader(data);
128
88
  const state = SHEET_STATES[reader.u32()] ?? 'visible';
129
- reader.skip(4); // iTabId: the sheet's stable id, which the model assigns itself.
89
+ reader.skip(4);
130
90
  const relId = reader.nullableWideString();
131
91
  return { name: reader.wideString(), relId, state };
132
92
  }
133
- // `BrtExternSheet` ([MS-XLSB] 2.4.677): the `Xti` table every 3-D reference indexes into.
134
93
  function readExternSheets(data) {
135
94
  const reader = new RecordReader(data);
136
95
  const count = reader.u32();
137
96
  const entries = [];
138
- // Each entry is three 4-byte fields; checking the count against what the record holds keeps a forged
139
- // one from driving the loop rather than the record's own length.
140
97
  if (count * XTI_BYTES > reader.remaining)
141
98
  return entries;
142
99
  for (let index = 0; index < count; index++) {
@@ -145,12 +102,10 @@ function readExternSheets(data) {
145
102
  return entries;
146
103
  }
147
104
  const XTI_BYTES = 12;
148
- // `BrtName` ([MS-XLSB] 2.4.673). The target is left undecoded here: it is a token stream that may cite
149
- // a sheet by index, and the sheet bundle is not necessarily complete at this point in the stream.
150
105
  function readName(data) {
151
106
  const reader = new RecordReader(data);
152
107
  const flags = reader.u32();
153
- reader.skip(1); // chKey: the Alt-key shortcut a macro name can carry.
108
+ reader.skip(1);
154
109
  const itab = reader.u32();
155
110
  const name = reader.wideString();
156
111
  const rgce = reader.bytes(reader.u32());
@@ -163,19 +118,8 @@ function readName(data) {
163
118
  rgcb,
164
119
  };
165
120
  }
166
- // `itab` for a workbook-global name; any other value is a zero-based sheet index.
167
121
  const GLOBAL_NAME_SCOPE = 0xffffffff;
168
- // `fFunc`: the name registers something callable rather than a range.
169
122
  const NAME_IS_FUNCTION = 0x00000002;
170
- // The workbook's defined names, as the model holds them.
171
- //
172
- // Two kinds of `BrtName` are dropped, both because the XML form does not persist them either — so
173
- // carrying them through would make the two readings of one workbook disagree. A *function* name is
174
- // Excel's registration of a callable (every post-2007 function gets one, `_xlfn.TEXTJOIN` and
175
- // friends); its target is the placeholder `#NAME?`, not a range. And `_xlnm._FilterDatabase` is the
176
- // built-in Excel derives from a sheet's autofilter, which the model reconstructs from the autofilter
177
- // itself. A name whose target uses a token this reader cannot decode is dropped too, rather than
178
- // surfaced with a target that is a guess.
179
123
  function definedNames(declaration, scope) {
180
124
  const names = [];
181
125
  for (const declared of declaration.names) {
@@ -188,12 +132,10 @@ function definedNames(declaration, scope) {
188
132
  names.push({
189
133
  name: declared.name,
190
134
  ...(sheet === undefined ? {} : { scope: sheet }),
191
- // Stripped back to the readable form, the same normalisation the XML reader applies.
192
135
  refersTo: unmangleFunctions(refersTo),
193
136
  });
194
137
  }
195
138
  return names;
196
139
  }
197
140
  const FILTER_DATABASE_NAME = '_xlnm._FilterDatabase';
198
- // `hsState` ([MS-XLSB] 2.4.303), indexed by its stored value.
199
141
  const SHEET_STATES = ['visible', 'hidden', 'veryHidden'];
@@ -1,24 +1,4 @@
1
- // The BIFF12 record framing every `.bin` part of an `.xlsb` package is built from ([MS-XLSB] 2.1.4).
2
- //
3
- // A part is a bare concatenation of records — no header, no index, no terminator — each framed as a
4
- // variable-length type, a variable-length size, and that many payload bytes. Both prefixes are
5
- // 7-bits-per-byte with the high bit meaning "one more byte follows": the type is 1–2 bytes (so the
6
- // single-byte space is reserved for the hot records — a cell is one byte of framing), the size is 1–4.
7
- //
8
- // **This is the reader's hostile-input frontier.** The declared size is attacker-controlled, so it is
9
- // never allowed to drive an allocation: a record's payload is handed out as a `subarray` *view* onto
10
- // the part the inflate bound already materialised and capped, and a size that would run past the end
11
- // of the part is rejected outright rather than clamped. A lying length therefore costs nothing and
12
- // fails closed, and the whole stream's memory is exactly the part's own bytes.
13
1
  import { XlsbParseError } from './errors.js';
14
- /**
15
- * Decode a `.bin` part into its record sequence, in order.
16
- *
17
- * Lazy: a caller that stops early (having found what it needs) never frames the rest of the part.
18
- *
19
- * @throws {XlsbParseError} if a record header is truncated or a record's declared size runs past the
20
- * end of the part.
21
- */
22
2
  export function* readRecords(part) {
23
3
  let offset = 0;
24
4
  while (offset < part.length) {
@@ -26,8 +6,6 @@ export function* readRecords(part) {
26
6
  let type = lowType & 0x7f;
27
7
  if ((lowType & 0x80) !== 0)
28
8
  type |= (byteAt(part, offset++) & 0x7f) << 7;
29
- // Up to four size bytes, least-significant seven bits first. The fourth byte's continuation bit
30
- // is specified as ignored, which the fixed bound expresses: the loop simply stops there.
31
9
  let size = 0;
32
10
  for (let index = 0; index < 4; index++) {
33
11
  const piece = byteAt(part, offset++);
@@ -35,9 +13,6 @@ export function* readRecords(part) {
35
13
  if ((piece & 0x80) === 0)
36
14
  break;
37
15
  }
38
- // The one check that makes a forged size harmless: compare it against what the part *actually*
39
- // holds, before anything is handed out. `part.length - offset` cannot be negative here — byteAt
40
- // has already proven every header byte was in range.
41
16
  if (size > part.length - offset) {
42
17
  throw new XlsbParseError(`BIFF12 record ${type} declares ${size} bytes but only ${part.length - offset} remain in the part`);
43
18
  }
@@ -45,9 +20,6 @@ export function* readRecords(part) {
45
20
  offset += size;
46
21
  }
47
22
  }
48
- // Every header byte goes through here, so a part that ends mid-header fails closed rather than
49
- // folding `undefined` into the arithmetic (which `noUncheckedIndexedAccess` would otherwise let
50
- // through as a silent NaN-shaped type or size).
51
23
  function byteAt(part, index) {
52
24
  const value = part[index];
53
25
  if (value === undefined)
@@ -1,15 +1,4 @@
1
- // The BIFF12 record types this reader consumes, by their [MS-XLSB] "Record Enumeration" (section 2.3)
2
- // numbers. Named constants rather than magic numbers at the switch sites: a record number carries no
3
- // hint of what it is, so `BRT.CellRk` is the difference between a readable parser and a table of
4
- // unexplained integers.
5
- //
6
- // Deliberately partial. [MS-XLSB] enumerates ~760 record types; listing the ones we do not decode
7
- // would claim coverage we do not have. A record whose type is absent here is skipped by the parsers,
8
- // which is the correct behaviour for a forward-compatible reader — the format grows, and an unknown
9
- // record is always safely framed (its size is in the stream) even when its meaning is not known.
10
- /** BIFF12 record numbers, grouped by the part whose stream they appear in. */
11
1
  export const BRT = {
12
- // Cell table (worksheet part). The single-byte record space — these are the hot path.
13
2
  RowHdr: 0,
14
3
  CellBlank: 1,
15
4
  CellRk: 2,
@@ -25,13 +14,9 @@ export const BRT = {
25
14
  CellRString: 62,
26
15
  ColInfo: 60,
27
16
  MergeCell: 176,
28
- // Shared-string table.
29
17
  SSTItem: 19,
30
18
  BeginSst: 159,
31
19
  EndSst: 160,
32
- // Style sheet: each collection is a Begin/End pair around its entries, and `XF` appears inside two
33
- // of them (the named-style layer and the direct-format layer), so the style parser tracks which
34
- // collection it is in rather than keying on the record number alone.
35
20
  Fmt: 44,
36
21
  Font: 43,
37
22
  Fill: 45,
@@ -52,18 +37,14 @@ export const BRT = {
52
37
  EndCellXFs: 618,
53
38
  BeginStyles: 619,
54
39
  EndStyles: 620,
55
- // Workbook part.
56
40
  BundleSh: 156,
57
41
  BeginBundleShs: 143,
58
42
  EndBundleShs: 144,
59
43
  Name: 39,
60
- // The externals block: which workbooks a formula can reach, and which sheets of them each `ixti` a
61
- // 3-D reference carries names. `SupSelf` declares a supporting book that is *this* workbook.
62
44
  BeginExternals: 353,
63
45
  EndExternals: 354,
64
46
  SupSelf: 357,
65
47
  ExternSheet: 362,
66
- // Worksheet structure.
67
48
  WsProp: 147,
68
49
  WsDim: 148,
69
50
  WsFmtInfo: 485,
@@ -1,9 +1,3 @@
1
- // Cell-gathering state machine for the worksheet-body reader. It owns the still-textual pieces of the
2
- // one `<c>` currently being read — its address/type/style, formula, cached value, inline text, and
3
- // rich-text runs — plus the shared-formula master map that spans the whole sheet. Each `<c>` resets
4
- // the per-cell state on {@link beginCell} and commits it on {@link finalize}, so this class is the
5
- // single owner of "what has this cell gathered so far" — to a cell what {@link RunAccumulator} is to a
6
- // rich string. Value *decoding* stays in `cell-value.ts`; this class only gathers the raw pieces.
7
1
  import { decodeAddress, encodeAddress } from '../../core/address.js';
8
2
  import { translateFormula, unmangleFunctions } from '../../core/formula.js';
9
3
  import { boolPresent } from '../../xml/xml-read.js';
@@ -17,8 +11,6 @@ export class CellAccumulator {
17
11
  #col = -1;
18
12
  #row = -1;
19
13
  #formula = '';
20
- // Shared-formula bookkeeping. A master `<f t="shared" ref si>TEXT</f>` seeds the group; every clone
21
- // `<f t="shared" si/>` in the sheet references it by `si` and carries no text of its own.
22
14
  #formulaShared = false;
23
15
  #formulaSi = -1;
24
16
  #sharedClone = false;
@@ -28,27 +20,19 @@ export class CellAccumulator {
28
20
  #hasFormula = false;
29
21
  #hasValue = false;
30
22
  #runs = new RunAccumulator();
31
- // Masters always precede their clones (Excel keeps the master top-left), so a clone resolves against
32
- // a map filled as the sheet streams: the master's formula translated to the clone's position.
33
23
  #masters = new Map();
34
- /** This cell's `<c r>` address (`"B3"`), or '' when it carried none. */
35
24
  get ref() {
36
25
  return this.#ref;
37
26
  }
38
- /** This cell's own `<c s>` style index, or -1 when it carries none. */
39
27
  get styleIndex() {
40
28
  return this.#style;
41
29
  }
42
- /** This cell's 1-based column, or -1 when its address was absent or unparseable. */
43
30
  get col() {
44
31
  return this.#col;
45
32
  }
46
- /** The rich-text run accumulator, driven by the surrounding parser's `<r>`/`<rPr>` handling. */
47
33
  get runs() {
48
34
  return this.#runs;
49
35
  }
50
- // Begin a new `<c>`: record its address/type/style and clear every per-cell gathered field so the
51
- // last cell's formula, value, runs, or shared/data-table declaration cannot bleed into this one.
52
36
  beginCell(attrs) {
53
37
  this.#ref = attrs.r ?? '';
54
38
  this.#type = attrs.t ?? '';
@@ -66,9 +50,6 @@ export class CellAccumulator {
66
50
  this.#sharedClone = false;
67
51
  this.#dataTable = null;
68
52
  }
69
- // Begin an `<f>`: record its shared-formula grouping and any data-table declaration. A self-closing
70
- // `<f t="shared" si/>` is a clone — it fires no close and carries no text — so mark it here to
71
- // resolve against its master when the cell finalises.
72
53
  beginFormula(attrs, selfClosing) {
73
54
  this.#formulaShared = attrs.t === 'shared';
74
55
  this.#formulaSi = attrs.si !== undefined ? Number(attrs.si) : -1;
@@ -92,22 +73,14 @@ export class CellAccumulator {
92
73
  this.#valueText = text;
93
74
  this.#hasValue = true;
94
75
  }
95
- // Begin an `<is>`: clear the inline string and its runs so a rich value built from a previous cell's
96
- // runs keeps its own array.
97
76
  beginInlineString() {
98
77
  this.#inlineText = '';
99
78
  this.#runs.reset();
100
79
  }
101
- // Route a `<t>`'s text: to the open run when one is active, otherwise to the inline string when the
102
- // parser is inside an `<is>`. A run takes precedence — a run is also inside the inline string.
103
80
  appendText(text, inInlineString) {
104
81
  if (!this.#runs.appendText(text) && inInlineString)
105
82
  this.#inlineText += text;
106
83
  }
107
- // Commit the gathered cell to the sheet with its already-resolved style (the caller applies the
108
- // cell → row → column inheritance order). A data-table cell surfaces its declaration; a shared-formula
109
- // master seeds the group before finalising, and a clone resolves to the master translated to its own
110
- // position. Everything else decodes as an ordinary `<c>` payload. An address-less cell is a no-op.
111
84
  finalize(sheet, sharedStrings, style) {
112
85
  if (this.#ref === '')
113
86
  return;
@@ -138,7 +111,6 @@ export class CellAccumulator {
138
111
  const value = {
139
112
  sharedFormula: encodeAddress(master.col, master.row),
140
113
  formula: unmangleFunctions(translated),
141
- // A clone's cached result honours the cell's date format the same way a plain formula's does.
142
114
  ...(this.#hasValue
143
115
  ? { result: decodeFormulaResult(this.#type, this.#valueText, style?.numFmt) }
144
116
  : {}),
@@ -153,10 +125,6 @@ export class CellAccumulator {
153
125
  applyXfToCell(cell, style);
154
126
  cell.value = this.decode(sharedStrings, style);
155
127
  }
156
- // Decode the gathered pieces into a plain cell value, resolving the shared pool and date formats but
157
- // NOT the shared-formula / data-table declarations {@link finalize} handles. This is what a data
158
- // read (the streaming reader) wants: the cell's own value, with a shared-formula clone surfacing its
159
- // cached result rather than a translated formula it will not evaluate.
160
128
  decode(sharedStrings, style) {
161
129
  const raw = {
162
130
  type: this.#type,
@@ -1,34 +1,17 @@
1
- // Decoding a worksheet cell's on-disk `<c>` payload into a model {@link CellValue}.
2
- //
3
- // This is the single value-decoding surface both readers share: the buffered reader
4
- // (`./read.ts`) and the streaming row reader (`./read-rows.ts`). Keeping it in one place is
5
- // what guarantees a cell read one row at a time decodes identically to the same cell read as
6
- // part of a whole workbook — a divergence here would be a silent data bug in exactly one path.
7
1
  import { isDateFormat, serialToDate } from '../../core/date.js';
8
2
  import { unmangleFunctions } from '../../core/formula.js';
9
3
  import { isErrorCode, } from '../../core/value.js';
10
4
  import { boolStrict } from '../../xml/xml-read.js';
11
- /**
12
- * Decode a gathered cell into its model value. A formula cell becomes a `{formula, result?}`
13
- * object (the on-disk `_xlfn.`/`_xlpm.` mangling stripped back to the readable name); a plain
14
- * numeric cell under a date number format becomes a {@link Date}; everything else decodes by its
15
- * `t` type. `numFmt` is the cell's resolved number-format code, used only for date detection.
16
- */
17
5
  export function decodeCellContent(raw, sharedStrings, numFmt) {
18
6
  if (raw.hasFormula) {
19
7
  const stored = unmangleFunctions(raw.formula);
20
8
  const result = raw.hasValue ? decodeFormulaResult(raw.type, raw.valueText, numFmt) : undefined;
21
9
  return result === undefined ? { formula: stored } : { formula: stored, result };
22
10
  }
23
- // An inline string built from `<r>` runs is rich text — surface its runs rather than flattening
24
- // them to the concatenated `inlineText` a plain string would decode to.
25
11
  if (raw.type === 'inlineStr' && raw.richTextRuns !== undefined && raw.richTextRuns.length > 0) {
26
12
  return { richText: raw.richTextRuns };
27
13
  }
28
14
  const value = decodeValue(raw.type, raw.valueText, raw.inlineText, raw.hasValue, sharedStrings);
29
- // A number stored under a date format is a date serial — surface it as a Date so a written
30
- // date round-trips as a date, not a bare number. Only plain numeric cells qualify; a string,
31
- // boolean, or formula result under a date format keeps its own kind.
32
15
  return typeof value === 'number' && numFmt !== undefined && isDateFormat(numFmt)
33
16
  ? serialToDate(value)
34
17
  : value;
@@ -40,13 +23,8 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
40
23
  case 'str':
41
24
  return valueText;
42
25
  case 'd':
43
- // A Strict-mode (ISO/IEC 29500 Strict) date cell stores an ISO 8601 value directly, not a
44
- // serial. Parse it literally — an ISO date is UTC — so it reads as the date it states rather
45
- // than a 1900-epoch serial the transitional decoder would fabricate from the text.
46
26
  return valueText === '' ? null : new Date(valueText);
47
27
  case 's': {
48
- // A `t="s"` cell indexes the shared pool; the entry is a plain string or, when Excel pooled a
49
- // rich value, a {@link RichTextValue} whose runs surface here rather than being flattened.
50
28
  const index = Number(valueText);
51
29
  return Number.isInteger(index) ? (sharedStrings[index] ?? '') : '';
52
30
  }
@@ -58,19 +36,12 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
58
36
  return hasValue ? Number(valueText) : null;
59
37
  }
60
38
  }
61
- /** Decode a formula's cached `<v>` result by its `t` type, coercing a numeric result under a date
62
- * `numFmt` to a {@link Date} exactly as a bare numeric cell is — so a date-valued formula result
63
- * (e.g. `TODAY()`) reads back as a Date, not a serial. Shared by the buffered reader's shared-formula
64
- * clone resolution, which caches a result the same way a plain formula cell does. */
65
39
  export function decodeFormulaResult(type, valueText, numFmt) {
66
40
  const result = decodeResult(type, valueText);
67
41
  return typeof result === 'number' && numFmt !== undefined && isDateFormat(numFmt)
68
42
  ? serialToDate(result)
69
43
  : result;
70
44
  }
71
- // The formula-result subset of `decodeValue`: a cached result is only ever a string, boolean,
72
- // error, or number — never a shared-string index, inline string, or Strict-mode date — so this
73
- // handles just those cases rather than the full cell-value grammar.
74
45
  function decodeResult(type, valueText) {
75
46
  switch (type) {
76
47
  case 'str':
@@ -0,0 +1,6 @@
1
+ import type { Color } from '../../core/style.ts';
2
+ /** Serialise a {@link Color} as the attribute list a `<color>`-shaped element carries. */
3
+ export declare function colorAttrs(color: Color): string;
4
+ export declare function parseColor(attrs: {
5
+ readonly [k: string]: string;
6
+ }): Color;
@@ -0,0 +1,42 @@
1
+ import { AuthoringError } from '../../errors.js';
2
+ function normalizeArgb(argb) {
3
+ const hex = argb.startsWith('#') ? argb.slice(1) : argb;
4
+ const rgb = hex.length === 6 ? `FF${hex}` : hex;
5
+ if (!/^[0-9a-fA-F]{8}$/.test(rgb)) {
6
+ throw new AuthoringError(`Invalid ARGB colour ${JSON.stringify(argb)}: expected 6 or 8 hexadecimal digits`);
7
+ }
8
+ return rgb;
9
+ }
10
+ export function colorAttrs(color) {
11
+ const parts = [];
12
+ if (color.argb !== undefined)
13
+ parts.push(`rgb="${normalizeArgb(color.argb)}"`);
14
+ if (color.theme !== undefined)
15
+ parts.push(`theme="${color.theme}"`);
16
+ if (color.tint !== undefined)
17
+ parts.push(`tint="${color.tint}"`);
18
+ if (color.indexed !== undefined)
19
+ parts.push(`indexed="${color.indexed}"`);
20
+ return parts.join(' ');
21
+ }
22
+ export function parseColor(attrs) {
23
+ const color = {};
24
+ if (attrs.rgb !== undefined)
25
+ color.argb = attrs.rgb;
26
+ if (attrs.theme !== undefined) {
27
+ const theme = Number(attrs.theme);
28
+ if (Number.isInteger(theme))
29
+ color.theme = theme;
30
+ }
31
+ if (attrs.tint !== undefined) {
32
+ const tint = Number(attrs.tint);
33
+ if (Number.isFinite(tint))
34
+ color.tint = tint;
35
+ }
36
+ if (attrs.indexed !== undefined) {
37
+ const indexed = Number(attrs.indexed);
38
+ if (Number.isInteger(indexed))
39
+ color.indexed = indexed;
40
+ }
41
+ return color;
42
+ }
@@ -1,49 +1,13 @@
1
- // Cell comments — the `xl/comments{n}.xml` part, its `xl/drawings/vmlDrawing{n}.vml` companion, and the
2
- // reader that maps a comment back onto its cell.
3
- //
4
- // A comment is anchored to a cell by A1 reference and rendered by Excel as a floating box. The box's
5
- // geometry lives in a legacy VML drawing (the pre-DrawingML shape format Excel still requires here);
6
- // the text lives in the comments part. Both are emitted together — a comments part with no matching
7
- // `<legacyDrawing>`/VML reads as text but renders nothing, so we never split them.
8
- //
9
- // Two different things share this one wire form:
10
- // • a user's **note** (`cell.note`) — a single anonymous annotation, the whole of what the part held
11
- // before 2018;
12
- // • the legacy **fallback** Excel writes beside every modern threaded comment (see
13
- // `threaded-comments.ts`), so a pre-2018 reader still sees the conversation. Its text is a fixed
14
- // boilerplate wrapping a copy of the thread, and its author is a synthetic `tc={headId}` entry.
15
- //
16
- // That `tc=` author and the comment's `xr:uid` are how Excel binds a cell back to its thread — not
17
- // decoration. Verified against desktop Excel: a package whose threadedComment part, persons registry,
18
- // relationships and content types all survive intact still reads back as ordinary notes with zero
19
- // threads once those two are lost. So the fallback is *derived from the thread model* on write and
20
- // *suppressed on read*, rather than round-tripped as a plain note.
21
1
  import { decodeAddress } from '../../core/address.js';
22
2
  import { escapeAttr, escapeText, textElement, XML_DECLARATION } from '../../xml/xml.js';
23
3
  import { localName, parseXml } from '../../xml/xml-read.js';
24
4
  import { MARKUP_COMPATIBILITY_NS, REVISION_NS, SPREADSHEETML_NS } from './namespaces.js';
25
- /**
26
- * Gather every comment a sheet must write: its cells' notes, plus one legacy fallback per conversation
27
- * in `threads`. A comment anchors to its cell regardless of the cell's value, so a note (or a thread) on
28
- * an otherwise-empty cell is collected too.
29
- *
30
- * `threads` is the conversations the *package* will carry, not simply the ones the sheet holds — the
31
- * caller decides, because a fallback beside a thread whose `threadedComment` part is missing is worse
32
- * than no fallback at all: verified against desktop Excel, such a comment shows as neither a thread nor
33
- * a note, so the text disappears entirely.
34
- *
35
- * Ordered by cell, row-major, the way Excel writes the list — so a fallback lands interleaved among the
36
- * notes rather than appended after them, and the VML shapes follow the same order.
37
- */
38
5
  export function collectComments(sheet, threads) {
39
6
  const fallbacks = threadFallbacks(threads);
40
7
  const anchored = new Set(fallbacks.map((fallback) => fallback.ref));
41
8
  const comments = [...fallbacks];
42
9
  for (const { cells } of sheet.rows()) {
43
10
  for (const cell of cells) {
44
- // Excel refuses to put a note and a thread on one cell, so a file carrying both (only a foreign
45
- // generator or a hand-edit makes one) is written back as the thread alone: two comments on one ref
46
- // is a shape Excel repairs by dropping both, which would lose the conversation as well as the note.
47
11
  if (cell.note === undefined || anchored.has(cell.address))
48
12
  continue;
49
13
  comments.push({ ref: cell.address, row: cell.row, col: cell.col, text: cell.note });
@@ -51,8 +15,6 @@ export function collectComments(sheet, threads) {
51
15
  }
52
16
  return comments.sort((a, b) => a.row - b.row || a.col - b.col);
53
17
  }
54
- // One legacy fallback per conversation, keyed to the thread head whose id binds it. A thread with no
55
- // messages has nothing to write and no id to bind by, so it contributes none.
56
18
  function threadFallbacks(threads) {
57
19
  const fallbacks = [];
58
20
  for (const thread of threads) {
@@ -64,31 +26,15 @@ function threadFallbacks(threads) {
64
26
  }
65
27
  return fallbacks;
66
28
  }
67
- // The boilerplate Excel puts in front of every fallback, captured verbatim from an Excel-authored file.
68
- // It is what a pre-2018 reader shows the user, so it is reproduced exactly rather than paraphrased.
69
29
  const FALLBACK_PREAMBLE = '[Threaded comment]\n\nYour version of Excel allows you to read this threaded comment; however, any ' +
70
30
  'edits to it will get removed if the file is opened in a newer version of Excel. Learn more: ' +
71
31
  'https://go.microsoft.com/fwlink/?linkid=870924\n\n';
72
- // A whole conversation flattened into the one comment a pre-2018 reader can render: the opening message
73
- // under `Comment:`, then each reply under its own `Reply:`, every body indented four spaces. Verified
74
- // against desktop Excel for a thread with three replies — `Reply:` repeats per reply rather than the
75
- // replies being joined under one heading.
76
32
  function fallbackText(thread) {
77
33
  const [head, ...replies] = thread.comments;
78
34
  const body = replies.map((reply) => `\nReply:\n ${reply.text}`).join('');
79
35
  return `${FALLBACK_PREAMBLE}Comment:\n ${head?.text ?? ''}${body}`;
80
36
  }
81
- // `xr:uid` lives in the 2014 revision namespace, declared `mc:Ignorable` exactly as Excel declares it so
82
- // a consumer that does not know the prefix skips the attribute instead of rejecting the part.
83
37
  const REVISION_NS_ATTRS = ` xmlns:mc="${MARKUP_COMPATIBILITY_NS}" mc:Ignorable="xr" xmlns:xr="${REVISION_NS}"`;
84
- /**
85
- * The `xl/comments{n}.xml` part.
86
- *
87
- * Authors are laid out the way Excel lays them out: one synthetic `tc={headId}` entry per threaded
88
- * conversation first, then a single anonymous author shared by every note (the model carries no note
89
- * author). Each comment points at its own author by index, and a fallback additionally carries the
90
- * `xr:uid` naming its thread — the pair that keeps Excel treating the cell as threaded.
91
- */
92
38
  export function commentsXml(comments) {
93
39
  const authorIdByThreadId = new Map();
94
40
  for (const { threadId } of comments) {
@@ -116,7 +62,6 @@ export function commentsXml(comments) {
116
62
  `<commentList>${list}</commentList>` +
117
63
  '</comments>');
118
64
  }
119
- // VML namespaces and the one shape type (a text box) every comment reuses.
120
65
  const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
121
66
  'xmlns:o="urn:schemas-microsoft-com:office:office" ' +
122
67
  'xmlns:x="urn:schemas-microsoft-com:office:excel">' +
@@ -124,11 +69,6 @@ const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
124
69
  '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" ' +
125
70
  'path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/>' +
126
71
  '<v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>';
127
- /** The `xl/drawings/vmlDrawing{n}.vml` companion: one hidden text-box shape per comment, in the same
128
- * order as the comments part. Anchor coordinates place the box a couple of cells down-and-right of its
129
- * owner; Excel refines them on open, so the values are a sensible starting geometry rather than a
130
- * pixel-exact layout. A thread's fallback shape is `ObjectType="Note"` like any other — Excel draws the
131
- * threaded-comment card itself and only needs the shape to exist. */
132
72
  export function vmlDrawingXml(comments) {
133
73
  const shapes = comments
134
74
  .map((comment, i) => {
@@ -148,18 +88,8 @@ export function vmlDrawingXml(comments) {
148
88
  .join('');
149
89
  return `${VML_HEADER}${shapes}</xml>`;
150
90
  }
151
- // A comment names its author by index into `<authors>`, so an empty entry must still occupy its slot —
152
- // presenting the self-closing `<author/>` an author-less file writes as an empty element gives it the
153
- // close that pushes it. Without this every later index would shift by one and a note could inherit a
154
- // thread's `tc=` author.
155
91
  const COMMENT_EMPTY_CLOSES = new Set(['author']);
156
- // The author string marking a comment as a thread's legacy fallback: `tc={headThreadId}`.
157
92
  const THREAD_AUTHOR_PREFIX = 'tc=';
158
- /**
159
- * Parse a `comments{n}.xml` part into a map of A1 reference → comment. Text runs within one comment are
160
- * concatenated; an author-name run is Excel's own convention and is not stripped, so a note reads back
161
- * as exactly the text that was written.
162
- */
163
93
  export function parseComments(xml) {
164
94
  const comments = new Map();
165
95
  const authors = [];
@@ -210,26 +140,12 @@ export function parseComments(xml) {
210
140
  }, { closeEmptyElements: COMMENT_EMPTY_CLOSES });
211
141
  return comments;
212
142
  }
213
- // A missing or non-numeric `authorId` indexes nothing, so `authors[NaN]` is undefined and the comment
214
- // reads as a plain note — the safe direction, since mistaking a note for a fallback would delete it.
215
143
  function threadIdOf(author) {
216
144
  if (author === undefined || !author.startsWith(THREAD_AUTHOR_PREFIX))
217
145
  return undefined;
218
146
  const id = author.slice(THREAD_AUTHOR_PREFIX.length);
219
147
  return id === '' ? undefined : id;
220
148
  }
221
- /**
222
- * Apply a parsed comments part onto a sheet's cells as notes, addressing each by its A1 reference.
223
- *
224
- * A thread's legacy fallback is not a note and does not become one: its text is boilerplate wrapping a
225
- * copy of the conversation, so surfacing it as `cell.note` hands the caller garbage — and on write it
226
- * would be re-emitted as a plain note, destroying the `tc=`/`xr:uid` binding and leaving Excel unable to
227
- * see the thread at all.
228
- *
229
- * Suppressed only for a conversation the reader actually holds: a file whose thread part is missing or
230
- * damaged has nothing else left, so there the boilerplate is kept rather than the content lost. Call
231
- * after the sheet's threads are restored, since that is what this reads to decide.
232
- */
233
149
  export function applyNotes(sheet, comments) {
234
150
  const headIds = new Set(sheet.commentThreads.flatMap((thread) => {
235
151
  const head = thread.comments[0];
@@ -1,5 +1,5 @@
1
1
  import type { ConditionalFormatting } from '../../core/conditional-formatting.ts';
2
- import { type StyleRegistry } from './styles.ts';
2
+ import type { StyleRegistry } from './styles.ts';
3
3
  /**
4
4
  * Serialise every conditional formatting on a sheet into its `<conditionalFormatting>` blocks, in
5
5
  * insertion order. Returns '' when the sheet has none. A rule missing a `priority` is assigned the