@lotics/xlsx 0.1.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 (90) hide show
  1. package/package.json +30 -0
  2. package/src/auto_sum.test.ts +71 -0
  3. package/src/auto_sum.ts +83 -0
  4. package/src/canvas_cf.ts +135 -0
  5. package/src/canvas_chart.ts +687 -0
  6. package/src/canvas_fill.ts +156 -0
  7. package/src/canvas_images.ts +99 -0
  8. package/src/canvas_layout.test.ts +159 -0
  9. package/src/canvas_layout.ts +239 -0
  10. package/src/canvas_renderer.ts +1010 -0
  11. package/src/canvas_shape.ts +242 -0
  12. package/src/canvas_sparkline.ts +163 -0
  13. package/src/canvas_text.ts +454 -0
  14. package/src/cell_editor.ts +558 -0
  15. package/src/clipboard.test.ts +145 -0
  16. package/src/clipboard.ts +341 -0
  17. package/src/command_history.ts +102 -0
  18. package/src/csv_parser.test.ts +130 -0
  19. package/src/csv_parser.ts +172 -0
  20. package/src/data_validation.test.ts +95 -0
  21. package/src/data_validation.ts +114 -0
  22. package/src/editing_layer.test.ts +309 -0
  23. package/src/excel_cf_evaluate.test.ts +293 -0
  24. package/src/excel_cf_evaluate.ts +719 -0
  25. package/src/excel_cf_icons.ts +108 -0
  26. package/src/excel_cf_types.ts +67 -0
  27. package/src/excel_numfmt.test.ts +607 -0
  28. package/src/excel_numfmt.ts +1033 -0
  29. package/src/excel_parser.test.ts +1061 -0
  30. package/src/excel_parser.ts +393 -0
  31. package/src/excel_pattern_fills.ts +64 -0
  32. package/src/excel_table_styles.ts +160 -0
  33. package/src/excel_utils.test.ts +108 -0
  34. package/src/excel_utils.ts +162 -0
  35. package/src/fast-formula-parser.d.ts +53 -0
  36. package/src/fill_logic.ts +257 -0
  37. package/src/fill_patterns.test.ts +90 -0
  38. package/src/fill_patterns.ts +71 -0
  39. package/src/flash_fill.test.ts +75 -0
  40. package/src/flash_fill.ts +221 -0
  41. package/src/formula_deps.ts +189 -0
  42. package/src/formula_engine.test.ts +348 -0
  43. package/src/formula_engine.ts +401 -0
  44. package/src/formula_highlight.test.ts +81 -0
  45. package/src/formula_highlight.ts +139 -0
  46. package/src/formula_suggest.ts +100 -0
  47. package/src/hidden_sheets.test.ts +49 -0
  48. package/src/index.ts +149 -0
  49. package/src/keyboard_nav.test.ts +394 -0
  50. package/src/keyboard_nav.ts +891 -0
  51. package/src/move_logic.ts +63 -0
  52. package/src/numfmt_type.test.ts +158 -0
  53. package/src/numfmt_type.ts +231 -0
  54. package/src/ooxml_cell_format.ts +70 -0
  55. package/src/ooxml_chart.test.ts +85 -0
  56. package/src/ooxml_chart.ts +347 -0
  57. package/src/ooxml_chart_types.ts +207 -0
  58. package/src/ooxml_color.ts +339 -0
  59. package/src/ooxml_drawing.test.ts +87 -0
  60. package/src/ooxml_drawing.ts +287 -0
  61. package/src/ooxml_pivot.test.ts +195 -0
  62. package/src/ooxml_pivot.ts +468 -0
  63. package/src/ooxml_pivot_types.ts +165 -0
  64. package/src/ooxml_shape.ts +355 -0
  65. package/src/ooxml_sheet.ts +1271 -0
  66. package/src/ooxml_styles.ts +556 -0
  67. package/src/ooxml_table.test.ts +70 -0
  68. package/src/ooxml_table.ts +131 -0
  69. package/src/ooxml_workbook.test.ts +40 -0
  70. package/src/ooxml_workbook.ts +259 -0
  71. package/src/ooxml_zip.ts +33 -0
  72. package/src/pivot_model.ts +237 -0
  73. package/src/pivot_recompute.test.ts +210 -0
  74. package/src/pivot_recompute.ts +413 -0
  75. package/src/selection_model.ts +364 -0
  76. package/src/spreadsheet_commands.test.ts +772 -0
  77. package/src/spreadsheet_commands.ts +1805 -0
  78. package/src/structured_refs.test.ts +128 -0
  79. package/src/structured_refs.ts +160 -0
  80. package/src/style_helpers.test.ts +33 -0
  81. package/src/style_helpers.ts +30 -0
  82. package/src/template_builder.test.ts +139 -0
  83. package/src/template_builder.ts +36 -0
  84. package/src/types.ts +239 -0
  85. package/src/workbook_model.test.ts +536 -0
  86. package/src/workbook_model.ts +911 -0
  87. package/src/xlsx_round_trip.test.ts +279 -0
  88. package/src/xlsx_writer.test.ts +488 -0
  89. package/src/xlsx_writer.ts +1549 -0
  90. package/src/xml_entities.ts +23 -0
@@ -0,0 +1,287 @@
1
+ // =============================================================================
2
+ // OOXML Drawing Parser
3
+ // =============================================================================
4
+ //
5
+ // Parses xl/drawings/drawingN.xml files to extract:
6
+ // - Two-cell anchors with EMU offsets
7
+ // - Chart references (rId → chart path)
8
+ // - Shape/textbox/connector objects
9
+ // - Image references
10
+ // =============================================================================
11
+
12
+ import type { DrawingAnchor, ParsedDrawing } from "./ooxml_chart_types";
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ export interface DrawingRelation {
19
+ type: "chart" | "image" | "shape";
20
+ anchor: DrawingAnchor;
21
+ rId?: string;
22
+ drawing?: ParsedDrawing;
23
+ }
24
+
25
+ // =============================================================================
26
+ // Parser
27
+ // =============================================================================
28
+
29
+ /**
30
+ * Parse a drawing XML file and extract all anchor objects.
31
+ */
32
+ export function parseDrawingXml(
33
+ xml: string,
34
+ parseXml: (xml: string) => Record<string, unknown>,
35
+ ): DrawingRelation[] {
36
+ const doc = parseXml(xml);
37
+ const wsDrawing = doc["xdr:wsDr"] ?? doc["wsDr"] ?? doc;
38
+ if (!wsDrawing || typeof wsDrawing !== "object") return [];
39
+
40
+ const results: DrawingRelation[] = [];
41
+ const obj = wsDrawing as Record<string, unknown>;
42
+
43
+ // Two-cell anchors
44
+ const twoCellAnchors = ensureArray(obj["xdr:twoCellAnchor"] ?? obj["twoCellAnchor"]);
45
+ for (const anchorNode of twoCellAnchors) {
46
+ if (!anchorNode || typeof anchorNode !== "object") continue;
47
+ const anchor = parseTwoCellAnchor(anchorNode as Record<string, unknown>);
48
+ if (!anchor) continue;
49
+
50
+ const a = anchorNode as Record<string, unknown>;
51
+
52
+ // Check for chart frame
53
+ const graphicFrame = a["xdr:graphicFrame"] ?? a["graphicFrame"];
54
+ if (graphicFrame && typeof graphicFrame === "object") {
55
+ const rId = extractChartRId(graphicFrame as Record<string, unknown>);
56
+ if (rId) {
57
+ results.push({ type: "chart", anchor, rId });
58
+ continue;
59
+ }
60
+ }
61
+
62
+ // Check for shape
63
+ const sp = a["xdr:sp"] ?? a["sp"];
64
+ if (sp && typeof sp === "object") {
65
+ const drawing = parseShapeNode(sp as Record<string, unknown>, anchor);
66
+ results.push({ type: "shape", anchor, drawing });
67
+ continue;
68
+ }
69
+
70
+ // Check for picture (image)
71
+ const pic = a["xdr:pic"] ?? a["pic"];
72
+ if (pic && typeof pic === "object") {
73
+ const rId = extractPicRId(pic as Record<string, unknown>);
74
+ if (rId) {
75
+ results.push({ type: "image", anchor, rId });
76
+ }
77
+ continue;
78
+ }
79
+
80
+ // Check for connection shape (connector)
81
+ const cxnSp = a["xdr:cxnSp"] ?? a["cxnSp"];
82
+ if (cxnSp && typeof cxnSp === "object") {
83
+ const drawing = parseConnectorNode(cxnSp as Record<string, unknown>, anchor);
84
+ results.push({ type: "shape", anchor, drawing });
85
+ }
86
+ }
87
+
88
+ // One-cell anchors (typically for images)
89
+ const oneCellAnchors = ensureArray(obj["xdr:oneCellAnchor"] ?? obj["oneCellAnchor"]);
90
+ for (const anchorNode of oneCellAnchors) {
91
+ if (!anchorNode || typeof anchorNode !== "object") continue;
92
+ const a = anchorNode as Record<string, unknown>;
93
+
94
+ const from = parseAnchorPoint(a, "xdr:from", "from");
95
+ if (!from) continue;
96
+
97
+ // Use extent for width/height
98
+ const anchor: DrawingAnchor = {
99
+ from,
100
+ to: { row: from.row + 5, col: from.col + 3 }, // approximate
101
+ };
102
+
103
+ const pic = a["xdr:pic"] ?? a["pic"];
104
+ if (pic && typeof pic === "object") {
105
+ const rId = extractPicRId(pic as Record<string, unknown>);
106
+ if (rId) {
107
+ results.push({ type: "image", anchor, rId });
108
+ }
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ // =============================================================================
116
+ // Anchor parsing
117
+ // =============================================================================
118
+
119
+ function parseTwoCellAnchor(node: Record<string, unknown>): DrawingAnchor | undefined {
120
+ const from = parseAnchorPoint(node, "xdr:from", "from");
121
+ const to = parseAnchorPoint(node, "xdr:to", "to");
122
+ if (!from || !to) return undefined;
123
+ return { from, to };
124
+ }
125
+
126
+ function parseAnchorPoint(
127
+ node: Record<string, unknown>,
128
+ key1: string,
129
+ key2: string,
130
+ ): DrawingAnchor["from"] | undefined {
131
+ const pt = node[key1] ?? node[key2];
132
+ if (!pt || typeof pt !== "object") return undefined;
133
+ const o = pt as Record<string, unknown>;
134
+
135
+ const col = parseIntVal(o["xdr:col"] ?? o["col"]);
136
+ const row = parseIntVal(o["xdr:row"] ?? o["row"]);
137
+ const colOffset = parseIntVal(o["xdr:colOff"] ?? o["colOff"]);
138
+ const rowOffset = parseIntVal(o["xdr:rowOff"] ?? o["rowOff"]);
139
+
140
+ if (col === undefined || row === undefined) return undefined;
141
+ return { row, col, rowOffset, colOffset };
142
+ }
143
+
144
+ // =============================================================================
145
+ // Shape parsing
146
+ // =============================================================================
147
+
148
+ function parseShapeNode(sp: Record<string, unknown>, anchor: DrawingAnchor): ParsedDrawing {
149
+ const spPr = sp["xdr:spPr"] ?? sp["spPr"] ?? {};
150
+ const txBody = sp["xdr:txBody"] ?? sp["txBody"];
151
+
152
+ // Geometry
153
+ const prstGeom = (spPr as Record<string, unknown>)["a:prstGeom"]
154
+ ?? (spPr as Record<string, unknown>)["prstGeom"];
155
+ const geometry = prstGeom && typeof prstGeom === "object"
156
+ ? ((prstGeom as Record<string, unknown>)["@_prst"] as string | undefined) ?? "rect"
157
+ : "rect";
158
+
159
+ // Fill
160
+ const fillColor = extractFillColor(spPr as Record<string, unknown>);
161
+ const outlineColor = extractOutlineColor(spPr as Record<string, unknown>);
162
+
163
+ // Text
164
+ const text = extractTextContent(txBody);
165
+
166
+ const isTextBox = text !== undefined && geometry === "rect";
167
+
168
+ return {
169
+ type: isTextBox ? "textbox" : "shape",
170
+ geometry,
171
+ text,
172
+ fillColor,
173
+ outlineColor,
174
+ anchor,
175
+ };
176
+ }
177
+
178
+ function parseConnectorNode(cxnSp: Record<string, unknown>, anchor: DrawingAnchor): ParsedDrawing {
179
+ const spPr = cxnSp["xdr:spPr"] ?? cxnSp["spPr"] ?? {};
180
+ const outlineColor = extractOutlineColor(spPr as Record<string, unknown>);
181
+
182
+ return {
183
+ type: "connector",
184
+ geometry: "line",
185
+ outlineColor,
186
+ anchor,
187
+ };
188
+ }
189
+
190
+ // =============================================================================
191
+ // Color extraction
192
+ // =============================================================================
193
+
194
+ function extractFillColor(spPr: Record<string, unknown>): string | undefined {
195
+ const solidFill = spPr["a:solidFill"] ?? spPr["solidFill"];
196
+ if (solidFill && typeof solidFill === "object") {
197
+ return extractSrgb(solidFill as Record<string, unknown>);
198
+ }
199
+ return undefined;
200
+ }
201
+
202
+ function extractOutlineColor(spPr: Record<string, unknown>): string | undefined {
203
+ const ln = spPr["a:ln"] ?? spPr["ln"];
204
+ if (!ln || typeof ln !== "object") return undefined;
205
+ const solidFill = (ln as Record<string, unknown>)["a:solidFill"]
206
+ ?? (ln as Record<string, unknown>)["solidFill"];
207
+ if (solidFill && typeof solidFill === "object") {
208
+ return extractSrgb(solidFill as Record<string, unknown>);
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ function extractSrgb(fill: Record<string, unknown>): string | undefined {
214
+ const srgb = fill["a:srgbClr"] ?? fill["srgbClr"];
215
+ if (srgb && typeof srgb === "object") {
216
+ const val = (srgb as Record<string, unknown>)["@_val"];
217
+ if (typeof val === "string") return `#${val}`;
218
+ }
219
+ return undefined;
220
+ }
221
+
222
+ // =============================================================================
223
+ // Text extraction
224
+ // =============================================================================
225
+
226
+ function extractTextContent(txBody: unknown): string | undefined {
227
+ if (!txBody || typeof txBody !== "object") return undefined;
228
+ const paras = ensureArray((txBody as Record<string, unknown>)["a:p"] ?? (txBody as Record<string, unknown>)["p"]);
229
+ const texts: string[] = [];
230
+ for (const para of paras) {
231
+ if (!para || typeof para !== "object") continue;
232
+ const runs = ensureArray((para as Record<string, unknown>)["a:r"] ?? (para as Record<string, unknown>)["r"]);
233
+ for (const run of runs) {
234
+ if (!run || typeof run !== "object") continue;
235
+ const t = (run as Record<string, unknown>)["a:t"] ?? (run as Record<string, unknown>)["t"];
236
+ if (typeof t === "string") texts.push(t);
237
+ }
238
+ }
239
+ return texts.length > 0 ? texts.join("") : undefined;
240
+ }
241
+
242
+ // =============================================================================
243
+ // Relationship extraction
244
+ // =============================================================================
245
+
246
+ function extractChartRId(graphicFrame: Record<string, unknown>): string | undefined {
247
+ const graphic = graphicFrame["a:graphic"] ?? graphicFrame["graphic"];
248
+ if (!graphic || typeof graphic !== "object") return undefined;
249
+ const graphicData = (graphic as Record<string, unknown>)["a:graphicData"]
250
+ ?? (graphic as Record<string, unknown>)["graphicData"];
251
+ if (!graphicData || typeof graphicData !== "object") return undefined;
252
+ const chartRef = (graphicData as Record<string, unknown>)["c:chart"]
253
+ ?? (graphicData as Record<string, unknown>)["chart"];
254
+ if (!chartRef || typeof chartRef !== "object") return undefined;
255
+ const rId = (chartRef as Record<string, unknown>)["@_r:id"]
256
+ ?? (chartRef as Record<string, unknown>)["@_id"];
257
+ return typeof rId === "string" ? rId : undefined;
258
+ }
259
+
260
+ function extractPicRId(pic: Record<string, unknown>): string | undefined {
261
+ const blipFill = pic["xdr:blipFill"] ?? pic["blipFill"];
262
+ if (!blipFill || typeof blipFill !== "object") return undefined;
263
+ const blip = (blipFill as Record<string, unknown>)["a:blip"]
264
+ ?? (blipFill as Record<string, unknown>)["blip"];
265
+ if (!blip || typeof blip !== "object") return undefined;
266
+ const rId = (blip as Record<string, unknown>)["@_r:embed"]
267
+ ?? (blip as Record<string, unknown>)["@_embed"];
268
+ return typeof rId === "string" ? rId : undefined;
269
+ }
270
+
271
+ // =============================================================================
272
+ // Helpers
273
+ // =============================================================================
274
+
275
+ function ensureArray(val: unknown): unknown[] {
276
+ if (val === undefined || val === null) return [];
277
+ return Array.isArray(val) ? val : [val];
278
+ }
279
+
280
+ function parseIntVal(val: unknown): number | undefined {
281
+ if (typeof val === "number") return val;
282
+ if (typeof val === "string") {
283
+ const n = parseInt(val, 10);
284
+ return isNaN(n) ? undefined : n;
285
+ }
286
+ return undefined;
287
+ }
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { XMLParser } from "fast-xml-parser";
3
+ import {
4
+ parsePivotTableXml,
5
+ parsePivotCacheDefinitionXml,
6
+ extractPivotTableRIds,
7
+ extractWorkbookPivotCaches,
8
+ } from "./ooxml_pivot";
9
+
10
+ const parser = new XMLParser({
11
+ ignoreAttributes: false,
12
+ attributeNamePrefix: "@_",
13
+ isArray: (tagName) =>
14
+ tagName === "Relationship" ||
15
+ tagName === "pivotField" ||
16
+ tagName === "cacheField" ||
17
+ tagName === "field" ||
18
+ tagName === "pageField" ||
19
+ tagName === "dataField" ||
20
+ tagName === "pivotCache",
21
+ });
22
+ const parseXml = (xml: string) => parser.parse(xml) as Record<string, unknown>;
23
+
24
+ // =============================================================================
25
+ // PivotTable
26
+ // =============================================================================
27
+
28
+ const PIVOT_TABLE_XML = `<?xml version="1.0"?>
29
+ <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
30
+ name="SalesByRegion" cacheId="1" rowGrandTotals="1" colGrandTotals="1">
31
+ <location ref="A3:E12" firstHeaderRow="1" firstDataCol="1" firstDataRow="2"/>
32
+ <pivotFields count="3">
33
+ <pivotField axis="axisRow" defaultSubtotal="1" sumSubtotal="1"/>
34
+ <pivotField axis="axisCol" defaultSubtotal="1"/>
35
+ <pivotField dataField="1" showAll="0"/>
36
+ </pivotFields>
37
+ <rowFields count="1">
38
+ <field x="0"/>
39
+ </rowFields>
40
+ <colFields count="1">
41
+ <field x="1"/>
42
+ </colFields>
43
+ <dataFields count="1">
44
+ <dataField name="Sum of Revenue" fld="2" subtotal="sum"/>
45
+ </dataFields>
46
+ <pivotTableStyleInfo name="PivotStyleLight16" showRowStripes="1"/>
47
+ </pivotTableDefinition>`;
48
+
49
+ describe("parsePivotTableXml", () => {
50
+ it("extracts the basic shape of a pivot table", () => {
51
+ const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
52
+ expect(pivot.name).toBe("SalesByRegion");
53
+ expect(pivot.cacheId).toBe(1);
54
+ expect(pivot.ref).toBe("A3:E12");
55
+ expect(pivot.firstHeaderRow).toBe(1);
56
+ expect(pivot.firstDataCol).toBe(1);
57
+ expect(pivot.firstDataRow).toBe(2);
58
+ });
59
+
60
+ it("classifies fields onto axes", () => {
61
+ const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
62
+ expect(pivot.fields).toHaveLength(3);
63
+ expect(pivot.fields[0].axis).toBe("row");
64
+ expect(pivot.fields[1].axis).toBe("column");
65
+ // Field 2 is referenced by the dataField but doesn't carry an axis attr.
66
+ expect(pivot.fields[2].axis).toBeUndefined();
67
+ expect(pivot.fields[0].subtotalFunction).toBe("sum");
68
+ });
69
+
70
+ it("captures the row/col field ordering", () => {
71
+ const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
72
+ expect(pivot.rowFieldIndices).toEqual([0]);
73
+ expect(pivot.colFieldIndices).toEqual([1]);
74
+ expect(pivot.pageFieldIndices).toEqual([]);
75
+ });
76
+
77
+ it("parses the values area", () => {
78
+ const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
79
+ expect(pivot.dataFields).toEqual([
80
+ { name: "Sum of Revenue", fieldIndex: 2, subtotal: "sum", numFmt: undefined },
81
+ ]);
82
+ });
83
+
84
+ it("captures display flags and style", () => {
85
+ const pivot = parsePivotTableXml(PIVOT_TABLE_XML, parseXml);
86
+ expect(pivot.styleName).toBe("PivotStyleLight16");
87
+ expect(pivot.display.rowGrandTotals).toBe(true);
88
+ expect(pivot.display.colGrandTotals).toBe(true);
89
+ expect(pivot.display.showRowStripes).toBe(true);
90
+ });
91
+ });
92
+
93
+ // =============================================================================
94
+ // PivotCache
95
+ // =============================================================================
96
+
97
+ const PIVOT_CACHE_XML = `<?xml version="1.0"?>
98
+ <pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
99
+ refreshOnLoad="1" recordCount="4">
100
+ <cacheSource type="worksheet">
101
+ <worksheetSource sheet="Data" ref="A1:C5"/>
102
+ </cacheSource>
103
+ <cacheFields count="3">
104
+ <cacheField name="Region">
105
+ <sharedItems>
106
+ <s v="North"/>
107
+ <s v="South"/>
108
+ </sharedItems>
109
+ </cacheField>
110
+ <cacheField name="Quarter">
111
+ <sharedItems>
112
+ <s v="Q1"/>
113
+ <s v="Q2"/>
114
+ </sharedItems>
115
+ </cacheField>
116
+ <cacheField name="Revenue" numFmtId="4">
117
+ <sharedItems containsNumber="1">
118
+ <n v="100"/>
119
+ <n v="200"/>
120
+ <n v="300"/>
121
+ <n v="400"/>
122
+ </sharedItems>
123
+ </cacheField>
124
+ </cacheFields>
125
+ </pivotCacheDefinition>`;
126
+
127
+ describe("parsePivotCacheDefinitionXml", () => {
128
+ it("captures source range and refresh flag", () => {
129
+ const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
130
+ expect(cache.id).toBe(1);
131
+ expect(cache.refreshOnLoad).toBe(true);
132
+ expect(cache.source).toEqual({
133
+ type: "worksheet",
134
+ sheetName: "Data",
135
+ ref: "A1:C5",
136
+ });
137
+ });
138
+
139
+ it("parses string cache items", () => {
140
+ const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
141
+ expect(cache.fields[0].name).toBe("Region");
142
+ expect(cache.fields[0].items).toEqual([
143
+ { kind: "string", value: "North" },
144
+ { kind: "string", value: "South" },
145
+ ]);
146
+ });
147
+
148
+ it("parses numeric cache items and keeps numeric flag", () => {
149
+ const cache = parsePivotCacheDefinitionXml(PIVOT_CACHE_XML, 1, parseXml);
150
+ const revenue = cache.fields[2];
151
+ expect(revenue.containsNumber).toBe(true);
152
+ expect(revenue.items.map((i) => i.kind)).toEqual([
153
+ "number",
154
+ "number",
155
+ "number",
156
+ "number",
157
+ ]);
158
+ expect((revenue.items[0] as { kind: "number"; value: number }).value).toBe(100);
159
+ });
160
+ });
161
+
162
+ // =============================================================================
163
+ // Relationship + workbook helpers
164
+ // =============================================================================
165
+
166
+ const SHEET_RELS_XML = `<?xml version="1.0"?>
167
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
168
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" Target="../pivotTables/pivotTable1.xml"/>
169
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing1.xml"/>
170
+ </Relationships>`;
171
+
172
+ describe("extractPivotTableRIds", () => {
173
+ it("returns only pivot-table relationships", () => {
174
+ const map = extractPivotTableRIds(parseXml(SHEET_RELS_XML));
175
+ expect(map.size).toBe(1);
176
+ expect(map.get("rId1")).toBe("../pivotTables/pivotTable1.xml");
177
+ });
178
+ });
179
+
180
+ const WORKBOOK_XML = `<?xml version="1.0"?>
181
+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
182
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
183
+ <pivotCaches>
184
+ <pivotCache cacheId="1" r:id="rId7"/>
185
+ <pivotCache cacheId="2" r:id="rId8"/>
186
+ </pivotCaches>
187
+ </workbook>`;
188
+
189
+ describe("extractWorkbookPivotCaches", () => {
190
+ it("returns cacheId → rId mapping", () => {
191
+ const map = extractWorkbookPivotCaches(parseXml(WORKBOOK_XML));
192
+ expect(map.get(1)).toBe("rId7");
193
+ expect(map.get(2)).toBe("rId8");
194
+ });
195
+ });