@shbernal/ts-xlsx 1.0.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 (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,680 @@
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
+ import { decodeRange } from "../../core/address.js";
20
+ import { unmangleFunctions } from "../../core/formula.js";
21
+ import { INTERNAL } from "../../core/internal.js";
22
+ import { Workbook } from "../../core/workbook.js";
23
+ import { WORKBOOK_PROTECTION_CREDENTIAL_ATTRS, } from "../../core/workbook-protection.js";
24
+ import { boolStrict, localName, openElements, parseXml } from "../../xml/xml-read.js";
25
+ import { UnsupportedFormatError } from "../opc/errors.js";
26
+ import { extensionOf, relsPathFor } from "../opc/part-paths.js";
27
+ import { capturePartClosure, contentTypeResolver, packageAccessors, parseRelationshipRecords, parseRelationships, relationshipTargetByType, relationshipTargetsByType, resolveRelativePart, resolveWorkbookPart, sheetRelTarget, } from "../opc/read-opc.js";
28
+ import { DEFAULT_MAX_UNCOMPRESSED } from "../opc/read-options.js";
29
+ import { inflateSpreadsheetPackage } from "../opc/sniff-format.js";
30
+ import { readXlsbPackage, XLSB_WORKBOOK_PART } from "../xlsb/read.js";
31
+ import { applyNotes, parseComments } from "./comments.js";
32
+ import { parseConditionalFormattings, parseDxfs } from "./conditional-formatting.js";
33
+ import { applyDataValidations, parseDataValidations, parseExtendedDataValidations, } from "./data-validation.js";
34
+ import { applyHyperlinks, parseSheetHyperlinks } from "./hyperlinks.js";
35
+ import { drawingHasUnmodeledContent, parseDrawing } from "./images.js";
36
+ import { parsePivotTable } from "./pivot-read.js";
37
+ import { parseStyleTable } from "./read-styles.js";
38
+ import { parseWorksheet } from "./read-worksheet.js";
39
+ import { parseSharedStrings } from "./shared-strings-read.js";
40
+ import { parseIndexedColors, parseMruColors, parseTableStyles } from "./styles.js";
41
+ import { parseTable } from "./tables.js";
42
+ 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
+ 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
+ export { DEFAULT_MAX_UNCOMPRESSED } from "../opc/read-options.js";
50
+ 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 {@link 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
+ export function readXlsx(data, options = {}) {
67
+ const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
68
+ const files = inflateSpreadsheetPackage(data, cap);
69
+ const pkg = packageAccessors(files);
70
+ const { partText } = pkg;
71
+ const workbookXml = partText('xl/workbook.xml');
72
+ 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
+ if (files[XLSB_WORKBOOK_PART] !== undefined)
76
+ return readXlsbPackage(files);
77
+ throw new UnsupportedFormatError('unknown');
78
+ }
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
+ const contentTypeOf = contentTypeResolver(partText('[Content_Types].xml') ?? '');
83
+ const workbookRelsXml = partText('xl/_rels/workbook.xml.rels') ?? '';
84
+ const rels = parseRelationships(workbookRelsXml);
85
+ 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
+ const stylesXml = partText('xl/styles.xml') ?? '';
90
+ const { cellXfs: xfStyles, namedStyles, defaultFont } = parseStyleTable(stylesXml);
91
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ if (namedStyles.length > 1)
110
+ 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
+ workbook[INTERNAL].restoreDefaultFont(defaultFont);
115
+ const core = partText('docProps/core.xml');
116
+ if (core !== undefined)
117
+ applyCoreProperties(workbook, core);
118
+ workbook.protection = parseWorkbookProtection(workbookXml);
119
+ 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
+ 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
+ const imageIdByMediaPath = new Map();
127
+ const sheetOrder = [];
128
+ for (const { name, relId, state } of parseWorkbookSheets(workbookXml)) {
129
+ const target = rels.get(relId);
130
+ const sheet = workbook.addWorksheet(name, state === undefined ? undefined : { state });
131
+ sheetOrder.push(name);
132
+ const path = target === undefined ? undefined : resolveWorkbookPart(target);
133
+ const sheetXml = path === undefined ? undefined : partText(path);
134
+ if (sheetXml !== undefined)
135
+ parseWorksheet(sheetXml, sheet, sharedStrings, xfStyles);
136
+ if (path !== undefined) {
137
+ if (sheetXml !== undefined) {
138
+ const sheetRels = parseRelationships(partText(relsPathFor(path)) ?? '');
139
+ applyHyperlinks(sheet, parseSheetHyperlinks(sheetXml), sheetRels);
140
+ applyDataValidations(sheet, [
141
+ ...parseDataValidations(sheetXml),
142
+ ...parseExtendedDataValidations(sheetXml),
143
+ ]);
144
+ for (const cf of parseConditionalFormattings(sheetXml))
145
+ sheet.addConditionalFormatting(cf);
146
+ }
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
+ const threads = readSheetCommentThreads(path, pkg, workbook);
150
+ if (threads.length > 0)
151
+ sheet[INTERNAL].restoreCommentThreads(threads);
152
+ const comments = readSheetComments(path, pkg);
153
+ if (comments !== undefined)
154
+ applyNotes(sheet, comments);
155
+ readSheetImages(path, pkg, workbook, sheet, imageIdByMediaPath);
156
+ readSheetBackground(path, pkg, workbook, sheet, imageIdByMediaPath);
157
+ if (sheetXml !== undefined) {
158
+ readSheetPreservedReferences(path, sheetXml, pkg, contentTypeOf, sheet);
159
+ }
160
+ readSheetTables(path, pkg, sheet);
161
+ readSheetPivotTables(path, pkg, sheet);
162
+ const printerSettings = readSheetPrinterSettings(path, pkg);
163
+ if (printerSettings !== undefined)
164
+ sheet.pageSetup.printerSettings = printerSettings;
165
+ }
166
+ }
167
+ readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook);
168
+ 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
+ for (const name of parseWorkbookDefinedNames(workbookXml, sheetOrder)) {
172
+ workbook.defineName(name);
173
+ }
174
+ return workbook;
175
+ }
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
+ function readSheetComments(sheetPath, pkg) {
180
+ const commentsPath = sheetRelTarget(sheetPath, pkg.partText, 'comments');
181
+ if (commentsPath === undefined)
182
+ return undefined;
183
+ const commentsXml = pkg.partText(commentsPath);
184
+ if (commentsXml === undefined)
185
+ return undefined;
186
+ return parseComments(commentsXml);
187
+ }
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
+ function readWorkbookPersons(workbookRelsXml, pkg, workbook) {
193
+ const target = relationshipTargetByType(workbookRelsXml, 'person');
194
+ const xml = target === undefined ? undefined : pkg.partText(resolveWorkbookPart(target));
195
+ if (xml !== undefined)
196
+ workbook[INTERNAL].restorePersons(parsePersons(xml));
197
+ }
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
+ function readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook) {
209
+ const target = relationshipTargetByType(workbookRelsXml, 'theme');
210
+ if (target === undefined)
211
+ return;
212
+ const entryPath = resolveWorkbookPart(target);
213
+ const parts = capturePartClosure(entryPath, pkg.partText, pkg.partBytes, contentTypeOf);
214
+ if (parts !== undefined)
215
+ workbook[INTERNAL].restoreThemePart({ entryPath, parts });
216
+ }
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
+ function readSheetCommentThreads(sheetPath, pkg, workbook) {
228
+ const path = sheetRelTarget(sheetPath, pkg.partText, 'threadedComment');
229
+ const xml = path === undefined ? undefined : pkg.partText(path);
230
+ if (xml === undefined)
231
+ return [];
232
+ return buildCommentThreads(parseThreadedComments(xml), (id) => workbook.getPerson(id));
233
+ }
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
+ function readSheetPrinterSettings(sheetPath, pkg) {
240
+ const path = sheetRelTarget(sheetPath, pkg.partText, 'printerSettings');
241
+ return path === undefined ? undefined : pkg.partBytes(path);
242
+ }
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
+ function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
248
+ const { partText, partBytes } = pkg;
249
+ const drawingPath = sheetRelTarget(sheetPath, partText, 'drawing');
250
+ if (drawingPath === undefined)
251
+ return;
252
+ const drawingXml = partText(drawingPath);
253
+ if (drawingXml === undefined)
254
+ 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
+ if (drawingHasUnmodeledContent(drawingXml))
260
+ return;
261
+ const drawingRels = parseRelationships(partText(relsPathFor(drawingPath)) ?? '');
262
+ for (const anchor of parseDrawing(drawingXml)) {
263
+ const target = drawingRels.get(anchor.embed);
264
+ if (target === undefined)
265
+ continue;
266
+ const mediaPath = resolveRelativePart(drawingPath, target);
267
+ let id = imageIdByMediaPath.get(mediaPath);
268
+ if (id === undefined) {
269
+ const bytes = partBytes(mediaPath);
270
+ if (bytes === undefined)
271
+ continue;
272
+ id = workbook.addImage({ buffer: bytes, extension: extensionOf(mediaPath) });
273
+ imageIdByMediaPath.set(mediaPath, id);
274
+ }
275
+ const rot = anchor.rotation !== undefined ? { rotation: anchor.rotation } : {};
276
+ if (anchor.to !== undefined) {
277
+ const mode = anchor.editAs !== undefined ? { editAs: anchor.editAs } : {};
278
+ sheet.addImageAnchor(id, { from: anchor.from, to: anchor.to, ...mode, ...rot });
279
+ }
280
+ else if (anchor.ext !== undefined) {
281
+ sheet.addImageAnchor(id, { from: anchor.from, ext: anchor.ext, ...rot });
282
+ }
283
+ }
284
+ }
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
+ function readSheetBackground(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
291
+ const mediaPath = sheetRelTarget(sheetPath, pkg.partText, 'image');
292
+ if (mediaPath === undefined)
293
+ return;
294
+ let id = imageIdByMediaPath.get(mediaPath);
295
+ if (id === undefined) {
296
+ const bytes = pkg.partBytes(mediaPath);
297
+ if (bytes === undefined)
298
+ return;
299
+ id = workbook.addImage({ buffer: bytes, extension: extensionOf(mediaPath) });
300
+ imageIdByMediaPath.set(mediaPath, id);
301
+ }
302
+ sheet.addBackgroundImage(id);
303
+ }
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
+ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, sheet) {
315
+ const { partText, partBytes } = pkg;
316
+ const relsXml = partText(relsPathFor(sheetPath));
317
+ if (relsXml === undefined)
318
+ return;
319
+ const records = parseRelationshipRecords(relsXml);
320
+ const recordById = new Map(records.map((record) => [record.id, record]));
321
+ const capture = (element, relType, target) => {
322
+ const entryPath = resolveRelativePart(sheetPath, target);
323
+ const parts = capturePartClosure(entryPath, partText, partBytes, contentTypeOf);
324
+ if (parts !== undefined)
325
+ sheet[INTERNAL].addPreservedReference({ element, relType, entryPath, parts });
326
+ };
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
+ const referenceElements = sheet.images.length === 0 ? ['drawing', 'legacyDrawingHF'] : ['legacyDrawingHF'];
332
+ for (const element of referenceElements) {
333
+ const relId = worksheetReferenceRelId(sheetXml, element);
334
+ const record = relId === undefined ? undefined : recordById.get(relId);
335
+ if (record !== undefined && !record.external)
336
+ capture(element, record.type, record.target);
337
+ }
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
+ for (const record of records) {
342
+ if (record.external)
343
+ continue;
344
+ if (isPreservedSheetRelType(record.type))
345
+ capture(undefined, record.type, record.target);
346
+ }
347
+ }
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
+ function isPreservedSheetRelType(type) {
353
+ return type.endsWith('/pivotTable') || type.endsWith('/slicer');
354
+ }
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
+ function readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook) {
362
+ const { partText, partBytes } = pkg;
363
+ const relsXml = partText('xl/_rels/workbook.xml.rels');
364
+ if (relsXml === undefined)
365
+ return;
366
+ const cacheIdByRelId = parsePivotCacheRegistrations(workbookXml);
367
+ const externalIndexByRelId = parseExternalReferenceRegistrations(workbookXml);
368
+ for (const record of parseRelationshipRecords(relsXml)) {
369
+ if (record.external || !isPreservedWorkbookRelType(record.type))
370
+ continue;
371
+ const entryPath = resolveWorkbookPart(record.target);
372
+ const parts = capturePartClosure(entryPath, partText, partBytes, contentTypeOf);
373
+ if (parts === undefined)
374
+ continue;
375
+ const cacheId = cacheIdByRelId.get(record.id);
376
+ const externalReferenceIndex = externalIndexByRelId.get(record.id);
377
+ workbook[INTERNAL].addPreservedReference({
378
+ relType: record.type,
379
+ entryPath,
380
+ parts,
381
+ ...(cacheId !== undefined ? { pivotCacheId: cacheId } : {}),
382
+ ...(externalReferenceIndex !== undefined ? { externalReferenceIndex } : {}),
383
+ });
384
+ }
385
+ }
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
+ function readRootPreservedReferences(pkg, contentTypeOf, workbook) {
392
+ const { partText, partBytes } = pkg;
393
+ const relsXml = partText('_rels/.rels');
394
+ if (relsXml === undefined)
395
+ return;
396
+ for (const record of parseRelationshipRecords(relsXml)) {
397
+ if (record.external || isRegeneratedRootRelType(record.type))
398
+ continue;
399
+ const entryPath = resolveRelativePart('', record.target);
400
+ const parts = capturePartClosure(entryPath, partText, partBytes, contentTypeOf);
401
+ if (parts === undefined)
402
+ continue;
403
+ workbook[INTERNAL].addPreservedRootReference({ relType: record.type, entryPath, parts });
404
+ }
405
+ }
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
+ function isRegeneratedRootRelType(type) {
410
+ return (type.endsWith('/officeDocument') ||
411
+ type.endsWith('/core-properties') ||
412
+ type.endsWith('/extended-properties'));
413
+ }
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
+ function isPreservedWorkbookRelType(type) {
424
+ return (type.endsWith('/pivotCacheDefinition') ||
425
+ type.endsWith('/slicerCache') ||
426
+ type.endsWith('/vbaProject') ||
427
+ type.endsWith('/externalLink'));
428
+ }
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
+ function parsePivotCacheRegistrations(workbookXml) {
432
+ const byRelId = new Map();
433
+ for (const { attrs } of openElements(workbookXml, 'pivotCache')) {
434
+ if (attrs['r:id'] !== undefined && attrs.cacheId !== undefined) {
435
+ byRelId.set(attrs['r:id'], attrs.cacheId);
436
+ }
437
+ }
438
+ return byRelId;
439
+ }
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
+ function parseExternalReferenceRegistrations(workbookXml) {
445
+ const byRelId = new Map();
446
+ let index = 0;
447
+ for (const { attrs } of openElements(workbookXml, 'externalReference')) {
448
+ if (attrs['r:id'] !== undefined)
449
+ byRelId.set(attrs['r:id'], index++);
450
+ }
451
+ return byRelId;
452
+ }
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
+ function worksheetReferenceRelId(sheetXml, element) {
458
+ for (const { attrs } of openElements(sheetXml, element)) {
459
+ if (attrs['r:id'] !== undefined)
460
+ return attrs['r:id'];
461
+ }
462
+ return undefined;
463
+ }
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
+ function readSheetTables(sheetPath, pkg, sheet) {
469
+ const relsXml = pkg.partText(relsPathFor(sheetPath));
470
+ if (relsXml === undefined)
471
+ return;
472
+ for (const target of relationshipTargetsByType(relsXml, 'table')) {
473
+ const tableXml = pkg.partText(resolveRelativePart(sheetPath, target));
474
+ if (tableXml === undefined)
475
+ continue;
476
+ const options = parseTable(tableXml);
477
+ if (options !== undefined)
478
+ sheet.addTable(options);
479
+ }
480
+ dropMergesInsideTables(sheet);
481
+ }
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
+ function readSheetPivotTables(sheetPath, pkg, sheet) {
490
+ const { partText } = pkg;
491
+ const relsXml = partText(relsPathFor(sheetPath));
492
+ if (relsXml === undefined)
493
+ return;
494
+ for (const target of relationshipTargetsByType(relsXml, 'pivotTable')) {
495
+ const tablePath = resolveRelativePart(sheetPath, target);
496
+ const tableXml = partText(tablePath);
497
+ if (tableXml === undefined)
498
+ continue;
499
+ const cacheTarget = relationshipTargetByType(partText(relsPathFor(tablePath)) ?? '', 'pivotCacheDefinition');
500
+ const cacheXml = cacheTarget === undefined
501
+ ? ''
502
+ : (partText(resolveRelativePart(tablePath, cacheTarget)) ?? '');
503
+ sheet[INTERNAL].addLoadedPivotTable(parsePivotTable(tableXml, cacheXml));
504
+ }
505
+ }
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
+ function dropMergesInsideTables(sheet) {
511
+ const regions = sheet.tables.map((table) => table.region);
512
+ if (regions.length === 0)
513
+ return;
514
+ for (const range of [...sheet.merges]) {
515
+ const { top, left, bottom, right } = decodeRange(range);
516
+ if (top === undefined || left === undefined || bottom === undefined || right === undefined)
517
+ continue;
518
+ const overlaps = regions.some((region) => left <= region.right &&
519
+ right >= region.left &&
520
+ top <= region.bottom &&
521
+ bottom >= region.top);
522
+ if (overlaps)
523
+ sheet.unmergeCells(range);
524
+ }
525
+ }
526
+ export function parseWorkbookSheets(xml) {
527
+ const sheets = [];
528
+ for (const { attrs } of openElements(xml, 'sheet')) {
529
+ const entry = {
530
+ name: attrs.name ?? '',
531
+ relId: attrs['r:id'] ?? '',
532
+ };
533
+ if (attrs.state === 'hidden' || attrs.state === 'veryHidden')
534
+ entry.state = attrs.state;
535
+ sheets.push(entry);
536
+ }
537
+ return sheets;
538
+ }
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
+ export function parseWorkbookProtection(xml) {
544
+ let result;
545
+ parseXml(xml, {
546
+ onOpen(name, attrs) {
547
+ if (localName(name) !== 'workbookProtection')
548
+ return;
549
+ const protection = {};
550
+ if (boolStrict(attrs.lockStructure))
551
+ protection.lockStructure = true;
552
+ if (boolStrict(attrs.lockWindows))
553
+ protection.lockWindows = true;
554
+ if (boolStrict(attrs.lockRevision))
555
+ protection.lockRevision = true;
556
+ const credentials = {};
557
+ for (const key of WORKBOOK_PROTECTION_CREDENTIAL_ATTRS) {
558
+ const value = attrs[key];
559
+ if (value !== undefined)
560
+ credentials[key] = value;
561
+ }
562
+ if (Object.keys(credentials).length > 0)
563
+ protection.credentials = credentials;
564
+ result = protection;
565
+ },
566
+ });
567
+ return result;
568
+ }
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
+ export function applyWorkbookView(view, xml) {
578
+ for (const { attrs } of openElements(xml, 'workbookView')) {
579
+ applyViewNumber(attrs.xWindow, (value) => (view.x = value));
580
+ applyViewNumber(attrs.yWindow, (value) => (view.y = value));
581
+ applyViewNumber(attrs.windowWidth, (value) => (view.width = value));
582
+ applyViewNumber(attrs.windowHeight, (value) => (view.height = value));
583
+ applyViewNumber(attrs.activeTab, (value) => (view.activeTab = value));
584
+ if (attrs.visibility === 'hidden' || attrs.visibility === 'veryHidden') {
585
+ view.visibility = attrs.visibility;
586
+ }
587
+ if (boolStrict(attrs.minimized))
588
+ view.minimized = true;
589
+ return;
590
+ }
591
+ }
592
+ function applyViewNumber(raw, assign) {
593
+ if (raw === undefined)
594
+ return;
595
+ const value = Number(raw);
596
+ if (Number.isFinite(value))
597
+ assign(Math.trunc(value));
598
+ }
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
+ function parseWorkbookDefinedNames(xml, sheetOrder) {
604
+ const names = [];
605
+ let capture = false;
606
+ let refersTo = '';
607
+ let pending;
608
+ parseXml(xml, {
609
+ onOpen(name, attrs) {
610
+ if (localName(name) !== 'definedName' || attrs.name === undefined)
611
+ 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
+ if (attrs.name === '_xlnm._FilterDatabase')
616
+ return;
617
+ capture = true;
618
+ refersTo = '';
619
+ const scopeIndex = attrs.localSheetId === undefined ? -1 : Number(attrs.localSheetId);
620
+ const scope = sheetOrder[scopeIndex];
621
+ pending = { name: attrs.name };
622
+ if (scope !== undefined)
623
+ pending.scope = scope;
624
+ if (attrs.comment !== undefined)
625
+ pending.comment = attrs.comment;
626
+ if (boolStrict(attrs.hidden))
627
+ pending.hidden = true;
628
+ },
629
+ onText(chunk) {
630
+ if (capture)
631
+ refersTo += chunk;
632
+ },
633
+ onClose(name) {
634
+ if (localName(name) !== 'definedName' || pending === undefined)
635
+ 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
+ names.push({ ...pending, refersTo: unmangleFunctions(refersTo) });
639
+ capture = false;
640
+ pending = undefined;
641
+ },
642
+ });
643
+ return names;
644
+ }
645
+ // Core document properties live in docProps/core.xml under mixed namespaces
646
+ // (dc:creator, cp:lastModifiedBy, dcterms:created/modified); local names disambiguate.
647
+ const CORE_PROPERTY_LOCAL_NAMES = new Set(['creator', 'lastModifiedBy', 'created', 'modified']);
648
+ function applyCoreProperties(workbook, xml) {
649
+ let capture = '';
650
+ let text = '';
651
+ parseXml(xml, {
652
+ onOpen(name) {
653
+ const local = localName(name);
654
+ capture = CORE_PROPERTY_LOCAL_NAMES.has(local) ? local : '';
655
+ text = '';
656
+ },
657
+ onText(chunk) {
658
+ if (capture !== '')
659
+ text += chunk;
660
+ },
661
+ onClose(name) {
662
+ if (capture === '' || localName(name) !== capture)
663
+ return;
664
+ if (capture === 'creator')
665
+ workbook.properties.creator = text;
666
+ else if (capture === 'lastModifiedBy')
667
+ workbook.properties.lastModifiedBy = text;
668
+ else {
669
+ const date = new Date(text);
670
+ if (!Number.isNaN(date.getTime())) {
671
+ if (capture === 'created')
672
+ workbook.properties.created = date;
673
+ else
674
+ workbook.properties.modified = date;
675
+ }
676
+ }
677
+ capture = '';
678
+ },
679
+ });
680
+ }