@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,21 +1,3 @@
1
- // The buffered `.xlsx` reader: an OPC zip package in, a Workbook model out.
2
- //
3
- // It reconstructs the part of the model the writer emits today — sheet names and order,
4
- // cells holding a number, string, boolean, or formula, per-column width/visibility,
5
- // per-row height/visibility, merged ranges, page margins, and cell styles (pattern fills,
6
- // number formats, fonts, borders, alignment, and protection — per cell, or inherited from a
7
- // formatted row/column). Shared-formula slaves and the richer value kinds land as the model
8
- // grows; an unrecognised construct is skipped rather than guessed, so a foreign file reads
9
- // without crashing even where a facet is not yet materialised.
10
- //
11
- // This module is the orchestrator: it wires the parsed package parts together — the OPC/rel
12
- // resolution (`./read-opc.ts`), the style table (`./read-styles.ts`), and each worksheet body
13
- // (`./read-worksheet.ts`) — and owns the sheet-part discovery (notes, images, tables, pivots) and
14
- // preserved-reference capture that a faithful round-trip depends on.
15
- //
16
- // Untrusted input: inflate is bounded by a running byte counter (`./inflate.ts`) that caps
17
- // actual decompressed output rather than trusting the archive's forgeable size headers, and
18
- // the parser (ADR 0004) never expands entities.
19
1
  import { decodeRange } from '../../core/address.js';
20
2
  import { unmangleFunctions } from '../../core/formula.js';
21
3
  import { INTERNAL } from '../../core/internal.js';
@@ -33,36 +15,15 @@ import { parseConditionalFormattings, parseDxfs } from './conditional-formatting
33
15
  import { applyDataValidations, parseDataValidations, parseExtendedDataValidations, } from './data-validation.js';
34
16
  import { applyHyperlinks, parseSheetHyperlinks } from './hyperlinks.js';
35
17
  import { drawingHasUnmodeledContent, parseDrawing } from './images.js';
36
- import { parsePivotTable } from './pivot-read.js';
37
- import { parseStyleTable } from './read-styles.js';
18
+ import { parsePivotTable } from './read-pivot.js';
19
+ import { parseSharedStrings } from './read-shared-strings.js';
20
+ import { parseIndexedColors, parseMruColors, parseStyleTable, parseTableStyles, } from './read-styles.js';
38
21
  import { parseWorksheet } from './read-worksheet.js';
39
- import { parseSharedStrings } from './shared-strings-read.js';
40
- import { parseIndexedColors, parseMruColors, parseTableStyles } from './styles.js';
41
22
  import { parseTable } from './tables.js';
42
23
  import { buildCommentThreads, parsePersons, parseThreadedComments } from './threaded-comments.js';
43
- // Re-exported for the streaming reader (`./read-rows.ts`) and the public barrel, which import these
44
- // from here: the split into per-part parsers is internal, so the reader's import surface is stable.
45
24
  export { parseRelationships, resolveWorkbookPart } from '../opc/read-opc.js';
46
- // The inflate bound and its option bag are shared with the `.xlsb` reader and the row streamer, so
47
- // they are declared apart from all three; they stay reachable here because this is the entry point
48
- // callers reach for.
49
25
  export { DEFAULT_MAX_UNCOMPRESSED } from '../opc/read-options.js';
50
26
  export { parseStyleTable } from './read-styles.js';
51
- /**
52
- * Read a spreadsheet package into a {@link Workbook}.
53
- *
54
- * Both OOXML serialisations are accepted: an XML `.xlsx`, and a binary `.xlsb` (BIFF12), which is the
55
- * same OPC container with binary office-document parts. The two are auto-detected from the package
56
- * itself rather than from a file extension, so a caller never branches on which form it holds — and
57
- * the model produced is the same either way. See `../xlsb/read.ts` for what the binary path does not
58
- * yet decode.
59
- *
60
- * @throws {UnsupportedFormatError} if the input is neither — a legacy `.xls` (`.format === 'xls'`) or
61
- * an unrecognised/non-ZIP blob (`'unknown'`).
62
- * @throws {XlsbParseError} if a binary `.xlsb` part is malformed.
63
- * @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
64
- * truncated archive, or one exceeding the inflate bound (a probable zip bomb).
65
- */
66
27
  export function readXlsx(data, options = {}) {
67
28
  const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
68
29
  const files = inflateSpreadsheetPackage(data, cap);
@@ -70,59 +31,31 @@ export function readXlsx(data, options = {}) {
70
31
  const { partText } = pkg;
71
32
  const workbookXml = partText('xl/workbook.xml');
72
33
  if (workbookXml === undefined) {
73
- // No XML office document. A binary one means this is an `.xlsb`, which reads through the BIFF12
74
- // codec over the very same model — the package is already inflated, so it is handed over as-is.
75
34
  if (files[XLSB_WORKBOOK_PART] !== undefined)
76
35
  return readXlsbPackage(files);
77
36
  throw new UnsupportedFormatError('unknown');
78
37
  }
79
- // A part's content type is needed to faithfully re-declare any part preserved verbatim for
80
- // round-tripping (a vector-shape drawing, a header/footer image and its VML). Resolve it the way
81
- // OPC does: an explicit `<Override>` for the exact part, else the `<Default>` for its extension.
82
38
  const contentTypeOf = contentTypeResolver(partText('[Content_Types].xml') ?? '');
83
39
  const workbookRelsXml = partText('xl/_rels/workbook.xml.rels') ?? '';
84
40
  const rels = parseRelationships(workbookRelsXml);
85
41
  const sharedStrings = parseSharedStrings(partText('xl/sharedStrings.xml') ?? '');
86
- // The style table resolves a cell/row/column style index to its facets (fill, number
87
- // format); a package without one (a hand-rolled foreign file) yields an empty table and
88
- // every index reads as unstyled.
89
42
  const stylesXml = partText('xl/styles.xml') ?? '';
90
43
  const { cellXfs: xfStyles, namedStyles, defaultFont } = parseStyleTable(stylesXml);
91
44
  const workbook = new Workbook();
92
- // Preserve the differential-style table verbatim so conditional formatting's dxfId references stay
93
- // valid — and a foreign dxf's number format stays a real format code — across a re-write.
94
45
  workbook[INTERNAL].restoreDifferentialStyles(parseDxfs(stylesXml));
95
- // Preserve a custom indexed-color palette verbatim so an `indexed="…"` colour reference keeps its
96
- // intended RGB across a re-write instead of resolving to a different default-palette entry.
97
46
  workbook[INTERNAL].restoreIndexedColors(parseIndexedColors(stylesXml));
98
- // Preserve the author's "Recent Colors" swatches, which the model never reads but re-writing would
99
- // otherwise discard.
100
47
  workbook[INTERNAL].restoreMruColors(parseMruColors(stylesXml));
101
- // Preserve the custom table-style definitions so a table referencing one by name still resolves to
102
- // a real definition after a re-write instead of rendering unstyled.
103
48
  workbook[INTERNAL].restoreTableStyles(parseTableStyles(stylesXml));
104
- // Preserve the theme part so a branded colour/font scheme is not overwritten by the default theme
105
- // the writer emits for a workbook that has none.
106
49
  readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook);
107
- // Preserve the named cell-style layer only when a file declares one beyond the Normal default, so an
108
- // ordinary workbook keeps an empty named-style table and emits just the default on write.
109
50
  if (namedStyles.length > 1)
110
51
  workbook[INTERNAL].restoreNamedStyles(namedStyles);
111
- // Preserve the declared default font (font id 0) so a re-write emits the face the file itself named
112
- // rather than an assumed Calibri — which would change every empty cell and the metric every
113
- // character-unit column width is expressed in.
114
52
  workbook[INTERNAL].restoreDefaultFont(defaultFont);
115
53
  const core = partText('docProps/core.xml');
116
54
  if (core !== undefined)
117
55
  applyCoreProperties(workbook, core);
118
56
  workbook.protection = parseWorkbookProtection(workbookXml);
119
57
  applyWorkbookView(workbook.view, workbookXml);
120
- // The threaded-comment author registry is workbook-level, and every conversation on every sheet
121
- // resolves its authors and @mentions through it — so it is restored before the sheet loop that reads
122
- // those conversations, not alongside the other workbook-level parts below.
123
58
  readWorkbookPersons(workbookRelsXml, pkg, workbook);
124
- // A picture used on more than one sheet is one media part; caching by media path keeps it a single
125
- // workbook image so a re-write does not duplicate the bytes.
126
59
  const imageIdByMediaPath = new Map();
127
60
  const sheetOrder = [];
128
61
  for (const { name, relId, state } of parseWorkbookSheets(workbookXml)) {
@@ -144,8 +77,6 @@ export function readXlsx(data, options = {}) {
144
77
  for (const cf of parseConditionalFormattings(sheetXml))
145
78
  sheet.addConditionalFormatting(cf);
146
79
  }
147
- // Threads before notes: a threaded cell's comments-part entry is the thread's legacy fallback, not
148
- // a note, and `applyNotes` reads the sheet's restored threads to tell the two apart.
149
80
  const threads = readSheetCommentThreads(path, pkg, workbook);
150
81
  if (threads.length > 0)
151
82
  sheet[INTERNAL].restoreCommentThreads(threads);
@@ -166,16 +97,11 @@ export function readXlsx(data, options = {}) {
166
97
  }
167
98
  readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook);
168
99
  readRootPreservedReferences(pkg, contentTypeOf, workbook);
169
- // Defined names follow the sheets: a scoped name's `localSheetId` indexes the sheet order, which
170
- // is why the names are read only once every sheet is registered.
171
100
  for (const name of parseWorkbookDefinedNames(workbookXml, sheetOrder)) {
172
101
  workbook.defineName(name);
173
102
  }
174
103
  return workbook;
175
104
  }
176
- // A sheet's comments live in a comments part reached through the sheet's own relationships: the sheet
177
- // declares a relationship of type `.../comments` whose target resolves (relative to the sheet's
178
- // directory) to the comments part. A sheet with no rels part or no such relationship simply has none.
179
105
  function readSheetComments(sheetPath, pkg) {
180
106
  const commentsPath = sheetRelTarget(sheetPath, pkg.partText, 'comments');
181
107
  if (commentsPath === undefined)
@@ -185,26 +111,12 @@ function readSheetComments(sheetPath, pkg) {
185
111
  return undefined;
186
112
  return parseComments(commentsXml);
187
113
  }
188
- // The workbook's threaded-comment identity registry: a relationship of type `.../person` names
189
- // `xl/persons/person.xml`, whose entries every message's `personId` and every mention's
190
- // `mentionpersonId` resolve through. A workbook with no threaded comments declares no such
191
- // relationship and keeps an empty registry.
192
114
  function readWorkbookPersons(workbookRelsXml, pkg, workbook) {
193
115
  const target = relationshipTargetByType(workbookRelsXml, 'person');
194
116
  const xml = target === undefined ? undefined : pkg.partText(resolveWorkbookPart(target));
195
117
  if (xml !== undefined)
196
118
  workbook[INTERNAL].restorePersons(parsePersons(xml));
197
119
  }
198
- // The workbook's theme part: the `<clrScheme>`/`<fontScheme>`/`<fmtScheme>` every `theme="n"` colour
199
- // and every `scheme="major|minor"` font in the package resolves against. It is reached through the
200
- // workbook's `.../theme` relationship rather than assumed at `xl/theme/theme1.xml`, because the target
201
- // is rel-relative and a foreign package is free to name the part anything.
202
- //
203
- // Captured with its transitive part closure, not as a lone string: a theme can carry its own
204
- // relationships (a picture used as a themed fill, wired by an `r:embed` into the theme's rels part),
205
- // and re-emitting the theme body without them would leave that reference dangling — which Excel
206
- // reports as a package needing repair. A package that declares no theme leaves the workbook on the
207
- // library's default, which is also what a dangling relationship target degrades to.
208
120
  function readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook) {
209
121
  const target = relationshipTargetByType(workbookRelsXml, 'theme');
210
122
  if (target === undefined)
@@ -214,16 +126,6 @@ function readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook) {
214
126
  if (parts !== undefined)
215
127
  workbook[INTERNAL].restoreThemePart({ entryPath, parts });
216
128
  }
217
- // A sheet's threaded conversations live in a `xl/threadedComments/threadedComment{n}.xml` part reached
218
- // through a relationship of type `.../threadedComment` on the sheet's own rels — the same discovery
219
- // shape as the notes part above, and deliberately separate from it: a thread and a legacy note are
220
- // different features that happen to share a sheet. The messages are grouped into threads and their
221
- // authors resolved against the workbook registry, so each thread lands self-contained.
222
- //
223
- // What lands here IS what a re-write emits: the thread part is re-serialised from these threads, and so is
224
- // the legacy fallback `<comment>` that binds each cell to its conversation (see `comments.ts`). Anything
225
- // this reader drops is therefore dropped from the file — which is why a message too damaged to place is
226
- // still kept wherever it can be, and why the anchor is canonicalised here rather than trusted downstream.
227
129
  function readSheetCommentThreads(sheetPath, pkg, workbook) {
228
130
  const path = sheetRelTarget(sheetPath, pkg.partText, 'threadedComment');
229
131
  const xml = path === undefined ? undefined : pkg.partText(path);
@@ -231,19 +133,10 @@ function readSheetCommentThreads(sheetPath, pkg, workbook) {
231
133
  return [];
232
134
  return buildCommentThreads(parseThreadedComments(xml), (id) => workbook.getPerson(id));
233
135
  }
234
- // A sheet's printer-settings blob is an opaque binary part linked from `<pageSetup r:id>`: the sheet
235
- // declares a relationship of type `.../printerSettings` whose target resolves to a `.bin` part. We
236
- // keep the raw bytes verbatim — the DEVMODE inside is platform-specific and the model never
237
- // interprets it, only round-trips it so re-writing the file preserves the user's print configuration.
238
- // A sheet with no rels part or no such relationship simply has none.
239
136
  function readSheetPrinterSettings(sheetPath, pkg) {
240
137
  const path = sheetRelTarget(sheetPath, pkg.partText, 'printerSettings');
241
138
  return path === undefined ? undefined : pkg.partBytes(path);
242
139
  }
243
- // A sheet's anchored images live in a drawing part reached through the sheet's own relationships: a
244
- // relationship of type `.../drawing` names the drawing part, whose own relationships map each
245
- // picture's embed id to a media part under `xl/media/`. Each anchor becomes a workbook image (deduped
246
- // by media path) placed back on the sheet at its two-cell anchor.
247
140
  function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
248
141
  const { partText, partBytes } = pkg;
249
142
  const drawingPath = sheetRelTarget(sheetPath, partText, 'drawing');
@@ -252,10 +145,6 @@ function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
252
145
  const drawingXml = partText(drawingPath);
253
146
  if (drawingXml === undefined)
254
147
  return;
255
- // A drawing that also holds a chart or shape is preserved whole (see readSheetPreservedReferences),
256
- // so its pictures must not be modeled here: modeling them would leave the sheet with images, which
257
- // suppresses that preservation and drops the chart. Leaving `sheet.images` empty routes the entire
258
- // drawing — pictures included — through byte-preservation, keeping every anchor faithful.
259
148
  if (drawingHasUnmodeledContent(drawingXml))
260
149
  return;
261
150
  const drawingRels = parseRelationships(partText(relsPathFor(drawingPath)) ?? '');
@@ -282,11 +171,6 @@ function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
282
171
  }
283
172
  }
284
173
  }
285
- // A sheet background is a workbook image referenced by the worksheet's `<picture>` element through a
286
- // sheet-local relationship of type `.../image`. Unlike an anchored image (whose image relationships
287
- // live in the drawing part's own rels), the background's relationship sits directly on the sheet, so
288
- // it is the sheet rels' sole image relationship. The bytes are deduped against images shared with a
289
- // drawing, keeping one media part per picture across a re-write.
290
174
  function readSheetBackground(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
291
175
  const mediaPath = sheetRelTarget(sheetPath, pkg.partText, 'image');
292
176
  if (mediaPath === undefined)
@@ -301,16 +185,6 @@ function readSheetBackground(sheetPath, pkg, workbook, sheet, imageIdByMediaPath
301
185
  }
302
186
  sheet.addBackgroundImage(id);
303
187
  }
304
- // Capture the worksheet-level references to package content the model does not interpret, so a
305
- // round-trip re-emits them verbatim instead of dropping them:
306
- // • `<drawing>` — but only when the reader modeled no anchored image from it: either a drawing that
307
- // holds no pictures at all (a chart or shape), or a mixed drawing whose pictures the reader
308
- // declined to model precisely so the whole part (chart included) rides here verbatim. A drawing
309
- // whose pictures were modeled is owned by the model and re-serialised from it; capturing it here
310
- // too would double-emit those pictures.
311
- // • `<legacyDrawingHF>` — a header/footer image's VML, which the model never interprets.
312
- // Each reference's target part and the transitive closure of parts it reaches (a VML's image, a
313
- // drawing's media) are captured with their bytes, content types, and relationships.
314
188
  function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, sheet) {
315
189
  const { partText, partBytes } = pkg;
316
190
  const relsXml = partText(relsPathFor(sheetPath));
@@ -324,10 +198,6 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
324
198
  if (parts !== undefined)
325
199
  sheet[INTERNAL].addPreservedReference({ element, relType, entryPath, parts });
326
200
  };
327
- // Element-wired references: a `<drawing>`/`<legacyDrawingHF>` names its part by an `r:id` in the
328
- // sheet body. A `<drawing>` is preserved only when the reader modeled no picture from it — a
329
- // chart/shape-only drawing, or a mixed one the reader left unmodeled — since one whose pictures are
330
- // modeled is re-serialised from the model.
331
201
  const referenceElements = sheet.images.length === 0 ? ['drawing', 'legacyDrawingHF'] : ['legacyDrawingHF'];
332
202
  for (const element of referenceElements) {
333
203
  const relId = worksheetReferenceRelId(sheetXml, element);
@@ -335,9 +205,6 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
335
205
  if (record !== undefined && !record.external)
336
206
  capture(element, record.type, record.target);
337
207
  }
338
- // Relationship-wired references: a pivot table or slicer is reached through a sheet relationship
339
- // with no worksheet child pointing at it — Excel discovers it by scanning the sheet's rels. Preserve
340
- // each so the pivots/slicers a fill-and-save workflow does not touch are not dropped.
341
208
  for (const record of records) {
342
209
  if (record.external)
343
210
  continue;
@@ -345,19 +212,9 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
345
212
  capture(undefined, record.type, record.target);
346
213
  }
347
214
  }
348
- // A sheet relationship the model does not consume but must round-trip: a pivot table or a slicer. Every
349
- // other sheet rel kind (drawing, printerSettings, table, comments, threadedComment, hyperlinks, background
350
- // image, the comment VML) is modeled and re-serialised from the model, so preserving it here would emit
351
- // the part twice.
352
215
  function isPreservedSheetRelType(type) {
353
216
  return type.endsWith('/pivotTable') || type.endsWith('/slicer');
354
217
  }
355
- // Capture the workbook-level references to package content the model does not interpret — pivot
356
- // caches (`pivotCacheDefinition`), slicer caches (`slicerCache`), and external links (`externalLink`,
357
- // each a link to a source workbook) — so a round-trip re-emits them instead of dropping the pivots,
358
- // slicers, and linked-workbook references they back. A pivot cache's `<pivotCaches>` registration (its
359
- // `cacheId`) and an external link's `<externalReferences>` position (its `[n]` index) are captured
360
- // alongside so the wiring a pivot table or a formula resolves through survives too.
361
218
  function readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook) {
362
219
  const { partText, partBytes } = pkg;
363
220
  const relsXml = partText('xl/_rels/workbook.xml.rels');
@@ -383,11 +240,6 @@ function readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbo
383
240
  });
384
241
  }
385
242
  }
386
- // Content wired from the package's own `_rels/.rels` that the writer does not regenerate from the
387
- // model — the ribbon customUI parts, custom document properties, a thumbnail. The writer rebuilds the
388
- // root rels for the parts it models (the workbook, and core/app properties), so every other root
389
- // relationship's target would be dropped on write; capturing its closure here re-declares it verbatim.
390
- // External targets and the three regenerated relationship types are skipped.
391
243
  function readRootPreservedReferences(pkg, contentTypeOf, workbook) {
392
244
  const { partText, partBytes } = pkg;
393
245
  const relsXml = partText('_rels/.rels');
@@ -403,31 +255,17 @@ function readRootPreservedReferences(pkg, contentTypeOf, workbook) {
403
255
  workbook[INTERNAL].addPreservedRootReference({ relType: record.type, entryPath, parts });
404
256
  }
405
257
  }
406
- // The three root relationships the writer regenerates from the model on every write: the office
407
- // document and the core/extended document properties. Every other root relationship is unmodeled and
408
- // is preserved verbatim by {@link readRootPreservedReferences} rather than dropped.
409
258
  function isRegeneratedRootRelType(type) {
410
259
  return (type.endsWith('/officeDocument') ||
411
260
  type.endsWith('/core-properties') ||
412
261
  type.endsWith('/extended-properties'));
413
262
  }
414
- // A workbook relationship the model does not consume but must round-trip: a pivot cache, a slicer
415
- // cache, an external link (the pointer to a linked source workbook), or a macro-enabled workbook's VBA
416
- // project. Worksheets, styles, theme, shared strings, and the threaded-comment person registry are modeled
417
- // and re-serialised from the model. Preserving vbaProject here — rather than silently dropping it, as an
418
- // unrecognised relationship type otherwise would — is what keeps loading and re-saving a .xlsm from
419
- // discarding its macros; the content-type override in workbook-xml.ts is the other half, so the re-emitted
420
- // package still declares itself macro-enabled. Preserving externalLink is what keeps a formula's `[n]`
421
- // external reference from dangling: the link part and its `<externalReferences>` registration are both
422
- // re-emitted.
423
263
  function isPreservedWorkbookRelType(type) {
424
264
  return (type.endsWith('/pivotCacheDefinition') ||
425
265
  type.endsWith('/slicerCache') ||
426
266
  type.endsWith('/vbaProject') ||
427
267
  type.endsWith('/externalLink'));
428
268
  }
429
- // Map each `<pivotCache>` registration in the workbook's `<pivotCaches>` to the relationship id that
430
- // reaches its cache definition, so a preserved cache carries the `cacheId` a pivot table refers to.
431
269
  function parsePivotCacheRegistrations(workbookXml) {
432
270
  const byRelId = new Map();
433
271
  for (const { attrs } of openElements(workbookXml, 'pivotCache')) {
@@ -437,10 +275,6 @@ function parsePivotCacheRegistrations(workbookXml) {
437
275
  }
438
276
  return byRelId;
439
277
  }
440
- // Map each `<externalReference>` in the workbook's `<externalReferences>` to its 0-based position, keyed
441
- // by the relationship id it wires. That position is the `[n]` index a formula or defined name resolves
442
- // an external cell through (`[1]Sheet!$A$1`), so preserving it lets the writer re-emit the block in the
443
- // original order and keep every `[n]` pointing at the same linked workbook.
444
278
  function parseExternalReferenceRegistrations(workbookXml) {
445
279
  const byRelId = new Map();
446
280
  let index = 0;
@@ -450,10 +284,6 @@ function parseExternalReferenceRegistrations(workbookXml) {
450
284
  }
451
285
  return byRelId;
452
286
  }
453
- // The `r:id` of the first `<drawing>` / `<legacyDrawingHF>` element in a worksheet, or undefined when
454
- // the sheet declares none. The reference lives in the worksheet XML (not distinguishable by
455
- // relationship Type — a header/footer VML and a comment VML share the `vmlDrawing` type), so the
456
- // specific relationship is found by reading the element's `r:id` here.
457
287
  function worksheetReferenceRelId(sheetXml, element) {
458
288
  for (const { attrs } of openElements(sheetXml, element)) {
459
289
  if (attrs['r:id'] !== undefined)
@@ -461,10 +291,6 @@ function worksheetReferenceRelId(sheetXml, element) {
461
291
  }
462
292
  return undefined;
463
293
  }
464
- // A sheet's tables live in `xl/tables/table{n}.xml` parts, each reached through a relationship of
465
- // type `.../table` on the sheet's own rels. The writer emits one relationship per table; each part
466
- // is parsed back into the model and re-registered in definition order. A part that fails to parse
467
- // (missing name/ref/columns — Excel corruption) is skipped rather than crashing the whole read.
468
294
  function readSheetTables(sheetPath, pkg, sheet) {
469
295
  const relsXml = pkg.partText(relsPathFor(sheetPath));
470
296
  if (relsXml === undefined)
@@ -479,13 +305,6 @@ function readSheetTables(sheetPath, pkg, sheet) {
479
305
  }
480
306
  dropMergesInsideTables(sheet);
481
307
  }
482
- // Reconstruct an inspectable model of each pivot table hosted on a sheet. A pivot is reached by a
483
- // sheet relationship of type `.../pivotTable`; the pivot-table part carries its own relationship of
484
- // type `.../pivotCacheDefinition` to the cache holding the field catalogue and source range. Both
485
- // parts are parsed and combined into a read-only view registered on the sheet — separate from the
486
- // byte-preservation that actually round-trips the pivot, so this never changes what is re-emitted.
487
- // The read is lenient: a pivot whose cache is missing still yields a (partial) model rather than
488
- // throwing, matching Excel's tolerance for a damaged package on load.
489
308
  function readSheetPivotTables(sheetPath, pkg, sheet) {
490
309
  const { partText } = pkg;
491
310
  const relsXml = partText(relsPathFor(sheetPath));
@@ -503,10 +322,6 @@ function readSheetPivotTables(sheetPath, pkg, sheet) {
503
322
  sheet[INTERNAL].addLoadedPivotTable(parsePivotTable(tableXml, cacheXml));
504
323
  }
505
324
  }
506
- // Excel forbids a merged range inside a formatted table and repairs such a file on load by dropping
507
- // the merge. A worksheet's merges are read before its tables, so a real file carrying that invalid
508
- // geometry lands in the model intact; this applies the same repair once the tables are known, so a
509
- // re-write does not surface the Excel-invalid geometry the writer (correctly) rejects.
510
325
  function dropMergesInsideTables(sheet) {
511
326
  const regions = sheet.tables.map((table) => table.region);
512
327
  if (regions.length === 0)
@@ -536,10 +351,6 @@ export function parseWorkbookSheets(xml) {
536
351
  }
537
352
  return sheets;
538
353
  }
539
- // Read the workbook's structure/window protection (`<workbookProtection>`). The three lock flags are
540
- // decoded as booleans (an absent or "0" attribute stays unlocked), and only the whitelisted
541
- // password/agile-hash attributes are preserved verbatim — a hostile or unknown attribute is dropped
542
- // rather than echoed back on write. Returns undefined when the workbook declares no protection.
543
354
  export function parseWorkbookProtection(xml) {
544
355
  let result;
545
356
  parseXml(xml, {
@@ -566,14 +377,6 @@ export function parseWorkbookProtection(xml) {
566
377
  });
567
378
  return result;
568
379
  }
569
- // Restore the workbook's saved window state from `<bookViews><workbookView/>` onto the model's view,
570
- // so a round-trip hands back the geometry and active tab the author left rather than stamping the
571
- // library's defaults over them. Only the first `<workbookView>` is read — the model carries one view,
572
- // which is all Excel writes and all a single consuming window can restore.
573
- //
574
- // Each attribute is applied only when the source carried a usable value; an absent or non-numeric one
575
- // leaves the default in place, so a truncated or hostile element degrades to a valid window rather
576
- // than a NaN geometry that would serialise as garbage.
577
380
  export function applyWorkbookView(view, xml) {
578
381
  for (const { attrs } of openElements(xml, 'workbookView')) {
579
382
  applyViewNumber(attrs.xWindow, (value) => (view.x = value));
@@ -596,10 +399,6 @@ function applyViewNumber(raw, assign) {
596
399
  if (Number.isFinite(value))
597
400
  assign(Math.trunc(value));
598
401
  }
599
- // Reconstruct the workbook's defined names. Each `<definedName>` carries its name (and optional
600
- // comment/hidden flag) as attributes and its refersTo formula as text content; a `localSheetId`
601
- // maps back through the sheet order to the scope sheet's name. A name whose localSheetId is out of
602
- // range (a foreign file referencing a sheet we did not load) is left global rather than dropped.
603
402
  function parseWorkbookDefinedNames(xml, sheetOrder) {
604
403
  const names = [];
605
404
  let capture = false;
@@ -609,9 +408,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
609
408
  onOpen(name, attrs) {
610
409
  if (localName(name) !== 'definedName' || attrs.name === undefined)
611
410
  return;
612
- // `_xlnm._FilterDatabase` is the built-in Excel derives from a sheet's autofilter, not a
613
- // user-defined name: it is reconstructed from the sheet's `<autoFilter>` element, so skip it
614
- // here to keep it off `Workbook.definedNames` and out of a duplicating round-trip.
615
411
  if (attrs.name === '_xlnm._FilterDatabase')
616
412
  return;
617
413
  capture = true;
@@ -633,8 +429,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
633
429
  onClose(name) {
634
430
  if (localName(name) !== 'definedName' || pending === undefined)
635
431
  return;
636
- // Strip the `_xlfn.`/`_xlpm.` prefixes back to the readable name, the same normalisation the
637
- // reader applies to a cell formula, so the model never holds the on-disk mangling.
638
432
  names.push({ ...pending, refersTo: unmangleFunctions(refersTo) });
639
433
  capture = false;
640
434
  pending = undefined;
@@ -642,8 +436,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
642
436
  });
643
437
  return names;
644
438
  }
645
- // Core document properties live in docProps/core.xml under mixed namespaces
646
- // (dc:creator, cp:lastModifiedBy, dcterms:created/modified); local names disambiguate.
647
439
  const CORE_PROPERTY_LOCAL_NAMES = new Set(['creator', 'lastModifiedBy', 'created', 'modified']);
648
440
  function applyCoreProperties(workbook, xml) {
649
441
  let capture = '';
@@ -1,10 +1,3 @@
1
- // The relationship-type URIs and namespace groupings the writer's serialisers reference. The
2
- // canonical namespace set lives in `namespaces.ts`; the writer-local groupings here (the props
3
- // vocabularies, the content-types namespace, and the rel-type URIs derived from them) are plumbing
4
- // every part of the writer references.
5
- //
6
- // The `.rels` part envelope these types are written into is container-level, not SpreadsheetML, and
7
- // lives in `../opc/rels.ts`.
8
1
  import { CONTENT_TYPES_NS, PKG_RELS_NS, RELATIONSHIPS_NS } from '../opc/namespaces.js';
9
2
  import { SPREADSHEETML_NS } from './namespaces.js';
10
3
  export const NS = {
@@ -19,8 +12,6 @@ export const NS = {
19
12
  dcmitype: 'http://purl.org/dc/dcmitype/',
20
13
  xsi: 'http://www.w3.org/2001/XMLSchema-instance',
21
14
  };
22
- // The Office extension relationships namespace the 2018 threaded-comment feature wires both of its parts
23
- // through. Local to this table: nothing outside it needs the base URI.
24
15
  const MS_OFFICE_2017_RELS_NS = 'http://schemas.microsoft.com/office/2017/10/relationships';
25
16
  export const REL = {
26
17
  worksheet: `${NS.docRels}/worksheet`,
@@ -40,8 +31,6 @@ export const REL = {
40
31
  pivotTable: `${NS.docRels}/pivotTable`,
41
32
  pivotCacheDefinition: `${NS.docRels}/pivotCacheDefinition`,
42
33
  pivotCacheRecords: `${NS.docRels}/pivotCacheRecords`,
43
- // Threaded comments are a Microsoft extension, so both types live under the 2017/10 Office
44
- // relationships namespace rather than the standard officeDocument one.
45
34
  threadedComment: `${MS_OFFICE_2017_RELS_NS}/threadedComment`,
46
35
  person: `${MS_OFFICE_2017_RELS_NS}/person`,
47
36
  };
@@ -1,52 +1,36 @@
1
- // Rich-text run accumulation, shared by the two readers that parse `<r>` runs identically: an inline
2
- // string's `<is>` in a worksheet body and a pooled `<si>` in sharedStrings.xml. One `<r>` opens a run
3
- // (resetting its font and text so an unformatted run inherits nothing from the last), an `<rPr>` opens
4
- // the run's font bundle whose self-closing children each set one facet, and a `<t>` appends the run's
5
- // text; the run commits on `</r>`. The surrounding parser owns only whether it is inside an `<is>`/`<si>`
6
- // and where a bare (non-run) `<t>` goes.
7
1
  import { applyFontChild } from './read-styles.js';
8
2
  export class RunAccumulator {
9
3
  #runs = [];
10
4
  #font = null;
11
5
  #text = '';
12
6
  #inRun = false;
13
- // Discard accumulated runs, readying the accumulator for a fresh `<is>`/`<si>`. A new array is
14
- // installed, so a value already built from a previous reset's runs keeps its own array.
15
7
  reset() {
16
8
  this.#runs = [];
17
9
  this.#font = null;
18
10
  this.#inRun = false;
19
11
  }
20
- /** The runs gathered so far. */
21
12
  get runs() {
22
13
  return this.#runs;
23
14
  }
24
- // Open a run: reset the per-run font and text so an unformatted run inherits nothing from the last.
25
15
  beginRun() {
26
16
  this.#inRun = true;
27
17
  this.#font = null;
28
18
  this.#text = '';
29
19
  }
30
- // Open the run's `<rPr>` font bundle; its children stream in via {@link applyProperty}.
31
20
  beginProperties() {
32
21
  if (this.#inRun)
33
22
  this.#font = {};
34
23
  }
35
- // Apply one `<rPr>` child (`<b/>`, `<sz>`, `<color>`, `<rFont>`, …) to the open run's font; a no-op
36
- // when no `<rPr>` is open.
37
24
  applyProperty(local, attrs) {
38
25
  if (this.#font !== null)
39
26
  applyFontChild(this.#font, local, attrs);
40
27
  }
41
- // Append a `<t>`'s text to the open run. Returns false when no run is open, so the caller routes the
42
- // text to its surrounding plain/inline string instead.
43
28
  appendText(text) {
44
29
  if (!this.#inRun)
45
30
  return false;
46
31
  this.#text += text;
47
32
  return true;
48
33
  }
49
- // Commit the open run — with its font, only if that font set at least one facet — to the runs list.
50
34
  endRun() {
51
35
  if (!this.#inRun)
52
36
  return;
@@ -1,17 +1,5 @@
1
- // Rich-text runs — a cell value composed of independently-formatted text runs.
2
- //
3
- // OOXML stores rich text as a sequence of `<r>` runs, each an optional `<rPr>` (the run's font, in
4
- // the CT_RPrElt shape — identical to a styles `<font>` except the face element is `<rFont>`, not
5
- // `<name>`) followed by a `<t>` text element. The writer serialises a rich-text value inline
6
- // (`t="inlineStr"`), never into the shared-strings table, matching how it writes every other string
7
- // value; the reader reconstructs the runs while scanning the inline string.
8
1
  import { textElement } from '../../xml/xml.js';
9
2
  import { fontXml } from './styles.js';
10
- /**
11
- * Serialise a rich-text value's runs as the inner content of an `<is>` element. A zero-length run
12
- * is dropped: an empty `<t/>` is schema-invalid — Excel flags the file as corrupt — and an empty
13
- * run contributes nothing to the rendered text, so omitting it is loss-free.
14
- */
15
3
  export function richTextRunsXml(runs) {
16
4
  return runs
17
5
  .filter((run) => run.text !== '')
@@ -1,13 +1,6 @@
1
- // Shared-formula group planning: assigning each master/clone group a sheet-unique index and the
2
- // master's `ref` range before the row loop serialises a single cell.
3
1
  import { encodeAddress } from '../../core/address.js';
4
2
  import { isFormulaValue, isSharedFormulaValue } from '../../core/value.js';
5
3
  import { AuthoringError } from '../../errors.js';
6
- // Plan a sheet's shared-formula groups: every clone cell (a {@link SharedFormulaValue}) names its
7
- // master by address, so group the clones by master, assign each group a sheet-unique `si`, and record
8
- // the `ref` range (master through the furthest clone) on the master. Excel requires the master to sit
9
- // at the top-left of that range, so a clone above or left of its master — or a master with no formula
10
- // (an orphan) — is rejected here, named, rather than emitted as a package Excel repairs on open.
11
4
  export function planSharedFormulas(sheet) {
12
5
  const groups = new Map();
13
6
  for (const { cells } of sheet.rows()) {
@@ -1,29 +1,10 @@
1
- // The shared-strings table: `xl/sharedStrings.xml`, the workbook-wide pool a `t="s"` cell indexes.
2
- //
3
- // When the writer runs with `useSharedStrings`, a plain string cell value is interned here — an
4
- // identical string is stored once and every cell holding it references the single `<si>` entry by
5
- // index — rather than repeating the text inline in each cell. This trades a second part and an
6
- // indirection for a smaller package when strings repeat, the storage Excel itself prefers.
7
- //
8
- // Both plain strings and rich text are pooled: a plain value becomes a `<si><t>…</t></si>` entry, a
9
- // rich value a `<si><r>…</r>…</si>` entry carrying its per-run formatting — the rich `<si>` runs Excel
10
- // itself writes. A `t="s"` cell then indexes either, and the reader reconstructs the runs, so pooled
11
- // rich text round-trips its formatting rather than flattening to text.
12
1
  import { textElement, XML_DECLARATION } from '../../xml/xml.js';
13
2
  import { SPREADSHEETML_NS } from './namespaces.js';
14
3
  import { richTextRunsXml } from './rich-text.js';
15
- /**
16
- * Interns cell string values into the shared-strings pool. {@link intern} returns the index a
17
- * `t="s"` cell writes as its `<v>`; identical entries collapse to one, so `count` (total references)
18
- * and `uniqueCount` (distinct entries) diverge exactly as Excel records them. Each entry is stored as
19
- * its rendered `<si>` inner XML, which is also its dedup key — a plain string (`<t>…`) and rich runs
20
- * (`<r>…`) render to distinct markup, so the two kinds never collide in the pool.
21
- */
22
4
  export class SharedStringTable {
23
5
  #indexByEntry = new Map();
24
6
  #entries = [];
25
7
  #references = 0;
26
- /** Intern a plain or rich string and return its `<si>` index, reusing the entry when it repeats. */
27
8
  intern(value) {
28
9
  this.#references += 1;
29
10
  const inner = typeof value === 'string' ? textElement(value) : richTextRunsXml(value.richText);
@@ -35,11 +16,9 @@ export class SharedStringTable {
35
16
  this.#entries.push(inner);
36
17
  return index;
37
18
  }
38
- /** Whether no string has been interned — the writer omits the part entirely when so. */
39
19
  get isEmpty() {
40
20
  return this.#entries.length === 0;
41
21
  }
42
- /** Serialise the pool as the `xl/sharedStrings.xml` part. */
43
22
  toXml() {
44
23
  const items = this.#entries.map((inner) => `<si>${inner}</si>`).join('');
45
24
  return (XML_DECLARATION +